python 中粘包问题(tcp通信下)python中上传下载文件实现方法。

一花一世界,一叶一菩提。

粘包现象

粘包的现象:

[root@localhost]# netstat -ano
活动连接

  协议  本地地址          外部地址        状态           PID
  TCP    0.0.0.0:135            0.0.0.0:0              LISTENING       468
  TCP    0.0.0.0:445            0.0.0.0:0              LISTENING       4
  TCP    0.0.0.0:5040           0.0.0.0:0              LISTENING       1988
  TCP    0.0.0.0:5357           0.0.0.0:0              LISTENING       4
  TCP    0.0.0.0:11200          0.0.0.0:0              LISTENING       5420
  TCP    0.0.0.0:16422          0.0.0.0:0              LISTENING       5420
  TCP    0.0.0.0:27036          0.0.0.0:0              LISTENING       7232
  TCP    0.0.0.0:49664          0.0.0.0:0              LISTENING       660
  TCP    0.0.0.0:49665          0.0.0.0:0              LISTENING       1444
  TCP    0.0.0.0:49666          0.0.0.0:0              LISTENING       1744
  TCP    0.0.0.0:49667          0.0.0.0:0              LISTENING       3684
  TCP    0.0.0.0:49671          0.0.0.0:0              LISTENING       736
  TCP    0.0.0.0:49679          0
[root@localhost]# 

明显可以看出这是一个==不完整的返回!==那么我们如果在输入一次 dir结果会是什么样的呢?

篇幅问题请自行测试吧。(文章后面可能会给附加文件!)**

在这里插入图片描述

明显的一个粘包问题!

为什么会有粘包问题呢?

须知:只有TCP有粘包现象,UDP永远不会粘包,为何,且听我娓娓道来

首先需要掌握一个socket收发消息的原理

img

我们都知道我们把TCP协议称之为流水协议,就是把它当做流水一样去看待,一滴滴的水从一头滴落到另外一头。

我们来举个例子:

第一次我们客户端传输了700个字节,然后我们服务端开始取,因为我们设置的是1024个字节为收取,第一次我们收取到的是700个字节。

那么第二次我们客户端又传输了1500个字节,然后我们服务端开始取,因为我们设置的是1024个字节为收取所以这一次我们最多可以收取到的是1024个字节。那么我们还收取第二次吗?(除非是在循环取的,不然我们是不可能继续取的。而且如果是循环是不是就感觉很LOW)

那么第三次我们客户端又传输了300个字节,然后我们服务端开始取,因为我们设置的是1024个字节为收取这一次我们收取到的是1024个字节。那么为什么是1024个字节,而不是300个字节呢,(那就要说道TCP协议了,TCP协议中它是以个稳定协议,也就是说它永远都会把所有数据都发完,不会留着,也不会随机发多少一说),而我们发过去的数据量有都是存在服务端的缓存中不会丢失的,就像我们去井口去取水,我们不会把最下面的水拿出来把,拿出来的肯定是最上面的。所以就发生了所谓的粘包现象,也就是说,一个没取干净,导致全程乱序的事故

总结:

简而言之:所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

两种情况下会发生粘包。

一:发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

二:接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

解决粘包问题的方法

struct模块
用这个struct模块有什么好处

为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

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

>>> struct.pack('i',1111111111111)

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围

img

TCP通信下普通报头添加

对于就以种要求的报头处理方法

(比如只需要知道后续文件的长度。)

服务端:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
服务端应该满足的特性:
    1、一直对外提供服务
    2、并发地提供服务
