一、socket缓冲区

研究粘包之前先看看socket缓冲区的问题:

二、socket缓存区的详细解释

每个socket被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

TCP协议独立于write()/send()函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

read()/recv()函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

这些I/O缓冲区特性可整理如下:
1.I/O缓冲区在每个TCP套接字中单独存在;
2.I/O缓冲区在创建套接字时自动生成;
3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
4.关闭套接字将丢失输入缓冲区中的数据。

输入输出缓冲区的默认大小一般都是8K,可以通过getsockopt()函数获取

须知:只有TCP有粘包现象,UDP永远不会粘包!

具体原因:
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。

而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看来,根本不知道该文件的字节流从何处开始,在何处结束

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

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

TCP和UDP的区别

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个包更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

解决粘包需要注意的问题

tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头。

udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠

tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

三、两种情况下会发生粘包。

1、接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

服务端

import socket
import subprocess phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5) while 1: # 循环连接客户端
conn, client_addr = phone.accept()
print(client_addr) while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
conn.send(correct_msg + error_msg)
except ConnectionResetError:
break conn.close()
phone.close()

客户端

import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 买电话
phone.connect(('127.0.0.1',8080)) # 与客户端建立连接, 拨号 while 1:
cmd = input('>>>')
phone.send(cmd.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('gbk'))
phone.close()
# 由于客户端发的命令获取的结果大小已经超过1024,那么下次在输入命令,会继续取上次残留到缓存区的数据。

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

服务端

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5) conn, client_addr = phone.accept()
frist_data = conn.recv(1024)
print('1:',frist_data.decode('utf-8')) # 1: helloworld
second_data = conn.recv(1024)
print('2:',second_data.decode('utf-8')) conn.close()
phone.close()

客户端

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
phone.send(b'hello')
phone.send(b'world') phone.close()
# 两次返送信息时间间隔太短,数据小,造成服务端一次收取

四、粘包的解决方案

粘包问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕如何让发送端在发送数据前,把自己将要发送的字节流总数按照固定字节发送给接收端,后面跟上总数据,然后接收端先接收固定字节的总字节流,就可以知道需要接收的数据长度,再来一个死循环接收完所有数据。

方案一、低端版

服务端:

import socket
import subprocess
import struct server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8082))
server.listen(5) while 1:
conn, addr = server.accept()
print(conn, addr) while 1:
try:
cmd = conn.recv(1024)
obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
ret = obj.stdout.read()
ret_err = obj.stderr.read()
total_size = len(ret+ret_err)
total_size_bytes = struct.pack('i', total_size )
# print(total_size)
conn.send(total_size_bytes)
conn.send(ret)
conn.send(ret_err) except Exception:
break
conn.close()
server.close()

客户端:

import socket
import struct client = socket.socket()
client.connect(('127.0.0.1', 8082)) while 1:
msg = input('>>>').strip()
if not msg:continue
elif msg.upper() == 'Q':break
client.send(msg.encode('utf-8'))
header_size_bytes = client.recv(4)
header_size =struct.unpack('i', header_size_bytes)[0] client_recv_data = b''
size = 0
while size < header_size:
data = client.recv(1024)
client_recv_data += data
size += len(data) rst = client_recv_data.decode('gbk')
print(rst) client.close()

结果:

服务端显示
<socket.socket fd=236, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8082), raddr=('127.0.0.1', 14106)> ('127.0.0.1', 14106)
931
1790
5040
7802
8130 客户端:
执行dir,ipconfig,tasklist,netstat -an等命令都可以正常显示

这种方法当文件较小时可以使用,当文件太大时,total_size_bytes = struct.pack('i', total_size )也无法表示

方案二、高端版(可自定制报头版)

整体的流程解释:

