day4 课堂笔记 网络编程

今日内容大纲

  1. 粘包现象
  2. socket缓冲区
  3. recv的工作原理
  4. 产生粘包的情况讨论
  5. 粘包的解决方案

1. 粘包现象

  • 测试

    s1 = '中国万岁'
    print(len(s1)) # 获取该字符串字符总字数
    
    b1 = s1.encode('utf-8')
    print(len(b1)) # 获取该bytes数据的字节的总个数
    
  • 执行远程命令

    import socket
    import subprocess
    # server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 基于网络的TCP协议
    server = socket.socket()  # 创建socket对象
    
    server.bind(('127.0.0.1',8848))
    # 127.0.0.1 本地回环地址,只允许本机自己连接。
    
    # 3. 待机等待其他人连接
    server.listen(5)
    while 1:
        conn, addr = server.accept()  # 阻塞,等待客户端链接
        while 1:
            try:
            # 4. 接电话收发消息
                from_client_cmd = conn.recv(1024)
                if from_client_cmd.upper() == b'Q':
                    break
                obj = subprocess.Popen(from_client_cmd.decode('utf-8'),
                                       shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE,
    
                                       )
                result = obj.stdout.read() + obj.stderr.read()
                print(f'总字节个数{len(result)}')
                conn.send(result)
    
            except ConnectionResetError:
                break
    
            # 关闭管道
        conn.close()
    server.close()
    
    
    
    import socket
    
    client = socket.socket()  # 创建客户端对象
    
    client.connect(('127.0.0.1', 8848))  # 链接服务端
    while 1:
        to_server_data = input('请输入命令:>>>>')
        client.send(to_server_data.encode('utf-8'))
        if to_server_data.upper() =+--= 'Q':
            break
        from_server_data = client.recv(2048)  # 阻塞
        print(f'获取的总字节个数{len(from_server_data)}')
        print(f"结果:{from_server_data.decode('gbk')}")
    
    client.close()
    
    

    粘包现象(个人理解):recv(n)当客户端输入指令时,服务端会返回数据,当这个数据大于n时,客户端只接收自己能接收的最大数据量,剩余的会储存在socket缓冲区,当执行想一次命令时,才会将之前未输出数据再一次输出,每次大小为n,不足n时全部取出,而之后的其他指令对应的数据就会在socket缓冲池中排序,当某条指令的对应的数据第二次仍旧没有取完时,剩下的数据则会与下一次命令对应的数据进行拼接这种现象被称为,粘包现象(就是数据粘在一起的意思)(n的单位为字节)

2. socket缓冲区

每个socket被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

​ write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送至目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管他们有没有达到目标机器,也不管他们何时被发送到网络,这些都是TCP协议负责的事。

​ TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

​ read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取

​ 输入输出缓冲区的默认大小一般都是 8K(可修改)

  • 为什么设置缓冲区?

    • 可以让数据暂存,防止数据丢失。

      当数据大于客户端可接收最大数据量时,会暂存在缓冲区,如果没有的话,由于其他地方无法存入这些数据那么,其余的数据将会丢失。

    • 收发数据流畅不间断、稳定,提升收发数据的效率 。

      因为所有的数据在传输中都有可能被硬件影响导致效率降低,现在两端获取数据都是从缓冲池中获取,避免了这个问题。

3. recv工作原理

  • 源码分析

    源码解释:
    Receive up to buffersize bytes from the socket.
    接收来自socket缓冲区的字节数据,
    For the optional flags argument, see the Unix manual.
    对于这些设置的参数,可以查看Unix手册。
    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.
    关闭远程端并读取所有数据后,返回空字符串。
    
  • 代码测试

    1. 测试 recv会一直从缓冲区取值,直到取完为止
    #服务端
    import socket
    phone =socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.bind(('127.0.0.1',8080))
    phone.listen(5)
    conn, client_addr = phone.accept()
    from_client_data1 = conn.recv(2)
    print(from_client_data1)
    from_client_data2 = conn.recv(2)
    print(from_client_data2)
    from_client_data3 = conn.recv(1)
    print(from_client_data3)
    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'))
    phone.close()
    
    
    1. 验证服务端缓冲区取完了,又执行recv,此时客户端20秒内不关闭的前提下,recv处于阻塞状态。
    # 服务端
    import socket
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.bind(('127.0.0.1',8080))
    phone.listen(5)
    conn, client_addr = phone.accept()
    from_client_data = conn.recv(1024)
    print(from_client_data)
    print(111)
    conn.recv(1024) 
    print(222)
    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) # 此时程序阻塞20秒左右,因为缓冲区数据取完了,并且20秒内,客户端没有关闭。
    phone.close()
    
    1. 验证服务端缓冲区取完了,又执行了recv,此时客户端处于关闭状态,则recv会取到空字符串。
    # 客户端
    import socket
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.bind(('127.0.0.1', 8080))
    phone.listen(5)
    conn, client_addr = phone.accept()
    from_client_data1 = conn.recv(1024)
    print(from_client_data1)
    from_client_data2 = conn.recv(1024)
    print(from_client_data2)
    from_client_data3 = conn.recv(1024)
    print(from_client_data3)
    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'))
    phone.close()
    

