网络编程(四)

前言

经过之前三篇文章的学习我们大致了解了互联网是如何基于五层协议进行通信了,由应用层产生数据,然后进行封装,依次经过传输层,网络层,数据链路层的封装最后由物理层发送电信号进行数据的传输,在接收端首先由物理层接收到字节型的数据,然后拆包,验证 MAC 地址, IP 地址和端口,最后由目标应用获取数据并解码出来.

那么为了使用 TCP 协议建立连接,不用去管什么三次握手四次挥手以及数据到底是怎么在互联网中传输的,在应用层和传输层虚拟出一个抽象层,叫 socket( 套接字)层,这是一个模块为我们封装好了很多底层的协议,以便于我们只关注与实际的数据传输,而不用关系底层的实现.

所以我们就直接使用套接字和网络通信进行交互.

socket

socket是什么

socket是应用层与 TCP/IP协议族通信的中间软件抽象层,它是一组接口.在设计模式中, socket 就是一个模块,它把复杂的 TCP/IP 协议族隐藏在 socket 接口后面,对用户来说,一组简单的接口就是全部,让 socket 去组织数据,以符合指定的协议.

也有人说, socket 其实就是 ip 和 port 的组合, ip 用来标识互联网中的一台主机的位置,而 port 是用来标识这台机器上的一个应用程序, ip 地址是配置到网卡的,而 port 是应用程序开启的,这样 ip 和 port 就标识了互联网中独一无二的一个应用程序.

套接字发展史即分类

套接字起源于20世纪70年代加利福尼亚大学伯克利分校版本的 Unix, 即 BSD Unix. 因此,有时人们也把套接字称为’伯克利套接字’或’ BSD 套接字’.一开始,套接字被设计用在同一台主机上的多个应用程序之间的通信.这也被称之为进程间通信或 IPC(inter-process communication).套接字有两大家族,分别是基于文件型和基于网络型.我们主要学习基于网络型的套接字.

  1. 基于文件类型的套接字家族:

    套接字家族名字: AF_UNIX

    UNIX 哲学为一切皆文件(其实也不是所有的),基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信.

  2. 基于网络类型的套接字家族:

    套接字家族名字: AF_INET

    基于网络也就是 OSI 协议进行数据通信

套接字工作流程

其实就跟打电话很像,有拨号,接电话,通信,挂电话

首先由服务端初始化一个套接字对象,然后与本机的某个大于1023的端口绑定( bind),接着对该端口进行监听(listen),调用 accept 接口阻塞等待客户端的连接.如果在这时客户端初始化一个套接字对象,然后连接(connect)服务端的 ip 和port,如果连接成功,这时客户端与服务端的连接就建立成功了.

这时候可能有人再问了,三次握手呢?

当客户端 connect 服务端的时候就说明三次握手开始了,如果服务端 accept 有返回值,那么就说明三次握手成功.(可以使用 wireshark 进行抓包分析)wireshark 教程

TCP 连接建立成功后客户端和服务端就可以进行数据交互了.客户端发送数据请求,服务端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次通信结束.

SOCKET使用

套接字服务端通用使用方法

from socket import * sct = socket(socket_family, socket_type, protocal=0) socket_family 可以为AF_UNIX 或 AF_INET,socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM.protocol 一般不填,缺省值为0.

获取 tcp/ip 套接字

tcpSock = socket(AF_INET, SOCK_STREAM)

绑定 ip port

tcpSock.bind(('', 8080))

开始监听

tcpSock.listen(5)

被动接受 TCP 客户端连接(阻塞式)

tcpSock.accept()

获取 udp/ip 套接字

udpSock = socket(AF_UNIX, SOCK_DGRAM)

绑定 ip port

udpSock.bind(('', 8081))

套接字客户端通用方法

连接服务端主动初始化 TCP 服务器连接

tcpSock.connect(('', 8080))

公共用途的套接字函数

sock.recv() 接收 TCP 数据

sock.send() 发送 TCP 数据( send 在待发送数据量大于已端缓存区剩余空间时,数据将会丢失)

sock.sendall() 发送完整的 TCP 数据(本质就是循环调用 send,sendall 在待发送数据量大于已端缓存区剩余空间时,数据不丢失,循环调用 send 知道发完)

