用 Python 的 asyncio 库写了一个简单的 socket5 代理,莫名其妙的卡住,请大家帮忙看下!

2019-06-27 16:49:30 +08:00
 1462326016

我在 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。

3529 次点击
所在节点    Python
18 条回复
qizheshang
2019-06-27 17:32:53 +08:00
asyncio.run(main()),你这个 run 从哪里来的?跑不通
# -*- coding: utf-8 -*- python3 这个完全可以去掉了
wwqgtxx
2019-06-27 17:41:03 +08:00
@qizheshang py3.7 新加的函数
lcdtyph
2019-06-27 18:43:49 +08:00
我觉得问题出在 handle_echo 的那个 while True 里,
如果是远程先发送数据的话就死在 await reader.read 上了
janxin
2019-06-27 18:45:09 +08:00
@qizheshang 3.7 asyncio 加了很多新函数
exch4nge
2019-06-27 18:53:33 +08:00
如果资源大于 64k ( 1024*64 )的时候,一次性读不完所有内容,所以你仅仅把一部分请求内容转发,一端会继续等待,并不会发送数据
XiaoxiaoPu
2019-06-27 18:59:08 +08:00
代理要全双工的,你的代码只做了一个方向的数据中转。从网上找了个例子,你可以参考下: github
.com /msoedov/toxic_proxy/blob/master/toxic_proxy/app.py#L41
2pang
2019-06-27 19:07:21 +08:00
从 A 读发给 B 然后从 B 读发给 A 你不觉得这很容易死锁么
假如应该从 B 读发给 A 的时候 你一直等待在从 A 读
exch4nge
2019-06-27 19:13:03 +08:00
@exch4nge #5 补充一下,你这个 while True 里面逻辑是严格有顺序的,就是先从 Client 读数据,然后发送到 Server,然后从 Server 等待回应,然后再把回应发到 Client。这种模式在 HTTP 1.1 却是没什么问题,是请求-响应模式。不过,资源大于 64k 的时候,你紧紧把部分资源回应发到 Client,Client 还会等着你发剩余的部分,不会向你这个 Proxy 发送数据,所以你会在那个语句卡住。
1、 要么读写的时候读到数据干为止再转发,
2、要么同时分别处理 C -> S 和 S -> C 两个方向的请求,比如用两个线程(或协程)一个负责从 Client 读数据转发到 Server,另一个负责从 Server 读数据转发到 Client
exch4nge
2019-06-27 19:16:46 +08:00
@exch4nge #8 却是 -> 确实, 紧紧 -> 仅仅;
上面说的两种中,还是 2 好一些,难免遇到 server 卡住,发一部分不给你发剩余的,Proxy 又没有分析 http 响应包不知道包边界在哪。
1462326016
2019-06-28 08:14:40 +08:00
@qizheshang 这个是 Python3.7 新添加的函数,跑不通的话可以用
```loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(main()))```
这个
1462326016
2019-06-28 08:15:56 +08:00
@lcdtyph 但是远程我在上面只是进行了 tcp 连接,没有其他操作,应该不会是远程先发数据
1462326016
2019-06-28 08:17:29 +08:00
@exch4nge 数据大小的话可以看 stackoverflow 里边贴的日志,我打印了数据大小,没有超出。。我怀疑是不是没有全部发送出去,像之前的 sock.send 和 sock.sendall 的区别。
1462326016
2019-06-28 08:18:47 +08:00
@exch4nge 好的好的, 非常感谢, 我尝试一下
1462326016
2019-06-28 08:19:46 +08:00
@2pang 了解了,感谢提醒,我尝试下全双工的写法。
1462326016
2019-06-28 08:20:39 +08:00
@XiaoxiaoPu 我参考一下,尝试下全双工的写法, 感谢
1462326016
2019-06-28 20:25:52 +08:00
感谢大家的帮助,改成全双工已经没有问题了。
```
# -*- 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:
print('too short...')
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
up_stream = _pipe(reader, writer_remote, host)
down_stream = _pipe(reader_remote, writer, host)
await asyncio.gather(up_stream, down_stream)
if result[0] == 5 and result[1] == 1 and result[3] == 1:
ip = '.'.join([str(a) for a in unpack('!BBBB', data[4:8])])
port = unpack('H', data[8:10])[0]
print(f'ip {ip},port {port}')
try:
reader_remote, writer_remote = await asyncio.open_connection(ip, port)
writer.write(pack('!8B', 5, 0, 0, 1, *unpack('!BBBB', data[4:8])) + pack('!H', port))
await writer.drain()
print(f'connect success !{ip}')
except (TimeoutError, ConnectionRefusedError) as _:
print(f'connect failed !{ip},{repr(_)}')
writer.write(pack('!8B', 5, 3, 0, 1, *unpack('!BBBB', data[4:8])) + pack('!H', port))
await writer.drain()
writer.close()
return
up_stream = _pipe(reader, writer_remote, ip)
down_stream = _pipe(reader_remote, writer, ip)
await asyncio.gather(up_stream, down_stream)


async def _pipe(reader, writer, host):
while reader.at_eof:
try:
data = await reader.read(1024 * 64)
if not data:
writer.close()
break
except (ConnectionAbortedError, ConnectionResetError) as _:
writer.close()
print(f'{host} 异常退出 {repr(_)}')
break
try:
writer.write(data)
await writer.drain()
except (ConnectionAbortedError, ConnectionResetError) as _:
writer.close()
print(f'{host} 异常退出 {repr(_)}')
break
print(f'{host} 退出')


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()


loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(main()))

```
代码在此
1462326016
2019-06-28 20:26:46 +08:00
啊,格式乱了,不知道怎么格式化成 markdown。。。无语。
qizheshang
2019-07-09 19:05:17 +08:00
@janxin 好吧 我一直用的是 3.6

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/578047

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX