网络编程基础:粘包现象、基于UDP协议的套接字

粘包现象:

如上篇博客中最后的示例,客户端有个 phone.recv(2014) , 当服务端发送给客户端的数据大于1024个字节时, 多于1024的数据就会残留在管道中,下次客户端再给服务端发命令时,残留在管道的数据会先发送给客户端,新命令产生的数据会排在上次命令残留数据的后面发送到客户端,即两次结果的数据粘在一起了, 这个就是粘包现象。

粘包现象的原理分析:

# 运行一个软件或程序需要的硬件:CPU、内存、硬盘
#     CPU负责执行;CPU执行需要数据,而数据从内存中取,最后可以把数据存到硬盘(内存速度会比硬盘快很多)
# 操作系统所占的内存空间和应用程序所占的内存空间互相隔离


# 客户端的 phone.send(数据) send是应用软件的代码,是发送给操作系统的命令,应用软件先把要发送的数据copy给操作系统的内存然后让操作系统把该数据发送出去
# 应用软件把数据复制给操作系统后,操作系统怎么发这个数据应用软件控制不了
# recv的完成需要2步:1.recv对应的操作系统等待接收对方传过来的数据;(耗时长);2.recv的操作系统将接收的数据复制给应用软件(耗时短,因为是本地copy)
# send的完成只需要1步: 将数据从自己(应用软件)的内存空间复制到操作系统的内存空间。

# send和recv对比
# 1. 不管是recv还是send都不是在直接接收对方的数据,而是在操作自己的操作系统内存---> so,不是一个send就要对应一个recv
# 2. recv:
#         wait data 耗时非常长
#         copy data
#    send:
#         copy data
# 3. TCP协议的特点:发送端为了将多个发往接收端的包能有效的发到对方,会将多次间隔较小且数据量小的数据,合并成一个大的数据包,然后进行封装;这样接收端就难以分辨出来了,即面向流的通信是无消息保护边界的。

粘包解决方法普通版(制作自己的“报头”):

补充知识点struct模块:

import struct

# 制作报头
res = struct.pack("i",123498654)  # 输出结果是bytes格式 # i 代表整型,如果是整型,res这个bytes就是固定长度4(跟后面整数具体的大小无关); #有两个参数:第一个是格式("i"代表整型),第二个是值
print(res,type(res),len(res))
"""
struct.pack可用于制作报头,把描述信息传入第二个参数value
"""
# 打印结果
# b'x9ep\x07' <class 'bytes'> 4


# 解析报头
unpack_res = struct.unpack("i",res)  # 对res这个bytes格式的字符串进行解包 # 解包结果为元祖形式 # 也有两个参数: 1. 格式("i") 2. bytes格式的字符串
print(unpack_res)

# 打印结果
# (123498654,)

粘包解决方法普通版客户端代码如下:

import socket
import struct

client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",9901))
while True:
    # 1. 发命令
    cmd = input(">>>").strip()
    if not cmd:continue
    client.send(cmd.encode("utf-8"))

    # 2. 拿到命令结果并打印
    # 第一步:先收“报头”
    head = client.recv(4)  # head为bytes格式; # 由于报头的长度固定为4,所以recv应该为4
    # 第二步:从报头解析出对真实数据的描述信息(真实数据的长度)
    total_size = struct.unpack("i",head)[0]  # 对head这个报头解析  # struct.unpack()的结果是元祖的形式

    # 第三步:接收真实的数据
    """开始循环接收服务端发来的真实数据"""
    recv_size = 0  # 用于计算接收到的bytes数
    total_recv_res = b""  # 设置一个bytes格式的空字符串,用于拼接、接收服务端发来的真实数据
    while recv_size < total_size:   # 已经接收的bytes数小于服务端发送的全部字节数
        recv_res = client.recv(1024)
        total_recv_res += recv_res  # 把每次从服务端接收到的真实数据添加到total_recv_res 里面
        recv_size += len(recv_res)  # 每次从服务端接收到的数据的bytes数加到 recv_size里面; 不要用 recv_size += 1024,这种方法不能准确计算出bytes数
    print(total_recv_res.decode("gbk"))

phone.close()

服务端代码:

import subprocess
import struct
import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",9901))
phone.listen(5)

