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


問題概述

最近在處理一些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。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM