python之网络编程粘包

一、粘包

  1. 粘包现象

    # 服务端
    import socket
    import subprocess
    phone = socket.socket()
    phone.bind(('127.0.0.1',8888))
    phone.listen(5)
    while 1:
        conn,addr = phone.accept()
        while 1:
            cmd = conn.recv(1024)
            ret = subprocess.Popen(cmd.decode('utf-8'),
                                  shell=True,
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.PIPE)
            result = ret.stdout.read() + ret.stderr.read()
            conn.send(result)
        conn.close()
    phone.close()
    # 客户端
    import socket
    import subprocess
    phone = socket.socket()
    phone.connect(('127.0.0.1',8888))
    while 1:
        cmd = input('>>>')
        phone.send(cmd.encode('utd-8'))
        ret = phone.recv(1024)
        print(ret.decode('gbk'))
    phone.close()

    ? 以上两个服务器和客户端,当服务端发送的内容超过1024时,客户端并不会将内容一次全部读取出来,而会在客户端下次输入命令时将上次剩余的内容读取出来,而下次命令的结果又会滞留到下下次。这种现象就是粘包,它与系统缓冲区有关。

    服务端 客户端
    第一次dir 发送418个字节 接收418个字节
    第二次ipconfig 发送1517个字节 接收1024个字节
    第三次dir 发送418个字节 接收493个字节
  2. 系统缓冲区

    '''每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
    
    write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
    
    TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
    
    read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
    
    这些I/O缓冲区特性可整理如下:
    
    1.I/O缓冲区在每个TCP套接字中单独存在;
    2.I/O缓冲区在创建套接字时自动生成;
    3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
    4.关闭套接字将丢失输入缓冲区中的数据。'''
    
    # 输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:
    
    1.unsigned optVal;
    2.int optLen = sizeof(int);
    3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen);
    4.printf("Buffer length: %d\n", optVal);

    只有TCP才有粘包现象,UDP永远不会粘包。

    缓冲区的作用:

    没有缓冲区的话,如果网络出现波动或短暂异常,接收数据就会出现短暂的中断,影响下载和上传的效率;但是缓冲区解决了下载和上传的问题,带来了粘包问题。

  3. 产生原因
    • recv会产生粘包(如果recv接收到数据量(1024)小于发送的数据量,第一次只能接收规定的数量1024,第二次接受剩余的数据量)

      # 服务端
      import socket
      import subprocess
      phone = socket.socket()
      phone.bind(('127.0.0.1',8888))
      phone.listen(5)
      while 1:
          conn,addr = phone.accept()
          while 1:
              try:
                  cmd = conn.recv(1024)
                  ret = subprocess.Popen(cmd.decode('utf-8'),
                                        shell=True,
                                        stdout=subprocess.PIPE,
                                        stderr=subprocess.PIPE)
                  correct_msg = ret.stdout.read()
                  error_msg = ret.stderr.read()
                  except ConnectionResetError:
                      break
          conn.close()
      phone.close()
      
      # 客户端
      import socket
      phone = socket.socket()
      phone.connect(('127.0.0.1',8888))
      while 1:
          cmd = input('>>>')
          phone.send(cmd.encode('utf-8'))
          from_server_data = phone.recv(1024)
          print(from_server_data.decode('gbk'))
      phone.close()
      # 由于客户端发的命令获取的结果大小已经超过1024,那么下次在输入命令,会继续取上次残留到缓存区的数据。
    • send也可能发生粘包现象(连续send少量的数据发送到缓冲区,由于缓冲区的机制,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络)
      # 服务端
      import socket
      phone = socket.socket()
      phone.bind(('127.0.0.1', 8080))
      phone.listen(5)
      conn, client_addr = phone.accept()
      first_data = conn.recv(1024)
      print('1:',first_data.decode('utf-8')) # 1:helloworld
      second_data = connrecv(1024)
      print('2:',second_data.decode('utf-8'))
      conn.close()
      phone.close()
      
      # 客户端
      import socket
      phone = socket.socket()
      phone.connect(('127.0.0.1', 8080))
      phone.send(b'hello')
      phone.send(b'world')
      phone.close()
      # 两次发送信息时间间隔太短,数据小,造成服务端一次收取
  4. 解决方案
    1. 错误事例:

      • 扩大recv的上限,不是解决这个问题的根本原因,因为recv到的东西都会直接存放到内存
      • 故意延长recv的时间,会严重影响程序运行效率
    2. 解决粘包现象的思路分析

      recv的工作原理:

      '''
      源码解释:
      Receive up to buffersize bytes from the socket.
      接收来自socket缓冲区的字节数据,
      When no data is available, block untilat least one byte is available or until the remote end is closed.
      当缓冲区没有数据可取时,recv会一直处于阻塞状态,直到缓冲区至少有一个字节数据可取,或者远程端关闭。
      When the remote end is closed and all data is read, return the empty string.
      关闭远程端并读取所有数据后,返回空字符串。
      '''
      # 服务端
      import socket
      phone = socket.socket()
      phone.bind(('127.0.0.1',8888))
      phone.listen(5)
      conn,client_addr = phone.accept()
      from_client_data1 = conn.recv(2)
      print(from_client_data1)  # b'he'
      from_client_data2 = conn.recv(2)
      print(from_client_data2)  # b'll'
      from_client_data3 = conn.recv(1)
      print(from_client_data3)  # b'o'
      conn.close()
      phone.close()
      
      # 客户端
      import socket
      import time
      phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
      phone.connect(('127.0.0.1',8080))
      phone.send('hello'.encode('utf-8'))
      time.sleep(20)
      phone.close()

      总体思路:根据recv工作原理,我们可以采取send一次,recv多次的方法解决粘包问题。

      1. 在第二次给服务器发送命令之前,应该循环recv直至将所有的数据全部取完
      2. 当发送的总bytes数与接收的总bytes数相等时,循环结束
      3. 通过len()获取发送的总bytes数
      4. 将总bytes数转化为固定长度的bytes并且报头与数据内容封装到一起发送
      5. 接收时先recv(4个字节),得到发送的总bytes数
      6. 将每次recv的字节数累加,当等于发送的总字节数时循环终止

      代码实现:

      # 服务端
      import socket
      import subprocess
      import struct
      phone = socket.socket()
      phone.bind(('127.0.0.1',8888))
      phone.listen(5)
      print('start')
      conn,addr = phone.accept()
      while 1:
          try:
              cmd = conn.recv(1024)
              obj = subprocess.Popen(cmd.decode('utf-8'),
                                    shell=True,
                                    stdout=subprocess.PIPE,
                                    stderr=subprocess.PIPE)
              result = obj.stdout.read()+obj.stderr.read()
              total_size = len(result)  # 1.制作报头
              total_size_bytes = struct.pack('i',total_size)    # 2.将不固定长度int类型转化为固定长度bytes  4 个字节,将一个数字转化成等长度的bytes类型
              conn.send(total_size_bytes)   # 3.发送报头
              conn.send(result)
          except ConnectionResetError:
              break
      conn.close()
      phone.close()
      
      # 客户端
      import socket
      import struct
      phone = socket.socket()
      while 1:
          cmd = input('>>>').strip()
          phone.send(cmd.encode('utf-8'))
          head_bytes = phone.recv(4)    # 1.接收报头
          total_size = struct.unpack('i',head_bytes)[0] # 将报头反解回int类型
          total_data = b''
          while len(total_data) < total_size:   # 循环接收原数据
              total_data += phone.recv(1024)
          print(total_data.decode('gbk'))
      phone.close()

      但是这种方案有个问题,当数据非常大时(即字节长度过长时)struct会报错;报头信息不可能只包含数据大小,还应有md5,文件名,文件路径等。

      可采取同样的方法将这些所有的信息封装到一个字典中,在给字典添加报头统计字典的bytes长度,先将报头发送过去,得到字典的长度,在接收等长度的字节得到字典,通过字典拿到数据的总bytes长度。

      # 服务端
      import socket
      import subprocess
      import struct
      import json
      phone = socket.socket()
      phone.bind(('127.0.0.1',8888))
      phone.listen(3)
      print('start')
      conn,addr = phone.accept()
      while 1:
          try:
              cmd = conn.recv(1024)
              obj = subprocess.Popen(cmd.decode('utf-8'),
                                    shell=True,
                                    stdout=subprocess.PIPE,
                                    stderr=subprocess.PIPE)
              result = obj.stdout.read()+obj.stderr.read()
              result = result.decode('gbk').encode('utf-8')
              head_dict = {'md5':'fcea920f7412b5da7be0cf42b8c93759',
                          'file_name':'命令',
                          'file_size':len(result)}  # 1.制作字典
              head_dict_json = json.dumps(head_dict)    # 2.将字典转化为json字符串
              head_dict_json_bytes = head_dict_json.encode('utf-8') # 3.将json字符串转化为bytes
              head_len = len(head_dict_json_bytes)  # 4.获取报头长度
              head_len_bytes = struct.pack('i',head_len)    # 5.将长度转化为固定4个字节
              conn.send(head_len_bytes) # 6.发送固定4个字节
              conn.send(head_dict_json_bytes)   # 7.发送报头
              conn.send(result) # 8.发送原数据
          except ConnectionResetError:
              break
      conn.close()
      phone.close()
      
      # 客户端
      import socket
      import subprocess
      import struct
      import json
      phone = socket.socket()
      phone.connect(('127.0.0.1',8888))
      while 1:
          cmd = input('>>>').strip()
          phone.send(cmd.encode('utf-8'))
          head_bytes = phone.recv(4)    # 1.接收报头
          head_size = struct.unpack('i',head_bytes)[0]  # 2.将报头反解回int类型
          head_dict_json_bytes = phone.recv(head_size)  # 3.接收bytes类型字典
          head_dict_json = head_dict_json_bytes.decode('utf-8') # 4.得到序列化的字典
          head_dict = json.loads(head_dict_json)    # 5.得到字典
          total_data = b''
          while len(total_data) < head_dict['file_size']:
              total_data += phone.recv(1024)
          print(total_data.decode('utf-8'))
      phone.close()

