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 地址。
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);
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 控件
和 TcpListener
内部都TcpClient
依赖于 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);
连接到服务器
中的所有 Connect
TcpClient
、 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);
socket.Listen(10);
catch (SocketException)
socket.Dispose();
接受服务器连接
在后台,传入的 TCP 连接在接受时始终创建新的套接字。 TcpListener
Socket可以通过 或 AcceptSocketAsync()) 直接 (AcceptSocket()接受实例,也可以通过 AcceptTcpClient() 和 AcceptTcpClientAsync()) 接受TcpClient (。
请考虑以下 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
时,需要使用 方法实例化 NetworkStreamGetStream() ,以便能够发送和接收数据。 使用 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、 SendAsyncReceive 和 ReceiveAsync) ,而不是创建 NetworkStream。
使用套接字通过 TCP 发送和接收数据
.NET 中的网络
Socket
TcpClient
TcpListener
NetworkStream