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 从 Internet 资源请求数据。 TcpClient
的方法和属性会摘录为了通过 TCP 请求和接收数据而创建的 Socket 的详细信息。 与远程设备的连接表示为流,因此可以使用 .NET Framework 流处理技术读取和写入数据。
TCP 协议与远程终结点建立连接,然后使用此连接发送和接收数据包。 TCP 负责确保将数据包发送到终结点,并在数据包到达时以正确的顺序对其进行汇编。
创建 IP 终结点
使用 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
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
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);
创建服务器套接字
就像与原始 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();
创建 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。