黏包问题

这样一个实例

import socket
import subprocess sk_server = socket.socket() # 创建 socket对象
sk_server.bind(('localhost', 8080)) # 建立socket
sk_server.listen(5) # 开启监听
conn, addr = sk_server.accept() # 接收客户端信息
while True:
command = conn.recv(1024).decode()
cmd_res = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) # 执行命令
stdout = cmd_res.stdout.read()
stderr = cmd_res.stderr.read()
result = stdout if stdout else stderr
print('result:', result)
conn.sendall(result) # 发送命令结果

server.py

import socket

sk_client = socket.socket()
sk_client.connect(('localhost', 8080))
while True:
cmd = input('>>>').strip()
if not cmd: continue
sk_client.sendall(cmd.encode())
result = sk_client.recv(1024).decode('gbk')
print(result)

client.py

运行起来,我们在客户端输入 tasklist (windows查看所有进程),然后在输入 dir(查看当前目录信息)

执行完 tasklist 后,再次执行 dir 时,发现输出结果是 tasklist 未显示出来的部分。这种情况,就称之为 黏包。

【注意:只有TCP有粘包现象,UDP永远不会粘包】

黏包成因

tcp协议的拆包机制:

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。 大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。

面向流的通信特点和Nagle算法
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

udp和tcp一次发送数据长度的限制

    用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送) 

    用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。

补充说明

会发生黏包的两种情况:

1. 发送方的缓存机制:发送端需要等待缓冲区满才发送出去,造成黏包
2. 接收方的缓存机制:接收方不能及时接收缓冲区的包,造成多个包接收

总结:

黏包现象只发生在tcp协议中:
1. 从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信特点;
2. 实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

黏包的解决方案:

黏包的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决黏包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知道,然后接收端来一个死循环收完所有数据。

解决方案一:

import socket
import subprocess sk_server = socket.socket()
sk_server.bind(('localhost', 8080))
sk_server.listen(5)
conn, addr = sk_server.accept() while True:
command = conn.recv(1024).decode()
cmd_res = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout = cmd_res.stdout.read()
stderr = cmd_res.stderr.read()
result = stdout if stdout else stderr
res_size = len(result)
conn.sendall(str(res_size).encode())
response = conn.recv(1024)
conn.sendall(result)

server.py

import socket

sk_client = socket.socket()
sk_client.connect(('localhost', 8080)) while True:
command = input('>>>').strip()
if not command: continue
sk_client.sendall(command.encode())
res_size = sk_client.recv(1024).decode()
sk_client.sendall(b'')
revice_size = 0
while revice_size != int(res_size):
data = sk_client.recv(1024)
revice_size += len(data)
print(data.decode('gbk'))

client.py

使用这种方式存在的问题:
    程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用 send 去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

解决方案二:

使用 struct模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接收的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。

struct 模块

该模块可以把一个类型,如数字,转成固定长度的bytes

In [1]: import struct

In [2]: s = struct.pack('i', 111111)    # 使用 pack 方法将int类型转换为固定的 4 个字节

In [3]: s
Out[3]: b'\x07\xb2\x01\x00' In [4]: len(s) # 固定的 4 个字节
Out[4]: 4 In [5]: struct.unpack('i', s) # 使用 unpack 方法将 4 个字节还原为字符,类型为元组
Out[5]: (111111,)

使用 struct 解决黏包

借助 struct 模块,我们知道长度数字可以被转换成一个标准大小的 4 个字节数字。因此可以利用这个特点来预先发送数据长度。

import socket, struct
import subprocess sk_server = socket.socket()
sk_server.bind(('localhost', 8080))
sk_server.listen(5)
conn, addr = sk_server.accept()
while True:
command = conn.recv(1024).decode()
res_cmd = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout = res_cmd.stdout.read()
stderr = res_cmd.stderr.read()
result = stdout if stdout else stderr
res_size = len(result)
conn.sendall(struct.pack('i', res_size))
conn.sendall(result)

server.py

