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的編解碼器都可以在此使用。