TCP通信过程中遇到粘包拆包解决全过程

网络 通信技术
在使用TCP协议进行通信时,听到最多的也就是粘包和拆包问题。本文就来看看,如何解决粘包和拆包问题。

[[359421]]

 在使用TCP协议进行通信时,听到最多的也就是粘包和拆包问题。本文就来看看,如何解决粘包和拆包问题。

01一 TCP的粘包/拆包的问题以及解决

在解决TCP粘包和拆包,我们先看看一种思想。来看看读取一个Int数据的Demo,体会下这种思想。

1.1 ReplayingDecoder

1. 自定义解码器,从ByteBuf读取一个Int。(重点,一定要看懂这段代码)

  1. public class IntegerHeaderFrameDecoder extends ByteToMessageDecoder { 
  2.     @Override 
  3.     protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) throws Exception { 
  4.       if (buf.readableBytes() < 4) { 
  5.         return
  6.       } 
  7.       buf.markReaderIndex();//标记下当前读指针。 
  8.       int length = buf.readInt();//从ByteBuf读出一个int 
  9.       if (buf.readableBytes() < length) { 
  10.         buf.resetReaderIndex();//恢复到刚才标记的读指针 
  11.         return;  
  12.       } 
  13.       out.add(buf.readBytes(length)); 
  14.     } 

2. 使用ReplayingDecoder进行优化()

  1. public class IntegerHeaderFrameDecoder extends ReplayingDecoder<Void> { 
  2.     protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) throws Exception { 
  3.        out.add(buf.readBytes(buf.readInt())); 
  4.     } 

3. ReplayingDecoder使用说明(重点,要理解的)

  • a 使⽤了特殊的ByteBuf,叫做ReplayingDecoderByteBuf,扩展了ByteBuf
  • b 重写了ByteBuf的readXxx()等⽅法,会先检查可读字节⻓度,⼀旦检测到不满⾜要求就直接抛出REPLAY(REPLAY继承ERROR)
  • c ReplayingDecoder重写了ByteToMessageDecoder的callDecode()⽅法,捕获Signal并在catch块中重置ByteBuf的readerIndex。
  • d 继续等待数据,直到有了数据后继续读取,这样就可以保证读取到需要读取的数据。
  • e 类定义中的泛型S是⼀个⽤于记录解码状态的状态机枚举类,在state(S s)、checkpoint(S s)等⽅法中会⽤到。在简单解码时也可以⽤java.lang.Void来占位。

总结:

ReplayingDecoder是ByteToMessageDecoder的子类,扩展了ByteBuf。从写了readXxx()等⽅法,当前ByteBuf中数据小于代取数据,等待数据满足,才能取数据。就可以省略手动实现这段代码。

4. 注意

  1. 1 buffer的部分操作(readBytes(ByteBuffer dst)、retain()、release()等⽅法会直接抛出异常) 
  2. 2 在某些情况下会影响性能(如多次对同⼀段消息解码) 

