黏包的解决方法
-
方案一
解决黏包的问题方法在于先找到问题的根源,接收端不知道发送端要发送多长的字节流长度,所以需要在发送端在发送数据之前,将要发送字节流的长度告知接收端,接收端用一个死循环接收所有的数据。
server端import socket, subprocess ip_port = ('127.0.0.1', 8080) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn, addr = s.accept() print('客户端', addr) while True: msg = conn.recv(1024) if not msg: break res = subprocess.Popen(msg.decode('gbk'), shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) err = res.stderr.read() if err: ret = err else: ret = res.stdout.read() data_length = len(ret) conn.send(str(data_length).encode('gbk')) data = conn.recv(1024).decode('gbk') if data == 'recv_ready': conn.sendall(ret) conn.close()
client端
# _*_coding:utf-8_*_ import socket, time s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) res = s.connect_ex(('127.0.0.1', 8080)) while True: msg = input('>>: ').strip() if len(msg) == 0: continue if msg == 'quit': break s.send(msg.encode('gbk')) length = int(s.recv(1024).decode('gbk')) s.send('recv_ready'.encode('gbk')) send_size = 0 recv_size = 0 data = b'' while recv_size < length: data += s.recv(1024) recv_size += len(data) print(data.decode('gbk'))
但是这样做的话也会存在一定的问题,就是程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
-
方案二
方案一是多了一次发送请求,现在我们可以借助一个模块struct,帮我们解决这个问题,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。
该模块可以把一个类型,如数字,转成固定长度的bytes>>> struct.pack('i',1111111111111) struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
-
远程执行命令的程序
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。发送时 接收时 先发送struct转换好的数据长度4字节 先接受4个字节使用struct转换成数字来获取要接收的数据长度 再发送数据 再按照长度接收数据 server端
import socket, subprocess,struct ip_port = ('127.0.0.1', 8080) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn, addr = s.accept() print('客户端', addr) while True: msg = conn.recv(1024) if not msg: break res = subprocess.Popen(msg.decode('gbk'), shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) err = res.stderr.read() if err: ret = err else: ret = res.stdout.read() data_length = len(ret) num = struct.pack('i',data_length) conn.send(num) conn.sendall(ret) conn.close()
client端
# _*_coding:utf-8_*_ import socket, time,struct s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) res = s.connect_ex(('127.0.0.1', 8080)) while True: msg = input('>>: ').strip() if len(msg) == 0: continue if msg == 'quit': break s.send(msg.encode('gbk')) num = s.recv(4) length = struct.unpack('i',num)[0] send_size = 0 recv_size = 0 data = b'' while recv_size < length: data += s.recv(1024) recv_size += len(data) print(data.decode('gbk'))
-
文件上传小案例来了解struct
我们还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)发送时 接收时 先发报头长度 先收报头长度,用struct取出来 再编码报头内容然后发送 根据取出的长度收取报头内容,然后解码,反序列化 最后发真实内容 从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容 import socket import struct import json sk = socket.socket() sk.bind(('127.0.0.1',8088)) sk.listen() buffer = 1024 conn,addr = sk.accept() # 先收报头4个bytes,得到报头长度的字节格式 head_len = conn.recv(4) # 提取报头的长度 head_len = struct.unpack('i',head_len)[0] # 按照报头长度x,收取报头的bytes格式 json_head = conn.recv(head_len).decode('utf-8') # 提取报头 head = json.loads(json_head) filesize = head['filesize'] with open(head['filename'],'wb') as f: while filesize: if filesize >= buffer: content = conn.recv(buffer) f.write(content) filesize -= buffer else: content = conn.recv(filesize) f.write(content) break conn.close() sk.close()
client端
import socket import os import json import struct sk = socket.socket() sk.connect(('127.0.0.1',8088)) buffer = 1024 # 为了黏包,必须自定制报头 header = {'filepath':r'C:\Users\xxxxxx\Desktop','filename':r'xxx.txt','filesize':None} file_path = os.path.join(header['filepath'],header['filename']) file_size = os.path.getsize(file_path) header['filesize'] = file_size # 将字典类型转换成字符串类型 json_header = json.dumps(header) # 为了该报头能传送,需要序列化并且转为bytes bytes_header = json_header.encode('utf-8') header_len = len(bytes_header) # 为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节 pack_len = struct.pack('i',header_len) # 先发报头的长度,4个bytes sk.send(pack_len) # 再发报头的字节格式 sk.send(bytes_header) with open(file_path,'rb') as f: while file_size: if file_size >= buffer: content = f.read(buffer) # 然后发真实内容的字节格式 sk.send(content) file_size -= buffer else: content = f.read(file_size) sk.send(content) break sk.close()
socket的更多方法介绍
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据
s.sendall() 发送TCP数据
s.recvfrom() 接收UDP数据
s.sendto() 发送UDP数据
s.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
- socket.send()和socket.sendall()的用法
- send()的返回值是发送的字节数量,这个数量值可能小于要发送的string的字节数,也就是说可能无法发送string中所有的数据。如果有错误则会抛出异常。
- 尝试发送string的所有数据,成功则返回None,失败则抛出异常。
代码一和代码二是等价的# 代码一 sock.sendall('Hello world\n') # 代码二 buffer = 'Hello world\n' while buffer: bytes = sock.send(buffer) buffer = buffer[bytes:]
验证客户端链接的合法性
如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么利用hmac+加盐的方式来实现
server端
import socket
import os
import hmac
sercet_key = b'tm'
sk = socket.socket()
sk.bind(('127.0.0.1',8088))
sk.listen()
def check_hf(conn):
msg = os.urandom(32)
conn.send(msg)
h = hmac.new(sercet_key,msg)
digest = h.digest()
client_digest = conn.recv(1024)
return hmac.compare_digest(digest,client_digest)
conn,addr = sk.accept()
ret = check_hf(conn)
if ret:
print("合法的客户端请求")
else:
print("不合法的客户端请求")
conn.close()
sk.close()
client端
import socket
import hmac
sercet_key = b'tm'
sk = socket.socket()
sk.connect(('127.0.0.1',8088))
msg = sk.recv(1024)
h = hmac.new(sercet_key,msg)
digest = h.digest()
sk.send(digest)
sk.close()
socketserver
server端
import socketserver
class Myserver(socketserver.BaseRequestHandler):
def handle(self):
self.data = self.request.recv(1024).strip()
print("{} wrote:".format(self.client_address[0]))
print(self.data)
self.request.sendall(self.data.upper())
if __name__ == "__main__":
HOST, PORT = "127.0.0.1", 9999
# 设置allow_reuse_address允许服务器重用地址
socketserver.TCPServer.allow_reuse_address = True
# 创建一个server, 将服务地址绑定到127.0.0.1:9999
server = socketserver.TCPServer((HOST, PORT),Myserver)
# 让server永远运行下去,除非强制停止程序
server.serve_forever()
client端
import socket
HOST, PORT = "127.0.0.1", 9999
data = "hello"
# 创建一个socket链接,SOCK_STREAM代表使用TCP协议
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((HOST, PORT)) # 链接到客户端
sock.sendall(bytes(data + "\n", "utf-8")) # 向服务端发送数据
received = str(sock.recv(1024), "utf-8")# 从服务端接收数据
print("Sent: {}".format(data))
print("Received: {}".format(received))