Python中使用asyncio库编写socket5代理时程序卡住,如何排查问题?
我在 stackoverflow 提问了,暂时还没有人回复。网址在此。 https://stackoverflow.com/questions/56786505/when-i-created-a-socket5-proxy-server-using-asyncio-my-code-was-unexpectedly-bl
# -*- coding: utf-8 -*-
import asyncio
from struct import unpack, pack
async def handle_echo(reader, writer):
data = await reader.read(1024 * 64)
addr = writer.get_extra_info(‘peername’)
print(f"connect from {addr!r}")
if len(data) < 3:
writer.close()
return
result = unpack(’!BBB’, data[:3])
writer.write(b’\x05\x00’)
await writer.drain()
data = await reader.read(1024 * 64)
result = unpack(’!4B’, data[:4])
if result[0] == 5 and result[1] == 1 and result[3] == 3:
host_len = unpack(’!B’, data[4:5])[0]
host = data[5:host_len + 5].decode()
port = unpack(’!H’, data[host_len + 5:])[0]
print(f’len {host_len},host {host},port {port}’)
try:
reader_remote, writer_remote = await asyncio.open_connection(host, port)
writer.write(pack(’!5B’, 5, 0, 0, 3, host_len) + host.encode() + pack(’!H’, port))
await writer.drain()
print(f’connect success !{host}’)
except (TimeoutError, ConnectionRefusedError) as _:
print(f’connect failed !{host}’)
writer.write(pack(’!5B’, 5, 3, 0, 3, host_len) + host.encode() + pack(’!H’, port))
await writer.drain()
writer.close()
return
while True:
client_data = await reader.read(1024 * 64)
print(f’{host} client->local {len(client_data)}’)
if not client_data:
writer_remote.close()
writer.close()
print(f’{host} disconnect’)
return
writer_remote.write(client_data)
await writer_remote.drain()
print(f’{host} local->remote !{len(client_data)}’)
remote_data = await reader_remote.read(1024 * 64)
print(f’{host} remote->local! {len(remote_data)}’)
writer.write(remote_data)
await writer.drain()
print(f’{host} local->client! {len(remote_data)}’)
async def main():
server = await asyncio.start_server(
handle_echo, ‘0.0.0.0’, 3333)
addr = server.sockets[0].getsockname()
print(f'Serving on {addr}')
async with server:
await server.serve_forever()
asyncio.run(main())
如果一个网页只有一个 html 的话就没问题,例如:example.com 。但是像 baidu.com 这种的要加载好多 js 和 css 的就不行了,打印几行日志就卡住了,应该是卡在这里,client_data = await reader.read(1024 * 64),不知道为什么。系统 win10,python3.7.3。
Python中使用asyncio库编写socket5代理时程序卡住,如何排查问题?
asyncio.run(main()),你这个 run 从哪里来的?跑不通
# -- coding: utf-8 -- python3 这个完全可以去掉了
遇到 asyncio 写的 SOCKS5 代理卡住,得先看几个关键点。卡住通常是因为连接没正确关闭、事件循环被阻塞,或者协议状态机卡在某个状态了。
先给个能跑的最小化示例,方便你对照排查:
import asyncio
import socket
async def handle_client(reader, writer):
try:
# 1. 读取客户端SOCKS5握手
data = await reader.read(1024)
if not data or data[0] != 0x05:
writer.close()
return
# 2. 发送认证方法(这里用NO AUTH)
writer.write(b'\x05\x00')
await writer.drain()
# 3. 读取请求
data = await reader.read(1024)
if len(data) < 7:
writer.close()
return
# 4. 解析目标地址(简化版,只处理IPv4)
addr_type = data[3]
if addr_type == 0x01: # IPv4
target_addr = socket.inet_ntoa(data[4:8])
target_port = int.from_bytes(data[8:10], 'big')
# 5. 连接目标服务器
target_reader, target_writer = await asyncio.open_connection(
target_addr, target_port
)
# 6. 回复客户端连接成功
writer.write(b'\x05\x00\x00\x01' + data[4:10])
await writer.drain()
# 7. 双向转发数据
await asyncio.gather(
forward(reader, target_writer),
forward(target_reader, writer)
)
except Exception as e:
print(f"Error: {e}")
finally:
writer.close()
async def forward(reader, writer):
try:
while True:
data = await reader.read(4096)
if not data:
break
writer.write(data)
await writer.drain()
except:
pass
finally:
writer.close()
async def main():
server = await asyncio.start_server(handle_client, '127.0.0.1', 1080)
async with server:
await server.serve_forever()
if __name__ == '__main__':
asyncio.run(main())
排查时重点检查这些地方:
- 看死锁 - 用
asyncio.wait_for(awaitable, timeout)给关键操作加超时,比如await reader.read()和网络连接 - 看资源释放 - 所有
writer对象都要在finally里close(),用async with管理连接更稳妥 - 看事件循环 - 别在协程里用同步阻塞操作,比如
socket.recv()或time.sleep() - 看协议逻辑 - SOCKS5 状态机要处理完所有握手阶段,特别是 ATYP 字段和认证流程
- 加调试输出 - 在每个协议阶段打印日志,看卡在哪一步
用 asyncio.all_tasks() 可以看当前所有任务状态,配合日志能发现哪个协程没结束。
总结:重点查超时、资源释放和协议状态机。
py3.7 新加的函数
3.7 asyncio 加了很多新函数
如果资源大于 64k ( 1024*64 )的时候,一次性读不完所有内容,所以你仅仅把一部分请求内容转发,一端会继续等待,并不会发送数据
代理要全双工的,你的代码只做了一个方向的数据中转。从网上找了个例子,你可以参考下: github
.com /msoedov/toxic_proxy/blob/master/toxic_proxy/app.py#L41
从 A 读发给 B 然后从 B 读发给 A 你不觉得这很容易死锁么
假如应该从 B 读发给 A 的时候 你一直等待在从 A 读
#5 补充一下,你这个 while True 里面逻辑是严格有顺序的,就是先从 Client 读数据,然后发送到 Server,然后从 Server 等待回应,然后再把回应发到 Client。这种模式在 HTTP 1.1 却是没什么问题,是请求-响应模式。不过,资源大于 64k 的时候,你紧紧把部分资源回应发到 Client,Client 还会等着你发剩余的部分,不会向你这个 Proxy 发送数据,所以你会在那个语句卡住。
1、 要么读写的时候读到数据干为止再转发,
2、要么同时分别处理 C -> S 和 S -> C 两个方向的请求,比如用两个线程(或协程)一个负责从 Client 读数据转发到 Server,另一个负责从 Server 读数据转发到 Client
#8 却是 -> 确实, 紧紧 -> 仅仅;
上面说的两种中,还是 2 好一些,难免遇到 server 卡住,发一部分不给你发剩余的,Proxy 又没有分析 http 响应包不知道包边界在哪。
这个是 Python3.7 新添加的函数,跑不通的话可以用loop = asyncio.get_event_loop()<br>loop.run_until_complete(asyncio.gather(main()))
这个
但是远程我在上面只是进行了 tcp 连接,没有其他操作,应该不会是远程先发数据
数据大小的话可以看 stackoverflow 里边贴的日志,我打印了数据大小,没有超出。。我怀疑是不是没有全部发送出去,像之前的 sock.send 和 sock.sendall 的区别。
好的好的, 非常感谢, 我尝试一下
了解了,感谢提醒,我尝试下全双工的写法。
我参考一下,尝试下全双工的写法, 感谢
感谢大家的帮助,改成全双工已经没有问题了。<br># -*- coding: utf-8 -*-<br>import asyncio<br>from struct import unpack, pack<br><br><br>async def handle_echo(reader, writer):<br> data = await reader.read(1024 * 64)<br> addr = writer.get_extra_info('peername')<br> print(f"connect from {addr!r}")<br> if len(data) < 3:<br> print('too short...')<br> writer.close()<br> return<br> result = unpack('!BBB', data[:3])<br> writer.write(b'\x05\x00')<br> await writer.drain()<br> data = await reader.read(1024 * 64)<br> result = unpack('!4B', data[:4])<br> if result[0] == 5 and result[1] == 1 and result[3] == 3:<br> host_len = unpack('!B', data[4:5])[0]<br> host = data[5:host_len + 5].decode()<br> port = unpack('!H', data[host_len + 5:])[0]<br> print(f'len {host_len},host {host},port {port}')<br> try:<br> reader_remote, writer_remote = await asyncio.open_connection(host, port)<br> writer.write(pack('!5B', 5, 0, 0, 3, host_len) + host.encode() + pack('!H', port))<br> await writer.drain()<br> print(f'connect success !{host}')<br> except (TimeoutError, ConnectionRefusedError) as _:<br> print(f'connect failed !{host}')<br> writer.write(pack('!5B', 5, 3, 0, 3, host_len) + host.encode() + pack('!H', port))<br> await writer.drain()<br> writer.close()<br> return<br> up_stream = _pipe(reader, writer_remote, host)<br> down_stream = _pipe(reader_remote, writer, host)<br> await asyncio.gather(up_stream, down_stream)<br> if result[0] == 5 and result[1] == 1 and result[3] == 1:<br> ip = '.'.join([str(a) for a in unpack('!BBBB', data[4:8])])<br> port = unpack('H', data[8:10])[0]<br> print(f'ip {ip},port {port}')<br> try:<br> reader_remote, writer_remote = await asyncio.open_connection(ip, port)<br> writer.write(pack('!8B', 5, 0, 0, 1, *unpack('!BBBB', data[4:8])) + pack('!H', port))<br> await writer.drain()<br> print(f'connect success !{ip}')<br> except (TimeoutError, ConnectionRefusedError) as _:<br> print(f'connect failed !{ip},{repr(_)}')<br> writer.write(pack('!8B', 5, 3, 0, 1, *unpack('!BBBB', data[4:8])) + pack('!H', port))<br> await writer.drain()<br> writer.close()<br> return<br> up_stream = _pipe(reader, writer_remote, ip)<br> down_stream = _pipe(reader_remote, writer, ip)<br> await asyncio.gather(up_stream, down_stream)<br><br><br>async def _pipe(reader, writer, host):<br> while reader.at_eof:<br> try:<br> data = await reader.read(1024 * 64)<br> if not data:<br> writer.close()<br> break<br> except (ConnectionAbortedError, ConnectionResetError) as _:<br> writer.close()<br> print(f'{host} 异常退出 {repr(_)}')<br> break<br> try:<br> writer.write(data)<br> await writer.drain()<br> except (ConnectionAbortedError, ConnectionResetError) as _:<br> writer.close()<br> print(f'{host} 异常退出 {repr(_)}')<br> break<br> print(f'{host} 退出')<br><br><br>async def main():<br> server = await asyncio.start_server(<br> handle_echo, '0.0.0.0', 3333)<br><br> addr = server.sockets[0].getsockname()<br> print(f'Serving on {addr}')<br><br> async with server:<br> await server.serve_forever()<br><br><br>loop = asyncio.get_event_loop()<br>loop.run_until_complete(asyncio.gather(main()))<br><br>
代码在此
啊,格式乱了,不知道怎么格式化成 markdown。。。无语。
好吧 我一直用的是 3.6

