鱼相信要成为一个更好的开发人员,你必须更好地理解你天天使用的底层软件系统,包括编程语言、编译器和解释器、数据库和操做系统、web服务器和web框架。
有一天,一个女人出去散步到一个建筑工地,看见三个男人在工做。python
她问第一个男人:“你在干什么?”第一个男人被这个问题惹恼了,叫道:“你没看见我在砌砖吗?”web
她对回答不满意,问第二我的在作什么。第二个男人回答说:“我在建一堵砖墙。”而后,他把注意力转向第一个男人,说:“嘿,你刚砌过了墙的尽头。你须要把最后一块砖头摘下来。”shell
她又一次对答案不满意,问第三我的在干什么。那人抬头望着天空对她说:“我正在建造世界上最大的大教堂。”当他站在那里仰望天空时,另外两我的开始为那块乱七八糟的砖头争吵起来。男人转身对前两我的说:“嘿,伙计们,别担忧那块砖头。这是一个内墙,它会被粉刷过,没有人会看到那块砖。换一层吧。”数据库
这个故事的寓意是,当你了解整个系统,了解不一样部分(砖块、墙壁、大教堂)如何组合在一块儿时,你能够更快地识别和解决问题(错误的砖块)。django
文章这样的开头,与《建立一个简单的Web服务器》有什么关系?编程
鱼相信要成为一个更好的开发人员,你必须更好地理解你天天使用的底层软件系统,包括编程语言、编译器和解释器、数据库和操做系统、web服务器和web框架。并且,为了更好更深刻地了解这些系统,你必须从头开始,一块一块地,一堵墙一堵墙地从新构建它们。flask
孔子这样说:segmentfault
听而易忘
见而易记
作而易懂
鱼但愿你能够相信,对不一样的软件系统进行造轮子,来学习它们的工做方式,是一个好办法。浏览器
在这个由三部分组成的系列中,鱼将向你展现如何构建本身的基本Web服务器。Here we go!bash
首先,什么是Web服务器?
简单滴说,它是一个网络服务器,位于物理服务器之上(没看错,就是服务器上的服务器),而后它等待客户端发送请求。当它接收到请求时,它生成一个响应并将其发送回客户端。客户端和服务器之间的通讯使用HTTP协议进行。客户端能够是你的浏览器,也能够是任何其余讲HTTP的软件。
一个很是简单的Web服务器实现是什么样子的?
能够看下鱼的这个例子,这个例子是用Python编写的(在Python3.7+上进行了测试),可是即便你不了解Python(这是一种很容易掌握的语言,请尝试!)你仍然应该可以从下面的代码和解释中理解概念:
# 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
,而后使用如下命令运行它:
$ python webserver1.py Serving HTTP on port 8888 …
如今,在Web浏览器的地址栏输入http://localhost:8888/hello
,按Enter,而后你应该看到“Hello World!”显示在浏览器中,以下所示:
快去实践一遍吧,简单地一匹!
鱼们来分析一下它究竟是如何工做的。
首先让鱼们从你输入的网址开始。它被称为URL,其基本结构以下
这个就是你要告诉浏览器的地址,至关于你要求浏览器查找和链接到的Web服务器的地址,以及服务器上要为你获取的页面(路径)。
可是,在浏览器发送HTTP请求以前,它首先须要与Web服务器创建TCP链接。而后浏览器经过TCP链接向服务器发送HTTP请求,并等待服务器发送HTTP响应。
当你的浏览器收到来自服务器的应答时,它会显示出来,在这种状况下,它会显示“Hello, World!”
如今,让鱼们来更详细地探讨,客户端和服务器在发送HTTP请求和响应以前如何创建TCP链接。为此,鱼使用所谓的套接字进行模拟。你将经过在命令行上使用telnet手动模拟浏览器,而不是直接使用浏览器。
在运行Web服务器的同一台计算机上,在命令行上启动telnet会话,指定要链接到本地主机和8888的端口,而后按Enter键:
$ telnet localhost 8888 Trying 127.0.0.1 … Connected to localhost.
此时,你已经与本地主机上运行的服务器创建了TCP链接,并准备发送和接收HTTP消息。在下面的图片中,你能够看到一个服务器必须通过的标准过程,才能接受新的TCP链接。
更多地关注造壳。
同时,鱼们继续试验,在同一个telnet会话中,输入“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!
就在此时,你手动模拟了你的浏览器!你发送了一个HTTP请求并获得了一个HTTP响应。这是HTTP请求的基本结构:
HTTP请求由这些元素组成:
为了简单起见,鱼们的Web服务器此时彻底忽略了上面的请求数据。你也能够输入任何垃圾而不是“GET/hello HTTP/1.1”,而后你仍然会获得一个“hello,World!”回应。
输入请求行并按Enter键后,客户机将请求发送到服务器,服务器读取请求行,打印请求行并返回正确的HTTP响应。
如下是服务器发送回客户端的HTTP响应(在本例中为telnet):
让鱼们解剖一下。HTTP响应由这几个元素组成:
响应状态行HTTP/1.1 200 OK
由HTTP协议版本、HTTP状态码和HTTP状态码缘由短语OK组成。当浏览器获得响应时,它会显示响应的主体,这就是为何你会看到“Hello,World!”在你的浏览器中。
这就是Web服务器工做的基本模式。总而言之:
要创建TCP链接,客户端和服务器都使用套接字。
如今你有了一个很是基本的工做Web服务器,你可使用浏览器或其余HTTP客户端进行测试。正如你已经看到并但愿尝试过的那样,经过使用telnet并手动键入HTTP请求,你也能够成为一我的工HTTP客服端。
接下来鱼们要提出一个问题:
“在不对服务器进行任何更改的前提下,你如何在新开发的Web服务器下,运行/适配不一样的Django应用程序、Flask应用程序和pyrampid应用程序”
在过去,你对Python Web框架的选择会限制你对可用Web服务器的选择,反之亦然。若是框架和服务器设计为协同工做,那么通常是可行的:
可是,当你尝试将服务器和非设计为协同工做的框架结合在一块儿时,可能会遇到不match的问题:
基本上,你必须使用协同工做的东西,但有可能不是你想要使用的东西。好比你但愿用ServerA的某个特性和FrameworkB的某个功能,可是FrameworkA不能知足你。
那么,如何确保可使用多个Web框架运行Web服务器,而没必要对Web服务器或Web框架进行代码更改呢?这个问题的答案就是Python Web服务器网关接口(简称WSGI,发音为wizgy)。
WSGI容许开发人员将Web框架与Web服务器解耦。如今,你能够混合和匹配Web服务器和Web框架,并选择适合你须要的配对。例如,可使用Gunicorn、Nginx/uWSGI或Waitress运行Django、Flask或Pyramid。这样的解耦,得益于服务器和框架中的WSGI支持:
所以,WSGI就是问题的答案。你的Web服务器必须实现WSGI接口的服务器部分,而且要求全部的python web框架都已经实现了WSGI接口的框架端。这样,就能够将它们混合使用,而无需修改服务器代码以适应特定的Web框架。
如今你知道,Web服务器和Web框架对WSGI的支持容许你选择适合你的配对,但这也有利于服务器和框架开发人员,由于他们能够专一于本身喜欢的专业领域,而不是相互干涉。其余语言也有相似的接口:例如,Java有Servlet API,Ruby有Rack。
理想很美好,但凡事都得“Show me the code”。鱼们来看看这个很是简单的WSGI服务器实现:
# 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()
代码仍是比较简单的(不到150行),你们均可以理解,应该不会出现陷入细节泥潭的状况。上面的服务器还能够作更多的事情——它能够运行用你喜好的Web框架编写的基本Web应用程序,不管是Pyramid、Flask、Django仍是其余Python WSGI框架。
让鱼们来运行试试看。将上述代码保存为webserver2.py
。若是你试图在没有任何参数的状况下运行它,它会提醒错误并退出。
$ python webserver2.py Provide a WSGI application object as module:callable
它真的须要你的Web应用服务。要运行服务器,只须要安装Python。可是要运行使用Pyramid、Flask和Django编写的应用程序,你须要先安装这些框架,让鱼们把这三个都安装好。鱼首选的方法是使用venv建立一个虚拟环境,以避免影响现有环境(venv在Python3.3版本以及以上默认自带)。只需按照下面的步骤建立并激活一个虚拟环境,而后安装全部三个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
此时,你须要建立一个Web应用程序。鱼们先从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()
如今,你可使用本身的Web服务器,来启动你的Pyramid应用程序了:
(lsbaws) $ python webserver2.py pyramidapp:app WSGIServer: Serving HTTP on port 8888 ...
你刚才告诉服务器从python模块“Pyramid app”加载可调用的“app”,你的服务器如今能够接收请求并将它们转发到你的pyramid中叫app的应用程序。
同时,从webserver2.py
代码中能够看出,应用程序如今只处理一个路由:/hello路由。在浏览器网站上输入http://localhost:8888/hello
地址,按回车键,而后观察结果:
固然,你也能够在命令行中使用curl工具,也能达到一样的效果:
$ curl -v http://localhost:8888/hello ...
你能够看下服务器和curl都输出了些什么。
如今鱼们移步到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 ...
在浏览器网站上输入http://localhost:8888/hello
地址,按回车键:
同样的,你也能够在命令行中使用curl工具:
$ curl -v http://localhost:8888/hello ...
服务器还能够处理Django应用程序吗?固然能够,试试看!不过,它涉及的内容稍微多一些,鱼建议复制整个repo并使用djangoapp.py
。
下面的源代码基本上将Django helloworld项目(使用Django的Django-admin.py startproject
命令预先建立)添加到当前Python路径,而后导入项目的WSGI应用程序。
import sys sys.path.insert(0, './helloworld') from helloworld import wsgi app = wsgi.application
将这段代码保存为djangoapp.py
,而后运行起来:
(lsbaws) $ python webserver2.py djangoapp:app WSGIServer: Serving HTTP on port 8888 ...
在浏览器网站上输入地址,按回车键:
虽然已经运行过不少次,可是你依然能够在命令行中使用curl工具,为了验证Django:
$ curl -v http://localhost:8888/hello ...
ok,到目前为止,鱼们把三个服务器都轮了一遍,若是你还没亲手试过,最好动下手,看是没啥用的。
好吧,你已经体验过WSGI的强大功能:它容许你混合匹配你的Web服务器和Web框架。
WSGI在Python Web服务器和pythonweb框架之间提供了一个最小的接口。WSGI很简单,并且很容易在服务器端和框架端实现。
鱼们来分析下鱼们以前实现的WSGI的代码。
如下代码段显示了服务器和接口的框架端:
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 ‘application' 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)
它的工做原理以下:
下面是界面的可视化表示:
到目前为止,鱼们已经轮过了Pyramid、Flask和Django Web应用程序,还看到了实现WSGI规范的服务器端的服务器代码。
当你使用这些框架之一编写Web应用程序时,你能够在更高的级别上工做,而不直接使用WSGI。
但鱼知道你对WSGI接口的框架方面也很好奇,由于你正在阅读本文。
更多地关注造壳。
所以,让鱼们建立一个极简的WSGI Web应用程序/Web框架,而不使用Pyramid、Flask或Django,并在服务器上运行它:
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
,运行它:
(lsbaws) $ python webserver2.py wsgiapp:app WSGIServer: Serving HTTP on port 8888 ...
在浏览器网站上输入地址,按回车键:
这样,鱼们就实现了一个极简的WSGI Web“框架”,没错,就是这么简单。
如今,让鱼们回到服务器向客户端传输的内容。
下面是服务器在使用HTTP客户端调用金字塔应用程序时生成的HTTP响应:
这个响应有一些是你在前文中能看到的熟悉部分,但它也有一些新的内容。例如,它有四个你之前从未见过的HTTP头:内容类型、内容长度、日期和服务器。其实这些是Web服务器响应一般应该具备的头。不过,这些都不是严格要求的。报头的目的是传输关于HTTP请求/响应的附加信息。
如今你已经了解了更多关于WSGI接口的信息,一样的,下面HTTP响应,其中包含了有关生成它的部分的更多信息:
鱼尚未提到“environ”字典,但基本上它是一个Python字典,必须包含WSGI规范指定的某些WSGI和CGI变量。服务器在分析请求后从HTTP请求中获取字典的值。这就是字典的内容:
Web框架使用该字典中的信息,根据指定的路由、请求方法等信息,决定使用哪一个视图、从何处读取请求体以及在何处写入错误(若是有的话)。
如今,你已经建立了本身的WSGI Web服务器,并使用不一样的Web框架编写了Web应用程序。并且,你还建立了一个简单的Web应用程序/Web框架。
让鱼们回顾一下你的WSGI Web服务器必须作些什么来服务针对WSGI应用程序的请求:
首先,服务器启动并加载Web框架/应用程序提供的可调用的“应用程序”;
而后,服务器读取一个请求;
而后,服务器解析它;
而后,它使用请求数据构建一个“environ”字典;
而后,它用“environ”字典调用“application”,用“start_response”做为参数调用“start_response”,并返回一个响应体;
而后,服务器使用对“application”对象的调用返回的数据以及可调用的“start_response”设置的状态和响应头来构造HTTP响应;
最后,服务器将HTTP响应发送回客户端。
就是这些步骤,贯穿了鱼们的整个服务流程。
如今你有了一个能够工做的WSGI服务器,它能够为使用符合WSGI的Web框架(如Django、Flask、Pyramid或你本身的WSGI框架)编写的基本Web应用程序提供服务。最理想的,是服务器能够与多个Web框架一块儿使用,而不须要对服务器代码库进行任何更改。
但这还不够完美,甚至还有明显的缺点。
鱼们来思考一下:“为了提升你的程序的性能,你如何让你的服务器一次处理多个请求?”
在前面,鱼们建立了一个极简的WSGI服务器,它能够处理基本的HTTP GET请求。可是,它是一个“迭代服务器”,一次处理一个客户机请求。在处理完当前客户端请求以前,它没法接受新链接。有些客户端可能不满意,由于他们将不得不排队等候,而对于繁忙的服务器,排队现象尤为严重。
鱼们来看一眼鱼们的“迭代服务器”,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()
为了更直观地观察服务器一次只处理一个请求,鱼们稍微“下降一下性能”,修改服务器并在向客户端发送响应后添加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()
而后运行起来:
$ python webserver3b.py
在命令行中请求一下,你立刻就能看到“Hello, World!”:
$ curl http://localhost:8888/hello Hello, World!
而后,紧接着,鱼们赶忙发起第二个请求:
$ curl http://localhost:8888/hello
若是你足够地“赶忙”,在60秒内完成了发起了这两个请求,那么第二个请求应该不会立刻产生任何输出,而应该只是hung在那里。服务器也不该该在其标准输出上打印新的请求体。下面是在鱼的Mac上的状况(右下角以黄色突出显示的窗口显示第二个curl命令挂起,等待服务器接受链接):
当你等了60秒到了,你就会看到第二个“Hello, World!”出现了,而后服务器继续hung住60秒。
鱼们看到服务器完成对第一个curl客户机请求的服务,而后仅在第二个请求休眠60秒后才开始处理它。这一切都是由于“迭代服务器”是按顺序或迭代地进行的,一步一个步骤,或者在鱼们的状况下,一次处理一个请求。
为了更好地分析鱼们怎么解决这个性能问题,鱼们来谈谈客户端和服务器之间的通讯。
鱼们为了让两个程序经过网络相互通讯,必须使用套接字/Socket。前面的代码鱼们使用了Socker,那么什么是Socket?
Socket是通讯端点的抽象,它容许你的程序使用文件描述符与另外一个程序通讯。在本文中,鱼将特别讨论Linux/Mac OS X上的TCP/IP socket。
其中,鱼们须要理解,什么是Socket Pair(套接字对)?
TCP链接的Socket Pair是一个4元组,用于标识TCP链接的两个端点:本地IP地址、本地端口、外部IP地址和外部端口。Socket Pair惟一标识网络上的每一个TCP链接。标识每一个链接点的两个值(IP地址和端口号)一般称为Socket。
因此,元组{10.10.10.2:49152, 12.12.12.3:8888}是一个Socket Pair,它惟一地标识出客户端上两个终端的TCP链接;而元组 {12.12.12.3:8888, 10.10.10.2:49152}也是一个Socket Pair,标识出服务器上两个终端的TCP链接。地址12.12.12.3和端口8888两个值,能标识TCP链接的服务器端点,在这里鱼们称之为Socket(客户端依然)。
服务器建立Socket并开始接受客户端链接的标准顺序以下:
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)
listen方法仅由服务器调用,客户端没有。它告诉内核应该接受这个Socket的传入链接请求。
完成后,服务器开始在一个循环中一次接受一个客户端链接。当有可用的链接时,accept调用返回已链接的客户端Socket。而后,服务器从链接的客户端Socket中读取请求数据,在其标准输出上打印数据,并将消息发送回客户端。而后,服务器关闭客户端链接,并准备再次接受新的客户端链接。
鱼们再来看看客户端经过TCP/IP与服务器通讯所需执行的操做:
鱼们再看看客户端链接服务器再打印返回的代码,比较简单:
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())
客户端建立完Socket以后,须要链接服务端,这是经过connect函数作到的:
sock.connect(('localhost', 8888))
客户端只需提供远程IP地址或主机名以及要链接到的服务器的远程端口号便可。
你可能已经注意到客户端没有调用bind和accept,是的,客户端不须要调用bind,由于客户端不关心本地IP地址和本地端口号。
当客户端调用connect时,内核中的TCP/IP堆栈会自动分配本地IP地址和本地端口。本地端口称为临时端口,通常来讲很快就释放了。
通常,经常使用服务的端口称为经常使用端口,如HTTP服务的80端口,SSH服务的22端口。
若是你想知道你的客服端的本地端口是什么,可一启动Python shell并与在本地主机上运行的服务器创建客户端链接,而后查看内核为你建立的套接字分配的临时端口(在尝试如下示例以前启动服务器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)
在上述状况下,内核将临时端口60589分配给Socket。
除了Socket以外,鱼须要快速介绍一些其余重要的概念,分别是进程和文件描述符。你很快就会明白这些概念为何很重要。
什么是进程?进程只是执行程序的一个实例。例如,当服务器代码被执行时,它被加载到内存中,执行程序的一个实例称为进程。内核记录了一堆关于进程的信息,它的进程ID就是一个例子,用来跟踪它。当你运行迭代服务器webserver3a.py或webserver3b.py时,你只是运行一个进程。
在终端中启动webserver3b.py:
$ python webserver3b.py
在另外一个终端中,使用ps命令查看这个进程:
$ ps | grep webserver3b | grep -v grep 7182 ttys003 0:00.04 python webserver3b.py
ps命令显示你实际上只运行了一个Python进程webserver3b
。当建立一个进程时,内核会为它分配一个进程ID,即PID。在UNIX中,每一个用户进程都有一个父进程,该父进程又有本身的进程ID,称为父进程ID,简称PPID。鱼假设你在默认状况下运行BASH shell,当你启动服务器时,会建立一个带有PID的新进程,其父PID设置为bashshell的PID。
再次启动Python shell,它将建立一个新进程,而后鱼们使用os.getpid() 和os.getppid() 系统调用获取pythonshell进程的PID和父PID(bashshell的PID)。
而后,在另外一个终端窗口中为PPID(父进程ID,在鱼的例子中是3148)运行ps命令和grep。在下面的屏幕截图中,你能够看到鱼的Mac OS X上的子Python shell进程和父BASH shell进程之间的父子关系示例:
另外一个须要知道的重要概念是文件描述符。那么什么是文件描述符?是一个非负整数。
?什么鬼非负整数?
内核在打开现有文件、建立新文件或建立新Socket时返回给进程一个非负整数,这个非负整数就是文件描述符。
你可能据说过,在UNIX中,一切都是文件。内核经过文件描述符引用进程的打开文件。当你须要读或写一个文件时,你能够用文件描述符来识别它。
Python为你提供了处理文件(和socket)的高级对象,你没必要直接使用文件描述符来标识文件,但实际上,在UNIX中,文件和socket是经过它们的整数文件描述符来标识的。
默认状况下,unixshell将文件描述符0分配给进程的标准输入,将文件描述符1分配给进程的标准输出,将文件描述符2分配给标准错误。
如前所述,尽管Python提供了一个高级文件或相似文件的对象,但你始终能够对该对象使用 fileno() 方法来获取与该文件关联的文件描述符。回到Python shell,看看鱼们如何作到这一点:
>>> 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时,一般会使用高级文件/Socket对象,但有时可能须要直接使用文件描述符。
下面是一个例子,说明了如何使用以文件描述符整数为参数的write系统调用将字符串写入标准输出:
>>> import sys >>> import os >>> res = os.write(sys.stdout.fileno(), 'hello\n') hello
这里有一个有趣的事,不过对你来讲再也不奇怪,由于你已经知道全部东西都是Unix中的一个文件,你的socket也有一个与之相关的文件描述符。一样,在Python中建立一个socket时,会返回一个对象,而不是一个非负整数,但你始终可使用前面提到的fileno() 方法直接访问socket的整数文件描述符。
>>> import socket >>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) >>> sock.fileno() 3
鱼还想提一件事:你是否注意到在“迭代服务器”webserver3b.py
中,当服务器进程休眠60秒时,你仍然可使用第二个curl命令链接到服务器?固然,curl没有当即输出任何内容,它只是挂在那里,但为何服务器当时不接受链接,客户端也没有当即被拒绝,而是可以链接到服务器?答案是socket对象的listen方法及其BACKLOG参数,鱼在代码中称之为REQUEST-QUEUE-SIZE
。BACKLOG参数肯定内核中传入链接请求的队列大小。当服务器webserver3b.py
处于睡眠状态时,你运行的第二个curl命令可以链接到服务器,由于内核在服务器套接字的传入链接请求队列中有足够的可用空间。
虽然增长BACKLOG参数并不能神奇地将你的服务器转变为一次能够处理多个客户机请求的服务器,可是对于繁忙的服务器,有一个至关大的backlog参数是很重要的,这样accept调用就没必要等待创建新链接,而是能够当即从队列中获取新链接,并当即开始处理客户端请求。
到目前为止,文章以上的内容覆盖了不少知识点,鱼们复习下:
如今鱼们已经准备好回答那一个问题:“为了提升你的程序的性能,你如何让你的服务器一次处理多个请求?”
或者鱼们换一个问法:“你怎么写一个并发的服务器呢?”
写一个并发服务器最简单的方法,是在Unix系统下使用fork()系统调用。
下面是新的闪亮登场的并发服务器的代码,命名为webserver3c.py
,它能够同时处理多个客户端请求(在鱼们的迭代服务器示例webserver3b.py中,每一个子进程睡眠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是如何工做的以前,鱼们先尝试一下并亲眼确认服务器确实能够同时处理多个客户机请求。
不一样于webserver3a.py
和webserver3b.py
,使用如下命令行启动服务器:
$ python webserver3c.py
在迭代服务器上尝试一样的两个curl命令,而后鱼们能够看到,即便服务器子进程在服务客户机请求后睡眠60秒,它也不会影响其余客户端的请求,由于它们由不一样的彻底独立的进程提供服务。你应该看到curl命令输出“Hello,World!”立刻就hung住60秒。你能够继续运行任意数量的curl命令(固然不能大于fd的上限),全部这些命令都将当即输出服务器的响应“Hello,World”,不会有任何明显的延迟。
关于fork()要注意一点,一个代码里面调用fork一次,它会返回两次:一次在父进程中,一次在子进程中。派生新进程时,返回给子进程的进程ID为0。当fork在父进程中返回时,它返回子进程的PID。
鱼依稀记得当鱼第一次读到并尝试fork的时候鱼是多么的迷,如今这在鱼看来依然很神奇。
更多地关注造壳。
当父进程派生新的子进程时,子进程将获取父进程的文件描述符的副本:
你可能注意到上面代码中的父进程关闭了客户端链接:
else: # parent client_connection.close() # close parent copy and loop over
那么,当一个子进程的父进程关闭了同一个套接字,它为何还能从客户端socket里面读取数据呢?
答案如上图所示。内核使用描述符引用计数来决定是否关闭socket。它只在其描述符引用计数变为0时,关闭套接字。
当服务器建立子进程时,子进程获取父进程文件描述符的副本,内核增长这些描述符的引用计数。在一个父进程和一个子进程的状况下,客户端socket的描述符引用计数为2,当上面代码中的父进程关闭客户端链接套接字时,它只会减小其引用计数,该计数将变为1,不足以致使内核关闭socket。
另外,子进程还关闭父进程侦听套接字的副本,由于子进程不关心接受新的客户端链接,它只关心处理来自已创建的客户端链接的请求:
listen_socket.close() # close child copy
鱼将在本文后面讨论若是不关闭重复的描述符会发生什么。
从并发服务器的源代码中能够看到,服务器父进程如今的惟一角色是接受一个新的客户端链接,派生一个新的子进程来处理该客户端请求,并循环接受另外一个客户端链接,仅此而已。服务器父进程不处理客户端请求,而是让其子进程处理。
鱼们先讨论另外一个问题,鱼们所说的两个事件同时发生是什么意思?
当鱼们说两个事件同时发生时,一般是指它们同时发生。这个定义很好,但你应该记住严格的定义:
若是你看不出哪一个程序会先发生,那么两个事件是并发的。
再次重申一下,如今是时候回顾一下你迄今为止所涉及的主要思想和概念了。
fork()
系统调用让鱼们看看若是不关闭父进程和子进程中的重复套接字描述符,将会发生什么。webserver3d.py
是并发服务器的修改版本,其中服务器不关闭重复的描述符:
########################################################################### # 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()
运行起来:
$ python webserver3d.py
使用curl请求服务器:
$ curl http://localhost:8888/hello Hello, World!
curl打印了并发服务器的响应,但它没有终止并一直挂起。服务器再也不休眠60秒:其子进程主动处理客户端请求,关闭客户端链接并退出,但客户端这边的curl仍然没有终止。
为何curl不终止?缘由是文件描述符还有余。
当子进程关闭客户端链接时,内核减小了该客户端套接字的引用计数,计数变为1。服务器子进程已退出,但客户端套接字未被内核关闭,由于该套接字描述符的引用计数不是0。所以,终止数据包(在TCP/IP术语中称为FIN)未发送到客户端,客户端保持在线。
若是长时间运行的服务器没有关闭重复的文件描述符,它最终将耗尽可用的文件描述符:
使用Control-C
终止你webserver3d.py
程序,检查下你所在服务器上的默承认用资源,可使用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机器上,服务器进程可使用的打开文件描述符(打开的文件)的最大数量是1024个。
如今让鱼们看看若是服务器不关闭重复的描述符,它将如何耗尽可用的文件描述符。在现有或新的终端窗口中,将服务器的最大打开文件描述符数设置为256:
$ ulimit -n 256
在同一个终端中启动服务:
$ python webserver3d.py
而后使用如下的自动化代码client3.py
,模拟请求数量比较多的客户端:
##################################################################### # 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)
在一个新的终端中,启动client3.py
,请求量设定为300:
$ python client3.py --max-clients=300
很快你的服务器就会爆炸。这是鱼实验时异常的截图:
试验给鱼们带来的教训很清楚:服务器应该关闭重复的描述符。
但即便你关闭了重复的描述符,你尚未走出困境,由于你的服务器还有一个问题,那就是僵尸进程!
是的,其实你的服务器代码实际上建立了僵尸进程。让鱼们看看怎么作。鱼们先从新启动服务器:
$ python webserver3d.py
在另外一个终端中,使用curl请求:
$ curl http://localhost:8888/hello
如今运行ps命令来显示正在运行的Python进程。这是鱼的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的进程的状态是Z+,进程的名称是<deflunct>?那是鱼们的僵尸进程。甚至鱼们还不能杀死他们。
即便鱼们使用kill -9
,他们依然会存在。
什么是僵尸进程?为何鱼们的服务器要建立僵尸进程?
僵尸进程是已终止的进程,但其父进程还没有等待它,也还没有收到其终止状态。当子进程在其父进程以前退出时,内核会将子进程变成僵尸进程,并存储一些有关该进程的信息,供其父进程之后检索。存储的信息一般是进程ID、进程终止状态和进程的资源使用状况。
因此说,僵尸进程是有目的的,可是若是你的服务器不处理这些僵尸进程,你的系统最终就会被阻塞。
让鱼们试验看看不清理僵尸进程会怎么样。
首先中止正在运行的服务器,并在新的终端窗口中,使用ulimit命令将max user processess设置为400(确保将open files设置为一个很高的数字,也就是说500):
$ ulimit -u 400 $ ulimit -n 500
在刚刚运行$ulimit-u 400
命令的同一终端中,启动服务器webserver3d.py
:
$ python webserver3d.py
在新的终端窗口中,启动client3.py
并告诉它建立500个到服务器的同时链接:
$ python client3.py --max-clients=500
很快,你的服务器就会出现一个OSError:Resource temporary unavailable
异常,当它试图建立一个新的子进程时,建立失败,由于它已经达到了容许建立的最大子进程数的限制。这是鱼机器上异常的截图:
如你所见,若是不处理僵尸进程,就会给长期运行的服务器带来问题。鱼将后面会讨论服务器应该如何处理僵尸问题。
鱼们再回顾一下知识点:
那么你须要作什么来处理掉僵尸进程呢?你须要修改服务器代码,以等待僵尸进程,得到其终止状态。而后能够经过修改服务器来调用等待系统调用来完成此操做。
不幸的是,这远不是理想的,由于若是调用wait,将阻塞服务器,从而有效地阻止服务器处理新的客户端链接请求。还有其余选择吗?是的,有,其中一个是信号处理程序和等待系统调用的组合。
鱼们来看一下工做原理。当子进程退出时,内核发送一个SIGCHLD信号。父进程能够设置一个信号处理程序,以异步通知该SIGCHLD事件,而后它能够等待子进程收集其终止状态,从而防止僵尸进程留在周围。
顺便说一下,异步事件意味着父进程不能提早知道事件将要发生。
修改服务器代码以设定SIGCHLD事件,并在事件处理程序中等待终止的子进程。修改代码得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()
启动服务器:
$ python webserver3e.py
再请求一次:
$ curl http://localhost:8888/hello
看服务器终端:
发生了什么呢?accept调用失败了,返回了EINTR错误。
当子进程退出致使SIGCHLD事件后,父进程accept调用会被阻塞,而后致使激活了信号处理程序,当信号处理程序完成时,accept系统调用被中断:
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:
不过这是个很简单的问题,只须要从新启动accept系统调用。修改版本webserver3f.py
解决了这个问题:
########################################################################### # 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()
启动升级后的webserver3f.py
:
$ python webserver3f.py
请求一下:
$ curl http://localhost:8888/hello
如今再也不有EINTR异常了。
接下来鱼们再验证是否再也不有僵尸进程,以及带有wait调用的SIGCHLD事件处理程序是否处理已终止的子进程。
要作到这一点,只需运行ps命令,并亲自查看再也不有Z+状态的Python进程(再也不有<definct>进程)就ok了。
好吧,到目前为止还不错。没问题吧?好吧,差很少了。再次尝试webserver3f.py
,但不要使用curl发出一个请求,而是使用client3.py
建立128个同时链接:
$ python client3.py --max-clients 128
运行下ps命令看下:
$ ps auxw | grep -i python | grep -v grep
然而,僵尸进程还在。
此次是什么问题呢?当你同时运行128个客户端并创建128个链接时,服务器上的子进程处理这些请求并几乎同时退出,致使大量SIGCHLD信号被发送到父进程。问题是,信号没有排队,服务器进程错过了几个信号,致使几个僵尸进程无人值守:
解决这个问题的方法是,设置一个SIGCHLD事件处理程序,可是不要等待,而是在循环中使用带WNOHANG选项的waitpid系统调用,以确保全部终止的子进程都获得处理。如下是修改后的服务器代码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()
启动服务:
$ python webserver3g.py
发起请求:
$ python client3.py --max-clients 128
如今能够确认没有僵尸进程了。
恭喜!代码的旅途很漫长,但总算结束了。
如今你拥有了本身的简单并发服务器,代码能够做为你进一步面向生产级Web服务器的基础。
接下来是什么?正如乔希·比林斯所说,
“就像一张邮票,坚持一件事,直到你到达那里。”
开始掌握基本知识,质疑你已经知道的,而后老是深刻挖掘。
“若是你只学方法,你就会被方法束缚住。但若是你学会了原则,你就能够设计出本身的方法。” —— 爱默生。
下面是鱼为这篇文章中的大部份内容而绘制的书籍列表。它们将帮助你拓宽和加深你对鱼所涉及主题的知识。鱼强烈建议你以某种方式去买那些书:从你的朋友那里借,从你当地的图书馆里看,或者在亚马逊上买:
先这样吧