while True:
    conn,client_addr = phone.accept()

    while True:
        try:
            # 1. 收命令
            cmd = conn.recv(1024)

            # 2. 执行命令,拿到结果
            obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()

            # 3. 把命令结果返回给客户端
            # 第一步:制定固定长度的“报头”(报头一定需要是固定长度)
            head = struct.pack("i",len(stdout)+len(stderr))  # head 是bytes格式,长度固定为4

            # 第二步: 把报头发送给客户端
            conn.send(head)

            # 第三步:再发送真实的结果数据
            # conn.send(stdout+stderr) 解决的方法如下所示:
            conn.send(stdout)
            conn.send(stderr)  # 不需要再用“+”,因为这种形式的发送TCP协议就会把数据粘在一起
        except ConnectionResetError:
            break

    conn.close()
phone.close()

粘包解决最终版(利用字典制作自己的报头)

服务端代码:

import socket
import subprocess
import json
import struct

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

while True:
    conn,addr = server.accept()

    while True:
        try:
            # 1. 收命令
            cmd = conn.recv(1024)

            # 2. 处理命令
            obj = subprocess.Popen(cmd.decode("utf-8"),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            stdout = obj.stdout.read()   # bytes格式
            stderr = obj.stderr.read()

            # 3. 把处理结果发送给客户端
            """
            报头应该包含多种信息,而不只是只包含真实信息的长度,所以考虑利用字典去制定报头
            """
            # 3.1 用字典形式制作报头
            header_dict = {
                "filename":"cmd处理",
                "md5":"xxxxxx",
                "total_size":len(stdout)+len(stderr)
            }  # 字典不能用于send(),只有bytes才可以
            header_json = json.dumps(header_dict)  # 将报头字典转化成json格式的字符串
            header_bytes = header_json.encode("utf-8")  # 将json形式的字符串转化成bytes格式
            """
            字典是报头,报头转化成bytes之后你并不能确定bytes的个数,但报头又需要是固定长度,
            所以先把head_bytes(报头的bytes格式)的长度利用struct模块打包成固定长度发送给客户端,然后再把header_bytes发送给客户端,
            对应的,客户端先收报头长度,然后再接收报头长度个数的bytes,那么客户端第二次接收的bytes就是报头的完整信息
            """
            # 3.2 发送报头bytes的长度
            header_length = struct.pack("i",len(header_bytes))  # 把报头bytes的个数利用struct.pack()打包、制定成固定长度(4)
            conn.send(header_length)
            # 3.3 发送报头bytes的真实信息
            conn.send(header_bytes)
            # 3.4 发送处理结果的真实数据
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break
    conn.close()
server.close()

客户端代码:

import socket
import json
import struct

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

while True:
    cmd = input(">>>").strip()
    if not cmd: continue
    client.send(cmd.encode("utf-8"))

    # 1. 接收报头的长度
    obj_contain_length = client.recv(4)  # 因为报头bytes的长度已经经过struct.pack()的打包,长度固定为4
    header_bytes_length = struct.unpack("i",obj_contain_length)[0]  # 报头bytes的长度
    # 2. 接收报头的数据
    header_bytes = client.recv(header_bytes_length)  # 接收报头bytes个数的bytes数,就是完整的报头bytes信息
    header_json = header_bytes.decode("utf-8")  # 将bytes格式解码成json字符串格式
    header_dict = json.loads(header_json)  # 将报头的json字符串格式反序列化得到报头的字典形式
    total_size = header_dict.get("total_size")  # 得到报头字典里的处理结果的bytes数
    # 3. 接收处理结果的数据
    recv_size = 0
    total_recv_bytes = b""
    while recv_size < total_size:
        recv_bytes = client.recv(1024)
        total_recv_bytes += recv_bytes
        recv_size += len(recv_bytes)

    print(total_recv_bytes.decode("gbk"))

client.close()

文件传输功能:

文件的目录结构如下:

服务端代码:

import socket
import json
import struct
import os
import sys

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR)

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

