TCP粘包问题

1.什么是粘包?

粘包指的是数据与数据之间没有明确的分界线,导致不能正确的读取数据。换句话说就是接收方不知道发送方一次到底发送了多少数据。只有TCP才会出现粘包现象,UDP不会出现粘包现象,因为TCP是流式协议,UDP是数据报协议。那么要理解粘包问题,就需要先了解TCP协议传输数据时的具体流程:

应用程序无法直接操作硬件,应用程序想要发送数据必须将数据交给操作系统,由操作系统来完成数据的传输,但是操作系统要为所有的应用程序提供数据传输的服务,也就意味着操作系统不可能立马将数据发送出去,就需要为应用程序提供一个缓冲区,用于临时存放数据。

 

UDP:

UDP在收发数据时时基于数据包的,即一个一个的包的发送,包与包之间有着明确的分界,到达对方操作系统缓冲区后也是一个个独立的数据包,接收方从操作系统缓冲区中,将数据包加载到应用程序。这种方式存在的问题有:

1.发送方发送的数据长度每个操作系统会有不同的限制,数据超过限制则无法发送

2.接收方接收数据时如果应用程序的提供的缓存容量小于数据包的长度,则会发生数据丢失,而且缓冲区不可能无限大

 

TCP:

当我们需要传输较大的数据或者需要保证数据的完整性时,就需要使用TCP协议。

与UDP不同的是,TCP增加了一套校验规则来保证数据的完整性,会将超过TCP包最大长度的数据拆分为多个TCP包 并在传输数据时为每一个TCP数据包指定一个顺序号,接收方在收到TCP数据包后按照顺序将数据包进行重组,重组后的数据全都是二进制数据,且每次收到的二进制数据之间没有明显的分界,这样就会容易产生粘包现象。

产生粘包问题的三种情况:

1.当单个数据包较小时,接收方可能一次性读取了多个包的数据

2.当整体数据较大时,接收方可能一次只读取了一个包的一部分内容

3.另外TCP协议为了提高效率,增加了一种优化机制,会将数据较小且发送时间间隔较短的数据和并发送,该机制也会导致发送方将两个数据包黏在一起发送。

2.粘包的解决方案

1.基础解决方案

在发送数据前先发送数据长度

server.py

import socket
import subprocess

server = socket.socket()
server.bind(('127.0.0.1', 3456))
server.listen(5)
while True:
    client, addr = server.accept()
    while True:
        try:
            cmd = client.recv(1024).decode('utf-8')  # 接收命令
            if not cmd:
                client.close()
                break
            p = subprocess.Popen(cmd,  # 接收命令
                                 shell=True,  # 判断参数是否是一个系统命令
                                 stdout=subprocess.PIPE,  # 指定结果的输出管道
                                 stderr=subprocess.PIPE)  # 指定错误结果的输出管道

            std = p.stdout.read()  # 获取结果
            std_err = p.stderr.read()  # 获取错误的结果

            length = len(std) + len(std_err)  # 统计结果的长度
            len_size = str(length).encode('utf-8')  # 将int类型转换为bytes类型

            client.send(len_size)  # 发送结果的长度的bytes数据
            client.send(std)  # 发送结果数据
            client.send(std_err)  # 发送错误结果数据
        except ConnectionResetError as e:
            print(e)
            client.close()
            break

client.py

import socket

client = socket.socket()
client.connect(('127.0.0.1', 3456))
while True:
    cmd = input('>>:').strip().encode('utf-8')  # 用户输入命令,并且编码
    if not cmd:continue
    client.send(cmd)  # 发送命令
    length_size = client.recv(1024)  # 接收数据长度的bytes数据
    length = length_size.decode('utf-8')  # 将bytes反解成int类型,得到数据长度
    all_data = b''
    recv_len = 0
    while recv_len < int(length):  # 条件为如果循环接收的数据长度小于得到的数据长度则执行
        data = client.recv(1024)  # 接收真实数据
        recv_len += len(data)  # 累加接收的数据长度
        all_data += data  # 累加真实数据
    print(all_data.decode('gbk'))  # 解码打印最终结果

client.close()
解决粘包基础版

