Netty进阶-Netty篇

知识点前文请阅读: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)处理,但如果这么做,缺点是包的往返时间越长性能就越差

Netty进阶-Netty篇_第1张图片

  • 为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值

Netty进阶-Netty篇_第2张图片

  •  窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用
    • 图中窗口内的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 配置参数

  • MSS 限制

  • 链路层对一次能够发送的最大数据有限制,这个限制称之为 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

  • Nagle 算法

  • 即使发送一个字节,也需要加入 tcp(20) 头和 ip(20) 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由

  • 该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送

    • 如果 SO_SNDBUF 的数据达到 MSS,则需要发送

    • 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭

    • 如果 TCP_NODELAY = true,则需要发送

    • 已发送的数据都收到 ack 时,则需要发送

    • 上述条件不满足,但发生超时(一般为 200ms)则需要发送

    • 除上述情况,延迟发送

  • 解决方案

  1. 短链接,发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低

  2. 每一条消息采用固定长度,缺点浪费空间

  3. 每一条消息采用分隔符,例如 \n,缺点需要转义

  4. 每一条消息分为 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服务

Netty进阶-Netty篇_第3张图片

编写客户端,并向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

效果

Netty进阶-Netty篇_第4张图片


  • http 协议

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


  • 自定义协议

自定义协议要素

  1. 魔数,用来在第一时间判定是否是无效数据包

  2. 版本号,可以支持协议的升级

  3. 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk

  4. 指令类型,是登录、注册、单聊、群聊... 跟业务相关

  5. 请求序号,为了双工通信,提供异步能力

  6. 正文长度

  7. 消息正文

示例:

比如我们要定义一个聊天的系统,消息的类型是 抽象的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 out) throws Exception {
		// 1. 获取 4 个字节的魔数
		int magicNum = in.readInt();
		// 2. 获取 1 个字节的版本
		byte version = in.readByte();
		// 3. 获取 1 个字节的序列化方式
		byte serializerType = in.readByte();
		// 4. 获取 1 个字节的 指令类型
		byte messageType = in.readByte();
		// 5. 获取 4 个字节的请求序号
		int sequenceId = in.readInt();
		// 跳过填充的 字节
		in.readByte();
		// 6. 获取 4个字节的内容长度
		int length = in.readInt();
		// 7. 获取实际内容
		byte[] bytes = new byte[length];
		in.readBytes(bytes, 0, length);

		// 把字节流中 转成消息对象
		ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
		Message message = (Message) ois.readObject();
		
		log.debug(" === {}, {}, {},{},{},{}", magicNum, version, serializerType, messageType, sequenceId, length);
		log.debug(" === {}", message);

		out.add(message);    // 解析出来的数据要加入到集合中,不然后续的 handler获取不到消息
	}
} 
  

测试:

	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

@Sharable 注解

问:在 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 使用的操作,如果没有,就可以被共享。


ChannelInboundHandlerAdapter、SimpleChannelInboundHandler

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三次握手

  1. 第一次握手,client 发送 SYN 到 server,状态修改为 SYN_SEND,server 收到,状态改变为 SYN_REVD,并将该请求放入 sync queue 半连接队列

  2. 第二次握手,server 回复 SYN + ACK 给 client,client 收到,状态改变为 ESTABLISHED,并发送 ACK 给 server

  3. 第三次握手,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 内核会快速释放此端口

你可能感兴趣的:(netty,netty)