• 网络编程基础:粘包现象、基于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)
  • 相关阅读:
    第二周作业(软件需求分析与系统设计)
    自我介绍
    2019春总结作业
    2019春第十六周作业
    2019春第十五周作业
    2019春第十四周作业
    2019春第十二周作业
    2019春第十一周作业
    2019春第十周作业
    2019春第九周作业
  • 原文地址:https://www.cnblogs.com/neozheng/p/8563631.html
Copyright © 2020-2023  润新知