但是由于negle优化机制的存在,长度信息和数据还是有可能会发生粘包现象,而对方并不知道长度信息具体几个字节,所以现在的问题是如何能够将长度信息做成一个固定长度的bytes数据。所以有个内置模块:struct模块为我们提供了一个功能,可以将int类型转换为固定长度的bytes。

2.解决粘包问题升级版

import struct

# 整型转bytes
res1 = struct.pack('i', 1000)  # 无论数字(int)有多大,最后结果的长度都是4
res2 = struct.pack('q', 1000)  # 无论数字(long long)有多大,最后结果的长度都是8

print(res1, len(res1))  # b'\xe8\x03\x00\x00' 4
print(res2, len(res2))  # b'\xe8\x03\x00\x00\x00\x00\x00\x00' 8

# bytes转整型
data1 = struct.unpack('i', res1)  
data2 = struct.unpack('q', res2)  
# 结果是一个元组的形式
print(data1)  # (1000,)  
print(data2)  # (1000,)
server.py

import socket
import subprocess
import struct

server = socket.socket()
server.bind(('127.0.0.1', 3456))
server.listen(5)
while True:
    client, addr = server.accept()
    while True:
        try:
            cmd = client.recv(1024).decode('utf-8')  # 接收命令
            if not cmd:
                client.close()
                break
            p = subprocess.Popen(cmd,  # 接收命令
                                 shell=True,  # 判断参数是否是一个系统命令
                                 stdout=subprocess.PIPE,  # 指定结果的输出管道
                                 stderr=subprocess.PIPE)  # 指定错误结果的输出管道
            
            std = p.stdout.read()  # 获取结果
            std_err = p.stderr.read()  # 获取错误的结果
            
            length = len(std) + len(std_err)  # 统计结果的长度
            len_size = struct.pack('i', length)  # 将int类型转换为bytes类型
            
            client.send(len_size)  # 发送结果的长度的bytes数据
            client.send(std)  # 发送结果数据
            client.send(std_err)  # 发送错误结果数据
        except ConnectionResetError as e:
            print(e)
            client.close()
            break

client.py

import socket
import struct

client = socket.socket()
client.connect(('127.0.0.1', 3456))
while True:
    cmd = input('>>:').strip().encode('utf-8')  # 用户输入命令,并且编码
    if not cmd:continue
    client.send(cmd)  # 发送命令
    length_size = client.recv(4)  # 接收数据长度的bytes数据
    length = struct.unpack('i', length_size)[0]  # 将bytes反解成int类型,得到数据长度
    all_data = b''
    recv_len = 0
    while recv_len < length:  # 条件为如果循环接收的数据长度小于得到的数据长度则执行
        data = client.recv(1024)  # 接收真实数据
        recv_len += len(data)  # 累加接收的数据长度
        all_data += data  # 累加真实数据
    print(all_data.decode('gbk'))  # 解码打印最终结果

client.close()
解决粘包升级版

3.自定义报头解决粘包

上述方案已经完美解决了粘包问题,但是扩展性不高,假如我们要实现文件的上传和下载,不光要传输文件数据,还需要传输文件名字,md5值等等,如何能实现?

解决步骤:

发送端:

1.先将所有的额外信息打包到一个头中

2.然后先发送头部数据

3.最后发送真实数据

接收端:

1.接收固定长度的头部长度数据

 2.根据长度数据获取头部数据

3.根据头部数据获取真实数据

server.py

import socket
import subprocess
import datetime
import struct
import json
server = socket.socket()
server.bind(('127.0.0.1', 3456))
server.listen(5)
while True:
    client, addr = server.accept()
    while True:
        try:
            cmd = client.recv(1024).decode('utf-8')
            if not cmd:
                client.close()
                break
            p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            std = p.stdout.read()
            std_err = p.stderr.read()

            # 自定义报头
            t = {}
            t['time'] = str(datetime.datetime.now())
            t['size'] = len(std) + len(std_err)

            json_data = json.dumps(t)  # 序列化成json格式的数据
            t_data = json_data.encode('utf-8')  # 将json格式的数据转换成字节
            json_len = struct.pack('i', len(t_data))

            client.send(json_len)
            client.send(t_data)

            client.send(std)
            client.send(std_err)
        except ConnectionResetError as e:
            print(e)
            client.close()
            break

