Python进阶----粘包,解决粘包(旗舰版)

Python进阶----粘包,解决粘包(旗舰版)

一丶粘包

只有TCP有粘包现象,UDP永远不会粘包

什么是粘包

    存在于客户端接收数据时,不能一次性收取全部缓冲区中的数据.当下一次再有数据来时,缓冲区中剩余的数据会和新的数据'粘连'在一起.这就是粘包现象

### 什么是粘包?

# 存在于TCP/IP协议中数据粘连在一起,


### socket中造成粘包现象的原因是什么?
#  客户端不能一次性接收完缓冲区里的所有数据,
#  服务端接受客户端发送数据时,由于缓冲区没有存满,回先把数据'聚合'在客户端的缓冲区中,当客户端缓存区存满之后,在发给服务端


### 那些情况会发生粘包现象?

#  1. 客户端多次send少量数据, 会在客户端send输出缓冲区堆积, 当缓冲区堆积到一定程度 .会一次性把缓冲区的数据发给服务端
# 造成粘包现象
#  2. 客户端接收服务端发送的数据,会在客户端的recv输入缓存堆积,由于每次接收数据有限,当第二次服务端把数据发送过来时
# 数据会堆积在客户端的输入缓冲区.造成粘包

简单代码,产生粘包现象:👇

### 客户端 
# -*-coding:utf-8-*-
# Author:Ds
import  socket

client=socket.socket()

client.connect(('127.0.0.1',7777))


while 1:
    ui=input('请输入有命令:>>').strip()
    client.send(ui.encode('utf-8'))  # 向服务端发送指令
	
    ###***重点
      # 客户端 send完之后,  就会执行recv() 等待接收数据
      # 第一次 输入:dir指令        能够一次性接收完服务端返回的数据
      # 第二次 输入:ipconfig指令   不能一次性接收完服务端返回的数据,部分残余数据放在缓冲区中
      # 第三次 输入:dir指令时       由于上一次的返回的数据还没全部取完,新的指令数据还未到达缓冲区.	  # 总结: 如果缓冲区承载的数据过大时,每次只接收1024字节,当新的指令执行完,返回回来的数据会'怼'到缓冲区,造成数据粘连现象. 这就是粘包
    
    
    ser_data=client.recv(1024)  # 接收到服务端发送的字节码	

    print(ser_data.decode('utf-8'))   # 解码 输出

client.close()






### 服务端
# -*-coding:utf-8-*-
# Author:Ds
import socket
import subprocess # 执行 系统cmd指令
server=socket.socket()

server.bind(("127.0.0.1",7777))	# 绑定IP和端口
server.listen(5)


while 1:

    conn  , addr=server.accept() # 等待接收连接
    print(conn,addr)

    while 1:
        try:
            cmd=conn.recv(1024)     # 接收客户端发送过来的数据
            cmd=cmd.decode('utf-8') # 将客户端发送过来的字节进行解码

            # 执行客户端发来的系统命令
            obj=subprocess.Popen(cmd,
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
            # 这是执行系统命令之后产生正确结果或错误结果,
            # 字节形式
            result=(obj.stdout.read()+obj.stderr.read()).decode('gbk')

            conn.send(result.encode('utf-8') ) # 向客户端发送结果,字节形式
            
        except Exception :
            break
    conn.close()
server.close()

二丶系统缓冲区

socket缓冲区

    如下图:👇

      1.每个socket被创建后,都会分配两个缓冲区,输入缓冲区(recv)和输出缓冲区(send)

      2.send和recv都不是直接从网络中直接读取数据,而是从各自缓冲区读入和输出数据.

      3.数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制

img

I/O缓存区特性

# 1.I/O缓冲区在每个TCP套接字中单独存在;

# 2.I/O缓冲区在创建套接字时自动生成;

# 3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据;

# 4.关闭套接字将丢失输入缓冲区中的数据。

缓冲区的作用:

a、提高执行效率。 协调数据的收发(接受和处理)速度
b、减少内存负担。 减少和磁盘的交互
c、保持数据稳定性

利用socket查看系统缓冲区的大小(8K)

# -*-coding:utf-8-*-
# Author:Ds

import socket
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  # 重用ip地址和端口
server.bind(('127.0.0.1',8010))
server.listen(3)
print(server.getsockopt(socket.SOL_SOCKET,socket.SO_SNDBUF))  # 输出缓冲区大小
print(server.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF))  # 输入缓冲区大小