while True:
    conn,addr = server.accept()

    while True:
        try:
            # 1. 收命令
            res = conn.recv(1024) # b"get a.txt"

            # 2. 解析命令,提取相应命令参数
            cmd,file_name = res.decode("utf-8").split()  # ["get","a.txt"]

            # 3. 以读的模式打开文件,读取文件内容发送给客户端
            # 第一步:用字典形式制作报头
            file = os.path.join(BASE_DIR,"share",file_name)
            header_dict = {
                "filename":file_name,
                "md5":"xxxxxx",
                "total_size": os.path.getsize(file)
            }
            header_json = json.dumps(header_dict)
            header_bytes = header_json.encode("utf-8")

            # 第二步:发送报头bytes的长度
            header_length = struct.pack("i",len(header_bytes))
            conn.send(header_length)
            # 第三步:发送报头bytes的真实信息
            conn.send(header_bytes)

            # 4. 发送处理结果的真实数据
            with open(file,"rb") as f:
                for line in f:
                    conn.send(line)  # 单行发送跟一下全部发送效果上没有区别,因为单行发送也是粘在一起

        except ConnectionResetError:
            break
    conn.close()
server.close()

客户端代码:

import socket
import json
import struct
import os,sys


BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR)

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

while True:
    # 1. 发命令
    cmd = input(">>>").strip()
    if not cmd: continue
    client.send(cmd.encode("utf-8"))

    # 2. 以写的模式打开一个新文件,把服务端发来的文件内容写入新文件
    # 第一步: 接收报头的长度
    obj_contain_length = client.recv(4)
    header_bytes_length = struct.unpack("i",obj_contain_length)[0]
    # 第二步:再收报头,从报头中解析出对真实信息的描述信息
    header_bytes = client.recv(header_bytes_length)
    header_json = header_bytes.decode("utf-8")
    header_dict = json.loads(header_json)
    total_size = header_dict.get("total_size")
    file_name = header_dict["filename"]
    # 3. 接收真实的数据
    with open(os.path.join(BASE_DIR,"download",file_name),"wb") as f:
        recv_size = 0
        while recv_size < total_size:
            line = client.recv(1024)
            f.write(line)
            recv_size += len(line)
            print("文件总大小:%s;已下载:%s;已下载比例:%s"%(total_size,recv_size,(recv_size/total_size)))

client.close()

文件传输功能函数版:

客户端代码:

import socket
import json
import struct
import os,sys


BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR)

"""下载功能:即从服务端接收文件"""
def get(client,cmd):
    # 2. 以写的模式打开一个新文件,把服务端发来的文件内容写入新文件
    # 第一步: 接收报头的长度
    file_name = cmd[1]
    obj_contain_length = client.recv(4)
    header_bytes_length = struct.unpack("i", obj_contain_length)[0]
    # 第二步:再收报头,从报头中解析出对真实信息的描述信息
    header_bytes = client.recv(header_bytes_length)
    header_json = header_bytes.decode("utf-8")
    header_dict = json.loads(header_json)
    total_size = header_dict.get("total_size")
    # file_name = header_dict["filename"]
    # 3. 接收真实的数据
    with open(os.path.join(BASE_DIR, "download", file_name), "wb") as f:
        recv_size = 0
        while recv_size < total_size:
            line = client.recv(1024)
            f.write(line)
            recv_size += len(line)
            print("文件总大小:%s;已下载:%s;已下载比例:%s" % (total_size, recv_size, (recv_size / total_size)))

"""上传功能:即发送文件给服务端"""
def put(client,cmd):
    file_name = cmd[1]
    file = os.path.join(BASE_DIR,"download",file_name)
    # 1. 制定报头
    file_size = os.path.getsize(file)
    head_dict ={
        "filename":file_name,
        "dm5": "xxxxxxx",
        "total_size":file_size
    }
    head_json = json.dumps(head_dict)
    head_bytes = head_json.encode("utf-8")
    # 2. 发送报头
    head_bytes_length = struct.pack("i",len(head_bytes))
    client.send(head_bytes_length)
    client.send(head_bytes)
    # 3. 发送真实数据
    with open(file,"rb") as f:
        send_size = 0
        for line in f:
            client.send(line)
            send_size += len(line)
            print("文件总大小:%s;已上传:%s;上传比例:%s"%(file_size,send_size,(send_size/file_size)))