二、文件上传

# 服务端
import socket
import json
import struct
import os

MY_FILE = os.path.join(os.path.dirname(__file__),'上传文件') # 文件保存路径

def socket_server():
    server = socket.socket()
    server.bind(('127.0.0.1',8888))
    server.listen(5)
    conn,addr = server.accept()
    head_len_bytes = conn.recv(4) # 得到bytes类型字典长度
    head_len = struct.unpack('i',head_len_bytes) # 得到字典长度
    file_info_json_bytes = conn.recv(head_len) # 得到bytes类型字典
    file_info_json = file_info_json_bytes.decode('utf-8') # 得到序列化字典
    file_info = json.loads(file_info_json) # 得到字典
    with open(os.path.join(MY_FILE,file_info['file_new_name']),mode='wb') as f: # 接受文件,收一段写一段
        total_size = 0
        while total_size < file_info['file_size']: # 当接收长度等于总长度结束
            every_data = conn.recv(1024)
            f.write(every_data)
            total_size += every_data
    conn.close()
    server.close()

socket.server()
# 客户端
import socket
import json
import struct
import os
import hashlib

FILE_PATH = os.path.join(os.path.dirname(__file__),'demo.mp4')

def file_md5(file): # 对文件加密
    md5 = hashlib.md5()
    with open(file,mode='rb') as f:
        while 1:
            data = f.read(1024)
            if data:
                md5.updata(data)
            else:
                return md5.hexdigest()

