关于netty相关的基础知识可以点击此链接
知识来源:netty学习视频
协议设计与解析
了解协议
客户端与服务器端进行通信必须遵守一定的协议,也就是需要遵守的规范。就拿Redis的协议为例,redis中协议如下:
比如我现在要执行
set name hushang
就一个简单的存key的操作 redis会将这行命令看成一个数组,数组的内容就是这三个东西发送的格式就是
*数组的长度 换行 $第一个数据的长度 换行 然后再跟内容 回车换行分割...
即上面的的一行命令最终要发送的格式就是
*3\r\n$3\r\nset\r\n$4\r\nname\r\n$7\r\nhushang\r\n
这个时候我们客户端编写的代码就会成如下的样子,是active事件,连接建立成功客户端就发送数据。
netty它提供了很多现成的协议,就不需要我们自己在一步一步的根据协议来拼装数据了。
http协议
http协议很复杂,如果让我们自己去实现还是很复杂了,还好netty已经帮我们把http协议的编码解码都实现好了,我们只需要简单的配置即可使用。
服务器端的代码如下:核心要看的还是添加handler的地方。
package com.hs.nettyIntermediate.mode2;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.internal.SuppressJava6Requirement;
import lombok.extern.slf4j.Slf4j;
/**
* 测试http协议 充当服务端
* @author hs
* @date 2021/07/24
*/
@Slf4j
public class ServerSocketChannel {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try{
new ServerBootstrap()
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 日志信息相关的handler
nioSocketChannel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
// 添加处理http协议编解码的handler
nioSocketChannel.pipeline().addLast(new HttpServerCodec());
// 再添加一个自定义handler 对上面编解码器得到的结果进行处理
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws ...{
// 看一下经过http编解码后传递过来的msg是什么类型
log.debug("----> {}", msg.getClass());
// 最后打印的结果是 有两个
// class io.netty.handler.codec.http.DefaultHttpRequest
// class io.netty.handler.codec.http.LastHttpContent$1
}
});
}
})
.bind(8080)
.sync()
.channel()
.closeFuture()
.sync();
}catch(Exception e){
log.error("server error", e);
}finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
我明明就只是想看一下编解码器处理的msg类型,为什么最后会打印两个嘞。这是因为http的解码器将我们的请求解码成了两部分,第一部分是HttpRequest
它包含请求行和请求头 ;第二部分为HttpContent
它包含请求体。虽然get请求没有请求体,但还是会有该对象,既然该对象存储的数据为null。
所以我们需要对着两个部分进行分别的处理,所以将来的代码就会变为
// 添加处理http协议编解码的handler
nioSocketChannel.pipeline().addLast(new HttpServerCodec());
// 再添加一个自定义handler 对上面编解码器得到的结果进行处理
nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws 。。{
// 看一下经过http编解码后传递过来的msg是什么类型
log.debug("----> {}", msg.getClass());
if (msg instanceof HttpRequest) { // 处理请求行 请求头
} else if (msg instanceof HttpContent) { // 处理请求体
}
}
});
但是这样不太方便,假如我只是get请求,只需要处理请求行和请求头就可以了,我不想在做if判断,就可以对如上的代码进行一个简化
// 添加处理http协议编解码的handler
nioSocketChannel.pipeline().addLast(new HttpServerCodec());
// 它也是一个入站处理器,但它可以只关心某一种类型的消息
// 就比如我现在只关心HttpRequest这个消息 就直接在泛型中添加该类型即可。
nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest){
// 获取请求行
httpRequest.uri();
// 获取请求头
httpRequest.headers();
}
});
以上都是处理请求,请求处理完成后接下里就是处理响应了。netty为我们提供了一个DefaultFullHttpResponse
对象,我们只需要对该对象进行一些操作,然后将该对象通过channel写回,该对象就会经过http解码器,然后再将数据写回。
// 添加处理http协议编解码的handler
nioSocketChannel.pipeline().addLast(new HttpServerCodec());
// 它也是一个入站处理器,但它可以只关心某一种类型的消息
// 就比如我现在只关心HttpRequest这个消息 就直接在泛型中添加该类型即可。
nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpRequest httpRequest){
// 获取请求行
httpRequest.uri();
// 获取请求头
httpRequest.headers();
// 接下来处理响应 创建DefaultFullHttpResponse对象
// 该对象需要两个参数,第一个是http协议的版本,第二个是响应的状态码
DefaultFullHttpResponse response =
new DefaultFullHttpResponse(httpRequest.protocolVersion() , HttpResponseStatus.OK);
// 再添加一点响应数据 需要通过content()方法
response.content().writeBytes("<h1>hello, world<h1>".getBytes());
// 最后就是将该对象通过channel写出
channelHandlerContext.writeAndFlush(response);
}
});
运行服务器,然后浏览器发送请求的结果为:
这里浏览器一直转圈等待的原因是,我们没有告诉浏览器我们的数据长度有多少,浏览器就会之前等待 转圈接收更多的响应内容。
所以还需要在响应头中加一个ContentLength
DefaultFullHttpResponse response =
new DefaultFullHttpResponse(httpRequest.protocolVersion() , HttpResponseStatus.OK);
// 再添加一点响应数据 需要通过content()方法
byte[] bytes = "<h1>hello, world<h1>".getBytes();
response.content().writeBytes(bytes);
// 添加响应头 设置响应数据的长度
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
// 最后就是将该对象通过channel写出
channelHandlerContext.writeAndFlush(response);
自定义协议
- 魔数:用来第一时间判定是否是无效的数据包
- 版本号:支持协议的升级
- 序列化算法:消息正文到底采用那种序列化与反序列化方式,可以由此扩展。例如:json、protobuf、hessian、jdk
- 指令类型:是登录、注册、单聊、群聊。。。跟业务相关
- 请求序号:为了全双工通信,提供异步能力,就是因为异步,就需要一个序号来让响应的数据对应之前哪个请求
- 正文长度
- 正文数据
编解码
接下来是一个聊天室的案例。可以主动发送条聊天信息,也有回应一条信息,我们给所有消息相关的类都创建一个抽象的父类。接下来为我们的消息创建一个编解码器,首先自定义一个类,然后继承ByteToMessageCodec
类,这个类需要传递一个泛型,其实类其实就是让Bytebuf和我们的消息进行转换,我们的消息类型就需要这个要指定的泛型来决定。这里的泛型就是所有消息类的抽象父类。
所有消息都需要继承的一个抽象父类如下
@Data
@AllArgsConstructor
@NoArgsConstructor
public abstract class Message implements Serializable {
// 请求序号 为了双工通信,提供异步功能
private int sequenceId;
// 抽象方法,返回消息的类型,每个消息都要重写该方法,因为子类自己知道它的消息类型到底是登录、或是发送消息等等
public abstract int getMessageType();
// 登录相关的消息类型
public static final int LoginRequestMessage = 0;
public static final int LoginResponseMessage = 1;
// 聊天相关
public static final int ChatRequestMessage = 2;
public static final int ChatResponseMessage = 3;
// 创建聊天室相关
public static final int GroupCreateRequestMessage = 4;
public static final int GroupCreateResponseMessage = 5;
}
编解码器代码如下
package com.hs.nettyIntermediate.mode2.protocol;
import com.hs.nettyIntermediate.mode2.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
/**
* 自定义编解码器
* @author hs
* @date 2021/07/25
*/
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
// 继承ByteToMessageCodec类会重写该类中的两个方法,一个的编码 另一个是解码
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
// 该方法中的参数,netty已经帮我们创建好了一个ByteBuf,我们只需要将我们的消息按照协议规定的格式写入该ByteBuf即可
// 1. 魔数 ,往Bytebuf中添加一个4字节的魔数,可以随便规定一个魔数
byteBuf.writeBytes(new byte[]{1,2,3,4});
// 2. 字节的版本
byteBuf.writeByte(1);
// 3. 序列化算法,我先暂时约定 0 表示用jdk来序列化 , 1 表示用json序列化 …… 就先用一个字节来代表序列化的方式
byteBuf.writeByte(0);
// 4. 指令类型 写入一个字节的指令类型
byteBuf.writeByte(message.getMessageType());
// 5. 请求序号 4个字节 从消息对象中拿
byteBuf.writeInt(message.getSequenceId());
// 除正文以外的字节数加起来是15个字节 不是2的整数倍,所以在加一个字节占位,其实没有实际作用
byteBuf.writeByte(0xff);
// 6. 获取正文,我们现在的Message对象不能直接在网络中传输 需要将Message对象通过序列化将它变为字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(message);
byte[] bytes = bos.toByteArray();
// 7. 正文长度 4个字节
byteBuf.writeInt(bytes.length);
// 8. 正文
byteBuf.writeBytes(bytes);
}
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
// 该方法对应的解码,也就是当消息发送过来后 要按照我们规定的协议格式 解析 最后将正文解析出来
int magicNumber = byteBuf.readInt();
byte version = byteBuf.readByte();
byte serializerArithmetic = byteBuf.readByte();
byte messageType = byteBuf.readByte();
int sequenceId = byteBuf.readInt();
byteBuf.readByte();
int length = byteBuf.readInt();
// 创建一个byte数组存放正文
byte[] bytes = new byte[length];
byteBuf.readBytes(bytes, 0, length);
// 接下来进行反序列化 将字节数组转化为对象,首先应该判断采用哪种方式反序列化
Message message = null;
if (serializerArithmetic == 0){
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
message = (Message) new ObjectInputStream(byteArrayInputStream).readObject();
}
// 最后将解析出来的消息存入该方法的第三个参数list集合中
list.add(message);
log.debug("{}, {}, {}, {}, {}, {}", magicNumber, version, serializerArithmetic,
messageType, sequenceId, length);
log.debug("{} ", message);
}
}
然后进行测试编解码器的测试
首先创建一个要传输的类,就比如登录相关的吧,继承我们上面定义的抽象父类Message
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginRequestMessage extends Message{
private String username;
private String password;
@Override
public int getMessageType() {
// 这里返回的是消息类型 是父类中的静态常量
return LoginRequestMessage;
}
}
然后继续测试编码器。
public class MessageCodecTest {
public static void main(String[] args) {
// 使用EmbeddedChannel来进行测试
EmbeddedChannel channel = new EmbeddedChannel(
new LoggingHandler(),
new MessageCodec()
);
// 首先测试编码器,也是往出站处理器中写数据
channel.writeOutbound(new LoginRequestMessage("zhangsan", "123456"));
}
}
结果为:
接下来测试解码器:
public class MessageCodecTest {
public static void main(String[] args) throws Exception {
// 使用EmbeddedChannel来进行测试
EmbeddedChannel channel = new EmbeddedChannel(
new LoggingHandler(),
new MessageCodec()
);
// 接下来测试解码器,那我们就需要先获取到编码后的字节数组,然后将字节数组往入站处理器中写入
// 1. 先准备一个Bytebuf,因为我们编写的编解码处理器中的encode方法就是将消息存入Bytebuf中
// 所以我们可以直接调用该方法把消息写入到我们的自己创建的Bytebuf中
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null, new LoginRequestMessage("zhangsan", "123456"), buffer);
// 2. 测试解码器 往入站处理器中写数据
channel.writeInbound(buffer);
}
}
结果为:
现在这种情况如果出现了黏包倒还好,因为它是先读取到了长度,然后在读取数据。但如果出现了半包的情况就不能解决了,还会报异常。所以还需要配合LTV解码器LengthFieldBasedFrameDecoder
来一起使用。
接下来最终测试的代码为:
public class MessageCodecTest {
public static void main(String[] args) throws Exception {
// 使用EmbeddedChannel来进行测试
EmbeddedChannel channel = new EmbeddedChannel(
new LengthFieldBasedFrameDecoder(2048, 12, 4, 0, 0), // 帧解码器
new LoggingHandler(), // 日志
new MessageCodec() // 我们自定义的编解码器
);
// 首先测试编码器,也是往出站处理器中写数据
channel.writeOutbound(new LoginRequestMessage("zhangsan", "123456"));
// 接下来测试解码器,那我们就需要先获取到编码后的字节数组,然后将字节数组往入站处理器中写入
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null, new LoginRequestMessage("zhangsan", "123456"), buffer);
channel.writeInbound(buffer);
}
}
注意:writeInbound()方法往入站处理器写入一个ByteBuf后,会调用release()方法,让ByteBuf的引用计数-1
模拟半包
@Sharable
以前,不管我们是创建服务器端ServerBootstrap还是客户端的Bootstrap,在添加并使用Handler时都是创建一个新的Handler实例,那能不能我创建一个Handler,然后被多个Channel使用嘞?
答案是要根据具体情况具体分析,首先需要知道最后做事的都是Eventloop,Handler只是一道工序,就比如LTV解码器,如果这个使用工人1使用它读取了一个半包的数据,接下来工人2又使用它读取了一些数据,那么LTV解码器就有可能将两个数据拼接在一起给工人1用,这是因为两个线程使用的同一个对象。
但是比如打印日志的Handler就没关系,可以多个channel使用,因为它不会保存数据,不会像LTV解码器一样记录上一个没有读取完的数据,它是只要有数据来了它就打印
有这么多的Handler,那我们怎么知道哪些Handler能被多个Channel共享,哪些有不能被共享使用嘞?netty给我们提供的Handler都加了一个标记,这个标记就是告诉我们这个Handler能否是线程安全的。也就是@Sharable注解,比如我们点进日志LoggingHandler
的源码中就会发现
@Sharable
public class LoggingHandler extends ChannelDuplexHandler {
private static final LogLevel DEFAULT_LEVEL;
protected final InternalLogger logger;
protected final InternalLogLevel internalLevel;
private final LogLevel level;
public LoggingHandler() {
this(DEFAULT_LEVEL);
}
……
}
而LTV解码器 LengthFieldBasedFrameDecoder
的类上面却是干干净净的。啥注解都没有。
那么如何判断我们自己定义的Handler能否被多个Channel共享嘞?就判断它有没有在多个事件中共享的数据,也就是哪些状态信息。如果能够确定被共享的话,我们也可以在我们自己的Handler类上面加@Sharable,但是需要注意,我们现在自定义的一个Handler编解码器,虽然可以被共享,但是加了@Sharable注解会启动报错,这是因为它继承的父类ByteToMessageCodec
问题 ,在这个父类中会进行一个判断,它不允许它的子类加@Sharable注解,如果想要自己的Handler能够加上该注解的话就需要换一个父类继承MessageToMessageCodec<>
,这个类需要给两个泛型,之前的那个父类就是将ByteBuf转换为一个消息,我们要给这个消息的类型,而这个类是消息转消息,需要给两个类型。
聊天室实例
业务介绍
需要三个接口
1、第一个接口是一个用户登录接口,用来判断用户是否登录成功
2、第二个接口是一个会话接口,主要功能就是绑定用户名和channel,因为到时候给用户发生消息,实际上是往channel中写。然后还有就是根据用户名获取channel
3、第三个接口就是一个群聊的接口,主要功能是:创建群聊 需要一个群名和多个用户名,加入一个用户,移除一个用户 ,移除群聊,获取所有成员, 获取所有成员的channel
登录
首先是客户端在自定义入站Handler中的active事件中编写客户端登录的业务,因为获取用户的输入会阻塞住EventloopGroup中的线程,所以需要在该方法中新开一个线程获取用户的输入后,创建一个loginRequest对象,将这个对象通过ctx写给服务器端,服务器创建一个SimpleChannelInbountHandler来处理loginrequest对象,判断用户名密码是否正确,然后返回一个loginResponse对象,客户端再在自定义入站Handler重写read事件方法,来处理服务器响应的登录成功或失败的数据
两个线程通信问题:
获取用户的输入,然后发送数据是一个新创建的线程,读取服务器响应的登录是否成功是Eventloop线程,这就需要两个线程的通信问题了。需要CountDownLatch
类 中的方法countDown() 计数器减一 await() 当计数器为0才执行之后的代码
。在客户端向服务器发送完数据后就阻塞住,然后等待服务器的响应,等另一个线程获取数据后在唤醒该线程,该线程在进行判断,如果账号密码错误就关闭channel,如果正确就打印具体的操作界面,等待用户进行下一步操作。
单聊
当用户登录成功后,就在命令行打印操作菜单,等待用户输入命名以及消息接收方姓名,消息正文。也就是创建一个单聊相关的message对象存储数据,然后将数据通过ctx对象发送给服务器。服务器在创建一个handler专门处理单聊,通过消息接收方的用户名获取到channel,然后通过channel将消息发送给对方。当然,这里服务器需要在验证用户登录成功后将用户名和channel对应关系保存。
群聊
群聊的需求有创建群聊、往群聊中发送消息、加入群聊、退出群聊、获取群成员。在服务器这边都创建几个专门处理这几类特有对象的Handler,然后都添加进pipeline,在各自重写的channelRead()方法中写相应的处理逻辑。
创建群聊:发送给服务器一个群名和一个set集合存储的群成员姓名,创建成功后还需要获取所有成员的channel,给它们发送一条已经加入群聊的消息
退出
当客户端正常断开,会在服务器那边触发一个InActive事件,我们可以在自定义入站handler中处理该事件,主要就是将之前保存的用户与channel移除掉。
如果客户端没有通过命令行输入退出的命令,而是直接停止程序的运行,那么服务器这边会出一个异常,我们可以通过重写自定义入站handler中的exceptionCaught()
方法, 这就是一个异常会触发的事件,
空闲检测
在网络编程中容易出现连接假死的情况,原因如下:
- 网络设备出现故障,例如网卡、机房等,这个时候底层的TCP连接已经断开了,但应用程序没有感知,任然占用这资源
- 公网网络不稳定,出现丢包。如果连续出现丢包,这时现象就是客户端数据发不出去,服务端也—直收不到数据,就这么—直耗着
- 应用程序线程阻塞,无法进行数据读写
连接假死就会造成一下几个问题:
- 假死占用的资源不能自动释放
- 向假死的连接发送数据,得到的反馈是发送超时
netty提供了一种检测连接假死的手段——空闲检测器 IdleStateHandler(最大读空闲时间,写空闲时间,读写空闲时间)
其实就是添加这个Handler,它的作用就是用来判断读写时间是否过长。例如
// 这就表示5秒如何没有收到客户端发送过来的数据就会让服务器任务链接假死了。
nioSocketChannel.pipeline().addLast(new IdleStateHandler(5, 0, 0));
如果超过了5秒还没有收到数据就会触发一个IdleState#READER_IDLE
事件,那么谁来处理该事件嘞?还需要我们自定义一个Handler来处理事件,这个Handler应该是一个双向的,应该既能处理入站数据也能处理出站数据。
创建一个nioSocketChannel.pipeline().addLast(new ChannelDuplexHandler())
它就是一个双向的handle,该handler需要监听IdleState#READER_IDLE
事件,这是一个自定义事件,不能像以前一样直接重写channelRed() \ channelActive()…方法。而是应该要重写userEventTriggered()
方法,该方法专门处理一些特殊的事件 。userEventTriggered()
方法的参数2为事件消息类型,从IdleStateHandler类的源码中的注释可以知道,READER_IDLE事件的类型为IdleStateEvent
先进行强转,然后使用if判断是否触发读空闲事件
我们可以在这里当读空闲事件发生时就将这个channel给释放掉ctx.channel().close();
客户端发送心跳包
空闲监测主要就是处理客户端因为一些网络原因断开连接了,但是服务器这边的资源还没有释放,进而释放资源。可以如果客户端还是正常连接,只是有一段时间没有进行操作而已,这个时候就不该将channel给释放。我们客户端应该隔一段时间就向服务器发送一个心跳包。
其实就是在客户端加一个写空闲监测,如果一段时间没有向服务器发送数据就自动发送一个消息,也就是心跳包。只是改变了IdleStateHandler(0, 3, 0)
这和if判断两个地方
优化
扩展序列化算法
在我们之前的学习 自定义协议的时候,我们知道协议一般分为两个部分,协议头和消息正文,协议头就是魔数+版本号+序列化算法+请求类型等等。我们需要将消息对象转换为字节数组,然后进行传输,接收方再将字节数组转换回对象。之前所使用的是jdk提供的序列化与反序列化算法。
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(message);
byte[] bytes = bos.toByteArray();
// 反序列化 length是解析协议头部分 消息长度
byte[] bytes = new byte[length];
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Message message = (Message) new ObjectInputStream(byteArrayInputStream).readObject();
这种方式虽然简单,但是效率不高,我们可以进行一个重构,创建一个接口专门来存储序列化算法,这里其实就是先将之前的序列化与反序列化的代码抽取出来了。
public interface MySerializer {
// 序列化算法 将一个对象转换为字节数组
<T> byte[] serializer(T object);
// 反序列化算法 将一个字节数组转换为对象,该方法的参数1就是指定字节数组转换为什么类型的对象
<T> T dSerializer(Class<T> clazz, byte[] bytes);
// 定义一个枚举,因为该枚举实现了这个接口,所以枚举内的每个元素就相当于是实现这个接口的类
enum Algorithm implements MySerializer{
java{
@Override
public <T> byte[] serializer(T object) {
ByteArrayOutputStream byteArrayOutputStream = null;
try {
byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
} catch (IOException e) {
e.printStackTrace();
}
return byteArrayOutputStream.toByteArray();
}
@Override
public <T> T dSerializer(Class<T> clazz, byte[] bytes) {
T obj = null;
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
obj = (T)objectInputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
}
}
}
这个时候编解码器的代码如下:
package com.hs.nettyIntermediate.mode2.protocol;
import com.hs.nettyIntermediate.mode2.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;
/**
* 自定义编解码器,使用我们扩展的序列化算法
* @author hs
* @date 2021/07/25
*/
@Slf4j
public class MessageCodec2 extends ByteToMessageCodec<Message> {
// 继承ByteToMessageCodec类会重写该类中的两个方法,一个的编码 另一个是解码
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
// 该方法中的参数,netty已经帮我们创建好了一个ByteBuf,我们只需要将我们的消息按照协议规定的格式写入该ByteBuf即可
// 1. 魔数 ,往Bytebuf中添加一个4字节的魔数,可以随便规定一个魔数
byteBuf.writeBytes(new byte[]{1,2,3,4});
// 2. 字节的版本
byteBuf.writeByte(1);
// 3. 序列化算法,我先暂时约定 0 表示用jdk来序列化 , 1 表示用json序列化 …… 就先用一个字节来代表序列化的方式
byteBuf.writeByte(0);
// 4. 指令类型 写入一个字节的指令类型
byteBuf.writeByte(message.getMessageType());
// 5. 请求序号 4个字节 从消息对象中拿
byteBuf.writeInt(message.getSequenceId());
// 除正文以外的字节数加起来是15个字节 不是2的整数倍,所以在加一个字节占位,其实没有实际作用
byteBuf.writeByte(0xff);
// 6. 进行序列化--------------------------------------------------------
byte[] bytes = MySerializer.Algorithm.java.serializer(message);
// 7. 正文长度 4个字节
byteBuf.writeInt(bytes.length);
// 8. 正文
byteBuf.writeBytes(bytes);
}
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
// 该方法对应的解码,也就是当消息发送过来后 要按照我们规定的协议格式 解析 最后将正文解析出来
int magicNumber = byteBuf.readInt();
byte version = byteBuf.readByte();
byte serializerArithmetic = byteBuf.readByte();
byte messageType = byteBuf.readByte();
int sequenceId = byteBuf.readInt();
byteBuf.readByte();
int length = byteBuf.readInt();
// 创建一个byte数组存放正文
byte[] bytes = new byte[length];
byteBuf.readBytes(bytes, 0, length);
// 接下来进行反序列化 -----------------------------------------
Message message = MySerializer.Algorithm.java.dSerializer(Message.class, bytes);
// 最后将解析出来的消息存入该方法的第三个参数list集合中
list.add(message);
log.debug("{}, {}, {}, {}, {}, {}", magicNumber, version, serializerArithmetic, messageType, sequenceId, length);
log.debug("{} ", message);
}
}
接下来再添加一个json的序列化与反序列化
package com.hs.nettyIntermediate.mode2.protocol;
import com.google.gson.Gson;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* @author hs
* @date 2021/07/31
*/
public interface MySerializer {
// 序列化算法 将一个对象转换为字节数组
<T> byte[] serializer(T object);
// 反序列化算法 将一个字节数组转换为对象,该方法的参数1就是指定字节数组转换为什么类型的对象
<T> T dSerializer(Class<T> clazz, byte[] bytes);
// 定义一个枚举,因为该枚举实现了这个接口,所以枚举内的每个元素就相当于是实现这个接口的类
enum Algorithm implements MySerializer{
java{
@Override
public <T> byte[] serializer(T object) {
ByteArrayOutputStream byteArrayOutputStream = null;
try {
byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
} catch (IOException e) {
e.printStackTrace();
}
return byteArrayOutputStream.toByteArray();
}
@Override
public <T> T dSerializer(Class<T> clazz, byte[] bytes) {
T obj = null;
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
obj = (T)objectInputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
},
json{
// json 有很多解析包,这里是使用的谷歌的gson
@Override
public <T> byte[] serializer(T object) {
String str = new Gson().toJson(object);
return str.getBytes(StandardCharsets.UTF_8);
}
@Override
public <T> T dSerializer(Class<T> clazz, byte[] bytes) {
String str = new String(bytes, StandardCharsets.UTF_8);
return new Gson().fromJson(str, clazz);
}
}
}
}
这个时候,编解码器如何选择使用哪种序列化与反序列化算法嘞,我们不想在程序中写死就可以采用配置文件的方式。
首先创建给application.properties配置文件
serializer.algorithm=json
然后创建一个读取配置文件的类
package com.hs.nettyIntermediate.mode1.config;
import com.hs.nettyIntermediate.mode2.protocol.MySerializer;
import java.io.InputStream;
import java.util.Properties;
/**
* 将选择序列化算法 提取为配置文件
* @author hs
* @date 2021/07/31
*/
public class MyCofig {
// 读取配置文件
static Properties properties;
static {
try(InputStream is = MyCofig.class.getResourceAsStream("/application.properties")){
properties = new Properties();
properties.load(is);
}catch (Exception e){
e.printStackTrace();
}
}
// 返回接口中的枚举对象
public MySerializer.Algorithm getAlgorithm(){
String algorithm = properties.getProperty("serializer.algorithm");
// 如果配置文件中没有就需要给一个默认的值
if (algorithm == null){
return MySerializer.Algorithm.java;
}else{
return MySerializer.Algorithm.valueOf(algorithm);
}
}
}
接着再对编解码器进行优化,在编码器设置序列化算法的那一个字节为 改为
// 3. 序列化算法,从配置文件中获取该枚举对象 然后使用ordinal()获取这是第几个 是从0开始计数的
byteBuf.writeByte(new MyConfig().getAlgorithm().ordinal());
...
// 6. 获取正文,我们现在的Message对象不能直接在网络中传输 需要将Message对象通过序列化将它变为字节数组
byte[] bytes = new MyConfig().getAlgorithm().serializer(message);
解码器部分:
// 接下来进行反序列化 将字节数组转化为对象,通过配置文件来获取序列化方式
// 通过上面协议头 获取的序列化算法字节,从接口中的枚举获取对应的序列化对象
MySerializer.Algorithm algorithm = MySerializer.Algorithm.values()[serializerArithmetic];
// 反序列化时,这里不能直接传递一个父类型,需要确定具体的消息类型,
// 这里需要在父类中定义所有的子类消息类型与对应的数字,然后通过协议头部分的消息类型 也就是一个数字获取对应的类型
Message message = algorithm.dSerializer(Message.getMessageClass(messageType), bytes);
总结:就是将编解码器中的序列化与反序列化抽取出来,创建一个接口,接口中就两个方法,然后创建一个枚举内部类,实现该接口,枚举中的属性就是进行某个具体的序列化与反序列化。
然后就是创建一个配置文件,让用户指定序列化方式,还需要创建一个类用来读取配置文件,并提供一个方法返回枚举的成员。最后就是在编解码器类中修改指定序列化的一个字节,以及进行序列化与反序列化。反序列化的时候不能直接指定父类的类型,需要指定具体的字节数组转为什么类型,这里在编码时指定了一个字节的消息类型,在解码的时候就需要利用这个消息类型(数字)获取到具体的类型,在进行解码操作。
参数
如果是客户端,new Bootstrap().option()
方法中可以配置这些参数,这个方法是给SocketChannel配置参数
如果是服务器端,要稍微复杂一些。 服务器端有两个方法
new ServerBootstrap().option()
这个方法是给ServerSocketChannel配置的参数new ServerBootstrap().childoption()
这个方法是给SocketChannel配置的参数
所有可以配置的参数都写在了一个ChannelOption
类中
CONNECT_TIMEOUT_MILLIS
控制客户端在建立连接时的超时时间,如果客户端在指定的时间内没有连接成功就会抛出一个异常。具体配置该参数的代码如下:
public class Client {
public static void main(String[] args) throws InterruptedException {
new Bootstrap()
.group(new NioEventLoopGroup())
// 添加参数,连接超时参数,如果2秒还没有和服务器建立连接就会报异常
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new LoggingHandler());
}
})
.connect("localhost", 8080)
.sync()
.channel()
.closeFuture()
.sync();
}
}
需要小小注意一点,就算我这里的参数设置了5秒连接超时时间,但最后连接的时候如果2秒没有连接成功还是会报一个java.net中的一个异常,这是这里不报netty中的异常了。
SO_TIMEOUT
这个参数也是一个超时时间,只不过它是用在阻塞io环境中的,我们阻塞io中accept()和read()默认都是无限等待的,如果不想它永远阻塞就可以使用该参数调整超时时间。
我们知道主线程调用了connect()方法,该方法是异步的,由Eventloop线程去执行具体的连接,主线程继续执行到sync()就阻塞住了。这个连超时的实现其实就是使用了Eventloop的一个定时任务,如果到我们指定的事件还没有连接成功就会执行定时任务,如果连接成功了就会取消定时任务的执行,这个定时任务所做的事情就是创建一个异常对象,然后利用promise中的tryFailure()方法将创建的异常对象存入promise中,然后唤醒阻塞的主线程,主线程这边得到了结果,发现是一个异常,主线程也就停了,如果不是异常就会继续往下走获取channel对象。
SO_BACKLOG
它属于ServerSocketChannel,在学习该参数之前先学习tcp的三次握手。
-
当客户端调用connet()方法的时候就会向服务器发送给一个连接信息,也就是一个数据包,也就是SYN。
-
当服务器收到这个数据包的后,会将数据包中的连接信息存入半连接队列中。当然这个时候客户端和服务器端的状态会发生变化,客户端回变为syn_send,服务器会变为syn_rcvd
-
服务器就针对刚才的syn给客户端一个应答,并且会将自己的syn数据包发送给客户端
-
客户端接收到了服务器的ack后就会把自己的状态改为established,客户端在对刚才服务器发送过来的syn也给一个ack应答。
-
服务器收到客户端的ack应答后也将自己的状态改为established,当三次握手完成后,服务器并不能直接拿到这个连接来使用,而是将这个连接从半连接队列全连接队列
为什么服务器不直接拿到连接就去用,而是放到全连接队列中嘞?这是应该服务器处理accept()连接数是有限的,可以先将经过三次握手后的连接存入队列中,当服务器又能继续处理accept事件就了从队列中拿连接。
三次握手是发生在accept之前的。
了解完三次握手与后面两个队列的关系后,接下来就了解如何利用linux系统文件来设置队列的大小
而SO_BACKLOG参数就是我们在程序中设置accept queue 全连接队列的大小。我们以前使用nio创建服务器时在服务器端会调用bind()方法指定端口号,但其实该方法还可以填第二个参数,就是设置全连接队列大小。如果我即在程序中设置了队列大小,又在linux服务器的配置文件设置了队列大小,那么最终采用的是二者中的较小值。
但是netty设置SO_BACKLOG参数并不在bind()方法中,需要使用下面的方式配置
new ServerBootstrap().option(ChannelOption.SO_BACKLOG, 1024)
ulimit -n
属于操作系统的参数,它是限制一个进程能够同时打开最大文件描述符的数量,在linux中,不管是文件还是socket都是使用的一个文件描述符来表示的,当文件描述符达到这个设置的上限,在想打开这个文件就会报错。也就是说再想经过这个进程建立socket连接也连接不了的。这个限制就是为了保护我们的系统,避免每个进程建立的socket数过多。所以如果我们的服务器有高并发的需求,那就需要调整该参数了。
这个只是一个临时的调整,建议将它放在一个启动脚本中, -n 是固定写法 再后面加一个数字,表示运行打开文件描述符的数量。
TCP_NODELAY
属于SocketChannel参数。我们之前学习黏包半包的时候学习了Nagle算法,它会把一些小的数据 先留着,攒够一批了再发送,这样就可能造成接收方始终得不到我们发送的数据
netty的TCP_NODELAY 默认值是false 默认开启了Nagle算法,但是我们服务器还是希望消息及时的发送,所以需要在客户端这边将值改为true,不开启Nagle算法。
SO_SNDBUF和SO_RCVBUF
这就是之前将tcp滑动窗口是讲的发送缓冲区和接收缓冲区。他俩就决定了滑动窗口的上限,但是不建议我们调整这两个参数,早期需要我们自己根据硬件情况进行调整,现在的操作系统比较智能,它会根据通信双方的网络带宽来调整这两个值
ALLOCATOR
属于SocketChannel。用来分配ByteBuf。我们以前在handler 中创建ByteBuf时 使用ctx.alloc()
获取的就是这个分配器对象。那我们拿到这个对象还能干什么嘞?比如我们使用netty的ctx.alloc().buffer();
默认获取的是池化的直接内存,如果我想改变就要使用该对象了。 接下来就通过源码一步一步去找如何修改池化与非池化,直接内存与堆内存
首先是找到ChannelConfig
接口,然后在到它的实现类DefaultChannelConfig
,在该类的构造方法中就可以看见比较多的参数的默认值,然后找到我们这次的目标this.allocator = ByteBufAllocator.DEFAULT;
跟着点进去,找到最终赋值的地方
static {
MAX_BYTES_PER_CHAR_UTF8 = (int)CharsetUtil.encoder(CharsetUtil.UTF_8).maxBytesPerChar();
// 这里读取系统的参数,得到到底是不是池化
String allocType = SystemPropertyUtil.get("io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
Object alloc;
// 然后在根据得到的字符串决定为alloc相应的值,这里我们继续往下面点 UnpooledByteBufAllocator.DEFAULT
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else if ("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
}
。。。
}
最后会找到 是否不分配直接内存,这里也都读取的一个系统变量 io.netty.noPreferDirect
DIRECT_BUFFER_PREFERRED = CLEANER != NOOP && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.noPreferDirect: {}", !DIRECT_BUFFER_PREFERRED);
}
RCVBUF_ALLOCATOR
属于SocketChannel,用来控制netty的接收缓冲区大小,并且可以根据接收的消息动态的调整接收缓冲区大小,统一采direct直接内存,至于池化还非池化还是要恩局allocator决定。这是因为从网络上读写数据,直接内存要比推内存的效率高,so,netty对于这种io的读写操作就对规定了只能使用直接内存
rpc
搭建rpc简单框架
rpc其实就是一端调用另一个端的方法
还是使用的原有的网络聊天消息的内容,创建两个类 一个用于rpc请求,另一个用于rpc响应,这两个类都继承于message父类。首先是rpc用于请求的类,该类需要的参数有:调用接口的全类名(需要通过它获取到该接口的实现类对象)、需要执行的方法名、方法的返回值类型,方法形参类型,方法的形参值。总之就是利用反射执行方法所需的条件。rpc用于响应的类就两个参数,返回结果与返回异常。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RpcRequestMessage extends Message{
private int id; // 用于和响应消息对应
private Class<?> interfaceClass;
private String methodName;
private Class<?>[] methodParameterType;
private Object[] methodParameterValue;
// 设置消息类型
@Override
public int getMessageType() {
return 6;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RpcResponseMessage extends Message{
private int id; // 用于和请求消息对应
private Object result;
private Exception exception;
// 设置消息类型
@Override
public int getMessageType() {
return 7;
}
}
首先是rpc服务器端的实现,我们需要往pipeline中添加一个处理rpc请求消息对象的handler,在重写的方法中通过传递过来的msg参数也就是rpc请求对象,通过请求对象中的参数,利用反射执行相应的方法。得到方法的执行结果后存入rpc的响应消息中,然后将响应消息写回给客户端。
// 创建处理请求对象的Handler,然后将此Handler添加加pipeline中
public class RpcRequestHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, RpcRequestMessage rpcRequestMessage) throws Exception {
// 这里利用RpcRequestMessage对象 利用反射执行客户端要执行的方法,
// 然后将返回结果存入响应对象通过channel写回给客户端
}
}
接下来的rpc客户端的实现,其实就是在连接建立成功后,往服务器端写一个rpc请求对象,将该对象需要的参数都写好,然后发送给服务器端,服务器就会得到这个请求对象,然后利用反射执行,将执行结果存入rpc响应对象返回给客户端,客户端再创建一个处理rpc响应对象的Handler,接收结果。
// 处理响应的handler
public class RpcResponseHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, RpcResponseMessage rpcResponseMessage) throws Exception {
// 获取到服务器传递过来的结果
}
}
public static void main(String[] args) throws InterruptedException {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline().addLast(new LoggingHandler());
nioSocketChannel.pipeline().addLast(new RpcResponseHandler());// 上面自定义的handler
// 再添加一个自定义入站handler,在active事件中创建一个Rpc请求消息对象,发送给服务器
}
})
.connect("localhost", 8080)
.sync()
.channel()
.closeFuture()
.sync();
}
其实就是通过netty进行网络通信,然后利用反射执行服务器端的方法,然后将结果写回给客户端,这就是一个简单的rpc框架。
这里运行客户端发送rpc请求对象时会有错误,报错原因是这里发送java对象,采用的序列化算法是json,采用的谷歌的gson,因为这个java对象其中存储了几个class类型的属性,gson不知道怎么转了就会报错。
解决利用gson序列化 java对象中存储class属性 进行网络传输是的异常问题
需要创建一个类型转换器
/**
* 定义一个类型转换器,需要实现两个接口 ,接口都需要指定泛型表示需要java中的什么类型进行序列化或反序列化
*/
static class ClassCodec implements JsonSerializer<Class<?>>, JsonDeserializer<Class<?>>{
@Override
public Class<?> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
// 反序列化方法,这个时候是要把string转换为class,首先是拿到字符串 也就是全类名
try {
String str = jsonElement.getAsString();
return Class.forName(str);
} catch (ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
@Override
public JsonElement serialize(Class<?> aClass, Type type, JsonSerializationContext jsonSerializationContext) {
// 序列化方法,需要做的事情就是把class类型转换为json,其实就是通过class获取到类的全路径然后发送给服务器
// 这里的服务器只要拿到类的全路径就可以使用反射了
// 但是这里不能直接返回一个全路径的字符串,因为该方法的返回是一个JsonElement对象,所以就需要创建该对象然后封装值
// 有很多以Json开头的方法,因为string在gson中属于基本类型 所以这里创建的是JsonPrimitive对象
return new JsonPrimitive(aClass.getName());
}
}
类型转换器创建完成后如何使用?
// 后面方法是参数1是 哪一种类型需要使用转换器 参数2是我们创建的转换器
Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCodec()).create();
gson.toJson(String.class); // 再使用就不报错了
现在把创建类型转换器的代码放入序列化的接口中,然后在接口中的枚举属性中,将之前利用json的一段改写一下
enum Algorithm implements MySerializer{
java{
。。。
},
json{
// json 有很多解析包,这里是使用的谷歌的gson
@Override
public <T> byte[] serializer(T object) {
// 原始代码
// String str = new Gson().toJson(object);
// return str.getBytes(StandardCharsets.UTF_8);
// 使用我们自定义的类型转换器来进行序列化与反序列化
Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCodec()).create();
String str = gson.toJson(object);
return str.getBytes(StandardCharsets.UTF_8);
}
@Override
public <T> T dSerializer(Class<T> clazz, byte[] bytes) {
// 原始代码
// String str = new String(bytes, StandardCharsets.UTF_8);
// return new Gson().fromJson(str, clazz);
Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCodec()).create();
String str = new String(bytes, StandardCharsets.UTF_8);
return gson.fromJson(str, clazz);
}
}
}
获取channel
到目前为止只是完成了rpc的通信,但对于rpc框架来说 功能才刚刚开始。
目前为止的代码是客户端建立连接后就创建一个rpc请求对象,然后通过channel发送给服务器,但不应该我们在代码中将请求消息写死,最主要的就是获取channel,然后让使用者拿到channel后自己决定要发送什么消息
package com.hs.nettyIntermediate.mode3;
import com.hs.nettyIntermediate.mode2.protocol.MessageCodec2;
import com.hs.nettyIntermediate.mode3.handler.RpcResponseHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* rpc客户端 先抽取channel
* @author hs
* @date 2021/08/03
*/
public class RpcClientManager {
private static volatile Channel channel;
// 随便创建一个对象,用来锁对象
private static Object obj = new Object();
// channel不需要创建多个,实现单例
public static Channel getChannel() throws Exception {
if (channel == null){
synchronized (obj){
if (channel == null){
initChannel();
}
}
}
return channel;
}
// 初始化Channel
private static void initChannel() throws Exception {
// eventLoop
NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
// Handler
LoggingHandler loggingHandler = new LoggingHandler(LogLevel.DEBUG);
MessageCodec2 messageCodeHandler = new MessageCodec2();
RpcResponseHandler rpcResponseHandler = new RpcResponseHandler();
channel = new Bootstrap()
.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 黏包半包
socketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,11,4,0,0));
socketChannel.pipeline().addLast(loggingHandler); // 日志
socketChannel.pipeline().addLast(messageCodeHandler); // 自定义编解码器
socketChannel.pipeline().addLast(rpcResponseHandler); // 处理rpc响应的handler
}
})
.connect("localhost", 8080)
.sync()
.channel();
// channel关闭后,采用异步的方式释放EventLoopGroup。
channel.closeFuture().addListener(future -> {
eventLoopGroup.shutdownGracefully();
});
}
}
客户端——代理
我们创建的rpc框架现在发送消息是
我们可以创建一个代理,把正常的消息调用转换为我们底层需要的RpcRequestMessage对象的方式。
获取返回值
上面通过动态代理虽然成功创建了请求消息对象并发送给了服务器端,但是动态代理中目前还没有返回值,也就是是当前rpc框架使用者获得了代理对象,调用了方法但是没有返回值。接收服务器的返回值这时候是在EventLoopGroup中的线程在接收消息Handler中接收请求消息,可是这里又是主线程使用了动态代理需要拿到服务器发送过来的响应消息,这里就又是一个线程通信的问题。
还是可以使用netty为我们提供的Promise,可以把它看成一个背包,主线程在动态代理中向服务器发送了请求消息对象后就阻塞,等待Promise中有内容了就解除阻塞,从中获取内容返回。
在处理响应消息的Handler中也要添加一些内容,就是当响应消息对象发送过来了,将该消息存入Promise中,假如客户端发送了多条消息,这里就要有多个Promise,所以还需要创建一个类,用一个map集合存放一个请求与一个响应对应的Promise。也就是需要在动态代理中创建好请求消息对象后就往mao集合中添加一个Promise,key就是请求消息的id。Handler中获取了响应消息就通过响应消息的id从集合中取出Promise,再将响应消息对象存入Promise中,用完后记得要从集合中将该对象移除,因为一次请求响应后该对象也就不会再用了。
首先创建map结合用来存放Promise
然后是主线程这边将请求消息发送给服务器后就往那个map中添加Promise
创建Promise时 泛型应该将 ?改为Object
map中添加Promise后,就需要阻塞住主线程,可以使用promise.sync()
或者是 promise.await()
区别是执行结果出现了异常sync()抛异常,而await()不会。
当Promise中有结果了就不会阻塞了,接着就判断是否执行成功并将结果返回
最后是处理响应消息对象的Handler,需要将响应消息存入Map集合中的Promise中。
异常调用
目前为止考虑的都是正常的执行,如果当请求消息对象发送给服务器端,服务器通过反射执行相应的方法时出现了异常该怎么办?
客户端发送消息后会在处理黏包半包的位置报一个帧长度越界异常,之所以会报这个异常是因为我们现在如果利用反射执行方法报异常后会将异常对象存入响应消息对象返回给客户端,这里返回的异常信息很多,有异常的堆栈信息,从而返回了一万多个字节给客户端,而我们客户端黏包半包的帧长度设置为1024,所以客户端就报了帧越界异常。
修改就是在服务器端利用反射执行相应方法时,如果出现了异常不将整个异常对象发送给客户端,就发送异常消息即可e.getCause().getMessage()
总结
通过netty搭建一个rpc简易框架的过程:
- 自定义网络通信协议
- 定义各类消息对象
- 多个序列化算法
- 创建相应协议编解码器
- 创建服务器端与客户端,在外面创建NioEventloopGroup与一些可以共用的Handler,将Handler(黏包半包、日志信息、编解码器、处理各类消息对象的Handler)添加进pipeline。
- 定义rpc请求消息对象与响应消息对象,请求消息对象中的参数是id+执行类的接口全路径+方法名+参数类型+方法所有参数。响应消息对象就只是id+正常结果+异常对象。
- 在服务器端添加处理处理rpc请求消息的Handler,就是利用请求对象中的参数利用反射执行对应的方法,然后将结果封装进响应消息对象通过channel写回给客户端。
- 客户端创建连接,提取channel对象,还需要引进单例。
- 创建一个代理方法,为框架使用者提供要远程调用类的代理对象,然后调用相应方法。动态代理方法需要的参数是接口的Class类型,在动态代理的具体执行逻辑中利用接口class类型和方法对象以及方法参数创建一个rpc请求消息对象,在连接成功后获取第八步的channel往服务器发送请求消息对象
- 框架使用者就只需要通过我们提供的代理方法获取到代理对象并调用方法即可。现在还有返回结果的问题。
- 主线程获取的代理对象执行的动态代理,但是接收rpc响应消息对象是NioEventloopGroup中的线程。这里又有线程的通信问题,可以使用Promise来解决,但是会进行通信肯定会发送多次消息,所以需要创建多个Promise,那么如何保证响应的结果存入对应请求的Promise中?
- 创建一个map集合,key是消息的id,在动态代理业务逻辑中创建请求消息对象通过channel发送给服务器端后就获取对应的Promise调用sync()或await()阻塞。在EventLoop线程处理响应消息对象的Handler中接收到了结果,进行判断服务器是否执行成功,然后获取对应的Promise调用
setSuccess()
或者setFailure()
,移除map中的该Promise对象,主线程解除阻塞,判断是否执行成功进行对应的返回。
探究源码
启动流程
nio启动流程
因为netty的底层使用的是nio,所以先回忆一下nio的启动流程对于接下来要探究的netty启动流程也是有好处的。
-
创建一个选择器,监听多个channel发生的各类事件
Selector selector = Selector.open();
-
创建一个ServerSocketChannel,并且设置非阻塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false);
-
将serverSocketChannel注册进选择器中
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
这是jdk原生的ServerSocketChannel,将来如果selector发生了事件,会将这个事件交给Nio相应的类去处理,这里就使用到了attachment附件,通过附件将serverSocketChannel与NioServerSocketChannel进行绑定。
NioServerSocketChannel nioServerSocketChannel = new NioServerSocketChannel(); SelectionKey selectionKey = serverSocketChannel.register(selector, 0, attachment);
-
绑定端口
serverSocketChannel.bid(new InetSocketAddress(8080));
-
在selectionKey上注册一个它关心的事件类型
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
概述
上面nio的五个步骤是如何在netty中实现的?
下方代码是使用netty创建一个服务器的基本步骤
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new StringDecoder());
}
})
.bind(8080);
我们知道EventLoop包含了一个Selector和一个单线程执行器 ,也就是说.group(new NioEventLoopGroup())
这行语句可以看为是完成Nio的第一步创建一个选择器的。
Nio剩下的四个步骤其实都是在.bind(8080);
这行语句完成的,然后我们点进bind()方法,接着会进入到第一个比较重要的方法doBind
private ChannelFuture doBind(final SocketAddress localAddress) {
// initAndRegister()方法 所做的事情就是初始化和注册,相当于上面Nio的第二步和第三步
// 它会将ServerSocketChannel创建好后注册进Selector中。该方法返回一个Future对象,就说明该方法是异步的,
final ChannelFuture regFuture = this.initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
// 这里就会利用future对象调用isDone()进行判断,如果上面initAndRegister()方法干的活比较快,就会执行if语句,
// 但是一般情况下initAndRegister()方法中的nio线程将ServerSocketChannel和Selector进行绑定会比较慢 会执行else语句
} else if (regFuture.isDone()) {
ChannelPromise promise = channel.newPromise();
// doBind0()方法是相当于Nio的第四步 绑定端口,监听事件
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
final AbstractBootstrap.PendingRegistrationPromise promise = new AbstractBootstrap.PendingRegistrationPromise(channel);
// 从这里可以看出future对象采用了异步的方式执行下面的语句,下方的doBind0()方法也就不是主线程调用了
regFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
promise.setFailure(cause);
} else {
promise.registered();
// 进入到else语句后会在这里执行doBind0()方法
AbstractBootstrap.doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}
在正式开始之前需要了解ServerBootstrap.bind(8080);
是主线程调用的,然后进入到doBind()
方法 在进入到initAndRegister()
方法中,直到创建ServerSocketChannel都是主线程做的事,包括register的前一部分都是主线程,但是在Register中会启动Nio线程,后续的操作就不是在主线程中执行了,ServerSocketChannel注册进Selector中都是Nio线程做的事,如下图所示:
概述需要了解的就几件事
- init是创建ServerSocketChannel
- Register是将ServerSocketChannel注册进Selector中的,是nio线程执行的
- initAndRegister()会返回一个future对象,然后使用该对象进行if判断,一般情况下都会进入到else语句
- else语句中会利用future的异步方式,通过nio线程来执行doBind0()方法
init
接下来详细了解initAndRegister()
方法中的init部分。
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
// 这行就是创建一个channel,创建的就是NioServerSocketChannel。
// 再点进newChannel()方法就会发现里面是利用了反射调用无参构造方法获取的对象 constructor.newInstance()
// 这里不仅仅会创建NioServerSocketChannel。还会创建jdk的ServerSocketChannel
channel = this.channelFactory.newChannel();
// 创建NioServerSocketChannel对象后就调用了init()方法,具体方法如下方代码所示
this.init(channel);
} catch (Throwable var3) {
if (channel != null) {
channel.unsafe().closeForcibly();
return (new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE)).setFailure(var3);
}
return (new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE)).setFailure(var3);
}
// 上面的init部分 从这里开始就是register部分了
ChannelFuture regFuture = this.config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}
创建NioServerSocketChannel对象后就调用了init()方法
void init(Channel channel) throws Exception {
。。。
ChannelPipeline p = channel.pipeline();
。。。
// 这里就会发现创建NioServerSocketChannel后会往该channel的pipeline中添加一个Handler
// 这个ChannelHandler和其他hander不同的地方在于该handler的initChannel()方法只会执行一次
// 这里执行往pipeline中添加handler哦 还没有到执行的地步哦
p.addLast(new ChannelHandler[]{new ChannelInitializer<Channel>() {
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = ServerBootstrap.this.config.handler();
if (handler != null) {
pipeline.addLast(new ChannelHandler[]{handler});
}
ch.eventLoop().execute(new Runnable() {
public void run() {
pipeline.addLast(new ChannelHandler[]{new ServerBootstrap.ServerBootstrapAcceptor(ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)});
}
});
}
}});
}
所以initAndRegister()
方法中的init部分的作用就是
- 创建了一个NioServerSocketChannel,
- 并往该channel的pipeline中添加了一个Handler。
Register
接下来就轮到了Register部分
final ChannelFuture initAndRegister() {
Channel channel = null;
// init
try {
channel = this.channelFactory.newChannel();
this.init(channel);
} catch (Throwable var3) {
if (channel != null) {
channel.unsafe().closeForcibly();
return (new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE)).setFailure(var3);
}
return (new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE)).setFailure(var3);
}
// 上面的init部分 从这里开始就是register部分了
ChannelFuture regFuture = this.config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
首先是register部分的第一行代码ChannelFuture regFuture = this.config().group().register(channel);
该方法返回的是有个ChannelFuture对象,那么我们笃定该方法是异步的。然后我们点进该方法,多点几次就会进入到核心
经过的类如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8bC8ZvbT-1628604267435)(E:\Java笔记\picture\image-20210806125339453.png)]
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
if (eventLoop == null) {
throw new NullPointerException("eventLoop");
} else if (AbstractChannel.this.isRegistered()) {
promise.setFailure(new IllegalStateException("registered to an event loop already"));
} else if (!AbstractChannel.this.isCompatible(eventLoop)) {
promise.setFailure(new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
} else {
AbstractChannel.this.eventLoop = eventLoop;
// 到目前为止都是主线程在执行,下面这个if就是判断当前线程是否是nio线程,所以自然而然就进入到else语句中
if (eventLoop.inEventLoop()) {
this.register0(promise);
} else {
// 这里做的事情就是将正在做事的register0(promise)方法封装到了任务对象中,然后让eventLoop线程去执行
try {
// 这里第一次调用execute()方法会创建EventLoop线程,然后取执行run()方法,而不是早就创建好线程直接用。
eventLoop.execute(new Runnable() {
public void run() {
AbstractUnsafe.this.register0(promise);
}
});
} catch (Throwable var4) {
。。。
}
}
}
}
现在进入到register0(promise)
方法体中
private void register0(ChannelPromise promise) {
try {
if (!promise.setUncancellable() || !this.ensureOpen(promise)) {
return;
}
boolean firstRegistration = this.neverRegistered;
// 在netty源码中,一般do开头的方法就是真正做事的方法,具体功能如下方的代码
AbstractChannel.this.doRegister();
this.neverRegistered = false;
AbstractChannel.this.registered = true;
// doRegister()方法结束后还有下面这个方法也比较重要。我们上面在init的步骤中往channel的pipeline中添加了一个Handler,但是还没有被调用。下面这个方法就是调用init添加的Handler。
// 那个Handler的作用就是又往pipeline中添加另一个Handler,用来当acceptor事件发生后建立连接的
AbstractChannel.this.pipeline.invokeHandlerAddedIfNeeded();
// 在最开始的代码中主线程调用了initAndRegister()方法 返回了一个future对象,然后采用了异步的方式执行一些代码,异步的方法体需要等待Future对象中有数据了才会执行,那么谁来给数据嘞?就是下面这个safeSetSuccess()方法。
// 下面方法体中的promise就是initAndRegister()方法返回的future对象 方法意思就是给这个promise设置一个安全的成功值
// 这里给了结果 initAndRegister()方法返回的Future对象 异步方式的回调方法就会被执行 会执行方法体中的doBind0()方法。 回调方法体如下:
this.safeSetSuccess(promise);
AbstractChannel.this.pipeline.fireChannelRegistered();
if (AbstractChannel.this.isActive()) {
if (firstRegistration) {
AbstractChannel.this.pipeline.fireChannelActive();
} else if (AbstractChannel.this.config().isAutoRead()) {
this.beginRead();
}
}
} catch (Throwable var3) {
。。。
}
}
再进入到doRegister()
方法中 这里会进入到AbstractNioChannel
类的doRegister()
方法
protected void doRegister() throws Exception {
boolean selected = false;
while(true) {
try {
// this.javaChannel()就是拿到jdk原生的ServerSocketChannel
// 然后在用jdk原生的ServerSocketChannel调用register()方法,
// 该方法之前参数1需要绑定的Selector就是从我们现在的EventLoop中得到,刚开始没有关注事件,
// 最后的附件this就是NioServerSocketChannel
// 所以下面就和nio时学的serverSocketChannel.register(selector, 0, attachment)一样
this.selectionKey = this.javaChannel().register(this.eventLoop().unwrappedSelector(), 0, this);
// 这里的附件NioServerSocketChannel 之前讲过,如果jdk原生的ServerSocketChannel发生了事件,是由NioServerSocketChannel 调用一些对应的方法进行处理。
return;
} catch (CancelledKeyException var3) {
。。
}
}
}
initAndRegister()方法返回的Future对象 等待Future异步回调的方法
regFuture.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
promise.setFailure(cause);
} else {
promise.registered();
// 进入到else语句后会在这里执行doBind0()方法
AbstractBootstrap.doBind0(regFuture, channel, localAddress, promise);
}
}
});
Register做的事情就是:
- 从主线程切换到nio线程
- 将ServerSocketChannel注册进Selector中,附件和NioServerSocketChannel进行绑定
- 再执行了init时添加的Handler,又往pipeline中又添加了一个Handler,用来处理将来发生的accept事件
- 并为promise赋值,也就是initAndRegister()方法返回的Future对象赋值,让future对象能够执行异步方法 在该方法中调用doBind0()方法
doBind0()
经过了initAndRegister()之后,就轮到执行Future异步方式的回调函数中 调用的doBind0()方法了
private static void doBind0(final ChannelFuture regFuture, final Channel channel, final SocketAddress localAddress, final ChannelPromise promise) {
// netty的老套路,保存是EventLoop线程执行
channel.eventLoop().execute(new Runnable() {
public void run() {
if (regFuture.isSuccess()) {
// 继续往下面执行
channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
promise.setFailure(regFuture.cause());
}
}
});
}
一直往里面点
最后就进入到AbstractChannel
类中的bind()
方法
public final void bind(SocketAddress localAddress, ChannelPromise promise) {
this.assertEventLoop();
if (promise.setUncancellable() && this.ensureOpen(promise)) {
...
boolean wasActive = AbstractChannel.this.isActive();
try {
// 正真正做事的方法 do开头 它会执行ServerSocketChannel与端口号的绑定
AbstractChannel.this.doBind(localAddress);
} catch (Throwable var5) {
this.safeSetFailure(promise, var5);
this.closeIfClosed();
return;
}
// 在doBind()之后运行,
// 这里if就是判断就是 判断当前的ServerSocketChannel是否可以用了 是否是Active状态
if (!wasActive && AbstractChannel.this.isActive()) {
this.invokeLater(new Runnable() {
public void run() {
// 如果当前channel是Active状态了就会执行下面的语句
// 作用是触发channel中pipeline里面的所有Handler的active事件
AbstractChannel.this.pipeline.fireChannelActive();
}
});
}
this.safeSetSuccess(promise);
}
}
点进doBind()方法 选择 NioServerSocketChannel
类中的doBind()方法
:
protected void doBind(SocketAddress localAddress) throws Exception {
// 首先判断jdk版本是否大于等于7
if (PlatformDependent.javaVersion() >= 7) {
// this.javaChannel() 就是jdk的ServerSocketChannel,后面指定端口与设置全连接队列的大小
this.javaChannel().bind(localAddress, this.config.getBacklog());
} else {
this.javaChannel().socket().bind(localAddress, this.config.getBacklog());
}
}
doBind()
方法执行完后会执行pipeline.fireChannelActive()
方法,触发channel中pipeline里面的所有Handler的active事件。当前pipeline中的handler是:head–>acceptor–>tail。后两个Handler即使触发了active也不会做什么事情,主要做事的还是Head这一个Handler ,然后点进pipeline.fireChannelActive()
方法
public void channelActive(ChannelHandlerContext ctx) {
ctx.fireChannelActive();
// 真正让SelectorKey关注accept事件是下面的方法执行的
this.readIfIsAutoRead();
}
再往下执行 最后会进入到AbstractNioChannel
类的doBeginRead()
方法
protected void doBeginRead() throws Exception {
// 这里就是SelectionKey
SelectionKey selectionKey = this.selectionKey;
if (selectionKey.isValid()) {
this.readPending = true;
int interestOps = selectionKey.interestOps();
// 这里首先判断SelectionKey是否已经关注Accept事件,如果没有才会执行下面的方法
if ((interestOps & this.readInterestOp) == 0) {
// 这里的 | 就是+号 this.readInterestOp的值就是16
// nio中 selectionKey.interestOps(SelectionKey.OP_ACCEPT); 这里的SelectionKey.OP_ACCEPT也是16
selectionKey.interestOps(interestOps | this.readInterestOp);
}
}
}
到目前为止,nio中的五个步骤在netty中都执行到了
EventLoop
NioEventloop的重要组成:selector、线程、任务队列。我们现在找到这三个在源码中的位置
// selector
public final class NioEventLoop extends SingleThreadEventLoop {
...
private Selector selector;
private Selector unwrappedSelector;
private SelectedSelectionKeySet selectedKeys;
}
// 线程和任务队列在NioEventLoop的父类的父类中
public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
...
private final Queue<Runnable> taskQueue; // 任务队列
private volatile Thread thread; // 线程
...
private final Executor executor;// 这个就是一个单线程的线程池 也就是上面的那个线程
// 因为NioEventloop只有一个线程,但我们可能会提交多个任务,单线程同一时刻就只能执行一个任务,多出来的任务就放在任务队列中
// 然后由线程从队列中依次取出任务来执行
}
再进入到曾祖父类AbstractScheduledEventExecutor
public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor {
...
// 处理定时任务的任务队列
PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue;
}
NioEventLoop即会处理io任务,也会处理普通任务,还有定时任务。
Selector何时被创建
在NioEventloop
类的构造方法中
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler, EventLoopTaskQueueFactory queueFactory) {
super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory), rejectedExecutionHandler);
if (selectorProvider == null) {
throw new NullPointerException("selectorProvider");
} else if (strategy == null) {
throw new NullPointerException("selectStrategy");
} else {
this.provider = selectorProvider;
// 这里会调用openSelector()方法
NioEventLoop.SelectorTuple selectorTuple = this.openSelector();
this.selector = selectorTuple.selector;
this.unwrappedSelector = selectorTuple.unwrappedSelector;
this.selectStrategy = strategy;
}
}
进入到openSelector()
private NioEventLoop.SelectorTuple openSelector() {
final AbstractSelector unwrappedSelector;
try {
// 这里实际上就是创建一个Selector对象
unwrappedSelector = this.provider.openSelector();
} catch (IOException var7) {
throw new ChannelException("failed to open a new selector", var7);
}
。。。
}
以前我们在nio中使用的是Selector.open()
方法创建是Selector,这里对比一下这两种方式有什么区别
public abstract class Selector implements Closeable {
protected Selector() { }
public static Selector open() throws IOException {
// 可以看到 nio调用的open()方法内部也是和netty调用的一样的方法
return SelectorProvider.provider().openSelector();
}
。。。
}
Selector何时被创建?
在NioEventLoop的构造方法中被创建。
两个Selector成员变量
为什么在NioEventLoop中会两个Selector成员变量
// selector
public final class NioEventLoop extends SingleThreadEventLoop {
...
// 在源码中也就是下面这两个Selector,各自有什么作用
private Selector selector;
private Selector unwrappedSelector;
private SelectedSelectionKeySet selectedKeys;
}
在上面创建Selector时,调用nio底层方法provider.openSelector()
创建的Selector其实是赋值给了unwrappedSelector。为什么netty还要再加一个Selector嘞?因为在nio原生的Selector会有一个SelectionKeys集合,将来发生了事件我们要从个这里面获取事件的信息。这个集合的实现默认使用的set集合,我们都知道set遍历的性能并不高,因为它的底层是一个hash表,遍历hash表会先去遍历每个hash桶,然后再去遍历每个链表。
因为遍历的性能并不高,所以netty做了这样一个优化,将nio内部SelectionKeys集合给替换掉了,换为了基于数组的实现。
// 具体的实现还是在NioEventloop类的openSelector()方法中
private NioEventLoop.SelectorTuple openSelector() {
final AbstractSelector unwrappedSelector;
try {
// 创建selector
unwrappedSelector = this.provider.openSelector();
} catch (IOException var7) {
throw new ChannelException("failed to open a new selector", var7);
}
。。。
// 内部基于数组的实现
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
。。。
try {
// 这里是利用反射 先拿到Selector的实现类,然后获得私有成员变量
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
。。。
// 使用netty提供的一个反射工具类,将这个私有成员变量 暴力反射 然后可以调用
Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
if (cause != null) {
return cause;
} else {
// 暴力反射
cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
if (cause != null) {
return cause;
} else {
// 反射调用这两个成员变量,然后将原始的Selector对象用netty提供的替换掉
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
return null;
}
}
。。。
}
为什么在NioEventLoop中会两个Selector成员变量?
为了在遍历SelectinKeys时提高效率
- private Selector selector; 这个是包装后的 内部SelectinKeys基于数组实现的Selector
- private Selector unwrappedSelector; 这个是原始的Selector
EventLoop的nio线程何时被启动
第一次调用execute()方法时会创建EventLoop线程
EventLoop eventLoop = new NioEventLoopGroup().next();
eventLoop.execute(()->{
System.out.println("第一次调用execute()方法时会创建EventLoop线程");
})
主线程调用execute()方法,底层执行流程如下
public void execute(Runnable task) {
// 首先判断方法的参数 Runnable对象是否为null
if (task == null) {
throw new NullPointerException("task");
} else {
// 然后在判断当前线程是否为nio线程,这里是主线程调用的execute()方法 所以这里会返回fasle
boolean inEventLoop = this.inEventLoop();
this.addTask(task); // 把这个任务加入到任务队列中
if (!inEventLoop) { // false在取反就为true 就会进入到if里面
this.startThread(); // 然后就会执行startThread()方法首次开启这个线程
。。。
}
下面为SingleThreadEventExecutor类的startThread()方法
private void startThread() {
// 这里第一次进来时 state的值为1 所以第一次这里的条件满足 if的第二个条件就是将state从1改为2
// 所以 当以后再次调用该方法 if的条件就不会成立了,只有第一次会成立
if (this.state == 1 && STATE_UPDATER.compareAndSet(this, 1, 2)) {
boolean success = false;
try {
// 这里就是开启线程 , 如果开启成功就会将success变量变为true,所以findlly语句中也就不会执行了
// 如果开启线程抛异常了,finally语句就又会将state从2变为1
this.doStartThread();
success = true;
} finally {
if (!success) {
STATE_UPDATER.compareAndSet(this, 2, 1);
}
}
}
}
我们在点进doStartThread();
方法
private void doStartThread() {
// EventLoop中的线程还为null
assert this.thread == null;
// 这里的executor就是单线程的线程池 这里使用这里面的nio线程执行一个任务
this.executor.execute(new Runnable() {
public void run() {
// 执行的任务就是把当前的nio线程赋值为 EventLoop中的thread成员变量。到这里一步,thread也就有值了
SingleThreadEventExecutor.this.thread = Thread.currentThread();
if (SingleThreadEventExecutor.this.interrupted) {
SingleThreadEventExecutor.this.thread.interrupt();
}
...
label1907: {
try {
var112 = true;
// 这里的run方法也比较重要,它做的事是 一个死循环, 不断的去找任务、定时任务、io事件去执行
SingleThreadEventExecutor.this.run();
...
}
}
}
}
EventLoop的nio线程何时被启动
首次调用execute方法时启动,重复调用该方法也不会启动多次线程,因为底层有一个if判断,第一次启动有一个状态为statu值为1,当线程启动成功后就会将值变为2,所以再次调用该方法if判断也不会成立。
提交普通任务会不会结束Selector阻塞
上面说过,首次调用execute方法启动nio线程,还会调用一个run()方法,启动一个死循环,代码如下
protected void run() {
for (;;) {
try {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
// 当有事件发生的时候就会调用下方的select()方法
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
。。。
在点进select()方法
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
。。。
// 这里就是nio时我们见过的阻塞方法,避免线程一直在运行死循环,和我们使用时不一样的是这里不是调用的无参的select()方法
// netty调用的是有参的select()方法,当这里面的超时时间到了之后就会解除阻塞,之所以采用有参的select()方法
// 这是因为EventLoop不仅仅要处理io事件,还要处理一些其他任务,它不能一直阻塞。
// 这里当超时时间到了会解除阻塞,或者是有新任务提交也会唤醒它 以便及时的处理io事件之外的任务
int selectedKeys = selector.select(timeoutMillis);
selectCnt ++;
当有普通任务提交了,nio线程这里的超时时间还没到这里还是阻塞的,那到底是如何唤醒这里的嘞?
这是因为当主线程调用execute()方法,然后再从里面调用startThread()方法,该方法会判断status是否值为1,也就是首次被调用,然后在调用doStartThread()方法,这个方法里面就会启动Executor单线程池里面的nio线程,主线程到这暂时结束,nio线程为thread赋值,然后调用我们这里的死循环run()方法,再进入switch分支调用一个方法 进而导致selector.select(timeoutMillis)阻塞。
当有普通任务了,通过eventLoop.execute(()->{..})
添加普通任务,主线程又会执行一次execute()方法,又会调用startThread()方法,这时已经不是第一次调用该方法了所以不会做什么事情,然后startThread()方法执行完后接着执行execute()方法会调用一个wakeup()方法来唤醒nio阻塞的线程去执行普通任务。
提交普通任务会不会结束Selector阻塞
主线程会调用wakeup()方法唤醒阻塞的nio线程来执行普通任务。
wakeup()方法
通过上面引出了wakeup()方法,这里详细介绍该方法的执行条件
在NioEventLoop类中
protected void wakeup(boolean inEventLoop) {
if (!inEventLoop && this.wakenUp.compareAndSet(false, true)) {
this.selector.wakeup();
}
}
首先这里对if判断条件进行解读,前面一部分是表示只有EventLoop线程之外的线程提交任务才有机会执行wakeup()方法。如果的EventLoop线程自己提交的任务就会走其他的逻辑。wakenUP变量在定义时的类型是AtomicBoolean,它是采用cas的方式去设置值,多个线程在同一个时刻修改它的值只会有一个成功。它的作用是什么?
因为this.selector.wakeup();
是一个比较耗费性能的操作,所以我们应该避免对它的频繁调用。将来会有这样一个情况,有多个线程都来提交任务,都走到了上面这个地方,那么selector需要唤醒几次嘞,其实1次就足够了。所以就用到了wakenUp这个原子变量。
何时进入Select分支进行阻塞
在上面我们知道主线程首次调用eventLoop.execute(Runnable run)
—>调用startThread()
方法 —>判断statu是否为1 是否为首次调用execute()方法 —>调用doStartThread()
方法 nio线程接手主线程运行—>SingleThreadEventExecutor.this.run()
—> 死循环
protected void run() {
for (;;) {
try {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
// 这里才会让EventLoop线程 selector.select(timeoutMillis) 进入阻塞
// 那么什么条件下才会进入到这条分支嘞?
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
}
} catch (IOException e) {
rebuildSelector0();
handleLoopException(e);
continue;
}
。。。
}
决定进入select分支的条件就是switch中的代码selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())
。
点进方法:
public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
// 这里会根据上面传递过来的boolean变量决定,如果为false时就会走select阻塞分支
// 这个Boolean变量的作用就是当前是否有任务,如果没有任务就会进入select阻塞分支
return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
}
如果有任务 selectSupplier.get()
又会有什么作用。get()方法中又调用了selectNow()
方法
int selectNow() throws IOException {
try {
// 之前nio讲select()方法时讲了三个使用,一个是空参的select,第二个就是带超时时间的select 第三个就是selectNow()
// 该方法与前两个不同的地方是它不会阻塞,它会在selector上立刻查看是否有事件发生,如果没有就返回0
return selector.selectNow();
} finally {
// restore wakeup state if needed
if (wakenUp.get()) {
selector.wakeup();
}
}
}
所以,当没有任务时,才会进入Select分支,进行阻塞。如果有任务时 最终会调用selectNow()方法返回当前的任务数,并跳出switch多分支语句,执行switch下面的代码来处理任务。
会阻塞多久
当没有事件发生时,进入了select分支,最终执行selector.select(timeoutMillis);
那么这个超时时间为多久嘞,源码如下:
long currentTimeNanos = System.nanoTime(); // 当前时间
// 截止时间=当前时间+1秒 如果有定时任务 截止时间=当前时间+下一个定时任务开始的事件
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
// 超时时间 这里又减去了当前时间,后面又加上0.5毫秒,最后的是将纳秒转换为毫秒
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
。。。
}
所以当没有定时任务的情况下selector.select(timeoutMillis);
只会阻塞1秒左右。
那么什么时候会跳出select分支进入的select()方法的死循环
- 当前时间超过了截止时间,因为每一次循环都会重新为当前时间变量赋值
- 有任务发生了也会退出死循环
- 有事件发生了也会推出死循环
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime(); // 当前时间
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos); // 截止时间
for (;;) {
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
// 当前时间超过了截止时间 会退出死循环
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
// hasTasks()是否有任务
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break;
}
// 阻塞 当有事件发生会解除阻塞 selectedKeys也不为0
int selectedKeys = selector.select(timeoutMillis);
selectCnt ++;
// 所以又事件发生这里也会退出死循环
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}
。。。
long time = System.nanoTime();
。。。
currentTimeNanos = time;
}
// for end
。。
} catch (CancelledKeyException e) {
..
}
}
nio空轮询bug
nio中selector.select()方法有一个bug(jdk在linux环境下),本来正常情况下如果是无参的select()方法,只有在有事件发生时才会解除阻塞,如果是有超时时间的select()方法,没有事件发生需要等到超时时间才会解除阻塞。而这个bug有很小的几率会出现,那就是即使没有事件发生,超时时间也没到,也不会阻塞,特别是当好几个EventLoop线程都空轮询这就会很占用CPU资源。
netty解决了nio的空轮询bug,它解决的方法是采用一个循环计数的方式
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
// 就是这个selectCnt变量 初始值为0
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break;
}
// 阻塞 当有事件发生会解除阻塞 selectedKeys也不为0
int selectedKeys = selector.select(timeoutMillis);
// 每循环一次就会让计数++
selectCnt ++;
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}
。。。
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
selectCnt = 1;
// 这里首先判断有没有设置一个 期望值 大于0 并且如果 selectCnt大于了这个期望值就会退出死循环
// 这个期望值默认 会读取运行时的环境变量io.netty.selectorAutoRebuildThreshold
// 如果我们自己设置了值就以我们设置的为准,如果没有设置默认是512。
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// 当发生了空轮询bug如果出现了就会调用下面的方法,作用是重新创建一个selector,替换掉旧的selector,
// 还会把旧的selector中的一些信息赋值个新的,内部的实现还是比较复杂的。
selector = selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
// for end
。。
} catch (CancelledKeyException e) {
..
}
}
EventLoop—ioRatio
@Override
protected void run() {
for (;;) {
try {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
}
} catch (IOException e) {
rebuildSelector0();
handleLoopException(e);
continue;
}
// 当阻塞解除后,就会继续执行switch语句下面的代码来处理任务或者是发生的事件
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
// 如果ioRatio的值设置为100 就会先运行所有的io事件,然后在运行所有的普通任务
try {
processSelectedKeys();
} finally {
runAllTasks();
}
} else {// else 会做两件事
// 首先获取当前时间
final long ioStartTime = System.nanoTime();
try {
// 1. 处理所有的io事件
processSelectedKeys();
} finally {
// 处理完io事件后在获取一次当前时间,在和之前的事件相减,得到处理io事件的时间
final long ioTime = System.nanoTime() - ioStartTime;
// 2. 运行普通任务,普通任务能执行的事件就是 ioTime * (100 - ioRatio) / ioRatio 来控制的
// 当普通任务运行的时间超过了这里传递的事件 它就不会从任务队列中拿普通任务执行了
// 会等下一次循环 处理完io事件后再来执行普通任务,这样就实现了优先处理io事件
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
。。
}
}
如果普通任务的耗时比较长,那势必会影响到io事件的执行,毕竟EventLoop中只是一个单线程,netty为了避免因为普通任务的耗时较长影响到io事件,netty会做一个参数的控制——ioRatio 这个参数是控制处理io事件所占用的事件比例,它默认是50%
执行io事件,在哪进行事件判断
就从上面的源码中 ioRatio 参数的判断中 执行所有的io事件的方法processSelectedKeys();
跟进这个方法,就会进入到下面的方法中
private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
// 这里首先是拿到所有的SelectionKey
final SelectionKey k = selectedKeys.keys[i];
selectedKeys.keys[i] = null;
// 然后获取附件,也就是NioServerSocketChannel ,这里为什么要拿到这个对象嘞,
// 因为接下来要对SelectionKey进行各种各样的处理,也就是Handler,所以需要通过channel得到pipeline 在得到Handler
final Object a = k.attachment();
// 拿到channel了就进行判断是否是NioChannel
if (a instanceof AbstractNioChannel) {
//然后就会进入到这个方法
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (needsToSelectAgain) {
selectedKeys.reset(i + 1);
selectAgain();
i = -1;
}
}
}
processSelectedKey(k, (AbstractNioChannel) a);
方法的源码,就是在这里面进行各类事件的判断,然后进行相应的处理
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
final EventLoop eventLoop;
try {
eventLoop = ch.eventLoop();
} catch (Throwable ignored) {
return;
}
if (eventLoop != this || eventLoop == null) {
return;
}
unsafe.close(unsafe.voidPromise());
return;
}
try {
int readyOps = k.readyOps();
// 可连接事件 客户端的事件
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
// 可写事件
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
// 可读事件和连接事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
accept流程
首先会议nio中accept的流程
- selector.select()阻塞
- 遍历SelectionKeys
- 判断事件类型
- 创建SocketChannel
- 注册进Selector中
- 利用SelectionKey监听read事件
其实前面三步已经在上面学习EventLoop的源码中已经实现了,接下来重点关注后面的三步
在processSelectedKey(k, (AbstractNioChannel) a);
方法中进行事件判断
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
...
try {
int readyOps = k.readyOps();
// 可连接事件 客户端的事件
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
// 可写事件
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
// 可读事件和连接事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
当服务器启动,客户端启动就会进入到unsafe.read();
方法中
public void read() {
assert eventLoop().inEventLoop();
final ChannelConfig config = config();
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.reset(config);
boolean closed = false;
Throwable exception = null;
try {
try {
do {
// 这里就会创建SocketChannel 并将它设置为非阻塞的 , 这里的readBuf就是创建的NioSocketChannel
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
allocHandle.incMessagesRead(localRead);
} while (allocHandle.continueReading());
} catch (Throwable t) {
exception = t;
}
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
// 这里就是把刚刚建立的连接当成一个消息 给pipeline中的Handler去处理。
// 目前的handler共有:head--> accept ---> tail,所以这里肯定是accept这个handler来进行处理
// 其实接下来的两步 将SocketChannel注册进select并且监听read事件都是这个handler做的事
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
。。
} finally {
。。。
}
}
创建SocketChannel 并将它设置为非阻塞的方法
protected int doReadMessages(List<Object> buf) throws Exception {
// 这里面就是获得SocketChannel ,这里调用了一个工具类的方法,方法的具体实现就是ServerSocketChannel.accept()
SocketChannel ch = SocketUtils.accept(javaChannel());
try {
if (ch != null) {
// 得到了SocketChannel 还需要创建NioSocketChannel并为这两个建立联系
// 所以这里将就SocketChannel作为构造方法的参数传递给了NioSocketChannel
// 在构造方法中也会将SocketChannel设置为非阻塞
// 创建好后就将NioSocketChannel添加进list集合中 , 也就是一个消息,将来要给pipeline中的Handler去处理
buf.add(new NioSocketChannel(this, ch));
return 1;
}
} catch (Throwable t) {
。。
}
return 0;
}
接下来就不一步一步走了,最终它会到accept 这个Handler 中的方法中。最后会进入到ServerBootstrap
类中的静态内部类ServerBootstrapAcceptor
中的channelRead()
方法
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 这里的msg就是上面创建的NioSocketChannel
final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
// 这里会对NioSocketChannel 设置一些参数
setChannelOptions(child, childOptions, logger);
for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
try {
// 比较重要的方法就是这里的register(child) 它会做的事件就是把这个channel和EventLoop中的一个selector进行绑定
// 这个register()方法和之前 初始化ServerSocketChannel时调用的register()方法很类似
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
接着在进入register(child)
方法,最终会进入到AbstractChannel
类的register()
方法
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
if (eventLoop == null) {
throw new NullPointerException("eventLoop");
} else if (AbstractChannel.this.isRegistered()) {
promise.setFailure(new IllegalStateException("registered to an event loop already"));
} else if (!AbstractChannel.this.isCompatible(eventLoop)) {
promise.setFailure(new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
} else {
AbstractChannel.this.eventLoop = eventLoop;
// 这里会把当前线程和EventLoop线程进行判断,现在的线程虽然是EventLoop线程,但还是会进入到else语句中
// 这里因为目前的线程是NioServerSocketChannel的EventLoop线程,但是线程新建了一个NioSocketChannel,这两个channel肯定不能共用一个线程,所以就会进入到else中新开一个线程来执行register0(promise)方法
if (eventLoop.inEventLoop()) {
this.register0(promise);
} else {
try {
// 这里会拿到新的EventLoop,用新的EventLoop中的线程
eventLoop.execute(new Runnable() {
public void run() {
// 程序会走到这里。
AbstractUnsafe.this.register0(promise);
}
});
} catch (Throwable var4) {
。。。
}
}
}
}
AbstractUnsafe.this.register0(promise);
这行代码最后会调用doRegister()
方法,
doRegister()
方法就会先拿到jdk原生的SocketChannel,注册进当前EventLoop中的Selector中。并且把当前NioSocketChannel作为附件绑定上去
doRegister()
方法结束后 , register0(promise)
方法继续运行,调用pipeline.invokeHandlerAddedIfNeeded();
方法,其实就是给现在的NioSocketChannel加一个Handler,这些handler就是我们自己在代码中写的 ,这里会把我们写的handler 加到channel中。
register0(promise)
方法继续运行, 调用pipeline.fireChannelActive();
这里的作用就是拿到SelectionKey,然后关注read事件,这最后会在AbstractNioChannel
类的doBeginRead()
方法中添加关注read事件
read流程
还是在这个地方处理读事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
将断电打在这里,当第一次触发断点是连接,直接放行,再一次触发就是read事件了。
public final void read() {
final ChannelConfig config = config();
if (shouldBreakReadReady(config)) {
clearReadPending();
return;
}
final ChannelPipeline pipeline = pipeline();
// 获取Bytebuf的分配器,决定Bytebuf是池化还非池化
final ByteBufAllocator allocator = config.getAllocator();
// 可以动态调整Bytebuf的大小,并强制使用直接内存
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
boolean close = false;
try {
do {
// 这里就是分配具体的Bytebuf
byteBuf = allocHandle.allocate(allocator);
// 客户端发送了数据,服务器端这里调用doReadBytes()方法后救护我那个Bytebuf中填充内容
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
// nothing was read. release the buffer.
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0;
if (close) {
readPending = false;
}
break;
}
allocHandle.incMessagesRead(1);
readPending = false;
// 找到当前NioServerSocketChannel上的pipeline,然后触发一个读事件,
// 就是将这个Bytebuf依次传给入站Handler 依次去处理。
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
if (close) {
closeOnRead(pipeline);
}
} catch (Throwable t) {
handleReadException(pipeline, byteBuf, t, close, allocHandle);
} finally {
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
利用9个小时的时间,终于写完了这一整篇博客,应该很难会有人将我的这篇文章看完吧,从nio开始,到netty的基础组件与概念的学习,然后到netty在rpc框架中的应用,最后到源码部分。对于netty的学习总共耗时将近两个月,跟着视频学习,然后敲代码,做笔记。真的是我收获良多,希望大家最终都将学有所成,终将发光发热,未来可期。