HTTP 持久连接
HTTP通信中,client和server一问一答的方式。HTTP是基于TCP的应用层协议,通常在发送请求之前需要创建TCP连接,然后在收到响应之后会断开这个TCP连接。这就是常见的http短连接。既然有短连接,那么也有长连接。
HTTP协议最初的设计是无连接无状态的方式。为了维护状态,引入了cookie和session方式认证识别用户。早期的web开发中,为了给用户推送数据,通常使用所谓的长连接。那时的长连接还是基于短连接的方式实现,即通过client的轮询查询,在用户层面看起来连接并没有断开。随着技术的发展,又出现了Websockt和MQTT等通信协议。Websockt和MQTT则是全双工的通信协议。
相比全双工实现的长连接,我们还会在web开发中遇到伪
长连接。即HTTP协议中的keepalive模式。因为HTTP设计是无连接设计,请求应答结束之后就关闭了TCP连接。在http通信中,就会有大量的新建和销毁tcp连接的过程,那怕是同一个用户同一个客户端。为了优化这种方式,HTTP提出了KeepAlive
模式,即创建的tcp连接后,传输数据,server返回响应之后并不会关掉tcp连接,下一次http请求就能复用这个tcp连接。
这是一种协商式的连接,毕竟每次的http发送数据的时候,还是要单独为每个请求发送header之类的信息。相比全双工的websocket,一旦创建了连接,下一次就不需要再发送header,直接发送数据即可。因此描述http的keepalive应该是持久连接(HTTP persistent connection )更准确。
keepalive 简介
HTTP的keepalive模式提供了HTTP通信的时候复用TCP连接的协商功能。http1.0默认是关闭的,只有在http的header加入 Connection: Keep-Alive
才能开启。而http1.1则正相反,默认就打开了,只有显示的在header里加入Connection: close
才能关闭。现在的浏览器基本都是http1.1的协议,能否使用长连接,权看服务器的支持状况了。下图说明了开启keepalive模式的持久连接与短连接的通信示意图
当开启了持久连接,就不能使用返回EOF
的方式来判断数据结尾了。对于静态和动态的数据,可以使用Conent-Lenght
和
Transfer-Encoding`来做应用层的区分。
requests与持久连接
了解了keeplive模式,接下来我们就来使用keepalive方式。服务器使用Tornado,tornado实现了keepalive的处理,客户端我们可以分别使用同步的requests和异步的AsyncHTTPClient。
先写一个简单的服务器:
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不愧是一个"for human" 的软件,实现一个http客户端非常简单。
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()
运行 keepalive python requests-cli.py --type=short
,可以看见返回了数据,同时通过另外一个神器wireshark
抓包如下:
从抓包的情况来看,两次http请求,一共创建了两次tcp的握手连接和挥手断开。每次发送http数据都需要先创建tcp连接,然后就断开了连接。通常是客户端发起的断开连接。
requests 持久连接
requests的官网也说明了,基于urllib3
的方式,requests百分比实现了keepalive方式,只需要创建一个客户端session即可,代码如下:
def long_connection():
s = requests.Session()
resp = s.get(url)
print(resp.text)
resp = s.get(url)
print(resp.text)
s.close()
再次通过抓包如下图:
可以看到,同样也是两次http请求,只创建了一次tcp的握手和挥手。两次http请求都基于一个tcp连接。再次查看包43,可以看到下图中的报文header指定了keepalive。
AsyncHTTPClient与持久连接
tornado是一个优秀高性能异步非阻塞(non-block)web框架。如果torando的handler中也需要请求别的三方资源,使用requests的同步网络IO,将会block住整个tornado的进程。因此tornado也实现了异步的http客户端AsyncHTTPClient
。
短连接
使用AsyncHTTPClient也不难,但是想要使用其异步效果,就必须把其加入事件循环中,否则只有连接的创立,而没有数据的传输就退出了。
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)
运行上述代码,将会看到wirshark
中,创建了两次TCP连接和断开了连接,并没有发送http数据。为了发送http数据,还需要加入tornado的事件循环。即在最后一行加入tornado.ioloop.IOLoop.instance().start()
再次运行,客户端正常收到了数据,抓包如下:
抓包的结果咋一看像是持久连接,仔细一看却有两次握手和挥手的操作。的确,客户端发送异步http请求的时候,创建了两个端口49989
和49990
两个tcp连接。因为是异步的请求,因此先创建了两个连接,然后才发送数据,发送数据的时候都是基于所创建的端口进行的。也就是没有使用持久连接。
持久连接
AsyncHTTPClient使用持久连接也很简单。现在流行微服务架构。通常提供给客户端的服务称之为网关,网关从各种微服务中调用获取数据,通信的方式中,同步的有http和rpc,异步的有mq之类的。而http通常都是使用持久连接的方式。
下面我们介绍一下在tornado server的handler中使用async client请求微服务的资源。
再写一个简单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()
然后我们请求5050端口的服务,也连接发送两次http请求:
(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%
再看我们的抓包情况:
从图中可以看到,即使是两个请求,最终都是复用了断开为50784的tcp连接。
因为asynchttpclient默认使用的是SimpleAsyncHTTPClient,实现持久连接只需要配置一下tornado.httpclient.AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
即可。当然,这个需要tornado的版本4.2以上,当前的版本是4.5。
CurlAsyncHTTPClient依赖于pycurl。pycurl又依赖libcurl。在安装pycurl的时候,可能会出现link的问题。例如ImportError: pycurl: libcurl link-time version (7.37.1) is older than compile-time version (7.43.0) 。 解决了link问题,如果是mac系统,安装的时候可能出现
error: Setup script exited with error: command 'cc' failed
,多半是由于xcode做鬼,这里有一个解决说明
AsyncHTTPClient设置成为keepalive模式是全局性的,比较tornado是单进程单线程的,访问三方或者微服务,都是一个客户端,所有的模式都是持久连接。
短连接与持久连接的应用场景
持久连接可以减少tcp连接的创建和销毁,提升服务器的处理性能。但是并不是所有连接都得使用持久连接。长短连接都有其使用场景。
既然持久连接在于连接的持久,因此对于频繁通信,点对点的就可以使用。例如网关和微服务之间。如果创建了持久连接,就必须在意连接的存活状态。客户端一般不会主动关闭,因此服务端需要维护这个连接状态,对于一些长时间没有读写事件发生的连接,可以主动断开,节省资源。
对于一些用完就走的场景,也不需要使用持久连接。而另外一些需要全双工通信,例如推送和实时应用,则需要真正的长连接,比如MQTT实现推送和websocket实现实时应用等。
总结
微服务大行其道,从微观来看,增加了更多的网络IO。而IO又是最耗时的操作。相比之下,程式的计算速度就显得没那么紧要了。优化网络IO才是提升性能的关键。一些频繁通信的场景,使用持久连接或长连接更能优化大量TCP连接的创建和销毁。
就Python的而言,Tornado的诞生就是为了解决网络IO的瓶颈。并且很多tornado及其三方库的问题,都能在github和stackoverflow找到作者的参与和回答。可见作者对项目的负责。由于tornado单线程的特性,因此做任何IO操作,都需要考虑是否block。幸好有AsyncHTTPClinet,既可以提供异步IO,也可以实现持久连接,当然,tornado也支持websocket。