def socket_client():
    client = socket.socket()
    client.connect(('127.0.0.1',8888))
    file_info = {'MD5':'350f4337bfe2ab44c24765e61ef1666c',
                'file_name':FILE_PATH,
                'file_size':os.path.getsize(FILE_PATH),
                'file_new_name':'demo1.mp4'}    # 创建字典
    file_info_json = json.dumps(file_info)  # 对字典序列化
    file_info_json_bytes = file_info_json.encode('utf-8')# 转化为bytes类型
    head_len = len(file_info_json_bytes)    # 得到字典长度
    head_len_bytes = struct.pack('i',head_len)  # 转化为固定长度bytes
    client.send(head_len_bytes) #发送bytes类型字典长度
    client.send(file_info_json_bytes)   # 发送字典
    with open(FILE_PATH,mode='rb') as f: # 发送文件内容,读一段发一段
        total_size = 0
        while total_size < file_info['file_size']: # 当发送长度等于总长时结束
            every_data = f.read(1024)
            client.send(every_data)
            total_size +=len(every_data)
    client.close()

socket_client()

三、socketserver实现并发

使用socketserver可以实现和多个客户端通信

代码如下:

import socketserver # 引入模块
class MyServer(socketserver.BaseRequestHandler): # 类名随便定义,但必须继承socketserver.BaseRequestHandler此类
    def handle(self): # 写一个handle方法,固定名字
        while 1:
            # self.request相当于conn管道
            from_client_data = self.request.recv(1024).decode('utf-8')
            print(from_client_data)
            to_client_data = input('服务端回消息:').strip()
            self.request.send(to_client_data)