sock.recvfrom() 接收 UDP 数据

sock.sendto() 发送 UDP 数据

sock.getpeername() 连接到当前套接字的远端地址

sock.getsockname() 获取当前套接字的地址

sock.getsockopt() 返回指定套接字的参数

sock.setsockopt() 设置指定套接字的参数

sock.close() 关闭套接字

基于 TCP 连接的套接字

tcp 是基于连接的,必须先启动服务端,然后再启动客户端去连接服务端

tcp 服务端

from socket import *

# 创建tcp套接字对象
serverSock = socket(AF_INET, SOCK_STREAM)

# 绑定 ip 和端口
serverSock.bind(('', 8080))

# 监听连接
serverSock.listen(5)

# 被动等待客户端的连接,如果有客户端连接会返回一个用于和客户端通信的套接字以及客户端的地址
conn, addr = serverSock.accept()

# 接收客户端的数据
data = conn.recv(1024)

# 给客户端返回数据
conn.send(data2)

tcp 客户端

from socket import *

# 创建客户端套接字对象
clientSock = socket(AF_INET, SOCK_STREAM)

# 连接服务器
clientSock.connect(('', 8080))

# 发送数据到服务端
data = input('>>>').strip()
clientSock.send(data.encode('utf-8'))

# 接收服务端的数据
data = clientSock.recv(1024)
print(data.decode('utf-8'))

# 关闭客户端套接字
clientSock.close()

基于 TCP 的通信循环套接字

在上个版本中,服务端和客户端进行了一次通信就关闭了,很明显和实际应用不符合,所以加上通信循环,使得客户端可以和服务端进行多次通信.

服务端必须满足三点要求:

  1. 绑定一个固定的 ip 和 port
  2. 一直对外提供稳定的服务
  3. 能够支持并发(学了多进程多线程可以支持)

服务端

from socket import *

serverSock = socket(AF_INET, SOCK_STREAM)

serverSock.bind(('', 8081))

serverSock.listen(5)

conn, addr = serverSock.accept()

# 通信循环
while True:
    try:
        data = conn.recv(1024)
        if len(data) == 0:break # 针对 linux 系统
        print('-->收到客户端消息:', data)
        conn.send(data.upper())
    except ConnectionResetError:
        break
conn.close()
serverSock.close()

# 加上异常处理是因为当客户端异常断开连接时,服务端会报错

客户端

from socket import *

clientSock = socket(AF_INET, SOCK_STREAM)

client.connect(('', 8081))

# 通信循环
while True:
    msg = input('>>>:').stript()
    clientSock.send(msg.encode('utf-8'))
    data = clientSock.recv(1024)
    print(data)
clientSock.close()

基于 TCP 的连接循环通信循环套接字

在上一个版本中解决了客户端重复发消息的问题,但是作为一个服务端不可能只为一个客户提供服务,所以服务端必须可以接收多个连接(并发),这里实现的是伪并发.

服务端

from socket import *

serverSock = socket(AF_INET, SOCK_STREAM)
serverSock.bind(('', 8082))
serverSock.listen(5)

# 连接循环
while True:
    conn, addr = serverSock.accept()
    print(conn)
    
    # 通信循环
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0: break
            print('-->收到客户端的消息:', data)
            conn.send(data.upper())
        except ConnectionResetError:
            break
    conn.close()
serverSock.close()

客户端

from socket import *

clientSock = socket(AF_INET, SOCK_STREAM)

client.connect(('', 8081))

# 通信循环
while True:
    msg = input('>>>:').stript()
    clientSock.send(msg.encode('utf-8'))
    data = clientSock.recv(1024)
    print(data)
clientSock.close()

至此一个可以接受多个客户端连接的 C/S 通信程序就完成了,不过服务端连接了一个客户端,那么只能为这个客户端服务,只有等这个客户端断开连接才可以连接其他的客户端.

TCP 套接字的参数理解

在写服务端代码的时候,发现绑定的地方的 ip 我用了空的字符串,这表示是绑定到本机的,也可以用127.0.0.1来代替,然后还有一个参数为 listen, 里面填的5.

