网络编程-黏包

注意:只有TCP有粘包现象,UDP永远不会粘包

黏包的原因一:

udp接受一个数据包的代码ret, addr = sk.recvfrom(1024)
tcp接受一个数据包的代码ret = sk.recv(1024)
tcp与udp都需要指定接受的大小。区别在于tcp是可靠连接,有缓存机制,如果指定的大小不足以输出全部的数据包内容,会缓存着等待下一次读取。而udp则会直接丢弃未读取到的数据包内容。

TCP缓存机制.png

# -*- coding: UTF-8 -*-

"""
# @Time    : 2019-09-09 10:28
# @Author  : yanlei
# @FileName: server.py

远程执行cmd命令
"""
import socket
sk = socket.socket()

sk.bind(('127.0.0.1', 8080))
sk.listen()

conn, addr = sk.accept()
while True:
    cmd = input('>>>').encode('gbk')
    if cmd == b'q':
        conn.send(b'q')
        break
    conn.send(cmd)
    ret = conn.recv(1024).decode('gbk')
    print(ret)

conn.close()
sk.close()

# -*- coding: UTF-8 -*-

"""
# @Time    : 2019-09-09 10:28
# @Author  : yanlei
# @FileName: client.py

接受server端的命令后自己在机器上执行
"""
import socket
import subprocess
sk = socket.socket()
ip_port = ('127.0.0.1', 8080)

sk.connect(ip_port)

while True:
    cmd = sk.recv(1024).decode('gbk')
    if cmd == 'q':
        break
    ret = subprocess.Popen(cmd, shell=True,  # shell=True 直接执行操作系统的命令
                     stdout=subprocess.PIPE,  # 从管道中提取输出信息与错误信息
                     stderr=subprocess.PIPE)
    sk.send(ret.stdout.read())
    sk.send(ret.stderr.read())

sk.close()

黏包的原因二:

tcp优化算法引起的黏包现象

连续使用多个send(小的数据)连在一起,会发生黏包现象,是由tcp协议内部的优化算法引起的

# -*- coding: UTF-8 -*-

"""
# @Time    : 2019-09-09 11:31
# @Author  : yanlei
# @FileName: server.py
"""
import socket
sk = socket.socket()
sk.bind(('127.0.0.1', 8080))
sk.listen()

conn, addr = sk.accept()

ret1 = conn.recv(12)
ret2 = conn.recv(2)  # ret1将客户端发送过来的两个数据包都接受了,由于tcp的优化算法,ret2只会接受到一个空字符串
print(ret1)
print(ret2)
'''
b'helloworld'
b''
'''
conn.close()
sk.close()

# -*- coding: UTF-8 -*-

"""
# @Time    : 2019-09-09 11:31
# @Author  : yanlei
# @FileName: client.py
"""
import socket
sk = socket.socket()
ip_port = ('127.0.0.1', 8080)
sk.connect(ip_port)

sk.send(b'hello')
sk.send(b'world')

sk.close()

本质原因

不知道到底要接受多大的数据

解决办法

方法一:多写一次send与recv,先接收要传输的数据的大小,再接受数据
方法二:通过struct模块先将数据包大小转换成固定长度的bytes,通过pack 和 unpack来压缩与解压缩(该模块可以把一个类型,如数字,转成固定长度的bytes)

image.png

优缺点分析
方法一:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗。多了一次收发消息的过程,会更加耗时。
方法二:借助struct模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。
解决办法一代码

# -*- coding: UTF-8 -*-

"""
# @Time    : 2019-09-10 10:04
# @Author  : yanlei
# @FileName: server.py

通过多一次recv与send先发送数据包的大小,然后确定第二次recv的数据包大小
"""
# 1. 开套接字
# 2. 绑定ip and 端口
# 3. 开始监听
# 4. 接收 连接 与 地址
# 5. 发送信息/接收信息
# 6. 关闭连接 and 关闭端口
import socket
sk = socket.socket()
ip_port = ('127.0.0.1', 8080)
sk.bind(ip_port)
sk.listen()

