活动
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 类使用 TCP 从 Internet 资源请求数据。 TcpClient
的方法和属性会摘录为了通过 TCP 请求和接收数据而创建的 Socket 的详细信息。 与远程设备的连接表示为流,因此可以使用 .NET Framework 流处理技术读取和写入数据。
TCP 协议与远程终结点建立连接,然后使用此连接发送和接收数据包。 TCP 负责确保将数据包发送到终结点,并在数据包到达时以正确的顺序对其进行汇编。
使用 System.Net.Sockets 时,将网络终结点表示为对象 IPEndPoint。 IPEndPoint
是使用 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
类在高于 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 将
client
连接到端口 13 上的远程 TCP 时间服务器。 - 使用 NetworkStream 从远程主机读取数据。
- 声明
1_024
个字节的读取缓冲区。 - 将数据从
stream
读取到读取缓冲区。 - 将结果作为字符串写入控制台。
由于客户端知道消息较小,因此可以一次操作将整个消息读入读取缓冲区。 对于较大的消息或长度不确定的消息,客户端应更适当地使用缓冲区,并在 while
循环中读取。
重要
发送和接收消息时,服务器和客户端应提前了解 Encoding。 例如,如果服务器使用 ASCIIEncoding 进行通信,但客户端尝试使用 UTF8Encoding,消息将出现格式错误。
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 方法以停止侦听端口。
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);
此构造函数仅接受三个 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);
创建套接字后,此构造函数还将绑定到提供的本地 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);
此构造函数将尝试创建类似于默认构造函数的双堆栈,并将其连接到由 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);
就像与原始 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() 方法是组合 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();
使用 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。