.NET TCP、UDP、Socket、WebSocket

唐宋元明清2188發表於2024-07-25

做.NET應用開發肯定會用到網路通訊,而程序間通訊是客戶端開發使用頻率較高的場景。

程序間通訊方式主要有命名管道、訊息佇列、共享記憶體、Socket通訊,個人使用最多的是Sokcet相關。

而Socket也有很多使用方式,Socket、WebSocket、TcpClient、UdpClient,是不是很多?HttpClient與TcpClient、WebSocket之間有什麼關係?這裡我們分別介紹下這些通訊及使用方式

Socket

Socket是傳輸通訊協議麼?No,Socket是一種傳輸層和應用層之間、用於實現網路通訊的程式設計介面。Socket可以使用各種協議如TCP、UDP協議實現程序通訊,TCP/UDP才是傳輸通訊協議

Socket位於傳輸層與應用層之間,介面在System.Net.Sockets名稱空間下。下面是Socket以TCP通訊的DEMO:

    //建立一個 Socket 例項
    Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    
    //連線到伺服器
    clientSocket.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8000));
    
    //傳送資料
    string message = "Hello, Server!";
    byte[] data = Encoding.ASCII.GetBytes(message);
    clientSocket.Send(data);
    
    //接收資料
    byte[] buffer = new byte[1024];
    int bytesRead = clientSocket.Receive(buffer);
    Debug.WriteLine(Encoding.ASCII.GetString(buffer, 0, bytesRead));
    
    clientSocket.Close();

TcpClient/UdpClient

TCP/UDP均是位於傳輸層的通訊協議,所以Socket的使用也是位於傳輸層的通訊操作

TCP是面向連線,提供可靠、順序的資料流傳輸。用於一對一的通訊,即一個TCP連線只能有一個傳送方和一個接收方。詳細連線方式是,先透過三次握手建立連線、然後傳輸資料,傳輸資料完再透過4次揮手關閉連線。所以適用於需要資料完整性和可靠傳輸的場景

而UDP則是無連線的,不需要建立和維護連線狀態,不提供確認機制,也不重傳丟失的資料包,但也因此傳輸實時性高,適合低延時、資料量小、廣播場景

基於Socket抽象程式設計介面,TCP、UDP構建更高階別抽象網路程式設計TcpClient、UdpClient,它們用於簡化TCP網路程式設計中的常見任務

TcpClient、UdpClient是 .NET 提供的用於方便管理TCP和UDP網路通訊的類,下面是對應的Demo