if __name__ == '__main__':
    ip_port = ('127.0.0.1',8888)
    # socketserver.TCPServer.allow_reuse_address = True # 允许端口重用
    server = socketserver.ThreadingTCPServer(ip_port,MyServer)
    # 对socketserver.ThreadingTCPServer类实例化对象,将IP地址,端口号以及自己定义的类名传入,并返回一个对象
    server.server_forever() # 对象执行server_forever方法,开启服务端

源码剖析:

'''在整个socketserver这个模块中,其实就干了两件事情:1、一个是循环建立链接的部分,每个客户链接都可以连接成功  2、一个通讯循环的部分,就是每个客户端链接成功之后,要循环的和客户端进行通信。
看代码中的:server=socketserver.ThreadingTCPServer(('127.0.0.1',8090),MyServer)

还记得面向对象的继承吗?来,大家自己尝试着看看源码:

查找属性的顺序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer

实例化得到server,先找ThreadMinxIn中的__init__方法,发现没有init方法,然后找类ThreadingTCPServer的__init__,在TCPServer中找到,在里面创建了socket对象,进而执行server_bind(相当于bind),server_active(点进去看执行了listen)
找server下的serve_forever,在BaseServer中找到,进而执行self._handle_request_noblock(),该方法同样是在BaseServer中
执行self._handle_request_noblock()进而执行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),然后执行self.process_request(request, client_address)
在ThreadingMixIn中找到process_request,开启多线程应对并发,进而执行process_request_thread,执行self.finish_request(request, client_address)
上述四部分完成了链接循环,本部分开始进入处理通讯部分,在BaseServer中找到finish_request,触发我们自己定义的类的实例化,去找__init__方法,而我们自己定义的类没有该方法,则去它的父类也就是BaseRequestHandler中找....
源码分析总结:

基于tcp的socketserver我们自己定义的类中的

  self.server即套接字对象
  self.request即一个链接
  self.client_address即客户端地址
基于udp的socketserver我们自己定义的类中的

  self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
  self.client_address即客户端地址'''

原文地址:https://www.cnblogs.com/yaoqi17/p/11217881.html

时间: 2024-08-30 10:42:08

python之网络编程粘包的相关文章

Python之网络编程 黏包