我们可以把报头做成字典,字典里包含将要发送的真实数据的描述信息(大小之类的),然后json序列化,然后用struct将序列化后的数据长度打包成4个字节。
我们在网络上传输的所有数据都叫做数据包,数据包里的所有数据都叫做报文,报文里面不止有数据,还有ip地址、mac地址、端口号等等,其实所有的报文都有报头,这个报头是协议规定的。 发送时:
先发报头长度
再发报头,需要编码报头内容,其中包括数据的长度
最后发真实数据内容 接收时:
先接收报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待接收数据的描述信息(数据长度),然后再接收真实的数据内容

服务端

import socket
import subprocess
import struct
import json server_ip = ('192.168.10.1', 8088)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(server_ip)
server_socket.listen(5) while 1:
conn, ipaddr = server_socket.accept()
print(conn, ipaddr) while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,)
msg = ret.stdout.read()
err_msg = ret.stderr.read() # 1 数据总长度
total_size = len(msg) + len(err_msg)
# 2 制作报头字典
header_dict = {
'md5': 'fdsaf2143254f',
'file_name': 'f1.txt',
'total_size':total_size,
}
# 3 将报头字典转换成json再转换成bytes格式
header_dict_json = json.dumps(header_dict)
header_dict_bytes = header_dict_json.encode('utf-8')
# 4 将报头长度转换成固定长度的struct格式
header_dict_size = len(header_dict_bytes)
header_dict_size_struct = struct.pack('i', header_dict_size)
# 5 发送报头长度
conn.send(header_size_struct)
# 6 发送报头
conn.send(header_dict)
# 7 发送真实数据:
conn.send(msg)
conn.send(error_msg)
except ConnectionResetError:
break conn.close()
phone.close()

客户端

import socket
import struct
import json
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_ip = ('192.168.10.1', 8088)
client_socket.connect(server_ip) while 1:
cmd = input('>>>')
if not cmd:continue
elif cmd.strip().upper() == 'Q':break
client_socket.send(cmd.encode('utf-8')) # 1,接收固定报头长度的struct格式
header_size_struct = client_socket.recv(4) #2,将struct格式的报头长度转换成int类型
head_size = struct.unpack('i', header_size_struct)[0] #3,接收报头的字典的bytes类型,也就是json格式
header_bytes = client_socket.recv(head_size) #4,将报头由bytes类型解码成字典
header_dict = json.loads(header_bytes.decode('utf-8')) #5,从字典中获取数据总大小
total_size = header_dict['total_size'] #6,根据报头信息,循环接收真实数据
recv_size = 0
msg = b''
while recv_size < total_size:
recv_data = client_socket.recv(1024)
msg += recv_data
recv_size += len(recv_data)
#由于msg是服务器在windows下执行命令的结果,默认编码为gbk,所以客户端显示需要使用gbk解码
print(msg.decode('gbk')) client_socket.close()

五、例子:FTP上传下载文件(简单版)

FTP上传下载文件(简单版)

server端

import socket
import struct
import json
sk = socket.socket()
# buffer = 4096 # 当双方接收发送的大小比较大的时候,比如4096,就会丢数据,改小了就ok,在linux上也是ok的。
buffer = 1024 #每次接收数据的大小
sk.bind(('127.0.0.1',8081))
sk.listen() conn,addr = sk.accept()
print(conn, addr)
#接收文件头部大小,获取文件大小值,接收文件
head_len = conn.recv(4)
head_len = struct.unpack('i',head_len)[0] #解包
json_head = conn.recv(head_len).decode('utf-8') #反序列化
head = json.loads(json_head)
filesize = head['filesize'] with open(head['filename'],'wb') as f:
recv_size = 0
while recv_size < filesize:
content = conn.recv(buffer)
f.write(content)
recv_size += len(content)
print('文件名称:%s,文件大小:%s,已接收%s'%(head['filename'], filesize, recv_size))
conn.close()
sk.close()

client端

