美文网首页
Build a web server with python

Build a web server with python

作者: Mr_Puff | 来源:发表于2017-06-20 17:53 被阅读0次

Basic knowledge about HTTP

Pretty much every program on the web runs on a family of communication standards called Internet Protocol (IP), but which concerns us most of them is is the Transmission Control Protocol (TCP/IP), it makes communication between computers as simple as reading & writing text files.

We use IP to locate a computer on the internet and PORT to determine which programme we want to visit. So if someone has built a web server listening on port 80 on computer A of which IP is 10.22.122.345, then we can access it anywhere using 10.22.122.345:80. Unfortunately, we may not always remember such complex IP address, so Domain Name System (DNS) will automatically match nickname of that programme (such as 'www.example.org') with IP address number.

The Hypertext Transfer Protocol (HTTP) allows us to exchange data on the internet, Request/Response mechanism based on socket connection is the basic working mode:

http-cycle.png

Usage of socket

Socket allows processes on different computers to communicate with each other, most services on the web are based on socket. We will focus on server side socket, which mainly finish following tasks:

  • Opeing socket
  • Binding to host and port
  • Listening for coming connection
  • Creating connection and exchange data
  • Closing connection

We will create a simple web server which always return the same content to client connection:

import socket

HOST, PORT = '', 7002

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))

server_socket.listen(1)
print 'Serving HTTP on port %s ...' % PORT
while True:
    client_connection, client_address = server_socket.accept()
    request = client_connection.recv(1024)
    print request

    http_response = """\
    HTTP/1.1 200 OK\r\n
    Some content from server!
    """
    client_connection.sendall(http_response)
    client_connection.close()

Firstly, we created a server socket using socket.socket(socket.AF_INET, socket.SOCK_STREAM), the AF_INET means that we are using IPV4 address family and the SOCK_STREAM means that the socket is serving for TCP/IP connection.
Then, we add some socket option to make it reuse address and the backlog is 1, bind this server socket to given host and port. The listen method accept one parameter, it means the max connection OS can hold on before new connection is refused.
Return value of accept method is a pair (conn, address) where conn is a new socket object usable to send and receive data on the connection, and address is the address bound to the socket on the other end of the connection. client_connection can be used to exchange data between server and client, we often call recv to recieve data from client and sendall to send data to client.

Basic knowledge about WSGI

The Web Server Gateway Interface (WSGI) is a standard interface between web server software and web applications written in Python. Having a standard interface makes it easy to use an application that supports WSGI with a number of different web servers.
WSGI mainly working flow:

  • A callable object application must be given by web framework (such as Flask/Django..), its implementation has no limit.
  • Every time when web server received http request from client, the callable object application will be invoked. Then web server will transfer a dict that contains all CGI environment variables and another callable object start_response
  • Web framework will build HTTP response headers (including response status), then send them to start_response and generate response body
  • Finally, web server will construct a response contains all information and send it to client.
wsgi.png

So, let's begin with a simple wsgi demo:

from wsgiref.simple_server import make_server


class SimpleApp(object):

    def __call__(self, env, start_response):
        status = '200 OK'
        response_headers = [('Content-type', 'text/plain')]
        # set header and status
        start_response(status, response_headers)

        return [u"This is a wsgi application".encode('utf8')]