4. 产生粘包的情况讨论

  • 如果给客户端一定时间,输入缓冲区未取完的数据会与新到的数据粘在一起(上述已测试)

  • 连续短暂的发送少量数据, 接收时数据可能会粘在一起(没有明文解释原理,依系统而定)

    # 服务端
    import socket
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.bind(('127.0.0.1', 8080))
    phone.listen(5)
    conn, client_addr = phone.accept()
    from_client_data = conn.recv(1024)
    print(from_client_data)
    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('he'.encode('utf-8'))
    time.sleep(1)
    phone.send('ll'.encode('utf-8'))
    phone.send('o'.encode('utf-8'))
    phone.close
    
  • 如何解决粘包
    • 连续send这种粘包怎么解决?

      代码忽略

      这种粘包只能通过代码避免, 不能根本解决。

      就是不要连续的send数据。

    • 执行远程命令程序,如何每次将产生的结果全部取出?

      import socket
      import time
      client = socket.socket()
      client.connect(('127.0.0.1', 8080))
      while 1:
      	to_server_data = input('请输入命令>>')
      	client.send(to_server_data.encode('utf-8'))
      	if to_server_data.upper() == 'Q':
      		break
      	time.sleep(2)
      	from_server_data = client.recv(10000000)# 设置上限不是最终解决方式。
      	print(f"获取的总字节个数{len(from_server_data)}")
      	print(f"结果:{from_server_data.decode('gbk')}")
      client.close()
      

5. 粘包的解决方案

  • low版

    • 思路

      • socket收发数据并不是send与recv一一对应的,客户端发送一次命令给服务端,服务端通过命令产生结果发送回客户端,客户端循环接收完毕所有的数据之后,再次发送命令。

      • 那么我们的循环终止条件是什么?

        假设:

        服务端产生的结果客户端每次接受的数据循环次数
        1025recv(1024)2
        8000recv(1024)8
        10000recv(1024)10

        客户端循环接收数据,想要让客户端循环终止,我们应该需要让客户端知道每次产生的总数据量,那么怎么让客户端知道总数据量呢?

      • 服务端通过len一下,获取总的数据字节量,然后发给客户端;然后服务端再把所有的总数据发送给客户端

        如:len(‘fhiabnkghoihgoahjgiher’) --------> 数据类型一定是int类型(整型)

        服务端产生的总数据量int类型转换成bytes类型客户端接收结果
        2584str(2584).encode(‘utf-8’)recv(4)
        10000str(10000).encode(‘utf-8’)recv(5)
        11456str(11456).encode(‘utf-8’)recv(6)

        我们现在需要将不固定的int数据转化成固定长度的bytes类型。

        struct模块可以解决我们的问题。

      • struct模块

        import struct
        # 讲一个数字转化成等长度的bytes类型。
        ret = struct.pack('i', 123456)
        print(ret, type(ret), len(ret))
        
        # 通过unpack反解回来
        ret1 = struct.unpack('i', ret)[0]
        print(ret1, type(ret1))
        
      • 代码展示

        # 服务端
        import socket
        import subprocess
        import struct
        server = socket.socket()
        server.bind(('127.0.0.1', 8080))
        server.listen(5)
        while 1:
            conn, addr = server.accept()
            while 1:
                try:
                    from_client_cmd = conn.recv(1024)
                    if from_client_cmd.upper() == b'Q':
                        break
                    obj = subprocess.Popen(from_client_cmd.encode('utf-8'),
                                          shell = True,
                                          stdout = subprocess.PIPE,
                                          stderr = subprocess.PIPE)
                    result = obj.stdout.read() + obj.stderr.read()
                    print(f'总字节个数{len(result)}')
                    head_bytes = struct.pack('i', len(result)) # 固定4个字节
                    conn.send(head_bytes)
                    conn.send(result)
        
                except ConnectionResetError:
                    break
             # 关闭管道
            conn.close()
        server.close()
        
        
        
        
        # 客户端
        import socket
        import struct
        client = socket.socket()
        client.connect(('127.0.0.1', 8080))
        while 1:
            tto_server_data = input('请输入命令:>>>>')
            client.send(to_server_data.encode('utf-8'))
            if to_server_data.upper() == 'Q':
                break
            # from_server_data = client.recv(1024)  # 阻塞
        
            # 获取固定的头部
            head_bytes = client.recv(4)
            total_len = struct.unpack('i',head_bytes)[0]
        
            total = b''
            while len(total) < total_len:
                total += client.recv(1024)
        
        
            print(f'获取的总字节个数{total_len}')
            print(f"结果:{total.decode('gbk')}")
        
        client.close()
        

        获取固定的头部

        head_bytes = client.recv(4)
        total_len = struct.unpack('i',head_bytes)[0]
        
        total = b''
        while len(total) < total_len:
            total += client.recv(1024)
        
        
        print(f'获取的总字节个数{total_len}')
        print(f"结果:{total.decode('gbk')}")
        

      client.close()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值