粘包

粘包问题
【一】什么是粘包
须知:只有TCP有粘包现象,UDP永远不会粘包
【1】socket收发消息的原理
首先需要掌握一个socket收发消息的原理
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据
也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的
因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
【2】如何定义消息
可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外
发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率
发送方往往要收集到足够多的数据后才发送一个TCP段。
若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
【3】TCP
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket
因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
【4】UDP
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。
不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息)
这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
【5】小结
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
udp的recvfrom是阻塞的
一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
两种情况下会发生粘包。
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
【二】什么是粘包问题
客户端发送需要执行的代码
服务端接收到客户端传过来的代码
服务端调用方法执行代码并拿到执行后的结果
服务端将执行后的结果进行返回
客户端接收到服务端返回的结果并做打印输出
【1】服务端
【2】客户端
【3】问题引入
服务端:
执行代码,代码为空会报错
执行代码,返回的数据可能存在空/报错信息
客户端:
输入的指令长度,可能会超出范围
接受到的服务端反馈的结果可能会特别多
如何打印超出数据范围(缓存到系统里)的数据
【4】粘包问题
在 TCP 协议中是流式协议,数据是源源不断的传入到客户端中,但是客户端可以接受到的信息的长度是有限的
当接收到指定长度的信息后,客户端进行打印输出
剩余的其他数据会被缓存到 内存中
当再次执行其他命令时
新的数据的反馈结果,会叠加到上一次没有完全打印完全的信息的后面,造成数据的错乱
当客户端想打印新的命令的数据时,打印的其实是上一次没有打印完的数据
对数据造成的错乱
【5】粘包问题解决思路
拿到数据的总大小 recv_total_size
recv_size = 0 ,循环接收,每接收一次,recv_size += 接收的长度
直到 recv_size = recv_total_size 表示接受信息完毕,结束循环
【三】UDP协议不存在粘包问题
粘包问题出现的原因
TCP 协议是流式协议,数据像水流一样粘在一起,没有任何边界之分
收数据没有接收干净,有残留,就会和下一次的结果混淆在一起
解决粘包问题的核心法门就是
每次都收干净
不造成数据的混淆
【1】UDP协议不存在粘包问题
(1)服务端
(2)客户端
(3)小结
当我们启动udp服务端后,由udp客户端向服务端发送两条数据
但是在udp服务端只接收到了一条数据
这是因为 udp 是报式协议,传送数据过程中会将数据打包直接发走,不会对数据进行拼接操作(没有Nagle算法)
【2】TCP协议存在粘包问题
(1)服务端
(2)客户端
【3】小结
从以上我们可以看到
TCP协议传输过程中将我们的两次发送的数据拼接成了一个发送到服务端
通过比较我们可知,udp协议虽然不存在粘包问题,但是,udp协议的安全性有待考量
【四】TCP协议解决粘包问题基础
【1】解决思路
利用 struct 模块将传输过去的数据的总长度 打包 + 到头部进行发送
【2】服务端
【3】客户端
客户端可以完美的接收到查出额定长度以外的数据
同时这也是 自定义协议的 简单操作
【五】TCP协议解决粘包问题进阶
【0】解决思路
通过json模式 ---- 模版修改参数直接套用
【1】服务端
【2】客户端
【补充】struct模块
struct.pack()是Python内置模块struct中的一个函数
它的作用是将指定的数据按照指定的格式进行打包,并将打包后的结果转换成一个字节序列(byte string)
可以用于在网络上传输或者储存于文件中。
struct.pack(fmt, v1, v2, ...)
其中,fmt为格式字符串,指定了需要打包的数据的格式,后面的v1,v2,...则是需要打包的数据。
这些数据会按照fmt的格式被编码成二进制的字节串,并返回这个字节串。
fmt的常用格式符如下:
x --- 填充字节
c --- char类型,占1字节
b --- signed char类型,占1字节
B --- unsigned char类型,占1字节
h --- short类型,占2字节
H --- unsigned short类型,占2字节
i --- int类型,占4字节
I --- unsigned int类型,占4字节
l --- long类型,占4字节(32位机器上)或者8字节(64位机器上)
L --- unsigned long类型,占4字节(32位机器上)或者8字节(64位机器上)
q --- long long类型,占8字节
Q --- unsigned long long类型,占8字节
f --- float类型,占4字节
d --- double类型,占8字节
s --- char[]类型,占指定字节个数,需要用数字指定长度
p --- char[]类型,跟s一样,但通常用来表示字符串
? --- bool类型,占1字节
具体的格式化规则可以在Python文档中查看(链接)。
【作业】FTP文件传输器
【1】服务端
【2】客户端
Python
复制代码
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import socket
import os

class MYTCPClient:
address_family = socket.AF_INET

socket_type = socket.SOCK_STREAM

allow_reuse_address = False

max_packet_size = 8192

coding='utf-8'

request_queue_size = 5

def __init__(self, server_address, connect=True):
    self.server_address=server_address
    self.socket = socket.socket(self.address_family,
                                self.socket_type)
    if connect:
        try:
            self.client_connect()
        except:
            self.client_close()
            raise

def client_connect(self):
    self.socket.connect(self.server_address)

def client_close(self):
    self.socket.close()

def run(self):
    while True:
        inp=input(">>: ").strip()
        if not inp:continue
        l=inp.split()
        cmd=l[0]
        if hasattr(self,cmd):
            func=getattr(self,cmd)
            func(l)


def put(self,args):
    cmd=args[0]
    filename=args[1]
    if not os.path.isfile(filename):
        print('file:%s is not exists' %filename)
        return
    else:
        filesize=os.path.getsize(filename)

    head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
    print(head_dic)
    head_json=json.dumps(head_dic)
    head_json_bytes=bytes(head_json,encoding=self.coding)

    head_struct=struct.pack('i',len(head_json_bytes))
    self.socket.send(head_struct)
    self.socket.send(head_json_bytes)
    send_size=0
    with open(filename,'rb') as f:
        for line in f:
            self.socket.send(line)
            send_size+=len(line)
            print(send_size)
        else:
            print('upload successful')

client=MYTCPClient(('127.0.0.1',8080))

client.run()

posted @ 2024-05-15 14:10  zenopan  阅读(12)  评论(0编辑  收藏  举报