day 27
day 27
1. 粘包
1.1 什么是粘包
发送端可以是一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算法主要做两件事:
- 只有上一个分组得到确认,才会发送下一个分组
- 收集多个小分组,在一个确认到来时一起发送
Nagle算法造成了发送方可能会出现粘包问题。
-
接收方原因
TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
1.3 发生粘包的两种情况
-
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
# 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'))
-
接收方的缓存机制 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
# 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。
-
按照指定格式将Python数据转换为字符串,该字符串为字节流,如网络传输时,不能传输int,此时先将int转化为字节流,然后再发送;
-
按照指定格式将字节流转换为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()