目录
4. Listen() 监听请求连接 和 Accept() 接收连接请求
1. 网络基础
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/因特网互联协议)是一个协议族,它定义了网络通信的标准和规则。TCP/IP 是互联网的基础协议,它为数据从一台计算机传输到另一台计算机提供了可靠的方法。为了更好地理解和组织这些协议,TCP/IP 模型被划分为四个层次,每个层次负责不同的功能。
TCP/IP 四层模型
- 应用层 (Application Layer)
-
- 职责:提供应用程序间通信的功能。它包含了各种高级网络服务和协议,使得用户可以直接与网络交互。
- 常见协议:HTTP、HTTPS、FTP、SMTP、DNS、Telnet、SSH等。
- 作用:处理数据表示、编码及会话管理等问题;例如,HTTP 协议用于Web浏览,SMTP 用于电子邮件传输。
- 传输层 (Transport Layer)
-
- 职责:确保端到端的可靠或不可靠的数据传输。它负责在源主机和目标主机之间建立逻辑连接,并保证数据完整性和顺序性。
- 主要协议:
-
-
- TCP (Transmission Control Protocol):面向连接、可靠的传输协议,提供错误检测、流量控制、拥塞控制等功能。
- UDP (User Datagram Protocol):无连接、较轻量级的传输协议,适用于对速度要求较高而对可靠性要求较低的应用场景。
-
-
- 作用:分割数据成小块进行发送,并且在接收端重新组装;同时也负责确认机制以确保数据正确到达。
- 网络层 (Internet Layer)
-
- 职责:负责路由选择,即决定数据包如何在网络中从一个节点传递到另一个节点。此外,它还负责地址解析以及分片重组等工作。
- 主要协议:
-
-
- IP (Internet Protocol):规定了IP地址格式,用于标识设备的位置;同时定义了数据报文格式及其封装方式。
- ICMP (Internet Control Message Protocol):用于报告错误信息并交换有限的控制消息。
- ARP (Address Resolution Protocol) 和 RARP (Reverse Address Resolution Protocol):用于将物理硬件地址转换为IP地址或者反之。
-
-
- 作用:为每个数据包选择最佳路径,并根据需要拆分大尺寸的数据包以便适应不同网络的要求。
- 网络接口层 (Link Layer, 或称作 Network Interface Layer)
-
- 职责:直接与物理网络介质打交道,包括通过电缆、光纤或者其他传输媒介来发送和接收比特流。此层还涉及到MAC(Media Access Control)地址分配及访问控制策略。
- 主要协议:
-
-
- Ethernet:最常用的局域网标准之一。
- PPP (Point-to-Point Protocol):常用于拨号连接和DSL宽带接入。
-
-
- 作用:实现实际的数据帧传输,确保物理链路上的数据能够准确无误地传输给下一个节点。
2. Socket对象
Socket连接过程
- 服务器监听:服务器端socket并不定位具体的客户端socket,而是处于等待监听状态,实时监控网络状态。
- 客户端请求:客户端clientSocket发送连接请求,目标是服务器的serverSocket。为此,clientSocket必须知道serverSocket的地址和端口号,进行扫描发出连接请求。
- 连接确认:当服务器socket监听到或者是受到客户端socket的连接请求时,服务器就响应客户端的请求,建议一个新的socket,把服务器socket发送给客户端,一旦客户端确认连接,则连接建立。
注:在连接确认阶段:服务器socket即使在和一个客户端socket建立连接后,还在处于监听状态,仍然可以接收到其他客户端的连接请求,这也是一对多产生的原因。
Socket对象创建
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//监控 ip4 地址,套接字类型为 TCP ,协议类型为 TCP
//其有三个构造函数
public Socket(SocketInformation socketInformation);
public Socket(SocketType socketType, ProtocolType protocolType);
public Socket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType);
第一个构造函数,SocketInformation 对象保存的是
Socket(SocketType, ProtocolType)
参数含义
SocketType
指定 Socket 类的实例表示的套接字类型。
TCP 用主机的IP地址加上主机上的端口号作为 TCP 连接的端点,这种端点就叫做套接字(socket)或插口。 套接字用(IP地址:端口号)表示。
SocketType 是 enum 类型,其字段如下
SocketType | 值 | 对应的ProtocolType | 描述 |
Unknown | -1 | Unknown | 指定未知的 Socket 类型。 |
Stream(使用字节流) | 1 | Tcp | 支持可靠、双向、基于连接的字节流 |
Dgram(使用数据报) | 2 | Udp | 面向无连接 |
Raw | 3 | Icmp、lgmp | 支持对基础传输协议的访问 |
Rdm | 4 | 支持无连接、面向消息、以可靠方式发送的消息, 并保留数据中的消息边界 | |
Seqpacket | 5 | 在网络上提供排序字节流的面向连接且可靠的双向传输 |
ProtocolType
表示协议类型,是一个 enum 类型。
协议类型 | 值 | 描述 |
Ggp | 3 | 网关到网关协议。 |
Icmp | 1 | Internet 控制消息协议。 |
IcmpV6 | 58 | IPv6 的 Internet 控制消息协议。 |
Idp | 22 | Internet 数据报协议。 |
Igmp | 2 | Internet 组管理协议。 |
IP | 0 | Internet 协议。 |
IPSecAuthenticationHeader | 51 | IPv6 身份验证标头。 有关详细信息,请参阅https://www.ietf.org 上的 RFC 2292,第 2.2.1 节。 |
IPSecEncapsulatingSecurityPayload | 50 | IPv6 封装安全负载标头。 |
IPv4 | 4 | Internet 协议版本 4。 |
IPv6 | 41 | Internet 协议版本 6 (IPv6)。 |
IPv6DestinationOptions | 60 | IPv6 目标选项标头。 |
IPv6FragmentHeader | 44 | IPv6 片段标头。 |
IPv6HopByHopOptions | 0 | IPv6 逐跳选项标头。 |
IPv6NoNextHeader | 59 | IPv6 无下一个标头。 |
IPv6RoutingHeader | 43 | IPv6 路由标头。 |
Ipx | 1000 | Internet 数据包交换协议。 |
ND | 77 | 网络磁盘协议(非正式)。 |
Pup | 12 | PARC 通用数据包协议。 |
Raw | 255 | 原始 IP 数据包协议。 |
Spx | 1256 | 顺序包交换协议。 |
SpxII | 1257 | 顺序包交换版本 2 协议。 |
Tcp | 6 | 传输控制协议。 |
Udp | 17 | 用户数据报协议。 |
Unknown | -1 | 未知的协议。 |
Unspecified | 0 | 未指定的协议。 |
AddressFamily
表示使用的网络寻址方案,是一个 enum 类型。
地址类型 | 值 | 描述 |
AppleTalk | 16 | AppleTalk 地址。 |
Atm | 22 | 本机 ATM 服务地址。 |
Banyan | 21 | Banyan 地址。 |
Ccitt | 10 | CCITT 协议(如 X.25)的地址。 |
Chaos | 5 | MIT CHAOS 协议的地址。 |
Cluster | 24 | Microsoft 群集产品的地址。 |
DataKit | 9 | Datakit 协议的地址。 |
DataLink | 13 | 直接数据链接接口地址。 |
DecNet | 12 | DECnet 地址。 |
Ecma | 8 | 欧洲计算机制造商协会 (ECMA) 地址。 |
FireFox | 19 | FireFox 地址。 |
HyperChannel | 15 | NSC Hyperchannel 地址。 |
Ieee12844 | 25 | IEEE 1284.4 工作组地址。 |
ImpLink | 3 | ARPANET IMP 地址。 |
InterNetwork | 2 | IP 版本 4 的地址。 |
InterNetworkV6 | 23 | IP 版本 6 的地址。 |
Ipx | 6 | IPX 或 SPX 地址。 |
Irda | 26 | IrDA 地址。 |
Iso | 7 | ISO 协议的地址。 |
Lat | 14 | LAT 地址。 |
Max | 29 | MAX 地址。 |
NetBios | 17 | NetBios 地址。 |
NetworkDesigners | 28 | 支持网络设计器 OSI 网关的协议的地址。 |
NS | 6 | Xerox NS 协议的地址。 |
Osi | 7 | OSI 协议的地址。 |
Pup | 4 | PUP 协议的地址。 |
Sna | 11 | IBM SNA 地址。 |
Unix | 1 | Unix 本地到主机地址。 |
Unknown | -1 | 未知的地址族。 |
Unspecified | 0 | 未指定的地址族。 |
VoiceView | 18 | VoiceView 地址。 |
3. Bind() 绑定与 Connect() 连接
- Bind() 用于绑定 IPEndPoint 对象,在服务端使用。
- Connect() 在客户端使用,用于连接服务端。
Bind()
public void Bind (System.Net.EndPoint localEP);
使用方法
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress iP = IPAddress.Parse("127.0.0.1");
// 你将在在本地创建 IPEndPoint 对象,拥有此 ip:post 的访问权限。目的是绑定本地机器的某个端口,所有经过此端口的数据就归你管了。
serverSocket.Bind(new IPEndPoint(iP, 2300))
Connecct()
与远程主机建立连接。Connect() 有四个重载方法,不必关注,只需知道,必需提供 IP 和 Post 两个值。
使用方法
IPAddress iP = IPAddress.Parse("127.0.0.1");
IPEndPoint iPEndPoint = new IPEndPoint(iP, 2300);
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//创建与远程主机的连接
serverSocket.Connect(iPEndPoint);
4. Listen() 监听请求连接 和 Accept() 接收连接请求
Listen()方法
作用
Listen()
方法准备 Socket
以开始监听传入的连接请求。它设置了最大允许排队等待接受的连接数(即“backlog”),当有新的连接到来时,如果队列已满,则新连接将被拒绝。
参数
- backlog:指定可以排队的最大未处理连接数。这个值通常是一个小整数(如5或10),具体取决于预期的并发连接数量。请注意,在某些操作系统上,实际的最大队列长度可能会受到系统配置的影响。
Accept() 方法
作用
一旦 Socket
已经通过 Listen()
设置好监听状态,就可以使用 Accept()
方法来接收来自客户端的实际连接请求。Accept()
是一个阻塞调用,意味着它会一直等待直到有一个新的连接建立为止。成功接收到连接后,Accept()
返回一个新的 Socket
实例,专门用于与该特定客户端通信。
返回值
- 返回的新
Socket
:表示与客户端之间的连接。你可以使用这个新的套接字来进行数据的发送和接收操作。
异步版本
为了提高性能并避免长时间阻塞主线程,建议使用异步版本的方法如 AcceptAsync()
或者结合 async
/await
关键字来实现非阻塞的连接处理。
示例代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
public class TcpServer {
private Socket _listener; // 服务器监听用的套接字
/// <summary>
/// 启动服务器并开始监听指定端口上的连接请求。
/// </summary>
/// <param name="port">要监听的端口号。</param>
public async Task StartAsync(int port) {
// 创建一个新的TCP Socket对象,用于监听传入的连接请求
_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 定义本地终结点(IP地址和端口),这里使用IPAddress.Any表示接受所有网络接口上的连接
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, port);
// 将套接字绑定到指定的本地终结点
_listener.Bind(localEndPoint);
// 开始监听连接请求,设置最大排队长度为10(即同时允许最多10个未处理的连接请求)
_listener.Listen(10);
Console.WriteLine("Server is listening for connections...");
// 进入无限循环,持续等待并处理新的客户端连接
while (true) {
// 异步地接受一个客户端连接请求,并返回一个新的Socket实例来与该客户端通信
Socket clientSocket = await AcceptClientAsync();
// 为每个新连接启动一个新的任务,以便并发处理多个客户端
// 使用_ = Task.Run(...)忽略返回的任务,因为我们不需要显式等待它完成
_ = Task.Run(() => HandleClient(clientSocket));
}
}
/// <summary>
/// 异步地接受一个客户端连接请求。
/// </summary>
/// <returns>一个新的Socket实例,代表与客户端之间的连接。</returns>
private async Task<Socket> AcceptClientAsync() {
return await Task.Factory.FromAsync(
_listener.BeginAccept, // 开始异步接受连接
_listener.EndAccept, // 结束异步操作并获取结果
null // 状态对象,这里不使用
);
}
/// <summary>
/// 处理来自客户端的连接,包括接收数据、发送响应以及关闭连接。
/// </summary>
/// <param name="clientSocket">与客户端通信的Socket实例。</param>
private void HandleClient(Socket clientSocket) {
try {
// 准备缓冲区以接收来自客户端的数据
byte[] buffer = new byte[1024];
// 接收客户端发送的数据,注意这是一个阻塞调用,直到有数据可读
int bytesRead = clientSocket.Receive(buffer);
// 如果没有读取到任何数据,则认为客户端已经断开连接
if (bytesRead == 0) {
Console.WriteLine("Client disconnected.");
return;
}
// 将接收到的字节数组转换为字符串形式
string message = Encoding.ASCII.GetString(buffer, 0, bytesRead);
Console.WriteLine($"Received from client: {message}");
// 构建要发送给客户端的响应消息
string response = "Hello, Client!";
byte[] responseBuffer = Encoding.ASCII.GetBytes(response);
// 发送响应给客户端,这也是一个阻塞调用,直到所有数据都被发送出去
clientSocket.Send(responseBuffer);
} catch (Exception ex) {
// 捕获并打印可能发生的异常信息
Console.WriteLine($"Error handling client: {ex.Message}");
} finally {
// 关闭客户端连接,确保资源被正确释放
try {
clientSocket.Shutdown(SocketShutdown.Both); // 停止发送和接收数据
clientSocket.Close(); // 关闭套接字
} catch (SocketException se) {
// 如果在关闭过程中出现错误(例如客户端已提前断开),则忽略之
Console.WriteLine($"Error closing socket: {se.Message}");
}
Console.WriteLine("Client connection closed.");
}
}
}
关键点总结
- 创建监听套接字:我们首先创建了一个
Socket
对象_listener
,用于监听特定端口上的连接请求。 - 绑定本地终结点:通过
Bind()
方法将套接字绑定到一个本地 IP 地址和端口组合上,这样它可以开始接受来自外部的连接。 - 开始监听:调用
Listen()
方法使套接字进入监听状态,并设定最大排队长度。 - 异步接受连接:使用
AcceptClientAsync()
方法异步地接受客户端连接请求,避免阻塞主线程。 - 并发处理客户端:每当有一个新的客户端连接时,都会启动一个新的任务来处理该连接,从而实现并发处理多个客户端的能力。
- 安全关闭连接:无论是否发生异常,都确保在
finally
块中关闭客户端连接,以防止资源泄漏。
这段代码展示了如何构建一个简单的 TCP 服务器,它能够监听传入的连接请求,并在接收到请求后启动新的任务来处理每个客户端。此外,还包含了适当的错误处理逻辑,以保证程序的健壮性。
5. Receive() 与 Send()
Receive() 方法
作用
Receive()
方法用于从已连接的套接字接收数据。它是一个阻塞调用,默认情况下会一直等待直到有数据可读或发生错误为止。该方法返回实际接收到的数据量(以字节数表示),如果返回值为0,则意味着对端已经关闭了连接。
参数
- buffer:一个字节数组,用于存储接收到的数据。
- offset:指定缓冲区中开始写入数据的位置(通常为0)。
- size:要接收的最大字节数。
- socketFlags:一个
SocketFlags
枚举值,可以用来指定额外的行为选项(如是否要求非阻塞操作等)。
返回值
- int:实际接收到的数据长度(字节数)。如果返回0,则表示对端已断开连接。
异常
- SocketException:当出现网络问题或其他与套接字相关的错误时抛出。
- ObjectDisposedException:当尝试在一个已经关闭的套接字上调用此方法时抛出。
Send() 方法
作用
Send()
方法用于向已连接的套接字发送数据。同样地,这也是一个阻塞调用,默认情况下会等待直到所有数据都被成功发送出去或发生错误。它返回实际发送的数据量(以字节数表示)。
参数
- buffer:包含要发送的数据的字节数组。
- offset:指定缓冲区中开始读取数据的位置(通常为0)。
- size:要发送的数据长度。
- socketFlags:一个
SocketFlags
枚举值,可以用来指定额外的行为选项。
返回值
- int:实际发送的数据长度(字节数)。如果发送的数据少于请求的数量,可能是因为对端已经关闭了连接或者其他原因。
异常
- SocketException:当出现网络问题或其他与套接字相关的错误时抛出。
- ObjectDisposedException:当尝试在一个已经关闭的套接字上调用此方法时抛出。
示例代码
以下是结合 Receive()
和 Send()
的完整示例,展示了如何在一个简单的TCP服务器中使用它们:
public class TcpServer {
private Socket _listener; // 服务器监听用的套接字
/// <summary>
/// 启动服务器并开始监听指定端口上的连接请求。
/// </summary>
/// <param name="port">要监听的端口号。</param>
public void Start(int port) {
// 创建一个新的TCP Socket对象,用于监听传入的连接请求
_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 定义本地终结点(IP地址和端口),这里使用IPAddress.Any表示接受所有网络接口上的连接
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, port);
// 将套接字绑定到指定的本地终结点
_listener.Bind(localEndPoint);
// 开始监听连接请求,设置最大排队长度为10(即同时允许最多10个未处理的连接请求)
_listener.Listen(10);
Console.WriteLine("Server is listening for connections...");
// 接受一个客户端连接请求
Socket clientSocket = _listener.Accept();
Console.WriteLine("A client has connected.");
try {
HandleClient(clientSocket); // 处理客户端连接
} catch (Exception ex) {
Console.WriteLine($"Error handling client: {ex.Message}");
} finally {
clientSocket.Close(); // 确保客户端连接被正确关闭
_listener.Close(); // 关闭监听套接字
}
}
/// <summary>
/// 处理来自客户端的连接,包括接收数据、发送响应以及关闭连接。
/// </summary>
/// <param name="clientSocket">与客户端通信的Socket实例。</param>
private void HandleClient(Socket clientSocket) {
byte[] buffer = new byte[1024]; // 准备缓冲区以接收来自客户端的数据
while (true) {
try {
// 接收客户端发送的数据
int bytesRead = clientSocket.Receive(buffer);
// 如果没有读取到任何数据,则认为客户端已经断开连接
if (bytesRead == 0) {
Console.WriteLine("Client disconnected.");
break;
}
// 将接收到的字节数组转换为字符串形式
string message = Encoding.ASCII.GetString(buffer, 0, bytesRead);
Console.WriteLine($"Received from client: {message}");
// 构建要发送给客户端的响应消息
string response = "Message received!";
byte[] responseBuffer = Encoding.ASCII.GetBytes(response);
// 发送响应给客户端
int bytesSent = clientSocket.Send(responseBuffer);
Console.WriteLine($"Sent {bytesSent} bytes back to the client.");
} catch (SocketException se) {
Console.WriteLine($"Socket error: {se.Message}");
break;
} catch (Exception ex) {
Console.WriteLine($"General error: {ex.Message}");
break;
}
}
}
}
6. 释放资源
Shutdown() 方法
作用
Shutdown()
方法用于禁用 Socket
的发送和/或接收功能。这允许你优雅地终止通信,确保所有已排队的数据都被处理完毕。
参数
- how:一个
SocketShutdown
枚举值,指定了要关闭的操作类型:
-
Send
:停止发送数据,但仍允许接收。Receive
:停止接收数据,但仍然可以发送。Both
:同时停止发送和接收。
示例代码
// 禁用发送和接收功能
clientSocket.Shutdown(SocketShutdown.Both);
Disconnect() 方法
作用
Disconnect()
方法用于关闭 Socket
连接,并根据提供的参数决定是否允许重用该 Socket
实例。这对于需要快速重新建立连接的应用场景特别有用,因为它可以在不断开物理连接的情况下更改绑定地址或端口。
参数
- reuseSocket:一个布尔值,指示是否允许重用此
Socket
实例。如果设置为true
,则可以在不创建新实例的情况下再次调用Connect()
或Bind()
方法。
示例代码
// 断开连接并允许重用套接字
clientSocket.Disconnect(true);
Close() 方法
作用
Close()
方法不仅关闭了 Socket
连接,还会立即释放与之关联的所有资源(如文件描述符)。它有两种形式:
- 无参版本:立即关闭
Socket
并释放资源。 - 带参版本:提供了一个超时时间(以毫秒计),在这个时间内尝试将已经排队的数据发送出去,然后再关闭连接。
示例代码
// 立即关闭并释放资源
clientSocket.Close();
// 带有超时的关闭,确保已排队的数据被发送出去
clientSocket.Close(5000); // 等待最多5秒
Dispose() 方法
作用
Dispose()
方法是实现 IDisposable
接口的一部分,用于显式释放未托管资源。通常情况下,你应该通过 using
语句自动管理这些资源,但在某些特殊情况下,你可能需要手动调用 Dispose()
来确保资源被及时释放。
示例代码
using (Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) {
// 使用套接字进行通信...
} // 在这里,Dispose() 会被自动调用
// 或者手动调用
clientSocket.Dispose();
关键点总结
- 优雅终止:使用
Shutdown()
方法可以确保所有已排队的数据都已被发送或接收,然后再正式断开连接。 - 快速重连:当需要频繁地建立和断开连接时,
Disconnect(true)
可以提高性能,因为它允许重用同一个Socket
实例。 - 立即释放资源:
Close()
方法适用于希望立即释放所有资源的情况。对于带有超时参数的版本,它还允许在关闭前等待一定时间以确保数据传输完成。 - 资源管理:始终优先考虑使用
using
语句来自动管理Socket
资源。如果你选择不这样做,请务必记得调用Dispose()
方法以防止资源泄漏。
最佳实践建议
- 顺序操作:推荐按照
Shutdown()
->Close()
的顺序来结束连接。首先调用Shutdown()
确保所有数据被处理,然后调用Close()
释放资源。 - 异常处理:无论何时调用上述任何方法,都应该围绕它们包装适当的
try-catch
块,以捕获并处理可能出现的异常。 - 避免重复关闭:不要多次调用
Close()
或Dispose()
,因为一旦资源被释放,再次调用可能会导致错误。
以下是一个结合了最佳实践的完整示例:
public void HandleClient(Socket clientSocket) {
try {
// 处理客户端连接...
// 优雅地关闭连接,确保所有数据都被发送和接收
clientSocket.Shutdown(SocketShutdown.Both);
} catch (SocketException se) {
Console.WriteLine($"Socket error: {se.Message}");
} catch (ObjectDisposedException ode) {
Console.WriteLine($"Socket has been disposed: {ode.Message}");
} catch (Exception ex) {
Console.WriteLine($"General error: {ex.Message}");
} finally {
try {
// 确保资源被正确释放
clientSocket.Close(5000); // 等待最多5秒以发送剩余数据
} catch (Exception ex) {
Console.WriteLine($"Error closing socket: {ex.Message}");
}
}
}