什么是粘包,如何解决?
小的知识点
s1 = 'q'
s2 = b'q'
print(type(s1),type(s2))
print(s1.encode('utf-8'))
#<class 'str'> <class 'bytes'>
# b'q'
bytes 类型:
ASCII 字符,在字符前面 b " "
非 ASCII 类型,比如中文,先转化为字符串,然后再转为 bytes 类型
s1 = "太白jx"
print(len(s1)) # 4
b1 = s1.encode('utf=8') # bytes 类型
print(b1) # b'\xe5\xa4\xaa\xe7\x99\xbdjx'
print(len(b1)) # 8
一 : socket
看socket之前,先来回顾一下五层通讯流程:
但实际上从传输层开始以及以下,都是操作系统帮咱们完成的,下面的各种包头封装的过程,用咱们去一个一个做么?NO!
Socket又称为套接字,它是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。当我们使用不同的协议进行通信时就得使用不同的接口,还得处理不同协议的各种细节,这就增加了开发的难度,软件也不易于扩展(就像我们开发一套公司管理系统一样,报账、会议预定、请假等功能不需要单独写系统,而是一个系统上多个功能接口,不需要知道每个功能如何去实现的)。
于是UNIX BSD就发明了socket这种东西,socket屏蔽了各个协议的通信细节,使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。这就好比操作系统给我们提供了使用底层硬件功能的系统调用,通过系统调用我们可以方便的使用磁盘(文件操作),使用内存,而无需自己去进行磁盘读写,内存管理。socket其实也是一样的东西,就是提供了tcp/ip协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用tcp/ip协议的功能了。
其实站在你的角度上看,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。也有人将socket说成ip+port,因为ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序。 所以我们只要确立了ip和port就能找到一个应用程序,并且使用socket模块来与之通信。
第一版,单个客户端与服务端通信(low版) 基于 TCP 协议的 socket 的简单通信
server : 服务端
# 网络通信与打电话(诺基亚)是一样的。
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.bind(('127.0.0.1',8080)) # 0 ~ 65535 1024之前系统分配好的端口 绑定电话卡
phone.listen(5) # 同一时刻有5个请求,但是可以有N多个链接。 开机。
conn, client_addr = phone.accept() # 接电话
print(conn, client_addr, sep='\n')
from_client_data = conn.recv(1024) # 一次接收的最大限制 bytes
print(from_client_data.decode('utf-8'))
conn.send(from_client_data.upper())
conn.close() # 挂电话
phone.close() # 关机
**client : 客户端 **
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
phone.send('hello'.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data)
phone.close() # 挂电话
第二版,通信循环 基于TCP 协议的 socket 循环通信
server
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
conn, client_addr = phone.accept()
print(conn, client_addr, sep='\n')
while 1: # 循环收发消息
try:
from_client_data = conn.recv(1024)
print(from_client_data.decode('utf-8'))
conn.send(from_client_data + b'SB')
except ConnectionResetError:
break
conn.close()
phone.close()
client
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
while 1: # 循环收发消息
client_data = input('>>>')
phone.send(client_data.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('utf-8'))
phone.close() # 挂电话
第三版, 通信,连接循环 基于 TCP 协议的 socket 连接 加循环通信
server
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
while 1 : # 循环连接客户端
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
from_client_data = conn.recv(1024)
print(from_client_data.decode('utf-8'))
conn.send(from_client_data + b'SB')
except ConnectionResetError:
break
conn.close()
phone.close()
client
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
while 1:
client_data = input('>>>')
phone.send(client_data.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('utf-8'))
phone.close() # 挂电话
远程执行命令的示例 : 基于 TCP 协议的 socket 应用实例,执行远程操作
import socket
import subprocess
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
while 1 : # 循环连接客户端
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
conn.send(correct_msg + error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
while 1:
cmd = input('>>>')
phone.send(cmd.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('gbk'))
phone.close() # 挂电话
粘包
什么是粘包??
指 TCP 协议中,发送方发送的若干数据到接收方接收时粘成一包,从接收缓冲区来看,后一包数据的头,紧接着前一包数据的尾.
什么情况出现粘包???
- 第一种 : 连续短暂的 send 多次(数据量较小),统一缓存到 recv 缓存区,你的数据会统一发送出去
- 第二种 : send 的数据过大,大于对方 recv 的上限时,对方第二次 recv 时,会接收上一次没有 recv 完的剩余数据
讲粘包之前先看看socket缓冲区的问题:
为什么设置缓冲区 ???
- 暂时缓存一些数据
- 缓存地区如果你的网络存在波动,保证数据收发稳定匀速
**缺点 : **
造成了粘包现象
为什么会有报错???
有时候把中文劈成两半,而接收的又是字节,所以报错
两种情况下会发生粘包。
1,接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
老师课堂说的意思是 : 连续短暂的 send 多次(数据量较小,你的数据会统一发送出去)
import socket
import subprocess
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1: # 循环连接客户端
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
conn.send(correct_msg + error_msg)
except ConnectionResetError:
break
conn.close()
phone.close()
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号
while 1:
cmd = input('>>>')
phone.send(cmd.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('gbk'))
phone.close()
# 由于客户端发的命令获取的结果大小已经超过1024,那么下次在输入命令,会继续取上次残留到缓存区的数据。
2,发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,会合到一起,产生粘包) send 的数据过大,大于对方 recv 的上限时,对方第二次 recv 时,会接收上一次没有recv完剩余的
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
conn, client_addr = phone.accept()
frist_data = conn.recv(1024)
print('1:',frist_data.decode('utf-8')) # 1: helloworld
second_data = conn.recv(1024)
print('2:',second_data.decode('utf-8'))
conn.close()
phone.close()
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
phone.send(b'hello')
phone.send(b'world')
phone.close()
# 两次返送信息时间间隔太短,数据小,造成服务端一次收取
粘包的解决方案:
先介绍一下struct模块:
该模块可以把一个类型,如数字,转成固定长度的bytes
[](javascript:void(0)😉
import struct
# 将一个数字转化成等长度的bytes类型。
ret = struct.pack('i', 183346)
print(ret, type(ret), len(ret))
# 通过unpack反解回来
ret1 = struct.unpack('i',ret)[0]
print(ret1, type(ret1), len(ret1))
# 但是通过struct 处理不能处理太大
ret = struct.pack('l', 4323241232132324)
print(ret, type(ret), len(ret)) # 报错
如何解决粘包?
服务器发一次数据,10000 字节,客户端接收数据时,循环接收每次至多接收 1024 个字节,直至将所有的字节全部接收完毕,将接收的数据拼接在一起,最后解码.
遇到问题
-
recv 接收的次数无法确定,你发送总具体数据之前,先给我发送一个总数据的长度,5000 字节,然后再发送总数据
客户端 : 先接收一个长度,5000 字节,然后我再循环,
然后我再循环recv 控制循环的条件就是只要你接受的数据< 5000 一直接收。
-
遇到的问题: 总数据的长度转化成的字节数不固定
服务端:
conn.send(total_size)
conn.send(result)
total_size int类型
客户端:
total_size_bytes = phone.recv(4)
total_size
data = b''
while len(data) < total_size:
data = data + phone.recv(1024)
你要将total_size int类型转化成bytes类型才可以发送
387 ---- > str(387) '387' ---->bytes b'387' 长度 3bytes
4185 ----> str(4185) '4185' ---->bytes b'4185' 长度 4bytes
18000------------------------------------------------------> 长度 5bytes
我们要解决:
将不固定长度的int类型转化成固定长度的bytes并且还可以翻转回来。
stuct 模块 将不固定长度的数字,转化为固定长度的 bytes,然后再翻转回来
server端
# 1. 粘包第一种: send的数据过大,大于对方recv的上限时,对方第二次recv时,会接收上一次没有recv完的剩余的数据。
import socket
import subprocess
import struct
phone = socket.socket()
phone.bind(('127.0.0.1',8848))
phone.listen(2)
# listen: 2 允许有两个客户端加到半链接池,超过两个则会报错
while 1:
conn,addr = phone.accept() # 等待客户端链接我,阻塞状态中
# print(f'链接来了: {conn,addr}')
while 1:
try:
from_client_data = conn.recv(1024) # 接收命令
if from_client_data.upper() == b'Q':
print('客户端正常退出聊天了')
break
obj = subprocess.Popen(from_client_data.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
result = obj.stdout.read() + obj.stderr.read()
total_size = len(result)
print(f'总字节数:{total_size}')
# 1. 制作固定长度的报头
head_bytes = struct.pack('i',total_size)
# 2. 发送固定长度的报头
conn.send(head_bytes)
# 3. 发送总数据
conn.send(result)
except ConnectionResetError:
print('客户端链接中断了')
break
conn.close()
phone.close()
# import struct
# # 将一个数字转化成等长度的bytes类型。
# ret = struct.pack('i', 180000000)
# # print(ret, type(ret), len(ret))
#
# # 通过unpack反解回来
# ret1 = struct.unpack('i',ret)[0]
# # print(ret1)
# print(ret1, type(ret1))
# 总数据:总数据长度
# s1 = 'fjdslf太白金星jsfk疯狂夺金分离式的疯狂的数量方式登记拉开lagfdkjglkhjklh'
# b1 = s1.encode('utf-8')
# print(b1)
# print(len(b1))
client
import socket
import struct
phone = socket.socket()
phone.connect(('127.0.0.1',8848))
while 1:
to_server_data = input('>>>输入q或者Q退出').strip().encode('utf-8')
if not to_server_data:
# 服务端如果接受到了空的内容,服务端就会一直阻塞中,所以无论哪一端发送内容时,都不能为空发送
print('发送内容不能为空')
continue
phone.send(to_server_data)
if to_server_data.upper() == b'Q':
break
# 1. 接收报头
head_bytes = phone.recv(4)
# 2. 反解报头
total_size = struct.unpack('i',head_bytes)[0]
total_data = b''
while len(total_data) < total_size:
total_data += phone.recv(1024)
print(len(total_data))
print(total_data.decode('gbk'))
phone.close()