粘包与半包在TCP通信中是无法避免的现象,之前在学习NIO的过程中也遇到过黏包半包问题
创建一个客户端发送10次数据给客户端
public class QsNServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ChannelFuture future = new ServerBootstrap()
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
})
.bind(8080);
future.sync();
ChannelFuture closeFuture = future.channel().closeFuture();
closeFuture.sync();
}catch (Exception e) {
e.printStackTrace();
}finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
public class QsNClient {
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ChannelFuture future = new Bootstrap()
.group(worker)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//此方法会在建立连接后执行
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
ctx.writeAndFlush(buffer);
}
}
});
}
})
.connect(new InetSocketAddress("localhost", 8080));
future.sync();
future.channel().closeFuture().sync();;
}catch (Exception e) {
e.printStackTrace();
}finally {
worker.shutdownGracefully();
}
}
}
执行后发现,客户端是分10次发送数据
但是服务端在接受的时候直接一起接受了,这就是所谓的粘包
一次性发送160字节的数据,看看服务端接收
public class QsBServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ChannelFuture future = new ServerBootstrap()
.option(ChannelOption.SO_RCVBUF,1) //设置接收缓冲区的大小,单位是字节,默认是1024
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
})
.bind(8080);
future.sync();
ChannelFuture closeFuture = future.channel().closeFuture();
closeFuture.sync();
}catch (Exception e) {
e.printStackTrace();
}finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
public class QsBClient {
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ChannelFuture future = new Bootstrap()
.group(worker)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//此方法会在建立连接后执行
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
}
//一次发送160字节
ctx.writeAndFlush(buffer);
}
});
}
})
.connect(new InetSocketAddress("localhost", 8080));
future.sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
}
多次测试,发现客户端收到并不是一次性收到的,这就是半包
本质是因为 TCP 是流式协议,消息无边界
粘包
半包
TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差
为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值
窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用
以粘包为例,需要发送10次数据,没发完一次就重新关闭连接,重新建立连接发送
修改客户端代码
public class Client1 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
send();
}
}
public static void send(){
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ChannelFuture future = new Bootstrap()
.group(worker)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//此方法会在建立连接后执行
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
ctx.writeAndFlush(buffer);
//发送完就关闭channel
ctx.close();
}
});
}
})
.connect(new InetSocketAddress("localhost", 8080));
future.sync();
future.channel().closeFuture().sync();;
}catch (Exception e) {
e.printStackTrace();
}finally {
worker.shutdownGracefully();
}
}
}
缺点
这种方法可以处理粘包,但是半包现象不太好处理
服务端可以通过FixedLengthFrameDecoder
来解码
添加服务端处理器,就可以按照自己设置的长度来解析收到的数据
修改服务端代码
与客户端约定好,每条信息10个字节的长度
ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
修改客户端代码
public class Client2 {
public static void main(String[] args) {
send();
}
public static void send() {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ChannelFuture future = new Bootstrap()
.group(worker)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//此方法会在建立连接后执行
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 发送内容随机的数据包
Random r = new Random();
char c = 'a';
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
byte[] bytes = new byte[10];
for (int j = 0; j < r.nextInt(10) + 1; j++) {
bytes[j] = (byte) c;
}
c++;
buffer.writeBytes(bytes);
}
ctx.writeAndFlush(buffer);
}
});
}
})
.connect(new InetSocketAddress("localhost", 8080));
future.sync();
future.channel().closeFuture().sync();
;
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
}
测试
可以看到服务端是可以处理数据的,而且和客户端什么时候flush没有关系
缺点
数据包的大小不好把握
服务端可以通过LineBasedFrameDecoder
来解码,他是根据\n
来分割不同信息的。
初始化时需要设置最大长度,例如设置最大长度为1024,当读取1024个字符都没有读到\n
分割符的话,就会报错。
插上一嘴,
DelimiterBasedFrameDecoder
在LineBasedFrameDecoder
基础上可以实现自定义分隔符
修改服务端代码
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
修改客户端代码
主要是修改发送信息的地方,在每条信息后面添加了\n
public class Client3 {
public static void main(String[] args) {
send();
}
public static void send() {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ChannelFuture future = new Bootstrap()
.group(worker)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//此方法会在建立连接后执行
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 发送内容随机的数据包
Random r = new Random();
char c = 'a';
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
for (int j = 1; j <= r.nextInt(16)+1; j++) {
buffer.writeByte((byte) c);
}
//ascii码10就是\n
buffer.writeByte(10);
c++;
}
ctx.writeAndFlush(buffer);
}
});
}
})
.connect(new InetSocketAddress("localhost", 8080));
future.sync();
future.channel().closeFuture().sync();
;
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
}
缺点
处理字符数据比较合适,但如果内容本身包含了分隔符(字节数据常常会有此情况),那么就会解析错误
服务端可以通过LengthFieldBasedFrameDecoder
来解码,这个是目前比较流行的方法,小黄在工作中对接设备上传过来的信息时,他们的信息也是类似于这种格式
主要有以下几个关键属性
案例介绍
源码中有很多案例,挑几个来介绍一下,加深理解
lengthFieldOffset为0,也就是说这串字节从第0位开始记录的就是长度字段
lengthFieldLength为2,也就是说从0开始数2个字节是用来记录长度字段的
长度字段的值记录着正文的长度,例如下面记录的000C就是12个字节
lengthAdjustment为0,也就是说长度字段读完后,就开始是正文内容了
以上设置不变
initialBytesToStrip设置为2,这是我们的过滤条件,解码时会忽略前2个字节,也就是长度字节,0的话代表不忽略,解码出来时带有长度字节信息的
Demo
现在我们来做一个小demo,规定消息体如下
前4个字节代表消息长度
第5个字节代表版本
后面是内容
解码后要求版本+内容
+--------+---------+----------------+ +---------+----------------+
| Length | Version | Actual Content |----->| Version | Actual Content |
| 12 | 1 | "HELLO, WORLD" | | 1 | "HELLO, WORLD" |
+--------+---------+----------------+ +---------+----------------+
代码
这次就不写客户端和服务端了,Netty为我们提供了一个比较便捷的测试类
public class Test {
public static void main(String[] args) {
//相当于服务端
EmbeddedChannel channel = new EmbeddedChannel(
new LengthFieldBasedFrameDecoder(1024,0,4,1,4),
new LoggingHandler(LogLevel.DEBUG)
);
//客户端发送数据
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
convert(buffer,"hello,world");
convert(buffer,"hi~~~");
channel.writeInbound(buffer);
}
/**
* 消息处理
* @param buf
* @param context 一条消息
*/
public static void convert(ByteBuf buf,String context) {
byte[] bytes = context.getBytes(Charset.defaultCharset());
//int占4个字节,前四个字节表示内容的长度
buf.writeInt(bytes.length);
//版本号为1
buf.writeByte(1);
//内容
buf.writeBytes(bytes);
}
}
测试
最终得到了我们想要的消息格式
TCP/IP 中消息传输基于流的方式,没有边界。
协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则
通俗来讲,就是为了通信双方可以将一串长文本转换成同样的语义。
redis是通过TCP来传输数据的,例如想实现
set name YellowStar
这串命令,它的协议如下*3\r\n #该条命令有几个词(每个命之间用回车+换行符隔开) \r\n $3\r\n #词的长度 set\r\n #词 $4\r\n name\r\n $10\r\n Yellowstar\r\n
通过以下代码像redis服务器发送一条指令
public class TestRedis {
public static void main(String[] args) {
NioEventLoopGroup group = new NioEventLoopGroup();
byte[] line = new byte[]{13,10};
try {
ChannelFuture future = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
// 会在连接 channel 建立成功后,会触发 active 事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = ctx.alloc().buffer();
buf.writeBytes("*3".getBytes());
buf.writeBytes(line);
buf.writeBytes("$3".getBytes());
buf.writeBytes(line);
buf.writeBytes("set".getBytes());
buf.writeBytes(line);
buf.writeBytes("$4".getBytes());
buf.writeBytes(line);
buf.writeBytes("name".getBytes());
buf.writeBytes(line);
buf.writeBytes("$10".getBytes());
buf.writeBytes(line);
buf.writeBytes("YellowStar".getBytes());
buf.writeBytes(line);
ctx.writeAndFlush(buf);
}
});
}
})
.connect(new InetSocketAddress("localhost", 6379)).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
group.shutdownGracefully();
}
}
}
执行后代码,发现数据成功发送,并且redis响应了一个ok
其实Netty支持很多种常见协议的,对http协议也做了支持,可以调用HttpServerCodec
实现解码和编码
@Slf4j
public class TestHttp {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
byte[] line = new byte[]{13,10};
try {
ChannelFuture future = new ServerBootstrap()
.group(boss,worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler());
//解析http协议
ch.pipeline().addLast(new HttpServerCodec());
//响应请求
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
log.debug("收到请求:{}",msg.getUri());
//响应 第一个参数http协议版本,与发送版本一致,第二个参数响应状态码
DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
byte[] bytes = "Hello, world!
".getBytes();
//告诉客户端响应体长度
response.headers().setInt(CONTENT_LENGTH, bytes.length);
//响应体内容
response.content().writeBytes(bytes);
//发送
ctx.writeAndFlush(response);
}
});
}
})
.bind(8080).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
使用浏览器发送http://localhost:8080/index.html
,服务端会解析成以下格式,包括请求头请求体之类的
并且浏览器也会得到响应结果
根据上面的要素,设计一个登录请求消息和登录响应消息
约定指的是通信双方对协议的约定,比如现在的约定如下
1,2,3,4
根据上面的约定,实现一个编解码器
准备Message抽象类
之后不同的消息类型统一继承该类即可
@Data
public abstract class Message implements Serializable {
/**
* 根据消息类型字节,获得对应的消息 class
* @param messageType 消息类型字节
* @return 消息 class
*/
public static Class<? extends Message> getMessageClass(int messageType) {
return messageClasses.get(messageType);
}
private int sequenceId;
private int messageType;
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;
public static final int GroupJoinRequestMessage = 6;
public static final int GroupJoinResponseMessage = 7;
public static final int GroupQuitRequestMessage = 8;
public static final int GroupQuitResponseMessage = 9;
public static final int GroupChatRequestMessage = 10;
public static final int GroupChatResponseMessage = 11;
public static final int GroupMembersRequestMessage = 12;
public static final int GroupMembersResponseMessage = 13;
public static final int PingMessage = 14;
public static final int PongMessage = 15;
/**
* 请求类型 byte 值
*/
public static final int RPC_MESSAGE_TYPE_REQUEST = 101;
/**
* 响应类型 byte 值
*/
public static final int RPC_MESSAGE_TYPE_RESPONSE = 102;
private static final Map<Integer, Class<? extends Message>> messageClasses = new HashMap<>();
static {
messageClasses.put(LoginRequestMessage, LoginRequestMessage.class);
messageClasses.put(LoginResponseMessage, LoginResponseMessage.class);
messageClasses.put(ChatRequestMessage, ChatRequestMessage.class);
messageClasses.put(ChatResponseMessage, ChatResponseMessage.class);
messageClasses.put(GroupCreateRequestMessage, GroupCreateRequestMessage.class);
messageClasses.put(GroupCreateResponseMessage, GroupCreateResponseMessage.class);
messageClasses.put(GroupJoinRequestMessage, GroupJoinRequestMessage.class);
messageClasses.put(GroupJoinResponseMessage, GroupJoinResponseMessage.class);
messageClasses.put(GroupQuitRequestMessage, GroupQuitRequestMessage.class);
messageClasses.put(GroupQuitResponseMessage, GroupQuitResponseMessage.class);
messageClasses.put(GroupChatRequestMessage, GroupChatRequestMessage.class);
messageClasses.put(GroupChatResponseMessage, GroupChatResponseMessage.class);
messageClasses.put(GroupMembersRequestMessage, GroupMembersRequestMessage.class);
messageClasses.put(GroupMembersResponseMessage, GroupMembersResponseMessage.class);
messageClasses.put(RPC_MESSAGE_TYPE_REQUEST, RpcRequestMessage.class);
messageClasses.put(RPC_MESSAGE_TYPE_RESPONSE, RpcResponseMessage.class);
}
}
解码器
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
// 1. 4个字节的魔数
out.writeBytes(new byte[]{1,2,3,4});
// 2. 1个字节的版本号
out.writeByte(1);
// 3. 1个字节的序列化算法,目前使用jdk
out.writeByte(0);
// 4. 1个字节的指令类型
out.writeByte(msg.getMessageType());
// 5. 4个字节的请求序号
out.writeInt(msg.getSequenceId());
// 6. 补全字节,一般来说都是2的次方
out.writeByte(0xff);
//序列化消息内容
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(msg);
byte[] bytes = baos.toByteArray();
// 7. 4个字节的正文长度
out.writeInt(bytes.length);
// 8. 正文
out.writeBytes(bytes);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 1. 魔数
int magicNumber = in.readInt();
// 2. 版本号
byte version = in.readByte();
// 3. 序列化算法
byte serializable = in.readByte();
// 4. 指令类型
byte messageType = in.readByte();
// 5. 请求序号
int sequence = in.readInt();
// 6. 补全
in.readByte();
// 7. 正文长度
int length = in.readInt();
// 8. 正文
byte[] bytes = new byte[length];
in.readBytes(bytes,0,length);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Message message = (Message) ois.readObject();
log.debug("{},{},{},{},{},{}",magicNumber,version,serializable,messageType,sequence,length);
log.debug("正文:{}",message);
//将正文交给下一个handler
out.add(message);
}
}
测试编码
public class TestCodec {
public static void main(String[] args) {
EmbeddedChannel channel = new EmbeddedChannel(
new LoggingHandler(LogLevel.DEBUG),
new MessageCodec()
);
//encode
LoginRequestMessage message = new LoginRequestMessage("YellowStar", "123456");
channel.writeOutbound(message);
}
}
通过输出观察
测试解码
public class TestCodec {
public static void main(String[] args) throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(
new LoggingHandler(LogLevel.DEBUG),
new MessageCodec()
);
//encode
LoginRequestMessage message = new LoginRequestMessage("YellowStar", "123456");
//decode
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null,message,buf); //编码
channel.writeInbound(buf);
}
}
通过日志可以看到,消息被正常翻译过来
public class TestCodec {
public static void main(String[] args) throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(
new LoggingHandler(LogLevel.DEBUG),
//通过LengthFieldBasedFrameDecoder来先一步处理
new LengthFieldBasedFrameDecoder(1024,12,4,0,0),
new MessageCodec()
);
LoginRequestMessage message = new LoginRequestMessage("YellowStar", "123456");
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null,message,buf); //编码
//模拟半包
//零拷贝
ByteBuf b1 = buf.slice(0, 100);
ByteBuf b2 = buf.slice(100, buf.readableBytes() - 100);
buf.retain(); //让计数器+1
channel.writeInbound(b1); // 会执行release()方法,计数器-1
channel.writeInbound(b2);
}
}
对于上述的解码器,不用保存状态,可以在多线程下被共享,一般都会将它提取出来,不用每次进去new一个新的,浪费内存
修改编码器
继承MessageToMessageCodec即可,MessageToMessageCodec默认接收到的是一个完整的信息
@Slf4j
@ChannelHandler.Sharable
public class MessageSharableCodec extends MessageToMessageCodec<ByteBuf,Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> list) throws Exception {
ByteBuf out = ByteBufAllocator.DEFAULT.buffer();
// 1. 4个字节的魔数
out.writeBytes(new byte[]{1,2,3,4});
// 2. 1个字节的版本号
out.writeByte(1);
// 3. 1个字节的序列化算法,目前使用jdk
out.writeByte(0);
// 4. 1个字节的指令类型
out.writeByte(msg.getMessageType());
// 5. 4个字节的请求序号
out.writeInt(msg.getSequenceId());
// 6. 补全字节,一般来说都是2的次方
out.writeByte(0xff);
//序列化消息内容
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(msg);
byte[] bytes = baos.toByteArray();
// 7. 4个字节的正文长度
out.writeInt(bytes.length);
// 8. 正文
out.writeBytes(bytes);
//将内容传递到下一个handler
list.add(out);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 1. 魔数
int magicNumber = in.readInt();
// 2. 版本号
byte version = in.readByte();
// 3. 序列化算法
byte serializable = in.readByte();
// 4. 指令类型
byte messageType = in.readByte();
// 5. 请求序号
int sequence = in.readInt();
// 6. 补全
in.readByte();
// 7. 正文长度
int length = in.readInt();
// 8. 正文
byte[] bytes = new byte[length];
in.readBytes(bytes,0,length);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Message message = (Message) ois.readObject();
log.debug("{},{},{},{},{},{}",magicNumber,version,serializable,messageType,sequence,length);
log.debug("正文:{}",message);
//将正文交给下一个handler
out.add(message);
}
}
聊天室具体代码,过于繁杂,小黄放在gitee上,有需要的同学自行查看 点击下载
原因
问题
服务器端解决
//5s 内如果没有收到 channel 的数据,会触发一个 IdleState#READER_IDLE 事件
ch.pipeline().addLast(new IdleStateHandler(5,0,0));
// ChannelDuplexHandler 可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler(){
// 用来触发特殊事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
//触发读空闲事件
if (event.state() == IdleState.READER_IDLE) {
log.debug("已经5秒没有获取到数据了");
ctx.channel().close();
}
}
});
客户端定时心跳
//每隔3秒触发一次IdleState#WRITER_IDLE事件
ch.pipeline().addLast(new IdleStateHandler(0,3,0));
ch.pipeline().addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
//触发空闲写事件
if (event.state() == IdleState.WRITER_IDLE) {
log.debug("3s 没有写数据了,发送一个心跳包");
ctx.writeAndFlush(new PingMessage());
}
}
});