client.py

import socket
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1', 3456))
while True:
    cmd = input('>>:').strip().encode('utf-8')
    if not cmd:continue
    client.send(cmd)

    json_len = client.recv(4)
    json_size = struct.unpack('i', json_len)[0]

    json_data = client.recv(json_size)
    json_dic = json.loads(json_data.decode('utf-8'))

    print('执行时间:%s' % json_dic['time'])
    all_data = b''
    recv_len = 0
    while recv_len < json_dic['size']:
        data = client.recv(1024)
        recv_len += len(data)
        all_data += data
    print(all_data.decode('gbk'))

client.close()
解决粘包问题终极版

 FTP文件上传下载程序:

import socket
import struct
import json
import os

server = socket.socket()

server.bind(("127.0.0.1", 9090))
server.listen()


def run():
    while True:
        client, addr = server.accept()
        while True:
            try:
                # 接收报头
                len_data = client.recv(4)
                if not len_data:
                    print("客户端已断开....")
                    client.close()
                    break
                head_len = struct.unpack("i", len_data)[0]
                head = json.loads(client.recv(head_len).decode("utf-8"))

                # 从报头中获取用户要执行的操作
                if head["opt"] == "login":
                    res = login(head)  # 调用登录  无论成功失败 都会得到一个结果
                    send_response(res, client)  # 将结果发送给客户端
                elif head["opt"] == "register":
                    res = register(head)  # 调用注册  无论成功失败 都会得到一个结果
                    send_response(res, client)  # 将结果发送给客户端
                else:
                    print("请求错误!")

            except ConnectionResetError:
                print("客户端异常断开...")
                client.close()
                break


def login(head):
    dir_path = r"D:\脱产5期内容\选课系统\黏包作业\登录注册\user_data"
    path = os.path.join(dir_path, head["name"])

    if not os.path.exists(path):
        response = {"status": "200", "msg": "用户名不存在!"}
        return response
    with open(path, "rt", encoding="utf-8") as f:
        user_dic = json.load(f)
        if user_dic["pwd"] == head["pwd"]:
            response = {"status": "200", "msg": "登录成功!"}
            return response
        else:
            response = {"status": "200", "msg": "密码错误!"}
            return response


def register(head):
    user_dic = {"name": head["name"], "pwd": head["pwd"]}
    dir_path = r"D:\脱产5期内容\选课系统\黏包作业\登录注册\user_data"
    path = os.path.join(dir_path, head["name"])

    if os.path.exists(path):
        print("用户名已存在!")
        response = {"status": "200", "msg": "用户名已存在!"}
        return response

    with open(path, "wt", encoding="utf-8") as f:
        json.dump(user_dic, f)
        response = {"status": "200", "msg": "注册成功"}
        return response


def send_response(resp, client):
    response_data = json.dumps(resp).encode("utf-8")
    client.send(struct.pack("i", len(response_data)))
    client.send(response_data)


run()
服务器
import socket
import struct
import json

c = socket.socket()
c.connect(("127.0.0.1", 9090))


def run():
    print("""
    1.登录
    2.注册
    3.退出
    """)

    res = input("请选择功能:")
    if res == "3":
        return
    elif res == "1":
        login()
    elif res == "2":
        register()
    else:
        print("输入错误!")


def login():
    name = input("name:")
    pwd = input("password:")
    dic = {"name": name, "pwd": pwd, "opt": "login"}
    res = send_request(dic)
    print(res)


def register():
    name = input("name:")
    pwd = input("password:")
    dic = {"name": name, "pwd": pwd, "opt": "register"}
    res = send_request(dic)
    print(res)


def send_request(dic):
    dic_data = json.dumps(dic).encode("utf-8")
    c.send(struct.pack("i", len(dic_data)))
    c.send(dic_data)

    # 接收返回结果
    res_len = struct.unpack("i", c.recv(4))[0]
    res = json.loads(c.recv(res_len).decode("utf-8"))

    return res


run()
客户端

 

posted @ 2018-12-26 18:32  起个名字、真难啊  阅读(477)  评论(0编辑  收藏  举报