"""
import socket
import subprocess
import struct

# 1、买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # SOCK_STREAM=》TCP协议

# 2、插手机卡
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 就是它,在bind前加

phone.bind(("127.0.0.1", 8080))  # 本地回环

# 3、开机
phone.listen(5)
print('starting %s:%s' % ("127.0.0.1", 8080))

# 4、等电话链接=>链接循环
while True:
    conn, client_addr = phone.accept()
    print(client_addr)
    # 5、收/发消息=>通信循环
    while True:
        try:
            cmd = conn.recv(1024)  # 最大接收的字节个数
            if len(cmd) == 0:  # 针对linux系统
                break

            obj = subprocess.Popen(cmd.decode('utf-8'),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE
                                   )

            stdout = obj.stdout.read()
            stderr = obj.stdout.read()
            total_size = len(stdout) + len(stderr)
            # 先发送数据的长度
            conn.send(struct.pack('i', total_size))
            # 发送真正的数据
            conn.send(stdout)
            conn.send(stderr)
        except Exception:  # 针对windows系统
            break

    # 6、关闭
    conn.close()  # 挂电话
phone.close()  # 关机

客户端:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import struct
# 1、买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # SOCK_STREAM=》TCP协议
# 2、拨电话
phone.connect(("127.0.0.1", 8080))
# 3、发/收消息=>通信循环
while True:
    cmd = input("[root@localhost]# ").strip()
    if len(cmd) == 0:
        continue
    phone.send(cmd.encode('utf-8'))
    # 先收数据的长度
    header = phone.recv(4)
    total_size = struct.unpack('i', header)[0]
    # 收真正的数据
    recv_size = 0
    res = b''
    while recv_size < total_size:
        data = phone.recv(1024)
        res += data
        recv_size += len(data)
    print(res.decode('gbk'))
# 4、关闭
phone.close()

结果:

自行测试(输入cmd 中的方法 例如 dir 等等…)

TCP通信下复杂报头添加

对于需要处理多个信息的情况

思路:

我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)

发送时:

先发报头长度

再编码报头内容然后发送

最后发真实内容

 

接收时:

先手报头长度,用struct取出来

根据取出的长度收取报头内容,然后解码,反序列化

从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

上传下载文件代码:

服务端:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import os
import struct
import json
import hashlib
from socket import *

server = socket(AF_INET, SOCK_STREAM)
# print(server)
server.bind(('127.0.0.1', 8082))
server.listen(5)
while True:
    conn, client_addr = server.accept()
    print(client_addr)
    while True:
        try:
            msg = conn.recv(1024).decode('utf-8')
            cmd,file_path=msg.split()
            m5 = hashlib.md5()
            print(cmd)
            if cmd == "get":
                with open(r'%s' % file_path, mode='rb') as f:
                    for line in f:
                         m5.update(line)

                md5=m5.hexdigest()
                # 一、制作报头
                header_dic={
                    "total_size":os.path.getsize(file_path),
                    "filename":os.path.basename(file_path),
                    "md5":md5}
                header_json=json.dumps(header_dic)
                header_json_bytes=header_json.encode('utf-8')
                # 二、发送数据
                # 1、先发送报头的长度
                header_size=len(header_json_bytes)
                conn.send(struct.pack('i',header_size))
                # 2、再发送报头
                conn.send(header_json_bytes)
                # 3、最后发送真实的数据
                with open(r'%s' %file_path,mode='rb') as f:
                    for line in f:
                        conn.send(line)
            elif cmd == 'dup':
                print('2')
                # 1、先接收报头的长度
                n = 0
                header = b''
                while n < 4:
                    data = conn.recv(1)
                    header += data
                    n += len(data)
                header_size = struct.unpack('i', header)[0]
                # 2、再接收报头
                header_json_bytes = conn.recv(header_size)
                header_json = header_json_bytes.decode('utf-8')
                header_dic = json.loads(header_json)
                # 3、最后接收真实的数据
                total_size = header_dic['total_size']
                filename = header_dic['filename']
                recv_size = 0
                with open(fr'{file_path}', mode='wb') as f:
                    while recv_size < total_size:
                        data = conn.recv(1024)
                        f.write(data)
                        recv_size += len(data)
        except Exception:
            break
    conn.close()

server.close()

客户端:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import struct
import json
import hashlib
import os
from socket import *
#客户端上传路径(服务器中所存放的位置)

#dup C:\Users\Administrator\Desktop\每日笔记\day32\图片2\timg.jpg

#客户端上传路径 (用户所需要上传文件的位置)

#D:\图片\timg.jpg


# 客户端下载路径(服务器中文件所在的位置):

#get D:\图片\timg.jpg

# 客户端下载路径(用户所需要存放该文件的位置):

#C:\Users\Administrator\Desktop\每日笔记\day32\图片\

client = socket(AF_INET, SOCK_STREAM)
# print(client)
client.connect(('127.0.0.1', 8082))

while True:
    cmd = input("服务器文件路径/或者目标地址: ").strip()  # get 文件路径
    addr=input('存放路径/目标存放文件路径: ').strip()
    if len(cmd) == 0:
        continue
    if len(addr) == 0:
        continue
    cmds, file_path = cmd.split()
    if cmds =='get':
        client.send(cmd.encode('utf-8'))
        # 1、先接收报头的长度
        n = 0
        header = b''
        while n < 4:
            data = client.recv(1)
            header += data
            n += len(data)
        header_size=struct.unpack('i',header)[0]
        # 2、再接收报头
        header_json_bytes=client.recv(header_size)
        header_json=header_json_bytes.decode('utf-8')
        header_dic=json.loads(header_json)
        print(header_dic)
        # 3、最后接收真实的数据
        total_size=header_dic['total_size']
        filename=header_dic['filename']
        recv_size = 0
        with open(fr"{addr}%s" %filename, mode='wb') as f:
            while recv_size < total_size:
                data = client.recv(1024)
                f.write(data)
                recv_size += len(data)
    elif cmds=='dup':

        client.send(cmd.encode('utf-8'))
        m5 = hashlib.md5()
        with open(fr'{addr}', mode='rb') as f:
            print('打开文件')
            for line in f:
                m5.update(line)
        md5 = m5.hexdigest()
        # 一、制作报头
        header_dic = {
            "total_size": os.path.getsize(addr),
            "filename": os.path.basename(addr),
            "md5": md5}
        print(header_dic)
        header_json = json.dumps(header_dic)
        header_json_bytes = header_json.encode('utf-8')
        # 二、发送数据
        # 1、先发送报头的长度
        header_size = len(header_json_bytes)
        client.send(struct.pack('i', header_size))
        # 2、再发送报头
        client.send(header_json_bytes)
        # 3、最后发送真实的数据
        with open(fr'{addr}', mode='rb') as f:
            for line in f:
                client.send(line)
        print('文件上传成功!')
client.close()

结果:

自行测试(输入上述客户端所提示的地址。请看清楚。)

峰哥详解地址

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值