python tcp黏包和解决方法

一、TCP协议 粘包现象 和解决方案

黏包现象
让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)
执行远程命令的模块

需要用到模块subprocess

subprocess通过子进程来执行外部指令,并通过input/output/error管道,获取子进程的执行的返回信息。

import subprocess
sub_obj = subprocess.Popen(
    ‘ls‘,  #系统指令
    shell=True,  #固定
    stdout=subprocess.PIPE, #标准输出  PIPE 管道,保存着指令的执行结果
    stderr=subprocess.PIPE  #标准错误输出
)
print(‘正确输出‘,sub_obj.stdout.read().decode(‘gbk‘))
print(‘错误输出‘,sub_obj.stderr.read().decode(‘gbk‘))

基于tcp协议实现的黏包

###server
while 1:
    from_client_cmd=conn.recv(1024)
    print(from_client_cmd.decode(‘utf-8‘))#接收客户端数据解码

    sub_obj=subprocess.Popen(
    from_client_cmd.decode(‘utf-8‘),
    shell=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE
    )
###client
import socket
client=socket.socket()
client.connect((‘127.0.0.1‘,8001))

while 1:
    cmd=input(‘请输入指令:‘)
    client.send(cmd.encode(‘utf-8‘))
    server_cmd_result=client.recv(1024)
    print(server_cmd_result.decode(‘gbk‘))

这就是黏包现象

因为每次执行,固定为1024字节。它只能接收到1024字节,那么超出部分怎么办?
等待下一次执行命令dir时,优先执行上一次,还没有传完的信息。传完之后,再执行dir命令

 总结:

发送过来的一整条信息
由于server端没有及时接受
后来发送的数据和之前没有接收完的数据黏在了一起
这就是著名的黏包现象

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。
不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

解决方案一

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

原理:

黏包现象的成因

  你不知道在哪儿断句

解决问题

  在发送数据的时候,先告诉对方要发送的大小就可以了

自定义协议

先和服务端商量好,发送多少字节,再传输数据。

####server
# 原理
# 黏包现象的成因
    # 你不知道在哪儿断句
# 解决问题
    # 在发送数据的时候,先告诉对方要发送的大小就可以了
        # 在发送的时候 先发送数据的大小 在发送内容
        # 在接受的时候 先接受大小 再根据大小接受内容
# 自定义协议

#_*_coding:utf-8_*_
from socket import *
ip_port=(‘127.0.0.1‘,8080)

tcp_socket_server=socket()
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)

conn,addr=tcp_socket_server.accept()
lenth = conn.recv(1)  # 接收1个字节,返回 b‘5‘
#print(lenth)
lenth = int(lenth.decode(‘utf-8‘))  # 转化字符串,返回5

data1=conn.recv(lenth)  # 接收5字节,返回 b‘hello‘
lenth2 = conn.recv(1)  # 接收1个字节
lenth2 = int(lenth2.decode(‘utf-8‘))  # 转化字符串,返回3
data2=conn.recv(lenth2)  # 接收3个字节,返回b‘egg‘

print(‘----->‘,data1.decode(‘utf-8‘))
print(‘----->‘,data2.decode(‘utf-8‘))

conn.close()
tcp_socket_server.close()
####client
import socket
BUFSIZE=1024
ip_port=(‘127.0.0.1‘,8080)

s=socket.socket()
res=s.connect_ex(ip_port)  # 功能与connect(address)相同,但是成功返回0,失败返回errno的值
lenth = str(len(‘hello‘)).encode(‘utf-8‘)  # 获取hello的字符的长度,并转化为str,最后编码
s.send(lenth)  # 发送数字5
s.send(‘hello‘.encode(‘utf-8‘))  # 发送hello
lenth = str(len(‘egg‘)).encode(‘utf-8‘)  # 获取长度,结果为3
s.send(lenth)  # 发送3
s.send(‘egg‘.encode(‘utf-8‘))  # 发送egg

s.close()
先执行服务端,再执行客户端,执行输出:

-----> hello
-----> egg
存在的问题:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

解决方案进阶

刚刚的方法,问题在于我们我们在发送

我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。

struct模块

该模块可以把一个类型,如数字,转成固定长度的bytes