import os
import json
import socket
import struct
sk = socket.socket()
sk.connect(('127.0.0.1',8081))
buffer = 1024 #读取文件的时候,每次读取的大小
head = {
'filepath':r'G:\share#需要下载的文件路径,也就是文件所在的文件夹
'filename':'1.py', #改成上面filepath下的一个文件
'filesize':None,
} file_path = os.path.join(head['filepath'],head['filename'])
filesize = os.path.getsize(file_path)
head['filesize'] = filesize
# json_head = json.dumps(head,ensure_ascii=False) #字典转换成字符串
json_head = json.dumps(head) #字典转换成字符串
bytes_head = json_head.encode('utf-8') #字符串转换成bytes类型 #计算head的长度,因为接收端先接收我们自己定制的报头
head_len = len(bytes_head) #报头长度
pack_len = struct.pack('i',head_len)
sk.send(pack_len) #先发送报头长度
sk.send(bytes_head) #再发送bytes类型的报头 #即便是视频文件,也是可以按行来读取的,也可以readline,也可以for循环,但是读取出来的数据大小就不固定了,影响效率,有可能读的比较小,也可能很大,像视频文件一般都是一行的二进制字节流。
#所有我们可以用read,设定一个一次读取内容的大小,一边读一边发,一边收一边写
with open(file_path,'rb') as f:
send_size = 0
while send_size < filesize:
content = f.read(buffer)
sk.send(content)
send_size += len(content)
print('文件大小:%s,已发送%s'%(filesize, send_size))
sk.close()

FTP上传下载文件(面向对象升级版)

server端

import socket
import struct
import json
import subprocess
import os class MYTCPServer:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
allow_reuse_address = False
max_packet_size = 1024
coding='utf-8'
request_queue_size = 5
server_dir='file_upload' def __init__(self, server_address, bind_and_activate=True):
"""Constructor. May be extended, do not override."""
self.server_address=server_address
self.socket = socket.socket(self.address_family,
self.socket_type)
if bind_and_activate:
try:
self.server_bind()
self.server_activate()
except:
self.server_close()
raise def server_bind(self):
"""Called by constructor to bind the socket.
"""
if self.allow_reuse_address:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(self.server_address)
self.server_address = self.socket.getsockname() def server_activate(self):
"""Called by constructor to activate the server.
"""
self.socket.listen(self.request_queue_size) def server_close(self):
"""Called to clean-up the server.
"""
self.socket.close() def get_request(self):
"""Get the request and client address from the socket.
"""
return self.socket.accept() def close_request(self, request):
"""Called to clean up an individual request."""
request.close() def run(self):
while True:
self.conn,self.client_addr=self.get_request()
print('from client ',self.client_addr)
while True:
try:
head_struct = self.conn.recv(4)
if not head_struct:break head_len = struct.unpack('i', head_struct)[0]
head_json = self.conn.recv(head_len).decode(self.coding)
head_dic = json.loads(head_json) print(head_dic)
#head_dic={'cmd':'put','filename':'a.txt','filesize':123123}
cmd=head_dic['cmd']
if hasattr(self,cmd):
func=getattr(self,cmd)
func(head_dic)
except Exception:
break def put(self,args):
file_path=os.path.normpath(os.path.join(
self.server_dir,
args['filename']
)) filesize=args['filesize']
recv_size=0
print('----->',file_path)
with open(file_path,'wb') as f:
while recv_size < filesize:
recv_data=self.conn.recv(self.max_packet_size)
f.write(recv_data)
recv_size+=len(recv_data)
print('recvsize:%s filesize:%s' %(recv_size,filesize)) tcpserver1=MYTCPServer(('127.0.0.1',8080))
tcpserver1.run()

client端

