zoukankan      html  css  js  c++  java
  • 37.网络编程之粘包

    粘包

    黏包现象
    让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)
    执行远程命令的模块

    需要用到模块subprocess

    subprocess通过子进程来执行外部指令,并通过input/output/error管道,获取子进程的执行的返回信息。

    import os
    import subprocess
    ret = os.popen('dir').read()
    print(ret)
    print('*'*50)
    ret = subprocess.Popen('dir',shell=True,stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    print(ret.stdout)
    print(ret.stderr)

    shell= True 可以执行一个普通系统命令
    stdout 表示一个容器,返回正常的信息
    stderr 存放错误信息的容器

    执行输出:

    驱动器 E 中的卷是 file
     卷的序列号是 8077-D7B9
     
     E:python_scriptday30黏包 的目录
     
    2018/05/07  14:54    <DIR>          .
    2018/05/07  14:54    <DIR>          ..
    2018/05/07  14:54               236 a.py
                   1 个文件            236 字节
                   2 个目录 183,394,840,576 可用字节
     
    **************************************************
    <_io.BufferedReader name=3>
    <_io.BufferedReader name=4>

    执行一个错误的命令

    import os
    import subprocess
    ret = os.popen('ls').read()
    print(ret)
    print('*'*50)
    ret = subprocess.Popen('ls',shell=True,stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    print('out:',ret.stdout.read().decode('gbk'))
    print('err:',ret.stderr.read().decode('gbk'))

    执行输出:

    os.popen() 执行一个错误的命令,显示乱码

    而subprocess则不会,它还是比较完善的。

    基于tcp协议实现的黏包

    用server端,让客户端执行一个命令

     server.py

    import socket
    sk = socket.socket()
    sk.bind(('127.0.0.1',9000))
    sk.listen()
     
    conn,addr = sk.accept()
    while True:
        cmd = input('>>>')
        conn.send(cmd.encode('utf-8'))
        if cmd == 'q': break
        ret1 = conn.recv(1024)
        print('stdout : ', ret1.decode('gbk'))
        ret2 = conn.recv(1024)
        print('stderr : ',ret2.decode('gbk'))
    conn.close()
    sk.close()

    client.py

    import socket
    import subprocess
     
    sk = socket.socket()
    sk.connect(('127.0.0.1',9000))
    while True:
        cmd = sk.recv(1024).decode('utf-8')
        print(cmd)
        if cmd == 'q':break
        ret = subprocess.Popen(cmd,shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
        out = ret.stdout.read()
        err = ret.stderr.read()
        print(out,'*****
    ',err)
        sk.send(b'out :'+out)
        sk.send(b'error :'+err)
    sk.close()

    先执行server.py,再执行client.py,执行效果如下:

    首先是执行了help命令,再执行dir命令

    但是为什么都是显示help的命令结果呢?

    这就是黏包现象

    因为每次执行,固定为1024字节。它只能接收到1024字节,那么超出部分怎么办?
    等待下一次执行命令dir时,优先执行上一次,还没有传完的信息。传完之后,再执行dir命令

     总结:

    发送过来的一整条信息
    由于server端没有及时接受
    后来发送的数据和之前没有接收完的数据黏在了一起
    这就是著名的黏包现象

    那么udp会发现黏包现象吗?实践一下,就知道了

    基于udp协议实现的黏包

    server.py

    #_*_coding:utf-8_*_
    from socket import *
    import subprocess
     
    ip_port=('127.0.0.1',9000)
    bufsize=1024
     
    udp_server=socket(AF_INET,SOCK_DGRAM)
    udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    udp_server.bind(ip_port)
     
    while True:
        #收消息
        cmd,addr=udp_server.recvfrom(bufsize)
        print('用户命令----->',cmd)
     
        #逻辑处理
        res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE)
        stderr=res.stderr.read()
        stdout=res.stdout.read()
     
        #发消息
        udp_server.sendto(stderr,addr)
        udp_server.sendto(stdout,addr)
    udp_server.close()

    client.py

    from socket import *
    ip_port=('127.0.0.1',9000)
    bufsize=1024
     
    udp_client=socket(AF_INET,SOCK_DGRAM)
     
    while True:
        msg=input('>>: ').strip()
        udp_client.sendto(msg.encode('utf-8'),ip_port)
        err,addr=udp_client.recvfrom(bufsize)
        out,addr=udp_client.recvfrom(bufsize)
        print(err)
        if err:
            print('error : %s'%err.decode('gbk'),end='')
        if out:
            print(out.decode('gbk'), end='')

    先执行server.py,再执行client.py,执行效果如下:

    >>: ipconfig
    Traceback (most recent call last):
    File "E:/python_script/day30/黏包/client.py", line 11, in <module>
    out,addr=udp_client.recvfrom(bufsize)
    OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。

    在客户端执行ipconfig,就报错了,提示缓冲区过大。所以说udp不会出现黏包

    总结:

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

    subprocess不能运行windows help命令,不是因为udp问题,而是subprocess问题。

    一,粘包问题详情 

    1.只有TCP有粘包现象,UDP永远不会粘包

     你的程序实际上无权直接操作网卡的,你操作网卡都是通过操作系统给用户程序暴露出来的接口,那每次你的程序要给远程发数据时,其实是先把数据从用户态copy到内核态,这样的操作是耗资源和时间的,频繁的在内核态和用户态之前交换数据势必会导致发送效率降低, 因此socket 为提高传输效率,发送方往往要收集到足够多的数据后才发送一次数据给对方。若连续几次需要send的数据都很少,通常TCP socket 会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

    2,首先需要掌握一个socket收发消息的原理

    发送端可以是1k,1k的发送数据。而接受端的应用程序可以2k,2k的提取数据,当然也有可能是3k或者多k提取数据,也就是说,应用程序是不可见的,因此TCP协议是面来那个流的协议,
    这也是容易出现粘包的原因。而UDP是面向无连接的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任一字节的数据,这一点和TCP是很同的。
    怎样定义消息呢?
    认为对方一次性write/send的数据为一个消息,需要命的是当对方send一条信息的时候,无论底层怎么样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
    例如基于TCP的套接字客户端往服务器端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看来根本不知道文件的字节流从何初开始,在何处结束。

    二、黏包成因

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

    根本原因

    发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

    tcp协议的拆包机制


    当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。 MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。
    大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。



    面向流的通信特点和Nagle算法

       1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,
        因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,
        然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
    
       2.UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,
        所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,
        对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
    
       3.对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,
        即便是你输入的是空内容(直接回车),那也不是空消息,也可以被发送,udp协议会帮你封装上消息头发送过去
    
      4.udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,
        但是会丢数据,不可靠
    
      5.可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

     基于tcp协议特点的黏包现象成因

    socket数据传输过程中用户态和内核态说明:

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

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

    此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

    UDP不会发生黏包

     
    UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。 
    不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,
    在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。 对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,
    即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。 不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,
    若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。
     

    补充说明:

     
     用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,
    则函数会返回错误。(丢弃这个包,不进行发送) 用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。
    而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。
     

    三,两种情况下会发生粘包:

    情况一 发送方的缓存机制

    1,发送端需要等到本机的缓冲区满了以后才发出去,造成粘包(发送数据时间间隔很短,数据很小,python使用了优化算法,合在一起,产生粘包)

     

    发送端内核态
    如果数据包过小,不会立即发送。先缓存了一小下,通过优化算法,将2次或者多次数据包,一次发送。

    如果数据包过大,分配发送。
    如果这个时候,再来一个大的数据包,也会拆分包。那么就发生黏包了。

     server.py

    from socket import *
     
    ip_port = ('127.0.0.1', 8080)
     
    tcp_socket_server = socket(AF_INET, SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
     
    conn, addr = tcp_socket_server.accept()
     
    data1 = conn.recv(10)
    data2 = conn.recv(10)
     
    print('----->', data1.decode('utf-8'))
    print('----->', data2.decode('utf-8'))
     
    conn.close()

    client.py

    import socket
     
    BUFSIZE = 1024
    ip_port = ('127.0.0.1', 8080)
     
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    res = s.connect_ex(ip_port)
     
    s.send('hello'.encode('utf-8'))
    s.send('egg'.encode('utf-8'))

    先执行server.py,再执行client.py

    server.py输出:

    -----> helloegg
    ----->

    从代码中,可以看出。client发送了2次,第一次发送hello,第二次发送egg

    服务端接收时了2次,但是第一次接收,直接是helloegg。第二次接收内容为空。

    为什么呢?这个是因为发送端的优化机制,导致的黏包

    情况二 接收方的缓存机制

    接收端不及时接受缓冲区的包,造成多个包接受(客户端发送一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,就产生粘包)

    server.py

    from socket import *
     
    ip_port = ('127.0.0.1', 8080)
     
    tcp_socket_server = socket(AF_INET, SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
     
    conn, addr = tcp_socket_server.accept()
     
    data1 = conn.recv(2)  # 一次没有收完整
    data2 = conn.recv(10)  # 下次收的时候,会先取旧的数据,然后取新的
     
    print('----->', data1.decode('utf-8'))
    print('----->', data2.decode('utf-8'))
     
    conn.close()

    client.py

    import socket
     
    BUFSIZE = 1024
    ip_port = ('127.0.0.1', 8080)
     
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    res = s.connect_ex(ip_port)
     
    s.send('hello egg'.encode('utf-8')) 

    先执行server.py,再执行client.py

    server.py输出:

    -----> he
    -----> llo egg

    从代码上来,client发送了2次数据给server端

    server端,第一次接收2字节,第二次接收10字节。

    所以第一次返回he,第二次,接收剩余的,返回llo egg

    注意:conn永远不会接收到空数据,conn断开连接的时候recv收到一个空,那么连接就会等待

    总结

    黏包现象只发生在tcp协议中:
    
    1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
    
    2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

    四,拆包的发生情况

     

    当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送过去

    补充问题一:为何tcp是可靠传输,udp是不可靠传输

    tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的

    而udp发送数据,对端是不会返回确认信息的,因此不可靠

    补充问题二:send(字节流)和recv(1024)及sendall是什么意思?

    recv里指定的1024意思是从缓存里一次拿出1024个字节的数据

    send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。

    五,粘包问题如何解决?

      问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

    5-1  简单的解决方法(从表面解决):

      在客户端发送下边添加一个时间睡眠,就可以避免粘包现象。在服务端接收的时候也要进行时间睡眠,才能有效的避免粘包情况。

    客户端:


    #!/usr/bin/env python
    # coding:utf-8 import socket import time import subprocess din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_port=('127.0.0.1',8080) din.connect(ip_port) din.send('hello world'.encode('utf-8')) time.sleep(3) din.send('sb'.encode('utf-8'))

    服务端:


    #!/usr/bin/env python
    # coding:utf-8 import socket import time import subprocess din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_port=('127.0.0.1',8080) din.bind(ip_port) din.listen(5) conn,deer=din.accept() data1=conn.recv(1024) time.sleep(4) data2=conn.recv(1024) print(data1) print(data2)

    上面解决方法肯定会出现很多纰漏,因为你不知道什么时候传输完,时间暂停的长短都会有问题,长的话效率低,短的话不合适,所以这种方法是不合适的。

    例子2:

    客户端:

    ##客户端
    import socket
    import struct
    
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.connect(('127.0.0.1',9909))
    
    while True:
        #1、发命令
        cmd=input('>>: ').strip() #ls /etc
        if not cmd:continue
        phone.send(cmd.encode('utf-8'))
        #2、拿命令的结果,并打印
    
        #第一步:先收报头(先拿到数据的长度)
        header=phone.recv(4)  #只收4个,收报头
        #第二步:从报头中解析出对真实数据的描述信息(数据的长度)
        total_size=struct.unpack('i',header)[0] #反解,i格式,从报头从拿出对我有用的信息
        #第三步:接收真实的数据   ##循环来取
        recv_size=0  
        recv_data=b'' #拼接操作,接收的都是bytes类型
        while recv_size < total_size:  ##收完while循环才会让你结束进入下次循环输入input
            res=phone.recv(1024) #1024是一个坑
            recv_data+=res  ###每收一次就做一次拼接;
            recv_size+=len(res) ##+收的数据长度
        print(recv_data.decode('gbk'))
    
    phone.close()

    服务端:

    ##服务端
    import socket
    import subprocess
    import struct
    
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    phone.bind(('127.0.0.1',9909)) #0-65535:0-1024给操作系统使用
    phone.listen(5)
    
    print('starting...')
    while True: # 链接循环
        conn,client_addr=phone.accept()
        print(client_addr)
    
        while True: #通信循环
            try:
                #1、收命令
                cmd=conn.recv(8096)
                if not cmd:break #适用于linux操作系统
    
                #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、把命令的结果返回给客户端
                #第一步:制作固定长度的报头  真实数据的信息   #一定得是固定长度,这样接收端才知道接收多少个数据,才能提取出报头来,才能提取出有用的信息来
                total_size = len(stdout) + len(stderr) #数据的长度
                header=struct.pack('i',total_size) #制作报头 i就是4个bytes
    
                #第二步:把报头发送给客户端  ###三个send就是三个包,有可能会粘在一起;头要是固定标准固定长度,因为即使粘到一起了,我接收固定的长度,粘包也影响不到了
                conn.send(header) #header本身就是bytes类型
    
                #第三步:再发送真实的数据 #有头有数据才是互联网协议
                conn.send(stdout)  ##省去了那个+号,因为它们本身会产生粘包,优化出来了
                conn.send(stderr)
    
            except ConnectionResetError: #适用于windows操作系统
                break
        conn.close()
    
    phone.close()

     这个解决方案只包含数据的长度,报头应该是对真实数据的描述信息,报头不仅包含数据长度的信息;命令的长度可能超过这个范围,用i模式就不行了

    5-2 普通的解决方法(从根本看问题):

    问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据

    为字节流加上自定义固定长度报头,报头中包含字节流长度,然后依次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。

    struct模块

    使用struct模块可以用于将Python的值根据格式符,转换为字符串(byte类型)

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

    pack(fmt, v1, v2, ...)     按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)
    
    unpack(fmt, string)       按照给定的格式(fmt)解析字节流string,返回解析出来的tuple
    
    calcsize(fmt)                 计算给定的格式(fmt)占用多少字节的内存

    struct中支持的格式如下表:

    FormatC TypePython字节数
    x pad byte no value 1
    c char string of length 1 1
    b signed char integer 1
    B unsigned char integer 1
    ? _Bool bool 1
    h short integer 2
    H unsigned short integer 2
    i int integer 4
    I unsigned int integer or long 4
    l long integer 4
    L unsigned long long 4
    q long long long 8
    Q unsigned long long long 8
    f float float 4
    d double float 8
    s char[] string 1

     使用案例

    #!/usr/bin/env python
    # coding:utf-8
    
    import struct
    
    res = struct.pack('i', 123)
    printres, type(res), len(res))  # b'{x00x00x00' <class 'bytes'> 4 封装一个4个字节的包
    
    res1 = struct.pack('q', 11122232323)
    print(res1, type(res1), len(res1))  # b'x03xccxefx96x02x00x00x00' <class 'bytes'> 8 封装一个8个字节的包
    
    print(struct.unpack('i', res)[0])  # 拆包
    print(struct.unpack('q', res1)[0])  #
    
    # 输出
    # b'{x00x00x00' <class 'bytes'> 4
    # b'x03xccxefx96x02x00x00x00' <class 'bytes'> 8
    # (123,)
    # (11122232323,)

    普通的客户端

    在Windows下的Python3实现

    # _*_ coding: utf-8 _*_
    import socket
    import struct
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.connect(('127.0.0.1',8880)) #连接服
    while True:
        # 发收消息
        cmd = input('请你输入命令>>:').strip()
        if not cmd:continue
        phone.send(cmd.encode('utf-8')) #发送
        #先收报头
        header_struct = phone.recv(4) #收四个
        unpack_res = struct.unpack('i',header_struct)
        total_size = unpack_res[0]  #总长度
        #后收数据
        recv_size = 0
        total_data=b''
        while recv_size<total_size: #循环的收
            recv_data = phone.recv(1024) #1024只是一个最大的限制
            recv_size+=len(recv_data) #
            total_data+=recv_data #
        print('返回的消息:%s'%total_data.decode('gbk'))
    phone.close()

    普通的服务端

    #!/usr/bin/env python
    # coding:utf-8
    
    import socket
    import subprocess
    import struct
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
    phone.bind(('127.0.0.1',8880)) #绑定手机卡
    phone.listen(5) #阻塞的最大数
    print('start runing.....')
    while True: #链接循环
        coon,addr = phone.accept()# 等待接电话
        print(coon,addr)
        while True: #通信循环
            # 收发消息
            cmd = coon.recv(1024) #接收的最大数
            print('接收的是:%s'%cmd.decode('utf-8'))
            #处理过程
            res = subprocess.Popen(cmd.decode('utf-8'),shell = True,
                                              stdout=subprocess.PIPE, #标准输出
                                              stderr=subprocess.PIPE #标准错误
                                    )
            stdout = res.stdout.read()
            stderr = res.stderr.read()
            #先发报头(转成固定长度的bytes类型,那么怎么转呢?就用到了struct模块)
            #len(stdout) + len(stderr)#统计数据的长度
            header = struct.pack('i',len(stdout)+len(stderr))#制作报头
            coon.send(header)
            #再发命令的结果
            coon.send(stdout)
            coon.send(stderr)
        coon.close()
    phone.close()

    客户端执行结果:

    C:Python36python3.exe G:/PycharmProject/test/test/cli.py
    请你输入命令>>:dir
    返回的消息: Volume in drive G is ???
     Volume Serial Number is 8E43-8056
    
     Directory of G:PycharmProject	est	est
    
    2019/08/16  15:55    <DIR>          .
    2019/08/16  15:55    <DIR>          ..
    2019/08/01  16:50                 0 abc
    2019/08/13  10:49                29 b.txt
    2019/08/16  15:55               793 cli.py
    2019/07/29  13:19    <DIR>          crm
    2019/07/29  11:50                36 manage.py
    2019/07/29  13:17    <DIR>          proj
    2019/08/16  15:55             1,269 ser.py
    2019/08/16  15:49               529 test3.py
    2019/08/16  15:45               564 test4.py
    2019/07/29  11:48                36 __init__.py
    2019/08/09  15:38    <DIR>          __pycache__
                   8 File(s)          3,256 bytes
                   5 Dir(s)     505,991,168 bytes free
    
    请你输入命令>>:ipconfig
    返回的消息:
    Windows IP Configuration
    
    
    Ethernet adapter ???:
    
       Media State . . . . . . . . . . . : Media disconnected
       Connection-specific DNS Suffix  . : 
    
    Wireless LAN adapter ????* 1:
    
       Media State . . . . . . . . . . . : Media disconnected
       Connection-specific DNS Suffix  . : 
    
    Wireless LAN adapter ????* 2:
    
       Media State . . . . . . . . . . . : Media disconnected
       Connection-specific DNS Suffix  . : 
    
    Ethernet adapter ??? 3:
    
       Connection-specific DNS Suffix  . : 
       Link-local IPv6 Address . . . . . : fe80::e426:cc63:c654:f906%11
       IPv4 Address. . . . . . . . . . . : 10.0.0.30
       Subnet Mask . . . . . . . . . . . : 255.255.255.0
       Default Gateway . . . . . . . . . : 
    
    Wireless LAN adapter WLAN:
    
       Connection-specific DNS Suffix  . : 
       IPv4 Address. . . . . . . . . . . : 192.168.9.196
       Subnet Mask . . . . . . . . . . . : 255.255.255.0
       Default Gateway . . . . . . . . . : 192.168.9.1
    
    Ethernet adapter ??? 2:
    
       Media State . . . . . . . . . . . : Media disconnected
       Connection-specific DNS Suffix  . : 
    
    Ethernet adapter ??? 4:
    
       Media State . . . . . . . . . . . : Media disconnected
       Connection-specific DNS Suffix  . : 
    
    Ethernet adapter ??????:
    
       Media State . . . . . . . . . . . : Media disconnected
       Connection-specific DNS Suffix  . : 
    
    请你输入命令>>:

    服务器端执行结果:

    C:Python36python3.exe G:/PycharmProject/test/test/ser.py
    start runing.....
    <socket.socket fd=532, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8880), raddr=('127.0.0.1', 65471)> ('127.0.0.1', 65471)
    接收的是:dir
    接收的是:ipconfig

     例子2:

    服务端:

    ##服务端
    import subprocess
    import struct
    import json
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    phone.bind(('127.0.0.1',9909)) #0-65535:0-1024给操作系统使用
    phone.listen(5)
    print('starting...')
    while True: # 链接循环
        conn,client_addr=phone.accept()
        print(client_addr)
        while True: #通信循环
            try:
                #1、收命令
                cmd=conn.recv(8096)
                if not cmd:break #适用于linux操作系统
                #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、把命令的结果返回给客户端
                #第一步:制作固定长度的报头
                header_dic={
                    'filename':'a.txt',
                    'md5':'xxdxxx',
                    'total_size': len(stdout) + len(stderr)
                }
                    #字典不能直接转成bytes类型,可以转成字符串;反解出字典类型json
                header_json=json.dumps(header_dic) #json格式的字符串
                header_bytes=header_json.encode('utf-8') #bytes类型
                #第二步:先发送报头的长度
                conn.send(struct.pack('i',len(header_bytes))) #4个bytes
                #第三步:再发报头
                conn.send(header_bytes)
                #第四步:再发送真实的数据
                conn.send(stdout)
                conn.send(stderr)
            except ConnectionResetError: #适用于windows操作系统
                break
        conn.close()
    phone.close()

    客户端:

    ##客户端
    import socket
    import struct
    import json
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.connect(('127.0.0.1',9909))
    while True:
        #1、发命令
        cmd=input('>>: ').strip() #ls /etc
        if not cmd:continue
        phone.send(cmd.encode('utf-8'))
        #2、拿命令的结果,并打印
        #第一步:先收报头的长度
        obj=phone.recv(4)
        header_size=struct.unpack('i',obj)[0]
        #第二步:再收报头
        header_bytes=phone.recv(header_size)
        #第三步:从报头中解析出对真实数据的描述信息
        header_json=header_bytes.decode('utf-8')
        header_dic=json.loads(header_json)
        print(header_dic)
        total_size=header_dic['total_size']
    
        #第四步:接收真实的数据
        recv_size=0
        recv_data=b''
        while recv_size < total_size:
            res=phone.recv(1024) #1024是一个坑
            recv_data+=res
            recv_size+=len(res)
    
        print(recv_data.decode('gbk'))
    
    phone.close()

    发送时:

    发送时先发报头长度再编码报头内容然后发送最后发真实内容

    接收时

    接收时先收报头长度,用struct取出来
    根据取出的长度收取报头内容,然后解码,反序列化
    从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

    5-3 优化版的解决方法(从根本解决问题)

    优化的解决粘包问题的思路就是服务端将报头信息进行优化,对要发送的内容用字典进行描述,首先字典不能直接进行网络传输,需要进行序列化转成json格式化字符串,然后转成bytes格式服务端进行发送,因为bytes格式的json字符串长度不是固定的,所以要用struct模块将bytes格式的json字符串长度压缩成固定长度,发送给客户端,客户端进行接受,反解就会得到完整的数据包。

     在Windows下的Python3实现

    终极版的客户端

    # _*_ coding: utf-8 _*_
    import socket
    import struct
    import json
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.connect(('127.0.0.1',8080)) #连接服务器
    while True:
        # 发收消息
        cmd = input('请你输入命令>>:').strip()
        if not cmd:continue
        phone.send(cmd.encode('utf-8')) #发送
        #先收报头的长度
        header_len = struct.unpack('i',phone.recv(4))[0]  #吧bytes类型的反解
        #在收报头
        header_bytes = phone.recv(header_len) #收过来的也是bytes类型
        header_json = header_bytes.decode('utf-8')   #拿到json格式的字典
        header_dic =