Tcp服務端:

 1 using System;
 2 using System.Net;
 3 using System.Net.Sockets;
 4 using System.Text;
 5 
 6 class TcpServerExample
 7 {
 8     public static void Main()
 9     {
10         TcpListener listener = new TcpListener(“127.0.0.1", 8000);
11         listener.Start();
12         Console.WriteLine("Server is listening on port 8000...");
13 
14         TcpClient client = listener.AcceptTcpClient();
15         NetworkStream stream = client.GetStream();
16         
17         byte[] data = new byte[1024];
18         int bytesRead = stream.Read(data, 0, data.Length);
19         Console.WriteLine("Received: " + Encoding.ASCII.GetString(data, 0, bytesRead));
20 
21         byte[] response = Encoding.ASCII.GetBytes("Hello, Client!");
22         stream.Write(response, 0, response.Length);
23 
24         stream.Close();
25         client.Close();
26         listener.Stop();
27     }
28 }

TCP客戶端:

 1 using System;
 2 using System.Net.Sockets;
 3 using System.Text;
 4 
 5 class TcpClientExample
 6 {
 7     public static void Main()
 8     {
 9         TcpClient client = new TcpClient("127.0.0.1", 8000);
10         NetworkStream stream = client.GetStream();
11 
12         byte[] message = Encoding.ASCII.GetBytes("Hello, Server!");
13         stream.Write(message, 0, message.Length);
14 
15         byte[] data = new byte[1024];
16         int bytesRead = stream.Read(data, 0, data.Length);
17         Debug.WriteLine("Received: " + Encoding.ASCII.GetString(data, 0, bytesRead));
18 
19         stream.Close();
20         client.Close();
21     }
22 }

Udp服務端:

 1 using System;
 2 using System.Net;
 3 using System.Net.Sockets;
 4 using System.Text;
 5 
 6 class UdpServerExample
 7 {
 8     public static void Main()
 9     {
10         UdpClient udpServer = new UdpClient(8000);
11         IPEndPoint remoteEP = new IPEndPoint(”127.0.0.1“, 0);
12 
13         Console.WriteLine("Server is listening on port 8000...");
14 
15         byte[] data = udpServer.Receive(ref remoteEP);
16         Console.WriteLine("Received: " + Encoding.ASCII.GetString(data));
17 
18         byte[] response = Encoding.ASCII.GetBytes("Hello, Client!");
19         udpServer.Send(response, response.Length, remoteEP);
20 
21         udpServer.Close();
22     }
23 }

Udp客戶端:

 1 using System;
 2 using System.Net;
 3 using System.Net.Sockets;
 4 using System.Text;
 5 
 6 class UdpClientExample
 7 {
 8     public static void Main()
 9     {
10         UdpClient udpClient = new UdpClient();
11         IPEndPoint remoteEP = new IPEndPoint(”127.0.0.1", 8000);
12 
13         byte[] message = Encoding.ASCII.GetBytes("Hello, Server!");
14         udpClient.Send(message, message.Length, remoteEP);
15 
16         byte[] data = udpClient.Receive(ref remoteEP);
17         Console.WriteLine("Received: " + Encoding.ASCII.GetString(data));
18 
19         udpClient.Close();
20     }
21 }

上面是基本的網路通訊DEMO,TcpClient用於基於連線、可靠的TCP通訊,適用於需要資料完整性和可靠傳輸的場景。Udp用於無連線、不保證傳輸的UDP通訊,適用於對實時性要求高、允許少量資料丟失的場景(如影片流)。會議場景下的傳屏軟體適合用這個協議,傳屏傳送端固定幀率一直推送,網路丟失幾幀問題不大,重要的是延時低了很多。

TcpClient、UdpClient是位於傳輸層的通訊類,分別實現了基於TCP和UDP協議的通訊功能。

HttpClient

講完傳輸層的網路通訊類,就要說下應用層的HttpClient,這是專門用於HTTP協議的通訊

Http與TCP/UDP均是網路通訊協議,TCP、UDP位於傳輸層,HTTP傳於應用層,而且HTTP是基於TCP面向連線的,它是客戶端單向發起的半雙工協議。HTTP1.1之後引入持久連線,允許一個TCP連線進行多次請求/響應傳輸。HTTP層相比TCP它關注請求、響應的內容

HttpClient是Http協議的通訊類,提供了封裝好的、高階的HTTP功能(如發起GET, POST請求,處理響應等)。

HttpClient可以用於Web介面如Restful API的呼叫,我這邊Windows應用的WebApi基礎元件庫就是用HttpClient實現的。

HttpClient類,在System.Net.Http.HttpClient名稱空間下,HttpClient的內部實現是基於Socket的。也就是說,HttpClient底層使用Socket介面來建立連線並傳輸資料,但它隱藏了這些細節,為開發者提供了一個更簡潔的API。

下面是我基於HttpClient實現的Web服務各類操作入口程式碼,可以簡單瀏覽下:

 1         /// <summary>
 2         /// 請求/推送資料
 3         /// </summary>
 4         /// <typeparam name="TResponse"></typeparam>
 5         /// <param name="request"></param>
 6         /// <returns></returns>
 7         public async Task<TResponse> RequestAsync<TResponse>(HttpRequest request) where TResponse : HttpResponse, new()
 8         {
 9             var requestUrl = request.GetRequestUrl();
10             try
11             {
12                 using var client = CreateHttpClient(request);
13                 var requestMethod = request.GetRequestMethod();
14                 switch (requestMethod)
15                 {
16                     case RequestMethod.Get:
17                         {
18                             using var response = await client.GetAsync(requestUrl);
19                             return await response.GetTResponseAsync<TResponse>();
20                         }
21                     case RequestMethod.Post:
22                         {
23                             using var httpContent = request.GetHttpContent();
24                             using var response = await client.PostAsync(requestUrl, httpContent);
25                             return await response.GetTResponseAsync<TResponse>();
26                         }
27                     case RequestMethod.Put:
28                         {
29                             using var httpContent = request.GetHttpContent();
30                             using var response = await client.PutAsync(requestUrl, httpContent);
31                             return await response.GetTResponseAsync<TResponse>();
32                         }
33                     case RequestMethod.Delete:
34                         {
35                             using var response = await client.DeleteAsync(requestUrl);
36                             return await response.GetTResponseAsync<TResponse>();
37                         }
38                     case RequestMethod.PostForm:
39                         {
40                             using var requestMessage = new HttpRequestMessage(HttpMethod.Post, requestUrl);
41                             using var httpContent = request.GetHttpContent();
42                             requestMessage.Content = httpContent;
43                             using var response = await client.SendAsync(requestMessage);
44                             return await response.GetTResponseAsync<TResponse>();
45                         }
46                 }
47                 return new TResponse() { Message = $"不支援的請求型別:{requestMethod}" };
48             }
49             catch (ArgumentNullException e)
50             {
51                 return new TResponse() { Code = NetErrorCodes.ParameterError, Message = e.Message, JsonData = e.StackTrace };
52             }
53             catch (TimeoutException e)
54             {
55                 return new TResponse() { Code = NetErrorCodes.TimeOut, Message = e.Message, JsonData = e.StackTrace };
56             }
57             catch (Exception e)
58             {
59                 return new TResponse() { Message = e.Message, JsonData = e.StackTrace };
60             }
61         }

HttpClient封裝後的網路基礎元件呼叫方式,也比較簡單。

新增介面請求說明,引數及請求引數均統一在一個類檔案裡定義好:

 1 /// <summary>
 2 /// 內網穿透註冊介面
 3 /// </summary>
 4 [Request("http://frp.supporter.ws.h3c.com/user/register",RequestMethod.Post)]
 5 [DataContract]
 6 internal class RegisterFrpRequest : HttpRequest
 7 {
 8     public RegisterFrpRequest(string sn, string appName)
 9     {
10         Sn = sn;
11         SeverNames = new List<RequestServiceName>()
12         {
13             new RequestServiceName(appName,"http")
14         };
15     }
16     [DataMember(Name = "sn")]
17     public string Sn { get; set; }
18 
19     [DataMember(Name = "localServerNames")]
20     public List<RequestServiceName> SeverNames { get; set; }
21 }

再定義請求結果返回資料,基類HttpResponse內有定義基本引數,狀態Success、狀態碼Code、返回描述資訊Message:

 1 [DataContract]
 2 class RegisterFrpResponse : HttpResponse
 3 {
 4 
 5     [DataMember(Name = "correlationId")]
 6     public string CorrelationId { get; set; }
 7 
 8     [DataMember(Name = "data")]
 9     public FrpRegisterData Data { get; set; }
10 
11     /// <summary>
12     /// 是否成功
13     /// </summary>
14     public bool IsSuccess => Success && Code == 200000 && Data != null;
15 }

然後,業務層可以進行簡潔、高效率的呼叫:

var netClient = new NetHttpClient();
var response = await netClient.RequestAsync<RegisterFrpResponse>(new RegisterFrpRequest(sn, appName));

WebSocket

WebSocket也是一個應用層通訊,不同於可以實現倆類協議TCP/UDP的Socket,WebSocket只依賴於HTTP/HTTPS連線。

一旦握手成功,客戶端和伺服器之間可以進行雙向資料傳輸,可以傳輸位元組資料也可以傳輸文字內容。

  • 持久連線:WebSocket 是持久化連線,除非主動關閉,否則在整個會話期間連線保持開放。

  • 全雙工通訊:客戶端和伺服器可以隨時傳送資料,通訊不再是單向的。使用System.Net.WebSockets.ClientWebSocket類來實現WebSocket通訊,透過減少 HTTP 請求/響應的開銷、延時較低。

WebSocketHttpClient呢,都用於應用層的網路通訊,但它們的用途和通訊協議是不同的。

  • HttpClient使用 HTTP 協議,WebSocket使用WebSocket協議,該協議在初始連線時透過 HTTP/HTTPS握手,然後轉換為基於TCP通訊的WebSocket協議。所以雖然都有使用HTTP協議,但WebSocket後續就切換至基於TCP的全雙工通訊了

  • HttpClient基於請求/響應模式,每次通訊由客戶端向伺服器發起請求。WebSocket提供全雙工通訊,客戶端和伺服器都可以主動傳送資料。

  • HttpClient主要用於訪問 RESTful API、下載檔案或者傳送HTTP請求。WebSocket主要用於實現低延遲的實時通訊,如程序間通訊、區域網通訊等。

我團隊Windows應用所使用的程序間通訊,就是基於WebSocketSharp封裝的。WebSocketSharp是一個功能全面、易於使用的第三方 WebSocket 庫 GitHub - sta/websocket-sharp

至於為啥不直接使用ClientWebSocket。。。是因為當時團隊還未切換.NET,使用的是.NETFramework。

後面團隊使用的區域網通訊基礎元件就是用ClientWebSocket了。

下面是我封裝的部分WebSocket通訊程式碼,事件傳送(廣播)、以及監聽其它客戶端傳送過來的事件訊息:

 1     /// <summary>
 2     /// 傳送訊息
 3     /// </summary>
 4     /// <typeparam name="TInput">傳送引數型別</typeparam>
 5     /// <param name="client">目標客戶端</param>
 6     /// <param name="innerEvent">事件名</param>
 7     /// <param name="data">傳送引數</param>
 8     /// <returns></returns>
 9     public async Task<ClientResponse> SendAsync<TInput>(string client, InnerEventItem innerEvent, TInput data)
10     {
11         var message = new ChannelSendingMessage(client, new ClientEvent(innerEvent.EventName, innerEvent.EventId, true), _sourceClient);
12         message.SetData<TInput>(data);
13         return await SendMessageAsync(ChannelMessageType.ClientCommunication, message);
14     }
15 
16     /// <summary>
17     /// 訂閱訊息
18     /// </summary>
19     /// <param name="client">目標客戶端</param>
20     /// <param name="innerEvent">事件名稱</param>
21     /// <param name="func">委託</param>
22     public ClientSubscribedEvent SubscribeFunc(string client, InnerEventItem innerEvent, Func<ClientResponse, object> func)
23     {
24         var eventName = innerEvent?.EventName;
25         if (string.IsNullOrEmpty(eventName) || func == null)
26         {
27             throw new ArgumentNullException($"{nameof(eventName)}或{nameof(func)},引數不能為空!");
28         }
29 
30         var subscribedEvent = new ClientSubscribedEvent(client, innerEvent, func);
31         SubscribeEvent(subscribedEvent);
32         return subscribedEvent;
33     }
34     /// <summary>
35     /// 訂閱訊息
36     /// </summary>
37     /// <param name="client">目標客戶端</param>
38     /// <param name="innerEvent">事件名稱</param>
39     /// <param name="func">委託</param>
40     public ClientSubscribedEvent SubscribeFuncTask(string client, InnerEventItem innerEvent, Func<ClientResponse, Task<object>> func)
41     {
42         var eventName = innerEvent?.EventName;
43         if (string.IsNullOrEmpty(eventName) || func == null)
44         {
45             throw new ArgumentNullException($"{nameof(eventName)}或{nameof(func)},引數不能為空!");
46         }
47 
48         var subscribedEvent = new ClientSubscribedEvent(client, innerEvent, func);
49         SubscribeEvent(subscribedEvent);
50         return subscribedEvent;
51     }
52 
53     /// <summary>
54     /// 訂閱訊息
55     /// </summary>
56     /// <param name="client">目標客戶端</param>
57     /// <param name="innerEvent">事件名稱</param>
58     /// <param name="action">委託</param>
59     public ClientSubscribedEvent Subscribe(string client, InnerEventItem innerEvent, Action<ClientResponse> action)
60     {
61         var eventName = innerEvent?.EventName;
62         if (string.IsNullOrEmpty(eventName) || action == null)
63         {
64             throw new ArgumentNullException($"{nameof(eventName)}或{nameof(action)},引數不能為空!");
65         }
66 
67         var subscribedEvent = new ClientSubscribedEvent(client, innerEvent, action);
68         SubscribeEvent(subscribedEvent);
69         return subscribedEvent;
70     }

關鍵詞:TCP/UDP,HTTP,Socket,TcpClient/UdpClient,HttpClient,WebSocket

相關文章