day30---网络编程之TCP粘包问题

1. subprocess模块

  • 描述:创建一个新的进程让其执行另外的程序,并与它进行通信,获取标准的输入、标准输出、标准错误以及返回码等

  • subprocess模块中定义了一个Popen类,通过它可以来创建进程,并与其进行复杂的交互,输出的结果需要使用read()来读取,并返回bytes类型

    import subprocess
    subprocess.Popen(arg, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    
    • args:需要执行的shell命令
    • shell:设置位True代表程序通过shell执行命令
    • stdin:标准输入
    • stdout:标准输出
    • stderr:标准错误输出
    • PIPE:管道(输入输出的对象)
    import subprocess
    for i in range(2):
            shell = input('>>>:').strip()
            data = subprocess.Popen(shell, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            print('正确输出是:')
            print(data.stdout.read())
            print('错误输出是:')
            print(data.stderr.read())
    >>>
    >>>:hostname
    正确输出是:
    b'Yan\r\n'
    错误输出是:
    b''
    >>>:aaa
    正确输出是:
    b''
    错误输出是:
    b"'aaa' \xb2\xbb\xca\xc7\xc4\xda\xb2\xbf\xbb\xf2\xcd\xe2\xb2\xbf\xc3\xfc\xc1\xee\xa3\xac\xd2\xb2\xb2\xbb\xca\xc7\xbf\xc9\xd4\xcb\xd0\xd0\xb5\xc4\xb3\xcc\xd0\xf2\r\n\xbb\xf2\xc5\xfa\xb4\xa6\xc0\xed\xce\xc4\xbc\xfe\xa1\xa3\r\n"
    

2. struct模块

  • 描述:把一个数据类型转为固定长度的bytes类型,可以用于网络传输

  • struct.pack:把数据类型根据指定的格式符转为字节流,返回bytes类型

  • struct.unpack:与pack相反,把字节流转为数据类型,返回元组形式,第0个值是转换位的数据类型的值
import struct
# 'i'代表int(整型),标准字节长度是:4;int的取值范围是: -2147483648 <= i(number) <= 2147483647
# 'q'代表long(长整型),标准字节长度是:8;'i'范围内的值也可以使用'q'
data1 = struct.pack('i', 2147483647)
print(data1)
print(len(data1))
print(struct.unpack('i', data1))
print('-' * 38)
data2 = struct.pack('q', 2147483648)
print(data2)
print(len(data2))
print(struct.unpack('q', data2))
print('-' * 38)
data3 = struct.pack('q', 1)
print(data3)
print(len(data3))
print(struct.unpack('q', data3)[0])
>>>
b'\xff\xff\xff\x7f'
4
(2147483647,)
--------------------------------------
b'\x00\x00\x00\x80\x00\x00\x00\x00'
8
(2147483648,)
--------------------------------------
b'\x01\x00\x00\x00\x00\x00\x00\x00'
8
1

3. 粘包现象

  • 功能描述:如下服务端和客户端代码,实现的功能是客户端成功与服务端建立连接,客户端可以远程执行服务端的系统命令(windows平台默认编码是'gbk'),并把执行结果返回给客户端
  • 问题描述:指定的字节大小是1024,如果接收的字节大于1024,剩余的数据会随着下一次执行的命令而接收,这样所有的命令和接收数据的结果会完全的混乱。如果把字节大小配置为无穷大,但是超过了内存的物理大小,会把程序搞崩溃,扔是无法从根本解决问题。这种不知道接收的数据大小而手动指定具体的字节大小,最终造成的不可预知的异常结果就是粘包现象
# 服务端
import subprocess
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
        conn, add = server.accept()
        print('[%s]已成功连接,端口是:[%s]' % (add[0], add[1]))
        while True:
            try:
                shell = conn.recv(1024)
                if not shell:
                    break
                data = subprocess.Popen(shell.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                stdout = data.stdout.read()
                stderr = data.stderr.read()
                conn.send(stdout + stderr)
            except ConnectionResetError:
                break
        conn.close()
server.close()
# 客户端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
        shell = input('请输入运行命令:').strip()
        if not shell:
            continue
        client.send(shell.encode('utf-8'))
        data = client.recv(1024)
        print(data.decode('gbk'))
client.close()
  • 只有TCP有粘包现象,UDP永远不会粘包
  • 发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区
  • 所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据
    • TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的
      • Nagle算法:把数据量较小并且时间间隔比较短的包合成一个包发送
    • UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的
    • tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头
    • udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
    • tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包
  • 两种情况下会发生粘包:
    • 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
    • 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

4. 解决粘包问题

  • 普通版:自定义报头

    • 发送:

      • 1. 先制作报头
      • 2. 再发送报头
      • 3. 最后再发送真实的数据
    • 接收:

      • 1. 先接收报头
      • 2. 最后再循环接收完整的数据
# 服务端
import subprocess
import struct
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
        conn, add = server.accept()
        print('[%s]已成功连接,端口是:[%s]' % (add[0], add[1]))
        while True:
            try:
                shell = conn.recv(1024)
                if not shell:
                    break
                data = subprocess.Popen(shell.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                stdout = data.stdout.read()
                stderr = data.stderr.read()
                # 第一步:制作报头
                total_size = len(stdout) + len(stderr)
                header = struct.pack('i', total_size)
                # 第二步:发送报头
                conn.send(header)
                # 第三步:发送真实数据
                conn.send(stdout)
                conn.send(stderr)
            except ConnectionResetError:
                break
        conn.close()
server.close()
# 客户端
import struct
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
    shell = input('请输入运行命令:').strip()
    if not shell:
        continue
    client.send(shell.encode('utf-8'))
    # 第一步:接收报头
    head = client.recv(4)
    total_size = struct.unpack('i', head)[0]
    # 第二步:循环收取完整的数据
    recv_size = 0
    data = b''
    while recv_size < total_size:
        recv_data = client.recv(1024)
        data += recv_data
        recv_size += len(recv_data)
    print(data.decode('gbk'))
client.close()
  • 高级版:把报头做成字典形式,字典里包含需要发送的真实数据的详细信息,然后再序列化成字符串,最后再转换成固定长度的字节流(使用'i'的方式,4个字节足够)

    • 发送:

      • 1. 先制作报头(使用字典格式存储)
      • 2. 把字典序列化为字符串
      • 3. 把字符串编码为bytes类型
      • 4. 把butes类型的长度转为字节流
      • 5. 再发送报头长度
      • 6. 然后发送报头
      • 7. 最后再发送真实的数据
    • 接收:

      • 1. 先接收报头长度
      • 2. 接收报头,解出报头内容
      • 3. 解码为字符串类型
      • 4. 反序列化位字典类型
      • 5. 在字典中获取报头内容
      • 6. 循环接收完整的数据
# 服务端
import subprocess
import struct
import json
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
        conn, add = server.accept()
        print('[%s]已成功连接,端口是:[%s]' % (add[0], add[1]))
        while True:
            try:
                shell = conn.recv(1024)
                if not shell:
                    break
                data = subprocess.Popen(shell.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                stdout = data.stdout.read()
                stderr = data.stderr.read()
                # 第一步:制作报头
                # 1. 使用字典存储报头中需要发送数据的详细信息
                header_dict = {
                    'total_size': len(stdout) + len(stderr),
                    'MD5': 'md5值',
                    'file_name': '文件路径和文件名',
                    'file_size': '文件大小',
                    'file_pwd': '文件加密认证'
                }
                # 2. 序列化成字符串类型
                header_json = json.dumps(header_dict)
                # 3. 编码为bytes类型
                header_bytes = header_json.encode('utf-8')
                # 4. 转换位字节流
                header_size = struct.pack('i', len(header_bytes))
                # 第二步:发送报头的长度
                conn.send(header_size)
                # 第三步:发送报头
                conn.send(header_bytes)
                # 第四步:发送真实数据
                conn.send(stdout)
                conn.send(stderr)
            except ConnectionResetError:
                break
        conn.close()
server.close()
# 客户端
import struct
import json
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
        shell = input('请输入运行命令:').strip()
        if not shell:
            continue
        client.send(shell.encode('utf-8'))
        # 第一步:接收报头长度
        header_len = client.recv(4)
        header_size = struct.unpack('i', header_len)[0]
        # 第二步:接收报头,解出报头内容
        # 1. 接收报头
        header_bytes = client.recv(header_size)
        # 2. 解码为字符串类型
        header_json = header_bytes.decode('utf-8')
        # 3. 反序列化为字典类型
        header_dict = json.loads(header_json)
        # 4. 在字典中获取报头内容
        total_size = header_dict['total_size']
        # 第三步:循环接收完整的数据
        recv_size = 0
        data = b''
        while recv_size < total_size:
            recv_data = client.recv(1024)
            data += recv_data
            recv_size += len(recv_data)
        print(data.decode('gbk'))
client.close()

5. 应用

  • 实现客户端的文件下载

# 服务端
import os
import struct
import json
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
def get(filename):
        header_dict = {
            'file_size': os.path.getsize(filename),
            'file_name': filename
        }
        header_json = json.dumps(header_dict)
        header_bytes = header_json.encode('utf-8')
        header_size = struct.pack('i', len(header_bytes))
        conn.send(header_size)
        conn.send(header_bytes)
        with open(filename, 'rb') as f:
            for i in f:
                conn.send(i)
while True:
        print('等待客户端连接中...')
        conn, add = server.accept()
        print('客户端[%s]连接成功,端口是[%s]' % (add[0], add[1]))
        while True:
            try:
                shell = conn.recv(1024)
                if not shell:
                    break
                cmd, filename = shell.decode('utf-8').split()
                if cmd == 'get':
                    get(filename)
            except ConnectionResetError:
                break
        conn.close()
server.close()
# 客户端
import struct
import json
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
def get():
        header_len = client.recv(4)
        header_size = struct.unpack('i', header_len)[0]
        header_bytes = client.recv(header_size)
        header_json = header_bytes.decode('utf-8')
        header_dict = json.loads(header_json)
        file_name = header_dict['file_name']
        file_size = header_dict['file_size']
        recv_size = 0
        with open(file_name, 'wb') as f:
            while recv_size < file_size:
                recv_data = client.recv(1024)
                f.write(recv_data)
                recv_size += len(recv_data)
while True:
        shell = input('下载请输入[get 文件名]:').strip()
        if not shell:
            continue
        client.send(shell.encode('utf-8'))
        cmd, filename = shell.split()
        if cmd == 'get':
            get()
client.close()
  • 实现客户端的文件上传

# 服务端
import struct
import json
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
def put():
        header_len = conn.recv(4)
        header_size = struct.unpack('i', header_len)[0]
        header_bytes = conn.recv(header_size)
        header_json = header_bytes.decode('utf-8')
        header_dict = json.loads(header_json)
        file_name = header_dict['file_name']
        file_size = header_dict['file_size']
        recv_size = 0
        with open(file_name, 'wb') as f:
            while recv_size < file_size:
                recv_data = conn.recv(1024)
                f.write(recv_data)
                recv_size += len(recv_data)
while True:
        print('等待客户端连接中...')
        conn, add = server.accept()
        print('客户端[%s]连接成功,端口是[%s]' % (add[0], add[1]))
        while True:
            try:
                shell = conn.recv(1024)
                if not shell:
                    break
                cmd, filename = shell.decode('utf-8').split()
                if cmd == 'put':
                    put()
            except ConnectionResetError:
                break
        conn.close()
server.close()
# 客户端
import os
import struct
import json
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
def put(filename):
        header_dict = {
            'file_size': os.path.getsize(filename),
            'file_name': filename
        }
        header_json = json.dumps(header_dict)
        header_bytes = header_json.encode('utf-8')
        header_size = struct.pack('i', len(header_bytes))
        client.send(header_size)
        client.send(header_bytes)
        with open(filename, 'rb') as f:
            for i in f:
                client.send(i)
while True:
        shell = input('上传请输入[put 文件名]:').strip()
        if not shell:
            continue
        client.send(shell.encode('utf-8'))
        cmd, filename = shell.split()
        put(filename)
client.close()
  • 实现客户端的文件上传和下载

# 服务端
import os
import struct
import json
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
def get(filename):
        header_dict = {
            'file_size': os.path.getsize(filename),
            'file_name': filename
        }
        header_json = json.dumps(header_dict)
        header_bytes = header_json.encode('utf-8')
        header_size = struct.pack('i', len(header_bytes))
        conn.send(header_size)
        conn.send(header_bytes)
        with open(filename, 'rb') as f:
            for i in f:
                conn.send(i)
def put():
        header_len = conn.recv(4)
        header_size = struct.unpack('i', header_len)[0]
        header_bytes = conn.recv(header_size)
        header_json = header_bytes.decode('utf-8')
        header_dict = json.loads(header_json)
        file_name = header_dict['file_name']
        file_size = header_dict['file_size']
        recv_size = 0
        with open(file_name, 'wb') as f:
            while recv_size < file_size:
                recv_data = conn.recv(1024)
                f.write(recv_data)
                recv_size += len(recv_data)
while True:
        print('等待客户端连接中...')
        conn, add = server.accept()
        print('客户端[%s]连接成功,端口是[%s]' % (add[0], add[1]))
        while True:
            try:
                shell = conn.recv(1024)
                if not shell:
                    break
                cmd, filename = shell.decode('utf-8').split()
                if cmd == 'put':
                    put()
                elif cmd == 'get':
                    get(filename)
            except ConnectionResetError:
                break
        conn.close()
server.close()
# 客户端
import os
import struct
import json
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
def get():
        header_len = client.recv(4)
        header_size = struct.unpack('i', header_len)[0]
        header_bytes = client.recv(header_size)
        header_json = header_bytes.decode('utf-8')
        header_dict = json.loads(header_json)
        file_name = header_dict['file_name']
        file_size = header_dict['file_size']
        recv_size = 0
        with open(file_name, 'wb') as f:
            while recv_size < file_size:
                recv_data = client.recv(1024)
                f.write(recv_data)
                recv_size += len(recv_data)
def put(filename):
        header_dict = {
            'file_size': os.path.getsize(filename),
            'file_name': filename
        }
        header_json = json.dumps(header_dict)
        header_bytes = header_json.encode('utf-8')
        header_size = struct.pack('i', len(header_bytes))
        client.send(header_size)
        client.send(header_bytes)
        with open(filename, 'rb') as f:
            for i in f:
                client.send(i)
while True:
        shell = input('请选择上传或下载[get/put 文件名]:').strip()
        if not shell:
            continue
        if len(shell.split()) == 2:
            client.send(shell.encode('utf-8'))
            cmd, filename = shell.split()
            if cmd == 'put':
                print('上传文件[%s]' % filename)
                put(filename)
            elif cmd == 'get':
                print('下载文件[%s]' % filename)
                get()
            else:
                print('输入有误,请重新输入!')
        else:
            print('输入有误,请重新输入!')
client.close()
  • 进度条版上传与下载

# 服务端
import os
import struct
import json
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
def get(filename):
        header_dict = {
            'file_size': os.path.getsize(filename),
            'file_name': filename
        }
        header_json = json.dumps(header_dict)
        header_bytes = header_json.encode('utf-8')
        header_size = struct.pack('i', len(header_bytes))
        conn.send(header_size)
        conn.send(header_bytes)
        with open(filename, 'rb') as f:
            for i in f:
                conn.send(i)
def put():
        header_len = conn.recv(4)
        header_size = struct.unpack('i', header_len)[0]
        header_bytes = conn.recv(header_size)
        header_json = header_bytes.decode('utf-8')
        header_dict = json.loads(header_json)
        file_name = header_dict['file_name']
        file_size = header_dict['file_size']
        recv_size = 0
        with open(file_name, 'wb') as f:
            while recv_size < file_size:
                recv_data = conn.recv(1024)
                f.write(recv_data)
                recv_size += len(recv_data)
while True:
        print('等待客户端连接中...')
        conn, add = server.accept()
        print('客户端[%s]连接成功,端口是[%s]' % (add[0], add[1]))
        while True:
            try:
                shell = conn.recv(1024)
                if not shell:
                    break
                cmd, filename = shell.decode('utf-8').split()
                if cmd == 'put':
                    put()
                elif cmd == 'get':
                    get(filename)
            except ConnectionResetError:
                break
        conn.close()
server.close()
#客户端
import os
import struct
import json
import socket
def progress(percent, mode, description=None):
        if mode == 'put':
            description = '上传'
        elif mode == 'get':
            description = '下载'
        if percent == 100:
            print('\r[%s完成]%s%% :  %s\n' % (description, percent, '#' * percent), end='')
        else:
            print('\r[正在%s]%s%% :  %s' % (description, percent, '#' * percent), end='')
client = socket.socket()
client.connect(('127.0.0.1', 8080))
def get():
        header_len = client.recv(4)
        header_size = struct.unpack('i', header_len)[0]
        header_bytes = client.recv(header_size)
        header_json = header_bytes.decode('utf-8')
        header_dict = json.loads(header_json)
        file_name = header_dict['file_name']
        file_size = header_dict['file_size']
        recv_size = 0
        with open(file_name, 'wb') as f:
            while recv_size < file_size:
                recv_data = client.recv(1024)
                f.write(recv_data)
                recv_size += len(recv_data)
                percent = int(100 * (recv_size / file_size))
                progress(percent, cmd)
def put(filename):
        header_dict = {
            'file_size': os.path.getsize(filename),
            'file_name': filename
        }
        header_json = json.dumps(header_dict)
        header_bytes = header_json.encode('utf-8')
        header_size = struct.pack('i', len(header_bytes))
        client.send(header_size)
        client.send(header_bytes)
        send_size = 0
        with open(filename, 'rb') as f:
            for i in f:
                client.send(i)
                send_size += len(i)
                percent = int(100 * (send_size / header_dict['file_size']))
                progress(percent, cmd)
while True:
        shell = input('请选择上传或下载[get/put 文件名]:').strip()
        if not shell:
            continue
        if len(shell.split()) == 2:
            client.send(shell.encode('utf-8'))
            cmd, filename = shell.split()
            if cmd == 'put':
                print('上传文件[%s]' % filename)
                put(filename)
            elif cmd == 'get':
                print('下载文件[%s]' % filename)
                get()
            else:
                print('输入有误,请重新输入!')
        else:
            print('输入有误,请重新输入!')
client.close()
posted @ 2017-11-28 20:05  _岩哥  阅读(146)  评论(0)    收藏  举报