Fork me on GitHub

day 27

day 27

1. 粘包

1.1 什么是粘包

socket原理

发送端可以是一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算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

1.2 为什么会粘包

主要是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

  • 发送方原因

    TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:

    1. 只有上一个分组得到确认,才会发送下一个分组
    2. 收集多个小分组,在一个确认到来时一起发送

    Nagle算法造成了发送方可能会出现粘包问题。

  • 接收方原因

    TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。

1.3 发生粘包的两种情况

  1. 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

    # server端
    import socket
    ip_port=('127.0.0.1',8080)
    
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    conn,addr=tcp_socket_server.accept()
    
    data1=conn.recv(10)
    data2=conn.recv(10)
    
    print('----->',data1.decode('utf-8'))
    print('----->',data2.decode('utf-8'))
    
    conn.close()
    
    # client端
    import socket
    BUFSIZE=1024
    ip_port=('127.0.0.1',8080)
    
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    res=s.connect_ex(ip_port)
    
    s.send('hello'.encode('utf-8'))
    s.send('egg'.encode('utf-8'))
    
  2. 接收方的缓存机制 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

    # server端
    from socket import *
    ip_port=('127.0.0.1',8080)
    
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    conn,addr=tcp_socket_server.accept()
    
    data1=conn.recv(2) #一次没有收完整
    data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的
    
    print('----->',data1.decode('utf-8'))
    print('----->',data2.decode('utf-8'))
    
    conn.close()
    
    # client端
    import socket
    BUFSIZE=1024
    ip_port=('127.0.0.1',8080)
    
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    res=s.connect_ex(ip_port)
    
    s.send('hello egg'.encode('utf-8'))
    

2 struct 模块

2.1 struct 模块介绍

struct 模块: 是用来将整型的数字转成固定长度的bytes。

  1. 按照指定格式将Python数据转换为字符串,该字符串为字节流,如网络传输时,不能传输int,此时先将int转化为字节流,然后再发送;

  2. 按照指定格式将字节流转换为Python指定的数据类型

    # 按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)
    pack(fmt, v1, v2, ...) 
     
    # 按照给定的格式(fmt)解析字节流string,返回解析出来的tuple
    unpack(fmt,string) 
    

    fmt 支持的格式包括:

2.2 解决粘包问题

# 服务端
from socket import *
 
import subprocess
import struct
import json
 
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
    conn,client_addr=server.accept()
    print(conn,client_addr)#(连接对象,客户端的ip和端口)
    while True:
        try:
            cmd=conn.recv(1024)
            obj=subprocess.Popen(
                cmd.decode('utf-8'),
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
                                 )
            stdout=obj.stdout.read()
            stderr=obj.stderr.read()
            #1、制作报头
            header_dic={
                'total_size':len(stdout)+len(stderr),
                'md5':'dgdsfsdfdsdfsfewrewge',
                'file_name':'a.txt'
            }
            header_json=json.dumps(header_dic)
            header_bytes=header_json.encode('utf-8')
            #2、先发送报头的长度
            header_size=len(header_bytes)
            conn.send(struct.pack('i',header_size))
            #3、发送报头
            conn.send(header_bytes)
            #4、发送真实的数据
            conn.send(stdout)
            conn.send(stderr)
 
        except ConnectionResetError:
            break
    conn.close()
server.close()
# 客户端
from socket import *
import json
import struct
 
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
 
while True:
    cmd=input(">>:").strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))
 
    #1、接收报文头的长度
    header_size=struct.unpack('i',client.recv(4))[0]
    #2、接收报文
    header_bytes=client.recv(header_size)
 
    #3、解析报文
    header_json=header_bytes.decode('utf-8')
    header_dic=json.loads(header_json)
    print(header_dic)
 
    #4、获取真实数据的长度
    totol_size=header_dic['total_size']
 
    #5、获取数据
    recv_size=0
    res=b''
    while recv_size<totol_size:
        recv_date=client.recv(1024)
        res+=recv_date
        recv_size+=len(recv_date)
 
    print(res.decode('gbk'))
client.close()

2.3 补充

import struct
#struct是用来将整型的数字转成固定长度的bytes.
import json
 
header_dic={
    'total_size':32322,
    'md5':'gdssfsfsdfsf',
    'filename':'a.txt'
 
}
#1、将报头字典序列化。
header_json=json.dumps(header_dic)
#2、将序列后的字典转成字节
header_bytes=header_json.encode('utf-8')
#3、获取序列的字字典转成字节的个数
header_size=len(header_bytes)
print(header_size)
#4、将这个个数转成固字长度的字节表示
obj=struct.pack('i',header_size)
print(obj,len(obj))
#、这个固定长度的字节经过反转后是一个元组。
res=struct.unpack('i',obj)
#、通过按索取值就可等到报头字典长度。
header_size=res[0]

3. 基于UDP协议的socket套接字

3.1 UDP套接字简单实现

# 服务端
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议-》UDP
server.bind(('127.0.0.1', 8080))

while True:
    data, client_addr = server.recvfrom(1024)
    print('===>', data, client_addr)
    server.sendto(data.upper(), client_addr)

server.close()
# 客户端
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议-》UDP

while True:
    msg = input('>>: ').strip()  # msg=''
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    data, server_addr = client.recvfrom(1024)
    print(data)

