zoukankan      html  css  js  c++  java
  • NewLife.Net——管道处理器解决粘包

    Tcp网络编程,必须要解决的一个问题就是粘包,尽管解决办法有很多,这里讲一个比较简单的方法。

    老规矩,先上代码:https://github.com/NewLifeX/NewLife.Net

    一、管道处理器

    新建管道处理器项目HandlerTest,源码复制自第一节课的EchoTest项目,增加一个管道处理器类

    class EchoHandler : Handler
    {
        public override Object Read(IHandlerContext context, Object message)
        {
            var session = context.Session;
    
            var pk = message as Packet;
            session.WriteLog("收到:{0}", pk.ToStr());
    
            // 把收到的数据发回去
            session.Send(pk);
    
            return null;
        }
    }

    EchoHandler继承自处理器基类Handler,重载Read方法,当网络层收到数据包时,会调用该方法。

    这里我们实现了Echo功能,并打印日志。返回null告知不再执行管道上的后续处理器。

    既然有了处理器,第一节课中的MyNetServer就用不上啦,在TestServer中改回来标准的NetServer

    // 实例化服务端,指定端口,同时在Tcp/Udp/IPv4/IPv6上监听
    var svr = new NetServer
    {
        Port = 1234,
        Log = XTrace.Log
    };
    svr.Add<EchoHandler>();
    svr.Start();

    这里的svr.Add<EchoHandler>()把上面的处理器给注册进去,大意就是由这个处理器来负责处理收到的网络数据包。

    跑起来服务端和客户端看看效果:

    可以看到,收发正常!

    二、粘包的产生

    真实应用场景中,不可能允许我们间隔1秒才发出一个网络包,直接就不该有等待。连续发送多个数据包,就很容易产生粘包。

    static void TestClient()
    {
        var uri = new NetUri("tcp://127.0.0.1:1234");
        //var uri = new NetUri("tcp://net.newlifex.com:1234");
        var client = uri.CreateRemote();
        client.Log = XTrace.Log;
        client.Received += (s, e) =>
        {
            XTrace.WriteLine("收到:{0}", e.Packet.ToStr());
        };
        client.Open();
    
        // 定时显示性能数据
        _timer = new TimerX(ShowStat, client, 100, 1000);
    
        // 循环发送数据
        for (var i = 0; i < 5; i++)
        {
            //Thread.Sleep(1000);
    
            var str = "你好" + (i + 1);
            client.Send(str);
        }
    
        //client.Dispose();
    }

    这里注释了睡眠语句,让它紧密发出5个数据包。注释后面的Dispose,让其有机会收到响应包。

    跑起来看到,粘包了!!!

    客户端发送5次,服务端作为一个包给接收了,整体处理,然后返回给客户端。

    粘包的解决办法很多,一般是加头部长度或者分隔符,也有取巧的办法直接设置NoDelay。

    从使用上来讲,相对可靠的做法是加头部长度。因为除了多个包粘在一起,还可能出现一个包被拆成两半,分别在前后两个包里面。

    三、普通粘包解法

    我们加上头部长度来解决解包问题。

    修改一下服务端,增加一个处理器

    static void TestServer()
    {
        // 实例化服务端,指定端口,同时在Tcp/Udp/IPv4/IPv6上监听
        var svr = new NetServer
        {
            Port = 1234,
            Log = XTrace.Log
        };
        //svr.Add(new LengthFieldCodec { Size = 4 });
        svr.Add<StandardCodec>();
        svr.Add<EchoHandler>();
    
        // 打开原始数据日志
        var ns = svr.Server;
        ns.LogSend = true;
        ns.LogReceive = true;
    
        svr.Start();
    
        _server = svr;
    
        // 定时显示性能数据
        _timer = new TimerX(ShowStat, svr, 100, 1000);
    }

    StandardCodec处理器是新生命团队标准封包。https://github.com/NewLifeX/X/tree/master/NewLife.Core/Net

    其固定4字节作为头部,其中后面两个字节标识负载长度。

    也可以使用LengthFieldCodec编码器(如上注释部分),并制定头部加4字节作为长度。

    编码器顺序非常重要,网络层收到数据包以后,会从前向后走过每一个处理器;SendAsync/SendMessage发送消息时,会从后向前走过每一个过滤器,逆序。

    客户端也要增加相应过滤器

    static void TestClient()
    {
        var uri = new NetUri("tcp://127.0.0.1:1234");
        //var uri = new NetUri("tcp://net.newlifex.com:1234");
        var client = uri.CreateRemote();
        client.Log = XTrace.Log;
        client.Received += (s, e) =>
        {
            var pk = e.Message as Packet;
            XTrace.WriteLine("收到:{0}", pk.ToStr());
        };
        //client.Add(new LengthFieldCodec { Size = 4 });
        client.Add<StandardCodec>();
    
        // 打开原始数据日志
        var ns = client;
        ns.LogSend = true;
        ns.LogReceive = true;
    
        client.Open();
    
        // 定时显示性能数据
        _timer = new TimerX(ShowStat, client, 100, 1000);
    
        // 循环发送数据
        for (var i = 0; i < 5; i++)
        {
            var str = "你好" + (i + 1);
            var pk = new Packet(str.GetBytes());
            client.SendAsync(pk);
        }
    }

    发送函数改为SendAsync,原来的Send(Packet pk)会绕过管道处理器。

    客户端接收时,e.Message表示经过处理器处理得到的消息,e.Packet表示原始数据包。

    同时,通过LogSend/LogReceive打开收发数据日志。

    上图效果,客户端发出第5个包,头部多了4个字节,其中07-00表示后续负载数据长度为7字节(NewLife)。

    服务端先收到第一个包11字节,然后收到44字节,这是4个包粘在一起。

    然后StandardCodec编码器成功将其拆分成为4个,并依次通过EchoHandler。

    到了客户端这边,也是后面4个粘在一起,并且也得到了正确拆分。

    如果一个大包被拆分为几个,StandardCodec也能缓冲合并,半包超过500~5000ms仍未能组合完整时将抛弃。

    四、总结

    借助管道处理器架构,我们轻易解决了粘包问题!

    显然,管道架构并非单纯为了粘包问题而设计,它有着非常重要的意义,加解密、压缩、各种协议处理,等等。

    管道架构的设计,参考了Netty,因此大部分Netty的编解码器都可以在此使用。

  • 相关阅读:
    Verdi 看波形常用快捷操作
    Tensorflow系列——Saver的用法
    Verilog-分频器
    (原创)列主元Gauss消去法的通用程序
    冒泡排序算法
    ADC 与实际电压值的关系
    直流耦合 交流耦合 耦合
    当前不会命中断点,源代码与原始版本不同
    示波器触发
    在头文件#pragma comment(lib,"glaux.lib");编译器提示waring C4081: 应输入“newline“
  • 原文地址:https://www.cnblogs.com/nnhy/p/newlife_net_handler.html
Copyright © 2011-2022 走看看