Let’s start with WSGI on the surface, which stands for Web Server Gateway Interface.

For those of you who didn’t know what WSGI meant before, I’m still not sure after reading the above explanation, so let’s give you a general idea. Finally, we will implement one ourselves to deepen our understanding of WSGI.

We are now writing Web applications in Python, either using the popular Flask, Django framework, or just writing one of our own. There are also a lot of server software to choose from, such as Apache, Nginx, IIS, etc., in addition, there are also a lot of niche software. But now the question is, how do I deploy it? Before the WSGI specification, one server would schedule Python applications this way and another that way, so that applications could be written to be deployed to a limited number of servers or servers, rather than being universal.

Note: The code below is based on Python 3.6.

Let’s say there’s a server

wsgi/server.py

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    response = b"""HTTP/1.1 200 OK Hello, World! """
    client_connection.sendall(response)
    client_connection.close()
Copy the code

Implementation is relatively simple, is to listen to port 8080, if there is a request to print at the terminal, and return Hello, World! The response.

Start the server in the terminal

➜ wsgi Python server.py Serving HTTP on 0.0.0.0 port 8080...Copy the code

Open another terminal and request

➜  ~ curl 127.0.0.1:8080
HTTP/1.1 200 OK

Hello, World!
Copy the code

Note The server is running properly.

There is also a Web application

wsgi/app.py

# coding=utf-8


def simple_app():
    return b'Hello, World! \r\n'
Copy the code

Now to deploy (and make the whole thing work), the simple and crude way is to call the corresponding method in the app directly from the server. Like this

wsgi/server2.py

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    from app import simple_app
    response = 'the HTTP / 1.1 200 OK \ r \ n \ r \ n'
    response = response.encode('utf-8')
    response += simple_app()

    client_connection.sendall(response)
    client_connection.close()
Copy the code

Run the script

Note: Because the same port is used, please close the last script first and then execute it, otherwise an error will be reported due to port conflict.

➜ wsgi Python server2.py Serving HTTP on 0.0.0.0 port 8080...Copy the code

Then ask to see what happens

➜  ~ curl 127.0.0.1:8080
Hello, World!
Copy the code

Yeah, that’s fine. However, the above server and application as a whole is running, so I change a server or application. Due to how the interaction between the server and application has no specification, such as server should how to apply the request message to, and how to tell the server to start when the application finishes returns a response, if is each figure, need to customize the application server, the application will custom server, this is an application can run up and also a little too much trouble.

The emergence of WSGI is, therefore, in order to solve the above problem, it states how to tell the requested information to the server to the application, the application of how to put the execution back to the server, in this case, the server and application by a standard management, by implementing the standard, the server and application can be optional collocation, flexibility is greatly increased.

The following diagram illustrates what WSGI specifications are.

First, the application must be a callable object, either a function or an object that implements a __call__() method.

On each request, the server calls the application through application_callable(environ, start_Response).

When the application is ready to return data after processing, it first calls the function start_response(status, headers, exec_info) that the service sends to it, and then returns the iterable as data. (For those of you who don’t understand iterables, check out my previous article “Figuring out Python iterators, iterables, and generators.”)

Environ must be a dictionary that contains information about the request, such as the request method and the request path, and start_response is a function that needs to be called after the application has finished processing, which is used to tell the service to set the response header or error handling, etc.

Status must be 999 Message here, such as 200 OK, 404 Not Found, etc. Headers is a list of primitives (header_name, header_value), The last parameter exec_info is optional and is usually used when the application fails.

Now that you know the general concept of WSGI, let’s implement one.

The first is the application

wsgi/wsgi_app.py

# coding=utf-8


def simple_app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type'.'text/plain')]
    start_response(status, response_headers)
    return [f'Request {environ["REQUEST_METHOD"]}'
            f' {environ["PATH_INFO"]} has been'
            f' processed\r\n'.encode('utf-8')]
Copy the code

Here we define a function (callable object) that uses environ, the content of the request passed to it by the server, using REQUEST_METHOD and PATH_INFO information. Start_response is called before returning so that the server can set some header information.

And then the server

wsgi/wsgi_server.py

# coding=utf-8

import socket

listener = socket.socket()
listener.setsockopt(socket.SOL_SOCKET,
                    socket.SO_REUSEADDR, 1)
listener.bind(('0.0.0.0', 8080))
listener.listen(1)
print('Serving HTTP on 0.0.0.0 port 8080 ...')

while True:
    client_connection, client_address = \
        listener.accept()
    print(f'Server received connection'
          f' from {client_address}')
    request = client_connection.recv(1024)
    print(f'request we received: {request}')

    headers_set = None

    def start_response(status, headers):
        global headers_set
        headers_set = [status, headers]

    method, path, _ = request.split(b' ', 2)
    environ = {'REQUEST_METHOD': method.decode('utf-8'),
               'PATH_INFO': path.decode('utf-8')}
    from wsgi_app import simple_app
    app_result = simple_app(environ, start_response)

    response_status, response_headers = headers_set
    response = f'the HTTP / 1.1 {response_status} \ r \ n'
    for header in response_headers:
        response += f'{header[0]}: {header[1]}\r\n'
    response += '\r\n'
    response = response.encode('utf-8')
    for data in app_result:
        response += data

    client_connection.sendall(response)
    client_connection.close()