黏包现象 系统缓冲区 缓冲区的作用 没有缓冲区 , 如果你的网路出现短暂的异常或者波动, 接收数据就会出现短暂的中断, 影响你的下载或者上传的效率 但是凡事都有双刃剑, 缓冲区解决了上传下载的传输效率问题 也带来了黏包的问题 讲粘包之前先看看socket缓冲区的问题: 每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区. write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器.一旦将数据写入到缓冲区,函数

第六章 - 网络编程 - 粘包

1.粘包: 多个包 多个命令的结果 粘到一起了 因为recv 1024限制了 导致的结果 参考:http://www.cnblogs.com/linhaifeng/articles/6129246.html 粘包底层原理分析: 1.运行一个软件 和 哪几个硬件 有关 硬盘 内存 cpu 2.启动程序: 硬盘程序 加载到 内存 启一个软件就占一个内存空间 os 就有一个内存空间 os的内存空间 和 软件的内存空间 彼此互相隔离 须知:只有TCP有粘包现象,UDP永远不会粘包. 所谓粘包问题主要还是

网络编程粘包问题

首先说为什么会粘包,在py2上运行时,服务器把两次发送的操作强制的合成一次发送给客户端,所以 粘在一起了,因为python3的版本是可以的,但是有的时候python3也会出现粘包现象. 解决粘包的问题有两种方法: 1  可以先sleep一下,这个样子就可以使缓冲区超时,就不在等下一次的了,这样就可以和下一条命令隔离开了 2  我在服务端来一个等待客户端确认,就ok了,这个确认不需要我们用户输入,而是客户端自动的给你来这个响应,就是说,客户端自动的写好代码,自动的给服务器一个响应,只要收到服务端的

20 网络编程 粘包现象与解决方案

1.https://www.cnblogs.com/guobaoyuan/p/6809447.html 发送数据包前 对包的长度进行计算 1. 比较low的方法是 len( package) 然后直接发送给接收端.这样会出现一个问题,就是接收端不知道你的这个 len(package)是几个字节,就也有可能会出现粘包问题. 2. 利用struct对包的长度打包为固定4个字节或8个字节. 3. struct.pack format参数为"i" 时只能打包长度为10的数字,那么还可以先将 长

【Python】网络编程

1.TCP编程 2.SocketServer模块 3.Twisted框架 4.UDP编程 1.TCP编程--TCP是面向连接的,其一般的设计如下: # encoding:utf-8 ''' Created on 2014-6-20 @author: Administrator ''' from socket import socket, AF_INET, SOCK_STREAM import time import threading class SockServer(object): def

python基础网络编程--转

python之网络编程 本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类: 消息传递(管道.FIFO.消息队列) 同步(互斥量.条件变量.读写锁.文件和写记录锁.信号量) 共享内存(匿名的和具名的) 远程过程调用(Solaris门和Sun RPC) 但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的.其实TCP/IP协议族已经帮我们解决了这个问题,网

TCP网络传输“粘包”问题,经典解决(附代码)

一.前言 关于TCP网络传输粘包,网上很多人写了原理.总结起来就一句话:这里拿Server和Client长连接,Server和Client之间通过信令传输做说明: Server发送的时候按照一条条信令发送,到达操作系统网络层,首先进入缓冲池,然后TCP协议层从池子中获取数据,传输给Client.我们知道TCP的传输有几个方案,比如,滑动窗口.1比特方案.所以Client收到的数据已经不可能是一个个完整的信令的. 个人理解TCP粘包的概念:它描述了一个场景:"信令是一个个紧挨着的,好像是被粘在一起

《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

python ==》 网络编程

 一.服务端和客户端 BS架构 (腾讯通软件:server+client) CS架构 (web网站) C/S架构与socket的关系: 我们学习socket就是为了完成C/S架构的开发 二.OSI七层模型 互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层 学习socket一定要先学习互联网协议: 1.首先:本节课程的目标就是教会你如何基于socket编程,来开发一款自己的C/S架构软件 2.其次:C/S架构的软件(软件属于应用层)是基于网络进行通信的 3.然后:网络的核心