import socket
import struct sk_client = socket.socket()
sk_client.connect(('localhost', 8080)) while True:
command = input('>>>').strip()
if not command: continue
sk_client.sendall(command.encode())
res = sk_client.recv(4)
res_size = struct.unpack('i', res)[0]
print(res_size)
revice_size = 0
while revice_size != res_size:
data = sk_client.recv(1024)
revice_size += len(data)
print(data.decode('gbk'))

client.py

这里还可以将报头做成字典字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用 struct 将序列化后的数据长度打包成4个字节.

这种方式用于需要初始化较多的信息

import socket, struct, json
import subprocess sk_server = socket.socket()
sk_server.bind(('localhost', 8080))
sk_server.listen(5) conn, addr = sk_server.accept()
while True:
command = conn.recv(1024).decode()
cmd_res = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout = cmd_res.stdout.read()
stderr = cmd_res.stderr.read()
result = stdout if stdout else stderr
headers = {'res_size': len(result)} # 将head信息组合成 字典类型
head_json = json.dumps(headers) # 转换为 json 类型
head_json_bytes = bytes(head_json, encoding='utf-8')
conn.send(struct.pack('i', len(head_json_bytes))) # 首先发送 head 信息的大小
conn.send(head_json_bytes) # 再次发送 head 信息
conn.send(result) # 最后 发送 执行命令的结果集合

server.py

import socket, struct, json

sk_client = socket.socket()
sk_client.connect(('localhost', 8080))
while True:
cmd = input('>>>').strip()
if not cmd: continue
sk_client.send(cmd.encode())
res_size = struct.unpack('i', sk_client.recv(1024))[0] # 首先获取 head 大小
head_json = sk_client.recv(res_size).decode() # 通过 head 大小获取 head 信息
head_dict = json.loads(head_json) # 转为 字典 类型
data_len = head_dict['res_size'] # 取出 结果集 大小
revice_size = 0
while revice_size != data_len: # 循环接收 结果集
data = sk_client.recv(1024)
revice_size += len(data)
print(data.decode('gbk'))

client.py

FTP小作业:上传下载文件

import socket
import json
import struct
import os class MyServer: request_queue_size = 5 def __init__(self, ip_port, bind_activate=True):
self.socket = socket.socket()
self.ip_port = ip_port
if bind_activate:
try:
self.activate()
except:
self.server_close()
raise def activate(self):
self.socket.bind(self.ip_port)
self.socket.listen(self.request_queue_size) def get_resquest(self):
return self.socket.accept() def server_close(self):
self.socket.close() def run(self):
while True:
self.conn, self.client_addr = self.get_resquest()
print('from client:', self.client_addr)
while True:
try:
head_struct = self.conn.recv(4)
head_len = struct.unpack('i', head_struct)[0]
head_json = self.conn.recv(head_len).decode()
head_dict = json.loads(head_json)
command = head_dict['command']
if hasattr(self, command):
func = getattr(self, command)
func(head_dict)
except Exception:
break def put(self, args):
filename = args['filename']
file_size = args['file_size']
file_path = os.path.join('file_upload', filename)
recv_size = 0
with open(file_path, 'wb') as f:
while recv_size != file_size:
recv_data = self.conn.recv(1024)
recv_size += len(recv_data)
f.write(recv_data) if __name__ == '__main__':
ftp_server = MyServer(('localhost', 8080))
ftp_server.run()

server.py