在启动一个服务端时,服务端就会进入 LISTEN 状态,表示监听客户端的连接,那么这个5就表示可以监听5个连接,在套接字里面有个名词叫半连接池,就是说启动服务端时,就自动开启一个半连接池,当客户端连接服务端的时候,表示一个半连接进入半连接池了,然后操作系统就从半连接池取出这个连接,开始处理这个连接,那么后来的连接也是放进这个半连接池里,因为填的5,所以加上服务端处理的那个,一共可以连接6个(处理一个),那么之后的客户端连接都会被服务端拒绝连接,只有等之前处理的或者是半连接池里的少了一个才可以连接上服务端.

那么这个半连接发生在 TCP 三次握手的第几次握手呢?发生在客户端 connect 的时候,也就是第一次握手的时候,第一次握手成功,客户端连接就进入了半连接池,那么等服务端 accept 的时候,就说明三次握手连接成功.

目前我们实现了一个不太完美的 C/S 程序,那么有什么办法可以让服务端既可以连接多个客户端,又可以同时为它们服务呢?答案是让连接循环和通信循环放在两个 py 文件里面.

模拟 SSH 实现远程执行命令

结合之前学习的模块 subprocess, 可以执行传过来的命令参数,在命令行中.那么我们使用客户端传送命令,在服务端运行 subprocess 执行命令,并把得到的结果传过去.

服务端

from socket import *
import subprocess

serverSock = socket(AF_INET, SOCK_STREAM)
serverSock.bind(('', 8085))
server.listen(5)

