zoukankan      html  css  js  c++  java
  • Day 6-3 粘包现象

    服务端:

     1 import socket
     2 import subprocess
     3 
     4 phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
     5 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
     6 phone.bind(("127.0.0.1", 8990))
     7 
     8 phone.listen(10)
     9 
    10 print("运行中...")
    11 while True:
    12     conn, client_ipaddr = phone.accept()
    13     print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
    14     while True:  # 通信循环
    15         try:
    16             # 1,接收客户端发送的命令
    17             cmd = conn.recv(1024)
    18             if not cmd: break
    19             # 2,在服务器上执行客户端发过来的命令
    20             cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
    21                                    stdout=subprocess.PIPE,
    22                                    stderr=subprocess.PIPE)
    23             stdout = cmd.stdout.read()
    24             stderr=cmd.stderr.read()
    25             # 3,把执行结果发送给客户端
    26             conn.send(stdout+stderr)
    27         except ConnectionResetError:  # 针对windows系统,客户端强制断开后,会报这个错误.
    28             break
    29     conn.close()
    30 phone.close()

    客户端:

     1 import socket
     2 import os
     3 if os.name =="nt":
     4     code = "GBK"
     5 else:
     6     code="utf-8"
     7 
     8 phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
     9 
    10 phone1.connect(("127.0.0.1", 8990))
    11 
    12 while True:
    13     #1,发送命令给服务器
    14     cmd = input("请输入你要发送的信息:").strip()
    15     if not cmd:continue  
    16     phone1.send(cmd.encode("utf-8"))
    17     #2,接收服务器执行命令后的结果.
    18     data = phone1.recv(1024)
    19     print(data.decode(code))
    20 phone1.close()

    我们分别启动服务端和客户端.然后在客户端上执行一个名 tree c: (windows系统).服务端返回的结果如下:

     1 C:
     2 ├─e_object
     3 ├─GeePlayerDownload
     4 ├─Intel
     5 │  └─Logs
     6 ├─Program Files
     7 │  ├─Common Files
     8 │  │  ├─Microsoft Shared
     9 │  │  │  ├─Filters
    10 │  │  │  ├─ink
    11 │  │  │  │  ├─ar-SA
    12 │  │  │  │  ├─bg-BG
    13 │  │  │  │  ├─cs-CZ
    14 │  │  │  │  ├─da-DK
    15 │  │  │  │  ├─de-DE
    16 │  │  │  │  ├─el-GR
    17 │  │  │  │  ├─en-US
    18 │  │  │  │  ├─es-ES
    19 │  │  │  │  ├─et-EE
    20 │  │  │  │  ├─fi-FI
    21 │  │  │  │  ├─fr-FR
    22 │  │  │  │  ├─fsdefinitions
    23 │  │  │  │  │  ├─auxpad
    24 │  │  │  │  │  ├─keypad
    25 │  │  │  │  │  ├─main
    26 │  │  │  │  │  ├─numbers
    27 │  │  │  │  │  ├─oskmenu
    28 │  │  │  │  │  ├─osknumpad
    29 │  │  │  │  │  ├─oskpred
    30 │  │  │  │  │  ├─symbols
    31 │  │  │  │  │  └─web
    32 │  │  │  │  ├─he-IL
    33 │  │  │  │  ├─hr-HR
    34 │  │  │  │  ├─hu-HU
    35 │  │  │  │  ├─HWRCustomization
    36 │  │  │  │  ├─it-IT
    37 │  │  │  │  ├─ja-

    我们此时,在客户端继续输入ifconfig 命令,发现返回的数据依然是上次tree c:的结果.这是为什么呢?

    这是因为,客户端一次只能接收1024个字节的数据,如果超过1024个字节,那么这些数据就会在服务器的IO缓存区里暂存下来.如果现在在客户端输入ipconfg命令后,在服务端返回数据给客户端时,因为IO缓存区还有上次tree命令存留的信息,所以会先把上次的信息返回给客户端.等tree命令所有的数据都返回给客户端后,才会返回ipconfig的数据.就造成了两条命令的结果都在某一次的返回数据中.这种现象就叫做粘包.

    粘包发生需要满足的条件:

    一,在客户端:

      由于TCP协议使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。如果连续发送2个2bytes的包,这时候在客户端就已经发生了粘包现象.但是此时在服务端不一定会发生粘包.

    二,服务端:

      如果这2个包没有超出服务器接收的最大字节数(1024),就不会发生粘包.如果服务器每次只接收1bytes,那么在服务端也会发生粘包.

    怎么解决粘包这种现象呢?有人说把客户端接收的最大字节值改成其他更大的数字,不就可以了吗?一般情况下,最大接收字节数的值不超过8192.超过这个数,会影响接收的稳定性和速度.

    send和recv对比:

    1.不管是send还是recv,都不是直接把数据发送给对方,而是通过系统发送.然后从系统内存中读取返回的数据.

    2.send和recv不是一一对应的.

    3.send工作流程:把数据发送给操作系统,让系统调用网卡进行发送.send就完成了工作

    recv工作流程,等待客户端发送过来的数据.这个时间比较长.接收到数据后,再从系统内存中调用数据.

    粘包问题只存在于TCP中,Not UDP

    还是看上图,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

    例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

    所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

    总结

    1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

      

      2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

      3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头

    解决粘包现象的思路:

      通过上述的实验和例子,我们知道,粘包现象的产生,主要是客户端不知道要接收多少数据(或者说多大的数据).那么,按照这个思路,那么我们知道,在服务端执行完命令后,我们可以在服务端获取结果的大小.再发送给客户端,让客户端知道被接收数据的大小,然后再通过一个循环,来接收数据即可.这时我们需要用一个新的模块,struct来制作报头信息.发送给客户端.

    import struct
    pack = struct.pack("i",10000)   # 定义格式
    print(pack,len(pack),type(pack))    # pack的类型是bytes,传输的时候,就不用encode了.
    
    t = struct.unpack("i",pack)     #解包,
    print(t)        # 获取元组形式的数据.
    t = struct.unpack("i",pack)[0] # 直接获取数据的值.
    
    """
    b"x10'x00x00" 4 <class 'bytes'>
    (10000,)
    直接获取: 10000
    """
     1 #!_*_ coding:utf-8 _*_
     2 import socket
     3 import subprocess
     4 import struct
     5 
     6 phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
     7 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
     8 phone.bind(("127.0.0.1", 8990))
     9 
    10 phone.listen(10)
    11 
    12 print("运行中...")
    13 while True:
    14     conn, client_ipaddr = phone.accept()
    15     print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
    16     while True:  # 通信循环
    17         try:
    18             # 1,接收客户端发送的命令
    19             cmd = conn.recv(1024)
    20             if not cmd: break
    21             # 2,在服务器上执行客户端发过来的命令
    22             cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
    23                                    stdout=subprocess.PIPE,
    24                                    stderr=subprocess.PIPE)
    25             stdout = cmd.stdout.read()
    26             stderr=cmd.stderr.read()
    27             # 3,把执行结果发送给客户端
    28             #3-1 把报头(固定长度)发送给客户端
    29             total_size = len(stdout+stderr)
    30             print(total_size)
    31             header = struct.pack("i",total_size) # i是类型,total_size是值.这个命令会把total_size打包成一个4个字节长度的字节数据类型
    32             conn.send(header) # 把报头发送给客户端
    33             #302 发送数据给客户端
    34 
    35             conn.send(stdout)
    36             conn.send(stderr)
    37         except ConnectionResetError:  # 针对windows系统,客户端强制断开后,会报这个错误.
    38             break
    39     conn.close()
    40 phone.close()
    粘包解决服务端
     1 #!_*_ coding:utf-8 _*_
     2 import socket
     3 import os
     4 import struct
     5 
     6 if os.name == "nt":
     7     code = "GBK"
     8 else:
     9     code = "utf-8"
    10 
    11 phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
    12 
    13 phone1.connect(("127.0.0.1", 8990))
    14 
    15 while True:
    16     # 1,发送命令给服务器
    17     cmd = input("请输入你要发送的信息:").strip()
    18     if not cmd: continue
    19     phone1.send(cmd.encode("utf-8"))
    20     # 2,接收服务器执行命令后的结果.
    21     # 2-1 接收服务器发过来的报头
    22     header = phone1.recv(4)  # 收报头
    23     total_size = struct.unpack("i", header)[0] #解包,并取出报头中数据
    24 
    25     # 2-2 循环接收数据
    26     recv_size = 0
    27     recv_data = b""
    28     while recv_size < total_size:
    29         data = phone1.recv(1024)  # 接收数据
    30         recv_data += data  # 拼接数据
    31         recv_size += len(data)  # 设置已接收数据的大小
    32     print(recv_data.decode(code))
    33 phone1.close()
    粘包解决客户端

    上面粘包解决办法中存在着一些问题:

    1,struct制作报头的时候,不管是i还是l模式,total_size都有可能超出它们俩的范围.程序就会报错.

    1   total_size = len(stdout+stderr)
    2             print(total_size)
    3             header = struct.pack("i",total_size) 

    2,报头信息不应该只有文件大小信息.还应该包含其他文件信息.

    新思路:

       设置一个字典,字典中包含了文件的信息,(大小,名称,md5等).然后通过json.dumps转换成字符串格式,再把转换后的数据转成bytes类型(便于网络传输)
    .再然后,通过struct模块,把bytes类型的制作成一个报头(报头长度依然是4bytes),发给客户端.然后客户端接收后,反序列化,获取字典中文件的大小.然后开始接收文件.

    服务端:

     1 #!_*_ coding:utf-8 _*_
     2 import socket
     3 import subprocess
     4 import struct
     5 import json
     6 
     7 phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
     8 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
     9 phone.bind(("127.0.0.1", 8990))
    10 
    11 phone.listen(10)
    12 
    13 print("运行中...")
    14 while True:
    15     conn, client_ipaddr = phone.accept()
    16     print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
    17     while True:  # 通信循环
    18         try:
    19             # 1,接收客户端发送的命令
    20             cmd = conn.recv(1024)
    21             if not cmd: break
    22             # 2,在服务器上执行客户端发过来的命令
    23             cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
    24                                    stdout=subprocess.PIPE,
    25                                    stderr=subprocess.PIPE)
    26             stdout = cmd.stdout.read()
    27             stderr=cmd.stderr.read()
    28             # 3,把执行结果发送给客户端
    29             #3-1 把报头(固定长度)发送给客户端
    30             header_dict ={"filename":"a.txt",
    31                           "md5":"a0id2ndnk23nmnm1bazi23",
    32                           "total_size":len(stdout+stderr)
    33             }
    34             #3-1-1,把字典序列化为字符串
    35             header_json = json.dumps(header_dict)
    36             #3-1-2,把序列化后的数据转成bytes类型,便于网络传输
    37             header_bytes = header_json.encode("utf-8")
    38             #3-1-3,把bytes类型的数据做成一个报头
    39             struct.pack("i",len(header_bytes))    # 对应客户端的 obj = phone1.recv(4)
    40             #3-1-4,发送报头给客户端
    41             conn.send(struct.pack("i",len(header_bytes)))
    42             #3-1-5.把报头信息发给客户端
    43             conn.send(header_bytes)     #对应客户端的header_bytes = phone1.recv(header_size)
    44             #302 发送数据给客户端
    45             conn.send(stdout)
    46             conn.send(stderr)
    47         except ConnectionResetError:  # 针对windows系统,客户端强制断开后,会报这个错误.
    48             break
    49     conn.close()
    50 phone.close()

    客户端:

     1 #!_*_ coding:utf-8 _*_
     2 import socket
     3 import os
     4 import struct
     5 import json
     6 if os.name == "nt":
     7     code = "GBK"
     8 else:
     9     code = "utf-8"
    10 
    11 phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
    12 
    13 phone1.connect(("127.0.0.1", 8990))
    14 
    15 while True:
    16     # 1,发送命令给服务器
    17     cmd = input("请输入你要发送的信息:").strip()
    18     if not cmd: continue
    19     phone1.send(cmd.encode("utf-8"))
    20     # 2,接收服务器执行命令后的结果.
    21     # 2-1 接收服务器发过来的报头
    22     obj = phone1.recv(4)  # 收报头
    23     header_size= struct.unpack("i",obj)[0]  #获取报头长度
    24     header_bytes = phone1.recv(header_size) # 收取报头信息(bytes格式)
    25     header_json = header_bytes.decode("utf-8") #解码报头信息
    26     header_dict=json.loads(header_json)         # 反序列化,获取字典内容
    27     print(header_dict)
    28     total_size = header_dict["total_size"]      # 获取total_size的值
    29 
    30     # 2-2 循环接收数据
    31     recv_size = 0
    32     recv_data = b""
    33     while recv_size < total_size:
    34         data = phone1.recv(1024)  # 接收数据
    35         recv_data += data  # 拼接数据
    36         recv_size += len(data)  # 设置已接收数据的大小
    37     print(recv_data.decode(code))
    38 phone1.close()

    制作报头的流程:

  • 相关阅读:
    钱到用时方恨少(随记)
    Ring0 打印log文件
    Mutation Testing(变异测试)
    GitHub
    常用js收藏
    ASP.NET初学者常用知识
    ASP.NET页面刷新方法总结
    C#中抽象类和接口的区别
    GridView 72般绝技
    55种网页常用小技巧
  • 原文地址:https://www.cnblogs.com/lovepy3/p/9127541.html
Copyright © 2011-2022 走看看