client.close()
  • UDP是无链接的,先启动哪一端都不会报错
  • UDP协议是数据报协议,发空的时候也会自带报头,因此客户端输入空,服务端也能收到

3.2 UDP套接字无粘包问题

# 服务端
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议-》udp
server.bind(('127.0.0.1', 8080))

data, client_addr = server.recvfrom(1024)  # b'hello'==>b'h'
print('第一次:', client_addr, data)

data, client_addr = server.recvfrom(1024)  # b'world' =>b'world'
print('第二次:', client_addr, data)
#
# data,client_addr=server.recvfrom(1024)
# print('第三次:',client_addr,data)

server.close()
# 客户端
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议-》udp

client.sendto('hello'.encode('utf-8'), ('127.0.0.1', 8080))
client.sendto('world'.encode('utf-8'), ('127.0.0.1', 8080))
# client.sendto(''.encode('utf-8'),('127.0.0.1',8080))

client.close()

3.3 基于UDP实现QQ聊天室

# 服务端
import socket

server = socket.socket(type=socket.SOCK_DGRAM)

server.bind(
    ('127.0.0.1', 9527)
)

while True:

    # 服务端接收客户端传过来的消息
    msg, addr = server.recvfrom(1024)  # (消息,客户端地址)
    msg1, addr1 = server.recvfrom(1024)  # (消息,客户端地址)
    msg2, addr2 = server.recvfrom(1024)  # (消息,客户端地址)

    print(addr)
    print(addr1)
    print(addr2)
    print(msg.decode('utf-8'))
    print(msg1.decode('utf-8'))
    print(msg2.decode('utf-8'))

    # 服务端往客户端发送消息
    send_msg = input('服务端发送消息:').encode('utf-8')
    server.sendto(send_msg, addr)
    server.sendto(send_msg, addr1)
    server.sendto(send_msg, addr2)
# 客户端 1
import socket

client = socket.socket(type=socket.SOCK_DGRAM)

server_ip_port = ('127.0.0.1', 9527)

while True:
    send_msg = input('客户端1: ').encode('utf-8')

    # 发送消息必须要加上对方地址
    client.sendto(send_msg, server_ip_port)

    # 能接收任何人的消息
    msg = client.recv(1024)

    print(msg.decode('utf-8'))
# 客户端 2
import socket

client = socket.socket(type=socket.SOCK_DGRAM)

server_ip_port = ('127.0.0.1', 9527)

while True:
    send_msg = input('客户端2: ').encode('utf-8')

    # 发送消息必须要加上对方地址
    client.sendto(send_msg, server_ip_port)

    # 能接收任何人的消息
    msg = client.recv(1024)

    print(msg.decode('utf-8'))
# 客户端 3
import socket

client = socket.socket(type=socket.SOCK_DGRAM)

server_ip_port = ('127.0.0.1', 9527)

while True:
    send_msg = input('客户端3: ').encode('utf-8')

    # 发送消息必须要加上对方地址
    client.sendto(send_msg, server_ip_port)

    # 能接收任何人的消息
    msg = client.recv(1024)

    print(msg.decode('utf-8'))

4 基于socketserver实现并发的socket编程

4.1 基于TCP协议

# 服务端
import socketserver


class MyHandler(socketserver.BaseRequestHandler):
    def handle(self):
        # 通信循环
        while True:
            # print(self.client_address)
            # print(self.request) #self.request=conn

            try:
                data = self.request.recv(1024)
                if len(data) == 0: break
                self.request.send(data.upper())
            except ConnectionResetError:
                break


if __name__ == '__main__':
    s = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyHandler, bind_and_activate=True)

    s.serve_forever()  # 代表连接循环
    # 循环建立连接,每建立一个连接就会启动一个线程(服务员)+调用Myhanlder类产生一个对象,调用该对象下的handle方法,专门与刚刚建立好的连接做通信循环
# 客户端 1 
import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))  # 指定服务端ip和端口

while True:
    # msg=input('>>: ').strip() #msg=''
    msg = 'client33333'  # msg=''
    if len(msg) == 0: continue
    phone.send(msg.encode('utf-8'))
    data = phone.recv(1024)
    print(data)

phone.close()
# 客户端 2 
import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))  # 指定服务端ip和端口

while True:
    # msg=input('>>: ').strip() #msg=''
    msg = 'client11111'  # msg=''
    if len(msg) == 0: continue
    phone.send(msg.encode('utf-8'))
    data = phone.recv(1024)
    print(data)

phone.close()

4.2 基于UDP协议

# 服务端
import socketserver


class MyHandler(socketserver.BaseRequestHandler):
    def handle(self):
        # 通信循环
        print(self.client_address)
        print(self.request)

        data = self.request[0]
        print('客户消息', data)
        self.request[1].sendto(data.upper(), self.client_address)


if __name__ == '__main__':
    s = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyHandler)
    s.serve_forever()
# 客户端 1 
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议-》udp

while True:
    # msg=input('>>: ').strip() #msg=''
    msg = 'client1111'
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    data, server_addr = client.recvfrom(1024)
    print(data)

client.close()
# 客户端 2 
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议-》udp

while True:
    # msg=input('>>: ').strip() #msg=''
    msg = 'client2222'
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    data, server_addr = client.recvfrom(1024)
    print(data)

client.close()
posted @ 2019-10-21 17:29  Yugaliii  阅读(105)  评论(0编辑  收藏  举报