# Socket
Socket(计算机术语为:套接字)是计算机网络编程中的一种通信机制,使网络上的两个程序通过一个双向的通信连接实现数据的交换。它是一个抽象层,提供了一套标准的接口,使得应用程序可以通过这套接口与网络通信协议进行交互,实现数据传输和共享。
在网络编程中,Socket 通常被用来实现客户端和服务端之间的通信。客户端通过 Socket 连接到服务端,并向服务端发送数据请求,服务端接收请求并处理后,再将结果返回给客户端。这种通信方式可以用于实现各种不同类型的网络应用程序,如 Web 服务器、聊天软件、在线游戏等。
Socket 有三个重要参数:IP、端口、协议。这三个概念共同构成了网络通信的基础。
# IP(Internet Protocol)
IP 协议位于 TCP/IP 协议的第三层 —— 网络层,IP 是一种网络协议,用于在互联网上寻址和路由数据包。每台计算机在网络上都分配有一个唯一的 IP 地址,类似于一个邮政地址,用于标识计算机在网络上的位置。IP 地址由四个十进制数(例如:192.168.0.1)组成,每个数的取值范围是 0 到 255。IPv4 是目前使用最广泛的 IP 版本,而 IPv6 则是一种更新的 IP 协议版本。
# 端口(Port)
端口是计算机上用于数据通信的逻辑连接点,端口号用于标识计算机上具体的应用程序或服务。一个端口可以看作是计算机中的门,通过这个门可以发送和接收数据。端口号是一个 16 位的数字,可以是 0 到 65535 之间的任意值。其中,0 到 1023 的端口号被称为 "知名端口",用于一些特殊的网络服务,例如 80 端口用于 HTTP 协议,22 端口用于 SSH 协议等。而大于 1023 的端口号则被称为 "动态端口",用于临时的网络连接。
# 协议(Protocol)
协议是在网络中进行通信的规则和约定。不同的协议定义了数据的格式、传输方式、错误处理等各个方面的规定。常见的网络协议有 TCP(Transmission Control Protocol)和 UDP(User Datagram Protocol)。TCP 协议提供可靠的、面向连接的通信,适用于需要确保数据完整性和顺序的场景,例如网页浏览、文件传输等。而 UDP 协议则是一种不可靠的、无连接的通信,适用于对实时性有较高需求的场景,例如音频和视频传输。
# UDP
- UDP 缺乏可靠性。UDP 本身不提供确认,序列号,超时重传等机制。UDP 数据报可能在网络中被复制,被重新排序。即 UDP 不保证数据报会到达其最终目的地,也不保证各个数据报的先后顺序,也不保证每个数据报只到达一次
- UDP 数据报是有长度的。每个 UDP 数据报都有长度,如果一个数据报正确地到达目的地,那么该数据报的长度将随数据一起传递给接收方。而 TCP 是一个字节流协议,没有任何(协议上的)记录边界
- UDP 是无连接的 UDP 客户和服务器之前不必存在长期的关系。UDP 发送数据报之前也不需要经过握手创建连接的过程
- UDP 支持多播和广播。
# TCP
- TCP 提供一种面向连接的、可靠的字节流服务
- 在一个 TCP 连接中,仅有两方进行彼此通信。广播和多播不能用于 TCP
- TCP 使用校验和,确认和重传机制来保证可靠传输
- TCP 给数据分节进行排序,并使用累积确认保证数据的顺序不变和非重复
- TCP 使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制
注意:TCP 并不能保证数据一定会被对方接收到,因为这是不可能的。TCP 能够做到的是,如果有可能,就把数据递送到接收方,否则就(通过放弃重传并且中断连接这一手段)通知用户。因此,准确说 TCP 也不是 100% 可靠的协议,它所能提供的是数据的可靠递送或故障的可靠通知。
# 三次握手
所谓三次握手 (Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送 3 个包。
三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect () 时,将触发三次握手。
# 四次挥手
TCP 的连接的拆除需要发送四个包,因此称为四次挥手 (Four-way handshake),也叫做改进的三次握手。客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close () 操作即可产生挥手操作。
# TCP 连接
TCP 协议中,客户端绑定端口后,先断开连接然后再次连接会报错,因为同一端口在短时间内(30 秒到 4 分钟之内)只能使用一次。因为客户端需要等待服务端的回复,如果超时,则断开连接。总而言之:先断开的一方,会延迟释放端口。
解决方法:
- 客户端每次连接使用不同的端口(这是大部分游戏的选择)。缺点:部分需求不允许更换端口。
- 让服务端先断开连接(让服务端先发送断开连接的请求)。缺点:端口越多,服务器压力越大。
原因:根据四次挥手的流程,先发送断开连接请求的会在最后断开连接。所以,让服务端先发送断开连接的请求,客户端先同意断开然后断开,因为已经断开了,所以可以再次连接。对于服务端,虽然没有立刻断开连接,但是服务端是接收连接请求的,如果客户端在断开后的短时间内再次发送连接请求,二者仍然可以正常连接。
# 流模式
TCP 的字节流模式指的是 TCP 将应用层交给它的数据视为无结构的字节流进行传输,不考虑这些数据在应用层的含义和分段情况。这意味着,发送方将应用层数据切分为多个数据段进行发送,在传输过程中,这些数据段可能会被拆分、合并,但最终到达接收方时,TCP 会重新组合这些数据段,保证数据的完整性和顺序。由于建立了连接,收到的数据都是同一主机发送的,所以可以让发送端 Write 一次,接收端 Read 多次,或者反过来
# CSharp 中用于实现网络通信的类
Socket 类、UdpClient 类和 TcpClient 类都是 CSharp 中用于实现网络通信的类。其中,Socket 类是最基础的网络通信类,提供了最底层、最灵活的网络通信接口。
如果需要更多的自由度和灵活性,那么 Socket 类可能更适合你的需求。在使用 Socket 类时,你需要更多地了解网络编程的底层知识、细节和原理,因为 Socket 类需要手动管理更多的网络细节(例如设置 Socket 选项、操作缓冲区等)。
UdpClient 类和 TcpClient 类则是基于 Socket 类封装的更高层次的网络通信类,提供了更方便和易用的接口。
UdpClient 类主要用于实现基于 UDP 协议的网络通信,提供了 Send 和 Receive 等方法。
TcpClient 类主要用于实现基于 TCP 协议的网络通信,提供了 Connect、GetStream 和 Read 等方法。
# UdpClient 和 TcpClient 的使用
获取自身的 IP 地址
/// <summary> | |
/// 获取自身的 IP 地址 | |
/// </summary> | |
/// <returns></returns> | |
private static string GetLocalIPAddress() | |
{ | |
string ipAddress = "0.0.0.0"; | |
IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName()); | |
foreach (IPAddress ip in host.AddressList) | |
{ | |
if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) | |
{ | |
ipAddress = ip.ToString(); | |
break; | |
} | |
} | |
return ipAddress; | |
} |
UdpClient
// 终结点,包含 IP 地址和端口 | |
// 自身的终结点 | |
IPEndPoint IP_my = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 4444); | |
// 目标终结点 | |
IPEndPoint IP_target = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 5555); | |
// 发送端 | |
UdpClient udpClient_my = new UdpClient(IP_my); | |
// 编码 | |
byte[] bytes_my = Encoding.UTF8.GetBytes("message"); | |
// 发送数据 | |
udpClient_my.Send(bytes_my, bytes_my.Length, IP_target); | |
// 接收端 | |
UdpClient udpClient_target = new UdpClient(IP_target); | |
// 任意 IP 地址,任意端口 | |
IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); | |
// 接收消息 | |
// 收到消息前,参数表示:需要接收(监听)的终结点 | |
// 收到消息以后,参数表示:实际的终结点 | |
// 在接收到消息前:阻塞线程 | |
// 接收到消息后:继续执行 | |
// 所以,必须通过辅助线程调用 | |
byte[] bytes_target = udpClient_target.Receive(ref endpoint); | |
// 解码 | |
string message = Encoding.UTF8.GetString(bytes_target); | |
// 手动释放资源 | |
udpClient_my.Dispose(); | |
udpClient_target.Dispose(); |
TcpClient
// 自身的终结点 | |
IPEndPoint IP_my = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 4444); | |
// 目标终结点 | |
IPEndPoint IP_target = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 5555); | |
// 绑定指定的端口 | |
//TcpClient tcpClient_my = new TcpClient(IP_my); | |
// 使用随机端口 | |
TcpClient tcpClient_my = new TcpClient(); | |
// 建立链接 | |
tcpClient_my.Connect(IP_target); | |
// 获取流 | |
NetworkStream stream_my = tcpClient_my.GetStream(); | |
// 编码 | |
byte[] bytes_my = Encoding.UTF8.GetBytes("message"); | |
// 写入数据 | |
stream_my.Write(bytes_my); | |
// 监听器 | |
TcpListener listener_target = new TcpListener(IP_target); | |
// 接收端 | |
TcpClient tcpClient_target; | |
// 流 | |
NetworkStream stream_target; | |
// 存储字节数据 | |
byte[] bytes_target = new byte[1024]; | |
// 有效字符数 | |
int count; | |
// 在监听到客户端链接前:阻塞线程 | |
// 在监听到客户端链接后:继续执行 | |
// 所以,必须通过辅助线程调用 | |
using (tcpClient_target = listener_target.AcceptTcpClient()) | |
{ | |
using (stream_target = tcpClient_target.GetStream()) | |
{ | |
while (true) | |
{ | |
// 在读取到数据前:阻塞线程 | |
// 在读取到数据后:继续执行 | |
// 如果需要监听多个客户端链接,则需要开启线程 | |
// 返回值:实际读取到的字节数 | |
count = stream_target.Read(bytes_target); | |
// 解码,只解析有效字符 | |
string message = Encoding.UTF8.GetString(bytes_target, 0, count); | |
} | |
} | |
} | |
// 手动释放资源 | |
stream_my.Dispose(); | |
tcpClient_my.Dispose(); | |
listener_target.Stop(); |
# 网络通信的例子
网络聊天室是一个很好的例子