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<NioSocketChannel>() {
				@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<NioSocketChannel>() {
				@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 配置参数

  • 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<NioSocketChannel>() {
				@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<NioSocketChannel>() {
				@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<NioSocketChannel>() {
				@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<NioSocketChannel>() {
				@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<NioSocketChannel>() {
				@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<NioSocketChannel>() {
				@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<NioSocketChannel>() {
				@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<NioSocketChannel>() {
				@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<SocketChannel>() {
						@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

效果


  • 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<SocketChannel>() {
				@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<HttpRequest>() {
						@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<Message> {

	/**
	 * 自定义消息协议 编码
	 * 消息出站时会调用
	 */
	@Override
	protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
		// 1. 4个字节的魔数,这里随便定义
		out.writeBytes(new byte[]{1, 2, 3, 4});
		// 2. 1个字节的版本
		out.writeByte(1);
		// 3. 1个字节的序列化方式,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<Object> 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

handlerAddedhandler 添加时触发此事件
channelRegisteredchannel 注册时触发此事件
channelActivechannel 连接成功时 活跃
channelReadchannel 可读消息
channelReadCompletechannel 消息读取完成后触发
channelInactivechannel 断开连接时 不活跃
channelUnregisteredchannel 注销时触发此事件
handlerRemovedhandler 移除时触发此事件
exceptionCaught发生异常时触发
channelRead0channel 关注的可读类型消息
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<NioSocketChannel>() {
				@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<NioSocketChannel>() {
				@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 内核会快速释放此端口

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

又逢乱世

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值