while True:
    conn, addr = serverSock.accept()
    while True:
        try:
            cmd = conn.recv(1024)
            if len(cmd):
                break
            obj = subprocess.Popen(cmd.decode('utf-8'),
                                  shell=True,
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            print(len(stdout) + len(stderr))
            conn.send(stdout+stderr)
        except ConnectionResetError:
            break
    conn.close()
serverSock.close()

客户端

from socket import *

clientSock = socket(AF_INET, SOCK_STREAM)
clientSock.connect(('', 8085))

while True:
    cmd = input('>>>').strip()
    if len(cmd) == 0:
        continue
    client.send(cmd.encode('utf-8'))
    
    client.send(cdm.encode(;utf-8))
    cmd_res = clientSock.recv(1024)
    print(cmd_res.decode('utf-8'))
clientSock.close()

结果表明当服务端发送的数据大于1024时,客户端一次收取数据量会收不完整,那么当你下次发送命令的时候,服务端回过来的数据会和上次未收完的数据粘在一起,这是一种粘包情况.

有时候命名关闭服务端了,重启服务端是会出现这种问题是因为当TCP主动关闭连接一方将继续等待一定时间,这是 TCP 连接状态中的 TIME_WAIT 状态,该状态的作用是用来重发可能丢失的 ACK 报文,其二当一个服务器断开连接时,该端口可能还会重复使用,那么该端口就不能被其他的应用使用.

Nagle 算法

1 Nagle 算法规则

当客户端连续发送时间间隔很短的两个数据包时,因为 TCP 协议为了优化传输效率和较少资源浪费,内部使用一种Nagle 的算法.Nagle 算法通常会在 TCP 程序里添加两行代码,在未确认数据发送的时候让发送器把数据送到系统缓存里.任何数据随后继续指导得到明显的数据去人或者直到攒到了一定数量的数据了再发包.

TCP/IP 协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方收到数据,也需要发送 ACK 表示确认.为了尽可能的利用网络带宽, TCP 总是希望尽可能的发送足够大的数据.(一个连接会设置 MSS 参数,因此, TCP/IP 希望每次都能以 MSS 尺寸的数据块来发送数据).Nagle 算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块.

Nagle 算法的基本定义时任意时刻,最多只能有一个未被确认的小段.所谓小段,指的是小于 MSS 尺寸的数据块,所谓未被确认,是指一个数据块发出后,没有收到对方的 ACK 确认数据已被收到.

Nagle 算法的规则:

  1. 如果包长度达到 MSS, 则允许发送;
  2. 如果该包含有 FIN, 则允许发送;
  3. 设置了 TCP_NODELAY 选项,则允许发送;
  4. 未设置 TCP_CORK 选项时,如所有发出去的小数据包(包长度小于 MSS)均被确认,则允许发送;
  5. 上述条件都未满足,但发生了超时(一般为200ms),则立即发送.

Nagle 算法只允许一个未被 ACK确认的包存在于网络,它并不管包的大小,因此它事实上就是一个扩展的停-等协议,只不过它是基于包停-等的,而不是基于字节停-等的. Nagle 算法完全由 TCP 协议的 ACK 机制决定,这会带来一些问题,比如如果对端 ACK 回复很快的话, Nagle 事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低.

Nagle 算法是 silly window syndrome(SWS)预防算法的一个半集.SWS 算法预防发送少量的数据, Nagle 算法是其在发送方的实现,而接收方要做的是不要通告缓冲空间的很小增长,不通知小窗口,除非缓冲区空间有显著地增长.这里显著地增长定义为完全大小的段( MSS)或增长到大于最大窗口的一半.

注意:BSD的实现是允许在空闲链接上发送大的写操作剩下的最后的小段,也就是说,当超过1个MSS数据发送时,内核先依次发送完n个MSS的数据包,然后再发送尾部的小数据包,其间不再延时等待。(假设网络不阻塞且接收窗口足够大)

举个例子,client端调用socket的write操作将一个int型数据(称为A块)写入到网络中,由于此时连接是空闲的(也就是说还没有未被确认的小段),因此这个int型数据会被马上发送到server端,接着,client端又调用write操作写入‘\r\n’(简称B块),这个时候,A块的ACK没有返回,所以可以认为已经存在了一个未被确认的小段,所以B块没有立即被发送,一直等待A块的ACK收到(大概40ms之后),B块才被发送。

这里还隐藏了一个问题,就是A块数据的ACK为什么40ms之后才收到?这是因为TCP/IP中不仅仅有nagle算法,还有一个TCP确认延迟机制 。当Server端收到数据之后,它并不会马上向client端发送ACK,而是会将ACK的发送延迟一段时间(假设为t),它希望在t时间内server端会向client端发送应答数据,这样ACK就能够和应答数据一起发送,就像是应答数据捎带着ACK过去。在我之前的时间中,t大概就是40ms。这就解释了为什么’\r\n’(B块)总是在A块之后40ms才发出。
  当然,TCP确认延迟40ms并不是一直不变的,TCP连接的延迟确认时间一般初始化为最小值40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。另外可以通过设置TCP_QUICKACK选项来取消确认延迟。

2. TCP_NODELAY 选项  

默认情况下,发送数据采用Nagle 算法。这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,使用TCP_NODELAY选项可以禁止Nagle 算法。

此时,应用程序向内核递交的每个数据包都会立即发送出去。需要注意的是,虽然禁止了Nagle 算法,但网络的传输仍然受到TCP确认延迟机制的影响。

3. TCP_CORK 选项  

所谓的CORK就是塞子的意思,形象地理解就是用CORK将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。设置该选项后,内核会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然若一定时间后(一般为200ms,该值尚待确认),内核仍然没有组合成一个MTU时也必须发送现有的数据(不可能让数据一直等待吧)。
  然而,TCP_CORK的实现可能并不像你想象的那么完美,CORK并不会将连接完全塞住。内核其实并不知道应用层到底什么时候会发送第二批数据用于和第一批数据拼接以达到MTU的大小,因此内核会给出一个时间限制,在该时间内没有拼接成一个大包(努力接近MTU)的话,内核就会无条件发送。也就是说若应用层程序发送小包数据的间隔不够短时,TCP_CORK就没有一点作用,反而失去了数据的实时性(每个小包数据都会延时一定时间再发送)。

4. Nagle算法与 CORK算法区别

Nagle算法和CORK算法非常类似,但是它们的着眼点不一样,Nagle算法主要避免网络因为太多的小包(协议头的比例非常之大)而拥塞,而CORK算法则是为了提高网络的利用率,使得总体上协议头占用的比例尽可能的小。如此看来这二者在避免发送小包上是一致的,在用户控制的层面上,Nagle算法完全不受用户socket的控制,你只能简单的设置TCP_NODELAY而禁用它,CORK算法同样也是通过设置或者清除TCP_CORK使能或者禁用之,然而Nagle算法关心的是网络拥塞问题,只要所有的ACK回来则发包,而CORK算法却可以关心内容,在前后数据包发送间隔很短的前提下(很重要,否则内核会帮你将分散的包发出),即使你是分散发送多个小数据包,你也可以通过使能CORK算法将这些内容拼接在一个包内,如果此时用Nagle算法的话,则可能做不到这一点。

TCP 粘包问题

在上面的 TCP 服务端和客户端收数据的时候填的都是1024字节,表示一次从操作系统的缓存区域收取最大1024字节的数据,当一次发送数据量小于1924字节的时候每次都可以把数据收取干净,这当然是没问题的,但是如果数据量大于1024呢?会发生什么现象.会发生粘包,

上述代码显示了第一中粘包情况,就是一方收取的数据量小于缓存中的数据量,造成粘包,另外一种是由于 Nagle算法导致的,当客户端连续发送两个数据量较小的包时,算法会将这两个一起发送造成粘包.

第二种粘包例子:

# 服务端
from socket import *
import subprocess

serverSock = socket(AF_INET, SOCK_STREAM)
serverSock.bind(('', 8086))
serverSock.listen(5)

conn, addr = serverSock.accept()
data1 = conn.recv(5)
print('第一次收:', data1)

data2 = conn.recv(5)
print('第二次收:', data2)

data3 = conn.recv(10)
print('第三次收:', data3)

data4 = conn.recv(5)
print('第四次收:', data4)

# 粘包问题是 tcp 协议流式传输数据的方式导致的
# 如何解决粘包问题:接收端能够精确地收干净每个数据包没有任何残留
# 客户端
from socket import *

clientSock = socket(AF_INET, SOCK_STREAM)
clientSock.connect(('', 8086))

# tcp 协议会将数据量较小且发送时间间隔的数据合并成一个包发送
clientSock.send(b'hello')
clientSock.send(b'world')
clientSock.send(b'musibii')
clientSock.send(b'1')
# 运行结果
第一次收: b'hello'
第二次收: b'world'
第三次收: b'musibii1'
第四次收: b''

可以看出当两次发包间隔时间较短且数据量较少时,会当成一个包发送出去.

那么怎么解决粘包问题呢?

一是因为数据量太大收取不完全导致的,二是因为两次发包间隔时间较短且数据量较小导致的,导致收取的一方将两次数据一次性收取完成.

解决办法一

还是以模拟 ssh 远程执行命令的代码作为例子:

首先根本原因是收取的一方不知道到底应该收取多少数据量导致的,那么我们在每次发包之前将数据的长度发过去,然后在发送真实数据,接收端先接收到真实数据的长度大小,然后依据真实数据长度大小来收包不就完美解决了吗.

看实例:

服务端

from socket import *
import subprocess
import struct # 该模块专门用来处理字节的数据类型

serverSock = socket(AF_INET, SOCK_STREAM)
serverSock.bind(('', 8088))
serverSock.listen(5)
while True:
    conn, addr = serverSock.accept()
    while True:
        try:
            cmd = conn.recv(1024)
            if len(cmd) == 0: 
                break
            obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            print(len(stdout) + len(stderr))
            data_len = len(stdout) + len(stderr)
            
            # 1. 先制作固定长度的报头
            header = struct.pack('i', data_len) # i表示转 int 型
            # 2. 发送报头
            conn.send(header)
            # 3. 发送真实数据
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break
    conn.close()
serverSock.close()

客户端

from socket import *
import struct

clientSock = socket(AF_INET, SOCK_STREAM)
clientSock.connect(('', 8088))

while True:
    cmd = input('>>>').strip()
    if len(cmd) == 0:
        continue
    clientSock.send(cmd.encode('utf-8'))
    # 1. 先收报头,从报头里解出真实数据长度
    header = clientSock.recv(1024)
    total_size = struct.unpack('i', header)[0]
    # 2. 接收真实数据
    cmd_res = b''
    recv_size = 0
    while recv_size < total_size:
        data = clientSock.recv(1024)
        recv_size += len(data)
        cmd_res += data
    print(cmd_res.decode('utf-8'))
clientSock.close()

可以看出来,数据可以收干净了,所以目前来讲是不存在粘包问题的,但是问题又来了,在实际应用中,想要传输的数据如果很大怎么办?这种办法也可以行得通吗?

当我把要传输的数据大小调大时:

# demo
import struct
data_len = struct.pack('1', 1111111111111111111)

运行结果

这说明 struct 转化 int 类型的时候是有大小限制的,数字太大的转不了,那么 struct 还有一个类型表示长整形的为 q, 来试试看.

# demo
import struct
data_len = struct.pack('q', 11111111111)

运行结果

可以看出来是可以转的,转成了8个字节,那么如果数字在大一点呢,总归有一个不能转的数字,那么这就不是最好的解决办法.

解决办法二

我们只想接收端接收到真实的数据长度就可以了,那么可不可以用一个容器把真实数据长度保存然后通过转化传输出去呢.

这就是在自己开发一款 C/S 程序时需要考虑的问题了,涉及到数据传输的话就需要自定义报头了.报头就是可以将真实数据的一些信息保存起来,先于真实数据发送出去

自定义报头

import struct
import json

header_dic = {
    'filename': 'musibii.jpg',
    'md5': '165069c8f668edb6b48f208f7a5c6a00',
    'total_size': 11111111111111111111111111111111111111
}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode('utf-8')
print(len(header_bytes), header_bytes)

data_bytes_len = struct.pack('i', len(header_bytes))

print(data_bytes_len,len(data_bytes_len))

运行结果

可以看出来,通过这种方法可以把数据量很大的东西发送给客户端.

具体解决办法

服务端

from socket import *
import json,struct

serverSock = socket(AF_INET, SOCK_STREAM)
serverSock.bind(('', 9090))
serverSock.listen(5)

while True:
    conn, addr = serverSock.accept()
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0:
                break
            obj = subprocess.Popen(data.decode('utf-8'), shell=True,
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            data_bytes_len = len(stdout) + len(stderr)
            
            # 1. 先制作报头
            header_dic = {
                'filename': 'musibii.jpg',
                'md5': '165069c8f668edb6b48f208f7a5c6a00',
                'total_size': data_bytes_len
            }
            header_json = json.dumps(header_dic)
            header_bytes = header_json.encode('utf-8')
            
            # 2. 先发送4个 bytes 的字典转化后的字节
            conn.send(struct.pack('i', len(header_bytes)))
            
            # 3. 发送完整报头
            conn.send(header_bytes)
            
            # 4. 发送真实数据
            conn.send(stdout)
            conn.send(stderr)
            
        except ConnectionResetError as e:
            print(e)
            break
    conn.close()
serverSock.close()

客户端

from socket import *
import struct,json

clientSock = socket(AF_INET, SOCK_STREAM)
clientSock.connect(('', 9090))

while True:
    cmd = input('>>>').strip()
    if len(cmd) == 0:
        continue
    clientSock.send(cmd.encode('utf-8'))
    
    # 1. 先收4bytes 的服务端发过来的转化报头
    header_size = struct.unpack('i', clientSock.recv(4))[0]
    
    # 2. 接收完整的报头,就是 header_dic
    header_bytes = clientSock.recv(header_size)
    
    # 3. 转化得到完整的报头字典
    header_json = header_bytes.decode('utf-8')
    header_dic  = json.loads(header_json)
    
    # 4. 根据 key 得到真实数据长度
    total_size = header_dic['total_size']
    
    # 5. 接受真正的数据
    cmd_res = b''
    recv_size = 0
    while recv_size < total_size:
        data = clientSock.recv(1024)
        recv_size += len(data)
        cmd_res += data
    print(cmd_res.decode('utf-8'))
clientSock.close()

这个程序就加了两个步骤,在发送真实数据之前发送了4个字节的报头,然后发送了真是的报头数据.

首先在服务端定义报头(字典类型),由字典转为 json 格式,由 json 格式转为二进制,由 struct 转成四个字节的数据,客户端收到4个字节struct 转化的数据, unpack 成二进制, decode 为 json 格式的数据类型,然后反序列化成 python 字典类型,得到里面的真实数据长度.然后服务端发送真实数据,客户端循环接收真实数据.

到此,就完美的解决了粘包的问题.如有不对的地方,欢迎指正.

Categories AI