• Python socket 粘包


    目录

    1 TCP的三次握手四次挥手

    1.1 三次握手

    1 客户端向服务端发起SYN请求,请求建立连接,

    2 服务端同意建立连接,回应ACK,同时服务端向客户端发起SYN请求

    3 客户端回应ACK

    1.2 四次挥手

    挥手是任意的,客户端和服务端都可以首先断开连接

    下面是以客户端发起的挥手

    1 客户端发送完数据后发起FIN 请求,之后直接断开,
    2 服务端返回一个ACK
    3此时服务端还没有接收完数据,所以没有断开连接,接收完后,服务端想要断开,发送FIN

    4 客户端返回ACK,结束

    现实中的是TIME WAIT大量存在于服务端,是服务端主动断开连接,它在等待客户端发送ACK

    总结

    1 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

    这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的.

    2 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态

    这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。

    3 TIMEWAIT的作用?

    主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。MSL在RFC 1122上建议是2分钟,而源自berkeley的TCP实现传统上使用30秒,因而,TIME_WAIT状态一般维持在1-4分钟。

    [TCP资料](http://blog.csdn.net/u011726984/article/details/50781212

    2 粘包现象

    粘包存在两个方面,一个是服务端,一个是客户端

    2.1 基于TCP制作远程执行命令操作(win服务端)

    下面是服务端放在本地win平台,用127.0.0.1自己测试的代码

    服务端

    # coding:utf-8
    # 基于TCP制作的远程执行命令的操作
    # version;版本1
    # 问题:有粘包现象
    
    
    import socket
    import subprocess
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # family=AF_INET, type=SOCK_STREAM, proto=0, _sock=None)   # 买手机
    # phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 就是它,在bind前加
    phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    
    phone.bind(('127.0.0.1', 8888))   # 绑定手机卡
    phone.listen(5)  # 开机 相当于挂起的电话连接
    
    print("starting...")
    
    while True:   # 通信循环
        conn, addr = phone.accept()  # 等待电话连接
        while True:  #
            try:  # 应对Windows系统 从缓存中读取空的情况
                data = conn.recv(1024)  # 收消息  从自己的缓存中找数据是最大的值
                if not data:
                    continue  # 应对Linux系统从缓存中读取空的情况
                res = subprocess.Popen(
                    data.decode("utf-8"), shell=True, stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE)   # 把读取的信息存放在管道中            
                '''
                # 这是判断错误的方式
                err = res.stderr.read()  # 首先判断错误信息
                if not err:   # 这是读取到的正常信息
                    res_cmd = res.stdout.read()
                else:
                    res_cmd = err      # 这是读取道的错误信息
                conn.send(res_cmd)   # 最后把接收到的数据发送到客户端
                '''
                # 这是直接发送的数据
                res_err = res.stderr.read()
                res_out = res.stdout.read()
                conn.send(res_err)
                conn.send(res_out)
            except Exception:
                break
        conn.close()   # 断开连接
    phone.close()  # 关闭服务器连接
    

    客户端

    import socket
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.connect(("127.0.0.1", 8888))  # 直接进行连接
    
    while True:
        cmd = input(">>:").strip()  # 输入去除空格 换行
        if not cmd:
            continue  # 没有输入内容 继续输入
        # phone.send(cmd.encode("utf-8"))  # 
        phone.send(bytes(cmd.encode("utf-8")))  # 默认发送的是字节bytes
        data = phone.recv(1024)    # 从客户端自己的缓存中寻找
        print(data.decode('gbk'))  # win 平台的默认编码是gbk 在Linux中用uft-8
    phone.close()  # 关闭客户端
    

    实验的时候首先开启服务端,然后开启客户端

    执行结果:

    >>:cd 
    D:Python.......
    
    >>:listen
    'listen' 不是内部或外部命令,也不是可运行的程序
    或批处理文件。
    
    >>:ls 
    'ls' 不是内部或外部命令,也不是可运行的程序
    或批处理文件。
    
    >>:tree
    卷 我的小D 的文件夹 PATH 列表
    卷序列号为 0005-B7F0
    D:.
    没有子文件夹 
    >>:hello
    'hello' 不是内部或外部命令,也不是可运行的程序
    或批处理文件。
    >>:
    

    2.1 基于TCP制作远程执行命令操作(Linux服务端)

    在Linux上需要修改IP地址位服务端的地址,客户端的解码方式修改成utf-8

    服务端

    # coding:utf-8
    # 基于TCP制作的远程执行命令的操作
    # version;版本1
    # 问题:有粘包现象  具体看执行的结果
    
    import socket
    import subprocess
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # family=AF_INET, type=SOCK_STREAM, proto=0, _sock=None)   # 买手机
    # phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 就是它,在bind前加
    phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    
    phone.bind(('192.168.16.200', 8800))   #  修改成Linux自己的IP地址
    phone.listen(5)  # 开机 相当于挂起的电话连接
    
    print("starting...")
    
    while True:   # 通信循环
        conn, addr = phone.accept()  # 等待电话连接
        while True:  #
            try:  # 应对Windows系统 从缓存中读取空的情况
                data = conn.recv(1024)  # 收消息  从自己的缓存中找数据是最大的值
                if not data:
                    continue  # 应对Linux系统从缓存中读取空的情况
                res = subprocess.Popen(
                    data.decode("utf-8"), shell=True, stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE)   # 把读取的信息存放在管道中            
                '''
                # 这是判断错误的方式
                err = res.stderr.read()  # 首先判断错误信息
                if not err:   # 这是读取到的正常信息
                    res_cmd = res.stdout.read()
                else:
                    res_cmd = err      # 这是读取道的错误信息
                conn.send(res_cmd)   # 最后把接收到的数据发送到客户端
                '''
                # 这是直接发送的数据
                res_err = res.stderr.read()
                res_out = res.stdout.read()
                conn.send(res_err)
                conn.send(res_out)
            except Exception:
                break
        conn.close()   # 断开连接
    phone.close()  # 关闭服务器连接
    

    客户端

    # coding:utf-8
    
    import socket
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # phone.connect(("127.0.0.1", 8888))  # 直接进行连接
    phone.connect(("192.168.16.200", 8800))  # Linux测试
    
    while True:
        cmd = input(">>:").strip()  # 输入去除空格 换行
        if not cmd:
            continue  # 没有输入内容 继续输入
        # phone.send(cmd.encode("utf-8"))  #
        phone.send(bytes(cmd.encode("utf-8")))  # 默认发送的是字节bytes
        data = phone.recv(1024)    # 从客户端自己的缓存中寻找
        # print(data.decode('gbk'))  # win 平台的默认编码是gbk 在Linux中用uft-8
        print(data.decode('utf-8'))  # win 平台的默认编码是gbk 在Linux中用uft-8
    phone.close()  # 关闭客户端
    

    执行结果

    >>:pwd 
    /
    
    >>:cat /etc/passwd
    root:x:0:0:root:/root:/bin/bash
    bin:x:1:1:bin:/bin:/sbin/nologin
    daemon:x:2:2:daemon:/sbin:/sbin/nologin
    adm:x:3:4:adm:/var/adm:/sbin/nologin
    lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
    sync:x:5:0:sync:/sbin:/bin/sync
    shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
    halt:x:7:0:halt:/sbin:/sbin/halt
    mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
    operator:x:11:0:operator:/root:/sbin/nologin
    games:x:12:100:games:/usr/games:/sbin/nologin
    ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
    nobody:x:99:99:Nobody:/:/sbin/nologin
    systemd-bus-proxy:x:999:998:systemd Bus Proxy:/:/sbin/nologin
    systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
    dbus:x:81:81:System message bus:/:/sbin/nologin
    polkitd:x:998:997:User for polkitd:/:/sbin/nologin
    tss:x:59:59:Account used by the trousers package to sandbox the tcsd daemon:/dev/null:/sbin/nologin
    sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
    postfix:x:89:89::/var/spool/postfix:/sbin/nologin
    hzx:x:1000:1000::/home/hzx:/bin/bash
    help:x:1001:1001::/home/hel
    >>:ls                # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!看下面的就是粘包
    p:/bin/bash
    egon:x:1002:1002::/home/egon:/bin/bash
    tom:x:1004:1004::/home/tom:/bin/bash
    jake:x:1005:1006::/home/jake:/bin/bash
    rose:x:1006:1007::/home/rose:/bin/bash
    nginx:x:997:995:Nginx web server:/var/lib/nginx:/sbin/nologin
    rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
    rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
    nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin
    
    >>:ls   # 现在才是正常执行的结果
    bin
    boot
    dev
    etc
    FtpServer
    hello.py
    home
    lib
    lib64
    media
    mnt
    opt
    proc
    Python-3.6.0
    Python-3.6.0.tgz
    root
    run
    sbin
    sdb1
    sdb2
    sdb3
    server.py
    share
    srv
    sys
    tail_new.py
    tail.py
    tartmp
    test
    test3
    tmp
    tmux
    tmux-2.3
    tmux-2.3.tar.gz
    usr
    var
    >>:
    

    2.2 粘包产生的原因:

    上面出现了粘包的问题,粘包产生的主要原因是TCP协议是面向流(socket.SOCK_STREAM)),意思就是说发送端可以一个字节一个字节的发送, 软件的速度是大于网络的速度(网络有延迟),TCP把数据量小,时间间隔小的进行封包,一块发送,这就无法区分包的边界

    接收端从自己的缓存中是有多少取多少,发送过来的是字节是没有边界的,所以收的时候是通过修改recv(1024)中的参数来增加一次性接收的值,但是这个如果是一个大文件的时候是有问题的,会全部去取到内存中,内存会爆。

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

    3 解决粘包

    类似以太网协议,在数据的前面加上固定长度的报头,报头中包含字节流的长度,然后一次send到对端。

    对端在接收时,先从自己的缓存中取出定长的报头,然后再获取真实的数据

     1 解决粘包的最终版Linux服务端
     2 
     3 
     4 import socket
     5 import subprocess
     6 import json
     7 import struct
     8 
     9 phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    10 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    11 # phone.bind(('127.0.0.1', 8120))
    12 phone.bind(('192.168.16.200', 8220))
    13 
    14 phone.listen(5)
    15 
    16 print("sever is starting...")
    17 while True:  # 通信循环
    18     conn, addr = phone.accept()
    19     print(conn)
    20     print(addr)
    21     while True:
    22         try:
    23             data = conn.recv(1024)  # conn.recv
    24             if not data:
    25                 continue
    26             res = subprocess.Popen(data.decode("utf-8"),
    27                                    shell=True,
    28                                    stdout=subprocess.PIPE,
    29                                    stderr=subprocess.PIPE)
    30 
    31             res_err = res.stderr.read()
    32             res_out = res.stdout.read()
    33 
    34             # 数据长度
    35             data_size = len(res_err) + len(res_out)
    36 
    37             # 制作报头
    38             head_dic = {"data_size": data_size}  # 把报头制作成字典格式
    39             head_json = json.dumps(head_dic)  # json 序列化
    40             head_bytes = head_json.encode("utf-8")  # 要发送需要转换成字节数据
    41 
    42             # 1 发送报头的长度
    43             head_len = len(head_bytes)  # 这是报头的长度
    44             conn.send(struct.pack("i", head_len))
    45 
    46             # 2 发送报头
    47             conn.send(head_bytes)
    48 
    49             # 3 发送真实的数据
    50             conn.send(res_err)
    51             conn.send(res_out)
    52 
    53         except Exception:
    54             break
    55     conn.close()
    56 phone.close()


    客户端

    import socket
    import json
    import struct
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # phone.connect(("192.168.16.200", 8120))
    phone.connect(("192.168.43.20", 8220))
    
    while True:
    cmd = input(">>:").strip()
    if not cmd:
    continue
    # phone.send(cmd.encode("utf-8"))
    phone.send(bytes(cmd,encoding="utf-8"))
    
    # 1 接收报头长度
    head_struct = phone.recv(4) # 先接收报头
    head_len = struct.unpack("i", head_struct)[0]
    
    # 2 接收报头
    
    head_bytes = phone.recv(head_len)
    head_json = head_bytes.decode("utf-8")
    head_dic = json.loads(head_json)
    print(head_dic)
    data_size = head_dic["data_size"]
    
    # 接收数据
    recv_size = 0
    recv_data = b''
    while recv_size < data_size:
    data = phone.recv(1024)
    recv_size += len(data)
    recv_data += data
    print(recv_data.decode("utf-8"))
    
    phone.close()

    解决粘包的最终版Linux服务端
    ```python
    #
    import socket
    import subprocess
    import json
    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', 8120))
    phone.bind(('192.168.16.200', 8220))

    phone.listen(5)

    print("sever is starting...")
    while True: # 通信循环
    conn, addr = phone.accept()
    print(conn)
    print(addr)
    while True:
    try:
    data = conn.recv(1024) # conn.recv
    if not data:
    continue
    res = subprocess.Popen(data.decode("utf-8"),
    shell=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE)

    res_err = res.stderr.read()
    res_out = res.stdout.read()

    # 数据长度
    data_size = len(res_err) + len(res_out)

    # 制作报头
    head_dic = {"data_size": data_size} # 把报头制作成字典格式
    head_json = json.dumps(head_dic) # json 序列化
    head_bytes = head_json.encode("utf-8") # 要发送需要转换成字节数据

    # 1 发送报头的长度
    head_len = len(head_bytes) # 这是报头的长度
    conn.send(struct.pack("i", head_len))

    # 2 发送报头
    conn.send(head_bytes)

    # 3 发送真实的数据
    conn.send(res_err)
    conn.send(res_out)

    except Exception:
    break
    conn.close()
    phone.close()

    ```


    客户端
    ```python
    #
    import socket
    import json
    import struct

    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # phone.connect(("192.168.16.200", 8120))
    phone.connect(("192.168.43.20", 8220))

    while True:
    cmd = input(">>:").strip()
    if not cmd:
    continue
    # phone.send(cmd.encode("utf-8"))
    phone.send(bytes(cmd,encoding="utf-8"))

    # 1 接收报头长度
    head_struct = phone.recv(4) # 先接收报头
    head_len = struct.unpack("i", head_struct)[0]

    # 2 接收报头

    head_bytes = phone.recv(head_len)
    head_json = head_bytes.decode("utf-8")
    head_dic = json.loads(head_json)
    print(head_dic)
    data_size = head_dic["data_size"]

    # 接收数据
    recv_size = 0
    recv_data = b''
    while recv_size < data_size:
    data = phone.recv(1024)
    recv_size += len(data)
    recv_data += data
    print(recv_data.decode("utf-8"))

    phone.close()

    ```

  • 相关阅读:
    js之iframe子页面与父页面通信
    js的event对象
    整洁代码的4个条件
    PYTHON 自然语言处理
    如何检测浏览器是否支持CSS3
    BootStrap前端框架使用方法详解
    如何使用repr调试python程序
    Python编程快速上手——Excel到CSV的转换程序案例分析
    C++和JAVA传统中积极的一面
    20个LINUX相关的网站
  • 原文地址:https://www.cnblogs.com/Python666/p/6811146.html
Copyright © 2020-2023  润新知