【Python Programe】WSGI (Web Server Gateway Interface)

Part1:

  What is a Web server?

HTTP Request/Response

  一个位于物理服务器上的网络服务器(服务器里的服务器),等待客户端去发送request,当服务器接收到request,就会生成一个response发送回客户端;

  客户端与服务器使用HTTP协议进行通信,客户端可以是浏览器或者其他使用HTTP协议的软件。

 

一个简单的WEB服务器实现

import socketHOST,PORT = '',8899listen_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 OKHello, 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.1HTTP/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 sysclass WSGIServer(object):address_family = socket.AF_INETsocket_type = socket.SOCK_STREAMrequest_queue_siez = 1def __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 porthost,port = self.listen_socket.getsockname()[:2]self.server_name = socket.getfqdn(host)self.server_port = port# return headersself.headers_set = []def set_app(self,application):self.application = applicationdef server_forever(self):listen_socket = self.listen_socketwhile 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 environenv = self.get_environ()# It's time to call our application callable and get# back a result that will become HTTP response bodyresult = 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 variablesenv['wsgi.version'] = (1, 0)env['wsgi.url_scheme'] = 'http'env['wsgi.input'] = StringIO(self.request_data)env['wsgi.errors'] = sys.stderrenv['wsgi.multithread'] = Falseenv['wsgi.multiprocess'] = Falseenv['wsgi.run_once'] = False# Required CGI variablesenv['REQUEST_METHOD'] = self.request_method  # GETenv['PATH_INFO'] = self.path                 # /helloenv['SERVER_NAME'] = self.server_name        # localhostenv['SERVER_PORT'] = str(self.server_port)   # 8888return envdef start_response(self,status,respnse_headers,exc_info=None):# Add necessary server headersserver_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_setresponse = '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 += dateprint(''.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) = '', 8899def make_server(server_address,application):server = WSGIServer(server_address)server.set_app(application)return serverif __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 Responsedef 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 wsgiapp = 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 clientheaders_set = []# Environment dictionary with WSGI/CGI variablesenviron = {}def start_response(status, response_headers, exc_info=None):headers_set[:] = [status, response_headers]# Server invokes the ‘application' callable and gets back the# response bodyresult = 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)

