問題概述
最近在處理一些TCP客戶端的項目,服務端是C語言開發的socket. 實際項目開始的時候使用默認的阻塞模式並未發現異常。代碼如下

1 public class SocketService 2 { 3 public delegate void TcpEventHandler1(byte[] receivebody, int length); 4 public event TcpEventHandler1 OnGetCS; 5 Socket client = null; 6 IPEndPoint endPoint = null; 7 public SocketService(string ip, int port) 8 { 9 client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 10 //client.Blocking = false;默認是阻塞模式 11 endPoint = new IPEndPoint(IPAddress.Parse(ip), port); 12 IsRcv = true; 13 } 14 15 Thread rthr = null;//異步線程用於接收數據 16 17 /// <summary> 18 /// 表示是否繼續接收數據 19 /// </summary> 20 public bool IsRcv { get; set; } 21 /// <summary> 22 /// 打開連接 23 /// </summary> 24 /// <returns></returns> 25 public bool Open() 26 { 27 if (client != null && endPoint != null) 28 { 29 try 30 { 31 client.Connect(endPoint); 32 Console.WriteLine("連接成功"); 33 34 //啟動異步監聽 35 rthr = new Thread(ReceiveMsg); 36 rthr.IsBackground = true; 37 rthr.SetApartmentState(ApartmentState.STA); 38 rthr.Start(); 39 return true; 40 } 41 catch 42 { 43 AbortThread(); 44 Console.WriteLine("連接失敗!"); 45 } 46 } 47 return false; 48 } 49 50 /// <summary> 51 /// 關閉接收數據線程 52 /// </summary> 53 private void AbortThread() 54 { 55 if (rthr != null) 56 { 57 rthr.Abort(); 58 } 59 } 60 61 /// <summary> 62 /// 關閉連接 63 /// </summary> 64 public void Close() 65 { 66 if (client.Connected) 67 { 68 client.Close(); 69 } 70 } 71 72 /// <summary> 73 /// 接收數據 74 /// </summary> 75 private void ReceiveMsg() 76 { 77 byte[] arrMsg = new byte[1024 * 1024]; 78 try 79 { 80 while (IsRcv) 81 { 82 int length = client.Receive(arrMsg);//阻塞模式,此次線程會停止繼續執行,直到socket內核有數據 83 byte type; 84 if (length > 0) 85 OnGetCS(arrMsg, length); //出發數據接收事件 86 } 87 } 88 catch (Exception ex) 89 { 90 rthr.Abort(); 91 client.Close(); 92 client = null; 93 Console.WriteLine("服務器斷開連接"); 94 } 95 } 96 }
當客戶運行久后就發現 從服務器端發過來的數據到處理完成整個環節消耗的時間比較多(比同行慢)。
使用TCP 監聽助手,和客戶端程序在OnGetCS處打印出時間比較分析,發現TCP助手顯示收到的時間會比客戶端程序顯示的快500-800MS左右。
.也就是說服務器已經吧數據發送到客戶端TCP緩沖區了,只是客戶端 int length = client.Receive(arrMsg); 並么有及時獲得相應。
查了很多資料都沒有查到有類似的問題。最后我用C#模擬做了一個TCP服務端與自己的TCP客戶端之間通信,則完全沒有延遲。
因此只能考慮語言特性的差別了。C#畢竟封裝了很多信息。這個時候再查看TCP監聽助手對比服務器是C的和C#的發現 C服務器在指令標記位沒有PSH標記位,而C#的則有這個標記位,如下圖(此處C#作為服務器的有興趣的可以自己去試)
查詢網絡上的一段解釋如下
PSH 的作用
TCP 模塊什么時候將數據發送出去(從發送緩沖區中取數據),以及 read 函數什么時候將數據從接收緩沖區讀取都是未知的。
如果使用 PSH 標志,上面這件事就確認下來了:
- 發送端
對於發送方來說,由 TCP 模塊自行決定,何時將接收緩沖區中的數據打包成 TCP 報文,並加上 PSH 標志(在圖 1 中,為了演示,我們假設人為的干涉了 PSH 標志位)。一般來說,每一次 write,都會將這一次的數據打包成一個或多個 TCP 報文段(如果數據量大於 MSS 的話,就會被打包成多個 TCP 段),並將最后一個 TCP 報文段標記為 PSH。
當然上面說的只是一般的情況,如果發送緩沖區滿了,TCP 同樣會將發送緩沖區中的所有數據打包發送。
- 接收端
如果接收方接收到了某個 TCP 報文段包含了 PSH 標志,則立即將緩沖區中的所有數據推送給應用進程(read 函數返回)。
當然有時候接收緩沖區滿了,也會推送。
通過這個解釋瞬間總算是明白了,早期C開發的很多TCP通信,都是不帶PSH標記位的,后來的產品很多都遵守這個模式了,然后我們的C#默認就是使用PSH標記位。 因此就導致了數據接收延遲500-800MS(根據PSH的解釋這個延遲具體多久是未知的)。
解決方案
最簡單的是服務器端增加這個標記位發送過來。一番討論后,人家寫這個服務器的人都已經離職了,沒人會處理。那么客戶是上帝,只能我們這邊來處理了。這里就要用到非阻止模式的socket了。
首先我在網上查到很多人說異步就是非阻止模式。這個完全是錯誤的。異步同步與阻止模式是沒有關系的兩個概念。 當阻塞模式下有一個線程不斷在等待緩沖區把數據交給它處理,異步的話就是觸發回調方法,同步的話就繼續執行同步的業務代碼。
而非阻塞模式的邏輯是,客戶端的連接,讀取數據線程都不會被阻塞,也就是會立即返回。比如連接的邏輯是客戶端發起connect連接,因為TCP連接有幾次握手的情況,需要一定的時間,然而非阻塞要求立即返回,這個時候系統會拋一個異常(Win32Excetion)。
我們則只需要在異常里處理這個TCP連接需要一定時間的問題。可以循環讀取TCP連接狀態來確認是否連接成功。client.Poll 方法來查詢當前連接狀態。同理讀取的時候也是在該異常里循環讀取。

1 public class SocketService 2 { 3 public delegate void TcpEventHandler1(byte[] receivebody, int length); 4 public event TcpEventHandler1 OnGetCS; 5 Socket client = null; 6 IPEndPoint endPoint = null; 7 public SocketService(string ip, int port) 8 { 9 client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 10 client.Blocking = false;//非阻塞模式,定時循環讀取緩沖區的數據把它拼接到緩沖區數據隊列 arrMsg 11 endPoint = new IPEndPoint(IPAddress.Parse(ip), port); 12 } 13 14 Thread rthr = null; 15 /// <summary> 16 /// 表示是否繼續接收數據 17 /// </summary> 18 public bool IsRcv { get; set; } 19 /// <summary> 20 /// 非阻塞模式 21 /// </summary> 22 /// <param name="timeout"></param> 23 /// <returns></returns> 24 public bool Open(int timeout = 1000) 25 { 26 bool connected = false; 27 if (client != null && endPoint != null) 28 { 29 try 30 { 31 client.Connect(endPoint);//此處不會阻塞,如果是正在連接服務器的話,則會跑出win32Excetion異常(這里如果是netcore在linux上的話,怎么也會拋出異常,具體異常自行查閱) 32 Console.WriteLine("連接成功"); 33 //啟動異步監聽 34 connected = true; 35 } 36 catch (Win32Exception ex) 37 { 38 if (ex.ErrorCode == 10035) // WSAEWOULDBLOCK is expected, means connect is in progress 39 { 40 var dt = DateTime.Now; 41 while (true)//循環讀取當前連接的狀態,如果timeout時間內還沒連接成功,則反饋連接失敗。 42 { 43 if (dt.AddMilliseconds(timeout) < DateTime.Now) 44 { 45 break; 46 } 47 connected = client.Poll(1000000, SelectMode.SelectWrite);//不會阻塞 48 if (connected) 49 { 50 connected = true; 51 break; 52 } 53 } 54 } 55 } 56 catch (Exception ex) 57 { 58 AbortThread(); 59 Console.WriteLine("連接失敗"); 60 } 61 } 62 if (connected) 63 { 64 StartReceive();//連接成功則啟動數據讀取線程 65 } 66 return connected; 67 } 68 69 private void StartReceive() 70 { 71 rthr = new Thread(ReceiveMsgNonBlock); 72 rthr.IsBackground = true; 73 rthr.SetApartmentState(ApartmentState.STA); //設置通信線程通信線程同步設置,才能在打開接受文件時 打開 文件選擇框 74 rthr.Start(); 75 } 76 77 private void AbortThread() 78 { 79 if (rthr != null) 80 { 81 rthr.Abort(); 82 } 83 } 84 85 public void Close() 86 { 87 if (client.Connected) 88 { 89 client.Close(); 90 } 91 } 92 93 /// <summary> 94 /// app端緩沖池 95 /// </summary> 96 byte[] arrMsg = new byte[1024 * 1024]; 97 /// <summary> 98 /// 當前緩沖池的長度 99 /// </summary> 100 int currentlength = 0; 101 102 /// <summary> 103 /// 讀取TCP緩沖數據 104 /// </summary> 105 private void ReceiveMsgNonBlock() 106 { 107 while (true) 108 { 109 try 110 { 111 byte[] tempBytes = new byte[1024 * 1024]; 112 113 int length = client.Receive(tempBytes);//此處不會阻塞,如果有數據則繼續,如果沒有數據則拋出Win32Exception異常(linux 下netcore自行查找異常類型 ) 114 115 DealMsg(tempBytes, length); 116 } 117 catch (Win32Exception ex) 118 { 119 120 if (ex.ErrorCode == 10035) // WSAEWOULDBLOCK is expected, means connect is in progress 121 { 122 Thread.Sleep(50); 123 } 124 125 } 126 catch (Exception ex) 127 { 128 rthr.Abort(); 129 client.Close(); 130 client = null; 131 Console.WriteLine("服務器斷開連接"); 132 break; 133 } 134 } 135 } 136 137 /// <summary> 138 /// 把當前讀取到的數據添加到app,並且根據自己的TCP約定的規則分析包頭包尾長度校驗等等信息,來確認在arrMsg中獲取自己想要的數據包最后交給OnGetCS事件 139 /// </summary> 140 /// <param name="bytes"></param> 141 /// <param name="length"></param> 142 public void DealMsg(byte[] bytes, int length) 143 { 144 //先把數據拷貝到 全局數組arrMsg 145 if (bytes.Length + this.currentlength > 1024 * 1024) 146 { 147 byte[] arrMsg = new byte[1024 * 1024]; 148 } 149 150 Array.Copy(bytes, 0, arrMsg, this.currentlength, length); 151 this.currentlength += length; 152 153 154 ///根據自己的包頭包尾的規則來截取TCP數據包,因為實際運行當中要考慮到服務端發送特別大的數據包,以及服務器太忙的時候分段發送數據包的情況。因此不能盲目的以為讀取的緩沖區的數據就是一個完成的數據包。 155 ///最終生成tmpMsg。 156 var tmpMsg = new byte[1000]; 157 OnGetCS(tmpMsg, tmpMsg.Length); 158 } 159 }
經過測試,通過循環主動去讀取緩沖帶完美的解決了客戶端緩慢的問題,實際運行的時候讀取緩沖區的時間間隔可以根據需求自行更改,本例中用了50ms。