• Python socket 粘包问题 报头


    一 socket(套接字)

    1.什么是socket:Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。它把复杂的TCP/IP协议族隐藏在Socket接口后面,了socket以后,无需自己编写代码实现三次握手,四次挥手,ARP请求,打包数据等等,socket已经封装好了,只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

    2.基本语法

    客户端
    import socket
    # 1.创建socket对象  
    client = socket.socket() 
    # 2.链接服务器    
    client.connect((ip,port)) 
    # 3.收发数据   通常需要循环
    client.send(二进制数据) #   只能发二进制数据
    client.recv(字节数)    #  收多少字节数 阻塞直到接收到数据
    # 4.断开链接   
    client.close()
    
    服务端
    # 1.创建socket对象
    server = socket.socket()
    # 2.绑定一个固定的ip和端口
    server.bind(ip,port))
    # 3.开始监听客户端的到来
    server.listen() # 可不填
    # 4.接收客户端的链接请求
    conn,addr = server.accept()  # 阻塞直到客户链接到来  没有新连接则不可能执行该函数
    # 5.收发数据   通常需要循环
    conn.send(二进制数据) #   只能发二进制数据
    conn.recv(字节数)    #  收多少字节数 阻塞直到接收到数据
    # 6.关闭双行通道和服务器
    conn.close()
    server.close()
    复制代码
    View Code
    服务端套接字函数
    s.bind()    绑定(主机,端口号)到套接字
    s.listen()  开始TCP监听
    s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来
    
    客户端套接字函数
    s.connect()     主动初始化TCP服务器连接
    s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
    
    公共用途的套接字函数
    s.recv()            接收TCP数据
    s.send()            发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
    s.sendall()         发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
    s.recvfrom()        接收UDP数据
    s.sendto()          发送UDP数据
    s.getpeername()     连接到当前套接字的远端的地址
    s.getsockname()     当前套接字的地址
    s.getsockopt()      返回指定套接字的参数
    s.setsockopt()      设置指定套接字的参数
    s.close()           关闭套接字
    
    面向锁的套接字方法
    s.setblocking()     设置套接字的阻塞与非阻塞模式
    s.settimeout()      设置阻塞套接字操作的超时时间
    s.gettimeout()      得到阻塞套接字操作的超时时间
    View Code

    3.循环连接和循环通信

    客户端
    import socket
    
    client = socket.socket()
    client.connect(('127.0.0.1',8080))
    
    while True:
        msg = input('>>>:').encode('utf-8')
        if len(msg) == 0:continue
        client.send(msg)
        data = client.recv(1024)
        print(data)
    
    服务端
    import socket
    
    """
    服务端
        固定的ip和port
        24小时不间断提供服务
    """
    server = socket.socket()  # 生成一个对象
    server.bind(('127.0.0.1',8080))  # 绑定ip和port
    server.listen(5)  # 半连接池
    
    while True:
        conn, addr = server.accept()  # 等到别人来  conn就类似于是双向通道
        print(addr)  # ('127.0.0.1', 51323) 客户端的地址
        while True:
            try:
                data = conn.recv(1024)
                print(data)  # b''  针对mac与linux 客户端异常退出之后 服务端不会报错 只会一直收b''
                if len(data) == 0:break
                conn.send(data.upper())
            except ConnectionResetError as e:
                print(e)
                break
        conn.close()
    View Code
    #    ==================================================客户端
    import socket
    from 二_CMD程序 import smallTool
    import struct
    
    client = socket.socket()
    try:
        client.connect(("127.0.0.1",1688))
        print("链接成功!")
        while True:
            msg = input("请输入要执行指令:").strip()
            if msg == "q": break
            if not msg: continue
            # 发送指令
            # 先发长度
            len_bytes = struct.pack("q",len(msg.encode("utf-8")))
            client.send(len_bytes)
            # 在发指令
            client.send(msg.encode("utf-8"))
    
            data = smallTool.recv_data(client)
            print(data.decode("GBK"))
    
        client.close()
    except ConnectionRefusedError as e:
        print("链接服务器失败了!",e)
    except ConnectionResetError as e:
        print("服务器挂了!", e)
        client.close()
    
    
    import socket
    import subprocess
    import struct
    from 二_CMD程序 import  smallTool
    
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.bind(("127.0.0.1",1688))
    server.listen()
    # back
    
    while True:
        # socket,addr一个元组 客户端的ip和port
        client,addr = server.accept()
        print("客户端链接成功!")
        # 循环收发数据
        while True:
            try:
                cmd = smallTool.recv_data(client)
                if not cmd:
                    break
                print(cmd)
    
                p = subprocess.Popen(cmd.decode("utf-8"),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
                # 不要先读err错误信息  它会卡主  原因不详  linux不会有问题  tasklist  netstat - ano啥的
                data = p.stdout.read()
                err_data = p.stderr.read()
    
                len_size = len(data) + len(err_data)
                print("服务器返回了: %s " %  len_size)
    
                len_bytes = struct.pack("q",len_size)
    
                # 在发送真实数据前先发送 长度
                client.send(len_bytes)
    
                # 返回的结果刚好就是二进制
                # 发送真实数据
                client.send(data + err_data)
    
    
            except ConnectionResetError as e:
                print("客户端了挂了!",e)
                break
        client.close()
    
    #server.close()
    View Code

    二.socket使用中常见问题及解决办法:

    1.端口占用

    问题发生原因:

    1.可能是由于你已经启动了服务器程序,却又再次启动了服务器程序,同一个端口不能被多个进程使用导致!

    2.三次握手或四次挥手时,发生了异常导致对方程序已经结束而服务器任然处于time_wait状态导致!

    3.在高并发的场景下,由于链接的客户端太多,也会产生大量处于time_wait状态的链接

    解决的方案:

    第1种原因,很简单关闭之前运行的服务器即可

    第2,3中原因导致的问题,有两种解决方案:

    from socket import SOL_SOCKET,SO_REUSEADDR
    sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
    View Code

    2.粘包

    1.什么是粘包:粘包指的是数据与数据之间没有明确的分界线,导致不能正确读取数据!

    2.

    粘包 仅发生在TCP协议中

    1. 发送端 发送的数据量小 并且间隔短 会粘

    2. 接收端 一次性读取了两次数据的内容 会粘

    3. 接收端 没有接收完整 剩余的内容 和下次发送的粘在一起

    无论是那种情况,其根本原因在于 接收端不知道数据到底有多少

    解决方案就是 提前告知接收方 数据的长度

    3.解决方法

    先发长度给对方 再发真实数据

    #发送端

     1.使用struct 将真实数据的长度转为固定的字节数据

     2.发送长度数据

     3.发送真实数据

    接收端

     1.先收长度数据 字节数固定

     2.再收真实数据 真实可能很长 需要循环接收

    发送端和接收端必须都处理粘包 才算真正的解决了

    客户端
    import socket
    
    client = socket.socket()
    client.connect(("127.0.0.1",1688))
    
    client.send("hello".encode("utf-8"))
    client.send("python".encode("utf-8"))
    client.close()
    
    服务端
    import socket
    
    server = socket.socket()
    server.bind(("127.0.0.1",1688))
    server.listen()
    client,addr = server.accept()
    
    data1 =  client.recv(5)
    print(data1)
    data2 = client.recv(6)
    print(data2)
    
    
    client.close()
    View Code

    4.struct模块

    但是由于negle优化机制的存在,长度信息和数据还是有可能会粘包,而接受方并不知道长度信息具体几个字节,所以现在的问题是如何能够长度信息做成一个固定长度的bytes数据</p>
    
    我们可以将字符串拼接为一个固定长度的字符 但是这样太麻烦,struct模块为我们提供了一个功能,可以将整数类型转换为固定长度的bytes,此时就派上用场了
    
    # struct模块使用
    import  struct
    # 整型转bytes
    res =  struct.pack("i",100)
    print(res)
    print(len(res))
    
    # bytes转整型
    res2 = struct.unpack("i",res) # 返回一个元组
    print(res2)
    print(res2[0])
    View Code
    客户端
    import socket
    import struct
    c = socket.socket()
    c.connect(("127.0.0.1",8888))
    while True:
        cmd = input(">>>:").strip()
        c.send(cmd.encode("utf-8"))
    
        data = c.recv(4)
        length = struct.unpack("i",data)[0]
        
        print(length)
        size = 0
        res = b""
        while size < length:
            temp = c.recv(1024)
            size += len(temp)
            res += temp
        print(res.decode("gbk"))
    
    服务端
    import socket
    import subprocess
    import struct
    server = socket.socket()
    server.bind(("127.0.0.1",8888))
    server.listen()
    
    while True:
        client, addr = server.accept()
        while True:
            cmd = client.recv(1024).decode("utf-8")
            p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
            data = p.stdout.read()+p.stderr.read()
            length = len(data)
            len_data = struct.pack("i",length)
            client.send(len_data)
    
            print(length)
            client.send(data)
    View Code

    自定义报头解决粘包

    1.什么是报头:当需要在传输数据时 传呼一些额外参数时就需要自定义报头例如我们要实现文件上传下载,不光要传输文件数据,还需要传输文件名字,md5值等等,报头本质是一个json 数据。

    2.

    客户端
    import socket
    import struct
    import json
    
    client = socket.socket()
    client.connect(('127.0.0.1',8080))
    
    while True:
        msg = input('>>>:').encode('utf-8')
        if len(msg) == 0:continue
        client.send(msg)
        # 1.先接受字典报头
        header_dict = client.recv(4)
        # 2.解析报头 获取字典的长度
        dict_size = struct.unpack('i',header_dict)[0]  # 解包的时候一定要加上索引0
        # 3.接收字典数据
        dict_bytes = client.recv(dict_size)
        dict_json = json.loads(dict_bytes.decode('utf-8'))
        # 4.从字典中获取信息
        print(dict_json)
        recv_size = 0
        real_data = b''
        while recv_size < dict_json.get('file_size'):  # real_size = 102400
            data = client.recv(1024)
            real_data += data
            recv_size += len(data)
        print(real_data.decode('gbk'))
    
    # 服务端
    import socket
    import subprocess
    import struct
    import json
    
    
    server = socket.socket()
    server.bind(('127.0.0.1',8080))
    server.listen(5)
    
    
    while True:
        conn, addr = server.accept()
        while True:
            try:
                cmd = conn.recv(1024)
                if len(cmd) == 0:break
                cmd = cmd.decode('utf-8')
                obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
                res = obj.stdout.read() + obj.stderr.read()
                d = {'name':'jason','file_size':len(res),'info':'asdhjkshasdad'}
                json_d = json.dumps(d)
                # 1.先制作一个字典的报头
                header = struct.pack('i',len(json_d))
                # 2.发送字典报头
                conn.send(header)
                # 3.发送字典
                conn.send(json_d.encode('utf-8'))
                # 4.再发真实数据
                conn.send(res)
                # conn.send(obj.stdout.read())
                # conn.send(obj.stderr.read())
            except ConnectionResetError:
                break
        conn.close()
    View Code
  • 相关阅读:
    2009年信息技术十大趋势
    转:ASP.NET MVC 第五个预览版和表单提交场景
    终于在博客园开通了
    与虚拟机Oracle连接出现ora12154问题的解决
    Frame框架页面加载中进度条的实现
    WordPress安装部署
    Xmarks不会关闭了,太好了!
    抠出淘宝的菜单树
    asp.net实现类似MaskTextBox功能
    win2003 64位系统下ODBC连接使用
  • 原文地址:https://www.cnblogs.com/tfzz/p/11317612.html
Copyright © 2020-2023  润新知