Learning-Python【29】:网络编程之粘包

粘包问题

  上一篇博客遗留了一个问题,在接收的最大字节数设置为 1024 时,当接收的结果大于1024,再执行下一条命令时还是会返回上一条命令未执行完成的结果。这就是粘包问题。

  因为TCP协议又叫流式协议,每次发送给客户端的数据实际上是发送到客户端所在操作系统的缓存上,客户端就是一个应用程序,需要通过操作系统才能调到缓存内的数据,而缓存的空间是一个队列,有 “先进先出” 的思想,当第一次的 tasklist 数据未接收完,第二次又来一个 dir 的数据时,只能等第一次先全部接收完成才会接收后面的。

  有一个解决方法是每次在接收数据时,都将数据的完整结果全部接收,这样就不会出现粘包现象。那该怎么样才能全部接收呢?有人说将接收的最大字节数设置大点不就能接收 tasklist 的全部执行结果了吗?这样做确实可以,但如果是文件的上传下载呢?客户端执行下载命令,服务端将下载的结果发送给客户端,客户端再接收,文件的大小是超过 GB、TB 的,那最大字节数该设置多大?其实设置再大也没有意义,因为客户端接收数据是通过自己操作系统的缓存空间接收的,缓存空间的大小不可能比自己计算机的物理内存还大,就算和物理内存一样大,假设物理内存是 8G,那你也只能一次收到 8GB 的数据,当发送的数据超过 8G 呢?

  TCP协议为了优化传输效率,而导致了粘包问题。客户端和服务端之间是基于网络收发数据,网络的 I/O 是越少越好,TCP有一种 Nagle 算法,是将多次时间间隔较短且数据量小的数据,合并成一个大的数据块,然后进行封包,这样,尽可能多的降低 I/O,从而提升程序的运行效率。但是接收端很难分辨出来,这就导致了粘包问题。

总结粘包问题:

粘包不一定会发生

如果发生了:1)可能是在客户端已经粘了

      2)客户端没有粘,可能是在服务端粘了

 客户端粘包:发送数据时间间隔很短,数据量很小,TCP优化算法会当做一个包发出去,产生粘包

from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)

conn, client_addr = server.accept()

data1 = conn.recv(1024)
print("第一次收: ", data1)

data2 = conn.recv(1024)
print("第二次收: ",data2)

data3 = conn.recv(1024)
print("第三次收: ",data3)

服务端

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

# TCP协议会将数据量较小且时间间隔较短的数据合并成一个数据报发送
client.send(b‘hello‘)
client.send(b‘world‘)
client.send(b‘qiu‘)

客户端

第一次收:  b‘helloworldqiu‘
第二次收:  b‘‘
第三次收:  b‘‘

运行结果

服务端粘包:客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包

from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)

conn, client_addr = server.accept()

data1 = conn.recv(1)
print("第一次收: ", data1)

data2 = conn.recv(2)
print("第二次收: ",data2)

data3 = conn.recv(1024)
print("第三次收: ",data3)

服务端

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

client.send(b‘hello‘)
client.send(b‘world‘)
client.send(b‘qiu‘)

客户端

第一次收:  b‘h‘
第二次收:  b‘el‘
第三次收:  b‘loworldqiu‘

运行结果

粘包问题的解决思路

  问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是发送端在发送数据前,发一个头文件包,里面包含每次要发送数据的长度,构成一个总长度,然后接收端用循环接收完所有数据,但是长度是整型,发送的数据是字节,所以要将整型转成字节类型再发送。

struct 模块

使用 struct 模块可以用于将 Python 的 int 类型转换为 bytes 类型

struct 模块中最重要的三个函数是pack(), unpack(), calcsize()

pack(fmt, v1, v2, ...):按照给定的格式 (fmt),把数据封装成字符串(实际上是类似于 C 语言中结构体的字节流)

unpack(fmt, string):按照给定的格式 (fmt) 解析字节流 string,返回解析出来的 tuple

calcsize(fmt):计算给定的格式 (fmt) 占用多少字节的内存

struct 中支持的格式如下表

import struct

obj = struct.pack(‘i‘, 1231)
print(obj)
print(len(obj))     # C语言中int类型占4个字节

res = struct.unpack("i", obj)
print(res)
print(res[0])

# 执行结果
b‘\xcf\x04\x00\x00‘
4
(1231,)
1231

struct模块

 模拟ssh实现远程执行命令(解决粘包问题简单版)

from socket import *
import subprocess
import struct

server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)

