Netty——3、进阶
1、粘包与半包
TCP以一个段(segment)为单位,每发送一个段就要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差。为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值。
窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用。
1.1、现象分析
粘包现象:发送abc def,接收abcdef。原因:
- 应用层:接收方ByteBuf设置太大(Netty默认1024);
- 滑动窗口:假设发送方256Bytes表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这256Bytes字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包。
- Nagle算法:会造成粘包。
半包现象:发送abcdef,接收abc def。原因:
- 应用层:接收方ByteBuf小于实际发送数据量;
- 滑动窗口:假设接收方的窗口只剩了128Bytes,发送方的报文大小是256Bytes,这时放不下了,只能先发送前128Bytes,等待ack后才能发送剩余部分,这就造成了半包。
- MSS限制:当发送的数据超过MSS限制后,会将数据切分发送,就会造成半包。
本质是因为TCP是流式协议,消息无边界。
1.2、解决方案
1.2.1、短连接
package com.clp.nettyPlus;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.AdaptiveRecvByteBufAllocator;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
class HelloWorldServer {
public static void main(String[] args) {
new HelloWorldServer().start();
}
void start() {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap()
// .option(ChannelOption.SO_RCVBUF, 10) //设置系统的接收缓冲区(接收的滑动窗口)
//调整netty的接收缓冲区,即ByteBuf(最小值、初始值、最大值)
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16,16,16))
.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//调试打印出服务端受到的信息
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("server error", e);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
package com.clp.nettyPlus;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
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.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorldClient {
static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
send();
}
System.out.println("finish");
}
public static void send() {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(worker)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override //在连接 channel 建立成功后触发active事件
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = ctx.alloc().buffer(16); //创建字节缓冲区
buf.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16,17});
ctx.writeAndFlush(buf); //发送这些数据
ctx.channel().close(); //断开连接
super.channelActive(ctx);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
结果:
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 16) that reached at the tail of the pipeline. Please check your pipeline configuration.
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded message pipeline : [LoggingHandler#0, DefaultChannelPipeline$TailContext#0]. Channel : [id: 0x38d017e9, L:/127.0.0.1:8080 - R:/127.0.0.1:57609].
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38d017e9, L:/127.0.0.1:8080 - R:/127.0.0.1:57609] READ COMPLETE
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38d017e9, L:/127.0.0.1:8080 - R:/127.0.0.1:57609] READ COMPLETE
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38d017e9, L:/127.0.0.1:8080 ! R:/127.0.0.1:57609] INACTIVE
09:29:32.081 [nioEventLoopGroup-3-6] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x38d017e9, L:/127.0.0.1:8080 ! R:/127.0.0.1:57609] UNREGISTERED
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626] REGISTERED
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626] ACTIVE
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626] READ: 16B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
+--------+-------------------------------------------------+----------------+
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 16, cap: 16) that reached at the tail of the pipeline. Please check your pipeline configuration.
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded message pipeline : [LoggingHandler#0, DefaultChannelPipeline$TailContext#0]. Channel : [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626].
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626] READ COMPLETE
09:29:32.081 [nioEventLoopGroup-3-7] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x630e6f03, L:/127.0.0.1:8080 - R:/127.0.0.1:57626] READ: 2B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 10 11 |.. |
+--------+-------------------------------------------------+----------------+
...
1.2.2、定长解码器
1.2.3、基于分割符的解码器
1.2.4、LTC解码器
lengthFieldOffset:长度字段偏移量,表明长度字段部分从哪开始
lengthFieldLength:长度字段本身占用的字节数
lengthAdjustment:长度之后还有几个字节才是内容
initialBytesToStrip:经过解析之后,从头开始要剥离几个字节
package com.clp.decodeEncode;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class TestLengthFieldDecoder {
public static void main(String[] args) {
EmbeddedChannel channel = new EmbeddedChannel(
new LengthFieldBasedFrameDecoder(
1024, 0, 4, 1, 5),
new LoggingHandler(LogLevel.DEBUG)
);
//发送 4 字节内容的长度,然后是实际内容
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
write(buffer, "Hello, world");
write(buffer, "Hi!");
channel.writeInbound(buffer);
}
private static void write(ByteBuf buffer, String content) {
byte[] bytes = content.getBytes(); //实际内容
int length = bytes.length; //实际内容长度
buffer.writeInt(length); //大端表示法写入长度
buffer.writeByte(1); //加上版本号 1字节
buffer.writeBytes(bytes); //写入内容
}
}
结果:
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 |Hello, world |
+--------+-------------------------------------------------+----------------+
10:32:24.729 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 3B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 69 21 |Hi! |
+--------+-------------------------------------------------+----------------+
10:32:24.729 [main] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE
2、协议设计与解析
2.1、自定义协议要素
- 魔数:用来在第一时间判断是否是无效数据包。
- 版本号:可以支持协议的升级。
- 序列化算法:消息正文到底采用哪种序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk。
- 指令类型:是登录、注册、单聊、群聊...跟业务相关。
- 请求序号:为了双工通信,提供异步能力。
- 正文长度。
- 消息正文,如json、xml、对象流 ...。
代码演示:
/**
* 自定义协议构成:
* 魔数:用来在第一时间判断是否是无效数据包。
* 版本号:可以支持协议的升级。
* 序列化算法:消息正文到底采用哪种序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk。
* 指令类型:是登录、注册、单聊、群聊...跟业务相关。
* 请求序号:为了双工通信,提供异步能力。
* 正文长度。
* 消息正文,如json、xml、对象流 ...。
*
* 需要配合 LengthFieldBasedFrameDecoder 一起使用
*
*/
@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf out) throws Exception {
// 4 字节的魔数
out.writeBytes(new byte[]{1, 2, 3, 4});
// 1 字节的版本
out.writeByte(1);
// 1 字节 使用的序列化方式 jdk=0;json=1
out.writeByte(0); // 使用jdk
// 1 字节 指令类型
out.writeByte(message.getMessageType());
// 4 字节的序号用于双工通信
out.writeInt(message.getSequenceId());
// 添加无意义字节为了对齐填充,保证字节长度为2的整数倍
out.writeByte(0xFF);
// 获取内容的字节数组
ByteArrayOutputStream boStream = new ByteArrayOutputStream();
ObjectOutputStream ooStream = new ObjectOutputStream(boStream);
ooStream.writeObject(message);
byte[] bytes = boStream.toByteArray();
// 4 字节的长度
out.writeInt(bytes.length);
// n 字节的内容
out.writeBytes(bytes);
}
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> list) throws Exception {
int magicNumber = in.readInt();
byte version = in.readByte();
byte serializerType = in.readByte();
byte messageType = in.readByte();
int sequenceId = in.readInt();
in.readByte();
int length = in.readInt();
byte[] bytes = new byte[length];
in.readBytes(bytes, 0, length);
Message message = null;
if (serializerType == 0) {
ObjectInputStream oiStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
message = (Message) oiStream.readObject();
}
log.debug("{}, {}, {}, {}, {}, {}", magicNumber, version, serializerType, messageType, sequenceId, length);
log.debug("{}", message);
if (message != null) {
list.add(message);
}
}
}
@Slf4j
public class ChatServer {
static LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0));
ch.pipeline().addLast(LOGGING_HANDLER);
ch.pipeline().addLast(new MessageCodec());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException exception) {
log.error("server error", exception);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
3、连接假死
连接假死的原因:
- 网络设备出现故障,例如网卡,机房等,底层的 TCP 连接已经断开了,但应用程序没有感知到,仍然占用着资源;
- 公网网络不稳定,出现丢包。如果连续出现丢包,这是的现象就是客户端数据发不出去,服务端也一直收不到数据,就这么一直耗着;
- 应用程序线程阻塞,无法进行数据读写。
问题:
- 假死的连接占用的资源不能自动释放;
- 向假死的连接发送数据,得到的反馈是发送超时。
解决:发送心跳包。