【Python Programe】WSGI (Web Server Gateway Interface)
Part1:
What is a Web server?
一个位于物理服务器上的网络服务器(服务器里的服务器),等待客户端去发送request,当服务器接收到request,就会生成一个response发送回客户端;
客户端与服务器使用HTTP协议进行通信,客户端可以是浏览器或者其他使用HTTP协议的软件。
一个简单的WEB服务器实现:
import socket HOST,PORT = '',8899 listen_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) listen_socket.setblocking(1) listen_socket.bind((HOST,PORT)) listen_socket.listen(1) print('Serving HTTP on port %s ...' % PORT) while True: client_connection,client_address = listen_socket.accept() request = client_connection.recv(1024) print(request) http_response = """ HTTP/1.1 200 OK Hello, World! """ client_connection.sendall(bytes(http_response,encoding='utf-8')) client_connection.close()
保存为webserver1.py 并且 命令行运行
$ python webserver1.py
Serving HTTP on port 8899 …
浏览器输入 http://localhost:8899/hello
刚才输入的WEB地址,它叫URL,这是它的基本结构:
它表示了浏览器要查找和连接的WEB服务器地址,和你要获取的服务器上的页面(路径)。
在浏览器发送HTTP request之前,他需要先与服务端建立TCP连接,然后浏览器在TCP连接上发送HTTP request,然后等待服务器回发HTTP response。当浏览器接收到响应后,显示响应,在本次例子中,浏览器显示“Hello, World!”。
在建立连接时使用到了socket,我们可以用命令行下的telnet模拟浏览器进行测试
在运行WEB服务器的同一台电脑上,命令行启动一个telnet session,指定连接到localhost主机,连接端口为8899,然后按回车:
$ telnet localhost 8899 Trying 127.0.0.1 … Connected to localhost.
此时,你已经和运行在你本地主机的服务器建立了TCP连接,已经准备好发送并接收HTTP消息了。
下图中你可以看到一个服务器要经过的标准步骤,然后才能接受新的TCP连接。
$ telnet localhost 8899 Trying 127.0.0.1 … Connected to localhost. GET /hello HTTP/1.1 HTTP/1.1 200 OK Hello, World!
通过这个流程模拟了浏览器,发送http request,获得http response
HTTP Request
HTTP请求由行组成。标明了HTTP方法(GET,我们要服务器返回给我们东西),我们想要的服务器上的“页面”路径/hello 和 协议版本
为了简单起见,此时我们的WEB服务器完全忽略了上面的请求行。你也可以输入任何字符取代“GET /hello HTTP/1.1”,你仍然会得到“Hello, World!”响应。
一旦你输入了请求行,敲了回车,客户端就发送请求给服务器,服务器读取请求行,打印出来然后返回相应的HTTP响应。
HTTP Response
以下是服务器回发给客户端(这个例子中是telnet)的HTTP响应:
Response 包含了状态行 HTTP/1.1 200 OK , 紧接着是一个必须的空白行!然后是HTTP response 内容
状态行 HTTP/1.1 200 OK ,包含了HTTP版本,HTTP状态码200,HTTP状态码短语OK,当浏览器获得获得response,就显示response里body的内容。
总的来说:
Web Server 创建一个 listening socket 和 在循环里 accepting 新连接,客户端初始化一个TCP连接,建立成功后,客户端发送HTTP request 给服务端,然后服务端响应 HTTP reponse,客户端和服务端都使用socket建立TCP连接。
现在你有了一个非常基础的WEB服务器,你可以用浏览器或其他的HTTP客户端测试它。
Question:
How do you run a Django application, Flask application, and Pyramid application under your freshly minted Web server without making a single change to the server to accommodate all those different Web frameworks ?
怎样在你刚完成的WEB服务器下运行 Django 应用、Flask 应用和 Pyramid 应用?在不单独修改服务器来适应这些不同的 WEB 框架的情况下?
Part2:
过去,你所选择的一个Python Web框架会限制你选择可用的Web服务器,反之亦然。如果框架和服务器设计的是可以一起工作的,那就很好:
但是,当你试着结合没有设计成可以一起工作的服务器和框架时,你可能要面对(可能你已经面对了)下面这种问题:
基本上,你只能用可以在一起工作的部分,而不是你想用的部分。
那么,怎样确保在不修改Web服务器和Web框架下,用你的Web服务器运行不同的Web框架?
答案就是Python Web Server Gateway Interface(或者缩写为WSGI,读作“wizgy”)。
WSGI允许开发者把框架的选择和服务器的选择分开。现在你可以真正地混合、匹配Web服务器和Web框架了。
你可以运行 Django, Flask, or Pyramid, 在 Gunicorn or Nginx/uWSGI or Waitress. 上。
你的Web服务器必须是实现WSGI接口的服务器,所有的现代Python Web框架已经实现了WSGI接口的框架端了,这就让你可以不用修改服务器代码,适应某个框架。
现在你了解了Web服务器和WEb框架支持的WSGI允许你选择一对合适的(服务器和框架),其他语言也有相似的接口:例如,Java有Servlet API,Ruby有Rack。
简单的WSGI Server 代码:
#!/usr/bin/env python # -*-coding:utf-8 -*- import socket from io import StringIO import sys class WSGIServer(object): address_family = socket.AF_INET socket_type = socket.SOCK_STREAM request_queue_siez = 1 def __init__(self,server_address): self.listen_socket = listen_socket = socket.socket( self.address_family, self.socket_type ) listen_socket.setblocking(1) listen_socket.bind(server_address) listen_socket.listen(self.request_queue_siez) # 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 self.headers_set = [] def set_app(self,application): self.application = application def server_forever(self): listen_socket = self.listen_socket while True: self.client_connection,client_address = listen_socket.accept() self.handle_one_request() def handle_one_request(self): self.request_data = request_data = str(self.client_connection.recv(1024),encoding='utf-8') # request line self.parse_request(request_data) # get environ 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) self.finish_response(result) def parse_request(self,text): request_line = text.splitlines()[0] (self.request_method, self.path, self.request_version, ) = request_line.split() def get_environ(self): env = {} # Required WSGI variables env['wsgi.version'] = (1, 0) env['wsgi.url_scheme'] = 'http' env['wsgi.input'] = 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,respnse_headers,exc_info=None): # Add necessary server headers server_headers = [ ('Date', 'Tue, 31 Mar 2017 12:54:48 GMT'), ('Server', 'WSGIServer 0.2'), ] self.headers_set = [status , respnse_headers + server_headers] def finish_response(self,result): result = str(result[0], encoding='utf8') try: status,response_headers = self.headers_set response = 'HTTP/1.1 {status}\r\n'.format(status=status) for header in response_headers: response += '{0}:{1}\r\n'.format(*header) response += '\r\n' for date in result: response += date print(''.join( '> {line}\n'.format(line=line) for line in response.splitlines() )) self.client_connection.sendall(bytes(response,encoding='utf-8')) finally: self.client_connection.close() SERVER_ADDRESS = (HOST, PORT) = '', 8899 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('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT)) httpd.server_forever()
它可以运行你喜欢的Web框架写的基本的Web应用,可以是Pyramid,Flask,Django,或者其他的Python WSGI框架。
安装pyramid、flask、django
$ [sudo] pip install virtualenv $ mkdir ~/envs $ virtualenv ~/envs/lsbaws/ $ cd ~/envs/lsbaws/ $ ls bin include lib $ source bin/activate (lsbaws) $ pip install pyramid (lsbaws) $ pip install flask (lsbaws) $ pip install django
pyramid
创建一个pyramid的工程,保存为pyramidapp.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()
命令行输入:
(lsbaws) $ python webserver2.py pyramidapp:app
WSGIServer: Serving HTTP on port 8888 ...
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
Django
import sys sys.path.insert(0, './helloworld') from helloworld import wsgi app = wsgi.application
WSGI可以让你把Web服务器和Web框架结合起来。
WSGI提供了Python Web服务器和Python Web框架之间的一个最小接口,在服务器和框架端都可以轻易实现。
下面的代码片段展示了(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 ['Hello world!'] run_application(app)
工作流程:
-
Framework 提供一个 可调用对象 application callable
-
服务器每次接收到HTTP Client request后,服务器把一个包含了WSGI/CGI变量的字典 和 一个 start_response’ callable 做为参数 传递给 ’application’ callable
-
Framework/Application 生成HTTP状态 和 HTTP响应头,然后把它们传给 start_response’ callable,让服务器保存它们。最后 Framework/Application 返回一个 response body
-
服务器把状态,响应头,响应体合并到HTTP响应里,然后传给 HTTP客户端(这步不是(WSGI)规格里的一部分
自定义Application
此时,我们不使用Framework,自己编写一个简单的app:
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 ['Hello world from a simple WSGI application!\n']
保存以上代码到wsgiapp.py文件
(lsbaws) $ python webserver2.py wsgiapp:app
WSGIServer: Serving HTTP on port 8899 ...
使用HTTP客户端调用Pyramid应用时生成的HTTP响应:
Content-Type, Content-Length, Date, 和Servedr。这些headers是Web服务器组合而成的。虽然他们并不是必须的。headers目的是传输HTTP请求/响应的额外信息。
’environ’字典,必须包含WSGI规范规定的必要的WSGI和CGI变量。
服务器在解析请求后,从HTTP请求拿到了字典的值,字典的内容看起来像下面这样:
Web框架使用字典里的信息来决定使用哪个视图,基于指定的路由,请求方法等,从哪里读请求体,错误写到哪里去,如果有的话。
总结
简要重述下WSGI Web服务器必须做哪些工作才能处理发给WSGI应用的请求吧:
-
首先,服务器启动并加载一个由Web框架/应用提供的可调用的’application’
-
然后,服务器读取请求
-
然后,服务器解析它
-
然后,服务器使用请求的数据创建了一个’environ’字典
-
然后,服务器使用’environ’字典和’start_response’做为参数调用’application’,并拿到返回的响应体。
-
然后,服务器使用调用’application’返回的数据,由’start_response’设置的状态和响应头,来构造HTTP响应。
-
最终,服务器把HTTP响应传回给户端。
现在你有了一个可工作的WSGI服务器,它可以处理兼容WSGI的Web框架如:Django,Flask,Pyramid或者你自己的WSGI框架。
最优秀的地方是,服务器可以在不修改代码的情况下,使用不同的Web框架。
Question:
How do you make your server handle more than one request at a time?
该怎么做才能让服务器同一时间处理多个请求呢?
Part3:
服务器同一时间只处理一个客户端请求,在每次发送给客户端响应后添加一个60秒的延迟进行测试:
#!/usr/bin/env python # -*-coding:utf-8 -*- 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__':
使用curl 命令来进行测试,屏幕上输出 hello World!:
$ curl http://localhost:8888/hello
Hello, World!
再打开另外一个terminal,输入同样的内容,发现不会立刻产生任何输出,而是挂起。而且服务器也不会打印出新请求。
当你等待足够长时间(大于60秒)后,你会看到第一个curl终止了,第二个curl在屏幕上打印出“Hello, World!”,然后挂起60秒,然后再终止:
服务器完成处理第一个curl客户端请求,然后睡眠60秒后开始处理第二个请求。
两个程序间的网络通信通常是使用 Socket(插座) 来完成的,它允许你的程序使用 file descriptor(文件描述符) 和别的程序通信。
本文将详细谈谈在Linux上的TCP/IP socket。理解socket的一个重要的概念是TCP socket pairs
socket pairs 是由 4-tuple (4元组) 构成,分别是本地ip,本地端口,目标ip,目标端口。
一个socket pairs 唯一标识着网络上的TCP连接
标识着每个 endpoint 终端的两个值:IP地址和端口号,通常被称为socket。
tuple{10.10.10.2:49152, 12.12.12.3:8888}是客户端TCP连接的唯一标识着两个终端的socket pairs
tuple{12.12.12.3:8888, 10.10.10.2:49152}是服务器TCP连接的唯一标识着两个终端的socket pairs
服务器创建一个socket并开始接受客户端连接的标准流程经历通常如下:
1、服务器创建一个TCP/IP Socket
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2、设置Socket options
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
3、服务器绑定地址
listen_socket.bind(SERVER_ADDRESS)
4、监听Socket
listen_socket.listen(REQUEST_QUEUE_SIZE)
listen方法只会被服务器调用。它告诉Kernel内核,它要接收这个socket上到来的连接请求
服务器开始循环地接收客户端连接。
当有连接到达时,accept call 返回Client Socket,服务器从Client Socket 读取request data,在 standard output标准输出中打印内容,发送信息给Client,然后服务器关闭客户端连接,准备好再次接受新的客户端连接。
下面是客户端使用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())
客户端仅需提供一个远程ip地址或者host name 和远程端口,
客户端没必要调用bind,是因为客户端不关心本地IP地址和本地端口号。
当客户端调用connect时,kernel 的TCP/IP栈自动分配一个本地IP址地和本地端口。
本地端口被称为暂时端口( ephemeral port),也就是,short-lived 端口。
服务器上标识着一个客户端连接的众所周知的服务的端口被称为well-known端口(举例来说,80就是HTTP,22就是SSH)
>>> import socket >>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) >>> sock.connect(('localhost', 8888)) >>> host, port = sock.getsockname() >>> host, port ('127.0.0.1', 60589)
上面这个例子中,内核分配了60589这个暂时端口。
What is a process?
进程就是一个正在运行的程序的实例。
当服务器代码执行时,它被加载进内存,运行起来的程序实例被称为进程。
内核Kernel记录了进程的一堆信息用于跟踪,进程ID就是一个例子。
在控制台窗口运行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里运行的服务器,那新进程的父进程ID就是BASH shell的进程ID。
子Python shell进程和父BASH shell进程的关系:
what is a file descriptor?
fire descriptor(文件描述符)是当你打开文件、创建文件、创建Socket时,内核返回的一个非负整数
你可能已经听过啦,在UNIX里一切皆文件。
内核使用文件描述符来追踪进程打开的文件,当需要读或写文件时,可以用文件描述符标识它;
Python给你包装成更高级别的对象来处理文件(和socket),你不必直接使用文件描述符来标识一个文件
但是,在底层,UNIX中是这样标识文件和socket的:通过它们的整数文件描述符。
默认情况下,UNIX shell分配文件描述符0给进程的标准输入,文件描述符1给进程的标准输出,文件描述符2给标准错误。
可以使用对象的 fileno() 方法来获取对应的文件描述符。
>>> import sys >>> sys.stdin <open file '<stdin>', mode 'r' at 0x102beb0c0> >>> sys.stdin.fileno() 0 >>> sys.stdout.fileno() 1 >>> sys.stderr.fileno() 2
使用write system call 去输出一个字符串,使用文件描述符作为参数。
>>> import sys >>> import os >>> res = os.write(sys.stdout.fileno(), 'hello\n') hello
Socket使用文件描述符:
>>> import socket >>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) >>> sock.fileno() 3
当服务器进程在60秒的睡眠时你仍然可以用curl命令来连接,但是curl没有立刻输出内容,它只是在那挂起。
因为设置了 socket对象的listen方法和它的BACKLOG参数, REQUEST_QUEUE_SIZE(请求队列长度)。
BACKLOG参数决定了内核为进入的连接请求准备的队列长度。
当服务器睡眠时,第二个curl命令可以连接到服务器,因为内核在服务器socket的进入连接请求队列上有足够的可用空间。
然而增加BACKLOG参数不会让服务器同时处理多个客户端请求,需要设置一个合理的backlog参数,这样accept调用就不用再等新连接到来,立刻就能从队列里获取新的连接,然后开始处理客户端请求。
How do you write a concurrent server?
在Unix上写一个并发服务器最简单的方法是使用fork()系统调用
它能同时处理多个客户端请求
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()
虽然服务器子进程在处理客户端请求时睡眠60秒,但不影响别的客户端,因为它们是被不同的完全独立的进程处理的。
你可以看到curl命令立刻就输出了“Hello, World!”,然后挂起60秒。
理解 fork() 最重要的一点是,你 fork 了一次,但它返回了两次!一个是在父进程里,一个是在子进程里。
当你 fork 了一个新进程,子进程返回的进程ID是0。父进程里fork返回的是子进程的PID
当父进程fork了一个新的子进程,子进程就获取了父进程文件描述符的拷贝:
你可能已经注意到啦,上面代码里的父进程关闭了客户端连接:
else: # parent client_connection.close() # close parent copy and loop over
如果它的父进程关闭了同一个socket,子进程为什么还能从客户端socket读取数据呢?
因为,内核使用描述符引用计数来决定是否关闭socket,只有当描述符引用计数为0时才关闭socket。
当服务器创建一个子进程时,子进程获取了父进程的文件描述符拷贝,内核增加了这些描述符的引用计数。
在一个父进程和一个子进程的场景中,客户端socket的描述符引用计数就成了2,
当父进程关闭了客户端连接socket,它仅仅把引用计数减为1,不会引发内核关闭这个socket。
子进程也把父进程的 listen_socket 拷贝给关闭了,因为子进程不用接受新连接,它只关心处理已经连接的客户端的请求
listen_socket.close() # close child copy
what happens if you do not close duplicate descriptors?
现在服务器父进程唯一的角色就是接受一个新的客户端连接,fork一个新的子进程来处理客户端请求,然后重复接受另一个客户端连接
What does it mean when we say that two events are concurrent?
当我们说两个事件并发时,我们通常表达的是它们同时发生。
定义为:如果你不能通过观察程序来知道哪个先发生的,那么这两个事件就是并发的。
Two events are concurrent if you cannot tell by looking at the program which will happen first.
服务器不关闭复制的描述符例子:
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()
curl 打印出来内容后,它并不终止而是一直挂起。
它的子进程处理了客户端请求,关闭了客户端连接然后退出,但是客户端curl仍然不终止。
当子进程关闭了客户端连接,内核减少引用计数,值变成了1。
服务器子进程退出,但是客户端socket没有被内核关闭掉,因为引用计数不是0,
所以,结果就是,终止数据包(在TCP/IP说法中叫做FIN)没有发送给客户端,所以客户端就保持在线啦。
这里还有个问题,如果服务器不关闭复制的文件描述符然后长时间运行,最终会耗尽可用文件描述符。
使用shell内建的命令ulimit检查一下shell默认设置的进程可用资源:
$ 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
在同一个控制台上启动webserver3d.py:
使用下面的Client代码进行测试:
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)
在新的控制台窗口里,启动client.py,让它创建300个连接同时连接服务器。
$ python client3.py --max-clients=300
很快服务器就崩了。
服务器应该关闭复制的描述符。但即使关闭了复制的描述符,你还没有接触到底层,因为你的服务器还有个问题,zombies僵尸!
再次运行服务器,在另一个控制台窗口运行curl命令
运行ps命令,显示运行着的Python进程。
$ 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+,进程的名称是 <defunct>,这个就是僵尸进程。它的问题在于,你杀死不了他们。
使用 kill -9 也杀死不了他们!
Zombies是它的父进程没有等它,还没有接收到它的终止状态。
当一个子进程比父进程先终止,内核把子进程转成僵尸,存储进程的一些信息,等着它的父进程以后获取。
存储的信息通常是进程ID,进程终止状态,进程使用的资源。
如果服务器不好好处理这些僵尸,系统就会越来越堵塞。
首先停止服务器,然后新开一个控制台窗口,使用ulimit命令设置最大用户进程为400(确保设置打开文件更高,如500):
$ ulimit -u 400
$ ulimit -n 500
启动Server
$ python webserver3d.py
新开一个控制台,启动Client
python client3.py --max-clients=500
服务器又一次崩了,是OSError的错误:抛出资源临时不可用的异常,当试图创建新的子进程时但创建不了时,因为达到了最大子进程数限制。
如果不处理好僵尸,服务器长时间运行就会出问题。
what do you need to do to take care of zombies ?
需要获取它们的终止状态。可以通过调用 wait 来解决。
不幸的是,如果调用wait,就会阻塞服务器,实际上就是阻止了服务器处理新的客户端连接请求。
我们可以使用signal handler 和 wait system call 相组合的方法!
当子进程结束时,内核发送一个SIGCHLD 信号,父进程可以设置一个Signal handler 来异步的被通知,然后就能wait子进程获取它的终止状态,因此阻止了僵尸进程出现。
asynchronous event 异步事件意味着父进程不会提前知道事件发生的时间。
SIGCHLD 信号:
子进程结束时, 父进程会收到这个信号。
signal(参数一,参数二)
-
参数一:我们要进行处理的信号。系统的信号我们可以再终端键入 kill -l查看(共64个)。其实这些信号时系统定义的宏。
-
参数二:我们处理的方式(是系统默认还是忽略还是捕获)。可以写一个handdle函数来处理我们捕获的信号。
那么 SIGCHILD 和 wait 到底是一个什么关系呢?
其实这两者之间没有必然的关系。
主进程可以直接调用waitpid or wait来回收子进程的结束状态,不一定非得通过SIGCHILD信号处理函数,也就是说waitpid or wait不是依靠SIGCHLD信号是否到达来判断子进程是否结束。但是如果主进程除了回收子进程状态以外还有其他的业务需要处理那么最好是通过SIGCHILD信号处理函数来调用waitpid or wait,因为这是异步的操作。
服务器端修改后代码为:
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))
# 绑定信号处理函数,将SIGCHLD绑定在函数grim_reaper上面 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()
观察服务器:
The call to accept failed with the error EINTR.
当子进程退出,引发SIGCHLD事件时,激活了事件处理器,此时父进程阻塞在accept调用,然后当事件处理器完成时,accept系统调用就中断了:
我们需要重新调用accept()
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()
现在我们使用Client直接创建128个并发的连接进行测试:
python client.py --max-clients 128
看到了吧,少年,僵尸又回来了!
当你运行128个并发客户端时,建立了128个连接,子进程处理了请求然后几乎同时终止了,这就引发了SIGCHLD信号洪水般的发给父进程。问题在于,UNIX信号往往是不会排队的,父进程错过了一些信号,导致了一些僵尸到处跑没人管:
解决方案就是设置一个SIGCHLD事件处理器,但不用wait了,改用waitpid system call,带上WNOHANG参数,循环处理,确保所有的终止的子进程都被处理掉。
pid_t waitpid(pid_t pid,int *status,int options)
从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。
参数status::用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。
参数pid:需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
参数option:提供了一些额外的选项来控制waitpid,目前在Linux中只支持 WNOHANG 和 WUNTRACED 两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用
如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
返回值:
当正常返回的时候,waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
以下是修改后的webserver3g.py:
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()
there are no more zombies. Yay! Life is good without zombies :)
现在你已经拥有了自己的简单并发服务器,而且这个代码有助于你在将来的工作中开发一个产品级的Web服务器。
修改第二部分的代码达到并发的效果,👉详情代码
What’s next? As Josh Billings said,
“Be like a postage stamp — stick to one thing until you get there.”
Start mastering the basics. Question what you already know. And always dig deeper.
“If you learn only methods, you’ll be tied to your methods. But if you learn principles, you can devise your own methods.” —Ralph Waldo Emerson”
https://ruslanspivak.com