zoukankan      html  css  js  c++  java
  • Python 之网络编程之socket(2)黏包现象和socketserver并发

    一:黏包

    ###tcp协议在发送数据时,会出现黏包现象.

        (1)数据粘包是因为在客户端/服务器端都会有一个数据缓冲区,

        缓冲区用来临时保存数据,为了保证能够完整的接收到数据,因此缓冲区都会设置的比较大。

        (2)在收发数据频繁时,由于tcp传输消息的无边界,不清楚应该截取多少长度

    导致客户端/服务器端,都有可能把多条数据当成是一条数据进行截取,造成黏包

     

    ### 黏包出现的两种情况

    #黏包现象一:

    在发送端,由于两个数据短,发送的时间隔较短,所以在发送端形成黏包

    #黏包现象二:

    在接收端,由于两个数据几乎同时被发送到对方的缓存中,所有在接收端形成了黏包

    #总结:

        发送端,包之间时间间隔短 或者 接收端,接受不及时, 就会黏包

        核心是因为tcp对数据无边界截取,不会按照发送的顺序判断

     

    ###黏包对比:tcpudp

    #tcp协议:

    优点:接收时数据之间无边界,有可能粘合几条数据成一条数据,造成黏包

    缺点:不限制数据包的大小,稳定传输不丢包

     

    #udp协议:

    优点:接收时候数据之间有边界,传输速度快,不黏包

    缺点:限制数据包的大小(受带宽路由器等因素影响),传输不稳定,可能丢包

     

    #tcpudp对于数据包来说都可以进行拆包和解包,理论上来讲,无论多大都能分次发送

    但是tcp一旦发送失败,对方无响应(对方无回执),tcp可以选择再发,直到对应响应完毕为止

    udp一旦发送失败,是不会询问对方是否有响应的,如果数据量过大,易丢包

     

    ### 解决黏包问题

    #解决黏包场景:

    应用场景在实时通讯时,需要阅读此次发的消息是什么

    #不需要解决黏包场景:

    下载或者上传文件的时候,最后要把包都结合在一起,黏包无所谓.

    1.黏包现象

    黏包现象一:

    服务端代码:

    import socket
    
    
    sk = socket.socket()
    # 在bind方法之前加上这句话,可以让一个端口重复使用
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    # 绑定地址端口(在网络上注册主机)
    sk.bind( ("127.0.0.1",9001) )
    sk.listen()
    
    conn,addr = sk.accept()
    conn.send("6".encode("utf-8"))
    message = "hello,my "
    conn.send(message.encode("utf-8"))
    conn.send("world".encode("utf-8"))
    
    
    # 四次挥手
    conn.close()
    # 退还端口
    sk.close()

    客户端代码:

    import socket
    
    sk = socket.socket()
    sk.connect(("127.0.0.1", 9001))
    
    res0 = int(sk.recv(1).decode("utf-8")) #res0 "6"
    print(res0)
    
    res1 = sk.recv(res0)
    print(res1)
    
    #print(res1.decode("utf-8"))
    res2 = sk.recv(20)
    print(res2)
    sk.close()

    服务端向客户端发送两次消息,客户端接收三次,其中第三次出现黏包现象,因为客户端设置只接收6个字节,而服务端第二次发送了8个字节数,所有将剩下2个字节与第三次发送的数据黏包一起发送过来了,现象如截图:

    黏包现象二:

    首先是服务端代码:

    import socket
    import time
    
    sk = socket.socket()
    #在bind方法之前加上这句话,可以让一个端口重复使用
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    
    #绑定地址端口(在网络上注册主机)
    sk.bind(("127.0.0.1", 9001))
    sk.listen()
    
    conn,addr = sk.accept()
    conn.send("000120".encode("utf-8"))
    message = 'hello' * 20
    conn.send(message.encode("utf-8"))
    
    #time.sleep(1)
    conn.send("world".encode("utf-8"))
    
    #四次挥手
    conn.close()
    
    #退还端口
    sk.close()

    然后是客户端代码:

    import socket
    import time
    
    sk = socket.socket()
    sk.connect(("127.0.0.1", 9001))
    
    #time.sleep(0.2)
    res0 = int(sk.recv(8).decode("utf-8"))  #res0 "6"
    print(res0)
    
    res1 = sk.recv(res0)
    print(res1)
    
    #print(res1.decode("utf-8"))
    res2 = sk.recv(10)
    print(res2)
    sk.close()

    这个因为在接收端,由于两个数据几乎同时被发送到对方的缓存中,所有在接收端形成了黏包,以至于第三次接收到的内容为空,因为已经黏包到第二次数据上了。如下截图:

     

    这种情况只要将第二次发送和第三次发送数据隔开一点时间,比如sleep1秒即可,即将注释#time.sleep(1)去掉后运行结果截图如下:

     

     

    但是这样黏包的现象依然存在,接下来我们先来介绍下可以解决黏包的模块的用法。

    2.struct

    python中的struct模块就提供了这样的机制,该模块的主要作用就是对python基本类型值与用python字符串格式表示的C struct类型间的转化(This module performs conversions between Python values and C structs represented as Python strings.)。stuct模块提供了很简单的几个函数,下面写例子。

    两个函数:pack()、unpack()

    struct模块最重要的两个函数就是pack()unpack()方法

    打包函数:pack(fmt, v1, v2, v3, ...)

    解包函数:unpack(fmt, buffer)

    #例:

    import struct
    # pack 把任意长度的数字转化成固定4个字节长度的字节流
    # unpack 将4个字节的值恢复成原本的数据,最后返回一个元组
    
    res = struct.pack("i",2372722) #不能超过int 范围
    print(res)
    print(len(res))
    
    res = struct.unpack("i",res)
    print(res)
    print(res[0],type(res[0]))

    结果为:

    b'r4$x00'

    4

    (2372722,)

    2372722 <class 'int'>

    3.struct解决黏包现象

    首先是服务端代码:

    import socket
    import struct
    
    sk = socket.socket()
    sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    sk.bind(("127.0.0.1", 9999))
    sk.listen()
    
    conn,addr = sk.accept()
    inp = input("C>>>msg:")
    msg = inp.encode("utf-8")
    
    #发送数据的长度通过pack进行替换,变成具有固定长度的4个字节的值
    res = struct.pack("i",len(msg))
    conn.send(res)
    
    #接下来,开始真正的发送数据
    conn.send(msg)
    conn.send("world".encode("utf-8"))
    
    res = conn.recv(1024)
    print(res)
    print(res.decode("utf-8"))
    
    #四次挥手
    conn.close()
    #退还端口
    sk.close()

    然后是客户端代码:

    import socket
    import struct
    import time
    
    sk = socket.socket()
    sk.connect(("127.0.0.1", 9999))
    time.sleep(0.1)
    
    #接收4个字节长度,它实际要发送的那个数字转化来的
    n = sk.recv(4)
    n = struct.unpack("i",n)[0]
    print(n)
    #接下来接收服务端发送过来的数据
    res1 = sk.recv(n)
    print(res1.decode("utf-8"))
    res2 = sk.recv(1024)
    print(res2.decode("utf-8"))
    
    #空格不是ascii编码中,大家注意
    sk.send(b'gqicuq_love_lin')
    
    #关闭连接
    sk.close()

    此时可以输入任何数据,且不会再造成黏包现象,运行后输入数据及输出不在黏包截图:

    输入端:

     

    输出端:

     

    二:socketserver并发

    #网络协议的最底层就是socket,基于原有socket模块,又封装了一层,就是socketserver

    socketserver 为了实现tcp协议,server端的并发.

     

    首先是对socketserver基本用法代码如下:

    服务端:

    import socketserver
    #需要自定义一个类,并继承socketserver.BaseRequestHandler
    class MyServer(socketserver.BaseRequestHandler):
        def handle(self):
            print(self.request)
            print("---->执行这句")
    
    
    #Threading ((ip,端口号),自定义类)
    server = socketserver.ThreadingTCPServer(("127.0.0.1", 9001), MyServer)
    
    #循环调用
    server.serve_forever()

    客户端:

    import socket
    
    sk = socket.socket()
    sk.connect(("127.0.0.1", 9001))
    sk.close()

    然后是使用socketserver来达到并发的效果:

    服务端代码:

    import socketserver
    class MyServer(socketserver.BaseRequestHandler):
        #在handle里面自定义收发逻辑
        def handle(self):
            print("--->这句话被执行了")
    
            conn = self.request
            while True:
                msg = conn.recv(1024).decode("utf-8")
                print(msg)
                conn.send(msg.upper().encode("utf-8"))
    
    
    #产生一个对象
    server = socketserver.ThreadingTCPServer(("127.0.0.1", 9999), MyServer)
    #循环不调用
    server.serve_forever()

    因为要做到并发的效果,所有这边启动了两个客户端,并为此区别,一个客户端发一段a字母,一个客户端发h字母。

    客户端1代码如下:

    import socket
    
    sk = socket.socket()
    sk.connect(("127.0.0.1", 9999))
    
    while True:
        sk.send(b'aaaaaaaaaaaaaaa')
        msg = sk.recv(1024)
        print(msg)
    
    sk.close()

    客户端2代码如下:

    import socket
    
    sk = socket.socket()
    sk.connect(("127.0.0.1", 9999))
    
    while True:
        sk.send(b"hhhhhhhhh")
        msg = sk.recv(1024)
        print(msg)

    运行截图如下:

    服务器端打印:

     

     

    客户端1打印:

     

    客户端2打印:

     

    就此形成了并发效果。

    而socketserver是需要严格的格式编写的,除了下图中红色框起来的可以自由编写,别的代码都是固定的:

  • 相关阅读:
    基于接口的动态代理和基于子类的动态代理
    JDBC连接数据库
    关于使用Binlog和canal来对MySQL的数据写入进行监控
    使用VMware12在CentOS7上部署docker实例
    VMWare12pro安装Centos 6.9教程
    读《Java并发编程的艺术》学习笔记(十)
    读《Java并发编程的艺术》学习笔记(九)
    读《Java并发编程的艺术》学习笔记(八)
    读《Java并发编程的艺术》学习笔记(七)
    读《Java并发编程的艺术》学习笔记(六)
  • 原文地址:https://www.cnblogs.com/hszstudypy/p/10990975.html
Copyright © 2011-2022 走看看