知识点前文请阅读:Netty入门
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
// 设置服务器端接收缓冲区大小为 10 字节
serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
/**
* 会在连接 connect 建立成功后,会触发 channelActive 事件
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 向服务器端发送 5 次数据,每次 发送16个字节
for (int i = 0; i < 5; i++) {
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});
ctx.writeAndFlush(buf);
}
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
15:23:38 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x8fc90ea3, L:/127.0.0.1:8080 - R:/127.0.0.1:53151] REGISTERED
15:23:38 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x8fc90ea3, L:/127.0.0.1:8080 - R:/127.0.0.1:53151] ACTIVE
15:23:38 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x8fc90ea3, L:/127.0.0.1:8080 - R:/127.0.0.1:53151] READ: 36B // 可以看到这里接收到 36 字节,客户端每次发送 16 个字节,服务器端这里一次接收了36字节,36 > 16 出现了粘包现象,16:16:4 最后4这里出现了半包现象
+-------------------------------------------------+
| 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 |................|
|00000010| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000020| 00 01 02 03 |.... |
+--------+-------------------------------------------------+----------------+
15:23:38 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x8fc90ea3, L:/127.0.0.1:8080 - R:/127.0.0.1:53151] READ COMPLETE
15:23:38 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x8fc90ea3, L:/127.0.0.1:8080 - R:/127.0.0.1:53151] READ: 40B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000010| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000020| 04 05 06 07 08 09 0a 0b |........ |
+--------+-------------------------------------------------+----------------+
15:23:38 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x8fc90ea3, L:/127.0.0.1:8080 - R:/127.0.0.1:53151] READ COMPLETE
15:23:38 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x8fc90ea3, L:/127.0.0.1:8080 - R:/127.0.0.1:53151] READ: 4B // 半包
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 0c 0d 0e 0f |.... |
+--------+-------------------------------------------------+----------------+
15:23:38 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x8fc90ea3, L:/127.0.0.1:8080 - R:/127.0.0.1:53151] READ COMPLETE
看 READ 类型,可以看到这里接收到 36 字节,客户端每次发送 16 个字节,服务器端这里一次接收了36字节,36 > 16 出现了粘包现象,16:16:4 最后4这里出现了半包现象
现象,发送 abc def,接收 abcdef
原因
应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
Nagle 算法:会造成粘包
现象,发送 abcdef,接收 abc def
原因
应用层:接收方 ByteBuf 小于实际发送数据量
滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
本质是因为 TCP 是流式协议,消息无边界,需要开发人员找出消息的边界
- TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差
- 为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值
- 窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用
- 图中窗口内的4段(1-1000、1001-2000、2001-3000、3001-4000)数据才允许被发送,当应答未到达前,窗口必须停止滑动
- 如果 1-1000 这个段的数据 ack 回来了,窗口就可以向前滑动,就可以发送 4001-5000 段的数据
- 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收
// 调整服务器端 滑动窗口大小,一般不用设置,在建立连接后系统自动设置,netty默认的是1024
serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
// 调整客户端 滑动窗口大小,一般不用设置,在建立连接后系统自动设置,netty默认的是1024
bootstrap.option(ChannelOption.SO_SNDBUF, 10);
// 调整 netty 的接收缓冲区大小(ByteBuf) AdaptiveRecvByteBufAllocator(最小值, 初始值, 最大值)
serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16));
/ 插播知识点 /
option:全局配置,给ServerSocketChannel 配置参数
childOption:单个 channel 连接的配置,给SocketChannel 配置参数
链路层对一次能够发送的最大数据有限制,这个限制称之为 MTU(maximum transmission unit),不同的链路设备的 MTU 值也有所不同,例如
以太网的 MTU 是 1500
FDDI(光纤分布式数据接口)的 MTU 是 4352
本地回环地址的 MTU 是 65535 - 本地测试不走网卡
MSS 是最大段长度(maximum segment size),它是 MTU 刨去 tcp 头和 ip 头后剩余能够作为数据传输的字节数
ipv4 tcp 头占用 20 bytes,ip 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460
TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送
MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS
即使发送一个字节,也需要加入 tcp(20) 头和 ip(20) 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由
该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送
如果 SO_SNDBUF 的数据达到 MSS,则需要发送
如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭
如果 TCP_NODELAY = true,则需要发送
已发送的数据都收到 ack 时,则需要发送
上述条件不满足,但发生超时(一般为 200ms)则需要发送
除上述情况,延迟发送
短链接,发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低
每一条消息采用固定长度,缺点浪费空间
每一条消息采用分隔符,例如 \n,缺点需要转义
每一条消息分为 head 和 body,head 中包含 body 的长度
- 解决方案一:短链接
服务器端
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
// 设置服务器端接收缓冲区大小为 10 字节
// serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
客户端
public static void main(String[] args) throws InterruptedException {
// 发送5次
for (int i = 0; i < 5; i++) {
send();
}
}
private static void send() {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
/**
* 会在连接 connect 建立成功后,会触发 channelActive 事件
*/
@Override
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});
ctx.writeAndFlush(buf);
// 发送一次消息后,就关闭 channel 通道,关闭连接
ctx.channel().close();
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
服务器端打印
16:37:52 [DEBUG] [nioEventLoopGroup-3-8] i.n.h.l.LoggingHandler - [id: 0xffa06125, L:/127.0.0.1:8080 - R:/127.0.0.1:51412] REGISTERED
16:37:52 [DEBUG] [nioEventLoopGroup-3-8] i.n.h.l.LoggingHandler - [id: 0xffa06125, L:/127.0.0.1:8080 - R:/127.0.0.1:51412] ACTIVE
16:37:52 [DEBUG] [nioEventLoopGroup-3-8] i.n.h.l.LoggingHandler - [id: 0xffa06125, L:/127.0.0.1:8080 - R:/127.0.0.1:51412] 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 |................|
+--------+-------------------------------------------------+----------------+
16:37:52 [DEBUG] [nioEventLoopGroup-3-8] i.n.h.l.LoggingHandler - [id: 0xffa06125, L:/127.0.0.1:8080 - R:/127.0.0.1:51412] READ COMPLETE
16:37:52 [DEBUG] [nioEventLoopGroup-3-8] i.n.h.l.LoggingHandler - [id: 0xffa06125, L:/127.0.0.1:8080 - R:/127.0.0.1:51412] READ COMPLETE
16:37:52 [DEBUG] [nioEventLoopGroup-3-8] i.n.h.l.LoggingHandler - [id: 0xffa06125, L:/127.0.0.1:8080 ! R:/127.0.0.1:51412] INACTIVE
16:37:52 [DEBUG] [nioEventLoopGroup-3-8] i.n.h.l.LoggingHandler - [id: 0xffa06125, L:/127.0.0.1:8080 ! R:/127.0.0.1:51412] UNREGISTERED
16:37:52 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x3f21d899, L:/127.0.0.1:8080 - R:/127.0.0.1:51429] REGISTERED
16:37:52 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x3f21d899, L:/127.0.0.1:8080 - R:/127.0.0.1:51429] ACTIVE
16:37:52 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x3f21d899, L:/127.0.0.1:8080 - R:/127.0.0.1:51429] 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 |................|
+--------+-------------------------------------------------+----------------+
16:37:52 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x3f21d899, L:/127.0.0.1:8080 - R:/127.0.0.1:51429] READ COMPLETE
16:37:52 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x3f21d899, L:/127.0.0.1:8080 - R:/127.0.0.1:51429] READ COMPLETE
16:37:52 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x3f21d899, L:/127.0.0.1:8080 ! R:/127.0.0.1:51429] INACTIVE
16:37:52 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x3f21d899, L:/127.0.0.1:8080 ! R:/127.0.0.1:51429] UNREGISTERED
......
可以看到可以数据没有粘在一起了,解决了粘包问题
但是短链接不能解决半包问题
测试短链接半包现象代码改造,调整服务器端netty接收缓冲区大小为16个字节,netty最小16个字节,因为取16的倍数(serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16));)------- 客户端一次发送超过16个字节的数据即可。这里就不再重复贴相似的代码了
- 解决方案二:固定长度
FixedLengthFrameDecoder:netty 提供的定长解码器,构造方法参数:和客户端约定传递消息的长度
服务器端
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// FixedLengthFrameDecoder:netty 提供的定长解码器,构造方法参数:和客户端约定传递消息的长度
ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
客户端
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
/**
* 会在连接 connect 建立成功后,会触发 channelActive 事件
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = ctx.alloc().buffer();
char c = '0';
for (int i = 0; i < 5; i++) {
byte[] bytes = fill10Bytes(c, new Random().nextInt(10) + 1);
c++;
buf.writeBytes(bytes);
}
ctx.writeAndFlush(buf);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
/**
* 发送的数据,字节长度不够时,补够字节长度
*/
public static byte[] fill10Bytes(char c, int len) {
byte[] bytes = new byte[10];
Arrays.fill(bytes, (byte) '_');
for (int i = 0; i < len; i++) {
bytes[i] = (byte) c;
}
System.out.println(new String(bytes));
return bytes;
}
服务器端打印
17:22:44 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x565dcfb4, L:/127.0.0.1:8080 - R:/127.0.0.1:53524] REGISTERED
17:22:44 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x565dcfb4, L:/127.0.0.1:8080 - R:/127.0.0.1:53524] ACTIVE
17:22:44 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x565dcfb4, L:/127.0.0.1:8080 - R:/127.0.0.1:53524] READ: 10B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 30 30 30 30 30 30 30 5f 5f 5f |0000000___ |
+--------+-------------------------------------------------+----------------+
17:22:44 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x565dcfb4, L:/127.0.0.1:8080 - R:/127.0.0.1:53524] READ: 10B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 31 31 31 5f 5f 5f 5f 5f 5f 5f |111_______ |
+--------+-------------------------------------------------+----------------+
17:22:44 [DEBUG] [nioEventLoopGroup-3-3] i.n.h.l.LoggingHandler - [id: 0x565dcfb4, L:/127.0.0.1:8080 - R:/127.0.0.1:53524] READ: 10B
......
- 解决方案三:分隔符
- LineBasedFrameDecoder:netty 提供的支持 \n 和 \r\n 符号的解码器,构造参数传遇到换行符的最大长度,当读了指定长度的字节时,还没有遇到换行符,将抛出异常
- DelimiterBasedFrameDecoder:netty 提供的支持 自定义符号的解码器,构造参数传符号的最大长度,和ByteBuf类型的自定义符号
服务器端
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, ByteBufAllocator.DEFAULT.buffer().writeBytes("|".getBytes(StandardCharsets.UTF_8))));
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
客户端
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
/**
* 会在连接 connect 建立成功后,会触发 channelActive 事件
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = ctx.alloc().buffer();
//buf.writeBytes("hello\nworld\nhi\nzhang san\n".getBytes(StandardCharsets.UTF_8));
buf.writeBytes("hello|world|hi|zhang san|".getBytes(StandardCharsets.UTF_8));
ctx.writeAndFlush(buf);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
服务器端打印
17:44:53 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x4cd3ff09, L:/127.0.0.1:8080 - R:/127.0.0.1:53948] REGISTERED
17:44:53 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x4cd3ff09, L:/127.0.0.1:8080 - R:/127.0.0.1:53948] ACTIVE
17:44:53 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x4cd3ff09, L:/127.0.0.1:8080 - R:/127.0.0.1:53948] READ: 5B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f |hello |
+--------+-------------------------------------------------+----------------+
17:44:53 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x4cd3ff09, L:/127.0.0.1:8080 - R:/127.0.0.1:53948] READ: 5B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 77 6f 72 6c 64 |world |
+--------+-------------------------------------------------+----------------+
17:44:53 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x4cd3ff09, L:/127.0.0.1:8080 - R:/127.0.0.1:53948] READ: 2B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 69 |hi |
+--------+-------------------------------------------------+----------------+
17:44:53 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x4cd3ff09, L:/127.0.0.1:8080 - R:/127.0.0.1:53948] READ: 9B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 7a 68 61 6e 67 20 73 61 6e |zhang san |
+--------+-------------------------------------------------+----------------+
17:44:53 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x4cd3ff09, L:/127.0.0.1:8080 - R:/127.0.0.1:53948] READ COMPLETE
- 解决方案四:预设长度 LTC解码器
LengthFieldBasedFrameDecoder:构造参数传(最大长度,记录长度偏移量,长度占用字节数,长度调整,剥离字节数)
服务器端
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 参数一:最大字节数,
// 参数二:长度在buf中的偏移量,因为我在buf中先写的是长度,所以长度的偏移量是0,如果先写入2个字节的数据,再写入长度,那么长度的偏移量就是2
// 参数三:长度占用的字节,我写的是一个int类型,占用4个字节
// 参数四:需要调整的长度
// 参数五:数据从头开始需要剥离的长度,现在buf中的数据是 ....hello, world 和 ....hi!,前面的4个点是长度,我们不需要长度,所以需要把长度剥离出去
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
客户端
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = ctx.alloc().buffer();
msg(buf, "hello, world");
msg(buf, "hi!");
ctx.writeAndFlush(buf);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
/**
* // 4 个字节的长度,然后是实际内容
*/
private static void msg(ByteBuf buf, String content) {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8); // 实际内容
int length = bytes.length; // 实际内容长度
buf.writeInt(length); // int的长度是4个字节,把长度写入
buf.writeBytes(bytes);
}
服务器端打印
18:36:43 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x21c2c017, L:/127.0.0.1:8080 - R:/127.0.0.1:59190] REGISTERED
18:36:43 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x21c2c017, L:/127.0.0.1:8080 - R:/127.0.0.1:59190] ACTIVE
18:36:43 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x21c2c017, L:/127.0.0.1:8080 - R:/127.0.0.1:59190] READ: 12B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 |hello, world |
+--------+-------------------------------------------------+----------------+
18:36:43 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x21c2c017, L:/127.0.0.1:8080 - R:/127.0.0.1:59190] READ: 3B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 69 21 |hi! |
+--------+-------------------------------------------------+----------------+
18:36:43 [DEBUG] [nioEventLoopGroup-3-1] i.n.h.l.LoggingHandler - [id: 0x21c2c017, L:/127.0.0.1:8080 - R:/127.0.0.1:59190] READ COMPLETE
加深 LengthFieldBasedFrameDecoder 解码器的理解,使用 EmbeddedChannel 工具类调试
public static void main(String[] args) {
EmbeddedChannel embeddedChannel = new EmbeddedChannel(
// 参数一:最大字节数,
// 参数二:长度在buf中的偏移量,因为我在buf中先写的是长度,所以长度的偏移量是0,如果先写入2个字节的数据,再写入长度,那么长度的偏移量就是2
// 参数三:长度占用的字节,我写的是一个int类型,占用4个字节
// 参数四:需要调整的长度,现在除了内容以外,在buf中又加了1个字节长度的版本号,所以需要调整的长度是1
// 参数五:数据从头开始需要剥离的长度,现在buf中的数据是 .....hello, world 和 .....hi!,前面的4个点是长度,第5个点是版本号,我们不需要长度,所以需要把长度剥离出去,最后的数据是 .hello, world 和 .hi!
new LengthFieldBasedFrameDecoder(1024, 0, 4, 1, 4)
, new LoggingHandler(LogLevel.DEBUG)
);
// 4 个字节的长度,然后是实际内容
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
msg(buf, "hello, world");
msg(buf, "hi!");
embeddedChannel.writeInbound(buf);
}
private static void msg(ByteBuf buf, String content) {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8); // 实际内容
int length = bytes.length; // 实际内容长度
// buf.writeBytes(new byte[]{1,2}); // 加入在长度之前多写入了两个字节的数据,那么LengthFieldBasedFrameDecoder的第二个参数就是2
buf.writeInt(length); // int的长度是4个字节,把长度写入
buf.writeByte(1); // 传入其他数据,例如版本号
buf.writeBytes(bytes);
}
打印
18:50:42 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] REGISTERED
18:50:42 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] ACTIVE
18:50:42 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 13B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 |.hello, world |
+--------+-------------------------------------------------+----------------+
18:50:42 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 4B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 68 69 21 |.hi! |
+--------+-------------------------------------------------+----------------+
18:50:42 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE
TCP/IP 中消息传输基于流的方式,没有边界。
协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则
例如:在网络上传输
下雨天留客天留我不留
是中文一句著名的无标点符号句子,在没有标点符号情况下,这句话有数种拆解方式,而意思却是完全不同,所以常被用作讲述标点符号的重要性。
下雨天留客,天留,我不留
下雨天,留客天,留我不?留
如何设计协议呢?其实就是给网络传输的信息加上“标点符号”。但通过分隔符来断句不是很好,因为分隔符本身如果用于传输,那么必须加以区分。因此,下面一种协议较为常用
定长字节表示内容长度 + 实际内容
以 redis 为例 学习协议
比如我们要向redis中存入一个键值对
命令:set key value
示例:set name zhangsan
redis 协议以*号开头,要使用的一条命令比喻成一个以空格分隔的数组,set命令的数组长度是3,那么要告诉redis我要发送的数组长度是3个(*3),然后写入内容,数组第一个元素set的长度是3 ($3),第一个元素的实际内容是set (set),数组第二个元素的长度是4 ($4),第二个元素的实际内容是name (name),数组第三个元素的长度是8 ($8),数组第三个元素的实际内容是zhangsan (zhangsan),内容之间要以回车换行进行分隔,组合起来就是以下内容
*3
$3
set
$4
name
$8
zhangsan
redis 示例
启动一个redis服务
编写客户端,并向redis中set一个键值对
public static void main(String[] args) throws InterruptedException {
final byte[] LINE = {13, 10}; // 回车换行
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
/**
* 连接成功后,执行此事件
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// redis 写
ByteBuf buf = ctx.alloc().buffer();
buf.writeBytes("*3".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
buf.writeBytes("set".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
buf.writeBytes("$4".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
buf.writeBytes("name".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
buf.writeBytes("$8".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
buf.writeBytes("zhangsan".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
ctx.writeAndFlush(buf);
// redis 读
/*buf.writeBytes("*2".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
buf.writeBytes("get".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
buf.writeBytes("$4".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
buf.writeBytes("name".getBytes(StandardCharsets.UTF_8));
buf.writeBytes(LINE);
ctx.writeAndFlush(buf);*/
}
/**
* channel 中有消息过来时,此事件读
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
String s = buf.toString(CharsetUtil.UTF_8);
System.err.println("返回消息:" + s);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 6379)).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
打印
11:48:28 [DEBUG] [nioEventLoopGroup-2-1] i.n.h.l.LoggingHandler - [id: 0xfc444831] REGISTERED
11:48:28 [DEBUG] [nioEventLoopGroup-2-1] i.n.h.l.LoggingHandler - [id: 0xfc444831] CONNECT: localhost/127.0.0.1:6379
11:48:28 [DEBUG] [nioEventLoopGroup-2-1] i.n.h.l.LoggingHandler - [id: 0xfc444831, L:/127.0.0.1:62599 - R:localhost/127.0.0.1:6379] ACTIVE
11:48:28 [DEBUG] [nioEventLoopGroup-2-1] i.n.h.l.LoggingHandler - [id: 0xfc444831, L:/127.0.0.1:62599 - R:localhost/127.0.0.1:6379] WRITE: 37B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 34 0d |*3..$3..set..$4.|
|00000010| 0a 6e 61 6d 65 0d 0a 24 38 0d 0a 7a 68 61 6e 67 |.name..$8..zhang|
|00000020| 73 61 6e 0d 0a |san.. |
+--------+-------------------------------------------------+----------------+
11:48:28 [DEBUG] [nioEventLoopGroup-2-1] i.n.h.l.LoggingHandler - [id: 0xfc444831, L:/127.0.0.1:62599 - R:localhost/127.0.0.1:6379] FLUSH
11:48:28 [DEBUG] [nioEventLoopGroup-2-1] i.n.h.l.LoggingHandler - [id: 0xfc444831, L:/127.0.0.1:62599 - R:localhost/127.0.0.1:6379] READ: 5B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 2b 4f 4b 0d 0a |+OK.. |
+--------+-------------------------------------------------+----------------+
11:48:28 [DEBUG] [nioEventLoopGroup-2-1] i.n.h.l.LoggingHandler - [id: 0xfc444831, L:/127.0.0.1:62599 - R:localhost/127.0.0.1:6379] READ COMPLETE
返回消息:+OK
效果
HttpServerCodec:netty 提供的 http 协议,像上面redis协议一样,如果是我们自己写一套http协议的话,工作量复杂度都还是挺高的。所以netty提供了http协议的编解码handler。此handler是一个组合的即包含入站HttpRequestDecoder,也包含出站HttpResponseEncoder的handler。
服务器端
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
// netty 提供的 http 协议的处理器
ch.pipeline().addLast(new HttpServerCodec());
/*
* 消息经过 http 协议处理器解码后,轮到我们来使用消息,处理相应的业务
* SimpleChannelInboundHandler:如果只对特定类型的消息感兴趣,可以使用此handler,泛型传感兴趣的类型,只有符合类型的消息,才会被此handler处理
* ChannelInboundHandlerAdapter:处理所有类型的消息
*/
ch.pipeline().addLast(new SimpleChannelInboundHandler() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
log.debug("获取请求头:{}", msg.uri());
/*
* 返回响应
* 第一个参数:协议版本
* 第二个参数:http响应状态码
*/
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
byte[] bytes = "Hello, world!".getBytes();
// 响应内容
response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
response.content().writeBytes(bytes);
// 写回响应
ctx.writeAndFlush(response);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
/ 插播知识点 /
SimpleChannelInboundHandler:如果只对特定类型的消息感兴趣,可以使用此handler,泛型传感兴趣的类型,只有符合类型的消息,才会被此handler处理
客户端
使用浏览器访问:http://localhost:8080
自定义协议要素
魔数,用来在第一时间判定是否是无效数据包
版本号,可以支持协议的升级
序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk
指令类型,是登录、注册、单聊、群聊... 跟业务相关
请求序号,为了双工通信,提供异步能力
正文长度
消息正文
示例:
比如我们要定义一个聊天的系统,消息的类型是 抽象的message,不同的消息类型有不同的实现,例如 登录、注册
Message.java 消息体
@Data
public abstract class Message implements Serializable {
/**
* 请求序号
*/
private int sequenceId;
/**
* 消息类型
*/
private int messageType;
public abstract int getMessageType();
public static final int LoginRequestMessage = 0; // 登录的请求消息
public static final int LoginResponseMessage = 1; // 登录的响应消息
}
LoginRequestMessage.java 消息体的具体实现
@Data
@ToString(callSuper = true)
public class LoginRequestMessage extends Message {
/**
* 登录账号
*/
private String username;
/**
* 登录密码
*/
private String password;
public LoginRequestMessage() {
}
public LoginRequestMessage(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public int getMessageType() {
return LoginRequestMessage;
}
}
MessageCodec.java 自定义消息协议---重点
@Slf4j
public class MessageCodec extends ByteToMessageCodec {
/**
* 自定义消息协议 编码
* 消息出站时会调用
*/
@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个字节的序列化方式,0:jdk,1:json
out.writeByte(0);
// 4. 1个字节的指令类型
out.writeByte(msg.getMessageType());
// 5. 4个字节的请求序号
out.writeInt(msg.getSequenceId());
// 对齐 2的n次方倍,填充
out.writeByte(0xff);
// 6.0 获取内容的字节数组
ByteArrayOutputStream bos = new ByteArrayOutputStream(); // 拿到最终的结果
ObjectOutputStream oos = new ObjectOutputStream(bos); // 可以把对象转成字节数组
oos.writeObject(msg); // 将 msg 消息对象,写入 oos, oos 又写入 bos
byte[] bytes = bos.toByteArray();
// 6.1 4个字节的内容长度
out.writeInt(bytes.length);
// 7. 写入内容
out.writeBytes(bytes);
}
/**
* 自定义消息协议 解码
* 消息入站时,会调用
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
测试:
public static void main(String[] args) throws Exception {
EmbeddedChannel channel = new EmbeddedChannel(
// 自定义协议也会出现 粘包、半包问题,所以需要使用 LengthFieldBasedFrameDecoder 处理粘包半包
/*
* 参数说明
* * * * 第一个参数:消息的最大字节数
* * * * 第二个参数:长度在buf中的偏移量,我们看MessageCodec中的消息体中长度之前有多少个字节,长度之前有 4个字节的魔数 1个字节的版本号等 一共12个字节,所以偏移量是12
* * * * 第三个参数:长度占用的字节,我写的是一个int类型,占用4个字节
* * * * 第四个参数:需要调整的长度,因为我们的数据是要走自己的编解码的,这里的数据都是有用的,所以不需要调整
* * * * 第五个参数:数据从开头需要剥离的长度,这里也不需要剥离
*/
new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0)
, new LoggingHandler(LogLevel.DEBUG)
, new MessageCodec());
// 编码 encode
LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123");
channel.writeOutbound(message); // out 出站,出站时要把消息编码
// 解码 decode
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null, message, buf);
channel.writeInbound(buf); // in 入站,入站时要把网络消息进行解码
}
打印:
18:32:02 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] REGISTERED
18:32:02 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] ACTIVE
18:32:02 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] WRITE: 220B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 01 00 00 00 00 00 00 ff 00 00 00 cc |................|
|00000010| ac ed 00 05 73 72 00 2c 63 6f 6d 2e 6c 69 78 78 |....sr.,com.lixx|
|00000020| 2e 64 65 6d 6f 2e 69 6d 2e 6d 65 73 73 61 67 65 |.demo.im.message|
|00000030| 2e 4c 6f 67 69 6e 52 65 71 75 65 73 74 4d 65 73 |.LoginRequestMes|
|00000040| 73 61 67 65 b7 2b d2 02 a3 cf 85 f0 02 00 02 4c |sage.+.........L|
|00000050| 00 08 70 61 73 73 77 6f 72 64 74 00 12 4c 6a 61 |..passwordt..Lja|
|00000060| 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 4c |va/lang/String;L|
|00000070| 00 08 75 73 65 72 6e 61 6d 65 71 00 7e 00 01 78 |..usernameq.~..x|
|00000080| 72 00 18 63 6f 6d 2e 6c 69 78 78 2e 64 65 6d 6f |r..com.lixx.demo|
|00000090| 2e 69 6d 2e 4d 65 73 73 61 67 65 d0 81 67 8f 04 |.im.Message..g..|
|000000a0| 8f b0 3b 02 00 02 49 00 0b 6d 65 73 73 61 67 65 |..;...I..message|
|000000b0| 54 79 70 65 49 00 0a 73 65 71 75 65 6e 63 65 49 |TypeI..sequenceI|
|000000c0| 64 78 70 00 00 00 00 00 00 00 00 74 00 03 31 32 |dxp........t..12|
|000000d0| 33 74 00 08 7a 68 61 6e 67 73 61 6e |3t..zhangsan |
+--------+-------------------------------------------------+----------------+
18:32:02 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] FLUSH
18:32:02 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ: 220B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 01 00 00 00 00 00 00 ff 00 00 00 cc |................|
|00000010| ac ed 00 05 73 72 00 2c 63 6f 6d 2e 6c 69 78 78 |....sr.,com.lixx|
|00000020| 2e 64 65 6d 6f 2e 69 6d 2e 6d 65 73 73 61 67 65 |.demo.im.message|
|00000030| 2e 4c 6f 67 69 6e 52 65 71 75 65 73 74 4d 65 73 |.LoginRequestMes|
|00000040| 73 61 67 65 b7 2b d2 02 a3 cf 85 f0 02 00 02 4c |sage.+.........L|
|00000050| 00 08 70 61 73 73 77 6f 72 64 74 00 12 4c 6a 61 |..passwordt..Lja|
|00000060| 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 4c |va/lang/String;L|
|00000070| 00 08 75 73 65 72 6e 61 6d 65 71 00 7e 00 01 78 |..usernameq.~..x|
|00000080| 72 00 18 63 6f 6d 2e 6c 69 78 78 2e 64 65 6d 6f |r..com.lixx.demo|
|00000090| 2e 69 6d 2e 4d 65 73 73 61 67 65 d0 81 67 8f 04 |.im.Message..g..|
|000000a0| 8f b0 3b 02 00 02 49 00 0b 6d 65 73 73 61 67 65 |..;...I..message|
|000000b0| 54 79 70 65 49 00 0a 73 65 71 75 65 6e 63 65 49 |TypeI..sequenceI|
|000000c0| 64 78 70 00 00 00 00 00 00 00 00 74 00 03 31 32 |dxp........t..12|
|000000d0| 33 74 00 08 7a 68 61 6e 67 73 61 6e |3t..zhangsan |
+--------+-------------------------------------------------+----------------+
18:32:02 [DEBUG] [main] c.l.d.i.MessageCodec - === 16909060, 1, 0,0,0,204
18:32:02 [DEBUG] [main] c.l.d.i.MessageCodec - === LoginRequestMessage(super=Message(sequenceId=0, messageType=0), username=zhangsan, password=123)
18:32:02 [DEBUG] [main] i.n.h.l.LoggingHandler - [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE
问:在 netty 的 pipeline 链中有很多 netty 提供的 handler,这些 handler 是否只需要只创建一个对象供其它 channel 共享,如下:
LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
......
ch.pipeline().addLast(LOGGING_HANDLER);
答:取决于 netty 提供的 handler 类中是否有注解 @Sharable,有注解的就可以只定义一个对象,没有注解的不可以共享,防止 handler 中 channel 的数据和其它 eventLoop 中的 channel 数据混乱出现多线程情况下结果不是正确值的线程安全问题。
我们自定义的 handler 是否可以被共享,要分析自定义 handler 是否有上一次未解读完的消息被下一次事件 channel 使用的操作,如果没有,就可以被共享。
handlerAdded | handler 添加时触发此事件 |
channelRegistered | channel 注册时触发此事件 |
channelActive | channel 连接成功时 活跃 |
channelRead | channel 可读消息 |
channelReadComplete | channel 消息读取完成后触发 |
channelInactive | channel 断开连接时 不活跃 |
channelUnregistered | channel 注销时触发此事件 |
handlerRemoved | handler 移除时触发此事件 |
exceptionCaught | 发生异常时触发 |
channelRead0 | channel 关注的可读类型消息 |
userEventTriggered | 触发特殊事件,配合 IdleStateHandler 一起使用 |
原因
网络设备出现故障,例如网卡,机房等,底层的 TCP 连接已经断开了,但应用程序没有感知到,仍然占用着资源。
公网网络不稳定,出现丢包。如果连续出现丢包,这时现象就是客户端数据发不出去,服务端也一直收不到数据,就这么一直耗着
应用程序线程阻塞,无法进行数据读写
问题
假死的连接占用的资源不能自动释放
向假死的连接发送数据,得到的反馈是发送超时
服务器端
怎么判断客户端连接是否假死呢?如果能收到客户端数据,说明没有假死。因此策略就可以定为,每隔一段时间就检查这段时间内是否接收到客户端数据,没有就可以判定为连接假死
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss, worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
/*
* 用来判断 读空闲时间和写空闲时间过长
* 第一个参数:读空闲时间是否超过指定的秒,超过时间没有收到 channel 的数据,会触发 IdleState#READER_IDLE 事件
* 第二个参数:写空闲时间是否超过指定的秒
* 第三个参数:读和写空闲时间是否超过指定的秒
* 读写时间,写时间一般是 读 时间的 2分之一左右
*/
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0));
// ChannelDuplexHandler:可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
// 用来触发特殊事件,配合 IdleStateHandler 一起使用
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) { // 触发了读空闲事件
log.debug("已经 5s 没有读到数据了");
}
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
客户端
客户端可以定时向服务器端发送数据,只要这个时间间隔小于服务器定义的空闲检测的时间间隔
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
/*
* 用来判断 读空闲时间和写空闲时间过长
* 第一个参数:读空闲时间是否超过指定的秒
* 第二个参数:写空闲时间是否超过指定的秒,超过时间没有向 channel 写数据,会触发 IdleState#WRITER_IDLE 事件
* 第三个参数:读和写空闲时间是否超过指定的秒
* 读写时间,写时间一般是 读 时间的 2分之一左右
*/
ch.pipeline().addLast(new IdleStateHandler(0, 3, 0));
// ChannelDuplexHandler:可以同时作为入站和出站处理器
ch.pipeline().addLast(new ChannelDuplexHandler() {
// 用来触发特殊事件,配合 IdleStateHandler 一起使用
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.WRITER_IDLE) { // 触发了写空闲事件
log.debug("已经 3s 没有写数据了, 发送心跳检测包");
// 发送心跳包
ByteBuf buf = ctx.alloc().buffer();
buf.writeByte(1);
ctx.writeAndFlush(buf);
}
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
}
}
new ServerBootstrap().option(); // 是给 ServerSocketChannel (服务器端的通道)配置参数
new ServerBootstrap().childOption(); // 是给 SocketChannel (客户端的通道)配置参数
CONNECT_TIMEOUT_MILLIS:指定的时间未连接上服务器抛异常,单位毫秒
/* * 指定的时间未连接上服务器抛异常,默认值30秒,单位毫秒 * 1:里面是一个定时任务,定时时间到了后会抛出ConnectTimeOutException异常,使用 promise 通知主线程连接超时了(netty 中线程之间的通信使用的是 promise) */ Bootstrap bootstrap = new Bootstrap(); bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000); ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync(); // 2. 主线程的 promise 在这里阻塞着等待 连接结果 channelFuture.channel().closeFuture().sync();
SO_TIMEOUT:主要用在阻塞 IO,阻塞 IO 中 accept、read 等都是无限等待的,如果不希望永远阻塞,使用它调整超时时间。在 NIO 中用不到
SO_BACKLOG:学习这个参数之前要先回顾一下著名的TCP三次握手
第一次握手,client 发送 SYN 到 server,状态修改为 SYN_SEND,server 收到,状态改变为 SYN_REVD,并将该请求放入 sync queue 半连接队列
第二次握手,server 回复 SYN + ACK 给 client,client 收到,状态改变为 ESTABLISHED,并发送 ACK 给 server
第三次握手,server 收到 ACK,状态改变为 ESTABLISHED,将该请求从 sync queue 放入 accept queue 全连接队列
其中
在 linux 2.2 之前,backlog 大小包括了两个队列的大小,在 2.2 之后,分别用下面两个参数来控制
sync queue - 半连接队列
大小通过 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,在
syncookies
启用的情况下,逻辑上没有最大值限制,这个设置便被忽略accept queue - 全连接队列
其大小通过 /proc/sys/net/core/somaxconn 指定,在使用 listen 函数时,内核会根据传入的 backlog 参数与系统参数,取二者的较小值
如果 accpet queue 队列满了,server 将发送一个拒绝连接的错误信息到 client
我们一般要在机器硬件能力强的情况下把机器的队列调的足够大,代码中通过SO_BACKLOG参数,来实际控制全连接队列的值。需要注意的是队列是在accept有堆积的情况下才会放在队列中。
new ServerBootstrap() .group(new NioEventLoopGroup()) .channel(NioServerSocketChannel.class) // Windows 平台默认值200,其他平台默认值128 .option(ChannelOption.SO_BACKLOG, 1024) .bind(8080).sync().channel().closeFuture().sync();
ulimit -n 数量 :允许同一个进程可以允许同时打开的文件描述符的数量,操作系统的参数
TCP_NODELAY:是否开启 Nagle 算法,false 默认值启用,true 禁用。推荐禁用
new ServerBootstrap() .group(new NioEventLoopGroup()) .channel(NioServerSocketChannel.class) // 是否开启 Nagle 算法,false 默认值启用,true 禁用。推荐禁用 .childOption(ChannelOption.TCP_NODELAY, true) .bind(8080).sync().channel().closeFuture().sync();
SO_SNDBUF & SO_RCVBUF 这两个值建议使用系统默认
SO_SNDBUF:调整客户端 滑动窗口大小,在建立连接后系统自动设置,netty默认的是1024,属于 SocketChannal 参数
SO_RCVBUF:服务器端接收缓冲区大小,单位字节。既可用于 SocketChannal 参数,也可以用于 ServerSocketChannal 参数(建议设置到 ServerSocketChannal 上)
RCVBUF_ALLOCATOR:控制 netty 接收缓冲区大小,负责入站数据的分配,决定入站缓冲区的大小(并可动态调整),统一采用 direct 直接内存,具体池化还是非池化由 allocator 决定
SO_REUSEADDR:快速复用端口,客户端断开连接后,端口会被linux内核占据一段时间,不能马上再次分配该端口,设置此配置为 true 内核会快速释放此端口