def run():
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    client.connect(("127.0.0.1",8089))

    while True:
        # 1. 发命令
        cmd = input(">>>").strip()
        if not cmd: continue
        client.send(cmd.encode("utf-8"))
        cmd_list = cmd.split()
        cmd_dict = {
            "get":get,
            "put":put
        }
        for k,v in cmd_dict.items():
            if cmd_list[0] == k:
                v(client,cmd_list)

    client.close()

if __name__ == "__main__":
    run()

服务端代码:

import socket
import json
import struct
import os
import sys

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR)

"""下载功能:即发送数据给客户端"""
def get(conn,cmd):
    file_name = cmd[1]
    file = os.path.join(BASE_DIR, "share", file_name)
    header_dict = {
        "filename": file_name,
        "md5": "xxxxxx",
        "total_size": os.path.getsize(file)
    }
    header_json = json.dumps(header_dict)
    header_bytes = header_json.encode("utf-8")

    header_length = struct.pack("i", len(header_bytes))
    conn.send(header_length)
    conn.send(header_bytes)

    # 发送处理结果的真实数据
    with open(file, "rb") as f:
        for line in f:
            conn.send(line)

"""上传功能:即接收客户端发来的数据"""
def put(conn,cmd):
    file_name = cmd[1]
    # 1. 先接收报头长度
    bytes_header_length = conn.recv(4)
    header_length = struct.unpack("i",bytes_header_length)[0]
    # 2. 接收报头数据
    header_bytes = conn.recv(header_length)  # bytes格式的字符串
    header_json = header_bytes.decode("utf-8")
    header = json.loads(header_json)
    total_size = header["total_size"]  # 要上传数据的总大小
    # 3. 接收真实的数据
    with open(os.path.join(BASE_DIR,"share",file_name),"wb") as f:
        recv_size = 0
        while recv_size < total_size:
            recv_bytes = conn.recv(1024)
            f.write(recv_bytes)
            recv_size += len(recv_bytes)


def run():
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind(("127.0.0.1",8089))
    server.listen(5)

    while True:
        conn,addr = server.accept()

        while True:
            try:
                # 1. 收命令
                res = conn.recv(1024) # b"get a.txt"
                # 2. 解析命令,提取相应命令参数
                cmd = res.decode("utf-8").split()  # ["get","a.txt"]

                cmd_dict = {
                    "get": get,
                    "put": put
                }
                for k,v in cmd_dict.items():
                    if cmd[0] == k:
                        v(conn,cmd)

            except ConnectionResetError:
                break
        conn.close()
    server.close()

if __name__ == "__main__":
    run()

基于UDP协议的套接字:

客户端代码:

from socket import *

client = socket(AF_INET,SOCK_DGRAM)

"""
UTP不需要建链接(通道)。所以不需要 connect
"""

while True:
    msg = input(">>>").strip()
    client.sendto(msg.encode("utf-8"),("127.0.0.1",8080))  # sendto传入两个参数:数据和接收端的IP和端口 # msg也是bytes格式

    data = client.recvfrom(1024)
    print(data)

client.close()

# 运行结果:
# (b'HELLO', ('127.0.0.1', 8080))


"""
UTP协议不会粘包
UTP协议能够发送空消息,所以不需要写 if not msg: continue
UTP协议一定是一个sendto对应一个 recvfrom
对于recvfrom(1024),在Windows上,如果接收的数据大于1024个bytes,会报错;在Linux上,如果接收的数据大于1024个bytes,程序只接收1024个,多余的数据就丢失了
"""

客户端代码:

from socket import *  # 导入socket模块时可以利用 import *

server = socket(AF_INET,SOCK_DGRAM)  # DGRAM 是UDP协议,即“数据报协议”
server.bind(("127.0.0.1",8080))  # UDP协议也需要bind

"""
UTP协议没有 listen和accept;因为UTP不需要建通道,而TCP中的listen和accept是为了建通道
"""
while True:
    data,addr = server.recvfrom(1)  # 接收数据;收到的也是bytes格式 # 接收到的数据是元祖形式:第一个元素是数据信息,第二个是发送端的IP和端口 # 数据信息也是bytes格式
    print(data,addr)

    server.sendto(data.upper(),addr)  # recvfrom中包含发送端的IP和端口,还通过这个IP端口发发送端回数据
server.close()

# 运行结果:
# b'hello' ('127.0.0.1', 53729)
原文地址:https://www.cnblogs.com/neozheng/p/8563631.html