NewLife.Net——管道处理器解决粘包
Tcp网络编程,必须要解决的一个问题就是粘包,尽管解决办法有很多,这里讲一个比较简单的方法。
老规矩,先上代码:https://github.com/nnhy/NewLife.Net.Tests
一、管道处理器
新建管道处理器项目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的编解码器都可以在此使用。
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法