基于Net平台---以太网通信(Socket)

目录

1. 网络基础

TCP/IP 四层模型

2. Socket对象

Socket连接过程

Socket对象创建

3. Bind() 绑定与 Connect() 连接

Bind()

Connecct()

4. Listen() 监听请求连接 和 Accept() 接收连接请求

Listen()方法

Accept() 方法

示例代码

关键点总结

5. Receive() 与 Send()

Receive() 方法

Send() 方法

示例代码

6. 释放资源

Shutdown() 方法

Disconnect() 方法

Close() 方法

Dispose() 方法

关键点总结

最佳实践建议


1. 网络基础

TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/因特网互联协议)是一个协议族,它定义了网络通信的标准和规则。TCP/IP 是互联网的基础协议,它为数据从一台计算机传输到另一台计算机提供了可靠的方法。为了更好地理解和组织这些协议,TCP/IP 模型被划分为四个层次,每个层次负责不同的功能。

TCP/IP 四层模型

  1. 应用层 (Application Layer)
    • 职责:提供应用程序间通信的功能。它包含了各种高级网络服务和协议,使得用户可以直接与网络交互。
    • 常见协议:HTTP、HTTPS、FTP、SMTP、DNS、Telnet、SSH等。
    • 作用:处理数据表示、编码及会话管理等问题;例如,HTTP 协议用于Web浏览,SMTP 用于电子邮件传输。
  1. 传输层 (Transport Layer)
    • 职责:确保端到端的可靠或不可靠的数据传输。它负责在源主机和目标主机之间建立逻辑连接,并保证数据完整性和顺序性。
    • 主要协议
      • TCP (Transmission Control Protocol):面向连接、可靠的传输协议,提供错误检测、流量控制、拥塞控制等功能。
      • UDP (User Datagram Protocol):无连接、较轻量级的传输协议,适用于对速度要求较高而对可靠性要求较低的应用场景。
    • 作用:分割数据成小块进行发送,并且在接收端重新组装;同时也负责确认机制以确保数据正确到达。
  1. 网络层 (Internet Layer)
    • 职责:负责路由选择,即决定数据包如何在网络中从一个节点传递到另一个节点。此外,它还负责地址解析以及分片重组等工作。
    • 主要协议
      • IP (Internet Protocol):规定了IP地址格式,用于标识设备的位置;同时定义了数据报文格式及其封装方式。
      • ICMP (Internet Control Message Protocol):用于报告错误信息并交换有限的控制消息。
      • ARP (Address Resolution Protocol)RARP (Reverse Address Resolution Protocol):用于将物理硬件地址转换为IP地址或者反之。
    • 作用:为每个数据包选择最佳路径,并根据需要拆分大尺寸的数据包以便适应不同网络的要求。
  1. 网络接口层 (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 连接,还会立即释放与之关联的所有资源(如文件描述符)。它有两种形式:

  1. 无参版本:立即关闭 Socket 并释放资源。
  2. 带参版本:提供了一个超时时间(以毫秒计),在这个时间内尝试将已经排队的数据发送出去,然后再关闭连接。
示例代码
// 立即关闭并释放资源
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}");
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值