def app(env, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    # set header and status
    start_response(status, response_headers)

    return [u"This is a wsgi application".encode('utf8')]

httpd = make_server('', 7000, app) 
# httpd = make_server('', 7000, SimpleApp()) 
print 'Serving http on port:7000'
httpd.serve_forever()

We used make_server to create a WSGI server, the 3rd parameter should be a callable object which can be a function or class with __call__ method.
Then we will lookup source code for some detail information. First, let's take a look at the make_server method in simple_server.py:

def make_server(
    host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler
):
    """Create a new WSGI server listening on `host` and `port` for `app`"""
    server = server_class((host, port), handler_class)
    server.set_app(app)
    return server

It returns a server instance which is presented by WSGIServer:

class WSGIServer(HTTPServer):

    """BaseHTTPServer that implements the Python WSGI protocol"""

    application = None

    def server_bind(self):
        """Override server_bind to store the server name."""
        HTTPServer.server_bind(self)
        self.setup_environ()

    def setup_environ(self):
        # Set up base environment
        env = self.base_environ = {}
        env['SERVER_NAME'] = self.server_name
        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
        env['SERVER_PORT'] = str(self.server_port)
        env['REMOTE_HOST']=''
        env['CONTENT_LENGTH']=''
        env['SCRIPT_NAME'] = ''

    def get_app(self):
        return self.application

    def set_app(self,application):
        self.application = application

So after calling make_server, the callable object application has been bind to WSGI server class.
Next question is what the framework do with incoming request, so we will search for method serve_forever. Note that, it was defined in SocketServer.BaseServer.

def serve_forever(self, poll_interval=0.5):
    """Handle one request at a time until shutdown.

    Polls for shutdown every poll_interval seconds. Ignores
    self.timeout. If you need to do periodic tasks, do them in
    another thread.
    """
    self.__is_shut_down.clear()
    try:
        while not self.__shutdown_request:
            # XXX: Consider using another file descriptor or
            # connecting to the socket to wake this up instead of
            # polling. Polling reduces our responsiveness to a
            # shutdown request and wastes cpu at all other times.
            r, w, e = _eintr_retry(select.select, [self], [], [],
                                   poll_interval)
            if self in r:
                self._handle_request_noblock()
    finally:
        self.__shutdown_request = False
        self.__is_shut_down.set()

Class hierarchy for WSGIServer:
SocketServer.BaseServer
|-SocketServer.TCPServer
|--BaseHTTPServer.HTTPServer
|---simple_server.WSGIServer
then we will find it calls process_request and then calls finish_request:

def process_request(self, request, client_address):
    """Call finish_request.

    Overridden by ForkingMixIn and ThreadingMixIn.

    """
    self.finish_request(request, client_address)
    self.shutdown_request(request)

In method finish_request, it constructed an instance of class BaseRequestHandler:

def __init__(self, request, client_address, server):
    self.request = request
    self.client_address = client_address
    self.server = server
    self.setup()
    try:
        self.handle()
    finally:
        self.finish()

Now, we will go through method handle and it was overrideen by WSGIRequestHandler:

class WSGIRequestHandler(BaseHTTPRequestHandler):
    # ...

    def handle(self):
        """Handle a single HTTP request"""

        self.raw_requestline = self.rfile.readline(65537)
        if len(self.raw_requestline) > 65536:
            self.requestline = ''
            self.request_version = ''
            self.command = ''
            self.send_error(414)
            return

        if not self.parse_request(): # An error code has been sent, just exit
            return

        handler = ServerHandler(
            self.rfile, self.wfile, self.get_stderr(), self.get_environ()
        )
        handler.request_handler = self      # backpointer for logging
        handler.run(self.server.get_app())

Now, we will go through method run defined in class BaseHandler:

def run(self, application):
    """Invoke the application"""
    # Note to self: don't move the close()!  Asynchronous servers shouldn't
    # call close() from finish_response(), so if you close() anywhere but
    # the double-error branch here, you'll break asynchronous servers by
    # prematurely closing.  Async servers must return from 'run()' without
    # closing if there might still be output to iterate over.
    try:
        self.setup_environ()
        self.result = application(self.environ, self.start_response)
        self.finish_response()
    except:
        try:
            self.handle_error()
        except:
            # If we get an error handling an error, just give up already!
            self.close()
            raise   # ...and let the actual server figure it out.

This method will invoke the application we transferred in, start_response method is called for build response status and response headers, finish_response will help to build a readable response for client.

Create WSGI Server

We've got the basic working style and code structure of python WSGI in previous chapter, so let's build our own WSGI server without using embed WSGI modules.
Key points was listed as following:

  • Bind to certain callable application
  • Parse request line
  • Invoke application
  • Build response header and body
  • Send response to client

Here comes the code:


import socket
import StringIO
import sys


class WSGIServer(object):
    address_family = socket.AF_INET
    socket_type = socket.SOCK_STREAM
    request_queue_size = 1

    def __init__(self, server_address):
        self.server_socket = server_socket = socket.socket(
            self.address_family,
            self.socket_type
        )
        # reuse the same address
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server_socket.bind(server_address)
        server_socket.listen(self.request_queue_size)
        # Get server host name and port
        host, port = self.server_socket.getsockname()[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port
        # Return headers set by Web framework/Web application
        self.headers_set = []

    def set_app(self, application):
        self.application = application

    def serve_forever(self):
        server_socket = self.server_socket
        while True:
            self.client_connection, client_address = server_socket.accept()
            # only handle one request.
            self.handle_one_request()

    def handle_one_request(self):
        self.request_data = request_data = self.client_connection.recv(1024)
        self.parse_request(request_data)

        # Construct environment dictionary using request data
        env = self.get_env()

        # It's time to call our application callable and get
        # back a result that will become HTTP response body
        result = self.application(env, self.start_response)

        # Construct a response and send it back to the client
        self.finish_response(result)

    def parse_request(self, text):
        try:
            request_line = text.splitlines()[0]
            request_line = request_line.rstrip('\r\n')
            # path the request line
            (self.request_method,  # GET
             self.path,  # /path
             self.request_version  # HTTP/1.1
            ) = request_line.split()
        except StandardError as e:
            pass

    def get_env(self):
        env = {}

        # WCGI variables
        env['wsgi.version'] = (1, 0)
        env['wsgi.url_scheme'] = 'http'
        env['wsgi.input'] = StringIO.StringIO(self.request_data)
        env['wsgi.errors'] = sys.stderr
        env['wsgi.multithread'] = False
        env['wsgi.multiprocess'] = False
        env['wsgi.run_once'] = False
        # basic CGI variables
        env['REQUEST_METHOD'] = self.request_method  # GET
        env['PATH_INFO'] = self.path  # /hello
        env['SERVER_NAME'] = self.server_name  # localhost
        env['SERVER_PORT'] = str(self.server_port)  # 8888
        return env

    def start_response(self, status, response_headers, exc_info=None):
        # Add necessary server headers
        server_headers = [
            ('Description', 'Build with python2'),
            ('Server', 'WSGIServer'),
        ]
        self.headers_set = [status, response_headers + server_headers]

    def finish_response(self, result):
        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 data in result:
                response += data
            self.client_connection.sendall(response)
        finally:
            self.client_connection.close()


SERVER_ADDRESS = (HOST, PORT) = '', 7002


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] #'flaskapp:app'
    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.serve_forever()

This file takes a command line argument, it looks like module:callable. So we will run a flask application on this server.
Let's create flask.py:

from flask import Flask
from flask import Response

flask_app = Flask('flaskapp')


@flask_app.route('/')
def index():
    return Response(
        'Welcome to the world of Flask!\n',
        mimetype='text/plain'
    )
app = flask_app.wsgi_app

Now, run the command python server.py flask:app, open a browser and take a look at the running flask application.

result.png

相关文章

网友评论

      本文标题:Build a web server with python

      本文链接:https://www.haomeiwen.com/subject/mpngqxtx.html