一、應用場景和實現方法
最近遇到一個與對端交互的特殊場景,對端限制了單個IP的端口連接數量。
如果我頻繁的打開和關閉TCP連接,只保證連接的數量小於限定值,對我來說,TCP連接已經關閉,端口也回收了,但是對方的端口還是識別爲被佔用的狀態,這就導致大量的連接被拒絕。
所以我查閱相關資料,實現了持久化連接、端口複用的功能,下面是一個基於Python2.7,用tornado 4.5.3框架寫的Demo。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import time
import tornado
import requests
import tornado.httpclient
from multiprocessing import Process
from tornado.httputil import url_concat
from tornado.web import RequestHandler, Application
http_client = None # 全局連接池,多進程每個進程一個,多線程可複用
class MyHandler(RequestHandler):
def __init__(self, application, request, **kwargs):
global http_client
# 選擇curl_httpclient而不是默認simple_httpclient,否則會不停關閉新建連接,連接數量只有一個
tornado.httpclient.AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient')
# 保證force_instance=False,否則會生成大量連接實例,佔用大量端口,max_clients表示最大連接數量
http_client = tornado.httpclient.AsyncHTTPClient(force_instance=False, max_clients=20, defaults=dict(request_timeout=5))
super(MyHandler, self).__init__(application, request, **kwargs)
@tornado.gen.coroutine
def post(self):
post_data = tornado.escape.json_decode(self.request.body or '{}')
url = url_concat('https://www.baidu.com', post_data) # 生成鏈接
request = tornado.httpclient.HTTPRequest(
url,
headers={'Connection': 'keep-alive'}, # 建議加上keep-alive請求頭保證長連接
request_timeout=5, validate_cert=False
) # 生成請求實例
response = yield tornado.gen.Task(http_client.fetch, request) # 發起交互請求
self.set_status(200)
self.set_header('Content-Type', 'application/json; charset=UTF-8')
self.finish({'code': 0, 'msg': 'OK', 'status_code': response.code})
def async_app(port):
application = Application([(r'/test/?', MyHandler)], logging='info', debug=True, xsrf_cookies=False)
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(port)
tornado.ioloop.IOLoop.instance().start()
def send_test_request(): # 發送測試請求的子進程
time.sleep(0.5)
while True:
for port in range(6205, 6209):
response = requests.post('http://127.0.0.1:%s/test/' % port, json={'test': 'test'})
print(response.json())
time.sleep(0.1)
if __name__ == "__main__":
request_process = Process(target=send_test_request)
request_process.start() # 先啓動子進程,否則會被tornado的ioloop阻塞住
for port in range(6205, 6209):
Process(target=async_app, args=(port,)).start()
request_process.join()
持久化連接的關鍵在於下面兩行:
tornado.httpclient.AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient')
http_client = tornado.httpclient.AsyncHTTPClient(force_instance=False, max_clients=20, defaults=dict(request_timeout=5))
第一行的作用是選擇curl_httpclient而不是默認的simple_httpclient生成連接池,否則會不停關閉、新建連接,同時連接數量只有一個。
第二行要保證force_instance=False,否則會生成大量連接實例,佔用大量端口,max_clients參數表示最大連接數量。
二、驗證
我們註釋掉第一行再運行Demo:
# tornado.httpclient.AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient')
http_client = tornado.httpclient.AsyncHTTPClient(force_instance=False, max_clients=20, defaults=dict(request_timeout=5))
效果是這樣:
可以看到使用了53942、53954、53966三個端口號。
然後我們把force_instance改成True:
tornado.httpclient.AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient')
http_client = tornado.httpclient.AsyncHTTPClient(force_instance=True, max_clients=20, defaults=dict(request_timeout=5))
效果是這樣:
可以看到使用了很多端口,肯定要被拒絕連接了。
最後我們不做修改,看看能不能保證端口複用:
tornado.httpclient.AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient')
http_client = tornado.httpclient.AsyncHTTPClient(force_instance=False, max_clients=20, defaults=dict(request_timeout=5))
效果是這樣:
可以看到一直都在複用45180、45176、45172、45168這4個端口,每個tornado進程使用一個連接池,每個連接池一直佔用一個端口,問題解決了。