魚相信要成為一個(gè)更好的開發(fā)人員敞峭,你必須更好地理解你每天使用的底層軟件系統(tǒng)踊谋,包括編程語言、編譯器和解釋器旋讹、數(shù)據(jù)庫和操作系統(tǒng)殖蚕、web服務(wù)器和web框架轿衔。
引言
有一天,一個(gè)女人出去散步到一個(gè)建筑工地睦疫,看見三個(gè)男人在工作呀枢。
她問第一個(gè)男人:“你在干什么?”第一個(gè)男人被這個(gè)問題惹惱了笼痛,叫道:“你沒看見我在砌磚嗎?”
她對(duì)回答不滿意琅拌,問第二個(gè)人在做什么缨伊。第二個(gè)男人回答說:“我在建一堵磚墻〗Γ”然后刻坊,他把注意力轉(zhuǎn)向第一個(gè)男人,說:“嘿党晋,你剛砌過了墻的盡頭谭胚。你需要把最后一塊磚頭摘下來∥床#”
她又一次對(duì)答案不滿意灾而,問第三個(gè)人在干什么。那人抬頭望著天空對(duì)她說:“我正在建造世界上最大的大教堂扳剿∨蕴耍”當(dāng)他站在那里仰望天空時(shí),另外兩個(gè)人開始為那塊亂七八糟的磚頭爭(zhēng)吵起來庇绽。男人轉(zhuǎn)身對(duì)前兩個(gè)人說:“嘿锡搜,伙計(jì)們,別擔(dān)心那塊磚頭瞧掺。這是一個(gè)內(nèi)墻耕餐,它會(huì)被粉刷過,沒有人會(huì)看到那塊磚辟狈。換一層吧肠缔。”
這個(gè)故事的寓意是上陕,當(dāng)你了解整個(gè)系統(tǒng)桩砰,了解不同部分(磚塊、墻壁释簿、大教堂)如何組合在一起時(shí)亚隅,你可以更快地識(shí)別和解決問題(錯(cuò)誤的磚塊)。
文章這樣的開頭庶溶,與《創(chuàng)建一個(gè)簡(jiǎn)單的Web服務(wù)器》有什么關(guān)系煮纵?
魚相信要成為一個(gè)更好的開發(fā)人員懂鸵,你必須更好地理解你每天使用的底層軟件系統(tǒng),包括編程語言行疏、編譯器和解釋器匆光、數(shù)據(jù)庫和操作系統(tǒng)、web服務(wù)器和web框架酿联。而且终息,為了更好更深入地了解這些系統(tǒng),你必須從頭開始贞让,一塊一塊地周崭,一堵墻一堵墻地重新構(gòu)建它們。
孔子這樣說:
聽而易忘
見而易記
做而易懂
魚希望你可以相信喳张,對(duì)不同的軟件系統(tǒng)進(jìn)行造輪子续镇,來學(xué)習(xí)它們的工作方式,是一個(gè)好辦法销部。
在這個(gè)由三部分組成的系列中摸航,魚將向你展示如何構(gòu)建自己的基本W(wǎng)eb服務(wù)器。Here we go舅桩!
初識(shí)Web服務(wù)器
首先酱虎,什么是Web服務(wù)器?
簡(jiǎn)單滴說江咳,它是一個(gè)網(wǎng)絡(luò)服務(wù)器逢净,位于物理服務(wù)器之上(沒看錯(cuò),就是服務(wù)器上的服務(wù)器)歼指,然后它等待客戶端發(fā)送請(qǐng)求爹土。當(dāng)它接收到請(qǐng)求時(shí),它生成一個(gè)響應(yīng)并將其發(fā)送回客戶端踩身≌鸵穑客戶端和服務(wù)器之間的通信使用HTTP協(xié)議進(jìn)行⌒瑁客戶端可以是你的瀏覽器琼娘,也可以是任何其他講HTTP的軟件。
最簡(jiǎn)單的Web服務(wù)器
一個(gè)非常簡(jiǎn)單的Web服務(wù)器實(shí)現(xiàn)是什么樣子的附鸽?
可以看下魚的這個(gè)例子脱拼,這個(gè)例子是用Python編寫的(在Python3.7+上進(jìn)行了測(cè)試),但是即使你不了解Python(這是一種很容易掌握的語言坷备,請(qǐng)嘗試Oㄅā)你仍然應(yīng)該能夠從下面的代碼和解釋中理解概念:
# Python3.7+
import socket
HOST, PORT = '', 8888
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind((HOST, PORT))
listen_socket.listen(1)
print(f'Serving HTTP on port {PORT} ...')
while True:
client_connection, client_address = listen_socket.accept()
request_data = client_connection.recv(1024)
print(request_data.decode('utf-8'))
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
client_connection.close()
將以上代碼保存為webserver1.py
,然后使用以下命令運(yùn)行它:
$ python webserver1.py
Serving HTTP on port 8888 …
現(xiàn)在省撑,在Web瀏覽器的地址欄輸入http://localhost:8888/hello
赌蔑,按Enter俯在,然后你應(yīng)該看到“Hello World!”顯示在瀏覽器中娃惯,如下所示:
快去實(shí)踐一遍吧跷乐,簡(jiǎn)單地一匹!
分析Web服務(wù)器工作原理
魚們來分析一下它到底是如何工作的趾浅。
首先讓魚們從你輸入的網(wǎng)址開始愕提。它被稱為URL,其基本結(jié)構(gòu)如下
這個(gè)就是你要告訴瀏覽器的地址皿哨,相當(dāng)于你要求瀏覽器查找和連接到的Web服務(wù)器的地址揪荣,以及服務(wù)器上要為你獲取的頁面(路徑)。
但是往史,在瀏覽器發(fā)送HTTP請(qǐng)求之前,它首先需要與Web服務(wù)器建立TCP連接佛舱。然后瀏覽器通過TCP連接向服務(wù)器發(fā)送HTTP請(qǐng)求椎例,并等待服務(wù)器發(fā)送HTTP響應(yīng)。
當(dāng)你的瀏覽器收到來自服務(wù)器的應(yīng)答時(shí)请祖,它會(huì)顯示出來订歪,在這種情況下,它會(huì)顯示“Hello, World!”
現(xiàn)在肆捕,讓魚們來更詳細(xì)地探討刷晋,客戶端和服務(wù)器在發(fā)送HTTP請(qǐng)求和響應(yīng)之前如何建立TCP連接。為此慎陵,魚使用所謂的套接字進(jìn)行模擬眼虱。你將通過在命令行上使用telnet手動(dòng)模擬瀏覽器,而不是直接使用瀏覽器席纽。
在運(yùn)行Web服務(wù)器的同一臺(tái)計(jì)算機(jī)上捏悬,在命令行上啟動(dòng)telnet會(huì)話,指定要連接到本地主機(jī)和8888的端口润梯,然后按Enter鍵:
$ telnet localhost 8888
Trying 127.0.0.1 …
Connected to localhost.
此時(shí)过牙,你已經(jīng)與本地主機(jī)上運(yùn)行的服務(wù)器建立了TCP連接,并準(zhǔn)備發(fā)送和接收HTTP消息纺铭。在下面的圖片中寇钉,你可以看到一個(gè)服務(wù)器必須經(jīng)過的標(biāo)準(zhǔn)過程,才能接受新的TCP連接舶赔。
更多地關(guān)注造殼扫倡。
同時(shí),魚們繼續(xù)試驗(yàn)顿痪,在同一個(gè)telnet會(huì)話中镊辕,輸入“GET /hello HTTP/1.1”油够,然后按Enter:
$ telnet localhost 8888
Trying 127.0.0.1 …
Connected to localhost.
GET /hello HTTP/1.1
HTTP/1.1 200 OK
Hello, World!
就在此時(shí),你手動(dòng)模擬了你的瀏覽器征懈!你發(fā)送了一個(gè)HTTP請(qǐng)求并得到了一個(gè)HTTP響應(yīng)石咬。這是HTTP請(qǐng)求的基本結(jié)構(gòu):
HTTP請(qǐng)求由這些元素組成:
- HTTP方法(GET)
- 表示所需服務(wù)器上的“頁面”的路徑(/hello)
- 協(xié)議版本(HTTP/1.1)
為了簡(jiǎn)單起見,魚們的Web服務(wù)器此時(shí)完全忽略了上面的請(qǐng)求數(shù)據(jù)卖哎。你也可以輸入任何垃圾而不是“GET/hello HTTP/1.1”鬼悠,然后你仍然會(huì)得到一個(gè)“hello,World亏娜!”回應(yīng)焕窝。
輸入請(qǐng)求行并按Enter鍵后,客戶機(jī)將請(qǐng)求發(fā)送到服務(wù)器维贺,服務(wù)器讀取請(qǐng)求行它掂,打印請(qǐng)求行并返回正確的HTTP響應(yīng)。
以下是服務(wù)器發(fā)送回客戶端的HTTP響應(yīng)(在本例中為telnet):
讓魚們解剖一下溯泣。HTTP響應(yīng)由這幾個(gè)元素組成:
- 響應(yīng)狀態(tài)行(協(xié)議版本+狀態(tài)碼/返回碼)虐秋,HTTP/1.1 200 OK,
- 一個(gè)必需的空行垃沦,
- HTTP響應(yīng)體客给。
響應(yīng)狀態(tài)行HTTP/1.1 200 OK
由HTTP協(xié)議版本、HTTP狀態(tài)碼和HTTP狀態(tài)碼原因短語OK組成肢簿。當(dāng)瀏覽器得到響應(yīng)時(shí)靶剑,它會(huì)顯示響應(yīng)的主體,這就是為什么你會(huì)看到“Hello池充,World桩引!”在你的瀏覽器中。
這就是Web服務(wù)器工作的基本模式收夸〔郏總而言之:
- Web服務(wù)器創(chuàng)建一個(gè)監(jiān)聽套接字,并開始在循環(huán)中接受新連接咱圆。
- 客戶端啟動(dòng)一個(gè)TCP連接笛辟,并在成功建立它之后,客戶端向服務(wù)器發(fā)送一個(gè)HTTP請(qǐng)求序苏,服務(wù)器返回HTTP響應(yīng)結(jié)果手幢,其中包含了展示給用戶看的響應(yīng)內(nèi)容。
要建立TCP連接忱详,客戶端和服務(wù)器都使用套接字围来。
現(xiàn)在你有了一個(gè)非常基本的工作Web服務(wù)器,你可以使用瀏覽器或其他HTTP客戶端進(jìn)行測(cè)試监透。正如你已經(jīng)看到并希望嘗試過的那樣桶错,通過使用telnet并手動(dòng)鍵入HTTP請(qǐng)求,你也可以成為一個(gè)人工HTTP客服端胀蛮。
接下來魚們要提出一個(gè)問題:
“在不對(duì)服務(wù)器進(jìn)行任何更改的前提下院刁,你如何在新開發(fā)的Web服務(wù)器下,運(yùn)行/適配不同的Django應(yīng)用程序粪狼、Flask應(yīng)用程序和pyrampid應(yīng)用程序”
解耦Web服務(wù)器和Python應(yīng)用程序 —— WSGI
在過去退腥,你對(duì)Python Web框架的選擇會(huì)限制你對(duì)可用Web服務(wù)器的選擇,反之亦然再榄。如果框架和服務(wù)器設(shè)計(jì)為協(xié)同工作狡刘,那么一般是可行的:
但是,當(dāng)你嘗試將服務(wù)器和非設(shè)計(jì)為協(xié)同工作的框架結(jié)合在一起時(shí)困鸥,可能會(huì)遇到不match的問題:
基本上嗅蔬,你必須使用協(xié)同工作的東西,但有可能不是你想要使用的東西疾就。比如你希望用ServerA的某個(gè)特性和FrameworkB的某個(gè)功能倦青,但是FrameworkA不能滿足你浴鸿。
那么专缠,如何確倍炝洌可以使用多個(gè)Web框架運(yùn)行Web服務(wù)器吴趴,而不必對(duì)Web服務(wù)器或Web框架進(jìn)行代碼更改呢漆诽?這個(gè)問題的答案就是Python Web服務(wù)器網(wǎng)關(guān)接口(簡(jiǎn)稱WSGI,發(fā)音為wizgy)锣枝。
WSGI允許開發(fā)人員將Web框架與Web服務(wù)器解耦∠崾茫現(xiàn)在,你可以混合和匹配Web服務(wù)器和Web框架撇叁,并選擇適合你需要的配對(duì)供鸠。例如,可以使用Gunicorn陨闹、Nginx/uWSGI或Waitress運(yùn)行Django楞捂、Flask或Pyramid。這樣的解耦趋厉,得益于服務(wù)器和框架中的WSGI支持:
因此寨闹,WSGI就是問題的答案。你的Web服務(wù)器必須實(shí)現(xiàn)WSGI接口的服務(wù)器部分君账,并且要求所有的python web框架都已經(jīng)實(shí)現(xiàn)了WSGI接口的框架端繁堡。這樣,就可以將它們混合使用,而無需修改服務(wù)器代碼以適應(yīng)特定的Web框架椭蹄。
現(xiàn)在你知道闻牡,Web服務(wù)器和Web框架對(duì)WSGI的支持允許你選擇適合你的配對(duì),但這也有利于服務(wù)器和框架開發(fā)人員绳矩,因?yàn)樗麄兛梢詫W⒂谧约合矚g的專業(yè)領(lǐng)域罩润,而不是相互干涉。其他語言也有類似的接口:例如埋酬,Java有Servlet API哨啃,Ruby有Rack。
編寫自己的WSGI服務(wù)器
理想很美好写妥,但凡事都得“Show me the code”拳球。魚們來看看這個(gè)非常簡(jiǎn)單的WSGI服務(wù)器實(shí)現(xiàn):
# Tested with Python 3.7+ (Mac OS X)
import io
import socket
import sys
class WSGIServer(object):
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
request_queue_size = 1
def __init__(self, server_address):
# Create a listening socket
self.listen_socket = listen_socket = socket.socket(
self.address_family,
self.socket_type
)
# Allow to reuse the same address
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind
listen_socket.bind(server_address)
# Activate
listen_socket.listen(self.request_queue_size)
# Get server host name and port
host, port = self.listen_socket.getsockname()[:2]
self.server_name = socket.getfqdn(host)
self.server_port = port
# Return headers set by Web framework/Web application
self.headers_set = []
def set_app(self, application):
self.application = application
def serve_forever(self):
listen_socket = self.listen_socket
while True:
# New client connection
self.client_connection, client_address = listen_socket.accept()
# Handle one request and close the client connection. Then
# loop over to wait for another client connection
self.handle_one_request()
def handle_one_request(self):
request_data = self.client_connection.recv(1024)
self.request_data = request_data = request_data.decode('utf-8')
# Print formatted request data a la 'curl -v'
print(''.join(
f'< {line}\n' for line in request_data.splitlines()
))
self.parse_request(request_data)
# Construct environment dictionary using request data
env = self.get_environ()
# It's time to call our application callable and get
# back a result that will become HTTP response body
result = self.application(env, self.start_response)
# Construct a response and send it back to the client
self.finish_response(result)
def parse_request(self, text):
request_line = text.splitlines()[0]
request_line = request_line.rstrip('\r\n')
# Break down the request line into components
(self.request_method, # GET
self.path, # /hello
self.request_version # HTTP/1.1
) = request_line.split()
def get_environ(self):
env = {}
# The following code snippet does not follow PEP8 conventions
# but it's formatted the way it is for demonstration purposes
# to emphasize the required variables and their values
#
# Required WSGI variables
env['wsgi.version'] = (1, 0)
env['wsgi.url_scheme'] = 'http'
env['wsgi.input'] = io.StringIO(self.request_data)
env['wsgi.errors'] = sys.stderr
env['wsgi.multithread'] = False
env['wsgi.multiprocess'] = False
env['wsgi.run_once'] = False
# Required CGI variables
env['REQUEST_METHOD'] = self.request_method # GET
env['PATH_INFO'] = self.path # /hello
env['SERVER_NAME'] = self.server_name # localhost
env['SERVER_PORT'] = str(self.server_port) # 8888
return env
def start_response(self, status, response_headers, exc_info=None):
# Add necessary server headers
server_headers = [
('Date', 'Mon, 15 Jul 2019 5:54:48 GMT'),
('Server', 'WSGIServer 0.2'),
]
self.headers_set = [status, response_headers + server_headers]
# To adhere to WSGI specification the start_response must return
# a 'write' callable. We simplicity's sake we'll ignore that detail
# for now.
# return self.finish_response
def finish_response(self, result):
try:
status, response_headers = self.headers_set
response = f'HTTP/1.1 {status}\r\n'
for header in response_headers:
response += '{0}: {1}\r\n'.format(*header)
response += '\r\n'
for data in result:
response += data.decode('utf-8')
# Print formatted response data a la 'curl -v'
print(''.join(
f'> {line}\n' for line in response.splitlines()
))
response_bytes = response.encode()
self.client_connection.sendall(response_bytes)
finally:
self.client_connection.close()
SERVER_ADDRESS = (HOST, PORT) = '', 8888
def make_server(server_address, application):
server = WSGIServer(server_address)
server.set_app(application)
return server
if __name__ == '__main__':
if len(sys.argv) < 2:
sys.exit('Provide a WSGI application object as module:callable')
app_path = sys.argv[1]
module, application = app_path.split(':')
module = __import__(module)
application = getattr(module, application)
httpd = make_server(SERVER_ADDRESS, application)
print(f'WSGIServer: Serving HTTP on port {PORT} ...\n')
httpd.serve_forever()
代碼還是比較簡(jiǎn)單的(不到150行),大家都可以理解珍特,應(yīng)該不會(huì)出現(xiàn)陷入細(xì)節(jié)泥潭的情況祝峻。上面的服務(wù)器還可以做更多的事情——它可以運(yùn)行用你喜愛的Web框架編寫的基本W(wǎng)eb應(yīng)用程序,無論是Pyramid扎筒、Flask莱找、Django還是其他Python WSGI框架。
讓魚們來運(yùn)行試試看嗜桌。將上述代碼保存為webserver2.py
奥溺。如果你試圖在沒有任何參數(shù)的情況下運(yùn)行它,它會(huì)提醒錯(cuò)誤并退出骨宠。
$ python webserver2.py
Provide a WSGI application object as module:callable
它真的需要你的Web應(yīng)用服務(wù)浮定。要運(yùn)行服務(wù)器,只需要安裝Python层亿。但是要運(yùn)行使用Pyramid桦卒、Flask和Django編寫的應(yīng)用程序,你需要先安裝這些框架匿又,讓魚們把這三個(gè)都安裝好方灾。魚首選的方法是使用venv創(chuàng)建一個(gè)虛擬環(huán)境,以免影響現(xiàn)有環(huán)境(venv在Python3.3版本以及以上默認(rèn)自帶)碌更。只需按照下面的步驟創(chuàng)建并激活一個(gè)虛擬環(huán)境裕偿,然后安裝所有三個(gè)Web框架。
$ python3 -m venv lsbaws
$ ls lsbaws
bin include lib pyvenv.cfg
$ source lsbaws/bin/activate
(lsbaws) $ pip install -U pip
(lsbaws) $ pip install pyramid
(lsbaws) $ pip install flask
(lsbaws) $ pip install django
搭建WSGI + Pyramid應(yīng)用程序
此時(shí)痛单,你需要?jiǎng)?chuàng)建一個(gè)Web應(yīng)用程序击费。魚們先從Pyramid開始。將以下代碼另存為金字塔app.py
保存到同一目錄webserver2.py
:
from pyramid.config import Configurator
from pyramid.response import Response
def hello_world(request):
return Response(
'Hello world from Pyramid!\n',
content_type='text/plain',
)
config = Configurator()
config.add_route('hello', '/hello')
config.add_view(hello_world, route_name='hello')
app = config.make_wsgi_app()
現(xiàn)在桦他,你可以使用自己的Web服務(wù)器蔫巩,來啟動(dòng)你的Pyramid應(yīng)用程序了:
(lsbaws) $ python webserver2.py pyramidapp:app
WSGIServer: Serving HTTP on port 8888 ...
你剛才告訴服務(wù)器從python模塊“Pyramid app”加載可調(diào)用的“app”谆棱,你的服務(wù)器現(xiàn)在可以接收請(qǐng)求并將它們轉(zhuǎn)發(fā)到你的pyramid中叫app的應(yīng)用程序。
同時(shí)圆仔,從webserver2.py
代碼中可以看出垃瞧,應(yīng)用程序現(xiàn)在只處理一個(gè)路由:/hello路由。在瀏覽器網(wǎng)站上輸入http://localhost:8888/hello
地址坪郭,按回車鍵个从,然后觀察結(jié)果:
當(dāng)然,你也可以在命令行中使用curl工具歪沃,也能達(dá)到同樣的效果:
$ curl -v http://localhost:8888/hello
...
你可以看下服務(wù)器和curl都輸出了些什么嗦锐。
搭建WSGI + Flask應(yīng)用程序
現(xiàn)在魚們移步到Flask,跟著魚一起做:
from flask import Flask
from flask import Response
flask_app = Flask('flaskapp')
@flask_app.route('/hello')
def hello_world():
return Response(
'Hello world from Flask!\n',
mimetype='text/plain'
)
app = flask_app.wsgi_app
將上述代碼保存為flaskapp.py
沪曙,然后允許它:
(lsbaws) $ python webserver2.py flaskapp:app
WSGIServer: Serving HTTP on port 8888 ...
在瀏覽器網(wǎng)站上輸入http://localhost:8888/hello
地址奕污,按回車鍵:
一樣的,你也可以在命令行中使用curl工具:
$ curl -v http://localhost:8888/hello
...
搭建WSGI + Django應(yīng)用程序
服務(wù)器還可以處理Django應(yīng)用程序嗎液走?當(dāng)然可以碳默,試試看!不過缘眶,它涉及的內(nèi)容稍微多一些嘱根,魚建議復(fù)制整個(gè)repo并使用djangoapp.py
。
下面的源代碼基本上將Django helloworld項(xiàng)目(使用Django的Django-admin.py startproject
命令預(yù)先創(chuàng)建)添加到當(dāng)前Python路徑巷懈,然后導(dǎo)入項(xiàng)目的WSGI應(yīng)用程序该抒。
import sys
sys.path.insert(0, './helloworld')
from helloworld import wsgi
app = wsgi.application
將這段代碼保存為djangoapp.py
,然后運(yùn)行起來:
(lsbaws) $ python webserver2.py djangoapp:app
WSGIServer: Serving HTTP on port 8888 ...
在瀏覽器網(wǎng)站上輸入地址顶燕,按回車鍵:
雖然已經(jīng)運(yùn)行過很多次凑保,但是你依然可以在命令行中使用curl工具,為了驗(yàn)證Django:
$ curl -v http://localhost:8888/hello
...
ok割岛,到目前為止,魚們把三個(gè)服務(wù)器都輪了一遍犯助,如果你還沒親手試過癣漆,最好動(dòng)下手,看是沒啥用的剂买。
WSGI程序分析
好吧惠爽,你已經(jīng)體驗(yàn)過WSGI的強(qiáng)大功能:它允許你混合匹配你的Web服務(wù)器和Web框架。
WSGI在Python Web服務(wù)器和pythonweb框架之間提供了一個(gè)最小的接口瞬哼。WSGI很簡(jiǎn)單婚肆,而且很容易在服務(wù)器端和框架端實(shí)現(xiàn)。
魚們來分析下魚們之前實(shí)現(xiàn)的WSGI的代碼坐慰。
以下代碼段顯示了服務(wù)器和接口的框架端:
def run_application(application):
"""Server code."""
# This is where an application/framework stores
# an HTTP status and HTTP response headers for the server
# to transmit to the client
headers_set = []
# Environment dictionary with WSGI/CGI variables
environ = {}
def start_response(status, response_headers, exc_info=None):
headers_set[:] = [status, response_headers]
# Server invokes the ‘a(chǎn)pplication' callable and gets back the
# response body
result = application(environ, start_response)
# Server builds an HTTP response and transmits it to the client
...
def app(environ, start_response):
"""A barebones WSGI app."""
start_response('200 OK', [('Content-Type', 'text/plain')])
return [b'Hello world!']
run_application(app)
它的工作原理如下:
- 框架提供了一個(gè)可調(diào)用的“應(yīng)用程序”(WSGI規(guī)范沒有規(guī)定應(yīng)該如何實(shí)現(xiàn))较性,指的就是魚們的業(yè)務(wù)代碼
- 服務(wù)器為從HTTP客戶端接收的每個(gè)請(qǐng)求調(diào)用可調(diào)用的“應(yīng)用程序”。它將包含WSGI/CGI變量的字典“environ”和可作為參數(shù)調(diào)用的“start_response”傳遞給可調(diào)用的“application”。
- 框架/應(yīng)用程序生成一個(gè)HTTP狀態(tài)和HTTP響應(yīng)頭赞咙,并將它們傳遞給服務(wù)器可調(diào)用的“start_response”來存儲(chǔ)它們责循。框架/應(yīng)用程序還返回一個(gè)響應(yīng)體攀操。
- 服務(wù)器將狀態(tài)院仿、響應(yīng)頭和響應(yīng)體組合成一個(gè)HTTP響應(yīng)并將其傳輸?shù)娇蛻舳耍ù瞬襟E不是規(guī)范的一部分,但它是流中的下一個(gè)邏輯步驟速和,一般都是需要的)
下面是界面的可視化表示:
到目前為止歹垫,魚們已經(jīng)輪過了Pyramid、Flask和Django Web應(yīng)用程序颠放,還看到了實(shí)現(xiàn)WSGI規(guī)范的服務(wù)器端的服務(wù)器代碼排惨。
當(dāng)你使用這些框架之一編寫Web應(yīng)用程序時(shí),你可以在更高的級(jí)別上工作慈迈,而不直接使用WSGI若贮。
但魚知道你對(duì)WSGI接口的框架方面也很好奇,因?yàn)槟阏陂喿x本文痒留。
更多地關(guān)注造殼谴麦。
因此,讓魚們創(chuàng)建一個(gè)極簡(jiǎn)的WSGI Web應(yīng)用程序/Web框架伸头,而不使用Pyramid匾效、Flask或Django,并在服務(wù)器上運(yùn)行它:
def app(environ, start_response):
"""A barebones WSGI application.
This is a starting point for your own Web framework :)
"""
status = '200 OK'
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)
return [b'Hello world from a simple WSGI application!\n']
將上述代碼保存為wsgiapp.py
恤磷,運(yùn)行它:
(lsbaws) $ python webserver2.py wsgiapp:app
WSGIServer: Serving HTTP on port 8888 ...
在瀏覽器網(wǎng)站上輸入地址面哼,按回車鍵:
這樣,魚們就實(shí)現(xiàn)了一個(gè)極簡(jiǎn)的WSGI Web“框架”扫步,沒錯(cuò)魔策,就是這么簡(jiǎn)單。
現(xiàn)在河胎,讓魚們回到服務(wù)器向客戶端傳輸?shù)膬?nèi)容闯袒。
下面是服務(wù)器在使用HTTP客戶端調(diào)用金字塔應(yīng)用程序時(shí)生成的HTTP響應(yīng):
這個(gè)響應(yīng)有一些是你在前文中能看到的熟悉部分,但它也有一些新的內(nèi)容游岳。例如政敢,它有四個(gè)你以前從未見過的HTTP頭:內(nèi)容類型、內(nèi)容長度胚迫、日期和服務(wù)器喷户。其實(shí)這些是Web服務(wù)器響應(yīng)通常應(yīng)該具有的頭。不過访锻,這些都不是嚴(yán)格要求的褪尝。報(bào)頭的目的是傳輸關(guān)于HTTP請(qǐng)求/響應(yīng)的附加信息闹获。
現(xiàn)在你已經(jīng)了解了更多關(guān)于WSGI接口的信息,同樣的恼五,下面HTTP響應(yīng)昌罩,其中包含了有關(guān)生成它的部分的更多信息:
魚還沒有提到“environ”字典,但基本上它是一個(gè)Python字典灾馒,必須包含WSGI規(guī)范指定的某些WSGI和CGI變量茎用。服務(wù)器在分析請(qǐng)求后從HTTP請(qǐng)求中獲取字典的值。這就是字典的內(nèi)容:
Web框架使用該字典中的信息睬罗,根據(jù)指定的路由轨功、請(qǐng)求方法等信息,決定使用哪個(gè)視圖容达、從何處讀取請(qǐng)求體以及在何處寫入錯(cuò)誤(如果有的話)古涧。
現(xiàn)在,你已經(jīng)創(chuàng)建了自己的WSGI Web服務(wù)器花盐,并使用不同的Web框架編寫了Web應(yīng)用程序羡滑。而且,你還創(chuàng)建了一個(gè)簡(jiǎn)單的Web應(yīng)用程序/Web框架算芯。
讓魚們回顧一下你的WSGI Web服務(wù)器必須做些什么來服務(wù)針對(duì)WSGI應(yīng)用程序的請(qǐng)求:
首先柒昏,服務(wù)器啟動(dòng)并加載Web框架/應(yīng)用程序提供的可調(diào)用的“應(yīng)用程序”;
然后熙揍,服務(wù)器讀取一個(gè)請(qǐng)求职祷;
然后,服務(wù)器解析它届囚;
然后有梆,它使用請(qǐng)求數(shù)據(jù)構(gòu)建一個(gè)“environ”字典;
然后意系,它用“environ”字典調(diào)用“application”泥耀,用“start_response”作為參數(shù)調(diào)用“start_response”,并返回一個(gè)響應(yīng)體蛔添;
然后痰催,服務(wù)器使用對(duì)“application”對(duì)象的調(diào)用返回的數(shù)據(jù)以及可調(diào)用的“start_response”設(shè)置的狀態(tài)和響應(yīng)頭來構(gòu)造HTTP響應(yīng);
最后作郭,服務(wù)器將HTTP響應(yīng)發(fā)送回客戶端陨囊。
就是這些步驟弦疮,貫穿了魚們的整個(gè)服務(wù)流程夹攒。
現(xiàn)在你有了一個(gè)可以工作的WSGI服務(wù)器,它可以為使用符合WSGI的Web框架(如Django胁塞、Flask咏尝、Pyramid或你自己的WSGI框架)編寫的基本W(wǎng)eb應(yīng)用程序提供服務(wù)压语。最理想的,是服務(wù)器可以與多個(gè)Web框架一起使用编检,而不需要對(duì)服務(wù)器代碼庫進(jìn)行任何更改胎食。
但這還不夠完美,甚至還有明顯的缺點(diǎn)允懂。
魚們來思考一下:“為了提高你的程序的性能厕怜,你如何讓你的服務(wù)器一次處理多個(gè)請(qǐng)求?”
創(chuàng)建并發(fā)服務(wù)器
在前面蕾总,魚們創(chuàng)建了一個(gè)極簡(jiǎn)的WSGI服務(wù)器粥航,它可以處理基本的HTTP GET請(qǐng)求。但是生百,它是一個(gè)“迭代服務(wù)器”递雀,一次處理一個(gè)客戶機(jī)請(qǐng)求。在處理完當(dāng)前客戶端請(qǐng)求之前蚀浆,它無法接受新連接缀程。有些客戶端可能不滿意,因?yàn)樗麄儗⒉坏貌慌抨?duì)等候市俊,而對(duì)于繁忙的服務(wù)器杨凑,排隊(duì)現(xiàn)象尤其嚴(yán)重。
串行的“迭代服務(wù)器”
魚們來看一眼魚們的“迭代服務(wù)器”秕衙,webserver3a.py:
#####################################################################
# Iterative server - webserver3a.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
#####################################################################
import socket
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5
def handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
while True:
client_connection, client_address = listen_socket.accept()
handle_request(client_connection)
client_connection.close()
if __name__ == '__main__':
serve_forever()
為了更直觀地觀察服務(wù)器一次只處理一個(gè)請(qǐng)求蠢甲,魚們稍微“降低一下性能”,修改服務(wù)器并在向客戶端發(fā)送響應(yīng)后添加60秒延遲据忘。
“降低性能”的代碼保存為webserver3b.py
:
#########################################################################
# Iterative server - webserver3b.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
# #
# - Server sleeps for 60 seconds after sending a response to a client #
#########################################################################
import socket
import time
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5
def handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
time.sleep(60) # sleep and block the process for 60 seconds
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
while True:
client_connection, client_address = listen_socket.accept()
handle_request(client_connection)
client_connection.close()
if __name__ == '__main__':
serve_forever()
然后運(yùn)行起來:
$ python webserver3b.py
在命令行中請(qǐng)求一下鹦牛,你馬上就能看到“Hello, World!”:
$ curl http://localhost:8888/hello
Hello, World!
然后,緊接著勇吊,魚們趕緊發(fā)起第二個(gè)請(qǐng)求:
$ curl http://localhost:8888/hello
如果你足夠地“趕緊”曼追,在60秒內(nèi)完成了發(fā)起了這兩個(gè)請(qǐng)求,那么第二個(gè)請(qǐng)求應(yīng)該不會(huì)馬上產(chǎn)生任何輸出汉规,而應(yīng)該只是hung在那里礼殊。服務(wù)器也不應(yīng)該在其標(biāo)準(zhǔn)輸出上打印新的請(qǐng)求體。下面是在魚的Mac上的情況(右下角以黃色突出顯示的窗口顯示第二個(gè)curl命令掛起针史,等待服務(wù)器接受連接):
當(dāng)你等了60秒到了晶伦,你就會(huì)看到第二個(gè)“Hello, World!”出現(xiàn)了,然后服務(wù)器繼續(xù)hung住60秒啄枕。
魚們看到服務(wù)器完成對(duì)第一個(gè)curl客戶機(jī)請(qǐng)求的服務(wù)婚陪,然后僅在第二個(gè)請(qǐng)求休眠60秒后才開始處理它。這一切都是因?yàn)椤暗?wù)器”是按順序或迭代地進(jìn)行的频祝,一步一個(gè)步驟泌参,或者在魚們的情況下脆淹,一次處理一個(gè)請(qǐng)求。
Socket沽一、進(jìn)程盖溺、文件描述符是什么
為了更好地分析魚們?cè)趺唇鉀Q這個(gè)性能問題,魚們來談?wù)効蛻舳撕头?wù)器之間的通信铣缠。
魚們?yōu)榱俗寖蓚€(gè)程序通過網(wǎng)絡(luò)相互通信烘嘱,必須使用套接字/Socket。前面的代碼魚們使用了Socker蝗蛙,那么什么是Socket拙友?
Socket是通信端點(diǎn)的抽象,它允許你的程序使用文件描述符與另一個(gè)程序通信歼郭。在本文中遗契,魚將特別討論Linux/Mac OS X上的TCP/IP socket。
其中病曾,魚們需要理解牍蜂,什么是Socket Pair(套接字對(duì))?
TCP連接的Socket Pair是一個(gè)4元組泰涂,用于標(biāo)識(shí)TCP連接的兩個(gè)端點(diǎn):本地IP地址鲫竞、本地端口、外部IP地址和外部端口逼蒙。Socket Pair唯一標(biāo)識(shí)網(wǎng)絡(luò)上的每個(gè)TCP連接从绘。標(biāo)識(shí)每個(gè)連接點(diǎn)的兩個(gè)值(IP地址和端口號(hào))通常稱為Socket。
所以是牢,元組{10.10.10.2:49152, 12.12.12.3:8888}是一個(gè)Socket Pair僵井,它唯一地標(biāo)識(shí)出客戶端上兩個(gè)終端的TCP連接;而元組 {12.12.12.3:8888, 10.10.10.2:49152}也是一個(gè)Socket Pair驳棱,標(biāo)識(shí)出服務(wù)器上兩個(gè)終端的TCP連接批什。地址12.12.12.3和端口8888兩個(gè)值,能標(biāo)識(shí)TCP連接的服務(wù)器端點(diǎn)社搅,在這里魚們稱之為Socket(客戶端依然)驻债。
服務(wù)器創(chuàng)建Socket并開始接受客戶端連接的標(biāo)準(zhǔn)順序如下:
- 服務(wù)器創(chuàng)建TCP/IP Socket。這是通過Python語句完成的:
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- 服務(wù)器可能會(huì)設(shè)置一些Socket參數(shù)選項(xiàng)(這是可選的參數(shù)形葬。魚們可以看到上面實(shí)現(xiàn)過的服務(wù)器代碼合呐,使用的是REUSEADDR,正是為了能夠反復(fù)使用同一地址)笙以。
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- 然后淌实,服務(wù)器綁定地址。bind函數(shù)為Socket分配一個(gè)本地協(xié)議地址。對(duì)于TCP翩伪,調(diào)用bind允許你指定端口號(hào)、IP地址谈息,或者兩者都指定缘屹,或者都不指定。
listen_socket.bind(SERVER_ADDRESS)
- 然后侠仇,監(jiān)聽轻姿。
listen_socket.listen(REQUEST_QUEUE_SIZE)
listen方法僅由服務(wù)器調(diào)用,客戶端沒有逻炊。它告訴內(nèi)核應(yīng)該接受這個(gè)Socket的傳入連接請(qǐng)求互亮。
完成后,服務(wù)器開始在一個(gè)循環(huán)中一次接受一個(gè)客戶端連接余素。當(dāng)有可用的連接時(shí)豹休,accept調(diào)用返回已連接的客戶端Socket。然后桨吊,服務(wù)器從連接的客戶端Socket中讀取請(qǐng)求數(shù)據(jù)威根,在其標(biāo)準(zhǔn)輸出上打印數(shù)據(jù),并將消息發(fā)送回客戶端。然后,服務(wù)器關(guān)閉客戶端連接嘹锁,并準(zhǔn)備再次接受新的客戶端連接誊锭。
魚們?cè)賮砜纯纯蛻舳送ㄟ^TCP/IP與服務(wù)器通信所需執(zhí)行的操作:
魚們?cè)倏纯纯蛻舳诉B接服務(wù)器再打印返回的代碼,比較簡(jiǎn)單:
import socket
# create a socket and connect to a server
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 8888))
# send and receive some data
sock.sendall(b'test')
data = sock.recv(1024)
print(data.decode())
客戶端創(chuàng)建完Socket之后竿音,需要連接服務(wù)端,這是通過connect函數(shù)做到的:
sock.connect(('localhost', 8888))
客戶端只需提供遠(yuǎn)程IP地址或主機(jī)名以及要連接到的服務(wù)器的遠(yuǎn)程端口號(hào)即可。
你可能已經(jīng)注意到客戶端沒有調(diào)用bind和accept谎砾,是的,客戶端不需要調(diào)用bind捧颅,因?yàn)榭蛻舳瞬魂P(guān)心本地IP地址和本地端口號(hào)棺榔。
當(dāng)客戶端調(diào)用connect時(shí),內(nèi)核中的TCP/IP堆棧會(huì)自動(dòng)分配本地IP地址和本地端口隘道。本地端口稱為臨時(shí)端口症歇,一般來說很快就釋放了。
一般谭梗,常用服務(wù)的端口稱為常用端口忘晤,如HTTP服務(wù)的80端口,SSH服務(wù)的22端口激捏。
如果你想知道你的客服端的本地端口是什么设塔,可一啟動(dòng)Python shell并與在本地主機(jī)上運(yùn)行的服務(wù)器建立客戶端連接,然后查看內(nèi)核為你創(chuàng)建的套接字分配的臨時(shí)端口(在嘗試以下示例之前啟動(dòng)服務(wù)器webserver3a.py或webserver3b.py):
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sock.connect(('localhost', 8888))
>>> host, port = sock.getsockname()[:2]
>>> host, port
('127.0.0.1', 60589)
在上述情況下远舅,內(nèi)核將臨時(shí)端口60589分配給Socket闰蛔。
除了Socket之外痕钢,魚需要快速介紹一些其他重要的概念,分別是進(jìn)程和文件描述符序六。你很快就會(huì)明白這些概念為什么很重要任连。
什么是進(jìn)程?進(jìn)程只是執(zhí)行程序的一個(gè)實(shí)例例诀。例如随抠,當(dāng)服務(wù)器代碼被執(zhí)行時(shí),它被加載到內(nèi)存中繁涂,執(zhí)行程序的一個(gè)實(shí)例稱為進(jìn)程拱她。內(nèi)核記錄了一堆關(guān)于進(jìn)程的信息,它的進(jìn)程ID就是一個(gè)例子扔罪,用來跟蹤它秉沼。當(dāng)你運(yùn)行迭代服務(wù)器webserver3a.py或webserver3b.py時(shí),你只是運(yùn)行一個(gè)進(jìn)程矿酵。
在終端中啟動(dòng)webserver3b.py:
$ python webserver3b.py
在另一個(gè)終端中氧猬,使用ps命令查看這個(gè)進(jìn)程:
$ ps | grep webserver3b | grep -v grep
7182 ttys003 0:00.04 python webserver3b.py
ps命令顯示你實(shí)際上只運(yùn)行了一個(gè)Python進(jìn)程webserver3b
。當(dāng)創(chuàng)建一個(gè)進(jìn)程時(shí)坏瘩,內(nèi)核會(huì)為它分配一個(gè)進(jìn)程ID盅抚,即PID。在UNIX中倔矾,每個(gè)用戶進(jìn)程都有一個(gè)父進(jìn)程妄均,該父進(jìn)程又有自己的進(jìn)程ID,稱為父進(jìn)程ID哪自,簡(jiǎn)稱PPID丰包。魚假設(shè)你在默認(rèn)情況下運(yùn)行BASH shell,當(dāng)你啟動(dòng)服務(wù)器時(shí)壤巷,會(huì)創(chuàng)建一個(gè)帶有PID的新進(jìn)程邑彪,其父PID設(shè)置為bashshell的PID。
再次啟動(dòng)Python shell胧华,它將創(chuàng)建一個(gè)新進(jìn)程寄症,然后魚們使用os.getpid() 和os.getppid() 系統(tǒng)調(diào)用獲取pythonshell進(jìn)程的PID和父PID(bashshell的PID)。
然后矩动,在另一個(gè)終端窗口中為PPID(父進(jìn)程ID有巧,在魚的例子中是3148)運(yùn)行ps命令和grep。在下面的屏幕截圖中悲没,你可以看到魚的Mac OS X上的子Python shell進(jìn)程和父BASH shell進(jìn)程之間的父子關(guān)系示例:
另一個(gè)需要知道的重要概念是文件描述符篮迎。那么什么是文件描述符?是一個(gè)非負(fù)整數(shù)。
甜橱?什么鬼非負(fù)整數(shù)逊笆?
內(nèi)核在打開現(xiàn)有文件、創(chuàng)建新文件或創(chuàng)建新Socket時(shí)返回給進(jìn)程一個(gè)非負(fù)整數(shù)岂傲,這個(gè)非負(fù)整數(shù)就是文件描述符难裆。
你可能聽說過,在UNIX中譬胎,一切都是文件。內(nèi)核通過文件描述符引用進(jìn)程的打開文件命锄。當(dāng)你需要讀或?qū)懸粋€(gè)文件時(shí)堰乔,你可以用文件描述符來識(shí)別它。
Python為你提供了處理文件(和socket)的高級(jí)對(duì)象脐恩,你不必直接使用文件描述符來標(biāo)識(shí)文件镐侯,但實(shí)際上,在UNIX中驶冒,文件和socket是通過它們的整數(shù)文件描述符來標(biāo)識(shí)的苟翻。
默認(rèn)情況下,unixshell將文件描述符0分配給進(jìn)程的標(biāo)準(zhǔn)輸入骗污,將文件描述符1分配給進(jìn)程的標(biāo)準(zhǔn)輸出崇猫,將文件描述符2分配給標(biāo)準(zhǔn)錯(cuò)誤。
如前所述需忿,盡管Python提供了一個(gè)高級(jí)文件或類似文件的對(duì)象诅炉,但你始終可以對(duì)該對(duì)象使用 fileno() 方法來獲取與該文件關(guān)聯(lián)的文件描述符∥堇澹回到Python shell涕烧,看看魚們?nèi)绾巫龅竭@一點(diǎn):
>>> import sys
>>> sys.stdin
<open file '<stdin>', mode 'r' at 0x102beb0c0>
>>> sys.stdin.fileno()
0
>>> sys.stdout.fileno()
1
>>> sys.stderr.fileno()
2
在Python中處理文件和Socket時(shí),通常會(huì)使用高級(jí)文件/Socket對(duì)象汗洒,但有時(shí)可能需要直接使用文件描述符议纯。
下面是一個(gè)例子,說明了如何使用以文件描述符整數(shù)為參數(shù)的write系統(tǒng)調(diào)用將字符串寫入標(biāo)準(zhǔn)輸出:
>>> import sys
>>> import os
>>> res = os.write(sys.stdout.fileno(), 'hello\n')
hello
這里有一個(gè)有趣的事溢谤,不過對(duì)你來說不再奇怪瞻凤,因?yàn)槟阋呀?jīng)知道所有東西都是Unix中的一個(gè)文件,你的socket也有一個(gè)與之相關(guān)的文件描述符世杀。同樣鲫构,在Python中創(chuàng)建一個(gè)socket時(shí),會(huì)返回一個(gè)對(duì)象玫坛,而不是一個(gè)非負(fù)整數(shù)结笨,但你始終可以使用前面提到的fileno() 方法直接訪問socket的整數(shù)文件描述符。
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> sock.fileno()
3
魚還想提一件事:你是否注意到在“迭代服務(wù)器”webserver3b.py
中,當(dāng)服務(wù)器進(jìn)程休眠60秒時(shí)炕吸,你仍然可以使用第二個(gè)curl命令連接到服務(wù)器伐憾?當(dāng)然,curl沒有立即輸出任何內(nèi)容赫模,它只是掛在那里树肃,但為什么服務(wù)器當(dāng)時(shí)不接受連接,客戶端也沒有立即被拒絕瀑罗,而是能夠連接到服務(wù)器胸嘴?答案是socket對(duì)象的listen方法及其BACKLOG參數(shù),魚在代碼中稱之為REQUEST-QUEUE-SIZE
斩祭。BACKLOG參數(shù)確定內(nèi)核中傳入連接請(qǐng)求的隊(duì)列大小劣像。當(dāng)服務(wù)器webserver3b.py
處于睡眠狀態(tài)時(shí),你運(yùn)行的第二個(gè)curl命令能夠連接到服務(wù)器摧玫,因?yàn)閮?nèi)核在服務(wù)器套接字的傳入連接請(qǐng)求隊(duì)列中有足夠的可用空間耳奕。
雖然增加BACKLOG參數(shù)并不能神奇地將你的服務(wù)器轉(zhuǎn)變?yōu)橐淮慰梢蕴幚矶鄠€(gè)客戶機(jī)請(qǐng)求的服務(wù)器,但是對(duì)于繁忙的服務(wù)器诬像,有一個(gè)相當(dāng)大的backlog參數(shù)是很重要的屋群,這樣accept調(diào)用就不必等待建立新連接,而是可以立即從隊(duì)列中獲取新連接坏挠,并立即開始處理客戶端請(qǐng)求芍躏。
到目前為止,文章以上的內(nèi)容覆蓋了很多知識(shí)點(diǎn)降狠,魚們復(fù)習(xí)下:
- 迭代服務(wù)器
- 服務(wù)器Socket創(chuàng)建序列 (socket, bind, listen, accept)
- 客戶端Socket創(chuàng)建序列 (socket, connect)
- Socket Pair(套接字對(duì))
- Socket
- 臨時(shí)端口和常用端口
- 進(jìn)程
- 進(jìn)程id(PID)纸肉,父進(jìn)程id(PPID),父子進(jìn)程關(guān)系
- 文件描述符
- BACKLOG參數(shù)的含義
使用fork編寫并發(fā)服務(wù)器
現(xiàn)在魚們已經(jīng)準(zhǔn)備好回答那一個(gè)問題:“為了提高你的程序的性能喊熟,你如何讓你的服務(wù)器一次處理多個(gè)請(qǐng)求柏肪?”
或者魚們換一個(gè)問法:“你怎么寫一個(gè)并發(fā)的服務(wù)器呢?”
寫一個(gè)并發(fā)服務(wù)器最簡(jiǎn)單的方法芥牌,是在Unix系統(tǒng)下使用fork()系統(tǒng)調(diào)用烦味。
下面是新的閃亮登場(chǎng)的并發(fā)服務(wù)器的代碼,命名為webserver3c.py
,它可以同時(shí)處理多個(gè)客戶端請(qǐng)求(在魚們的迭代服務(wù)器示例webserver3b.py中壁拉,每個(gè)子進(jìn)程睡眠60秒):
###########################################################################
# Concurrent server - webserver3c.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
# #
# - Child process sleeps for 60 seconds after handling a client's request #
# - Parent and child processes close duplicate descriptors #
# #
###########################################################################
import os
import socket
import time
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5
def handle_request(client_connection):
request = client_connection.recv(1024)
print(
'Child PID: {pid}. Parent PID {ppid}'.format(
pid=os.getpid(),
ppid=os.getppid(),
)
)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
time.sleep(60)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
print('Parent PID (PPID): {pid}\n'.format(pid=os.getpid()))
while True:
client_connection, client_address = listen_socket.accept()
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0) # child exits here
else: # parent
client_connection.close() # close parent copy and loop over
if __name__ == '__main__':
serve_forever()
在深入討論fork是如何工作的之前谬俄,魚們先嘗試一下并親眼確認(rèn)服務(wù)器確實(shí)可以同時(shí)處理多個(gè)客戶機(jī)請(qǐng)求。
不同于webserver3a.py
和webserver3b.py
弃理,使用以下命令行啟動(dòng)服務(wù)器:
$ python webserver3c.py
在迭代服務(wù)器上嘗試同樣的兩個(gè)curl命令溃论,然后魚們可以看到,即使服務(wù)器子進(jìn)程在服務(wù)客戶機(jī)請(qǐng)求后睡眠60秒痘昌,它也不會(huì)影響其他客戶端的請(qǐng)求钥勋,因?yàn)樗鼈冇刹煌耐耆?dú)立的進(jìn)程提供服務(wù)炬转。你應(yīng)該看到curl命令輸出“Hello,World算灸!”馬上就hung住60秒扼劈。你可以繼續(xù)運(yùn)行任意數(shù)量的curl命令(當(dāng)然不能大于fd的上限),所有這些命令都將立即輸出服務(wù)器的響應(yīng)“Hello菲驴,World”荐吵,不會(huì)有任何明顯的延遲。
關(guān)于fork()要注意一點(diǎn)赊瞬,一個(gè)代碼里面調(diào)用fork一次先煎,它會(huì)返回兩次:一次在父進(jìn)程中,一次在子進(jìn)程中巧涧。派生新進(jìn)程時(shí)薯蝎,返回給子進(jìn)程的進(jìn)程ID為0。當(dāng)fork在父進(jìn)程中返回時(shí)褒侧,它返回子進(jìn)程的PID良风。
魚依稀記得當(dāng)魚第一次讀到并嘗試fork的時(shí)候魚是多么的迷谊迄,現(xiàn)在這在魚看來依然很神奇闷供。
更多地關(guān)注造殼。
當(dāng)父進(jìn)程派生新的子進(jìn)程時(shí)统诺,子進(jìn)程將獲取父進(jìn)程的文件描述符的副本:
你可能注意到上面代碼中的父進(jìn)程關(guān)閉了客戶端連接:
else: # parent
client_connection.close() # close parent copy and loop over
那么歪脏,當(dāng)一個(gè)子進(jìn)程的父進(jìn)程關(guān)閉了同一個(gè)套接字,它為什么還能從客戶端socket里面讀取數(shù)據(jù)呢粮呢?
答案如上圖所示婿失。內(nèi)核使用描述符引用計(jì)數(shù)來決定是否關(guān)閉socket。它只在其描述符引用計(jì)數(shù)變?yōu)?時(shí)啄寡,關(guān)閉套接字豪硅。
當(dāng)服務(wù)器創(chuàng)建子進(jìn)程時(shí),子進(jìn)程獲取父進(jìn)程文件描述符的副本挺物,內(nèi)核增加這些描述符的引用計(jì)數(shù)懒浮。在一個(gè)父進(jìn)程和一個(gè)子進(jìn)程的情況下,客戶端socket的描述符引用計(jì)數(shù)為2识藤,當(dāng)上面代碼中的父進(jìn)程關(guān)閉客戶端連接套接字時(shí)砚著,它只會(huì)減少其引用計(jì)數(shù),該計(jì)數(shù)將變?yōu)?痴昧,不足以導(dǎo)致內(nèi)核關(guān)閉socket稽穆。
另外,子進(jìn)程還關(guān)閉父進(jìn)程偵聽套接字的副本赶撰,因?yàn)樽舆M(jìn)程不關(guān)心接受新的客戶端連接舌镶,它只關(guān)心處理來自已建立的客戶端連接的請(qǐng)求:
listen_socket.close() # close child copy
魚將在本文后面討論如果不關(guān)閉重復(fù)的描述符會(huì)發(fā)生什么柱彻。
從并發(fā)服務(wù)器的源代碼中可以看到,服務(wù)器父進(jìn)程現(xiàn)在的唯一角色是接受一個(gè)新的客戶端連接乎折,派生一個(gè)新的子進(jìn)程來處理該客戶端請(qǐng)求绒疗,并循環(huán)接受另一個(gè)客戶端連接,僅此而已骂澄。服務(wù)器父進(jìn)程不處理客戶端請(qǐng)求吓蘑,而是讓其子進(jìn)程處理。
魚們先討論另一個(gè)問題坟冲,魚們所說的兩個(gè)事件同時(shí)發(fā)生是什么意思磨镶?
當(dāng)魚們說兩個(gè)事件同時(shí)發(fā)生時(shí),通常是指它們同時(shí)發(fā)生健提。這個(gè)定義很好琳猫,但你應(yīng)該記住嚴(yán)格的定義:
如果你看不出哪個(gè)程序會(huì)先發(fā)生,那么兩個(gè)事件是并發(fā)的私痹。
再次重申一下脐嫂,現(xiàn)在是時(shí)候回顧一下你迄今為止所涉及的主要思想和概念了。
- 在Unix中編寫并發(fā)服務(wù)器的最簡(jiǎn)單方法是使用
fork()
系統(tǒng)調(diào)用 - 當(dāng)進(jìn)程分叉新進(jìn)程時(shí)紊遵,它將成為該新分叉子進(jìn)程的父進(jìn)程账千。
- 在調(diào)用fork之后,父級(jí)和子級(jí)共享相同的文件描述符暗膜。
- 內(nèi)核使用描述符引用計(jì)數(shù)來決定是否關(guān)閉文件/socket
- 服務(wù)器父進(jìn)程的角色:它現(xiàn)在所做的只是接受來自客戶端的新連接匀奏,派生子進(jìn)程來處理客戶端請(qǐng)求,然后循環(huán)接受新的客戶端連接学搜。
回收文件描述符
讓魚們看看如果不關(guān)閉父進(jìn)程和子進(jìn)程中的重復(fù)套接字描述符娃善,將會(huì)發(fā)生什么。webserver3d.py
是并發(fā)服務(wù)器的修改版本瑞佩,其中服務(wù)器不關(guān)閉重復(fù)的描述符:
###########################################################################
# Concurrent server - webserver3d.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
###########################################################################
import os
import socket
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5
def handle_request(client_connection):
request = client_connection.recv(1024)
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
clients = []
while True:
client_connection, client_address = listen_socket.accept()
# store the reference otherwise it's garbage collected
# on the next loop run
clients.append(client_connection)
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0) # child exits here
else: # parent
# client_connection.close()
print(len(clients))
if __name__ == '__main__':
serve_forever()
運(yùn)行起來:
$ python webserver3d.py
使用curl請(qǐng)求服務(wù)器:
$ curl http://localhost:8888/hello
Hello, World!
curl打印了并發(fā)服務(wù)器的響應(yīng)聚磺,但它沒有終止并一直掛起。服務(wù)器不再休眠60秒:其子進(jìn)程主動(dòng)處理客戶端請(qǐng)求炬丸,關(guān)閉客戶端連接并退出瘫寝,但客戶端這邊的curl仍然沒有終止。
為什么curl不終止御雕?原因是文件描述符還有余矢沿。
當(dāng)子進(jìn)程關(guān)閉客戶端連接時(shí),內(nèi)核減少了該客戶端套接字的引用計(jì)數(shù)酸纲,計(jì)數(shù)變?yōu)?捣鲸。服務(wù)器子進(jìn)程已退出,但客戶端套接字未被內(nèi)核關(guān)閉闽坡,因?yàn)樵撎捉幼置枋龇囊糜?jì)數(shù)不是0栽惶。因此愁溜,終止數(shù)據(jù)包(在TCP/IP術(shù)語中稱為FIN)未發(fā)送到客戶端,客戶端保持在線外厂。
如果長時(shí)間運(yùn)行的服務(wù)器沒有關(guān)閉重復(fù)的文件描述符冕象,它最終將耗盡可用的文件描述符:
使用Control-C
終止你webserver3d.py
程序,檢查下你所在服務(wù)器上的默認(rèn)可用資源汁蝶,可以使用ulimit
命令:
$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 3842
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 3842
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
如上所示渐扮,在魚的Ubuntu機(jī)器上,服務(wù)器進(jìn)程可以使用的打開文件描述符(打開的文件)的最大數(shù)量是1024個(gè)掖棉。
現(xiàn)在讓魚們看看如果服務(wù)器不關(guān)閉重復(fù)的描述符墓律,它將如何耗盡可用的文件描述符。在現(xiàn)有或新的終端窗口中幔亥,將服務(wù)器的最大打開文件描述符數(shù)設(shè)置為256:
$ ulimit -n 256
在同一個(gè)終端中啟動(dòng)服務(wù):
$ python webserver3d.py
然后使用以下的自動(dòng)化代碼client3.py
耻讽,模擬請(qǐng)求數(shù)量比較多的客戶端:
#####################################################################
# Test client - client3.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
#####################################################################
import argparse
import errno
import os
import socket
SERVER_ADDRESS = 'localhost', 8888
REQUEST = b"""\
GET /hello HTTP/1.1
Host: localhost:8888
"""
def main(max_clients, max_conns):
socks = []
for client_num in range(max_clients):
pid = os.fork()
if pid == 0:
for connection_num in range(max_conns):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(SERVER_ADDRESS)
sock.sendall(REQUEST)
socks.append(sock)
print(connection_num)
os._exit(0)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Test client for LSBAWS.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
'--max-conns',
type=int,
default=1024,
help='Maximum number of connections per client.'
)
parser.add_argument(
'--max-clients',
type=int,
default=1,
help='Maximum number of clients.'
)
args = parser.parse_args()
main(args.max_clients, args.max_conns)
在一個(gè)新的終端中,啟動(dòng)client3.py
帕棉,請(qǐng)求量設(shè)定為300:
$ python client3.py --max-clients=300
很快你的服務(wù)器就會(huì)爆炸针肥。這是魚實(shí)驗(yàn)時(shí)異常的截圖:
試驗(yàn)給魚們帶來的教訓(xùn)很清楚:服務(wù)器應(yīng)該關(guān)閉重復(fù)的描述符。
僵尸進(jìn)程的危害
但即使你關(guān)閉了重復(fù)的描述符香伴,你還沒有走出困境慰枕,因?yàn)槟愕姆?wù)器還有一個(gè)問題,那就是僵尸進(jìn)程瞒窒!
是的捺僻,其實(shí)你的服務(wù)器代碼實(shí)際上創(chuàng)建了僵尸進(jìn)程乡洼。讓魚們看看怎么做崇裁。魚們先重新啟動(dòng)服務(wù)器:
$ python webserver3d.py
在另一個(gè)終端中,使用curl請(qǐng)求:
$ curl http://localhost:8888/hello
現(xiàn)在運(yùn)行ps命令來顯示正在運(yùn)行的Python進(jìn)程束昵。這是魚的Ubuntu上ps輸出的例子:
$ ps auxw | grep -i python | grep -v grep
vagrant 9099 0.0 1.2 31804 6256 pts/0 S+ 16:33 0:00 python webserver3d.py
vagrant 9102 0.0 0.0 0 0 pts/0 Z+ 16:33 0:00 [python] <defunct>
你是否看到上面的第二行拔稳,其中顯示PID 9102的進(jìn)程的狀態(tài)是Z+,進(jìn)程的名稱是<deflunct>锹雏?那是魚們的僵尸進(jìn)程巴比。甚至魚們還不能殺死他們。
即使魚們使用kill -9
礁遵,他們依然會(huì)存在轻绞。
什么是僵尸進(jìn)程?為什么魚們的服務(wù)器要?jiǎng)?chuàng)建僵尸進(jìn)程佣耐?
僵尸進(jìn)程是已終止的進(jìn)程政勃,但其父進(jìn)程尚未等待它,也尚未收到其終止?fàn)顟B(tài)兼砖。當(dāng)子進(jìn)程在其父進(jìn)程之前退出時(shí)奸远,內(nèi)核會(huì)將子進(jìn)程變成僵尸進(jìn)程既棺,并存儲(chǔ)一些有關(guān)該進(jìn)程的信息,供其父進(jìn)程以后檢索懒叛。存儲(chǔ)的信息通常是進(jìn)程ID丸冕、進(jìn)程終止?fàn)顟B(tài)和進(jìn)程的資源使用情況。
所以說薛窥,僵尸進(jìn)程是有目的的胖烛,但是如果你的服務(wù)器不處理這些僵尸進(jìn)程,你的系統(tǒng)最終就會(huì)被阻塞诅迷。
讓魚們?cè)囼?yàn)看看不清理僵尸進(jìn)程會(huì)怎么樣洪己。
首先停止正在運(yùn)行的服務(wù)器,并在新的終端窗口中竟贯,使用ulimit命令將max user processess設(shè)置為400(確保將open files設(shè)置為一個(gè)很高的數(shù)字答捕,也就是說500):
$ ulimit -u 400
$ ulimit -n 500
在剛剛運(yùn)行$ulimit-u 400
命令的同一終端中,啟動(dòng)服務(wù)器webserver3d.py
:
$ python webserver3d.py
在新的終端窗口中屑那,啟動(dòng)client3.py
并告訴它創(chuàng)建500個(gè)到服務(wù)器的同時(shí)連接:
$ python client3.py --max-clients=500
很快拱镐,你的服務(wù)器就會(huì)出現(xiàn)一個(gè)OSError:Resource temporary unavailable
異常,當(dāng)它試圖創(chuàng)建一個(gè)新的子進(jìn)程時(shí)持际,創(chuàng)建失敗沃琅,因?yàn)樗呀?jīng)達(dá)到了允許創(chuàng)建的最大子進(jìn)程數(shù)的限制。這是魚機(jī)器上異常的截圖:
如你所見蜘欲,如果不處理僵尸進(jìn)程益眉,就會(huì)給長期運(yùn)行的服務(wù)器帶來問題。魚將后面會(huì)討論服務(wù)器應(yīng)該如何處理僵尸問題姥份。
魚們?cè)倩仡櫼幌轮R(shí)點(diǎn):
- 如果不關(guān)閉重復(fù)的描述符郭脂,則客戶端不會(huì)終止,因?yàn)榭蛻舳诉B接不會(huì)關(guān)閉澈歉。
- 如果不關(guān)閉重復(fù)的描述符展鸡,長期運(yùn)行的服務(wù)器最終將耗盡可用的文件描述符(最大打開文件數(shù))。
- 當(dāng)派生子進(jìn)程并退出埃难,而父進(jìn)程不等待它莹弊,也不收集其終止?fàn)顟B(tài)時(shí),它將成為一個(gè)僵尸進(jìn)程涡尘。
- 僵尸需要吃點(diǎn)東西忍弛,在魚們的例子中,這是記憶考抄。如果服務(wù)器不處理僵尸進(jìn)程细疚,那么它最終將耗盡可用進(jìn)程(最大用戶進(jìn)程)。
- 你不能殺僵尸座泳,你需要等待惠昔。
解決僵尸進(jìn)程:信號(hào)處理程序 + 等待系統(tǒng)調(diào)用
那么你需要做什么來處理掉僵尸進(jìn)程呢幕与?你需要修改服務(wù)器代碼,以等待僵尸進(jìn)程镇防,獲得其終止?fàn)顟B(tài)啦鸣。然后可以通過修改服務(wù)器來調(diào)用等待系統(tǒng)調(diào)用來完成此操作。
不幸的是来氧,這遠(yuǎn)不是理想的诫给,因?yàn)槿绻{(diào)用wait,將阻塞服務(wù)器啦扬,從而有效地阻止服務(wù)器處理新的客戶端連接請(qǐng)求中狂。還有其他選擇嗎?是的扑毡,有胃榕,其中一個(gè)是信號(hào)處理程序和等待系統(tǒng)調(diào)用的組合。
魚們來看一下工作原理瞄摊。當(dāng)子進(jìn)程退出時(shí)勋又,內(nèi)核發(fā)送一個(gè)SIGCHLD信號(hào)。父進(jìn)程可以設(shè)置一個(gè)信號(hào)處理程序换帜,以異步通知該SIGCHLD事件楔壤,然后它可以等待子進(jìn)程收集其終止?fàn)顟B(tài),從而防止僵尸進(jìn)程留在周圍惯驼。
順便說一下蹲嚣,異步事件意味著父進(jìn)程不能提前知道事件將要發(fā)生。
修改服務(wù)器代碼以設(shè)定SIGCHLD事件祟牲,并在事件處理程序中等待終止的子進(jìn)程隙畜。修改代碼得webserver3e.py
文件:
###########################################################################
# Concurrent server - webserver3e.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
###########################################################################
import os
import signal
import socket
import time
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5
def grim_reaper(signum, frame):
pid, status = os.wait()
print(
'Child {pid} terminated with status {status}'
'\n'.format(pid=pid, status=status)
)
def handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
# sleep to allow the parent to loop over to 'accept' and block there
time.sleep(3)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
signal.signal(signal.SIGCHLD, grim_reaper)
while True:
client_connection, client_address = listen_socket.accept()
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0)
else: # parent
client_connection.close()
if __name__ == '__main__':
serve_forever()
啟動(dòng)服務(wù)器:
$ python webserver3e.py
再請(qǐng)求一次:
$ curl http://localhost:8888/hello
看服務(wù)器終端:
發(fā)生了什么呢?accept調(diào)用失敗了疲眷,返回了EINTR錯(cuò)誤禾蚕。
當(dāng)子進(jìn)程退出導(dǎo)致SIGCHLD事件后您朽,父進(jìn)程accept調(diào)用會(huì)被阻塞,然后導(dǎo)致激活了信號(hào)處理程序,當(dāng)信號(hào)處理程序完成時(shí)摄职,accept系統(tǒng)調(diào)用被中斷:
Don’t worry, it’s a pretty simple problem to solve, though. All you need to do is to re-start the accept system call. Here is the modified version of the server webserver3f.py that handles that problem:
不過這是個(gè)很簡(jiǎn)單的問題脸候,只需要重新啟動(dòng)accept系統(tǒng)調(diào)用。修改版本webserver3f.py
解決了這個(gè)問題:
###########################################################################
# Concurrent server - webserver3f.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
###########################################################################
import errno
import os
import signal
import socket
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024
def grim_reaper(signum, frame):
pid, status = os.wait()
def handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
signal.signal(signal.SIGCHLD, grim_reaper)
while True:
try:
client_connection, client_address = listen_socket.accept()
except IOError as e:
code, msg = e.args
# restart 'accept' if it was interrupted
if code == errno.EINTR:
continue
else:
raise
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0)
else: # parent
client_connection.close() # close parent copy and loop over
if __name__ == '__main__':
serve_forever()
啟動(dòng)升級(jí)后的webserver3f.py
:
$ python webserver3f.py
請(qǐng)求一下:
$ curl http://localhost:8888/hello
現(xiàn)在不再有EINTR異常了讯屈。
接下來魚們?cè)衮?yàn)證是否不再有僵尸進(jìn)程蛋哭,以及帶有wait調(diào)用的SIGCHLD事件處理程序是否處理已終止的子進(jìn)程。
要做到這一點(diǎn)涮母,只需運(yùn)行ps命令谆趾,并親自查看不再有Z+狀態(tài)的Python進(jìn)程(不再有<definct>進(jìn)程)就ok了躁愿。
- 如果你用fork創(chuàng)建一個(gè)子進(jìn)程而不wait它,它就會(huì)變成僵尸進(jìn)程沪蓬;
- 處理僵尸進(jìn)程的方法:使用SIGCHLD事件處理程序彤钟,異步等待已終止的子進(jìn)程獲取其終止?fàn)顟B(tài);
- 當(dāng)使用事件處理程序時(shí)跷叉,你需要記住系統(tǒng)調(diào)用可能會(huì)被中斷逸雹,并且你需要為該場(chǎng)景做好準(zhǔn)備。
好吧云挟,到目前為止還不錯(cuò)梆砸。沒問題吧?好吧园欣,差不多了帖世。再次嘗試webserver3f.py
,但不要使用curl發(fā)出一個(gè)請(qǐng)求沸枯,而是使用client3.py
創(chuàng)建128個(gè)同時(shí)連接:
$ python client3.py --max-clients 128
運(yùn)行下ps命令看下:
$ ps auxw | grep -i python | grep -v grep
然而狮暑,僵尸進(jìn)程還在。
這次是什么問題呢辉饱?當(dāng)你同時(shí)運(yùn)行128個(gè)客戶端并建立128個(gè)連接時(shí)搬男,服務(wù)器上的子進(jìn)程處理這些請(qǐng)求并幾乎同時(shí)退出,導(dǎo)致大量SIGCHLD信號(hào)被發(fā)送到父進(jìn)程彭沼。問題是缔逛,信號(hào)沒有排隊(duì),服務(wù)器進(jìn)程錯(cuò)過了幾個(gè)信號(hào)姓惑,導(dǎo)致幾個(gè)僵尸進(jìn)程無人值守:
解決這個(gè)問題的方法是褐奴,設(shè)置一個(gè)SIGCHLD事件處理程序,但是不要等待于毙,而是在循環(huán)中使用帶WNOHANG選項(xiàng)的waitpid系統(tǒng)調(diào)用敦冬,以確保所有終止的子進(jìn)程都得到處理。以下是修改后的服務(wù)器代碼webserver3g.py
:
###########################################################################
# Concurrent server - webserver3g.py #
# #
# Tested with Python 2.7.9 & Python 3.4 on Ubuntu 14.04 & Mac OS X #
###########################################################################
import errno
import os
import signal
import socket
SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024
def grim_reaper(signum, frame):
while True:
try:
pid, status = os.waitpid(
-1, # Wait for any child process
os.WNOHANG # Do not block and return EWOULDBLOCK error
)
except OSError:
return
if pid == 0: # no more zombies
return
def handle_request(client_connection):
request = client_connection.recv(1024)
print(request.decode())
http_response = b"""\
HTTP/1.1 200 OK
Hello, World!
"""
client_connection.sendall(http_response)
def serve_forever():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind(SERVER_ADDRESS)
listen_socket.listen(REQUEST_QUEUE_SIZE)
print('Serving HTTP on port {port} ...'.format(port=PORT))
signal.signal(signal.SIGCHLD, grim_reaper)
while True:
try:
client_connection, client_address = listen_socket.accept()
except IOError as e:
code, msg = e.args
# restart 'accept' if it was interrupted
if code == errno.EINTR:
continue
else:
raise
pid = os.fork()
if pid == 0: # child
listen_socket.close() # close child copy
handle_request(client_connection)
client_connection.close()
os._exit(0)
else: # parent
client_connection.close() # close parent copy and loop over
if __name__ == '__main__':
serve_forever()
啟動(dòng)服務(wù):
$ python webserver3g.py
發(fā)起請(qǐng)求:
$ python client3.py --max-clients 128
現(xiàn)在可以確認(rèn)沒有僵尸進(jìn)程了唯沮。
總結(jié)
恭喜脖旱!代碼的旅途很漫長,但總算結(jié)束了介蛉。
現(xiàn)在你擁有了自己的簡(jiǎn)單并發(fā)服務(wù)器萌庆,代碼可以作為你進(jìn)一步面向生產(chǎn)級(jí)Web服務(wù)器的基礎(chǔ)。
接下來是什么币旧?正如喬霞眨·比林斯所說,
“就像一張郵票,堅(jiān)持一件事巍虫,直到你到達(dá)那里彭则。”
開始掌握基本知識(shí)占遥,質(zhì)疑你已經(jīng)知道的贰剥,然后總是深入挖掘。
“如果你只學(xué)方法筷频,你就會(huì)被方法束縛住蚌成。但如果你學(xué)會(huì)了原則,你就可以設(shè)計(jì)出自己的方法凛捏〉S牵” —— 愛默生。
下面是魚為這篇文章中的大部分內(nèi)容而繪制的書籍列表坯癣。它們將幫助你拓寬和加深你對(duì)魚所涉及主題的知識(shí)瓶盛。魚強(qiáng)烈建議你以某種方式去買那些書:從你的朋友那里借,從你當(dāng)?shù)氐膱D書館里看示罗,或者在亞馬遜上買:
- Unix Network Programming, Volume 1: The Sockets Networking API (3rd Edition)
- Advanced Programming in the UNIX Environment, 3rd Edition
- The Linux Programming Interface: A Linux and UNIX System Programming Handbook
- TCP/IP Illustrated, Volume 1: The Protocols (2nd Edition) (Addison-Wesley Professional Computing Series)
- The Little Book of SEMAPHORES (2nd Edition): The Ins and Outs of Concurrency Control and Common Mistakes. Also available for free on the author’s site here.
先這樣吧