文章目录
EventLoop组件
ChannelHandlerContext
- 保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象
- 即ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext也绑定了对应的pipeline和channel的信息,方便对ChannelHandler进行调用。
- 常用方法 :
- close
- flush
- writeAndFlush:将数据写到ChannelPipeline中当前ChannelHandler的下一个ChannelHandler开始处理(出站)
ChannelOption
- Netty在创建Channel实例后,一般都需要设置 ChannelOption参数。
- ChannelOption参数如下
- ChannelOption.SO_BACKLOG:对应TCP/IP协议listen函数中的backlog参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列里等待处理,backlog参数指定了队列的大小。(?是否可扩容)
- ChannelOption.SO_KEEPALIVE
EventLoopGroup和其实现类NioEventLoopGroup
- EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例。
- EventLoopGroup提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务。在Netty服务器端编程中,我们一般都需要提供两个EventLoopGroup:BossEventLoopGroup和WorkerEventLoopGroup。
- 通常一个服务端口即一个 ServerSocketChannel对应一个Selector和一个EventLoop线程。BossEventLoop负责接收客户端的连接并将SocketChannel交给WorkerEventLoopGroup来进行IO处理。
- BossEventLoopGroup通常是一个单线程的EventLoop,EventLoop维护着一个注册了ServerSocketChannel实例BossEventLoop的Selector实例 BossEventLoop不断轮询Selector将连接事件分离出来。
- 通常是OP_ACCEPT事件,然后将接收到的SocketChannel交给WorkerEventLoopGroup
- WorkerEventLoopGroup会由next选择其中一个EventLoop来将这个SocketChannel注册到其维护的Selector并对其后续的IO事件进行处理。
Unpooled类
- Netty提供了一个专门用来操作缓冲区的工具类
- 常用方法
- copiedBuffer(Charsequence string,Charset charset)
- 在Netty的Buffer中不需要flip读写反转,因为底层维护了readerIndex和writerIndex
Netty应用实例——群聊系统
心跳检测机制
- 当服务器超过3秒没有读时,提示读空闲
- 当服务器超过5秒没有写时,提示写空闲
- 当服务器超过7秒没有读写时,提示读写空闲
pipeline.addLast(new IdleStateHandler(3,5,7, TimeUnit.SECONDS));
pipeline.addLast(new GroupServerHeartbeatHandler());
- IdleStateHandler是netty提供的处理空闲状态的处理器。
- 当一段时间没有读/写时,会发送一个心跳检测包检测是否连接
- 当IdleStateHandler触发后,会传递给管道的下一个handler中去处理。
public class GroupServerHeartbeatHandler extends ChannelInboundHandlerAdapter {
/**
* 事件触发
* @param ctx
* @param evt 事件类型
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent){
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()){
case READER_IDLE:
eventType = "读空闲";
break;
case WRITER_IDLE:
eventType = "写空闲";
break;
case ALL_IDLE:
eventType = "读写空闲";
break;
}
System.out.println(ctx.channel().remoteAddress()+eventType);
System.out.println("服务器做相应处理");
}
}
}
WebSocket长连接
- 改变http协议多次请求约束,实现长连接,服务器可以发送消息给浏览器
- 客户端浏览器和服务端可以互相感应。比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知。
通过101状态码把HTTP协议升级为WebSocket协议。
bootstrap.group(bossGroup,workerGroup) //设置两个线程组
......
.handler(new LoggingHandler(LogLevel.INFO)); //给workerGroup的EventLoop对应的管道设置处理器
public class ServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//加入一个netty提供的 处理Http的编解码器
pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
//以块方式写
pipeline.addLast(new ChunkedWriteHandler());
/**
* 因为http数据在传输过程中是分段的,HttpObjectAggregator可以将分段数据拼接
*/
pipeline.addLast(new HttpObjectAggregator(8192));
//对于web socket,数据是以帧(frame)的形式传递
// WebSocketFrame有六个子类
// 浏览器发送请求时,ws://localhost:7000/xxxx 表示请求的URI
// WebSocketServerProtocolHandler的核心功能是将HTTP协议升级为WebSocket协议,保持长连接
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
//自定义的Handler
pipeline.addLast("MyHttpServerHandler",new HttpServerHandler());
}
}
页面:注意下面的socket要与上面的WebSocketServerProtocolHandler的路径对应
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
var socket;
//判断当前浏览器是否支持web socket编程
if (window.WebSocket){
socket = new WebSocket("ws://localhost:8998/hello");
//相当于channelRead0
socket.onmessage = function (ev) {
var rt = document.getElementById('responseText');
rt.value += "\n"+ev.data;
}
//相当于连接开启
socket.onopen = function (ev) {
var rt = document.getElementById('responseText');
rt.value = "连接开启了";
}
socket.onclose = function (ev) {
var rt = document.getElementById('responseText');
rt.value = "连接关闭了";
}
}else {
alert("WebSocket not supported")
}
function send(message) {
if (!window.WebSocket){
return;
}
// debugger;
socket.send(message);
}
</script>
<form onsubmit="return false">
<label>
<textarea name="message" style="height: 300px;width: 300px"></textarea>
</label>
<input type="button" value="发送" onclick="send(this.form.message.value)">
<label for="responseText">接收消息</label><textarea id="responseText" style="height: 300px;width: 300px" ></textarea>
<input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
</form>
</body>
</html>
Netty编解码器
编码和解码的基本介绍
- 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码
- codec(编码器)的组成部分有两个:decoder和encoder。encoder负责把业务数据转换成字节码数据,decoder负责把字节码数据转换成业务数据。
- Netty自身提供了一些codec
- StingEncoder : 对字符串数据进行编码
- ObjectEncoder:对java对象进行编码
- StingDecoder : 对字符串数据进行解码
- ObjectDecoder:对java对象进行解码
- Netty自身提供的ObjectEncoder可以用来实现POJO对象或各种业务对象的编码和解码,底层使用的仍是java序列化技术,而java序列化技术本身效率就不高,存在如下问题:
- 无法跨语言
- 序列化后体积太大
- 序列化性能太低
- 引出新的解决方案 Google的Protobuf
Protobuf
- 全称Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化。它很适合做数据存储或RPC数据交换格式。
- 以message的方式来管理数据的。
- 支持跨平台,跨语言。
- 高性能,高可靠性
- 使用Protobuf编译器自动生成代码,Protobuf将类的定义使用.proto文件进行描述。
- 然后通过protoc.exe编译器根据.proto自动生成.java文件
入门案例
引入依赖:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.13.0</version>
</dependency>
编写 Student.proto文件
syntax = "proto3";
option java_outer_classname = "StudentPOJO";//生成的外部类名,同时也是文件名
//protobuf 使用message管理数据
message Student{//会在StudentPOJO外部类生成一个内部类Student,它是真正发送到POJO对象
int32 id = 1;//Student类中有一个属性 名字为id,类型为int32(protobuf的类型) 1表示属性序号,不是值
string name = 2;
}
protoc 把.proto编译成StudentPOJO.java
protoc --proto_path=src --java_out=src/main/java src/main/proto/simple.proto
略 (p74-078)
编解码和handler的调用机制
Netty入站和出站机制
ChannelHandler充当了处理入站和出站数据的应用程序逻辑的容器。例如:实现ChannelInboundHandler接口(或ChannelInboundHandlerAdapter),你就可以接收入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。业务逻辑通常写在一个或者多个ChannelInboundHandler中。ChannelOutboundHandler原理一样。
-
ChannelPipeline提供了ChannelHandler链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过Pipeline的一系列ChannelOutboundHandler,并被这些Handler处理,反之则称为入站的。
-
当Netty发送或者接收一个消息的时候,就会发生一次数据转换。入站消息会被解码:从字节转换成另一种格式(比如java对象);如果是出站消息,它会被编码成字节。
-
Netty提供一系列实用的编解码器,他们都实现了ChannelInboundHandler 或者ChannelOutboundHandler接口。在这些类中,channelRead方法已经被重写了。以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
解码器 ByteToMessageDecoder
继承 ChannelInboundHandlerAdapter (ChannelInboundHandlerAdapter 又继承自ChannelHandlerAdapter ,实现了ChannelInboundHandler接口)
-
由于不可能知道远程节点是否一次性发送一个完整的信息,tcp有可能出现粘包拆包的问题,这个类会对入站数据进行缓冲,直到它准备好被处理。
-
举例来说:每次入站从ByteBuf中读取4字节,将其解码成一个int,然后将它添加到下一个List中。当没有更多元素可以被添加到该List中时,它的内容将会被发送给下一个ChannelInboundHandler 。int在被添加到List中时,会被自动装箱为Integer。在调用readInt()方法前必须验证所输入的 ByteBuf是否有足够的数据。
编解码案例:传输数据,以long的形式
流程图
- 不论对服务器和客户端,Decoder都是入站Handler,Encoder都是出站Handler。
自定义编解码器
自定义编码器:
public class MyLongToByteEncoder extends MessageToByteEncoder<Long> {
@Override
protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
System.out.println("MyLongToByteEncoder encode() 被调用");
System.out.println("msg="+msg);
out.writeLong(msg);
}
}
自定义解码器:
public class MyByteToLongDecoder extends ByteToMessageDecoder {
/**
* decode会根据接收的数据,被调用多次,直到确定没有新的元素被添加到list
* 或者是ByteBuf没有更多的可读字节为止。
* 如果List out 不为空,就会将out的内容传递给下一个ChannelInboundHandler处理
* 该处理器的方法也会被调用多次
* @param ctx
* @param in
* @param out
* @throws Exception
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// long 8个字节
System.out.println("MyByteToLongDecoder decode() 被调用");
if (in.readableBytes()>=8){
out.add(in.readLong());
}
}
}
编写ChannelInitializer(往pipeline里加入Handler):
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 加入 Long To Byte (Encoder)
pipeline.addLast(new MyLongToByteEncoder());
// 加入 Decoder
pipeline.addLast(new MyByteToLongDecoder());
// 加入主处理器,实现发送接收数据等
pipeline.addLast(new MyClientHandler());
}
}
结论:
- 不论解码器handler还是编码器handler即接收的消息类型必须与待处理的消息类型一致,否则handler不会被执行。
- 在解码器进行数据解码时,需要判断缓存区ByteBuf的数据是否足够,否则接收到的结果会根期望结果可能不一致。
其它编解码器
解码器 ReplayingDecoder
- ByteToMessageDecoder的子类,对其进行了扩展,不必调用readableBytes方法。参数S指定了用户状态管理的类型,其中Void代表不需要状态管理。
- 局限性:
- 并不是所有的ByteBuf操作都被支持,如果调用了一个不被支持的方法,将会抛出一个UnsupportedOperationException。
- ReplayingDecoder在某些情况下可能稍慢于ByteToMessageDecoder,例如网络缓慢并且消息格式复杂时,消息会被拆成了多个碎片,速度变慢。
可将上面的解码器改写为(去掉了一个判断):
public class MyByteToLongDecoder2 extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
out.add(in.readLong());
}
}
解码器 LineBasedFrameDecoder:这个类在Netty内部也有使用,它使用行尾控制字符(\n或\r\n)作为分隔符来解析数据。
解码器 DelimiterBasedFrameDecoder:使用自定义的特殊字符作为消息的分隔符。
HttpObjectDecoder:一个HTTP数据的解码器
LengthFieldBasedFrameDecoder:通过指定长度来标识整包消息,这样就可以自动的处理粘包和半包消息。
ZlibDecoder、Bzip2Decoder等等。
整合Log4j
- 添入依赖
- 配置Log4j,在resources/log4j.properties
略,需要再搜
TCP 粘包和拆包及解决方案
TCP 粘包和拆包基本介绍
- TCP是面向连接的,面向流的,提供高可靠服务。收发两端都要有一一成对的socket。因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
- 由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题。
- 如图,客户端发送了两个数据包D1和D2,服务器端读取的情况可能如下:
案例:
客户端发送10条数据:
//发送10条数据2
for (int i = 0; i < 10; i++) {
ByteBuf msg = Unpooled.copiedBuffer("hello ,server\n", CharsetUtil.UTF_8);
ctx.writeAndFlush(msg);
}
服务端接收的情况
hello ,server
服务器端接收到消息量 = 1
hello ,server
服务器端接收到消息量 = 2
hello ,server
服务器端接收到消息量 = 3
hello ,server
服务器端接收到消息量 = 4
hello ,server
hello ,server
服务器端接收到消息量 = 5
hello ,server
hello ,server
服务器端接收到消息量 = 6
hello ,server
hello ,server
服务器端接收到消息量 = 7
接收的次数是不确定的。
解决方案
- 使用自定义协议+编解码器来解决
- 整一个自定义的类(规定长度和格式相当于协议)
- 编写编解码器,把自定义协议类 和 字节码之间转换。
- 在initializer里加入编解码器handler
源码
引导类将通过传入的Class对象反射创建ChannelFactory。然后添加一些TCP的参数。
ServerBootstrap是个空构造,但是有默认的成员变量
private static final InternalLogger logger = InternalLoggerFactory.getInstance(ServerBootstrap.class);
private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>();
private final Map<AttributeKey<?>, Object> childAttrs = new LinkedHashMap<AttributeKey<?>, Object>();
private final ServerBootstrapConfig config = new ServerBootstrapConfig(this);
private volatile EventLoopGroup childGroup;
private volatile ChannelHandler childHandler;
还有一部分参数定义在AbstractBootstrap里
public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {
volatile EventLoopGroup group;
@SuppressWarnings("deprecation")
private volatile ChannelFactory<? extends C> channelFactory;
private volatile SocketAddress localAddress;
private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();
private final Map<AttributeKey<?>, Object> attrs = new LinkedHashMap<AttributeKey<?>, Object>();
private volatile ChannelHandler handler;
}
.option()方法会把传入的参数放到options 里。
.bind()方法最终会链接到AbstractBootstrap的doBind()方法
private ChannelFuture doBind(final SocketAddress localAddress) {
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
if (regFuture.isDone()) {
// At this point we know that the registration was complete and successful.
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
// Registration future is almost always fulfilled already, but just in case it's not.
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
// Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
// IllegalStateException once we try to access the EventLoop of the Channel.
promise.setFailure(cause);
} else {
// Registration was successful, so set the correct executor to use.
// See https://github.com/netty/netty/issues/2586
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}
addLast(Handler h)方法:
newContext(),然后addLast(cxt)
源码剖析从P92开始