zoukankan      html  css  js  c++  java
  • ~~网络编程(五):粘包现象~~

    进击のpython

    *****

    网络编程——粘包现象


    前面我们提到了套接字的使用方法,以及相关bug的排除

    还记得我们提到过一个1024吗?

    我们现在要针对这个来研究一下一个陷阱

    在研究这个陷阱之前我要先教你几条语句

    这是windows的命令啊

    ipfonfig 查看本地网卡的ip地址

    dir 查看某一个文件夹下的子文件名和子文件夹名

    tasklist 查看运行的进程

    那我这三条命令怎么执行呢??直接敲??

    好像没什么用,所以说我需要打开我的cmd窗口来键入这些命令

    而cmd也就是一个能把特殊的字母组合执行出来的一个程序而已

    当我在cmd里键入dir的时候得到的就是这些东西

    那我想在编译器里搞这个东西呢?

    哦!第一反应就是os模块

    import os
    os.system("dir")
    

    就执行起来了吧


    那我这算是拿到结果了吗?

    我觉得不算,为什么?

    咱们想要达到的效果是我在客户端输入一个dir发送给服务端,服务端给我返回这一堆东西才叫拿到结果了是吧

    import os
    
    res = os.system("dir")
    print(f"返回的结果是:{res}")
    

    那结果我打印的是什么呢??是0!那为什么是这个呢?

    这个0是代表这个命令是不是成功

    如果返回的是0,就是成功了,如果是非零,就是失败了!

    所以说他返回的是一个是否成功执行语句的状态,而不是执行语句的返回结果

    那os模块就被pass掉了,因为他无法返回我们需要的东西

    那除了os.还有什么吗?subprocess

    他下面有一个方法

    import subprocess
    subprocess.Popen()
    

    里面接收两个参数,第一个参数是字符串的命令

    第二个是shell=True,作用是在终端也就是cmd下运行

    那我这么写就没问题了

    import subprocess
    
    subprocess.Popen("dir", shell=True)
    

    但是我不要把这个结果给终端,我要把这个结果给客户端

    那我是不是就要把结果传进管道然后进行传输呢?

    好,那这个方法就可以传递第三个属性stdout = subprocess.PIPE

    这个管道是用来接收正确的结果的

    那错误的结果传在哪呢?第四个属性! stderr = subprocess.PIPE

    好,当我把所有的参数都填进去之后我们再看,是不是在控制台就没有输出结果了啊

    输出结果去哪了呢?放到管道里去了!

    import subprocess
    
    obj = subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    

    那我想把管道里的东西取出来怎么弄呢?

    我们先来看看我打印obj是个啥?很明显是个对象是吧

    那既然是对象就能调用方法

    print(obj.stdout)
    
    <_io.BufferedReader name=3>
    

    看到IO第一反应就是文件,用read()方法读一下

    你发现你打印的时候什么???这不就是我们想要的字节类型的数据嘛

    然后我们再来看

    import subprocess
    
    obj = subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    print(obj.stdout.read().decode("gbk"))
    print(obj.stdout.read().decode("gbk"))
    

    打印了几次?只有一次!为什么?

    因为我把数据放到管道之后,第一次打印就把结果拿出来了,第二次再拿就啥也拿不到了

    反而错误管道里就有信息了

    那现在就可以把代码写进去吧

    # 服务端
    import socket
    import subprocess
    
    # 买手机
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 绑定手机卡
    phone.bind(("127.0.0.1", 8080))
    phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 开机
    phone.listen(5)
    
    # 等电话
    connet, client_addr = phone.accept()
    
    # 收发消息
    while 1:
        try:
            k = connet.recv(1024)
            obj = subprocess.Popen(k.decode("gbk"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            connet.send(stdout + stderr)
        except ConnectionResetError:
            break
    
    # 挂电话
    connet.close()
    
    # 关机
    phone.close()
    
    # 客户端
    import socket
    
    # 买手机
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 拨号
    phone.connect(("127.0.0.1", 8080))
    
    # 发收信息
    while 1:
        msg = input(">>>")
        phone.send(msg.encode("gbk"))
        k = phone.recv(1024)
        k = k.decode("gbk")
        print(f"从服务端接收的消息:{k}")
    
    connet.close()
    # 关闭
    phone.close()
    

    其实到现在,我们其实完成了一个模拟ssh远程执行命令的操作,就是客户端获取主机的信息的操作

    但是我们会发现问题,我要是传的超过1024个字节的数据,怎么办呢???

    还有一个就是 stdout + stderr 这个加号就相当于新开个内存空间

    而不是直接在他本身上加,所以这个效率应该是可以优化的


    比如说我传一个超过1024的,会发生什么现象?

    我先打印一下D盘的文件

    我发现信息没有完整的显示出来

    然后我再打印一下IP信息

    我会发现,打印结果的上面,还有上一次打印没打印出来的信息!

    说明溢出的数据没有丢,而是等待下一次的调用然后传过去

    其实也很好理解

    我们这不是流式传输嘛,那水流还能 嘎巴 一下就断了?

    不能吧,当我拿到一点水后关闭通道,本该进来的水就因为容器太小的原因

    留在了外面,等到下次调用的时候,在外面的这一部分就先进来,然后再进其他的

    这个现象,就叫粘包!

    指的是多个包的返回值黏在一起了

    那应该怎么解决呢?


    我是不是可以在发消息之前,先告诉服务端我要传多大的文件

    然后服务端就对这个信息做出相应的操作

    有一种方法是把1024改一个更大的数,但其实

    还是治标不治本,因为你不知道返回值有多大,就有可能被超越

    那还有一种方法,那我没接完我就继续接呗

    那我就应该把数据长度发给客户端,然后再发送数据

    然后再说说+的问题,因为他是流数据,那我按顺序发,他是不是就自己拼上了啊

    connet.send(stdout)
    connet.send(stderr)
    

    而服务端首先应该接收到数据长度然后再接收数据是吧

    那我客户端大概应该这么写

    msg = input(">>>")
        phone.send(msg.encode("gbk"))
        re_len = 1025  # 数据长度
        re_size = 0
        r = b""  # 我传过来的是字节模式
        if re_size < re_len:
            k = phone.recv(1024)
            r += k
            re_size += len(k)
        print(f"从服务端接收的消息:{k}")
    

    那这个数据长度,就应该是服务端传过来的对吧

    所以服务端大概应该这么写

    stdout = obj.stdout.read()
    stderr = obj.stderr.read()
            
    r_size = len(stdout)+len(stderr)
    connet.send(str(r_size).encode("gbk")) # 数字模式不能传,只能传字符串
    connet.send(stdout)
    connet.send(stderr)
    

    但是问题就出现了!我这三个发送信息也是粘包,那我怎么能让客户端进行分辨?

    还记得我们在说传输数据的时候提到了报头的概念嘛?

    所以其实我们是在写报头,而报头是固定长度的

    所以我现在就要学会如何发报头对吧!


    那我们现在开始自定义报头吧

    这时候我们就需要学习一个新的模块struct

    struck.pack()
    

    相当于打包这里面传的是两个参数,第一个是数据类型,第二个的是数据

    res = struct.pack("i", 1234)
    print(res, type(res), len(res))
    

    打印的是:

    b'xd2x04x00x00' <class 'bytes'> 4
    

    所以,我这就算是拿到了报头的长度

    那我这打包怎么解包???

    res = struct.unpack("i",res)
    
    (1234,)
    

    我拿到的是元组,所以[0]是不是就拿到了1234了

    那长度是不是也就拿到了

    那服务端就可以写了

    r_size = len(stdout) + len(stderr)
    res = struct.pack("i", r_size)
    

    那客户端就知道怎么做了

    res = phone.recv(4)
    re_len = struct.unpack("i", res)[0]
    

    那总的来说,代码就如下:

    # 客户端
    import socket
    
    # 买手机
    import struct
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 拨号
    phone.connect(("127.0.0.1", 8080))
    
    # 发收信息
    while 1:
        msg = input(">>>")
        phone.send(msg.encode("gbk"))
        res = phone.recv(4)
        re_len = struct.unpack("i", res)[0]
    
        re_size = 0
        r = b""  # 我传过来的是字节模式
        while re_size < re_len:
            k = phone.recv(1024)
            r += k
            re_size += len(k)
            print(re_size, re_len)
        print(f'从服务端接收的消息:{r.decode("gbk")}')
    
    connet.close()
    # 关闭
    phone.close()
    
    # 服务端
    import socket
    import struct
    import subprocess
    
    # 买手机
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # 绑定手机卡
    phone.bind(("127.0.0.1", 8080))
    phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 开机
    phone.listen(5)
    
    # 等电话
    connet, client_addr = phone.accept()
    
    # 收发消息
    while 1:
        try:
            k = connet.recv(1024)
            obj = subprocess.Popen(k.decode("gbk"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
    
            r_size = len(stdout) + len(stderr)
            res = struct.pack("i", r_size)
            connet.send(res)
    
            connet.send(stdout)
            connet.send(stderr)
        except ConnectionResetError:
            break
    
    # 挂电话
    connet.close()
    
    # 关机
    phone.close()
    

    *这是有问题的*
    *你要继续看吖*
  • 相关阅读:
    计算机网络
    计算机网络知识总结
    final,static,super,this
    ArrayList
    基础面试题
    BATJ都爱问的多线程面试题
    Session过期、失效时间
    类加载器ClassLoader源码解析
    连接ftp服务器 JDK 1.7
    根据当前请求的特征,判断该请求是否来自手机终端
  • 原文地址:https://www.cnblogs.com/jevious/p/11321610.html
Copyright © 2011-2022 走看看