38、TCP粘包现象

网络编程 / 2021-01-09

一、TCP粘包现象

1.1、什么是粘包?

  TCP粘包指的是发送端发送的数据包到达接收端时黏成了一块块;从接收端缓存区来说,就好比高速上堵车了,一辆接着一辆排成长长的队伍,当堵塞解除后也是前面的车先走后面的车才能动!

1.2、粘包现象的原因?

  TCP协议簇属于流式协议,像水流一样无边无际;为了提升传输效率,它默认使用Nagle算法:

1.2.1、Nagle算法:

  发送端会将发出的包中,间隔较短且数据量较小的数据段合并成一个较大的数据段再进行下一步封装;就像你去寄快递一样,没理由你就寄那一个小包裹快递公司也用专车给你运送,那中国的快递公司早就倒闭光了!肯定是攒够一定的包裹数量(合并),接着全部装入车内,再一次性运往目的地(传输),到达目的地后再进行分拣(拆包),哪些包裹分派到哪个分区!TCP协议同理,它需要一种拆包机制来准确识别一段数据有多大,哪些小包合起来才是一个完整的数据!

  UDP协议是毋须事先建立连接的,发送数据也是跟领导下达任务一样,我才不管你有多难能不能完成之类;UDP也是发完就不管了,接收不完?接收不完的部分那就不要了呗!因此UDP是没有粘包这种概念的,但是可能会丢失数据啊,不然为什么说它是不可靠协议,只能用来作为重要性不高的任务!

1.2.2、缓存大小

  粘包主要原因就是接收端无法一次性接收完数据,接收不完的滞留于缓存区,下一波数据包到达时又粘在一起,无穷无尽!因为TCP接收到数据是先存放在缓存区,我们说过磁盘速度远远跟不上CPU,所以才有了内存这个中间层,应用程序主动从缓存区读取数据,如果接收速度超过读取速度就会造成上面说的粘包现象!

1.3、粘包现象演示

服务端

  这是我放在腾讯云主机上面的代码

#_*_coding:utf-8_*_
  
import socket
import subprocess

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 创建套接字
server.bind(('0.0.0.0', 6666))  # 绑定服务端IP地址与端口
server.listen(2)  # 并发数!
while True:
        connect, client_addr = server.accept()  # 等待连接
        print(f'接收到来自{client_addr} 的消息:')
        while True:
            receive_data = connect.recv(1024)
            if len(receive_data) == 0:
                break
            cmd_recive = subprocess.Popen(receive_data.decode('utf-8'), shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    stdin=subprocess.PIPE)
            stdout = cmd_recive.stdout.read()
            stdeer = cmd_recive.stderr.read()
            connect.send(stdout)
            connect.send(stdeer)
        connect.close()
客户端
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect_ex(('129.XX.115.XX', 6666))  # 连接远程服务端;connect_ex比connect多了个连接错误时返回异常状态码而不是报错
while True:
    msg = input('请输入交互内容,退出请键入quit:').strip()
    if len(msg) == 0:
        print('内容不能为空!')
        continue
    elif msg == 'quit':
        break
    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data.decode('utf-8'), end='')
client.close()
测试结果
# 这次远程返回结果比较短,可以完全接收,因为我设置了一次性最多接收1KB
请输入交互内容,退出请键入quit:ls
server_socket.py
test.sh
v2ray.sh

# 来个超长版返回值,因为太长了,我就放一点出来,实际是非常长的。
请输入交互内容,退出请键入quit:ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.1 169224 13128 ?        Ss    2020   2:27 /lib/systemd/systemd --system --deserialize 30
root           2  0.0  0.0      0     0 ?        S     2020   0:01 [kthreadd]
root           3  0.0  0.0      0     0 ?        I<    2020   0:00 [rcu_gp]

# 看到没,因为上一次的还没接收完,粘在一起了!
请输入交互内容,退出请键入quit:ls
    0     0 ?        S     2020   0:00 [cpuhp/1]
root          16  0.0  0.0      0     0 ?        S     2020   0:00 [idle_inject/1]
root          17  0.0  0.0      0     0 ?        S     2020   0:17 [migration/1]

1.4、如何解决粘包?

自定义协议

  既然是因为接收不完,且不知道数据的总长度,那我就让它知道不就完了!!!

  我们可以在发送真实数据前,先自定义数据流的长度;先发送数据流长度给对端,接着发完整的真实数据过去;对端做一个循环取值完整数据,至此粘包问题解决!客官请看下面真实案例,本人服务端是放在腾讯云上的一台Ubuntu上,客户端是我在Windows上面的pycharm!

  这已经完全可以算是一个远程控制软件了,还可以给被控制端输出命令和命令执行结果!

服务端:
import socket
import struct
import subprocess

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 创建套接字
server.bind(('0.0.0.0', 6666))  # 绑定服务端IP地址与端口
server.listen(2)  # 并发数!

while True:
    connect, client_addr = server.accept()  # 等待连接
    print(f'接收到来自{client_addr} 的消息:')
    try:  # 捕捉异常,发现报错就执行except中断本次连接
        while True:
            command = connect.recv(1024)
            if not len(command):
                break
            print('开始执行命令:', command)
            cmd_recive = subprocess.Popen(command.decode('utf-8'), shell=True,
                                          stdout=subprocess.PIPE,
                                          stderr=subprocess.PIPE)

            stdeer = cmd_recive.stderr.read()
            if stdeer:
                reply_data = stdeer
            else:
                reply_data = cmd_recive.stdout.read()
            connect.send(struct.pack('i', len(reply_data)))
            connect.sendall(reply_data)
            print(reply_data.decode('utf-8'))
    except ConnectionResetError as error:
        print('客户端已中断连接,等待连接中...')
        continue
    connect.close()
客户端:
import socket
import struct

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect_ex(('XX.XX.XX.XX', 6666))
while True:
    msg = input('请输入命令,退出键入quit:').strip()
    if not len(msg):
        print('内容不能为空!')
        continue
    elif msg == 'quit':
        break
    client.send(bytes(msg, encoding='utf-8'))

    head = client.recv(4)
    len_head = struct.unpack('i', head)[0]  # 复现数据流长度

    recv_size = 0
    total_data = b''
    while recv_size < len_head:  # 循环取值完整数据
        recv_data = client.recv(1024)  # 接收真实数据
        total_data += recv_data  # 拼接字符串
        recv_size += len(recv_data)  # 统计长度用于对比
    print(recv_data.decode('utf-8'), end='')  # 输出命令结果给被控制端
client.close()
世间微尘里 独爱茶酒中