# 65536   1K=1024B  1B=8bit

三丶产生粘包的原因

两种原因:

    一:客户端(recv)不能一次性读取缓冲区中的数据.服务端发送的数据大于客户端能够接受的数据量,客户端下次接收的时候先去接收上次遗留在缓冲区中的数据.造成粘包

    代码如下:👇

### 服务端
# -*-coding:utf-8-*-
# Author:Ds

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()





#### 客户端 

# -*-coding:utf-8-*-
# Author:Ds

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,那么下次在输入命令,会继续取上次残留到缓存区的数据

    二:客户端(send)向服务端发送数据的时候,发送多条少量数据. 由于缓冲区的没有存满,缓冲区会把这些数据聚集在一起发送个服务端.造成粘包

    代码如下:👇

### 服务端
# -*-coding:utf-8-*-
# Author:Ds
import  socket

server=socket.socket()
server.bind(('127.0.0.1',7878))

server.listen(5)


conn ,addr = server.accept()
print(conn,addr)

data1=conn.recv(1024)
data2=conn.recv(1024)
data=data1+data2
print(data.decode('utf-8'))
conn.close()
server.close()


#### 客户端 

# -*-coding:utf-8-*-
# Author:Ds
import socket

client=socket.socket()
client.connect(('127.0.0.1',7878))

client.send('123'.encode('utf-8'))
client.send('abc'.encode('utf-8'))
client.close()


## 两次返送信息时间间隔太短,数据小,造成服务端一次收取

四丶解决粘包(recv的工作原理)

简易版解决粘包

### 客户端
# -*-coding:utf-8-*-
# Author:Ds

import  socket

client=socket.socket()

client.connect(('127.0.0.1',9989))

while 1:
    ui=input('请输入指令:>>>').strip()
    if len(ui)<0:continue
    if ui.upper()=='Q':break

    client.send(ui.encode('utf-8'))

    message_size=int(client.recv(1024).decode('utf-8'))
    client.send(b'recv_ready')
    recv_size=0
    data=b''
    while recv_size<message_size:
        data+=client.recv(1024)
        recv_size+=len(data)

    print(data.decode('utf-8'))
### 服务端

# -*-coding:utf-8-*-
# Author:Ds
import socket
import subprocess
server=socket.socket()

server.bind(('127.0.0.1',9989))

server.listen(5)

