【Python Programe】WSGI (Web Server Gateway Interface)

Part1: python

  What is a Web server?git

HTTP Request/Response

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

  客户端与服务器使用HTTP协议进行通讯,客户端能够是浏览器或者其余使用HTTP协议的软件。web

 

一个简单的WEB服务器实现shell

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 而且 命令行运行django

$ python webserver1.py
Serving HTTP on port 8899 …

浏览器输入 http://localhost:8899/hello编程

刚才输入的WEB地址,它叫URL,这是它的基本结构:flask

  

  它表示了浏览器要查找和链接的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)

工做流程:

  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 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并开始接受客户端链接的标准流程经历一般以下:

一、服务器建立一个TCP/IP Socket

listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

二、设置Socket options

listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

三、服务器绑定地址

listen_socket.bind(SERVER_ADDRESS)

四、监听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

相关文章
相关标签/搜索