import struct
ret = struct.pack(‘i‘,1000000)  # i表示int类型
print(ret)
print(len(ret))  # 返回4

ret1 = struct.unpack(‘i‘,ret)  # 按照给定的格式(fmt)解析字节流string,返回解析出来的tuple
print(ret1)  # 返回一个元组

执行输出:
b‘@B\x0f\x00‘
4
(1000000,)

借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。

发送时 接收时

先发报头长度

先收报头长度,用struct取出来
再编码报头内容然后发送 根据取出的长度收取报头内容,然后解码,反序列化
最后发真实内容 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
####server
import socket
import subprocess
import struct
server = socket.socket()
ip_port = (‘127.0.0.1‘,8001)
server.bind(ip_port)
server.listen()
conn,addr = server.accept()
while 1:
    from_client_cmd = conn.recv(1024)#接收的大小

    print(from_client_cmd.decode(‘utf-8‘))#答应查看一下
    #接收到客户端发送来的系统指令,我服务端通过subprocess模块到服务端自己的系统里面执行这条指令
    sub_obj = subprocess.Popen(
        from_client_cmd.decode(‘utf-8‘),#解析客户端发来的命令
        shell=True,
        stdout=subprocess.PIPE,  #正确结果的存放位置
        stderr=subprocess.PIPE   #错误结果的存放位置
    )
    #从管道里面拿出结果,通过subprocess.Popen的实例化对象.stdout.read()方法来获取管道中的结果
    std_msg = sub_obj.stdout.read()#管道里面拿出结果

    #为了解决黏包现象,我们统计了一下消息的长度,先将消息的长度发送给客户端,客户端通过这个长度来接收后面我们要发送的真实数据
    std_msg_len = len(std_msg)
    print(‘指令的执行结果长度>>>>‘,len(std_msg))

    msg_lenint_struct = struct.pack(‘i‘,std_msg_len)#把长度byes加上int标识 4个长度

    conn.send(msg_lenint_struct+std_msg)#发送拼接给客户端
####client
import socket
import struct
client = socket.socket()
client.connect((‘127.0.0.1‘,8001))
while 1:
    cmd = input(‘请输入指令:‘)
    #发送指令
    client.send(cmd.encode(‘utf-8‘))#发送给server你想要执行的命令
    #接收数据长度,首先接收4个字节长度的数据,因为这个4个字节是长度
    server_res_len = client.recv(4)
    msg_len = struct.unpack(‘i‘,server_res_len)[0]

    print(‘来自服务端的消息长度‘,msg_len)
    #通过解包出来的长度,来接收后面的真实数据
    server_cmd_result = client.recv(msg_len)

    print(server_cmd_result.decode(‘gbk‘))

简单的文件传送

文件的上传和下载

需要文件的名字,文件的大小,文件的内容

自定义一个文件传输协议:

{‘filesize‘:000,‘filename‘:‘XXXX‘}

###server
import os
import json
import struct
import socket

# E:\BaiduYunDownload\AppleEthernet-master.zip
sk = socket.socket()
sk.bind((‘127.0.0.1‘, 9000))
sk.listen()

conn, addr = sk.accept()
print(addr)
dic = {‘filename‘: ‘python18期 2组员资料.rar‘,
       ‘filesize‘: os.path.getsize(r‘E:\python18期 2组员资料.rar‘)}
str_dic = json.dumps(dic).encode(‘utf-8‘)
dic_len = struct.pack(‘i‘, len(str_dic))
conn.send(dic_len)
conn.send(str_dic)
with open(r‘E:\python18期 2组员资料.rar‘, ‘rb‘) as f:
    content = f.read()
conn.send(content)
conn.close()
sk.close()
###client
import json
import struct
import socket
sk = socket.socket()
sk.connect((‘127.0.0.1‘, 9000))
dic_len = sk.recv(4)
dic_len = struct.unpack(‘i‘, dic_len)[0]
str_dic = sk.recv(dic_len).decode(‘utf-8‘)
dic = json.loads(str_dic)

with open(dic[‘filename‘], ‘wb‘) as f:
    content = sk.recv(dic[‘filesize‘])
    f.write(content)
sk.close()

注意:

大文件的传输,不能一次性读到内存里

