为什么会产生粘包问题
只有tcp有粘包的现象,udp永远不会有粘包
两种情况下会发生粘包。
1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
socket收发消息的原理
socket客户端向socket服务端发送消息,首先由客户端把消息发送给操作系统的缓存,由操作系统调用网卡发送给对方操作系统的缓存,
服务端的应用程序会从操作系统的缓存中接收数据。无论是发数据还是收数据,都是跟自己操作系统的缓存打交道。
所以说并不是一个send对应一个recv。
tcp协议的Nagle算法:
tcp协议会将数据量比较小,并且发送间隔比较短的多个数据合成一个数据块,进行封包一次性发送,这样可以提高效率。
这样,接收端就难于分辨出来了,所以可能会触发粘包问题。
struct模块的使用
import struct
header = struct.pack("i", 12312312) # struct.pack可以将一个整型打包成bytes类型,并且bytes固定长度为4个字节
print(header) # b'\xf8\xde\xbb\x00'
print(len(header)) # 4
total_size = struct.unpack("i", header)[0] # struct.unpack会将打包好的bytes类型,反解成打包前的整型数字(12312312,)
print(total_size) # 12312312
使用struct.pack中的“i”将int类型打包成bytes类型,数字有长度的限制,bytes的固定长度为4个字节,
使用struct.pack中的“q”将int类型打包成bytes类型,固定长度会增加一些,bytes的固定长度为8个字节
tcp自定义协议解决粘包问题代码展示
导入struct模块将int类型打包成bytes类型
导入struct模块将bytes类型反解成int类型
客户端
import socket
import struct
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8080))
while True:
cmd = input("shu ru").strip()
if len(cmd) == 0:
continue
client.send(cmd.encode("utf-8"))
# 先接收数据的长度
header = client.recv(4)
total_size = struct.unpack("i", header)[0] # struct.unpack会将打包好的bytes类型,反解成打包前的整型数字
# 再接收真正的数据
recv_size = 0
res = b""
while recv_size < total_size: # 循环取值,直到把total_size取空为止
data = client.recv(1024) # 接收服务端传过来的数据bytes类型
recv_size += len(data) # 统计传过来的数据长度
res += data # 统计服务端过来的bytes类型的数据
print(res.decode("gbk"))
服务端
import socket
import subprocess
import struct
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)
while True:
conn, client_addr = server.accept()
print(client_addr)
while True:
try:
data = conn.recv(1024)
if len(data) == 0:
break
cmd = data.decode("utf-8")
obj = subprocess.Popen(cmd, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
res1 = obj.stdout.read()
res2 = obj.stderr.read()
total_size = len(res1) + len(res2)
# 先把数据的长度发过去
header = struct.pack("i", total_size) # struct.pack可以将一个total_size打包成bytes类型,并且bytes固定长度为4个字节
conn.send(header) # 数据的长度
# 再发送真正的数据
conn.send(res1)
conn.send(res2)
except Exception:
break
conn.close()
tcp自定义协议解决粘包问题升级版代码展示
导入报头说明,形式为字典,导入struct模块进行数据类型的转换,导入json模块进行编码与解码
客户端
粘包问题升级版
import socket
import struct
import json
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8080))
while True:
cmd = input("shu ru").strip()
if len(cmd) == 0:
continue
client.send(cmd.encode("utf-8"))
# 先收4个字节,提取header_bytes的长度
header_bytes_len = struct.unpack("i", client.recv(4))[0]
# 再收报头字典header——bytes,提取header_dic
header_bytes = client.recv(header_bytes_len)
header_json = header_bytes.decode("utf-8")
header_dic = json.loads(header_json)
print(header_dic)
total_size = header_dic["total_size"]
# 再接收真正的数据
recv_size = 0
res = b""
while recv_size < total_size: # 循环取值,直到把total_size取空为止
data = client.recv(1024) # 接收服务端传过来的数据bytes类型
recv_size += len(data) # 统计传过来的数据长度
res += data # 统计服务端传过来的bytes类型的数据
print(res.decode("gbk"))
服务端
粘包问题升级版
import socket
import subprocess
import struct
import json
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 8080))
server.listen(5)
while True:
conn, client_addr = server.accept()
print(client_addr)
while True:
try:
data = conn.recv(1024)
if len(data) == 0:
break
cmd = data.decode("utf-8")
obj = subprocess.Popen(cmd, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
res1 = obj.stdout.read()
res2 = obj.stderr.read()
header_dic = {
"filename": "a.txt",
"total_size": len(res1) + len(res2),
"md5": "55lala55"
}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode("utf-8")
# 先发4个字节
conn.send(struct.pack("i", len(header_bytes)))
# 再发报头字典
conn.send(header_bytes)
# 最后再发送真正的数据
conn.send(res1)
conn.send(res2)
except Exception:
break
conn.close()
udp协议的使用
udp是无链接的,先启动哪一端都不会报错
udp协议只收本次的收的最大范围,多余的数据会直接丢失,没有tcp协议的缓存与粘包这些特性,udp协议又被称为不可靠协议
客户端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # udp协议又称为数据报
while True:
msg = input("shuru").strip()
client.sendto(msg.encode("utf-8"), ("127.0.0.1", 8080))
data, client_addr = client.recvfrom(1024)
print(data.decode("utf-8"))
client.close()
服务端
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # udp协议又称为数据报
server.bind(("127.0.0.1", 8080))
while True:
conn, client_addr = server.recvfrom(1024)
print(conn.decode("utf-8"))
server.sendto(conn.upper(), client_addr)
server.close()
并发与并行
并发:是伪并行,即看起来是同时运行。单个cpu+多道技术就可以实现并发,(并行也属于并发)
并行:同时运行,只有具备多个cpu才能实现并行
tcp协议下并发功能实现的代码
客户端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8080))
while True:
msg = input("shu ru")
if len(msg) == 0:
continue
client.send(msg.encode("utf-8"))
data = client.recv(1024)
print(data.decode("utf-8"))
socket.close()
服务端
import socketserver
class MyRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
while True:
try:
data = self.request.recv(1024) # self.request代表conn,表示连接通道
print(data.decode("utf-8"))
if len(data) == 0:
break
self.request.send(data.upper())
except Exception:
break
self.request.close()
server = socketserver.ThreadingTCPServer(("127.0.0.1", 8080), MyRequestHandler)
server.serve_forever()
udp协议下并发功能实现的代码
客户端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
msg = input("shuru").strip()
client.sendto(msg.encode("utf-8"), ("127.0.0.1", 8080))
data, server_addr = client.recvfrom(1024)
print(data.decode("utf-8"))
服务端
客户端每发来一条消息,都会起一个线程,会把客户端发来的消息封装到对象里面去,交给一个线程来运行handle方法
import socketserver
class MyRequestHanlder(socketserver.BaseRequestHandler):
def handle(self):
data, server = self.request # data代表客户端发来的数据,server是套接字对象
server.sendto(data.upper(), self.client_address) # 服务端给客户端回消息
server = socketserver.ThreadingUDPServer(("127.0.0.1", 8080), MyRequestHanlder) # 这行代码等同于定义了一个套接字对象,绑定了ip跟端口
server.serve_forever() # 这行代码代表了循环给客户端收发消息