【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

相關文章
相關標籤/搜索