工作流程:

  1. Framework 提供一个 可调用对象 application callable 

  2. 服务器每次接收到HTTP Client request后,服务器把一个包含了WSGI/CGI变量的字典  和 一个 start_response’ callable 做为参数 传递给 ’application’ callable

  3. Framework/Application 生成HTTP状态 和 HTTP响应头,然后把它们传给 start_response’ callable,让服务器保存它们。最后 Framework/Application 返回一个 response body

  4. 服务器把状态,响应头,响应体合并到HTTP响应里,然后传给 HTTP客户端(这步不是(WSGI)规格里的一部分

WSGI Interface

自定义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 timeSERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5def handle_request(client_connection):request = client_connection.recv(1024)print(request.decode())http_response = b"""\
HTTP/1.1 200 OKHello, World!
"""client_connection.sendall(http_response)time.sleep(60)  # sleep and block the process for 60 secondsdef 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 serversock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)sock.connect(('localhost', 8888))# send and receive some datasock.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 timeSERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5def 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 OKHello, 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:  # childlisten_socket.close()  # close child copy
            handle_request(client_connection)client_connection.close()os._exit(0)  # child exits hereelse:  # parentclient_connection.close()  # close parent copy and loop overif __name__ == '__main__':serve_forever()

  虽然服务器子进程在处理客户端请求时睡眠60秒,但不影响别的客户端,因为它们是被不同的完全独立的进程处理的。

  你可以看到curl命令立刻就输出了“Hello, World!”,然后挂起60秒。

  理解 fork() 最重要的一点是,你 fork 了一次,但它返回了两次!一个是在父进程里,一个是在子进程里。

  当你 fork 了一个新进程,子进程返回的进程ID是0。父进程里fork返回的是子进程的PID

  

  当父进程fork了一个新的子进程,子进程就获取了父进程文件描述符的拷贝:

  

  你可能已经注意到啦,上面代码里的父进程关闭了客户端连接:

else:  # parentclient_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 socketSERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5def handle_request(client_connection):request = client_connection.recv(1024)http_response = b"""\
HTTP/1.1 200 OKHello, 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:  # childlisten_socket.close()  # close child copy
            handle_request(client_connection)client_connection.close()os._exit(0)  # child exits hereelse:  # 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 socketSERVER_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 timeSERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5def 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 OKHello, World!
"""client_connection.sendall(http_response)# sleep to allow the parent to loop over to 'accept' and block theretime.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: # childlisten_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 socketSERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024def 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 OKHello, 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 interruptedif code == errno.EINTR:continueelse:raisepid = os.fork()if pid == 0:  # childlisten_socket.close()  # close child copy
            handle_request(client_connection)client_connection.close()os._exit(0)else:  # parentclient_connection.close()  # close parent copy and loop overif __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 socketSERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024def grim_reaper(signum, frame):while True:try:pid, status = os.waitpid(-1,          # Wait for any child processos.WNOHANG  # Do not block and return EWOULDBLOCK error
            )except OSError:returnif pid == 0:  # no more zombiesreturndef handle_request(client_connection):request = client_connection.recv(1024)print(request.decode())http_response = b"""\
HTTP/1.1 200 OKHello, 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 interruptedif code == errno.EINTR:continueelse:raisepid = os.fork()if pid == 0:  # childlisten_socket.close()  # close child copy
            handle_request(client_connection)client_connection.close()os._exit(0)else:  # parentclient_connection.close()  # close parent copy and loop overif __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

转载于:https://www.cnblogs.com/5poi/p/7531970.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/369228.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

华大单片机m4内核的滴答定时器_微处理器、单片机及其外设,处理还是控制?...

每项新应用设计都需要一个单片机或微处理器。当在两者之间选择其一时&#xff0c;需要考虑一些因素。以下是微处理器、单片机以及异构架构的概述。考虑选择微处理器(MPU)或者单片机(MCU)时&#xff0c;应用类型通常是关键因素。另一方面&#xff0c;最终选择取决于诸如操作系统…

安装提示卸载office_office2010 卸载工具

点击上方“蓝字”&#xff0c;关注我们获取更多免费资源我们为什么要用这个office2010卸载工具呢&#xff0c;很简单旧版本的office卸载不干净&#xff0c;在安装新版本的office时可能会遇到一些奇奇怪怪的问题。如果遇到无法安装office时&#xff0c;我们可以先使用office卸载…

人工通道会取消吗_二七政策将用于ETC?高速或将取消人工收费通道

随着社会的发展&#xff0c;有车一族越来越多&#xff0c;但是在这种情况下&#xff0c;堵车的情况就随处可见了&#xff0c;并且随着车辆的增多&#xff0c;高速收费通道的成本也增加了不少&#xff0c;而且通过时间越来越长&#xff0c;面对这种情况&#xff0c;交通局就和银…

在Oracle Cloud上的Prime-UI,JAX-RS和Jersey和Gson

如今&#xff0c;Oracle云无处不在。 最初&#xff0c;拉里&#xff08;Larry&#xff09;否认在很长一段时间内都需要云&#xff0c;并且在去年的开放世界&#xff08;Open World&#xff09;之后就宣布了一些非常早期的公告&#xff0c;而且可用性很差&#xff0c;似乎没有人…

推荐20个很有帮助的 Web 前端开发教程

在平常的搜索中&#xff0c;我碰到过很多有趣的信息&#xff0c;应用程序和文档&#xff0c;我把它们整理在下面这个列表。这是收藏的遇到的有用内容的一个伟大的方式&#xff0c;可以在你需要的时候方便查阅。相信你会在这个列表中发现对你很有用的资料。 1. CSS Vocabulary 一…

Scrapy爬虫框架解析

Scrapy框架解析Scrapy框架大致包括以下几个组件&#xff1a;Scrapy Engine、Spiders、Scheduler、Item Pipeline、Downloader&#xff1b;组件Scrapy Engine这是框架的核心&#xff0c;负责控制数据流在整个系统的各个组件间的流动过程&#xff0c;并且在特定动作发生时触发相应…

Couchbase 2.0归类视图简介

大多数应用程序必须处理“主/详细”类型的数据&#xff1a; 啤酒厂和啤酒 部门和员工 发票和项目 … 例如&#xff0c;这对于创建如下应用程序视图是必需的&#xff1a; 借助Couchbase和许多面向文档的数据库&#xff0c;您可以采用不同的方式来处理此问题&#xff0c;您…

(转)利用WPF的ListView进行大数据量异步加载

原文&#xff1a;http://www.cnblogs.com/scy251147/archive/2012/01/08/2305319.html 由于之前利用Winform的ListView进行大数据量加载的时候&#xff0c;诟病良多&#xff0c;所以今天试着用WPF的ListView来做了一下&#xff0c;结果没有让我失望&#xff0c;我将一个拥有430…

有关循环和判断的几个小问题

注意在while嵌套的if语句之外&#xff0c;还有一个cin>>n;因为刚刚没有这一部分的时候&#xff0c;执行的结果是这样的&#xff1a; 把while里面的cin>>n;注释掉了之后&#xff0c;在cmd里执行的时候&#xff0c;只能输入数字&#xff0c;但是没有任何的反应。 但其…

PHP7 学习笔记(五)安装event扩展(libevent)

一、描述&#xff1a;有效安排I/O&#xff0c;时间和信号的扩展 使用可用于特定平台的最佳I/O通知机制的事件,是PHP基础设施的libevent端口。 二、下载地址&#xff1a;http://pecl.php.net/package/event 三、安装支持库libevent&#xff0c;需要编译高版本&#xff08;这里以…

有关输出图形的代码,我觉得好难啊,好蒙啊。

这里的代码其实没看懂过&#xff0c;自己看到书上这一题的时候也是挺蒙的&#xff0c;压根不知道要怎么下手&#xff0c;照着书上把代码打进去之后也不清楚原理&#xff0c;可怕的是&#xff0c;反反复复对着答案敲了几遍代码&#xff0c;执行结果还是这样的&#xff0c;和课本…

Windows环境变量的应用

设置环境变量快速打开程序 如果你对桌面上密密麻麻的文件感到烦恼&#xff0c;那么下面的方法可以帮到你。 Step 1 在硬盘的某个位置添加一个文件夹&#xff0c;把你经常用的程序的快捷方式放进这个文件夹&#xff0c;快捷方式重命名最好简单易记&#xff0c;如图 注意 快捷方式…

mysql 备份数据库_mysql数据库备份

前一段时间因为误操作删除了一张表的几条数据&#xff0c;弄得很尴尬&#xff0c;正好这周有空就折腾了下数据备份的知识&#xff0c;现把mysql的数据备份相关实践和心得总结如下&#xff1a;一.使用mysqldump命令备份数据库&#xff1a;备份整个数据库(包括表结构和数据),用法…

JavaScript:Browser 对象

ylbtech-JavaScript&#xff1a;Browser 对象1. Window 对象返回顶部 1、Window 对象 Window 对象 Window 对象表示浏览器中打开的窗口。 如果文档包含框架&#xff08;<frame> 或 <iframe> 标签&#xff09;&#xff0c;浏览器会为 HTML 文档创建一个 window 对象…

深入理解line-height与vertical-align——前端布局常用属性

line-height、font-size、vertical-align是设置行内元素布局的关键属性。这三个属性是相互依赖的关系&#xff0c;改变行间距离、设置垂直对齐等都需要它们的通力合作。下面将主要介绍line-height与vertical-align&#xff1a; 行高 【定义】 line-height行高是指文本行基线之…

UWP开发入门(四)——自定义CommandBar

UWP开发入门&#xff08;四&#xff09;——自定义CommandBar 原文:UWP开发入门&#xff08;四&#xff09;——自定义CommandBar各位好&#xff0c;再次回到UWP开发入门系列&#xff0c;刚回归可能有些不适应&#xff0c;所以今天我们讲个简单的&#xff0c;自定义CommandBar&…

(转)Cobbler无人值守批量安装Linux系统

本文目录&#xff1a; 1.1 pxe安装系统 1.2 cobbler基本介绍 1.3 安装和配置cobbler 1.3.1 安装cobbler 1.3.2 配置dhcp和tftp 1.4 cobbler从本地光盘安装系统 1.4.1 生成distro 1.4.2 提供kickstart文件 1.4.3 提供profile 1.4.4 开始安装 1.5 比pxekickstart好的地方 1.6 让新…

Spring集成–从头开始应用程序,第1部分

开始之前 在本教程中&#xff0c;您将学习什么是Spring Integration &#xff0c;如何使用它以及有助于解决哪些问题。 我们将从头开始构建一个示例应用程序&#xff0c;并演示Spring Integration的一些核心组件。 如果您不熟悉Spring&#xff0c;请查看我编写的另一本有关Spri…

初学者Web介绍一些前端开发中的基本概念用到的技术

Web开发是比较费神的&#xff0c;需要掌握很多很多的东西&#xff0c;特别是从事前端开发的朋友&#xff0c;需要通十行才行。今天&#xff0c;本文向初学者介绍一些Web开发中的基本概念和用到的技术&#xff0c;从A到Z总共26项&#xff0c;每项对应一个概念或者技术。 初学者W…

Xcode 快捷键及代码格式化

按住apple键点击类名就可以定位到这个类中查看相关定义&#xff08;在日后的开发中我们会经常这么来做&#xff0c;毕竟要记住iOS开发中所有的API是不现实的&#xff0c;有些API我们可以通过这种方法来查找&#xff09; PS&#xff1a;下面都是网上百度后经过我自己整理&#x…