C# socket 阻止模式與非阻止模式應用例項

yibey發表於2021-01-27

問題概述

最近在處理一些TCP客戶端的專案,服務端是C語言開發的socket. 實際專案開始的時候使用預設的阻塞模式並未發現異常。程式碼如下

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 方法來查詢當前連線狀態。同理讀取的時候也是在該異常裡迴圈讀取。

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;//非阻塞模式,定時迴圈讀取緩衝區的資料把它拼接到緩衝區資料佇列 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。

 

相關文章