TCP 概述

重要

对于高级用户,强烈建议使用 Socket 类,而不是 TcpClientTcpListener

若要使用传输控制协议 (TCP),有两个选项:使用 Socket 以获得最大控制和性能,或使用 TcpClientTcpListener 帮助程序类。 TcpClientTcpListener 是在 System.Net.Sockets.Socket 类的基础上建立的,并负责传输数据的详细信息以便于使用。

协议类使用基础 Socket 类提供简单的网络服务访问,没有维护状态信息的开销,也无需了解设置协议特定的套接字的详细信息。 若要使用异步 Socket 方法,可以使用 NetworkStream 类提供的异步法。 若要访问未被协议类公开的 Socket 类功能,必须使用 Socket 类。

TcpClientTcpListener 代表使用 NetworkStream 类的网络。 使用 GetStream 方法返回网络流,然后调用此流的 NetworkStream.ReadAsyncNetworkStream.WriteAsync 方法。 NetworkStream 不拥有协议类的基础套接字,因此关闭它不会影响套接字。

使用 TcpClientTcpListener

TcpClient 类使用 TCP 从 Internet 资源请求数据。 TcpClient 的方法和属性会摘录为了通过 TCP 请求和接收数据而创建的 Socket 的详细信息。 与远程设备的连接表示为流,因此可以使用 .NET Framework 流处理技术读取和写入数据。

TCP 协议与远程终结点建立连接,然后使用此连接发送和接收数据包。 TCP 负责确保将数据包发送到终结点,并在数据包到达时以正确的顺序对其进行汇编。

创建 IP 终结点

使用 System.Net.Sockets 时,将网络终结点表示为对象 IPEndPointIPEndPoint 是使用 IPAddress 及其相应的端口号构造的。 在通过 Socket 发起对话之前,在应用和远程目标之间创建数据管道。

TCP/IP 使用一个网络地址和一个服务端口号来对唯一标识设备。 网络地址标识特定网络目标;端口号标识该设备要连接到的特定服务。 网络地址和服务端口的组合称为终结点,它在 .NET 中由 EndPoint 类表示。 会为每个受支持的地址系列定义 EndPoint 的后代;对于 IP 地址系列,类为 IPEndPoint

Dns 类向使用 TCP/IP Internet 服务的应用提供域名服务。 GetHostEntryAsync 方法查询 DNS 服务器以将用户友好的域名(如“host.contoso.com”)映射到数字形式的 Internet 地址(如 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 编号分配机构 (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.ConnectAsyncclient 连接到端口 13 上的远程 TCP 时间服务器。
  • 使用 NetworkStream 从远程主机读取数据。
  • 声明 1_024 个字节的读取缓冲区。
  • 将数据从 stream 读取到读取缓冲区。
  • 将结果作为字符串写入控制台。

由于客户端知道消息较小,因此可以一次操作将整个消息读入读取缓冲区。 对于较大的消息或长度不确定的消息,客户端应更适当地使用缓冲区,并在 while 循环中读取。

重要

发送和接收消息时,服务器和客户端应提前了解 Encoding。 例如,如果服务器使用 ASCIIEncoding 进行通信,但客户端尝试使用 UTF8Encoding,消息将出现格式错误。

创建 TcpListener

TcpListener 类型用于监视 TCP 端口上的传入请求,然后创建一个 SocketTcpClient 来管理与客户端的连接。 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 控制

TcpClientTcpListener 内部都依赖于 Socket 类,这意味着可以使用套接字直接实现对这些类执行的任何操作。 本部分演示了多个 TcpClientTcpListener 用例,以及功能 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。 有效值为:

请考虑以下 TCP 客户端代码:

using var client = new TcpClient(AddressFamily.InterNetwork);

前面的 TCP 客户端代码在功能上等效于下面的套接字代码:

using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

TcpClient(IPEndPoint) 构造函数

创建套接字后,此构造函数还将绑定到提供的 本地IPEndPointIPEndPoint.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) 构造函数

此构造函数将尝试创建类似于默认构造函数的双堆栈,并将其连接到由 hostnameport 对定义的远程 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 中的所有 ConnectConnectAsyncBeginConnectEndConnect 重载在功能上都等效于相应的 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);

创建服务器套接字

就像与原始 Socket 对应项功能等效的 TcpClient 实例一样,本部分将 TcpListener 构造函数映射到其相应的套接字代码。 要考虑的第一个构造函数是 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() 方法是组合 SocketBindListen() 功能的包装器。

请考虑以下 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的发送/接收方法(SendSendAsyncReceiveReceiveAsync),而不是创建 NetworkStream

另请参阅