zoukankan      html  css  js  c++  java
  • TCP解决粘包+传输文件

    # 解决粘包
    **只有TCP有粘包现象,UDP永远不会粘包**

    **粘包原因** 本质是不知道要收多少
    1.tcp一次收的过多,需要下次才接收完,造成粘包
    2.tcp发到内核态内存是几条内容较少的消息,TCP有Nigon算法,把多个内容较少的包合成一个,操作系统再发出去,所以客户端只会收一次,就全收到

    TCP:A端与B端有通信连接,A端send,B端就能收到
    UDP:A端一次sendto,B端必须有一次recvfrom与之对应,B端才能真正收到
    UDP 如果 一次发的>一次收的,收不了的部分全丢

    **解决思路**
    1. 第一次发数据量,第二次发数据
    2. 选定前n个字节,包含长度,其余为数据

    **jason解决办法** 报头为dict,可存取文件的额外信息,方便后续处理
    服务端
    ```python
    import socket
    import subprocess
    import struct
    import json

    server = socket.socket()
    server.bind(('127.0.0.1',8081))
    server.listen(5)

    while True:
    conn, addr = server.accept()
    print('连接成功')
    while True:
    try:
    cmd = conn.recv(1024)
    print('接收成功')
    # tcp客户端发空,会导致服务端夯住(udp不存在此问题,因自带报头,为地址和端口信息)
    if len(cmd) == 0:break
    cmd = cmd.decode('utf-8')
    #利用subprocess开启新进程,可接收命令,并调用shell去执行这个字符串
    obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    #res接收subprocess的shell标准化输出和标准化报误信息
    res = obj.stdout.read() + obj.stderr.read()
    #将模块返回的信息的长度,和其他需要提供的信息,做出字典
    d = {'file_size':len(res),'info':'xxxxxxx'}
    #字典序列化
    json_d = json.dumps(d)
    # 1.先制作一个提示客户端,将要发送的字典长度的报头
    # (struct模块,将int类型的长度,转化为二进制字符串,只占4字节)
    header = struct.pack('i',len(json_d))
    # header2=struct.pack('i',len(json_d.encode('utf8'))) 与上句效果相同,算的都是bytes长度

    # 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()
    ```

    客户端
    ```python
    import socket
    import struct
    import json

    client = socket.socket()
    client.connect(('127.0.0.1',8081))
    print('连接成功')
    while True:
    msg = input('>>>:').encode('utf-8')
    # tcp客户端发空,会导致服务端夯住(udp不存在此问题,因为自带报头,为地址和端口的信息)
    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.从字典中获取信息
    recv_size = 0
    real_data = b''
    # 必须是 < ,最后一次若本来可以刚好发完 即 recv_size = dict_json,大不了再接收一次
    # 若改为 = ,最后一次本来收完,却还满足判断条件,收到的就是空了,会夯住
    while recv_size < dict_json.get('file_size'):
    data = client.recv(1024)
    real_data += data
    recv_size += len(data)
    print(real_data.decode('gbk'))
    ```

    **egon解决办法**
    https://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
    https://www.cnblogs.com/gala/archive/2011/09/22/2184801.html
    服务端
    ```python
    from socket import *
    import subprocess
    import struct
    ip_port=('127.0.0.1',8080)
    back_log=5
    buffer_size=1024

    tcp_server=socket(AF_INET,SOCK_STREAM)
    tcp_server.bind(ip_port)
    tcp_server.listen(back_log)

    while True:
    conn,addr=tcp_server.accept()
    print('新的客户端链接',addr)
    while True:
    #收
    try:
    cmd=conn.recv(buffer_size)
    if not cmd:break
    print('收到客户端的命令',cmd)

    #执行命令,得到命令的运行结果cmd_res
    res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
    stderr=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stdin=subprocess.PIPE)
    err=res.stderr.read()
    if err:
    cmd_res=err
    else:
    cmd_res=res.stdout.read()

    #发
    if not cmd_res:
    cmd_res='执行成功'.encode('gbk')

    length=len(cmd_res)
    # 按照给定的格式(fmt),把数据封装成字符串
    # 即直接把整型变成字符串
    data_length=struct.pack('i',length)
    # 这里粘包无所谓,客户端知道代表长度是4个字节
    conn.send(data_length)
    conn.send(cmd_res)

    except Exception as e:
    print(e)
    break
    ```
    客户端
    ```python
    from socket import *
    import struct
    from functools import partial
    ip_port=('127.0.0.1',8080)
    back_log=5
    buffer_size=1024

    tcp_client=socket(AF_INET,SOCK_STREAM)
    tcp_client.connect(ip_port)

    while True:
    cmd=input('>>: ').strip()
    if not cmd:continue
    if cmd == 'quit':break

    tcp_client.send(cmd.encode('utf-8'))


    #解决粘包
    # int类型的数据,二进制标准长度就是4,所以先收4个
    length_data=tcp_client.recv(4)
    # 取[0]第一个值就是,解析就是“int”型
    length=struct.unpack('i',length_data)[0]

    recv_msg=''.join(iter(partial(tcp_client.recv, buffer_size), b''))
    # 这一段出了问题,可学习研究下 应该是缺一个 减 的函数
    #解析: 1.tcp_client.recv()函数接收信息,每次最多buffer_size个bytes,
    # 利用偏函数把tcp_client.recv()函数的第一个参数buffer_size固定住
    # 2.利用iter(),把偏函数变成可迭代对象
    # 3.每次从内核态内存获取一段数据,并拼接到'',直到tcp_client.recv内容为b''
    print('命令的执行结果是 ',recv_msg.decode('gbk'))

    tcp_client.close()
    ```
    补充:
    ```python
    #iter(object, sentinel)
    #只写一个参数:把第一个变成迭代器
    #加第二个参数:sentinel,迭代到sentinel内容立即停下

    #偏函数:固定函数的第一个参数
    from functools import partial
    def add(x,y):
    return x+y
    #指定add函数,把1传给add第一个参数
    func=partial(add,1)
    print(func(1)) #2
    print(func(1)) #3
    ```
    pack(fmt, v1, v2, ...)     按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)
    unpack(fmt, string)       按照给定的格式(fmt)解析字节流string,返回解析出来的tuple

    socket里的sendall,基于send(),就是一个死循环在send,直到所有数据发送。
    而send一次最多8k(8096bytes)较好,MTU网卡最大传输单元1500bytes,发送过大,数据就要分片重组,丢一个就失真了


    **subprocess**
    https://www.cnblogs.com/linhaifeng/articles/6129246.html#_label9

    **避免通信循环中的报错**
    1.服务端在conn.recv()下一句
    用if 为空 :break 解决conn断开,服务端一直收空消息造成的死循环
    2.服务端在通信循环 用处理客户端进程非正常中断造成的报错(远程主机强迫关闭了一个现有的连接)
    try:
    #通信循环
    except Exception as e:
    print(e)
    break

    传输文件-客户端上传至服务端
    ```python
    import socket
    import os
    import json
    import struct

    server = socket.socket()
    server.bind(('127.0.0.1',8080))
    server.listen(5)

    while True:
    conn,addr = server.accept()
    while True:
    try:
    header_len = conn.recv(4)
    # 解析字典报头
    header_len = struct.unpack('i',header_len)[0]
    # 再接收字典数据
    header_dic = conn.recv(header_len)
    real_dic = json.loads(header_dic.decode('utf-8'))
    # 获取数据长度
    total_size = real_dic.get('file_size')
    # 循环接收并写入文件
    recv_size = 0
    with open(real_dic.get('file_name'),'wb') as f:
    while recv_size < total_size:
    data = conn.recv(1024)
    f.write(data)
    recv_size += len(data)
    print('上传成功')
    except ConnectionResetError as e:
    print(e)
    break
    conn.close()
    ```

    客户端
    ```python
    import socket
    import json
    import os
    import struct

    client = socket.socket()
    client.connect(('127.0.0.1',8080))

    while True:
    # 获取电影列表 循环展示
    MOVIE_DIR = r'D:python脱产10期视频day25视频'
    movie_list = os.listdir(MOVIE_DIR)
    # print(movie_list)
    for i,movie in enumerate(movie_list,1):
    print(i,movie)
    # 用户选择
    choice = input('please choice movie to upload>>>:')
    # 判断是否是数字
    if choice.isdigit():
    # 将字符串数字转为int
    choice = int(choice) - 1
    # 判断用户选择在不在列表范围内
    if choice in range(0,len(movie_list)):
    # 获取到用户想上传的文件路径
    path = movie_list[choice]
    # 拼接文件的绝对路径
    file_path = os.path.join(MOVIE_DIR,path)
    # 获取文件大小
    file_size = os.path.getsize(file_path)
    # 定义一个字典
    res_d = {
    'file_name':'性感荷官在线发牌.mp4',
    'file_size':file_size,
    'msg':'注意身体,多喝营养快线'
    }
    # 序列化字典
    json_d = json.dumps(res_d)
    json_bytes = json_d.encode('utf-8')

    # 1.先制作字典格式的报头
    header = struct.pack('i',len(json_bytes))
    # 2.发送字典的报头
    client.send(header)
    # 3.再发字典
    client.send(json_bytes)
    # 4.再发文件数据(打开文件循环发送)
    with open(file_path,'rb') as f:
    for line in f:
    client.send(line)
    else:
    print('not in range')
    else:
    print('must be a number')
    ```

  • 相关阅读:
    聊聊 Java8 以后各个版本的新特性
    如何使用SpringBoot封装自己的Starter
    Git原理入门解析
    Linux磁盘管理:LVM逻辑卷的拉伸及缩减
    LVM在线扩容
    Ubuntu setup Static IP Address
    ubuntu修改主机名
    user.sh
    升级Dell的R810固件版本
    DSET收集ESXi硬件日志
  • 原文地址:https://www.cnblogs.com/lishuaing/p/11354060.html
Copyright © 2011-2022 走看看