import socket
import struct
import json
import os class MYTCPClient:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
allow_reuse_address = False
max_packet_size = 8192
coding='utf-8'
request_queue_size = 5 def __init__(self, server_address, connect=True):
self.server_address=server_address
self.socket = socket.socket(self.address_family,
self.socket_type)
if connect:
try:
self.client_connect()
except:
self.client_close()
raise def client_connect(self):
self.socket.connect(self.server_address) def client_close(self):
self.socket.close() def run(self):
while True:
inp=input(">>: ").strip()
if not inp:continue
l=inp.split()
cmd=l[0]
if hasattr(self,cmd):
func=getattr(self,cmd)
func(l) def put(self,args):
cmd=args[0]
filename=args[1]
if not os.path.isfile(filename):
print('file:%s is not exists' %filename)
return
else:
filesize=os.path.getsize(filename) head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
print(head_dic)
head_json=json.dumps(head_dic)
head_json_bytes=bytes(head_json,encoding=self.coding) head_struct=struct.pack('i',len(head_json_bytes))
self.socket.send(head_struct)
self.socket.send(head_json_bytes)
send_size=0
with open(filename,'rb') as f:
for line in f:
self.socket.send(line)
send_size+=len(line)
print(send_size)
else:
print('upload successful') client=MYTCPClient(('127.0.0.1',8080))
client.run()

打印进度条示例

