HTTP 持久連接
HTTP通信中爬迟,client和server一問(wèn)一答的方式。HTTP是基于TCP的應(yīng)用層協(xié)議,通常在發(fā)送請(qǐng)求之前需要?jiǎng)?chuàng)建TCP連接,然后在收到響應(yīng)之后會(huì)斷開(kāi)這個(gè)TCP連接隶垮。這就是常見(jiàn)的http短連接。既然有短連接秘噪,那么也有長(zhǎng)連接狸吞。
HTTP協(xié)議最初的設(shè)計(jì)是無(wú)連接無(wú)狀態(tài)的方式。為了維護(hù)狀態(tài),引入了cookie和session方式認(rèn)證識(shí)別用戶蹋偏。早期的web開(kāi)發(fā)中便斥,為了給用戶推送數(shù)據(jù),通常使用所謂的長(zhǎng)連接威始。那時(shí)的長(zhǎng)連接還是基于短連接的方式實(shí)現(xiàn)枢纠,即通過(guò)client的輪詢查詢,在用戶層面看起來(lái)連接并沒(méi)有斷開(kāi)黎棠。隨著技術(shù)的發(fā)展晋渺,又出現(xiàn)了Websockt和MQTT等通信協(xié)議。Websockt和MQTT則是全雙工的通信協(xié)議葫掉。
相比全雙工實(shí)現(xiàn)的長(zhǎng)連接些举,我們還會(huì)在web開(kāi)發(fā)中遇到偽
長(zhǎng)連接跟狱。即HTTP協(xié)議中的keepalive模式俭厚。因?yàn)镠TTP設(shè)計(jì)是無(wú)連接設(shè)計(jì),請(qǐng)求應(yīng)答結(jié)束之后就關(guān)閉了TCP連接驶臊。在http通信中挪挤,就會(huì)有大量的新建和銷毀tcp連接的過(guò)程,那怕是同一個(gè)用戶同一個(gè)客戶端关翎。為了優(yōu)化這種方式扛门,HTTP提出了KeepAlive
模式,即創(chuàng)建的tcp連接后纵寝,傳輸數(shù)據(jù)论寨,server返回響應(yīng)之后并不會(huì)關(guān)掉tcp連接,下一次http請(qǐng)求就能復(fù)用這個(gè)tcp連接爽茴。
這是一種協(xié)商式的連接葬凳,畢竟每次的http發(fā)送數(shù)據(jù)的時(shí)候,還是要單獨(dú)為每個(gè)請(qǐng)求發(fā)送header之類的信息室奏。相比全雙工的websocket火焰,一旦創(chuàng)建了連接,下一次就不需要再發(fā)送header胧沫,直接發(fā)送數(shù)據(jù)即可昌简。因此描述http的keepalive應(yīng)該是持久連接(HTTP persistent connection )更準(zhǔn)確。
keepalive 簡(jiǎn)介
HTTP的keepalive模式提供了HTTP通信的時(shí)候復(fù)用TCP連接的協(xié)商功能绒怨。http1.0默認(rèn)是關(guān)閉的纯赎,只有在http的header加入 Connection: Keep-Alive
才能開(kāi)啟。而http1.1則正相反南蹂,默認(rèn)就打開(kāi)了犬金,只有顯示的在header里加入Connection: close
才能關(guān)閉。現(xiàn)在的瀏覽器基本都是http1.1的協(xié)議,能否使用長(zhǎng)連接佑附,權(quán)看服務(wù)器的支持狀況了樊诺。下圖說(shuō)明了開(kāi)啟keepalive模式的持久連接與短連接的通信示意圖
當(dāng)開(kāi)啟了持久連接音同,就不能使用返回EOF
的方式來(lái)判斷數(shù)據(jù)結(jié)尾了词爬。對(duì)于靜態(tài)和動(dòng)態(tài)的數(shù)據(jù),可以使用Conent-Lenght
和
Transfer-Encoding`來(lái)做應(yīng)用層的區(qū)分权均。
requests與持久連接
了解了keeplive模式顿膨,接下來(lái)我們就來(lái)使用keepalive方式。服務(wù)器使用Tornado叽赊,tornado實(shí)現(xiàn)了keepalive的處理恋沃,客戶端我們可以分別使用同步的requests和異步的AsyncHTTPClient。
先寫(xiě)一個(gè)簡(jiǎn)單的服務(wù)器:
micro-server.py
import tornado.httpserver
import tornado.ioloop
import tornado.web
class IndexHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.finish('It works')
app = tornado.web.Application(
handlers=[
('/', IndexHandler),
],
debug=True
)
if __name__ == '__main__':
server = tornado.httpserver.HTTPServer(app)
server.listen(8000)
tornado.ioloop.IOLoop().instance().start()
requests 短連接
requests不愧是一個(gè)"for human" 的軟件必指,實(shí)現(xiàn)一個(gè)http客戶端非常簡(jiǎn)單囊咏。
import argparse
import requests
url = 'http://127.0.0.1:8000'
def short_connection():
resp = requests.get(url)
print(resp.text)
resp = requests.get(url)
print(resp.text)
def long_connection():
pass
if __name__ == '__main__':
ap = argparse.ArgumentParser()
ap.add_argument("-t", "--type", default="short")
args = ap.parse_args()
type_ = args.type
if type_ == 'short':
short_connection()
elif type_ == 'long':
long_connection()
運(yùn)行 keepalive python requests-cli.py --type=short
,可以看見(jiàn)返回了數(shù)據(jù)塔橡,同時(shí)通過(guò)另外一個(gè)神器wireshark
抓包如下:
從抓包的情況來(lái)看梅割,兩次http請(qǐng)求,一共創(chuàng)建了兩次tcp的握手連接和揮手?jǐn)嚅_(kāi)葛家。每次發(fā)送http數(shù)據(jù)都需要先創(chuàng)建tcp連接户辞,然后就斷開(kāi)了連接。通常是客戶端發(fā)起的斷開(kāi)連接癞谒。
requests 持久連接
requests的官網(wǎng)也說(shuō)明了底燎,基于urllib3
的方式,requests百分比實(shí)現(xiàn)了keepalive方式弹砚,只需要?jiǎng)?chuàng)建一個(gè)客戶端session即可双仍,代碼如下:
def long_connection():
s = requests.Session()
resp = s.get(url)
print(resp.text)
resp = s.get(url)
print(resp.text)
s.close()
再次通過(guò)抓包如下圖:
可以看到,同樣也是兩次http請(qǐng)求迅栅,只創(chuàng)建了一次tcp的握手和揮手殊校。兩次http請(qǐng)求都基于一個(gè)tcp連接。再次查看包43读存,可以看到下圖中的報(bào)文header指定了keepalive为流。
AsyncHTTPClient與持久連接
tornado是一個(gè)優(yōu)秀高性能異步非阻塞(non-block)web框架。如果torando的handler中也需要請(qǐng)求別的三方資源让簿,使用requests的同步網(wǎng)絡(luò)IO敬察,將會(huì)block住整個(gè)tornado的進(jìn)程。因此tornado也實(shí)現(xiàn)了異步的http客戶端AsyncHTTPClient
尔当。
短連接
使用AsyncHTTPClient也不難莲祸,但是想要使用其異步效果蹂安,就必須把其加入事件循環(huán)中,否則只有連接的創(chuàng)立锐帜,而沒(méi)有數(shù)據(jù)的傳輸就退出了田盈。
import tornado.httpclient
import tornado.ioloop
import time
url = 'http://127.0.0.1:8000'
def handle_response(response):
if response.error:
print("Error: %s" % response.error)
else:
print(response.body)
http_client = tornado.httpclient.AsyncHTTPClient()
http_client.fetch(url, handle_response)
http_client.fetch(url, handle_response)
運(yùn)行上述代碼,將會(huì)看到wirshark
中缴阎,創(chuàng)建了兩次TCP連接和斷開(kāi)了連接允瞧,并沒(méi)有發(fā)送http數(shù)據(jù)。為了發(fā)送http數(shù)據(jù)蛮拔,還需要加入tornado的事件循環(huán)述暂。即在最后一行加入tornado.ioloop.IOLoop.instance().start()
再次運(yùn)行,客戶端正常收到了數(shù)據(jù)建炫,抓包如下:
抓包的結(jié)果咋一看像是持久連接畦韭,仔細(xì)一看卻有兩次握手和揮手的操作。的確肛跌,客戶端發(fā)送異步http請(qǐng)求的時(shí)候艺配,創(chuàng)建了兩個(gè)端口49989
和49990
兩個(gè)tcp連接。因?yàn)槭钱惒降恼?qǐng)求惋砂,因此先創(chuàng)建了兩個(gè)連接妒挎,然后才發(fā)送數(shù)據(jù),發(fā)送數(shù)據(jù)的時(shí)候都是基于所創(chuàng)建的端口進(jìn)行的西饵。也就是沒(méi)有使用持久連接。
持久連接
AsyncHTTPClient使用持久連接也很簡(jiǎn)單×圮剑現(xiàn)在流行微服務(wù)架構(gòu)眷柔。通常提供給客戶端的服務(wù)稱之為網(wǎng)關(guān),網(wǎng)關(guān)從各種微服務(wù)中調(diào)用獲取數(shù)據(jù)原朝,通信的方式中驯嘱,同步的有http和rpc,異步的有mq之類的喳坠。而http通常都是使用持久連接的方式鞠评。
下面我們介紹一下在tornado server的handler中使用async client請(qǐng)求微服務(wù)的資源。
再寫(xiě)一個(gè)簡(jiǎn)單server
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import tornado.gen
import tornado.httpclient
import tornado.httpserver
import tornado.ioloop
import tornado.web
class AsyncKeepAliveHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
@tornado.gen.coroutine
def get(self, *args, **kwargs):
url = 'http://127.0.0.1:8000/'
http_client = tornado.httpclient.AsyncHTTPClient()
response = yield tornado.gen.Task(http_client.fetch, url)
print response.code
print response.body
self.finish("It works")
app = tornado.web.Application(
handlers=[
('/async/keepalive', AsyncKeepAliveHandler)
],
debug=True
)
if __name__ == '__main__':
server = tornado.httpserver.HTTPServer(app)
server.listen(5050)
tornado.httpclient.AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
tornado.ioloop.IOLoop().instance().start()
然后我們請(qǐng)求5050端口的服務(wù)壕鹉,也連接發(fā)送兩次http請(qǐng)求:
(venv)? keepalive curl http://127.0.0.1:5050/async/keepalive
It works% (venv)? keepalive curl http://127.0.0.1:5050/async/keepalive
It works%
再看我們的抓包情況:
從圖中可以看到剃幌,即使是兩個(gè)請(qǐng)求,最終都是復(fù)用了斷開(kāi)為50784的tcp連接晾浴。
因?yàn)閍synchttpclient默認(rèn)使用的是SimpleAsyncHTTPClient负乡,實(shí)現(xiàn)持久連接只需要配置一下tornado.httpclient.AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
即可。當(dāng)然脊凰,這個(gè)需要tornado的版本4.2以上抖棘,當(dāng)前的版本是4.5。
CurlAsyncHTTPClient依賴于pycurl。pycurl又依賴libcurl切省。在安裝pycurl的時(shí)候最岗,可能會(huì)出現(xiàn)link的問(wèn)題。例如ImportError: pycurl: libcurl link-time version (7.37.1) is older than compile-time version (7.43.0) 朝捆。 解決了link問(wèn)題仑性,如果是mac系統(tǒng),安裝的時(shí)候可能出現(xiàn)
error: Setup script exited with error: command 'cc' failed
右蹦,多半是由于xcode做鬼诊杆,這里有一個(gè)解決說(shuō)明
AsyncHTTPClient設(shè)置成為keepalive模式是全局性的,比較tornado是單進(jìn)程單線程的何陆,訪問(wèn)三方或者微服務(wù)晨汹,都是一個(gè)客戶端,所有的模式都是持久連接贷盲。
短連接與持久連接的應(yīng)用場(chǎng)景
持久連接可以減少tcp連接的創(chuàng)建和銷毀淘这,提升服務(wù)器的處理性能。但是并不是所有連接都得使用持久連接巩剖。長(zhǎng)短連接都有其使用場(chǎng)景铝穷。
既然持久連接在于連接的持久,因此對(duì)于頻繁通信佳魔,點(diǎn)對(duì)點(diǎn)的就可以使用曙聂。例如網(wǎng)關(guān)和微服務(wù)之間。如果創(chuàng)建了持久連接鞠鲜,就必須在意連接的存活狀態(tài)宁脊。客戶端一般不會(huì)主動(dòng)關(guān)閉贤姆,因此服務(wù)端需要維護(hù)這個(gè)連接狀態(tài)榆苞,對(duì)于一些長(zhǎng)時(shí)間沒(méi)有讀寫(xiě)事件發(fā)生的連接,可以主動(dòng)斷開(kāi)霞捡,節(jié)省資源坐漏。
對(duì)于一些用完就走的場(chǎng)景,也不需要使用持久連接碧信。而另外一些需要全雙工通信赊琳,例如推送和實(shí)時(shí)應(yīng)用,則需要真正的長(zhǎng)連接音婶,比如MQTT實(shí)現(xiàn)推送和websocket實(shí)現(xiàn)實(shí)時(shí)應(yīng)用等慨畸。
總結(jié)
微服務(wù)大行其道,從微觀來(lái)看衣式,增加了更多的網(wǎng)絡(luò)IO寸士。而IO又是最耗時(shí)的操作檐什。相比之下,程式的計(jì)算速度就顯得沒(méi)那么緊要了弱卡。優(yōu)化網(wǎng)絡(luò)IO才是提升性能的關(guān)鍵乃正。一些頻繁通信的場(chǎng)景,使用持久連接或長(zhǎng)連接更能優(yōu)化大量TCP連接的創(chuàng)建和銷毀婶博。
就Python的而言瓮具,Tornado的誕生就是為了解決網(wǎng)絡(luò)IO的瓶頸。并且很多tornado及其三方庫(kù)的問(wèn)題凡人,都能在github和stackoverflow找到作者的參與和回答名党。可見(jiàn)作者對(duì)項(xiàng)目的負(fù)責(zé)挠轴。由于tornado單線程的特性传睹,因此做任何IO操作,都需要考慮是否block岸晦。幸好有AsyncHTTPClinet欧啤,既可以提供異步IO,也可以實(shí)現(xiàn)持久連接启上,當(dāng)然邢隧,tornado也支持websocket。