proxynt 2.0.61rc2__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/PKG-INFO +1 -1
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/tcp_forward_client.py +14 -1
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/nat_serialization_v1.py +9 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/pool.py +41 -16
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/constant/message_type_constnat.py +6 -1
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/constant/system_constant.py +1 -1
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/PKG-INFO +1 -1
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/run_client.py +25 -11
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/tcp_forward_client.py +172 -58
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/websocket_handler.py +99 -60
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/LICENSE +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/MANIFEST.in +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/abstract_tunnel.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/clear_nonce_task.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/heart_beat_task.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/kcp_tunnel_impl.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/n4_tunnel_manager.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/quic_tunnel_impl.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/tunnel_protocol.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/udp_forward_client.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/cert_utils.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/crypto/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/crypto/table.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/encrypt_utils.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/kcp.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/logger_factory.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/n4_protocol.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/n4_punch.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/nat_serialization.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/nat_serialization_v2.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/register_append_data.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/speed_limit.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_abnf.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_app.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_cookiejar.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_core.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_dispatcher.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_exceptions.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_handshake.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_http.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_logging.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_socket.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_ssl_compat.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_url.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_utils.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_wsdump.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/echo-server.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_abnf.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_app.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_cookiejar.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_dispatcher.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_handshake_large_response.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_http.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_large_payloads.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_logging_helpers.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_reconnect_bad_fd.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_socket.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_socket_bugs.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_ssl_compat.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_ssl_edge_cases.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_url.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_utils.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_websocket.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/compliance/autobahn-test-report-Feb-03-2021.html +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/setup.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_abnf.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_app.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_cookiejar.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_core.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_dispatcher.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_exceptions.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_handshake.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_http.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_logging.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_socket.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_ssl_compat.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_url.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_utils.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_wsdump.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/echo-server.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_abnf.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_app.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_cookiejar.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_dispatcher.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_handshake_large_response.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_http.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_large_payloads.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_logging_helpers.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_reconnect_bad_fd.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_socket.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_socket_bugs.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_ssl_compat.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_ssl_edge_cases.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_url.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_utils.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_websocket.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/constant/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/context/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/context/context_utils.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/client_config_entity.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/message/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/message/message_entity.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/message/push_config_entity.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/message/tcp_over_websocket_message.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/server_config_entity.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/exceptions/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/exceptions/duplicated_name.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/exceptions/invalid_password.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/exceptions/replay_error.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/exceptions/signature_error.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/p2ptest/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/p2ptest/client.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/p2ptest/n4.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/p2ptest/tls_handshake_timeout_test.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/SOURCES.txt +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/dependency_links.txt +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/entry_points.txt +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/requires.txt +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/top_level.txt +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/run_server.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/admin_http_handler.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/n4.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/n4_signal_service.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/task/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/task/check_cookie_task.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/task/clear_nonce_task.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/task/heart_beat_task.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/__init__.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/base.html +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/css/fonts/element-icons.woff +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/css/index.css +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/ele_index.html +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/js/axios.min.js +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/js/index.js +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/js/vue.min.js +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/login.html +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/udp_forward_client.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/setup.cfg +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/setup.py +0 -0
- {proxynt-2.0.61rc2 → proxynt-2.1.0}/test_exchange.py +0 -0
|
@@ -9,7 +9,7 @@ from typing import Dict, List
|
|
|
9
9
|
|
|
10
10
|
from common.logger_factory import LoggerFactory
|
|
11
11
|
from common.nat_serialization import NatSerialization
|
|
12
|
-
from common.pool import SelectPool
|
|
12
|
+
from common.pool import SelectPool, PAUSE_REASON_FLOW_CONTROL
|
|
13
13
|
from common.register_append_data import ResisterAppendData
|
|
14
14
|
from common.speed_limit import SpeedLimiter
|
|
15
15
|
from constant.message_type_constnat import MessageTypeConstant
|
|
@@ -38,6 +38,7 @@ class TcpForwardClient:
|
|
|
38
38
|
self.ws = ws_sender # WsSender — thread-safe, send via IOLoop.add_callback
|
|
39
39
|
self.lock = Lock()
|
|
40
40
|
self.socket_event_loop = SelectPool()
|
|
41
|
+
self.flow_control_enabled = False # 服务端是否支持按连接流控, 由 PUSH_CONFIG 协商
|
|
41
42
|
|
|
42
43
|
# C2C client-to-client forward state
|
|
43
44
|
self.c2c_rules: List[dict] = []
|
|
@@ -382,6 +383,18 @@ class TcpForwardClient:
|
|
|
382
383
|
)
|
|
383
384
|
LoggerFactory.get_logger().debug('Send to websocket cost time %s' % (time.time() - start_time))
|
|
384
385
|
|
|
386
|
+
def set_local_reading_paused(self, uid: bytes, pause: bool):
|
|
387
|
+
"""服务端通知该 uid 积压过多, 暂停/恢复读取本地连接"""
|
|
388
|
+
connection = self.uid_to_socket_connection.get(uid)
|
|
389
|
+
if not connection:
|
|
390
|
+
return
|
|
391
|
+
if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
|
|
392
|
+
LoggerFactory.get_logger().debug('flow control %s uid: %s' % ('pause' if pause else 'resume', uid.hex()))
|
|
393
|
+
if pause:
|
|
394
|
+
self.socket_event_loop.pause_reading(connection.socket, PAUSE_REASON_FLOW_CONTROL)
|
|
395
|
+
else:
|
|
396
|
+
self.socket_event_loop.resume_reading(connection.socket, PAUSE_REASON_FLOW_CONTROL)
|
|
397
|
+
|
|
385
398
|
def send_by_uid(self, uid: bytes, msg: bytes):
|
|
386
399
|
connection = self.uid_to_socket_connection.get(uid)
|
|
387
400
|
if not connection:
|
|
@@ -74,6 +74,8 @@ class NatSerializationV1:
|
|
|
74
74
|
magic, mode_flag, len(target_client), len(target_service), len(source_rule_name), len(protocol),
|
|
75
75
|
uid, target_client, target_service, source_rule_name, protocol)
|
|
76
76
|
|
|
77
|
+
elif type_ in (MessageTypeConstant.FLOW_PAUSE, MessageTypeConstant.FLOW_RESUME):
|
|
78
|
+
body = struct.pack(f'{UID_LEN}s', data['data']['uid'])
|
|
77
79
|
elif type_ == MessageTypeConstant.PUSH_CONFIG:
|
|
78
80
|
body = json.dumps(data).encode()
|
|
79
81
|
elif type_ == MessageTypeConstant.PING:
|
|
@@ -190,6 +192,13 @@ class NatSerializationV1:
|
|
|
190
192
|
}
|
|
191
193
|
return return_data
|
|
192
194
|
|
|
195
|
+
elif type_.decode() in (MessageTypeConstant.FLOW_PAUSE, MessageTypeConstant.FLOW_RESUME):
|
|
196
|
+
return_data: MessageEntity = {
|
|
197
|
+
'type_': type_.decode(),
|
|
198
|
+
'data': {'uid': body[:UID_LEN]}
|
|
199
|
+
}
|
|
200
|
+
return return_data
|
|
201
|
+
|
|
193
202
|
elif type_ == MessageTypeConstant.PUSH_CONFIG.encode():
|
|
194
203
|
return_data: MessageEntity = json.loads(body.decode())
|
|
195
204
|
return return_data
|
|
@@ -4,6 +4,7 @@ import threading
|
|
|
4
4
|
import time
|
|
5
5
|
import traceback
|
|
6
6
|
from selectors import DefaultSelector, EVENT_READ
|
|
7
|
+
from typing import Dict, Set
|
|
7
8
|
|
|
8
9
|
from common.logger_factory import LoggerFactory
|
|
9
10
|
from common.register_append_data import ResisterAppendData
|
|
@@ -14,6 +15,9 @@ from constant.system_constant import SystemConstant
|
|
|
14
15
|
参考 shadowsocks eventloop 模式
|
|
15
16
|
"""
|
|
16
17
|
|
|
18
|
+
PAUSE_REASON_SPEED_LIMIT = 'speed_limit'
|
|
19
|
+
PAUSE_REASON_FLOW_CONTROL = 'flow_control'
|
|
20
|
+
|
|
17
21
|
|
|
18
22
|
class SelectPool:
|
|
19
23
|
|
|
@@ -26,6 +30,9 @@ class SelectPool:
|
|
|
26
30
|
self._wakeup_r.setblocking(False)
|
|
27
31
|
self._wakeup_w.setblocking(False)
|
|
28
32
|
self.selector.register(self._wakeup_r, EVENT_READ, None)
|
|
33
|
+
# 每个 socket 的暂停原因 (如 speed_limit / flow_control), 全部解除才恢复读取
|
|
34
|
+
self._pause_reasons: Dict[socket.socket, Set[str]] = dict()
|
|
35
|
+
self._pause_lock = threading.Lock()
|
|
29
36
|
|
|
30
37
|
def _wakeup(self):
|
|
31
38
|
try:
|
|
@@ -59,6 +66,8 @@ class SelectPool:
|
|
|
59
66
|
pass
|
|
60
67
|
if fileno in self.fileno_to_client:
|
|
61
68
|
self.fileno_to_client.pop(fileno)
|
|
69
|
+
with self._pause_lock:
|
|
70
|
+
self._pause_reasons.pop(s, None)
|
|
62
71
|
try:
|
|
63
72
|
self.selector.unregister(s)
|
|
64
73
|
except (KeyError, ValueError):
|
|
@@ -66,27 +75,43 @@ class SelectPool:
|
|
|
66
75
|
except OSError:
|
|
67
76
|
LoggerFactory.get_logger().error(traceback.format_exc())
|
|
68
77
|
|
|
69
|
-
def pause_reading(self, s: socket.socket):
|
|
78
|
+
def pause_reading(self, s: socket.socket, reason: str = PAUSE_REASON_SPEED_LIMIT):
|
|
70
79
|
"""暂停监听可读事件 (modify 掩码为 0)"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
with self._pause_lock:
|
|
81
|
+
try:
|
|
82
|
+
key = self.selector.get_key(s)
|
|
83
|
+
except (KeyError, ValueError):
|
|
84
|
+
return
|
|
85
|
+
reasons = self._pause_reasons.setdefault(s, set())
|
|
86
|
+
first_pause = not reasons
|
|
87
|
+
reasons.add(reason)
|
|
88
|
+
if first_pause:
|
|
89
|
+
try:
|
|
90
|
+
self.selector.modify(s, 0, key.data)
|
|
91
|
+
except (KeyError, ValueError):
|
|
92
|
+
pass
|
|
76
93
|
|
|
77
|
-
def resume_reading(self, s: socket.socket):
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
def resume_reading(self, s: socket.socket, reason: str = PAUSE_REASON_SPEED_LIMIT):
|
|
95
|
+
"""解除一个暂停原因, 没有其他原因时恢复监听可读事件"""
|
|
96
|
+
with self._pause_lock:
|
|
97
|
+
reasons = self._pause_reasons.get(s)
|
|
98
|
+
if not reasons or reason not in reasons:
|
|
99
|
+
return
|
|
100
|
+
reasons.discard(reason)
|
|
101
|
+
if reasons:
|
|
102
|
+
return
|
|
103
|
+
self._pause_reasons.pop(s, None)
|
|
104
|
+
try:
|
|
105
|
+
key = self.selector.get_key(s)
|
|
106
|
+
self.selector.modify(s, EVENT_READ, key.data)
|
|
107
|
+
except (KeyError, ValueError):
|
|
108
|
+
return
|
|
109
|
+
self._wakeup()
|
|
85
110
|
|
|
86
111
|
def pause_and_resume_later(self, s: socket.socket, delay_time: float):
|
|
87
112
|
"""暂停可读监听, delay_time 秒后恢复"""
|
|
88
|
-
self.pause_reading(s)
|
|
89
|
-
threading.Timer(delay_time, self.resume_reading, args=(s,)).start()
|
|
113
|
+
self.pause_reading(s, PAUSE_REASON_SPEED_LIMIT)
|
|
114
|
+
threading.Timer(delay_time, self.resume_reading, args=(s, PAUSE_REASON_SPEED_LIMIT)).start()
|
|
90
115
|
|
|
91
116
|
def run(self):
|
|
92
117
|
while True:
|
|
@@ -28,4 +28,9 @@ class MessageTypeConstant:
|
|
|
28
28
|
P2P_PRE_CONNECT = 'g'
|
|
29
29
|
P2P_EXCHANGE = 'h' # UDP exchange to establish NAT mapping
|
|
30
30
|
P2P_PEER_INFO = 'i' # Server sends peer's actual UDP port
|
|
31
|
-
P2P_PUNCH_REQUEST = 'j' # Request to initiate P2P hole punching
|
|
31
|
+
P2P_PUNCH_REQUEST = 'j' # Request to initiate P2P hole punching
|
|
32
|
+
|
|
33
|
+
# 按连接流控: 接收方某个 uid 积压过多时通知发送方暂停/恢复读取该 uid 的数据源
|
|
34
|
+
# 仅在双方通过 PUSH_CONFIG 协商支持后发送 (旧版本反序列化未知类型会报错)
|
|
35
|
+
FLOW_PAUSE = 'k'
|
|
36
|
+
FLOW_RESUME = 'l'
|
|
@@ -79,6 +79,7 @@ class WsSender:
|
|
|
79
79
|
"""Thread-safe WebSocket sender wrapping Tornado WebSocketClientConnection."""
|
|
80
80
|
|
|
81
81
|
MAX_PENDING_WRITES = 32
|
|
82
|
+
WRITE_BACKPRESSURE_TIMEOUT = 10
|
|
82
83
|
|
|
83
84
|
def __init__(self):
|
|
84
85
|
self._ws = None
|
|
@@ -104,24 +105,30 @@ class WsSender:
|
|
|
104
105
|
io.add_callback(ws.write_message, data, binary=True)
|
|
105
106
|
return
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
write_sem = self._write_sem
|
|
109
|
+
acquired = write_sem.acquire(timeout=self.WRITE_BACKPRESSURE_TIMEOUT)
|
|
110
|
+
if self._ws is None:
|
|
111
|
+
if acquired:
|
|
112
|
+
write_sem.release()
|
|
109
113
|
return
|
|
110
|
-
|
|
114
|
+
if not acquired:
|
|
115
|
+
# 不能丢弃: TCP 流中间少一块数据会导致对端协议错乱(如 sshd Bad packet length)
|
|
116
|
+
LoggerFactory.get_logger().warning(f'websocket write backlog over {self.WRITE_BACKPRESSURE_TIMEOUT}s, send without backpressure')
|
|
117
|
+
io.add_callback(self._do_write, data, write_sem if acquired else None)
|
|
111
118
|
|
|
112
|
-
def _do_write(self, data):
|
|
119
|
+
def _do_write(self, data, write_sem):
|
|
113
120
|
ws = self._ws
|
|
114
121
|
if ws is None:
|
|
115
|
-
|
|
122
|
+
if write_sem:
|
|
123
|
+
write_sem.release()
|
|
116
124
|
return
|
|
117
125
|
try:
|
|
118
126
|
future = ws.write_message(data, binary=True)
|
|
119
|
-
|
|
127
|
+
if write_sem:
|
|
128
|
+
ioloop.IOLoop.current().add_future(future, lambda f: write_sem.release())
|
|
120
129
|
except Exception:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _on_write_done(self):
|
|
124
|
-
self._write_sem.release()
|
|
130
|
+
if write_sem:
|
|
131
|
+
write_sem.release()
|
|
125
132
|
|
|
126
133
|
def close(self):
|
|
127
134
|
ws = self._ws
|
|
@@ -295,6 +302,7 @@ class WebsocketClient:
|
|
|
295
302
|
push_config: PushConfigEntity = message_data['data']
|
|
296
303
|
self.public_ip = push_config.get('public_ip')
|
|
297
304
|
self.public_port = push_config.get('public_port')
|
|
305
|
+
self.forward_client.flow_control_enabled = bool(push_config.get('server_flow_control', False))
|
|
298
306
|
|
|
299
307
|
for d in push_config['config_list']:
|
|
300
308
|
if d.get('speed_limit'):
|
|
@@ -306,6 +314,11 @@ class WebsocketClient:
|
|
|
306
314
|
self.forward_client.setup_c2c_tcp_listeners(c2c_rules)
|
|
307
315
|
self.udp_forward_client.setup_c2c_udp_listeners(c2c_rules)
|
|
308
316
|
|
|
317
|
+
elif msg_type == MessageTypeConstant.FLOW_PAUSE:
|
|
318
|
+
self.forward_client.set_local_reading_paused(message_data['data']['uid'], True)
|
|
319
|
+
elif msg_type == MessageTypeConstant.FLOW_RESUME:
|
|
320
|
+
self.forward_client.set_local_reading_paused(message_data['data']['uid'], False)
|
|
321
|
+
|
|
309
322
|
# P2P punch disabled
|
|
310
323
|
elif msg_type == MessageTypeConstant.P2P_PUNCH_REQUEST:
|
|
311
324
|
LoggerFactory.get_logger().info('P2P punch disabled, ignoring request')
|
|
@@ -337,7 +350,8 @@ class WebsocketClient:
|
|
|
337
350
|
'config_list': push_client_data,
|
|
338
351
|
"client_name": client_name,
|
|
339
352
|
'version': SystemConstant.VERSION,
|
|
340
|
-
'p2p_supported': False
|
|
353
|
+
'p2p_supported': False,
|
|
354
|
+
'flow_control': True
|
|
341
355
|
}
|
|
342
356
|
message: MessageEntity = {
|
|
343
357
|
'type_': MessageTypeConstant.PUSH_CONFIG,
|
|
@@ -8,14 +8,15 @@ from collections import defaultdict
|
|
|
8
8
|
from functools import partial
|
|
9
9
|
from threading import Thread, Lock
|
|
10
10
|
from asyncio import Lock as AsyncioLock
|
|
11
|
-
from
|
|
11
|
+
from collections import deque
|
|
12
|
+
from typing import Dict, Tuple, List, Set, Optional
|
|
12
13
|
import os
|
|
13
14
|
|
|
14
15
|
import tornado
|
|
15
16
|
|
|
16
17
|
from common.logger_factory import LoggerFactory
|
|
17
18
|
from common.nat_serialization import NatSerialization
|
|
18
|
-
from common.pool import SelectPool
|
|
19
|
+
from common.pool import SelectPool, PAUSE_REASON_FLOW_CONTROL
|
|
19
20
|
from common.register_append_data import ResisterAppendData
|
|
20
21
|
from common.speed_limit import SpeedLimiter
|
|
21
22
|
from constant.message_type_constnat import MessageTypeConstant
|
|
@@ -56,8 +57,8 @@ class PublicSocketConnection:
|
|
|
56
57
|
# Optimistic send mode configuration
|
|
57
58
|
self.optimistic_mode = True # Enable optimistic send by default
|
|
58
59
|
self.connection_confirmed = False # Whether connection is confirmed
|
|
59
|
-
self.connect_requested = False # Whether REQUEST_TO_CONNECT has been sent
|
|
60
60
|
self.early_data_buffer = [] # Buffer early data (data received before connection confirmation)
|
|
61
|
+
self.early_data_lock = Lock() # socket 线程缓冲与 tornado 线程 flush 互斥, 防止数据乱序/丢失
|
|
61
62
|
self.max_early_data_packets = 65536 * 4 # Maximum buffered packets
|
|
62
63
|
self.bytes_from_public: int = 0
|
|
63
64
|
self.bytes_to_public: int = 0
|
|
@@ -71,6 +72,70 @@ class PublicSocketConnection:
|
|
|
71
72
|
return f'{self.uid}_{self.socket}_{self.socket_server.name}'
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
SEND_QUEUE_HIGH_WATER = 4 * 1024 * 1024 # 单个 uid 积压超过该值时对来源 websocket 做背压
|
|
76
|
+
SEND_QUEUE_LOW_WATER = 1 * 1024 * 1024
|
|
77
|
+
# 客户端支持流控时: 超过 FLOW_HIGH_WATER 通知客户端暂停该 uid, 低于 FLOW_LOW_WATER 恢复
|
|
78
|
+
# 超过 FLOW_HARD_LIMIT 仍退回 websocket 级背压, 防止对端不响应时内存失控
|
|
79
|
+
FLOW_HIGH_WATER = 1 * 1024 * 1024
|
|
80
|
+
FLOW_LOW_WATER = 256 * 1024
|
|
81
|
+
FLOW_HARD_LIMIT = 8 * 1024 * 1024
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class UidSendQueue:
|
|
85
|
+
"""单个 uid 的有序发送队列, 只在 tornado 线程中使用"""
|
|
86
|
+
|
|
87
|
+
def __init__(self):
|
|
88
|
+
self._items = deque()
|
|
89
|
+
self._event = asyncio.Event()
|
|
90
|
+
self._drain_waiters: List[asyncio.Future] = []
|
|
91
|
+
self.size = 0
|
|
92
|
+
self.closed = False
|
|
93
|
+
self.remote_paused = False # 是否已通知客户端暂停该 uid
|
|
94
|
+
|
|
95
|
+
def put(self, message: bytes):
|
|
96
|
+
if self.closed:
|
|
97
|
+
return
|
|
98
|
+
self._items.append(message)
|
|
99
|
+
self.size += len(message)
|
|
100
|
+
self._event.set()
|
|
101
|
+
|
|
102
|
+
async def get(self) -> Optional[bytes]:
|
|
103
|
+
"""返回下一条数据, 队列关闭时返回 None"""
|
|
104
|
+
while not self._items:
|
|
105
|
+
if self.closed:
|
|
106
|
+
return None
|
|
107
|
+
self._event.clear()
|
|
108
|
+
await self._event.wait()
|
|
109
|
+
if self.closed:
|
|
110
|
+
return None
|
|
111
|
+
return self._items.popleft()
|
|
112
|
+
|
|
113
|
+
def task_done(self, length: int):
|
|
114
|
+
self.size -= length
|
|
115
|
+
if self.size <= SEND_QUEUE_LOW_WATER:
|
|
116
|
+
self._wake_drain_waiters()
|
|
117
|
+
|
|
118
|
+
def wait_drained(self) -> asyncio.Future:
|
|
119
|
+
future = asyncio.get_event_loop().create_future()
|
|
120
|
+
self._drain_waiters.append(future)
|
|
121
|
+
return future
|
|
122
|
+
|
|
123
|
+
def close(self):
|
|
124
|
+
if self.closed:
|
|
125
|
+
return
|
|
126
|
+
self.closed = True
|
|
127
|
+
self._items.clear()
|
|
128
|
+
self.size = 0
|
|
129
|
+
self._event.set()
|
|
130
|
+
self._wake_drain_waiters()
|
|
131
|
+
|
|
132
|
+
def _wake_drain_waiters(self):
|
|
133
|
+
waiters, self._drain_waiters = self._drain_waiters, []
|
|
134
|
+
for future in waiters:
|
|
135
|
+
if not future.done():
|
|
136
|
+
future.set_result(None)
|
|
137
|
+
|
|
138
|
+
|
|
74
139
|
class TcpForwardClient:
|
|
75
140
|
_instance = None
|
|
76
141
|
|
|
@@ -83,7 +148,7 @@ class TcpForwardClient:
|
|
|
83
148
|
self.tornado_loop = tornado_loop
|
|
84
149
|
self.close_lock = Lock()
|
|
85
150
|
self.client_name_to_lock: Dict[str, AsyncioLock] = dict()
|
|
86
|
-
self.
|
|
151
|
+
self.uid_to_send_queue: Dict[bytes, 'UidSendQueue'] = dict() # 每个 uid 一个有序发送队列
|
|
87
152
|
|
|
88
153
|
@classmethod
|
|
89
154
|
def get_instance(cls) -> 'TcpForwardClient':
|
|
@@ -138,6 +203,8 @@ class TcpForwardClient:
|
|
|
138
203
|
recv_error = None
|
|
139
204
|
try:
|
|
140
205
|
recv = each.recv(data.read_size)
|
|
206
|
+
except (BlockingIOError, InterruptedError):
|
|
207
|
+
return # 非阻塞 socket 的伪就绪, 等下次可读
|
|
141
208
|
except ConnectionResetError as e:
|
|
142
209
|
recv = b''
|
|
143
210
|
recv_error = e
|
|
@@ -153,30 +220,29 @@ class TcpForwardClient:
|
|
|
153
220
|
|
|
154
221
|
# Optimistic send mode: buffer data if connection not confirmed
|
|
155
222
|
if socket_connection.optimistic_mode and not socket_connection.connection_confirmed:
|
|
156
|
-
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return
|
|
223
|
+
with socket_connection.early_data_lock:
|
|
224
|
+
# 加锁后再判断一次, 避免与 CONNECT_CONFIRMED 的 flush 竞争
|
|
225
|
+
if not socket_connection.connection_confirmed:
|
|
226
|
+
if not recv:
|
|
227
|
+
# Client closed connection before confirmation, close immediately
|
|
228
|
+
LoggerFactory.get_logger().info(f'recv empty before confirmed, close uid: {socket_connection.uid}')
|
|
229
|
+
try:
|
|
230
|
+
self.close_connection(socket_connection)
|
|
231
|
+
except (OSError, ValueError, KeyError):
|
|
232
|
+
LoggerFactory.get_logger().error(f'Close error: {traceback.format_exc()}')
|
|
233
|
+
return
|
|
234
|
+
if len(socket_connection.early_data_buffer) < socket_connection.max_early_data_packets:
|
|
235
|
+
socket_connection.early_data_buffer.append(recv)
|
|
236
|
+
if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
|
|
237
|
+
LoggerFactory.get_logger().debug(f'Buffering early data uid: {socket_connection.uid}, len: {len(recv)}, buffer_size: {len(socket_connection.early_data_buffer)}')
|
|
238
|
+
else:
|
|
239
|
+
# Too much buffered data, connection may have failed, close connection
|
|
240
|
+
LoggerFactory.get_logger().warning(f'Too much early data buffered, closing connection uid: {socket_connection.uid}')
|
|
241
|
+
try:
|
|
242
|
+
self.close_connection(socket_connection)
|
|
243
|
+
except (OSError, ValueError, KeyError):
|
|
244
|
+
LoggerFactory.get_logger().error(f'Close error: {traceback.format_exc()}')
|
|
245
|
+
return
|
|
180
246
|
|
|
181
247
|
# --- 发送端限速:暂停读取,延迟恢复 ---
|
|
182
248
|
if data.speed_limiter and recv:
|
|
@@ -216,6 +282,8 @@ class TcpForwardClient:
|
|
|
216
282
|
client: socket.socket
|
|
217
283
|
# 启用 TCP_NODELAY 减少延迟(禁用 Nagle 算法)
|
|
218
284
|
client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
285
|
+
# 必须非阻塞: loop.sock_sendall 遇到阻塞 socket 会直接阻塞 send, 一个慢连接会卡住整个事件循环
|
|
286
|
+
client.setblocking(False)
|
|
219
287
|
except OSError:
|
|
220
288
|
return
|
|
221
289
|
server = self.listen_socket_to_public_server[server_socket]
|
|
@@ -225,6 +293,9 @@ class TcpForwardClient:
|
|
|
225
293
|
client_socket_connection = PublicSocketConnection(uid, client, server)
|
|
226
294
|
self.uid_to_connection[uid] = client_socket_connection
|
|
227
295
|
self.socket_to_connection[client] = client_socket_connection
|
|
296
|
+
# accept 后立即请求客户端建立连接, 不能等公网端先发数据: MySQL/VNC/FTP/SMTP 等由服务端先发数据的协议会卡住
|
|
297
|
+
# 早期数据与 CONNECT_CONFIRMED 的竞争由 early_data_lock 保证
|
|
298
|
+
self.request_to_connect(client_socket_connection)
|
|
228
299
|
append_data = ResisterAppendData(self.handle_message, register_append_data.speed_limiter)
|
|
229
300
|
self.socket_event_loop.register(client, append_data)
|
|
230
301
|
|
|
@@ -244,34 +315,72 @@ class TcpForwardClient:
|
|
|
244
315
|
partial(handler.write_message, NatSerialization.dumps(send_message, ContextUtils.get_password(), handler.compress_support, handler.protocol_version)), True
|
|
245
316
|
)
|
|
246
317
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
318
|
+
def enqueue_send(self, uid: bytes, message: bytes) -> Optional[asyncio.Future]:
|
|
319
|
+
"""
|
|
320
|
+
按 uid 入队发送, 立即返回, 保证同一 uid 内的数据顺序, 不同 uid 之间互不阻塞
|
|
321
|
+
队列积压超过高水位时返回一个 future, 调用方 await 它做背压; 否则返回 None
|
|
322
|
+
"""
|
|
323
|
+
connection = self.uid_to_connection.get(uid)
|
|
324
|
+
if not connection:
|
|
325
|
+
LoggerFactory.get_logger().debug(f'{uid.hex()} not in uid_to_connection, drop {len(message)} bytes')
|
|
326
|
+
return None
|
|
327
|
+
send_queue = self.uid_to_send_queue.get(uid)
|
|
328
|
+
if send_queue is None:
|
|
329
|
+
send_queue = UidSendQueue()
|
|
330
|
+
self.uid_to_send_queue[uid] = send_queue
|
|
331
|
+
asyncio.ensure_future(self._send_loop(connection, send_queue))
|
|
332
|
+
send_queue.put(message)
|
|
333
|
+
handler = connection.socket_server.websocket_handler
|
|
334
|
+
if handler.flow_control_support:
|
|
335
|
+
if send_queue.size > FLOW_HIGH_WATER and not send_queue.remote_paused:
|
|
336
|
+
send_queue.remote_paused = True
|
|
337
|
+
handler.send_flow_control(uid, True)
|
|
338
|
+
high_water = FLOW_HARD_LIMIT
|
|
339
|
+
else:
|
|
340
|
+
high_water = SEND_QUEUE_HIGH_WATER
|
|
341
|
+
if send_queue.size > high_water:
|
|
342
|
+
return send_queue.wait_drained()
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
async def _send_loop(self, connection: PublicSocketConnection, send_queue: 'UidSendQueue'):
|
|
346
|
+
uid = connection.uid
|
|
347
|
+
loop = asyncio.get_event_loop()
|
|
348
|
+
try:
|
|
349
|
+
while True:
|
|
350
|
+
message = await send_queue.get()
|
|
351
|
+
if message is None or self.uid_to_connection.get(uid) is not connection:
|
|
352
|
+
return
|
|
353
|
+
if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
|
|
354
|
+
LoggerFactory.get_logger().debug(f'send to socket uid: {uid.hex()}, len: {len(message)}')
|
|
355
|
+
try:
|
|
356
|
+
await asyncio.wait_for(loop.sock_sendall(connection.socket, message), timeout=30)
|
|
357
|
+
except asyncio.TimeoutError:
|
|
358
|
+
LoggerFactory.get_logger().warn(f"Socket send timeout for {uid.hex()}, closing connection")
|
|
359
|
+
await self.close_connection_async(connection, 'send timeout')
|
|
360
|
+
return
|
|
361
|
+
except OSError as e:
|
|
362
|
+
LoggerFactory.get_logger().warn(f'{uid.hex()} os error: {e}')
|
|
363
|
+
await self.close_connection_async(connection, f'send os error: {e}')
|
|
364
|
+
return
|
|
365
|
+
connection.bytes_to_public += len(message)
|
|
366
|
+
send_queue.task_done(len(message))
|
|
367
|
+
if send_queue.remote_paused and send_queue.size <= FLOW_LOW_WATER:
|
|
368
|
+
send_queue.remote_paused = False
|
|
369
|
+
connection.socket_server.websocket_handler.send_flow_control(uid, False)
|
|
370
|
+
if not message:
|
|
371
|
+
await self.close_connection_async(connection, 'client reported inner close')
|
|
372
|
+
return
|
|
373
|
+
finally:
|
|
374
|
+
send_queue.close()
|
|
375
|
+
if self.uid_to_send_queue.get(uid) is send_queue:
|
|
376
|
+
self.uid_to_send_queue.pop(uid, None)
|
|
377
|
+
|
|
378
|
+
def set_public_reading_paused(self, connection: PublicSocketConnection, pause: bool):
|
|
379
|
+
"""客户端通知暂停/恢复读取公网连接"""
|
|
380
|
+
if pause:
|
|
381
|
+
self.socket_event_loop.pause_reading(connection.socket, PAUSE_REASON_FLOW_CONTROL)
|
|
382
|
+
else:
|
|
383
|
+
self.socket_event_loop.resume_reading(connection.socket, PAUSE_REASON_FLOW_CONTROL)
|
|
275
384
|
|
|
276
385
|
async def close_connection_async(self, connection: PublicSocketConnection, reason: str = ''):
|
|
277
386
|
"""Asynchronously close connection to avoid blocking in event loop"""
|
|
@@ -284,7 +393,9 @@ class TcpForwardClient:
|
|
|
284
393
|
# Remove from tracking dictionaries
|
|
285
394
|
self.uid_to_connection.pop(uid, None)
|
|
286
395
|
self.socket_to_connection.pop(connection.socket, None)
|
|
287
|
-
self.
|
|
396
|
+
send_queue = self.uid_to_send_queue.pop(uid, None)
|
|
397
|
+
if send_queue:
|
|
398
|
+
send_queue.close()
|
|
288
399
|
connection.socket_server.delete_client(connection)
|
|
289
400
|
# Ensure unregister before closing
|
|
290
401
|
try:
|
|
@@ -309,7 +420,10 @@ class TcpForwardClient:
|
|
|
309
420
|
self.socket_event_loop.unregister(connection.socket)
|
|
310
421
|
self.uid_to_connection.pop(uid, None)
|
|
311
422
|
self.socket_to_connection.pop(connection.socket, None)
|
|
312
|
-
self.
|
|
423
|
+
send_queue = self.uid_to_send_queue.pop(uid, None)
|
|
424
|
+
if send_queue:
|
|
425
|
+
# 可能在 socket 线程调用, 交给 tornado 线程关闭队列
|
|
426
|
+
self.tornado_loop.add_callback(send_queue.close)
|
|
313
427
|
connection.socket_server.delete_client(connection)
|
|
314
428
|
connection.socket.close()
|
|
315
429
|
except Exception:
|