conn, addr = sk.accept()
while True:
    cmd = input('>>>请输入命令:')
    if cmd == 'q':
        conn.send(b'q')
        break
    conn.send(cmd.encode('gbk'))
    cmd_len = conn.recv(1024).decode('gbk')
    print(cmd_len)
    conn.send(b'ok')  # 为了防止tcp的优化算法,将几个小的连续的send的数据包一起发送产生黏包问题。发送一个ok已用来隔离客户端中数据包大小的send与数据包内容的send
    cmd_detail = conn.recv(int(cmd_len)).decode('gbk')
    print(cmd_detail)

conn.close()
sk.close()

# -*- coding: UTF-8 -*-

"""
# @Time    : 2019-09-10 10:11
# @Author  : yanlei
# @FileName: client.py


"""
import socket
import subprocess

sk = socket.socket()
ip_port = ('127.0.0.1', 8080)
sk.connect(ip_port)

while True:
    cmd = sk.recv(1024).decode('gbk')
    if cmd == 'q':
        break
    ret = subprocess.Popen(cmd, shell=True,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.PIPE)
    cmd_out = ret.stdout.read()
    cmd_err = ret.stderr.read()
    cmd_len = str(len(cmd_out) + len(cmd_err)).encode('gbk')
    sk.send(cmd_len)
    sk.recv(1024)  # 用来接收服务端发送的ok。只是为了隔离上面发送的数据包的大小与下面发送的具体的数据包内容,防止tcp优化算法将其一起发送过去
    sk.send(cmd_out)
    sk.send(cmd_err)

sk.close()

解决办法二代码
通过struct模块

# -*- coding: UTF-8 -*-

"""
# @Time    : 2019-09-10 10:30
# @Author  : yanlei
# @FileName: server.py

"""
# 1. 开套接字
# 2. 绑定ip and 端口
# 3. 开始监听
# 4. 接收 连接 与 地址
# 5. 发送信息/接收信息
# 6. 关闭连接 and 关闭端口
import socket
import struct
sk = socket.socket()
ip_port = ('127.0.0.1', 8080)
sk.bind(ip_port)
sk.listen()

conn, addr = sk.accept()
while True:
    cmd = input('>>>请输入命令:')
    if cmd == 'q':
        conn.send(b'q')
        break
    conn.send(cmd.encode('gbk'))
    cmd_len_bytes = conn.recv(4)  # 将数据包的大小转换成了长度为4的bytes类型,直接接受长度为4的数据就是数据包的长度了
    cmd_len = struct.unpack('i', cmd_len_bytes)[0]  # unpack 解压缩后得到一个元组,元组中的第一个元素就是要获得的数据包的大小-int型
    cmd_detail = conn.recv(cmd_len).decode('gbk')
    print(cmd_detail)

conn.close()
sk.close()
# -*- coding: UTF-8 -*-

"""
# @Time    : 2019-09-10 10:32
# @Author  : yanlei
# @FileName: client.py
"""
import socket
import subprocess
import struct

sk = socket.socket()
ip_port = ('127.0.0.1', 8080)
sk.connect(ip_port)

while True:
    cmd = sk.recv(1024).decode('gbk')
    if cmd == 'q':
        break
    ret = subprocess.Popen(cmd, shell=True,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.PIPE)
    cmd_out = ret.stdout.read()
    cmd_err = ret.stderr.read()
    cmd_len = len(cmd_out) + len(cmd_err)
    cmd_len_bytes = struct.pack('i', cmd_len)
    sk.send(cmd_len_bytes)
    sk.send(cmd_out)
    sk.send(cmd_err)

sk.close()

总结

为什么会出现黏包现象?

  1. 只有在TCP协议中才会出现黏包现象
  2. 因为TCP协议是面向流的协议
  3. 在发送的数据传输的过程中还有缓存机制来避免数据的丢失
  4. 在连续发送小数据的时候 以及 接收大小不符的时候都容易出现黏包现象

本质:在接收数据的时候不知道发送的数据的长度
解决黏包问题的办法:

  1. 在传输大量数据之前先告诉接收端要发送数据的大小(缺点:多一次消息的收发,会影响传输速度)
  2. 通过struct模块来定制协议

struct模块总结

  • 方法:pack , unpack bytes_num = struct.pack('i', num)
  • 模式: 'i'
  • pack之后的长度:4个字节
  • unpack之后拿到的数据是一个元组,元组中的第一个元素是pack之前的值

你可能感兴趣的:(网络编程-黏包)