#=========知识储备==========
#进度条的效果
[# ]
[## ]
[### ]
[#### ] #指定宽度
print('[%-15s]' %'#')
print('[%-15s]' %'##')
print('[%-15s]' %'###')
print('[%-15s]' %'####') #打印%
print('%s%%' %(100)) #第二个%号代表取消第一个%的特殊意义 #可传参来控制宽度
print('[%%-%ds]' %50) #[%-50s]
print(('[%%-%ds]' %50) %'#')
print(('[%%-%ds]' %50) %'##')
print(('[%%-%ds]' %50) %'###') #=========实现打印进度条函数==========
import sys
import time def progress(percent,width=50):
if percent >= 1:
percent=1
show_str = ('%%-%ds' % width) % (int(width*percent)*'|')
print('\r%s %d%%' %(show_str, int(100*percent)), end='') #=========应用==========
data_size=1025
recv_size=0
while recv_size < data_size:
time.sleep(0.1) #模拟数据的传输延迟
recv_size+=1024 #每次收1024 percent=recv_size/data_size #接收的比例
progress(percent,width=70) #进度条的宽度70

day41-解决粘包问题的更多相关文章

  1. c# socket 解决粘包,半包

    处理原理: 半包:即一条消息底层分几次发送,先有个头包读取整条消息的长度,当不满足长度时,将消息临时缓存起来,直到满足长度再解码 粘包:两条完整/不完整消息粘在一起,一般是解码完上一条消息,然后再判断 ...

  2. Socket解决粘包问题1

    粘包是指发送端发送的包速度过快,到接收端那边多包并成一个包的现象,比如发送端连续10次发送1个字符'a',因为发送的速度很快,接收端可能一次就收到了10个字符'aaaaaaaaaa',这就是接收端的粘 ...

  3. python3全栈开发-什么是粘包、粘包现象、如何解决粘包

    一.粘包现象 让我们基于tcp先制作一个远程执行命令的程序(1:执行错误命令 2:执行ls 3:执行ifconfig) 注意注意注意: res=subprocess.Popen(cmd.decode( ...

  4. Netty解决粘包和拆包问题的四种方案

    在RPC框架中,粘包和拆包问题是必须解决一个问题,因为RPC框架中,各个微服务相互之间都是维系了一个TCP长连接,比如dubbo就是一个全双工的长连接.由于微服务往对方发送信息的时候,所有的请求都是使 ...

  5. Python开发【socket篇】解决粘包

    客户端 import os import json import struct import socket sk = socket.socket() sk.connect(('127.0.0.1',8 ...

  6. 网络编程基础【day09】:socket解决粘包问题之MD5(八)

    本节内容 1.概述 2.代码实现 一.概述 上一篇博客讲到的用MD5来校验还是用的之前解决粘包的方法,就是客户端发送一个请求,等待服务端的确认的这样的一个笨方法.下面我们用另外一种方法:就是客户端已经 ...

  7. python全栈开发day28-网络编程之粘包、解决粘包,上传和下载的作业

    一.昨日内容回顾 1. tcp和udp编码 2. 自定义mysocket解决编码问题 二.今日内容总结 1.粘包 1)产生粘包原因: (1).接收方不知道消息之间的边界,不知道一次性要取多少字节的数据 ...

  8. Dealing with a Stream-based Transport 处理一个基于流的传输 粘包 即使关闭nagle算法,也不能解决粘包问题

    即使关闭nagle算法,也不能解决粘包问题 https://waylau.com/netty-4-user-guide/Getting%20Started/Dealing%20with%20a%20S ...

  9. python 解决粘包问题

    客户端发送hello,如果服务端 recv(1) ,那只能接收到 h 这一个字符,然后再recv(1) 一下,可以再接收一个 e , 因为客户端发送的结果长,所以只能把其他的先缓存下来,下次recv的 ...

  10. 【python】-- Socket粘包问题 ,解决粘包的几种方法、socket文件下载,md5值检验

    上一篇随笔:“socket 接收大数据”,在win系统上能够运行,并且解决了大数据量的数据传输出现的问题,但是运行在linux系统上就会出现如下图所示的情况: 就是服务端两次发送给客户端的数据(第一次 ...

随机推荐

  1. 知识点-jar包

    JAR文件的全称是Java Archive File,意思就是Java档案文件.通常JAR文件是一种压缩文件,与常见的ZIP压缩文件兼容,同城也被称为JAR包.JAR文件与zip文件的去区别就是在JA ...

  2. go中defer的理解--defer、return、返回值之间执行顺序

    defer可以读取有名返回值 func c() (i int) { defer func() { i++ }() return 1 } 输出结果是2. 在开头的时候,我们知道defer是在return ...

  3. redis问题:redis-server.exe双击闪退 win10系统

    转:https://blog.csdn.net/qq_40361770/article/details/80454248 解决方法: 1-win+R 打开命令行 2-cd至redis目录,例如 D:\ ...

  4. The Kernel Boot Process.内核引导过程

    原文标题:The Kernel Boot Process 原文地址:http://duartes.org/gustavo/blog/ [注:本人水平有限,只好挑一些国外高手的精彩文章翻译一下.一来自己 ...

  5. Android标题头滑动渐变,Titlebar滑动渐变,仿美团饿了么标题头渐变;

    原理就是滑动中改变透明度: 核心代码: rv.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public vo ...

  6. 【Selenium-WebDriver自学】Selenium测试设计技术(十三)

    Selenium页面对象模型 1.Selenium页面对象模型 优点 页面的对象模型是其中测试对象和功能被彼此分开,从而保持代码干净的实现. 对象保持独立的测试脚本.一个目的可以通过一个或多个测试脚本 ...

  7. QNetworkAccessManager post()和get()方法

    GET方式提交的数据最多只能有1024字节,而POST则没有此限制. 大文件传输用post(),小文件用get(), 第一次接触Qt的Http项目,今天看了一下Post和Get的基本使用方法,就开始尝 ...

  8. gentoo wireshark 安装

    安装软件 emerge --ask net-analyzer/wireshark 把用户加入 wireshark 组. gpasswd -a $USER wireshark 如果不像重新登录就可以使用 ...

  9. CPU UsageTimes Profile (cpu=times)

    HPROF工具能搜集CPU使用信息通过注入代码到每个方法进入点和退出点.因此能够统计方法真实调用次数和花费的时间. 它使用BCI(Byte Code Injection),所以比cpu=samples ...

  10. 13.App爬取相关库的安装(Charles,Mitmproxy,Appium)

    由于App没有像浏览器一样直观的后台请求工具,主要用一些抓包技术抓取数据.(目前也在学习安装,参考书籍.) 首先呢,一些简单的接口通过Charles或mitmproxy分析,找出规律,直接用程序去抓取 ...