继承ReplayingDecoder,错误示例和修改

  1. //这是⼀个错误的例⼦: 
  2. //消息中包含了2个integer,代码中decode⽅法会被调⽤两次,此时队列size不等于2,这段代码达不到期望结果。 
  3. public class MyDecoder extends ReplayingDecoder<Void> { 
  4.     private final Queue<Integervalues = new LinkedList<Integer>(); 
  5.     @Override 
  6.     public void decode(ByteBuf buf, List<Object> out) throws Exception { 
  7.         // A message contains 2 integers. 
  8.         values.offer(buf.readInt()); 
  9.         values.offer(buf.readInt()); 
  10.         assert values.size() == 2; 
  11.         out.add(values.poll() + values.poll()); 
  12.     } 

  1. //正确的做法: 
  2. public class MyDecoder extends ReplayingDecoder<Void> { 
  3.     private final Queue<Integervalues = new LinkedList<Integer>(); 
  4.     @Override 
  5.     public void decode(ByteBuf buf, List<Object> out) throws Exception { 
  6.         // Revert the state of the variable that might have been changed 
  7.         // since the last partial decode. 
  8.         values.clear(); 
  9.         // A message contains 2 integers. 
  10.         values.offer(buf.readInt()); 
  11.         values.offer(buf.readInt()); 
  12.         // Now we know this assertion will never fail. 
  13.         assert values.size() == 2; 
  14.         out.add(values.poll() + values.poll()); 
  15.     } 
  16. }  

ByteToIntegerDecoder2的实现

  1. public class ByteToIntegerDecoder2 extends ReplayingDecoder<Void> { 
  2.     /** 
  3.     * @param ctx 上下⽂ 
  4.     * @param in 输⼊的ByteBuf消息数据 
  5.     * @param out 转化后输出的容器 
  6.     * @throws Exception 
  7.     */ 
  8.     @Override 
  9.     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { 
  10.      out.add(in.readInt()); //读取到int类型数据,放⼊到输出,完成数据类型的转化 
  11.     } 

1.2 拆包和粘包问题重现(客户端向服务端发送十条数据)

1. 客户端启动类

  1. public class NettyClient { 
  2.     public static void main(String[] args) throws Exception{ 
  3.         EventLoopGroup worker = new NioEventLoopGroup(); 
  4.         try { 
  5.             // 服务器启动类 
  6.             Bootstrap bootstrap = new Bootstrap(); 
  7.             bootstrap.group(worker); 
  8.             bootstrap.channel(NioSocketChannel.class); 
  9.             bootstrap.handler(new ChannelInitializer<SocketChannel>() { 
  10.                 @Override 
  11.                 protected void initChannel(SocketChannel ch) throws Exception { 
  12.                      ch.pipeline().addLast(new ClientHandler()); 
  13.                 } 
  14.          }); 
  15.             ChannelFuture future = bootstrap.connect("127.0.0.1", 5566).sync(); 
  16.             future.channel().closeFuture().sync(); 
  17.         } finally { 
  18.          worker.shutdownGracefully(); 
  19.         } 
  20.     } 

2. 客户端ClientHandler

  1. public class ClientHandler extends SimpleChannelInboundHandler<ByteBuf> { 
  2.     private int count
  3.     @Override 
  4.     protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { 
  5.       System.out.println("接收到服务端的消息:" + 
  6.       msg.toString(CharsetUtil.UTF_8)); 
  7.       System.out.println("接收到服务端的消息数量:" + (++count)); 
  8.     } 
  9.     @Override 
  10.     public void channelActive(ChannelHandlerContext ctx) throws Exception { 
  11.       for (int i = 0; i < 10; i++) { 
  12.       ctx.writeAndFlush(Unpooled.copiedBuffer("from client a message!"
  13.       CharsetUtil.UTF_8)); 
  14.       } 
  15.     } 
  16.     @Override 
  17.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 
  18.       cause.printStackTrace(); 
  19.       ctx.close(); 
  20.     } 

3. 服务端NettyServer

  1. public class NettyServer { 
  2.     public static void main(String[] args) throws Exception { 
  3.       // 主线程,不处理任何业务逻辑,只是接收客户的连接请求 
  4.       EventLoopGroup boss = new NioEventLoopGroup(1); 
  5.       // ⼯作线程,线程数默认是:cpu*2 
  6.       EventLoopGroup worker = new NioEventLoopGroup(); 
  7.       try { 
  8.         // 服务器启动类 
  9.         ServerBootstrap serverBootstrap = new ServerBootstrap(); 
  10.         serverBootstrap.group(boss, worker); 
  11.         //配置server通道 
  12.         serverBootstrap.channel(NioServerSocketChannel.class); 
  13.         serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { 
  14.           @Override 
  15.           protected void initChannel(SocketChannel ch) throws Exception { 
  16.             ch.pipeline() 
  17.             .addLast(new ServerHandler()); 
  18.           } 
  19.         }); //worker线程的处理器 
  20.         ChannelFuture future = serverBootstrap.bind(5566).sync(); 
  21.         System.out.println("服务器启动完成。。。。。"); 
  22.         //等待服务端监听端⼝关闭 
  23.         future.channel().closeFuture().sync(); 
  24.       } finally { 
  25.         //优雅关闭 
  26.         boss.shutdownGracefully(); 
  27.         worker.shutdownGracefully(); 
  28.       } 
  29.     } 

4. ServerHandler

  1. public class ServerHandler extends SimpleChannelInboundHandler<ByteBuf> { 
  2.       private int count
  3.       @Override 
  4.       protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { 
  5.           System.out.println("服务端接收到消息:" + 
  6.           msg.toString(CharsetUtil.UTF_8)); 
  7.           System.out.println("服务端接收到消息数量:" + (++count)); 
  8.           ctx.writeAndFlush(Unpooled.copiedBuffer("ok", CharsetUtil.UTF_8)); 
  9.       } 

 

1.3 什么是TCP的粘包和拆包问题

TCP是流传递的,所谓流,就是没有界限的数据。服务端接受客户端数据,并不知道是一条还是多条。服务端在读取数据的时候会出现粘包问题。

因此服务端和客户端进行数据传递的时候,要制定好拆包规则。客户端按照该规则进行粘包,服务端按照该规则拆包。如果有任意违背该规则,服务端就不能拿到预期的数据。

1. 解决思路(三种)

  • 1. 在发送的数据包中添加头,在头⾥存储数据的⼤⼩,服务端就可以按照此⼤⼩来读取数据,这样就知道界限在哪⾥了。
  • 2. 以固定的⻓度发送数据,超出的分多次发送,不⾜的以0填充,接收端就以固定⻓度接收即可。
  • 3. 在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

1.4 实战:解决TCP的粘包/拆包问题

1. 自定义协议

  1. public class MyProtocol { 
  2.     private Integer length; //数据头:⻓度 
  3.     private byte[] body; //数据体 
  4.     public Integer getLength() { 
  5.      return length; 
  6.     } 
  7.     public void setLength(Integer length) { 
  8.      this.length = length; 
  9.     } 
  10.     public byte[] getBody() { 
  11.      return body; 
  12.     } 
  13.     public void setBody(byte[] body) { 
  14.      this.body = body; 
  15.     } 

2. 编码器

  1. public class MyEncoder extends MessageToByteEncoder<MyProtocol> { 
  2.   @Override 
  3.   protected void encode(ChannelHandlerContext ctx, MyProtocol msg, ByteBuf out) throws Exception { 
  4.     out.writeInt(msg.getLength()); 
  5.     out.writeBytes(msg.getBody()); 
  6.   } 

3. 解码器

  1. public class MyDecoder extends ReplayingDecoder<Void> { 
  2.     @Override 
  3.     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { 
  4.         int length = in.readInt(); //获取⻓度 
  5.         byte[] data = new byte[length]; //根据⻓度定义byte数组 
  6.         in.readBytes(data); //读取数据 
  7.         MyProtocol myProtocol = new MyProtocol(); 
  8.         myProtocol.setLength(length); 
  9.         myProtocol.setBody(data); 
  10.         out.add(myProtocol); 
  11.     } 

4. 客户端ClientHandler

  1. public class ClientHandler extends SimpleChannelInboundHandler<MyProtocol> { 
  2.     private int count
  3.     @Override 
  4.     protected void channelRead0(ChannelHandlerContext ctx, MyProtocol msg) throws Exception { 
  5.         System.out.println("接收到服务端的消息:" + new String(msg.getBody(), 
  6.         CharsetUtil.UTF_8)); 
  7.         System.out.println("接收到服务端的消息数量:" + (++count)); 
  8.     } 
  9.     @Override 
  10.     public void channelActive(ChannelHandlerContext ctx) throws Exception { 
  11.         for (int i = 0; i < 10; i++) { 
  12.             byte[] data = "from client a message!".getBytes(CharsetUtil.UTF_8); 
  13.             MyProtocol myProtocol = new MyProtocol(); 
  14.             myProtocol.setLength(data.length); 
  15.             myProtocol.setBody(data); 
  16.             ctx.writeAndFlush(myProtocol); 
  17.         } 
  18.     } 
  19.     @Override 
  20.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)throws Exception { 
  21.       cause.printStackTrace(); 
  22.       ctx.close(); 
  23.     } 

5. NettyClient

  1. public class NettyClient { 
  2.     public static void main(String[] args) throws Exception{ 
  3.         EventLoopGroup worker = new NioEventLoopGroup(); 
  4.         try { 
  5.             // 服务器启动类 
  6.             Bootstrap bootstrap = new Bootstrap(); 
  7.             bootstrap.group(worker); 
  8.             bootstrap.channel(NioSocketChannel.class); 
  9.             bootstrap.handler(new ChannelInitializer<SocketChannel>() { 
  10.               @Override 
  11.               protected void initChannel(SocketChannel ch) throws Exception { 
  12.                 ch.pipeline().addLast(new MyEncoder()); 
  13.                 ch.pipeline().addLast(new MyDecoder()); 
  14.                 ch.pipeline().addLast(new ClientHandler()); 
  15.               } 
  16.             }); 
  17.             ChannelFuture future = bootstrap.connect("127.0.0.1", 5566).sync(); 
  18.             future.channel().closeFuture().sync(); 
  19.         } finally { 
  20.          worker.shutdownGracefully(); 
  21.         } 
  22.     } 

6. ServerHandler

  1. public class ServerHandler extends SimpleChannelInboundHandler<MyProtocol> { 
  2.     private int count
  3.     @Override 
  4.     protected void channelRead0(ChannelHandlerContext ctx, MyProtocol msg) throws Exception { 
  5.         System.out.println("服务端接收到消息:" + new String(msg.getBody(), 
  6.         CharsetUtil.UTF_8)); 
  7.         System.out.println("服务端接收到消息数量:" + (++count)); 
  8.         byte[] data = "ok".getBytes(CharsetUtil.UTF_8); 
  9.         MyProtocol myProtocol = new MyProtocol(); 
  10.         myProtocol.setLength(data.length); 
  11.         myProtocol.setBody(data); 
  12.         ctx.writeAndFlush(myProtocol); 
  13.     } 

7. NettyServer

  1. public class NettyServer { 
  2.     public static void main(String[] args) throws Exception { 
  3.         // 主线程,不处理任何业务逻辑,只是接收客户的连接请求 
  4.         EventLoopGroup boss = new NioEventLoopGroup(1); 
  5.         // ⼯作线程,线程数默认是:cpu*2 
  6.         EventLoopGroup worker = new NioEventLoopGroup(); 
  7.         try { 
  8.             // 服务器启动类 
  9.             ServerBootstrap serverBootstrap = new ServerBootstrap(); 
  10.             serverBootstrap.group(boss, worker); 
  11.             //配置server通道 
  12.             serverBootstrap.channel(NioServerSocketChannel.class); 
  13.             serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() { 
  14.               @Override 
  15.               protected void initChannel(SocketChannel ch) throws Exception { 
  16.                   ch.pipeline() 
  17.                   .addLast(new MyDecoder()) 
  18.                   .addLast(new MyEncoder()) 
  19.                   .addLast(new ServerHandler()); 
  20.               } 
  21.             }); //worker线程的处理器 
  22.             ChannelFuture future = serverBootstrap.bind(5566).sync(); 
  23.             System.out.println("服务器启动完成。。。。。"); 
  24.             //等待服务端监听端⼝关闭 
  25.             future.channel().closeFuture().sync(); 
  26.         } finally { 
  27.             //优雅关闭 
  28.             boss.shutdownGracefully(); 
  29.             worker.shutdownGracefully(); 
  30.         } 
  31.     } 

8. 测试

02二 Netty核心源码解析

2.1 服务端启动过程刨析

1. 创建服务端Channel

  1. 1 ServerBootstrap对象的bind()⽅法,也是⼊⼝⽅法 
  2. 2 AbstractBootstrap中的initAndRegister()进⾏创建Channel 
  3.      创建Channel的⼯作由ReflectiveChannelFactory反射类中的newChannel()⽅法完成。 
  4. 3 NioServerSocketChannel中的构造⽅法中,通过jdk nio底层的SelectorProvider打开ServerSocketChannel。 
  5. 4 在AbstractNioChannel的构造⽅法中,设置channel为⾮阻塞:ch.configureBlocking(false); 
  6. 5 通过的AbstractChannel的构造⽅法,创建了id、unsafe、pipeline内容。 
  7. 6 通过NioServerSocketChannelConfig获取tcp底层的⼀些参数 

2. 初始化服务端Channel

  1. 1 AbstractBootstrap中的initAndRegister()进⾏初始化channel,代码:init(channel); 
  2.  
  3. 2 在ServerBootstrap中的init()⽅法设置channelOptions以及Attributes。 
  4.  
  5. 3 紧接着,将⽤户⾃定义参数、属性保存到局部变量currentChildOptions、currentChildAttrs,以 
  6.   供后⾯使⽤ 
  7.  
  8. 4 如果设置了serverBootstrap.handler()的话,会加⼊到pipeline中。 
  9.  
  10. 5 添加连接器ServerBootstrapAcceptor,有新连接加⼊后,将⾃定义的childHandler加⼊到连接的 
  11.   pipeline中: 

  1. ch.eventLoop().execute(new Runnable() { 
  2.     @Override 
  3.     public void run() { 
  4.       pipeline.addLast( 
  5.         new ServerBootstrapAcceptor(ch, currentChildGroup,currentChildHandler, currentChildOptions, currentChildAttrs)); 
  6.     } 
  7. }); 

  1. @Override 
  2. @SuppressWarnings("unchecked"
  3. public void channelRead(ChannelHandlerContext ctx, Object msg) {  
  4.     //当客户端有连接时才会执⾏ 
  5.     final Channel child = (Channel) msg; 
  6.     //将⾃定义的childHandler加⼊到连接的pipeline中 
  7.     child.pipeline().addLast(childHandler);  
  8.     setChannelOptions(child, childOptions, logger); 
  9.     setAttributes(child, childAttrs); 
  10.     try { 
  11.         childGroup.register(child).addListener(new ChannelFutureListener(){ 
  12.             @Override 
  13.             public void operationComplete(ChannelFuture future) throws Exception { 
  14.                 if (!future.isSuccess()) { 
  15.                  forceClose(child, future.cause()); 
  16.                 } 
  17.             } 
  18.         }); 
  19.     } catch (Throwable t) { 
  20.      forceClose(child, t); 
  21.     } 

3. 注册selector

  1. //进⾏注册 
  2. 1 initAndRegister()⽅法中的ChannelFuture regFuture = config().group().register(channel);  
  3.  
  4. 2 在io.netty.channel.AbstractChannel.AbstractUnsafe#register()中完成实际的注册 
  5.     2.1 AbstractChannel.this.eventLoop = eventLoop; 进⾏eventLoop的赋值操作,后续的IO事件 
  6.         ⼯作将在由该eventLoop执⾏。 
  7.      2.2 调⽤register0(promise)中的doRegister()进⾏实际的注册 
  8.      
  9. 3 io.netty.channel.nio.AbstractNioChannel#doRegister进⾏了⽅法实现 

  1. //通过jdk底层进⾏注册多路复⽤器 
  2. //javaChannel() --前⾯创建的channel 
  3. //eventLoop().unwrappedSelector() -- 获取selector 
  4. //注册感兴趣的事件为0,表明没有感兴趣的事件,后⾯会进⾏重新注册事件 
  5. //将this对象以attachment的形式注册到selector,⽅便后⾯拿到当前对象的内容 
  6. selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); 

4. 绑定端口

  1. 1 ⼊⼝在io.netty.bootstrap.AbstractBootstrap#doBind0(),启动⼀个线程进⾏执⾏绑定端⼝操作 
  2.  
  3. 2 调⽤io.netty.channel.AbstractChannelHandlerContext#bind(java.net.SocketAddress, 
  4.   io.netty.channel.ChannelPromise)⽅法,再次启动线程执⾏ 
  5.    
  6. 3 最终调⽤io.netty.channel.socket.nio.NioServerSocketChannel#doBind()⽅法进⾏绑定操作 

  1. //通过jdk底层的channel进⾏绑定 
  2. @SuppressJava6Requirement(reason = "Usage guarded by java version check"
  3.     @Override 
  4.     protected void doBind(SocketAddress localAddress) throws Exception { 
  5.       if (PlatformDependent.javaVersion() >= 7) { 
  6.          javaChannel().bind(localAddress, config.getBacklog()); 
  7.       } else { 
  8.           javaChannel().socket().bind(localAddress, 
  9.           config.getBacklog()); 
  10.     } 

什么时候进⾏更新selector的主从事件?最终在io.netty.channel.nio.AbstractNioChannel#doBeginRead()⽅法中完成的

  1. protected void doBeginRead() throws Exception { 
  2.     // Channel.read() or ChannelHandlerContext.read() was called 
  3.     final SelectionKey selectionKey = this.selectionKey; 
  4.     if (!selectionKey.isValid()) { 
  5.      return
  6.     } 
  7.     readPending = true
  8.     final int interestOps = selectionKey.interestOps(); 
  9.     if ((interestOps & readInterestOp) == 0) { 
  10.         selectionKey.interestOps(interestOps | readInterestOp); //设置 
  11.         感兴趣的事件为OP_ACCEPT 
  12.     } 
  13. //在NioServerSocketChannel的构造⽅法中进⾏了赋值 
  14. public NioServerSocketChannel(ServerSocketChannel channel) { 
  15.       super(null, channel, SelectionKey.OP_ACCEPT); 
  16.       config = new NioServerSocketChannelConfig(this, 
  17.       javaChannel().socket()); 

2.2 连接请求过程源码刨析

1. 新连接接入

  1. 入口在 
  2. io.netty.channel.nio.NioEventLoop#processSelectedKey(java.nio.channels.SelectionKey, 
  3. io.netty.channel.nio.AbstractNioChannel)中 
  4.     进⼊NioMessageUnsafe的read()⽅法 
  5.      
  6.     调⽤io.netty.channel.socket.nio.NioServerSocketChannel#doReadMessages() ⽅法,创建 
  7.     jdk底层的channel,封装成NioSocketChannel添加到List容器中 

  1. @Override 
  2. protected int doReadMessages(List<Object> buf) throws Exception { 
  3.     SocketChannel ch = SocketUtils.accept(javaChannel()); 
  4.     try { 
  5.         if (ch != null) { 
  6.           buf.add(new NioSocketChannel(this, ch)); 
  7.           return 1; 
  8.         } 
  9.     } catch (Throwable t) { 
  10.         logger.warn("Failed to create a new channel from an 
  11.         accepted socket.", t); 
  12.         try { 
  13.          ch.close(); 
  14.         } catch (Throwable t2) { 
  15.             logger.warn("Failed to close a socket.", t2); 
  16.         } 
  17.     } 
  18.     return 0; 

  1. 创建NioSocketChannel对象 
  2. new NioSocketChannel(this, ch),通过new的⽅式进⾏创建 
  3.     调⽤super的构造⽅法 
  4.         传⼊SelectionKey.OP_READ事件标识 
  5.         创建id、unsafe、pipeline对象 
  6.         设置⾮阻塞 ch.configureBlocking(false); 
  7.     创建NioSocketChannelConfig对象 

2. 注册读事件

  1. 在io.netty.channel.nio.AbstractNioMessageChannel.NioMessageUnsafe中的: 
  2. for (int i = 0; i < size; i ++) { 
  3.     readPending = false
  4.     pipeline.fireChannelRead(readBuf.get(i)); //传播读事件 

  1.  在io.netty.channel.AbstractChannelHandlerContext#invokeChannelRead(java.lang.Object)⽅ 
  2.  法中 
  3. private void invokeChannelRead(Object msg) { 
  4.     if (invokeHandler()) { 
  5.       try { 
  6.           //执⾏channelRead,需要注意的是,第⼀次执⾏是HeadHandler,第⼆次是 
  7.           ServerBootstrapAcceptor 
  8.           //通过ServerBootstrapAcceptor进⼊和 新连接接⼊的 注册selector相同的 
  9.           逻辑进⾏注册以及事件绑定 
  10.           ((ChannelInboundHandler) handler()).channelRead(this, msg); 
  11.       } catch (Throwable t) { 
  12.          invokeExceptionCaught(t); 
  13.       } 
  14.     } else { 
  15.      fireChannelRead(msg); 
  16.     } 

03三 使用Netty优化点

在使用Netty,一些简单的建议点。值得看看。

3.1 零拷贝

  • 1 Bytebuf使⽤⽤池化的DirectBuffer类型,不需要进⾏字节缓冲区的⼆次拷⻉。如果使⽤堆内存,JVM会先拷⻉到堆内,再写⼊Socket,就多了⼀次拷⻉。
  • 2 CompositeByteBuf将多个ByteBuf封装成⼀个ByteBuf,在添加ByteBuf时不需要进程拷⻉。
  • 3 Netty的⽂件传输类DefaultFileRegion的transferTo⽅法将⽂件发送到⽬标channel中,不需要进⾏循环拷⻉,提升了性能。

3.2 EventLoop的任务调度

  1. channel.eventLoop().execute(new Runnable() { 
  2.     @Override 
  3.     public void run() { 
  4.      channel.writeAndFlush(data) 
  5.     } 
  6. }); 

而不是使用hannel.writeAndFlush(data);EventLoop的任务调度直接放入到channel所对应的EventLoop的执行队列,后者会导致线程切换。备注:在writeAndFlush的底层,如果没有通过eventLoop执行的话,就会启动新的线程。

3.3 减少ChannelPipline的调⽤⻓度

  1. public class YourHandler extends ChannelInboundHandlerAdapter { 
  2.     @Override 
  3.     public void channelActive(ChannelHandlerContext ctx) { 
  4.     //msg从整个ChannelPipline中⾛⼀遍,所有的handler都要经过。 
  5.     ctx.channel().writeAndFlush(msg); 
  6.     //从当前handler⼀直到pipline的尾部,调⽤更短。 
  7.     ctx.writeAndFlush(msg); 
  8.     } 

3.4 减少ChannelHandler的创建(基本上不会配置)

如果channelhandler是⽆状态的(即不需要保存任何状态参数),那么使⽤Sharable注解,并在 bootstrap时只创建⼀个实例,减少GC。否则每次连接都会new出handler对象。

  1. @ChannelHandler.Shareable 
  2. public class StatelessHandler extends ChannelInboundHandlerAdapter { 
  3.     @Override 
  4.     public void channelActive(ChannelHandlerContext ctx) {} 
  5. public class MyInitializer extends ChannelInitializer<Channel> { 
  6.     private static final ChannelHandler INSTANCE = new StatelessHandler(); 
  7.     @Override 
  8.     public void initChannel(Channel ch) { 
  9.      ch.pipeline().addLast(INSTANCE); 
  10.     } 

注意:

ByteToMessageDecoder之类的编解码器是有状态的,不能使⽤Sharable注解。

3.5 配置参数的设置

  1. 服务端的bossGroup只需要设置为1即可,因为ServerSocketChannel在初始化阶段,只会 
  2. 注册到某⼀个eventLoop上,⽽这个eventLoop只会有⼀个线程在运⾏,所以没有必要设置为 
  3. 多线程。⽽ IO 线程,为了充分利⽤ CPU,同时考虑减少线上下⽂切换的开销,通常workGroup 
  4. 设置为CPU核数的两倍,这也是Netty提供的默认值。 
  5.  
  6. 在对于响应时间有⾼要求的场景,使⽤.childOption(ChannelOption.TCP_NODELAY, true
  7. 和.option(ChannelOption.TCP_NODELAY, true)来禁⽤nagle算法,不等待,⽴即发送。 

Netty相关的知识点也算是分享完毕了。后续我仍然分享Netty相关内容。主要是在工作中遇到问题,和新的感悟。

 

责任编辑:姜华 来源: 花花和Java
相关推荐

2019-10-17 11:06:32

TCP粘包通信协议

2020-01-06 15:23:41

NettyTCP粘包

2021-03-09 22:30:47

TCP拆包协议

2021-07-15 10:35:16

NettyTCPJava

2022-04-28 08:38:09

TCP协议解码器

2011-02-22 10:46:02

Samba配置

2009-12-08 17:56:16

WCF配置

2011-04-18 15:56:10

软件测试

2011-09-06 15:38:20

QT安装

2011-01-21 17:51:52

2009-04-13 12:37:18

2009-04-23 10:04:55

2010-03-01 17:01:03

Python编程技巧

2012-11-06 10:19:18

Java自定义加载Java类

2009-06-10 16:55:42

cygwin netb安装

2010-06-17 13:10:09

Linux Grub修

2010-11-19 10:11:49

Oracle物化视图

2010-03-10 13:24:45

Zend Debugg

2011-03-11 10:39:02

YUM安装LAMP

2010-06-12 10:03:20

Ubuntu Grub
点赞
收藏

51CTO技术栈公众号