import socket
import json, struct
import os, sys class MyClient:
def __init__(self, ip_port, connect=True):
self.socket = socket.socket()
self.ip_port = ip_port
if connect:
try:
self.connect()
except:
self.client_close()
raise def connect(self):
self.socket.connect(self.ip_port) def client_close(self):
self.socket.close() def run(self):
while True:
cmd = input('>>>').strip()
if not cmd: continue
cmd_str = cmd.split()[0]
if hasattr(self, cmd_str):
func = getattr(self, cmd_str)
func(cmd) def put(self, command):
if len(command) > 1:
filename = command.split()[1]
if os.path.isfile(filename):
cmd_str = command.split()[0]
file_size = os.path.getsize(filename)
head_dict = {'command': cmd_str, 'filename': filename, 'file_size': file_size}
head_json = json.dumps(head_dict)
head_json_bytes = bytes(head_json, encoding='utf-8')
head_json_strcut = struct.pack('i', len(head_json_bytes))
print(head_json_strcut)
self.socket.send(head_json_strcut)
self.socket.send(head_json_bytes)
with open(filename, 'rb') as f:
while True:
data = f.read(1024)
send_size = f.tell()
if not data:
print('upload successful.')
break
self.socket.send(data)
self.__progress(send_size, file_size, '上传中')
else:
print('\033[31;1m文件不存在.\033[0m') else:
print('\033[31;1m命令格式错误.\033[0m') def __progress(self, trans_size, file_size, mode):
bar_length = 100
percent = float(trans_size) / float(file_size)
hashes = '=' * int(percent * bar_length)
spaces = ' ' * int(bar_length - len(hashes))
sys.stdout.write('\r%s %.2fM/%.2fM %d%% [%s]'
%(mode, trans_size/1048576, file_size/1048576, percent*100, hashes+spaces)) if __name__ == '__main__':
ftp_client = MyClient(('localhost', 8080))
ftp_client.run()

client.py

socketserver 模块

主要类型

该模块有4个比较主要的类,其中常用的是 TCPServer 和 UDPServer

  1. TCPServer
  2. UDPServer
  3. UnixStreamServer: 类似于TCPServer提供面向数据流的套接字连接,但是旨在UNIX平台上可用;
  4. UnixDatagramServer: 类似于UDPServer提供面向数据报的套接字连接,但是旨在UNIX平台上可用;

这四个类型同步地处理请求,也就是说一个请求没有完成之前是不会处理下一个请求的,这种模式当然不适合生产环境,一个客户端连接就可能拖延所有的执行。所以这个模块还提供了两种支持异步处理的类:

  1. ForkingMixIn: 为每一个客户端请求派生一个新的进程专门处理;
  2. ThreadingMixIn: 为每一个客户端请求派生一个新的线程专门处理;

继承自这两个类型的服务端在处理新的客户端连接时不会阻塞,而是创建新的进/线程专门处理客户端请求。

编程框架

首先从高层面介绍一下使用SocketServer模块开发多进程/线程 异步服务器的流程:

  1. 根据需要选择一个合适的服务类型,如,面向TCP连接的多进程服务器: ForkingTCPServer ;
  2. 创建一个请求处理器(request handler)类型,这个类型的 handle()(类似于回调函数)方法中定义如何处理到达的客户端连接。
  3. 实例化服务器,传入服务器绑定的地址和第2步定义的请求处理器类;
  4. 调用服务器实例的 handle_request() 或 serve_forever() 方法,一次或多次处理客户请求。

使用 socketserver 实例:

import socketserver

class MyServer(socketserver.BaseRequestHandler):
def handle(self):
while True:
self.data = self.request.recv(1024).decode()
print('from client:', self.client_address)
print(self.data)
self.request.send(self.data.upper().encode()) if __name__ == '__main__':
HOST, PORT = 'localhost', 8080
server = socketserver.ThreadingTCPServer((HOST, PORT), MyServer)
server.serve_forever()

server.py

import socket

class MyClient:
def __init__(self, ip_port, connect=True):
self.client = socket.socket()
self.ip_port = ip_port
if connect:
try:
self.connect()
except:
self.client_close()
raise def connect(self):
self.client.connect(self.ip_port) def client_close(self):
self.client.close() def start(self):
while True:
cmd = input('>>>').strip()
if not cmd: continue
self.client.send(cmd.encode())
cmd_upper = self.client.recv(1024).decode()
print(cmd_upper) if __name__ == '__main__':
client = MyClient(('localhost', 8080))
client.start()

client.py

