Socket
# 介绍
Socket又称"套接字",他工作于应用层与传输层之间,本质上是将把TCP、UDP的复杂操作,抽象为简单的几个接口来供应用层调用,实现进程在网络中的数据通信。
# Socket家族
不同的Socket家族,其Socket的地址格式、通信范围、传输性能。常见的Socket家族有AF_INET、AF_INET6、AF_UNIX。
# AF_INET
用于跨主机的网络通信(如互联网或局域网),基于IPv4协议,通过IP地址和端口标识通信端点。
AF_INET数据需经过网络层封装、路由及网卡传输,受带宽和延迟影响,适用于远程通信。
地址格式:(host, port)
host为主机地址,可以是域名或IPv4地址,为空则表示绑定本机的所有网络接口。
port为端口号。
# AF_UNIX
AF_UNIX也称为AF_LOCAL,用于同一台主机上的进程间通信(IPC),通过文件系统路径标识端点,不依赖网络协议。
AF_UNIX直接在内核中拷贝数据,无需网络协议处理,效率更高且不受网络带宽限制。
地址格式:"xxx/xxx/xx"
在文件系统上的socket文件地址,例如:/tmp/socket。
# Socket类型
Socket也分不同的类型,不同的套接字类型使用的传输协议也不同,我们可以根据实际应用场景来选择合适的套接字类型。
# SOCK_STREAM
SOCK_STREAM即流套接字,也叫面向连接的套接字。它基于TCP(传输控制协议) 来提供可靠的、面向连接的服务。
他有如下几个特征:
数据是安全可靠传输的,丢失或错误的数据包会被发送方重传。
数据是作为字节流按照顺序传输的,接收方会根据数据包携带的序列号进行排序。
数据发送方和接收方的调用次数不需要匹配(不保留消息边界)。接收方可以一次读取多次发送的内容,也可以分多次读取一次发送的内容。
# SOCK_DGRAM
SOCK_DGRAM即数据报套接字,也叫无连接的套接字。它基于UDP(用户数据报协议) 提供快速、但不可靠的、无连接服务。
DGRAM是Data Gram的缩写,意为数据报。
他有如下几个特征:
强调快速传输而非可靠,数据可能在传输过程中可能丢失或重复,且无重传机制。
每个数据报文都是独立传输和处理的,不保证顺序到达。
数据发送方和接收方的调用次数需要严格匹配(保留消息边界),每一次发送的数据都会作为一个独立的数据报传输,每一次接收都需要恰好接收一个完整的数据报。
# Socket库
Python中可以使用socket库来实现基本的Socket,它可以访问底层操作系统Socket接口的全部方法。
import socket
# 创建Socket对象
socket.socket(family=AF_INET, type=SOCKET_STREAM)
family:套接字家族。
type:套接字类型。
# Socket对象方法
# 服务端方法
| 方法 | 描述 |
|---|---|
| s.bind(address) | 绑定地址到套接字, 在AF_INET下,地址以元组(host,port)形式表示。host为空字符串则会绑定到所有网络接口。 |
| s.listen(backlog) | 开启监听TCP连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5即可。 |
| s.accept() | 阻塞并等待客户端连接,客户端成功连接后会停止阻塞并返回元组(客户端对象, 客户端地址) |
# 客户端方法
| 方法 | 描述 |
|---|---|
| s.connect(address) | 向服务器发起连接请求,地址以元组(host,port)形式表示。如果连接出错,则返回socket.error错误。 |
| s.connect_ex(address) | 功能与connect相同,但是成功返回0,失败返回error的值。 |
# 通用方法
| 方法 | 描述 |
|---|---|
| s.recv(bufsize[,flag]) | 阻塞并等待TCP接受到数据,bufsize指定要接收的最大数据量,flag指定其他设置。 |
| s.send(string[,flag]) | 通过TCP发送string中的数据,返回实际发送的数据字节大小。 |
| s.sendall(string[,flag]) | 通过TCP尝试发送所有数据,成功返回None,失败则抛出异常。 |
| s.sendto(string[,flag], address) | 通过UDP发送数据string,address为数据接收端地址元组(host,port),返回值是发送的字节数。 |
| s.recvfrom(bufsize[,flag]) | 阻塞并等待UDP接受到数据,参数与recv()类似,返回值为元组(data,address),其中data为接收的字符串数据,address是发送端的地址。 |
| s.close() | 关闭套接字连接。 |
# 服务端例子
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
address = ("0.0.0.0", 8000)
s.bind(address)
s.listen(5)
print(f"Listen Start: {address}")
while True:
conn, addr = s.accept()
print(f"Accept connect: {addr}")
data = conn.recv(1024).decode("utf-8")
print(f"Receive data: {data}")
conn.send("Bye".encode("utf-8"))
conn.close()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 客户端例子
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.2.22", 8000))
s.send("Hello".encode("utf-8"))
print(s.recv(1024).decode("utf-8"))
s.close()
2
3
4
5
6
7
# TCP粘包问题
# 问题介绍
由于TCP协议基于字节流传输的特性,导致接收方无法区分消息边界,从而可能将多个数据包合并接收或单个数据包拆分接收的现象。
# 主流解决方案
为解决消息边界模糊问题,需在应用层设计协议。以下是三种常用方法:
# 固定长度法
原理:规定所有数据包长度一致(如64字节),不足时填充空字符。
优点:实现简单,接收方按固定长度分割即可。
缺点:浪费带宽(填充无效数据),灵活性差。
# 分隔符法
原理:在包尾添加特殊字符(如\r\n)标记结束,接收方按分隔符解析。
优点:易于实现(如FTP协议)。
缺点:需转义数据中可能出现的分隔符,效率较低。
# 包头+包体法(推荐)
原理:包头固定长度(如4字节),存储包体长度字段;接收方先读包头获取长度,再按长度读取包体。
优点:高效灵活,支持变长数据,无冗余传输。
实现示例:
- 发送端:将数据封装为
[长度字段][数据内容]发送。 - 接收端:循环读取缓冲区,先解析包头长度,再提取完整包体。
可以使用struct模块将数据长度打包为固定大小的字节放在开头发送。解包时先获取固定大小进行解包得到数据长度,再接收指定大小的数据。