上传一个视频,几台电脑之间能互相传,视频要3个G左右。

进阶需求,加一个登陆功能

server.py

import os
import json
import struct
import socket
import hashlib

sk = socket.socket()
sk.bind((‘127.0.0.1‘,9999))
sk.listen()

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

filename = ‘[电影天堂www.dy2018.com]移动迷宫3:死亡解药BD国英双语中英双字.mp4‘  # 文件名
absolute_path = os.path.join(‘E:\BaiduYunDownload‘,filename)  # 文件绝对路径
buffer_size = 1024*1024  # 缓冲大小,这里表示1MB

md5obj = hashlib.md5()
with open(absolute_path, ‘rb‘) as f:
    while True:
        content = f.read(buffer_size)  # 每次读取指定字节
        if content:
            md5obj.update(content)
        else:
            break  # 当内容为空时,终止循环

md5 = md5obj.hexdigest()
print(md5)  # 打印md5值

dic = {‘filename‘:filename, ‘filename_md5‘:str(md5),‘buffer_size‘:buffer_size,
       ‘filesize‘:os.path.getsize(absolute_path)}
str_dic = json.dumps(dic).encode(‘utf-8‘)  # 将字典转换为json
dic_len = struct.pack(‘i‘, len(str_dic))  # 获取字典长度,转换为struct
conn.send(dic_len)  # 发送字典长度
conn.send(str_dic)  # 发送字典

with open(absolute_path, ‘rb‘) as f:  # 打开文件
    while True:
        content = f.read(buffer_size)  # 每次读取指定大小的字节
        if content:  # 判断内容不为空
            conn.send(content)  # 每次读取指定大小的字节
        else:
            break

conn.close()  # 关闭连接
sk.close()  # 关闭套接字

client.py

import json
import struct
import socket
import hashlib
import time

start_time = time.time()
sk = socket.socket()
sk.connect((‘127.0.0.1‘,9999))

dic_len = sk.recv(4)  # 接收4字节,因为struct的int为4字节
dic_len = struct.unpack(‘i‘,dic_len)[0]  # 反解struct得到元组,获取元组第一个元素
#print(dic_len)  # 返回一个数字
str_dic = sk.recv(dic_len).decode(‘utf-8‘)  # 接收指定长度,获取完整的字典,并解码
#print(str_dic)  # json类型的字典
dic = json.loads(str_dic)  # 反序列化得到真正的字典
#print(dic)  # 返回字典

md5 = hashlib.md5()
with open(dic[‘filename‘],‘wb‘) as f:
    while True:
        content = sk.recv(dic[‘buffer_size‘])
        if not content:
            break
        md5.update(content)
    md5 = md5.hexdigest()
    print(md5)  # 打印md5值

    if dic[‘filename_md5‘] == str(md5):
        f.write(content)
        print(‘md5校验正确--下载成功‘)
    else:
        print(‘文件验证失败‘)

sk.close()

end_time = time.time()
print(‘本次下载花费了{}秒‘.format(end_time-start_time))

先执行server.py,再执行client.py

server输出:

(‘127.0.0.1‘, 54230)
30e63a254cf081e8e93c036b21057347

client输出:

30e63a254cf081e8e93c036b21057347
md5校验正确--下载成功
本次下载花费了25.687340021133423秒

import json

import struct

import socket

import hashlib

import time

start_time = time.time()

sk = socket.socket()

sk.connect((‘127.0.0.1‘,9999))

dic_len = sk.recv(4)  # 接收4字节,因为struct的int为4字节

dic_len = struct.unpack(‘i‘,dic_len)[0]  # 反解struct得到元组,获取元组第一个元素

#print(dic_len)  # 返回一个数字

str_dic = sk.recv(dic_len).decode(‘utf-8‘)  # 接收指定长度,获取完整的字典,并解码

#print(str_dic)  # json类型的字典

dic = json.loads(str_dic)  # 反序列化得到真正的字典

#print(dic)  # 返回字典

md5 = hashlib.md5()

