TCP 概觀
重要
強烈建議進階使用者使用 Socket 類別,而非 TcpClient
和 TcpListener
。
若要使用傳輸控制通訊協定 (TCP),您有兩個選項:使用 Socket 以獲得最大控制力和效能,或使用 TcpClient 和 TcpListener 協助程式類別。 TcpClient 和 TcpListener 建置於 System.Net.Sockets.Socket 類別之上,負責處理傳輸資料的細節,以方便使用。
通訊協定類別使用基礎 Socket
類別提供簡單的網路服務存取,不需要維護狀態資訊或了解設定通訊協定特定通訊端的詳細資料等成本。 若要使用非同步 Socket
方法,您可以使用 NetworkStream 類別所提供的非同步方法。 若要存取通訊協定類別未公開的 Socket
類別功能,您必須使用 Socket
類別。
TcpClient
和 TcpListener
代表使用 NetworkStream
類別的網路。 您使用 GetStream 方法來傳回網路資料流,然後呼叫資料流的 NetworkStream.ReadAsync 和 NetworkStream.WriteAsync 方法。 NetworkStream
並未擁有通訊協定類別的基礎通訊端,因此關閉並不會影響通訊端。
使用 TcpClient
和 TcpListener
TcpClient 類別會使用 TCP 向網際網路資源要求資料。 TcpClient
的方法和屬性擷取的詳細資料,可用來建立 Socket 以使用 TCP 要求和接收資料。 因為遠端裝置的連線是以資料流表示,所以可以使用 .NET Framework 資料流處理技術來讀取和寫入資料。
TCP 通訊協定會建立與遠端端點的連線,然後使用該連接來傳送和接收資料封包。 TCP 負責確保將資料封包傳送到端點,並在送達時以正確的順序組合。
建立 IP 端點
使用 System.Net.Sockets 時,您會將網路端點表示為 IPEndPoint 物件。 IPEndPoint
是以 IPAddress 及其對應的連接埠號碼來建構。 在透過 Socket 起始交談之前,您要先建立應用程式和遠端目的地之間的資料管道。
TCP/IP 會使用網路位址和服務連接埠編號來唯一識別服務。 網路位址可識別特定網路目的地;連接埠號碼則可識別該裝置上要連線的特定服務。 網路位址和服務連接埠的組合稱為端點,在 .NET 中是以 EndPoint 類別表示。 每個支援的位址系列已定義 EndPoint
的子系;對於 IP 位址系列,此類別是 IPEndPoint。
Dns 類別會提供網域名稱服務給使用 TCP/IP 網際網路服務的應用程式。 GetHostEntryAsync 方法會查詢 DNS 伺服器,以將使用者易記的網域名稱 (例如 "host.contoso.com") 對應到數字的網際網路位址 (例如 192.168.1.1
)。 GetHostEntryAsync
會傳回 Task<IPHostEntry>
,在等待時其中包含位址和所要求名稱之別名的清單。 在大部分情況下,您可以使用 AddressList 陣列中傳回的第一個位址。 下列程式碼可取得包含伺服器 host.contoso.com
之 IP 位址的 IPAddress。
IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("host.contoso.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];
提示
針對手動測試和偵錯目的,您通常可以使用 GetHostEntryAsync 方法以及從 Dns.GetHostName() 值取得的結果主機名稱,將 localhost 名稱解析為 IP 位址。 請考慮下列程式碼片段:
var hostName = Dns.GetHostName();
IPHostEntry localhost = await Dns.GetHostEntryAsync(hostName);
// This is the IP address of the local machine
IPAddress localIpAddress = localhost.AddressList[0];
Internet Assigned Numbers Authority (IANA) 定義了通用服務的連接埠號碼。 如需詳細資訊,請參閱 IANA:服務名稱和傳輸通訊協定連接埠號碼登錄)。 其他服務的已登錄連接埠編號範圍可以是 1,024 到 65,535。 下列程式碼會合併 host.contoso.com
的 IP 位址與連接埠號碼,以建立連線的遠端端點。
IPEndPoint ipEndPoint = new(ipAddress, 11_000);
在判斷遠端裝置的位址並選擇要用於連線的連接埠之後,應用程式可以建立與遠端裝置的連線。
建立 TcpClient
TcpClient
類別會提供抽象層級比 Socket
類別更高的 TCP 服務。 TcpClient
用來建立遠端主機的用戶端連線。 了解如何取得 IPEndPoint
後,假設您有要與所需的連接埠號碼配對的 IPAddress
。 下列範例示範如何設定 TcpClient
,以在 TCP 連接埠 13 上連線至時間伺服器:
var ipEndPoint = new IPEndPoint(ipAddress, 13);
using TcpClient client = new();
await client.ConnectAsync(ipEndPoint);
await using NetworkStream stream = client.GetStream();
var buffer = new byte[1_024];
int received = await stream.ReadAsync(buffer);
var message = Encoding.UTF8.GetString(buffer, 0, received);
Console.WriteLine($"Message received: \"{message}\"");
// Sample output:
// Message received: "📅 8/22/2022 9:07:17 AM 🕛"
在上述 C# 程式碼中:
- 從已知的
IPAddress
和連接埠建立IPEndPoint
。 - 具現化新的
TcpClient
物件。 - 使用 TcpClient.ConnectAsync 透過連接埠 13 將
client
連線至遠端 TCP 時間伺服器。 - 使用 NetworkStream 從遠端主機讀取資料。
- 宣告
1_024
個位元組的讀取緩衝區。 - 將資料從
stream
讀取到讀取緩衝區中。 - 將結果以字串形式寫入至主控台。
由於用戶端知道訊息很小,因此整個訊息可在單一作業中讀取到讀取緩衝區中。 若訊息較大,或訊息的長度不確定,用戶端應更適當地使用緩衝區,並在 while
迴圈中讀取。
重要
傳送和接收訊息時,伺服器和用戶端均應事先得知 Encoding。 例如,如果伺服器使用 ASCIIEncoding 進行通訊,但用戶端嘗試使用 UTF8Encoding,則訊息的格式會不正確。
建立 TcpListener
TcpListener 型別用來監視傳入要求的 TCP 連接埠,然後建立管理用戶端連線的 Socket
或 TcpClient
。 Start 方法可啟用接聽,而 Stop 方法則可停用連接埠上的接聽。 AcceptTcpClientAsync 方法會接受連入連線要求,並建立 TcpClient
來處理要求,而 AcceptSocketAsync 方法會接受連入連線要求,並建立 Socket
來處理要求。
下列範例示範如何建立使用 TcpListener
來監視 TCP 連接埠 13 的網路時間伺服器。 接受連入連線要求時,時間伺服器會回應主機伺服器的目前日期和時間。
var ipEndPoint = new IPEndPoint(IPAddress.Any, 13);
TcpListener listener = new(ipEndPoint);
try
{
listener.Start();
using TcpClient handler = await listener.AcceptTcpClientAsync();
await using NetworkStream stream = handler.GetStream();
var message = $"📅 {DateTime.Now} 🕛";
var dateTimeBytes = Encoding.UTF8.GetBytes(message);
await stream.WriteAsync(dateTimeBytes);
Console.WriteLine($"Sent message: \"{message}\"");
// Sample output:
// Sent message: "📅 8/22/2022 9:07:17 AM 🕛"
}
finally
{
listener.Stop();
}
在上述 C# 程式碼中:
- 使用 IPAddress.Any 和連接埠建立
IPEndPoint
。 - 具現化新的
TcpListener
物件。 - 呼叫 Start 方法以開始接聽連接埠。
- 使用 AcceptTcpClientAsync 方法中的
TcpClient
接受連入連線要求。 - 將目前的日期和時間編碼為字串訊息。
- 使用 NetworkStream 將資料寫入至連線的用戶端。
- 將傳送的訊息寫入至主控台。
- 最後,呼叫 Stop 方法以停止接聽連接埠。
透過 Socket
類別進行有限 TCP 控制
TcpClient
和 TcpListener
都在內部依賴 Socket
類別,這表示您透過這些類別所能做到的一切,都可直接使用通訊端來達成。 本節示範數個 TcpClient
和 TcpListener
使用案例,及其在功能上相當的 Socket
對應項目。
建立用戶端通訊端
TcpClient
的預設建構函式會嘗試透過 Socket(SocketType, ProtocolType) 建構函式來建立雙堆疊通訊端。 如果支援 IPv6,此建構函式就會建立雙堆疊通訊端,否則會回復為 IPv4。
請考量下列 TCP 用戶端程式碼:
using var client = new TcpClient();
前述 TCP 用戶端程式碼的功能相當於下列通訊端程式碼:
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
TcpClient(AddressFamily) 建構函式
此建構函式僅接受三個 AddressFamily
值,否則會擲回 ArgumentException。 有效值為:
- AddressFamily.InterNetwork:用於 IPv4 通訊端。
- AddressFamily.InterNetworkV6:用於 IPv6 通訊端。
- AddressFamily.Unknown:會嘗試建立雙堆疊通訊端,類似於預設建構函式。
請考量下列 TCP 用戶端程式碼:
using var client = new TcpClient(AddressFamily.InterNetwork);
前述 TCP 用戶端程式碼的功能相當於下列通訊端程式碼:
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
TcpClient(IPEndPoint) 建構函式
建立通訊端時,此建構函式也會繫結至所提供的本機 IPEndPoint
。 屬性 IPEndPoint.AddressFamily 是用來決定通訊端的位址系列。
請考量下列 TCP 用戶端程式碼:
var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var client = new TcpClient(endPoint);
前述 TCP 用戶端程式碼的功能相當於下列通訊端程式碼:
// Example IPEndPoint object
var endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5001);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);
TcpClient(String, Int32) 建構函式
類似於預設建構函式,此建構函式會嘗試建立雙重堆疊,並將其連線至 hostname
和 port
配對所定義的遠端 DNS 端點。
請考量下列 TCP 用戶端程式碼:
using var client = new TcpClient("www.example.com", 80);
前述 TCP 用戶端程式碼的功能相當於下列通訊端程式碼:
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);
連線至伺服器
TcpClient
中的所有 Connect
、ConnectAsync
、BeginConnect
和 EndConnect
多載在功能上都相當於對應的 Socket
方法。
請考量下列 TCP 用戶端程式碼:
using var client = new TcpClient();
client.Connect("www.example.com", 80);
上述 TcpClient
程式碼相當於下列通訊端程式碼:
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect("www.example.com", 80);
建立伺服器通訊端
TcpListener
建構函式非常類似在功能上相當於原始 Socket
對應項目的 TcpClient
執行個體,本節會將該建構函式對應至其對應的通訊端程式碼。 要考量的第一個建構函式是 TcpListener(IPAddress localaddr, int port)
。
var listener = new TcpListener(IPAddress.Loopback, 5000);
上述 TCP 接聽程式程式碼的功能相當於下列通訊端程式碼:
var ep = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
開始在伺服器上接聽
Start() 方法是結合 Socket
的 Bind 與 Listen() 功能的包裝函式。
請考量下列 TCP 接聽程式程式碼:
var listener = new TcpListener(IPAddress.Loopback, 5000);
listener.Start(10);
上述 TCP 接聽程式程式碼的功能相當於下列通訊端程式碼:
var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(endPoint);
try
{
socket.Listen(10);
}
catch (SocketException)
{
socket.Dispose();
}
接受伺服器連線
在幕後,連入 TCP 連線一律會在被接受後建立新的通訊端。 TcpListener
可以直接接受 Socket 執行個體 (透過 AcceptSocket() 或 AcceptSocketAsync()),也可以接受 TcpClient (透過 AcceptTcpClient() 和 AcceptTcpClientAsync())。
請考量下列 TcpListener
程式碼:
var listener = new TcpListener(IPAddress.Loopback, 5000);
using var acceptedSocket = await listener.AcceptSocketAsync();
// Synchronous alternative.
// var acceptedSocket = listener.AcceptSocket();
上述 TCP 接聽程式程式碼的功能相當於下列通訊端程式碼:
var endPoint = new IPEndPoint(IPAddress.Loopback, 5000);
using var socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
using var acceptedSocket = await socket.AcceptAsync();
// Synchronous alternative
// var acceptedSocket = socket.Accept();
建立 NetworkStream
以傳送和接收資料
使用 TcpClient
時,您必須使用 GetStream() 方法將 NetworkStream 具現化,才能傳送和接收資料。 使用 Socket
時,您必須手動建立 NetworkStream
。
請考量下列 TcpClient
程式碼:
using var client = new TcpClient();
using NetworkStream stream = client.GetStream();
這相當於下列通訊端程式碼:
using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
// Be aware that transferring the ownership means that closing/disposing the stream will also close the underlying socket.
using var stream = new NetworkStream(socket, ownsSocket: true);
提示
如果您的程式碼不需要使用 Stream 執行個體,您可以直接依賴 Socket
的傳送/接收方法 (Send、SendAsync、Receive 和 ReceiveAsync),而不建立 NetworkStream。