• 网络编程(三)——通信循环、链接循环、粘包问题


    通信循环、链接循环、粘包问题

    一、通信循环

    服务端和客户端可以进行连续的信息交流

    from socket import *
    
    ser_socket = socket(AF_INET, SOCK_STREAM)
    
    ser_socket.bind(('127.0.0.1', 8886))
    
    ser_socket.listen(5)
    
    conn, addr = ser_socket.accept()
    
    while True:
        try:               # 抛出异常,若不抛出处理,一旦客户端强行退出,服务端就会报错
            data = conn.recv(1024)
            print(data.decode('utf-8'))
    
            conn.send(data.upper())
        except ConnectionResetError:
            break
    
    conn.close()
    
    ser_socket.close()
    通信循环服务端
    from socket import *
    
    cli_socket = socket(AF_INET, SOCK_STREAM)
    
    cli_socket.connect(('127.0.0.1', 8886))
    
    #通信循环,可以多次输入
    while True:
        msg = input('>>>>:').strip()
        if len(msg) == 0:            # 如果输入为空,给服务端发送信息之后,服务端什么都没接受,一直处于阻塞状态
            continue
        cli_socket.send(msg.encode('utf-8'))
    
        data = cli_socket.recv(1024)
        print(data.decode('utf-8'))
    
    cli_socket.close()
    通信循环客户端

    tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制

    二、链接循环

    可以启动多个客户端,但是只有一个客户端是处于连接状态,其余部分在半连接池等待连接,等待的数量不能超过半连接池的最大监听数量

    from socket import *
    
    ser_socket = socket(AF_INET, SOCK_STREAM)
    
    ser_socket.bind(('127.0.0.1', 8886))
    
    ser_socket.listen(5)
    
    
    #链接循环,可以同时启动最多6个客户端,但是只有一个处于连接状态,其余最多5个在半连接池等待。只有当连接状态的客户端断开连接,下一个客户端才进入连接
    while True:
        conn, addr = ser_socket.accept()
    
        # 通信循环
        while True:
            try:
                data = conn.recv(1024)
                print(data.decode('utf-8'))
    
                conn.send(data.upper())
            except ConnectionResetError:
                break
    
        conn.close()
    
    ser_socket.close()
    链接循环服务端
    from socket import *
    
    cli_socket = socket(AF_INET, SOCK_STREAM)
    
    cli_socket.connect(('127.0.0.1', 8886))
    
    while True:
        msg = input('>>>>:').strip()
        if len(msg) == 0:
            continue
        cli_socket.send(msg.encode('utf-8'))
    
        data = cli_socket.recv(1024)
        print(data.decode('utf-8'))
    
    cli_socket.close()
    链接循环客户端

    三、粘包问题

    1、模拟ssh远程执行命令

    from socket import socket, AF_INET, SOCK_STREAM
    import subprocess
    
    ser_socket = socket(AF_INET, SOCK_STREAM)
    
    ser_socket.bind(('127.0.0.1', 8882))
    
    ser_socket.listen(5)
    while True:
        conn, addr = ser_socket.accept()
        while True:
            try:
                data = conn.recv(1024)
                obj = subprocess.Popen(data.decode('utf-8'), 
                                       shell=True, 
                                       stdout=subprocess.PIPE, 
                                       stderr=subprocess.PIPE)
                stdout = obj.stdout.read()
                stderr = obj.stderr.read()
    
                conn.send(stdout + stderr)
            except ConnectionResetError:
                break
        conn.close()
    
    ser_socket.close()
    服务端
    from socket import socket, AF_INET, SOCK_STREAM
    
    cli_socket = socket(AF_INET, SOCK_STREAM)
    
    cli_socket.connect(('127.0.0.1', 8882))
    
    while True:
        msg = input('>>>').strip()
        if len(msg) == 0:
            continue
        cli_socket.send(msg.encode('utf-8'))
        data = cli_socket.recv(1024)
        print(data.decode('gbk'))      #  Windows系统,默认编码gbk,所以用gbk解码
    
    cli_socket.close()
    客户端

    2、产生粘包原因

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

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

    在上面的例子中,如果执行命令tasklist,那么就会存在粘包问题。由于TCP协议是流式协议,所以数据都以数据流的形式传输。假如数据大小是123456,可是已经设定了接收的大小 是1024,所以只接受了数据中的一小部分,但是,剩余部分数据并不会消失,会一直存在于操作系统中,所以下一次接收数据的时候是优先从剩余数据中接收。这样所有数据就乱套了,这就是粘包问题。

    3、发生粘包的两种情况

    (1)发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

    from socket import *
    
    ser_socket = socket(AF_INET, SOCK_STREAM)
    
    ser_socket.bind(('127.0.0.1', 8886))
    
    ser_socket.listen(5)
    
    conn, addr = ser_socket.accept()
    
    data = conn.recv(1024)
    print('第一次接收:', data.decode('utf-8'))
    data1 = conn.recv(5)
    print('第二次接收:', data1.decode('utf-8'))
    data2 = conn.recv(1024)
    print('第三次接收:', data2.decode('utf-8'))
    
    conn.send(data.upper())
    
    conn.close()
    
    ser_socket.close()
    服务端
    from socket import *
    
    cli_socket = socket(AF_INET, SOCK_STREAM)
    
    cli_socket.connect(('127.0.0.1', 8886))
    
    cli_socket.send('hello'.encode('utf-8'))
    cli_socket.send('world'.encode('utf-8'))
    cli_socket.send('object'.encode('utf-8'))
    
    # data = cli_socket.recv(1024)
    # print(data.decode('utf-8'))
    
    cli_socket.close()
    客户端

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

      例如:模拟ssh远程执行命令,若执行tasklist命令,在客户端,无法几次性全部接受执行结果,所以剩余结果会在下一次执行命令式优先接收

    from socket import socket, AF_INET, SOCK_STREAM
    import subprocess
    
    ser_socket = socket(AF_INET, SOCK_STREAM)
    
    ser_socket.bind(('127.0.0.1', 8882))
    
    ser_socket.listen(5)
    while True:
        conn, addr = ser_socket.accept()
        while True:
            try:
                data = conn.recv(1024)
                obj = subprocess.Popen(data.decode('utf-8'),
                                       shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
                stdout = obj.stdout.read()
                stderr = obj.stderr.read()
    
                conn.send(stdout + stderr)
            except ConnectionResetError:
                break
        conn.close()
    
    ser_socket.close()
    服务端
    from socket import socket, AF_INET, SOCK_STREAM
    
    cli_socket = socket(AF_INET, SOCK_STREAM)
    
    cli_socket.connect(('127.0.0.1', 8882))
    
    while True:
        msg = input('>>>').strip()
        if len(msg) == 0:
            continue
        cli_socket.send(msg.encode('utf-8'))
        data = cli_socket.recv(1024)
        print(data.decode('gbk'))
    
    cli_socket.close()
    客户端

    4、解决粘包问题的方法

    问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。

     补充:struct模块

     可以把一个类型,如数字,转成固定长度的bytes字节类型数据

    import struct
    
    # 打包,将数据转成固定字节长度的数据,使在接收端可以知道报头长度,接收报头
    res = struct.pack('i', 12344566)
    print(res, len(res))              # b'xf6\xbcx00'   4
    
    res1 = struct.pack('i', 888888)
    print(res1, len(res1))            # b'8x90
    x00'    4
    
    
    # 解包,将数据从固定字节的数据中解出来,获取原数据(元组格式,元组的第一个值)
    res2=struct.unpack('i',res)
    print(res2)             # (12344566,)

    (1)简单版本

    # 服务端必须满足至少三点:
    # 1. 绑定一个固定的ip和port
    # 2. 一直对外提供服务,稳定运行
    # 3. 能够支持并发
    from socket import *
    import subprocess
    import struct
    
    server = socket(AF_INET, SOCK_STREAM)
    server.bind(('127.0.0.1', 8081))
    server.listen(5)
    
    # 链接循环
    while True:
        conn, client_addr = server.accept()
        print(client_addr)
    
        # 通信循环
        while True:
            try:
                cmd = conn.recv(1024) #cmd=b'dir'
                # if len(cmd) == 0: break  # 针对linux系统
                obj=subprocess.Popen(cmd.decode('utf-8'),
                                 shell=True,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE
                                 )
                stdout=obj.stdout.read()
                stderr=obj.stderr.read()
                # 1. 先制作固定长度的报头
                header=struct.pack('i',len(stdout) + len(stderr))
                # 2. 再发送报头
                conn.send(header)
                # 3. 最后发送真实的数据
                conn.send(stdout)
                conn.send(stderr)
            except ConnectionResetError:
                break
    
        conn.close()
    
    server.close()
    服务端
    from socket import *
    import struct
    
    client = socket(AF_INET, SOCK_STREAM)
    client.connect(('127.0.0.1', 8081))
    
    # 通信循环
    while True:
        cmd=input('>>: ').strip()
        if len(cmd) == 0:continue
        client.send(cmd.encode('utf-8'))
        #1. 先收报头,从报头里解出数据的长度
        header=client.recv(4)
        total_size=struct.unpack('i',header)[0]
        #2. 接收真正的数据
        cmd_res=b''
        recv_size=0
        while recv_size < total_size:
            data=client.recv(1024)
            recv_size+=len(data)
            cmd_res+=data
    
        print(cmd_res.decode('gbk'))
    
    client.close()
    客户端

    (2)终极版本

    由于简单版本中,struct模块转换的原数据的大小有限制,报头只含有数据长度,所以用字典来表示报头。

    json(json格式的字符串):数据以什么格式发送,接收到的还是原来的格式的数据

    struct:把json格式的数据转换成固定长度的字符串(bytes)数据,使报头和真正数据不粘在一起,在接收端可以接收报头

    # 服务端必须满足至少三点:
    # 1. 绑定一个固定的ip和port
    # 2. 一直对外提供服务,稳定运行
    # 3. 能够支持并发
    from socket import *
    import subprocess
    import struct
    import json
    
    server = socket(AF_INET, SOCK_STREAM)
    server.bind(('127.0.0.1', 8081))
    server.listen(5)
    
    # 链接循环
    while True:
        conn, client_addr = server.accept()
        print(client_addr)
    
        # 通信循环
        while True:
            try:
                cmd = conn.recv(1024)  # cmd=b'dir'
                if len(cmd) == 0: break  # 针对linux系统
                obj = subprocess.Popen(cmd.decode('utf-8'),
                                       shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE
                                       )
                stdout = obj.stdout.read()
                stderr = obj.stderr.read()
                # 1. 先制作报头
                header_dic = {
                    'filename': 'a.txt',
                    'md5': 'asdfasdf123123x1',
                    'total_size': len(stdout) + len(stderr)
                }
                header_json = json.dumps(header_dic)
                header_bytes = header_json.encode('utf-8')
    
                # 2. 先发送4个bytes(包含报头的长度)
                conn.send(struct.pack('i', len(header_bytes)))
                # 3  再发送报头
                conn.send(header_bytes)
    
                # 4. 最后发送真实的数据
                conn.send(stdout)
                conn.send(stderr)
            except ConnectionResetError:
                break
    
        conn.close()
    
    server.close()
    服务端
    from socket import *
    import struct
    import json
    
    client = socket(AF_INET, SOCK_STREAM)
    client.connect(('127.0.0.1', 8081))
    
    # 通信循环
    while True:
        cmd=input('>>: ').strip()
        if len(cmd) == 0:continue
        client.send(cmd.encode('utf-8'))
        #1. 先收4bytes,解出报头的长度
        header_size=struct.unpack('i',client.recv(4))[0]
    
        #2. 再接收报头,拿到header_dic
        header_bytes=client.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']
    
        #3. 接收真正的数据
        cmd_res=b''
        recv_size=0
        while recv_size < total_size:
            data=client.recv(1024)
            recv_size+=len(data)
            cmd_res+=data
    
        print(cmd_res.decode('gbk'))
    
    client.close()
    客户端
  • 相关阅读:
    工资是用来支付给责任的,责任越大,工资越高。 涨工资,是因为承担了更大的责任。
    水平分库分表的关键问题及解决思路
    APP多版本共存,服务端如何兼容?
    ListView动态加载数据分页(使用Handler+线程和AsyncTask两种方法)
    Java 并发专题 :闭锁 CountDownLatch 之一家人一起吃个饭
    Java进阶 创建和销毁对象
    Java OCR tesseract 图像智能字符识别技术
    网页信息抓取进阶 支持Js生成数据 Jsoup的不足之处
    从原理角度解析Android (Java) http 文件上传
    android pop3与imap方式接收邮件(javamail)
  • 原文地址:https://www.cnblogs.com/linagcheng/p/9579685.html
Copyright © 2020-2023  润新知