with open(dic[‘filename‘],‘wb‘) as f:

    while True:

        content = sk.recv(dic[‘buffer_size‘])

        if not content:

            break

        md5.update(content)

    md5 = md5.hexdigest()

    print(md5)  # 打印md5值

    if dic[‘filename_md5‘== str(md5):

        f.write(content)

        print(‘md5校验正确--下载成功‘)

    else:

        print(‘文件验证失败‘)

sk.close()

end_time = time.time()

print(‘本次下载花费了{}秒‘.format(end_time-start_time))

原文地址:https://www.cnblogs.com/zaizai1573/p/10230973.html

时间: 2024-10-02 06:23:15

python tcp黏包和解决方法的相关文章

网络通信中TCP出现的黏包以及解决方法 socket 模拟黏包

粘包问题概述 1.1  描述背景 采用TCP协议进行网络数据传送的软件设计中,普遍存在粘包问题.这主要是由于现代操作系统的网络传输机制所产生的.我们知道,网络通信采用的套接字(socket)技术,其实现实际是由系统内核提供一片连续缓存(流缓冲)来实现应用层程序与网卡接口之间的中转功能.多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使数据包的边界发生错位,导致读出错误的数据分包,进而曲解原始数据

python中黏包现象

#黏包:发送端发送数据,接收端不知道应如何去接收造成的一种数据混乱现象. #关于分包和黏包: #黏包:发送端发送两个字符串"hello"和"word",接收方却一次性接收到"helloword" #分包:发送端发送字符串"helloword",接收方却受到了两个字符串"hello"和"word" #虽然socket环境有这些问题,但是TCP传输数据能保证几点: #顺序不变,发送端发送he

netty]--最通用TCP黏包解决方案

netty]--最通用TCP黏包解决方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender 2017年02月19日 15:02:11 惜暮 阅读数:14555 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/u010853261/article/details/55803933 前面已经说过: TCP以流的方式进行数据传输,上层应用协议为了对消息进行区分,往往采用如下4种方式. (1)消息长度固定

Tcp编程常见问题及解决方法总结

问题1.粘包问题 解决方法一:TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满: 解决方法二:发送固定长度的消息 解决方法三:把消息的尺寸与消息一块发送 解决方法四:双方约定每次传送的大小 解决方法五:双方约定使用特殊标记来区分消息间隔 解决方法六:标准协议按协议规则处理,如Sip协议 问题2.字符串编码问题 将中文字符串用utf8编码格式转换为字节数组发送时,一个中文字符可能会占用2-4个字节(假设为3个字节),这3个

黏包以及解决

1 import struct 2 ret = struct.pack('i', 11111111) #把一串数据转化成长度为4的字节 3 print(ret, len(ret)) #b'\xc7\x8a\xa9\x00' 4 4 num = struct.unpack('i', ret) 5 print(num) #(11111111,) 返回的是一个元组,所以要取第一个 6 print(num[0]) #11111111 struct 模块 黏包:同时执行多条命令之后,得到的结果很有可能只有

sun.misc.BASE64Encoder找不到包,解决方法

右键项目->属性->java bulid path->jre System Library->access rules->resolution选择accessible,下面填上** 点击确定即可 Jun-1: sun.misc.BASE64Encoder找不到jar包的解决方法 1.右键项目->属性->java bulid path->jre System Library->access rules->resolution选择accessible

Android - "已安装了存在签名冲突的同名数据包",解决方法!

错误提示:已安装了存在签名冲突的同名数据包. 解决方法:打开Android Studio,打开logcat,用usb线连接你出错的手机,识别出手机之后,在你的项目后面,点击"run"按钮,随后AS会提示你,你删除先前安装的APP就可以了! 原文地址:https://www.cnblogs.com/sunylat/p/9962767.html

python 进程内存增长问题, 解决方法和工具

python 进程内存增长问题, 解决方法和工具 表现 解决方法 定位问题过程 gdb-python: 搞清楚python程序在做什么 准备gdb 接入gdb 查看线程 查看调用栈 coredump 其他命令 pyrasite: 连接进入python程序 psutil 查看python进程状态 guppy 取得内存使用的各种对象占用情况 无法回收的对象 不可回收对象的例子 ?? objgraph 查找循环引用 表现 运行环境: # uname -a Linux ** 3.10.0-327.el7

Python之黏包

黏包现象 让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd) res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) #结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的#就是GBK编码的,在接收端需要用GBK解码 #且只能从管道里读一次结果 同时执行多条