Copy the code

The server listening code has not changed much, except that it handles requests differently.

The start_response(status, headers) function is defined first and will not be called by itself.

It then calls the application, passing the current request information environ and the start_Response function above, and lets it decide what request information to use and call start_Response to set the header information before processing is ready to return data.

Now, after starting the server (that is, executing the server code, similar to what we did before, and not going into detail here), ask to see the results

➜  ~ curl 127.0.0.1:8080/user/1
Request GET /user/1 has been processed
Copy the code

Well, the procedure is normal.

For the sake of illustration above, the code is highly coupled, and the server code needs to be changed if the server needs to change applications, which is obviously problematic. Now that the principle is almost clear, let’s optimize the code

wsgi/wsgi_server_oop.py

# coding=utf-8

import socket
import sys


class WSGIServer:
    def __init__(self):
        self.listener = socket.socket()
        self.listener.setsockopt(socket.SOL_SOCKET,
                                 socket.SO_REUSEADDR, 1)
        self.listener.bind(('0.0.0.0', 8080))
        self.listener.listen(1)
        print('Serving HTTP on 0.0.0.0'
              ' port 8080 ... ')
        self.app = None
        self.headers_set = None

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

    def start_response(self, status, headers):
        self.headers_set = [status, headers]

    def serve_forever(self):
        while True:
            listener = self.listener
            client_connection, client_address = \
                listener.accept()
            print(f'Server received connection'
                  f' from {client_address}')
            request = client_connection.recv(1024)
            print(f'request we received: {request}')

            method, path, _ = request.split(b' ', 2)
            # For brevity, the padding here is somewhat arbitrary
            # Make your own improvements if you need them
            environ = {
                'wsgi.version': (1, 0),
                'wsgi.url_scheme': 'http'.'wsgi.input': request,
                'wsgi.errors': sys.stderr,
                'wsgi.multithread': False,
                'wsgi.multiprocess': False,
                'wsgi.run_once': False,
                'REQUEST_METHOD': method.decode('utf-8'),
                'PATH_INFO': path.decode('utf-8'),
                'SERVER_NAME': '127.0.0.1'.'SERVER_PORT': '8080',
            }
            app_result = self.app(environ, self.start_response)

            response_status, response_headers = self.headers_set
            response = f'the HTTP / 1.1 {response_status} \ r \ n'
            for header in response_headers:
                response += f'{header[0]}: {header[1]}\r\n'
            response += '\r\n'
            response = response.encode('utf-8')
            for data in app_result:
                response += data

            client_connection.sendall(response)
            client_connection.close()


if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.exit('Argv Error')
    app_path = sys.argv[1]
    module, app = app_path.split(':')
    module = __import__(module)
    app = getattr(module, app)

    server = WSGIServer()
    server.set_app(app)
    server.serve_forever()
Copy the code

The basic principles remain the same, except that the original code has been modified in an object-oriented manner and environ has added some necessary environment information.

You can use previous applications

➜  wsgi python wsgi_server_oop.py wsgi_app:simple_app
Serving HTTP on 0.0.0.0 port 8080 ...
Copy the code

request

➜  ~ curl 127.0.0.1:8080/user/1
Request GET /user/1 has been processed
Copy the code

I get the same answer as before.

Will Flask applications work? Let’s try it. Let’s create a new one

wsgi/flask_app.py

# coding=utf-8

from flask import Flask
from flask import Response

flask_app = Flask(__name__)


@flask_app.route('/user/<int:user_id>',
                 methods=['GET'])
def hello_world(user_id):
    return Response(
        f'Get /user/{user_id} has been'
        f' processed in flask app\r\n',
        mimetype='text/plain'
    )
Copy the code

Restart the server

➜  wsgi python wsgi_server_oop.py flask_app:flask_app
Serving HTTP on 0.0.0.0 port 8080 ...
Copy the code

request

➜  ~ curl 127.0.0.1:8080/user/1
Get /user/1 has been processed in flask app
Copy the code

Since Flask is WSGI compliant, there is no problem with execution.

At this point, a rough WSGI specification is in place, and while the code is not elegant, some of the core stuff is there. However, there are a lot of things left out, such as error handling, that are not enough to be used in a production environment, and those who want to know more about it should see PEP 3333.

Popular Web application frameworks such as Django, Bottle, etc., as well as Apahce, Nginx, Gunicorn, etc., all support this specification. Therefore, there is little problem with the framework and application being randomly matched.

reference

  • www.python.org/dev/peps/pe…
  • Ruslanspivak.com/lsbaws-part…
  • www.toptal.com/python/pyth…
  • Gist.github.com/thomasballi…

Follow the public account “Small Back end” to read more great articles.