[ python ] 网络编程(2)的更多相关文章

  1. Python 网络编程(二)

    Python 网络编程 上一篇博客介绍了socket的基本概念以及实现了简单的TCP和UDP的客户端.服务器程序,本篇博客主要对socket编程进行更深入的讲解 一.简化版ssh实现 这是一个极其简单 ...

  2. Python 网络编程(一)

    Python 网络编程 socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. ...

  3. Python学习(22)python网络编程

    Python 网络编程 Python 提供了两个级别访问的网络服务.: 低级别的网络服务支持基本的 Socket,它提供了标准的 BSD Sockets API,可以访问底层操作系统Socket接口的 ...

  4. Day07 - Python 网络编程 Socket

    1. Python 网络编程 Python 提供了两个级别访问网络服务: 低级别的网络服务支持基本的 Socket,它提供了标准的 BSD Sockets API,可以访问底层操作系统Socket接口 ...

  5. python网络编程-01

    python网络编程 1.socket模块介绍 ①在网络编程中的一个基本组件就是套接字(socket),socket是两个程序之间的“信息通道”. ②套接字包括两个部分:服务器套接字.客户机套接字 ③ ...

  6. 《Python网络编程》学习笔记--使用谷歌地理编码API获取一个JSON文档

    Foundations of Python Network Programing,Third Edition <python网络编程>,本书中的代码可在Github上搜索fopnp下载 本 ...

  7. Python网络编程基础pdf

    Python网络编程基础(高清版)PDF 百度网盘 链接:https://pan.baidu.com/s/1VGwGtMSZbE0bSZe-MBl6qA 提取码:mert 复制这段内容后打开百度网盘手 ...

  8. python 网络编程(Socket)

    # from wsgiref.simple_server import make_server## def RunServer(environ,start_response):# start_resp ...

  9. python 网络编程 IO多路复用之epoll

    python网络编程——IO多路复用之epoll 1.内核EPOLL模型讲解     此部分参考http://blog.csdn.net/mango_song/article/details/4264 ...

  10. 自学Python之路-Python网络编程

    自学Python之路-Python网络编程 自学Python之路[第一回]:1.11.2 1.3

随机推荐

  1. DjangoORM使用mysql注意

    注意事项1:需要在project下的setting里面做设置.让Django生成MySQL类型的数据库. 注意事项2:在Django内部,连MySQL的时候,需要添加下面2句代码: 4.******* ...

  2. Qt Widgets、QML、Qt Quick的区别

    Qt Widgets.QML.Qt Quick的区别 简述 看了之前关于 QML 的一些介绍,很多人难免会有一些疑惑: Q1:QML 和 Qt Quick 之间有什么区别? Q2:QtQuick 1. ...

  3. Windows用户相关操作

    获取所有用户 NET_API_STATUS NetUserEnum( LPCWSTR servername, DWORD level, DWORD filter, LPBYTE* bufptr, DW ...

  4. Zend Hash table 详解--转

    原文地址:http://www.phppan.com/2009/12/zend-hashtable/ 在PHP的Zend引擎中,有一个数据结构非常重要,它无处不在,是PHP数据存储的核心,各种常量.变 ...

  5. Android开发技术重要参考资料

    只言片语 有的时候看不懂别人的代码,觉得自己笨:其实,你想多了,看不懂不是因为你蠢而是别人的代码写得烂:所以,别那么宽容别人却苛责自己. 参考资料 郭霖 way 爱哥 有心 胡凯 robin trin ...

  6. python基础----模块、包

    一 模块                                                                                                 ...

  7. ORACLE获取某个时间段之间的月份列表

    返回1-31,或者1-12,或者某个 select rownum   from dual   connect by rownum<31 就是connect by http://marcospri ...

  8. Codeforces 894.D Ralph And His Tour in Binary Country

    D. Ralph And His Tour in Binary Country time limit per test 2.5 seconds memory limit per test 512 me ...

  9. jquery动态添加的元素绑定的事件不生效的问题

    我们可以通过 $(document).on('click', '#xxx', callback) 这种形式解决. 原因,一般情况下,我们是通过 $('#xxx').click(callback) 这种 ...

  10. bzoj 1030 AC自动机+dp

    代码: //先把给的单词建AC自动机并且转移fail,然后d[i][j]表示构造的文章到第i位时处在字典树的第j个节点的不包含单词的数量,最后用总的数量26^m //-d[m][0~sz]即可.其中不 ...