• Netty学习二 TCP粘包拆包以及Netty解决TCP粘包拆包


    1、TCP粘包拆包

    操作系统

    我们都知道,操作系统的核心是内核,独立于普通的应用程序之外,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
    为了保护用户进程不能直接访问操作内核,保证内核的安全,操作系统划分为两部分,一部分为内核空间,另一部分为用户空间。

    I/O模型

    为了操作系统的安全考虑,进程是无法直接访问I/O设备的,必须通过系统调用请求内核来完成I/O操作,而内核会为每个I/O设备维护一个缓冲区(buffer)

    整个请求过程为:

    1、用户进程发起请求,内核接受到请求之后,从I/O设备获取数据到buffer中
    2、再将buffer中的数据copy到用户进程的地址空间
    3、该用户进程获取到数据之后再响应客户端

    粘包和拆包

    同样的,操作系统通过TCP协议发送数据的时候,也会先将数据存放在缓冲区中,假设缓冲区的大小为1024个字节

    粘包

    如果发送的数据包比较小,远小于缓冲区的大小,TCP会将多个数据包合并为一个数据包发送,这就发生了粘包

    拆包

    如果发送的数据包比较大,远大于缓冲区的大小,TCP会将数据包拆分为多个数据包发送,这就发生了拆包

    • 服务端分两次读到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
    • 服务端一次接收到了两个数据包,D1和D2粘在一起,发生了粘包
    • 服务端分两次读到了两个数据包,一次读到了完整的D1包和D2的部分包D2_1,第二次读到了D2剩下的包D2_2
    • 服务端分两次读到了两个数据包,一次读到了D1的部分分D1_1,第二次读到了D1的剩下的包D1_2和完整的D2包

    2、TCP粘包拆包解决

    对于TCP粘包拆包问题,有以下4种解决办法 > (1) 消息定长,例如每个数据包的大小都为128字节,如果不够,空位补空格 > (2) 客户端在每个包的末尾使用固定的分隔符,例如 ,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的 ,然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包 > (3) 将消息分为头部和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息 > (4) 通过自定义协议进行粘包和拆包的处理

    3、Netty粘包拆包示例

    客户端向服务端发送3条消息,服务端在收到消息之后也回复客户端3条消息

    客户端

    public class HelloWorldClient {
    
    	private int port;
    	private String address;
    
    	public HelloWorldClient(int port, String address) {
    		this.port = port;
    		this.address = address;
    	}
    
    	public void start() {
    		EventLoopGroup group = new NioEventLoopGroup();
    
    		Bootstrap bootstrap = new Bootstrap();
    		bootstrap.group(group).channel(NioSocketChannel.class)
    				.handler(new ChannelInitializer<SocketChannel>(){
    
    					@Override
    					protected void initChannel(SocketChannel socketChannel) throws Exception {
    						ChannelPipeline pipeline = socketChannel.pipeline();
    						
    						pipeline.addLast("decoder", new StringDecoder());// 字符串解码和编码
    						pipeline.addLast("encoder", new StringEncoder());
    						pipeline.addLast("handler", new ChannelInboundHandlerAdapter(){
    							
    							@Override
    							public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    								System.out.println("客户端收到消息:[" + msg + "]");
    							}
    						});//自定义handler
    					}
    				});
    
    		try {
    			ChannelFuture future = bootstrap.connect(address,port).sync();
    			Channel channel = future.channel();
    			channel.writeAndFlush("我是客户端,地址:" + channel.remoteAddress());
    			channel.closeFuture().sync();
    		} catch (Exception e) {
    			e.printStackTrace();
    		} finally {
    			group.shutdownGracefully();
    		}
    	}
    
    	public static void main(String[] args) {
    		HelloWorldClient client = new HelloWorldClient(8888, "127.0.0.1");
    		client.start();
    	}
    }
    

    服务端

    public class HelloWorldServer {
    
    	private int port;
    	
    	public HelloWorldServer(int port) {
    		this.port = port;
    	}
    	
    	public void start(){
    		EventLoopGroup bossGroup = new NioEventLoopGroup();//创建父子线程组
    		EventLoopGroup workGroup = new NioEventLoopGroup();
    		
    		ServerBootstrap server = new ServerBootstrap();
    		server.group(bossGroup, workGroup)
    			  .channel(NioServerSocketChannel.class)//指定处理客户端的通道
    			  .childHandler(new ChannelInitializer<SocketChannel>(){
    
    				@Override
    				protected void initChannel(SocketChannel socketChannel) throws Exception {
    					ChannelPipeline pipeline = socketChannel.pipeline();
    					
    					pipeline.addLast("decoder", new StringDecoder());// 字符串解码和编码
    					pipeline.addLast("encoder", new StringEncoder());
    					pipeline.addLast("handler", new ChannelInboundHandlerAdapter(){
    						
    						@Override
    						public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    							System.out.println("服务端收到消息:[" + msg + "]");
    							ctx.writeAndFlush(Unpooled.copiedBuffer("我是服务端".getBytes()));
    							ctx.writeAndFlush(Unpooled.copiedBuffer("我是服务端".getBytes()));
    							ctx.writeAndFlush(Unpooled.copiedBuffer("我是服务端".getBytes()));
    						}
    					});//自定义handler
    				}
    			  });//通道初始化
    		try {
    			ChannelFuture future = server.bind(port).sync();
    			future.channel().closeFuture().sync();
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally{
    			bossGroup.shutdownGracefully();
    			workGroup.shutdownGracefully();
    		}
    	}
    
    	public static void main(String[] args) {
    		HelloWorldServer server = new HelloWorldServer(8888);
    		server.start();
    	}
    }
    

    运行结果:
    服务端

    客户端

    可以看到,客户端收到的3条消息都粘在一起了

    4、Netty解决粘包拆包

    4.1、采用定长解决

    客户端添加FixedLengthFrameDecoder消息定长解码器,服务端不用处理

    运行结果

    可以看到,客户端收到的消息确实是按字节数来分割的,但是由于一条消息的长度超过10个字节,所以在遇到中文字符时,会发生乱码,这也是定长分隔符的不足之处

    4.2、采用分隔符

    客户端修改initChannel方法,自定义 $ 为分隔符

    **服务端在消息的末尾加上自定义分隔符 $ **

    那么客户端收到的消息就是单独的消息了,没有粘包

  • 相关阅读:
    C# 实现 JAVA AES加密解密(转他人)
    转 Java、C#双语版配套AES加解密示例
    开发工具资料
    WebApi返回Json格式
    【转】NuGet.org 无法访问的解决方法
    使用Senparc.Weixin.WxOpen开发高可用的微信小程序
    WebApi资料
    WinForm资料
    Winform开发框架之终极应用
    Winform开发中另一种样式的OutLookBar工具条
  • 原文地址:https://www.cnblogs.com/lmj612/p/10976194.html
Copyright © 2020-2023  润新知