# 连接循环
while True:
    conn, client_addr = server.accept()

    # 通信循环
    while True:
        try:
            cmd = conn.recv(1024)  # cmd = b‘dir‘
            # 针对Linux系统
            if len(cmd) == 0:
                break
            # 命令的执行结果
            obj = subprocess.Popen(cmd.decode("utf-8"),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 1. 先制作固定长度的报头
            header = struct.pack("i", len(stdout) + len(stderr))
            # 2. 再发送报头
            conn.send(header)
            # 3. 最后发送真实的数据
            conn.send(stdout)
            conn.send(stderr)

        except ConnectionResetError:
            break

    conn.close()
server.close()

服务端

from socket import *
import struct

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

# 通信循环
while True:
    cmd = input("请输入: ").strip()
    if len(cmd) == "0":
        continue
    client.send(cmd.encode("utf-8"))
    # 1. 先收报头, 从报头里解出数据的长度
    header = client.recv(4)
    total_size = struct.unpack("i", header)[0]
    # 2. 接收真正的数据
    cmd_res = b""
    # 接收数据的长度初始值为0
    recv_size = 0
    # 当接收的数据长度小于报头长度
    while recv_size < total_size:
        data = client.recv(1024)
        recv_size += len(data)
        cmd_res += data

    print(cmd_res.decode("gbk"))

client.close()

客户端

这样写有一个限制,我在 struct 模块中设置的是 i 格式,只能用于传输较小的字节数,且此时报头里只包含数据长度信息,如果是上传下载文件,还可能包含文件名、文件大小、文件的 md5 值等其它信息,那这种方法就不适用了

可以考虑将报头设置成一个字典,包含相关的信息,然后将字典序列化成 JSON 格式发送,在接收方反序列化还能得到字典格式,且可以设置字典里的文件大小很大,但 JSON 的长度却很小

import json

header_dic = {
    "filename": "a.txt",
    "md5": "DASHJH423465CSA",
    "total_size":456165446511564651351456413514543543
}

header_json = json.dumps(header_dic)
print(len(header_json))

# 运行
99

报头字典序列化成JSON的长度

模拟ssh实现远程执行命令(解决粘包问题终极版)

from socket import *
import subprocess
import struct
import json

server = socket(AF_INET, SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)

# 连接循环
while True:
    conn, client_addr = server.accept()

    # 通信循环
    while True:
        try:
            cmd = conn.recv(1024)  # cmd = b‘dir‘
            # 针对Linux系统
            if len(cmd) == 0:
                break
            # 命令的执行结果
            obj = subprocess.Popen(cmd.decode("utf-8"),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 1. 先制作报头
            header_dic = {
                "filename": "a.txt",
                "md5": "DASHJH423465CSA",
                "total_size": len(stdout) + len(stderr)
            }
            # 将报头序列化成json格式的字符串
            header_json = json.dumps(header_dic)
            # json格式的字符串转成bytes类型
            header_bytes = header_json.encode("utf-8")

            # 2. 先发送4个bytes(包含报头的长度)
            conn.send(struct.pack("i", len(header_bytes)))

            # 3. 发送报头
            conn.send(header_bytes)

            # 4. 最后发送真实的数据
            conn.send(stdout)
            conn.send(stderr)

        except ConnectionResetError:
            break

    conn.close()
server.close()

服务端

from socket import *
import struct
import json

client = socket(AF_INET, SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

# 通信循环
while True:
    cmd = input("请输入: ").strip()
    if len(cmd) == "0":
        continue
    client.send(cmd.encode("utf-8"))
    # 1. 先收4个bytes, 解出报头长度
    header_size = struct.unpack("i", client.recv(4)[0])

    # 2. 再接收报头, 拿到head_dic
    header_bytes = client.recv(header_size)
    header_json = header_bytes.decode("utf-8")
    head_dic = json.loads(header_json)
    print(head_dic)
    total_size = head_dic["total_size"]

    # 3. 接收真正的数据
    cmd_res = b""
    # 接收数据的长度初始值为0
    recv_size = 0
    # 当接收的数据长度小于报头长度
    while recv_size < total_size:
        data = client.recv(1024)
        recv_size += len(data)
        cmd_res += data

    print(cmd_res.decode("gbk"))

client.close()

客户端

原文地址:https://www.cnblogs.com/qiuxirufeng/p/9910118.html

时间: 2024-07-31 23:55:10

Learning-Python【29】:网络编程之粘包的相关文章

网络编程中粘包的处理方法

写在前面的话:因为自己是才解除网络编程,在工作中第一次遇到粘包问题,我还不知道它是叫粘包问题,所以被整的晕头转向,百思不得其解,自己的代码到底哪里出了问题,最后只能单步调试程序才发现接收方接收到的数据并不一定是按自己设想那样,一次接收整个数据包,当时就想到用文件长度来判断是否接收完文件,之后读了UNP才知道是粘包问题.记录以下当时自己的处理方法. 面对网络编程中的发送文件时的粘包问题,我的处理方法是在要发送文件前,首先发送文件的长度,获取的文件长度是UlongLong类型的整数,发送 时需要转换

网络编程ssh,粘包

1.什么是socket? TCP,可靠地,面向连接协议,有阻塞rect udp,不可靠的,无线连接的服务 这里因为不需要阻塞,所以速度会很快,但安全性不高 2.关于客户端退出而服务器未退出的解决办法 1 import socket 2 sock=socket.socket() # TCP协议 3 IP_PORT=("127.0.0.1",8899) 4 sock.bind(IP_PORT) 5 sock.listen(5) 6 while 1: 7 conn,addr=sock.acc

网络编程-之------粘包现象

一.什么是粘包 须知:只有TCP有粘包现象,UDP永远不会粘包 粘包不一定会发生 如果发生了:1.可能是在客户端已经粘了 2.客户端没有粘,可能是在服务端粘了 首先需要掌握一个socket收发消息的原理 应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因.(因为TCP是流式协议,不知道啥时候开始,啥时候结束).而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提

《Python》网络编程之黏包

黏包 一.黏包现象 同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包. server端 import socket sk = socket.socket() sk.bind(('127.0.0.1', 9000)) sk.listen() conn, addr = sk.accept() conn.send(b'hello,') conn.send(b'world') conn.close() client端 import

网络编程基础粘包现象

粘包 tcp是流式传输,字节流,数据与数据之间是没有边界的 流式传输优点: 不限定长度 可靠传输 缺点: 慢 和一个人的通信连接conn会一直占用我们的通信资源 udp协议,面向数据包的传输 数据包优点 快 由于不需要建立连接,所以谁发的消息我都能接受到 缺点 不能传输过长的数据 不可靠 粘包现象 由于流式传输的特点,产生了数据连续发送的粘包现象. 在一个conn建立起来的连接上传输的多条数据是没有边界的 数据的发送和接收实际上不是在执行send/recv的时候就立刻被发送和接收,而是需要经过操

网络编程 之粘包问题、使用socketserver实现并发

一.粘包问题 注意:粘包问题只有tcp协议并且udp协议永远不会粘包 粘包问题的产生: 简述:粘包问题的产生主要是由于tcp协议传输数据(其内置的nagle算法来进行的)会将数据较小的且发送时间较短的合并成一个包从发送端发送出去,接收端不知道该怎么去想要的数据拿出来这样造成了粘包问题,另一方面是由于时间太短接收端没有及时拿干净 传来的数据造成数据混乱(这是因为tcp协议又叫流氏协议指的是其就像水流一样传输数据)才产生的粘包问题. 1.发送端由于时间太短造成多个包合在一起发送产生粘包问题的实例如下

网络编程之粘包

粘包: 传输层协议有tcp和udp两种 tcp:Transmission Control Protocol 传输控制协议,基于数据流,收发的消息不能为空,需要在客户端和服务端都添加空消息的处理机制 tcp是可靠性协议,数据的收发都需要确认信息,这就降低了传输效率,故为了减少确认次数,tcp采用了nagle算法 将多次间隔短且数据小的数据合成一个数据流,然后发送,tcp的数据没有明确的界限,无法区分数据的开始和结束, 这就导致了可能将多个信息并为一条信息,也就是粘包 还有一种粘包情况是当一个数据包

解决网络编程的粘包问题

# 服务端 import socketimport subprocessimport structimport json servers = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)servers.bind(('127.0.0.1', 7777))servers.listen(10)print('start settings') while True:    """连接循环""

python/socket编程之粘包

python/socket编程之粘包 粘包: 只有TCP有尿包现象,UDP永远不会粘包. 首先需要掌握一个socket收发消息的原理 发送端可以是1k,1k的发送数据而接受端的应用程序可以2k,2k的提取数据,当然也有可能是3k或者多k提取数据,也就是说,应用程序是不可见的,因此TCP协议是面来那个流的协议,这也是容易出现粘包的原因而UDP是面向笑死的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任一字节的数据,这一点和TCP是很同的.怎样定义消息呢?认为对方一次