while 1:
    conn,addr=server.accept()
    print(conn,addr)

    while 1:
        cmd=conn.recv(1024).decode('utf-8')
        obj=subprocess.Popen(cmd,
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        result=(obj.stdout.read()+obj.stderr.read()).decode('gbk').encode('utf-8')
        conn.send(str(len(result)).encode('utf-8'))

        data=conn.recv(1024).decode('utf-8')
        if data=='recv_ready':
            conn.sendall(result)
    conn.close()
server.close()

使用struct模块

  将一串数字转换成等长的字节.

    格式多种: 常用 i

img

    代码如下:👇

# -*-coding:utf-8-*-
# Author:Ds

import struct
#  'i' 的取值范围  -2147483648 <= number <= 2147483647

#   i 模式 等长四位 , 将一个数字转化成等长度的bytes类型。
res=struct.pack('i',123456)
print(res,len(res))

###  通过unpack反解回来
data=struct.unpack('i',res)
print(data)


### 但是通过struct 处理不能处理太大
ret = struct.pack('q', 4323241232132324)
print(ret, type(ret), len(ret))  # 报错

low版本解决粘包

### 服务端

import socket
import subprocess
import struct
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()
            
            # 1 制作固定报头
            total_size = len(correct_msg) + len(error_msg)
            header = struct.pack('i', total_size)
            
            # 2 发送报头
            conn.send(header)
            
            # 发送真实数据:
            conn.send(correct_msg)
            conn.send(error_msg)
        except ConnectionResetError:
            break

conn.close()
phone.close()


# 但是low版本有问题:
# 1,报头不只有总数据大小,而是还应该有MD5数据,文件名等等一些数据。
# 2,通过struct模块直接数据处理,不能处理太大。
### 客户端

import socket
import struct
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

phone.connect(('127.0.0.1',8080))


while 1:
    cmd = input('>>>').strip()
    if not cmd: continue
    phone.send(cmd.encode('utf-8'))
    
    # 1,接收固定报头
    header = phone.recv(4)
    
    # 2,解析报头
    print(struct.unpack('i', header)) # 这是一个元组 (xxx,)
    total_size = struct.unpack('i', header)[0] # 取第一个元素
    
    # 3,根据报头信息,接收真实数据
    recv_size = 0
    res = b''
    
    while recv_size < total_size:
        
        recv_data = phone.recv(1024)
        res += recv_data
        recv_size += len(recv_data)

    print(res.decode('gbk'))

phone.close()

旗舰版解决粘包

### 步骤解释
# 报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节

#发送时:
	1.先发报头长度
	2.再编码报头内容然后发送
	3.最后发真实内容


#接收时:
	1.先手报头长度,用struct取出来
	2.根据取出的长度收取报头内容,然后解码,反序列化
	3.从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

​ 案例代码,如下👇:

### 客户端

# -*-coding:utf-8-*-
# Author:Ds
import socket
import struct
import json

client = socket.socket()    # 实例化 socket对象

client.connect(('127.0.0.1', 8898)) # socket 连接IP和端口

while 1:
        ui = input('请输入指令:>>').strip()
        #  1. 发送 用户输入 指令
        client.send(ui.encode('utf-8'))

        #2.  接收 struct 封装的头 字节形式 ,  接收4个字节 head
        head = client.recv(4)
        # 3. struct反解 ,获得自定义报头的长度
        dic_length = struct.unpack('i', head)[0]

        #4.  接收自定义字典报头内容 字节形式
        head_dic = client.recv(int(dic_length))

        #5.  反序列化自定义字典,先解码在反序列化
        dic = json.loads(head_dic.decode('utf-8'))
        #6. 得到 真实内容的长度
        content_length = dic['size']

        # 7 设置一个字节变量 用于接收真实数据
        content = b''
        # 8 设置一个客户端接收长度
        recv_size = 0

        # 9 当客户端接收长度 小于 源数据长度,一直接收
        while recv_size < content_length:
            # 累加 真实数据,以字节形式
            content += client.recv(1024)
            # 累加 客户端接收的长度
            recv_size += len(content)
            
        # 接收完毕,解码内容
        print(content.decode('utf-8'))
client.close()

### 服务端
# -*-coding:utf-8-*-
# Author:Ds
import socket
import struct
import json
import time
import subprocess

server = socket.socket()            # 实例化 socket对象

server.bind(('127.0.0.1', 8898))    # 绑定ip和端口

server.listen(5)                    # 邦迪监听连接

while 1:
    conn, addr = server.accept()    # 等待连接
    print(conn, addr)               #  打印连接,和连接ip

    while 1:
        try:
            # 1. 接收客户端发送的 指令 ,字节形式
            cmd = conn.recv(1024)
            # 2. 执行指令,得到结果 , 指令是字符形式(需要解码)
            obj = subprocess.Popen(cmd.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)

            #3.得到结果字节 ,转换成utf-8的字节格式
            result = (obj.stdout.read() + obj.stderr.read()).decode('gbk').encode('utf-8')

            #4. 获得真实数据的字节长度
            total_res_bytes = len(result)

            #5. 自定制字典报头
            head_dic = {
                'time': time.localtime(time.time()),
                'size': total_res_bytes, # 字节长度
                'MD5': 'XXXXX',
                'file_name': '婚前视频',
            }

            # 6. 序列化字典 ,并将其转换成字节形式
            head_dic_bytes = json.dumps(head_dic).encode('utf-8')

            #7.  使用 struct 封装报头字典head_dic_bytes ,固定长度(4个字节)
            #   封装成字节,发送给客户端,还是按照字节取出来.
            head = struct.pack('i', len(head_dic_bytes))

            # 8 , 先将固定头发送给客户端
            conn.send(head)
            # 9 . 再将自定制报头发送给客户端
            conn.send(head_dic_bytes)
            # 10. 最后将真实结果发送给客户端
            conn.send(result)
            ### 这里就是拼接字节
                # 格式: 固定头 + 自定义报头 +真实数据
        except Exception:
            break
    conn.close()
server.close()

posted @ 2019-07-17 18:17  染指未来  阅读(389)  评论(0编辑  收藏  举报