深入浅出Web开发中聊了业务系统需要通过应用网关协议与Web服务器交互。

Browser <—HTTP协议—> Web Server <—应用网关协议—> 业务系统(PHP,Python,Java...)

WSGI

WSGI(Web Server Gateway Interface)是Python Web应用程序与Web服务器之间的通信协议,它定义了Web服务器如何与Web框架进行交互,从而实现了Web服务器和Web框架之间的解耦。

WSGI的作用就像是一个桥梁,它把Web服务器和Web框架连接起来,让它们能够进行有效的通信。Web服务器只需要遵循WSGI协议,将请求和响应传递给WSGI应用程序,而不需要了解具体的应用程序实现细节。同样地,Web框架也只需编写符合WSGI规范的应用程序,而不需要考虑与特定的Web服务器进行交互的问题。这种解耦的方式使得Web开发更加灵活和可扩展。

WSGI协议的实现非常简单。它只要求应用程序提供一个callable对象,接受两个参数:一个环境变量字典和一个可调用的start_response函数。Web服务器调用该callable,并传递环境变量字典和start_response函数作为参数。应用程序可以使用环境变量字典获取请求信息,然后通过调用start_response函数返回响应头信息。应用程序还需要返回一个可迭代的响应body对象。

# Web Server 进程
...
# 加载wsgi应用
...
# 处理Request调用
def wsgi_callable(environ, start_response):
    ...
    return body
...
# 返回Response给客户端
...

因为WSGI协议的简单实现,任何Python Web框架都可以轻松地与任何符合WSGI协议的Web服务器进行交互,从而实现灵活和可扩展的Web应用程序开发。

目前社区的Python Web框架,比如Flask、Django、Madara最终都是暴露一个WSGI的Callable对象作为应用的入口。

SimpleWSGIServer

详解HTTP协议中实现的SimpleHTTPServer改造成一个支持WSGI协议的Web服务器。

import socket  # 导入 socket 模块
from multiprocessing.dummy import Pool as ThreadPool  # 导入线程池模块ThreadPool,用于多线程处理请求
import io  # 导入io模块,用于发送和接收HTTP报文
import traceback  # 导入traceback模块,用于打印错误信息
import logging  # 导入logging模块,用于记录日志



class Server(object):
    # 定义一个类变量 SERVER_STRING,存储服务器的名称和版本信息
    SERVER_STRING = b"Server: SimpleHttpd/1.0.0\r\n"


    def __init__(self, host, port, worker_count=4):
        self._host = host  # 存储主机名
        self._port = port  # 存储端口号
        self._listen_fd = None  # 存储监听套接字
        self._worker_count = worker_count  # 存储工作线程数
        self._worker_pool = ThreadPool(worker_count)  # 创建指定数量的工作线程池
        self._logger = logging.getLogger("simple.httpd")  # 创建日志记录器
        self._logger.setLevel(logging.DEBUG)  # 设置日志级别为DEBUG
        self._logger.addHandler(logging.StreamHandler())  # 添加输出日志到控制台的处理器


    def run(self):
        self._listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 创建TCP/IP套接字
        self._listen_fd.bind((self._host, self._port))  # 绑定IP地址和端口号
        self._listen_fd.listen(self._worker_count)  # 监听客户端连接请求
        try:
            while True:
                conn, addr = self._listen_fd.accept()  # 接受客户端连接并返回新的套接字和地址
                self._worker_pool.apply_async(self.accept_request, (conn, addr,))  # 将连接套接字和地址交给工作线程异步执行处理
        except Exception as e:
            traceback.print_exc()  # 打印异常堆栈信息
        finally:
            self._listen_fd.close()  # 关闭监听套接字


    def accept_request(self, conn: socket.socket, addr):
        try:
            method, path, http_version, req_headers, req_body = self.recv_request(conn)
            status = ""
            # 构建environ字典
            environ = {
                'REQUEST_METHOD': method,  # 请求方法,例如 'POST'
                'SCRIPT_NAME': '',  # 脚本名称
                'PATH_INFO': path,  # 路径信息,例如'/hello/world'
                'QUERY_STRING': '',  # 查询字符串,例如'a=1&b=2'
                'CONTENT_TYPE': '',  # 请求体的类型,例如'application/json'
                'CONTENT_LENGTH': len(req_body),  # 请求体的长度(单位为字节)
                'SERVER_NAME': self._host,  # 服务器主机名
                'SERVER_PORT': str(self._port),  # 服务器端口号
                'HTTP_HOST': req_headers.get('Host', ''),
                'HTTP_USER_AGENT': req_headers.get('User-Agent', ''),  # HTTP请求头中的'User-Agent'字段
                'HTTP_ACCEPT': req_headers.get('Accept', ''),  # HTTP请求头中的'Accept'字段
                'HTTP_ACCEPT_LANGUAGE': req_headers.get('Accept-Language', ''),  # HTTP请求头中的'Accept-Language'字段
                'HTTP_ACCEPT_ENCODING': req_headers.get('Accept-Encoding', ''),  # HTTP请求头中的'Accept-Encoding'字段
                'HTTP_CONNECTION': req_headers.get('Connection', '')  # HTTP请求头中的'Connection'字段
            }
            environ['wsgi.input'] = io.BytesIO(req_body)
            # 执行wsgi应用
            body = self.wsgi_application(environ, self.build_start_response(conn, status))
            # 迭代响应body对象,返回给客户端
            for bt in body:
                self.send_response(conn, None, bt, None)
            self._logger.info("{}:{} {} {} {} {}".format(addr[0], addr[1], http_version, method, path, status))  # 记录日志信息
        except Exception as e:
            traceback.print_exc()  # 打印异常堆栈信息
        finally:
            conn.close()  # 关闭连接套接字


    def wsgi_application(self, environ, start_response):
        """
        wsgi应用
        """
        data = b"Hello, World!\n"
        start_response("200 OK", [
            ("Content-Type", "text/plain"),
            ("Content-Length", str(len(data)))
        ])
        return [data]


    def build_start_response(self, conn, the_status):
        """
        构建start_response函数
        """
        def start_response(status, headers):
            the_status = status
            self.send_response(conn, status=status, headers=headers)
        return start_response


    def send_response(self, conn: socket.socket, status=None, body=None, headers=None):
        if not status is None:
            conn.sendall("HTTP/1.0 {}\r\n".format(status).encode())
            conn.sendall(self.SERVER_STRING)
        if not headers is None:
            for header in headers:
                conn.sendall("{}: {}\r\n".format(*header).encode())
        if not body is None:
            if not isinstance(body, bytes):
                body = str(body).encode()
            conn.sendall(b"\r\n")
            conn.sendall(body)


    def recv_request(self, conn: socket.socket):
        # 读取请求行
        line = b''
        while not line.endswith(b'\r\n'):
            data = conn.recv(1)
            if not data:
                raise ConnectionError('Connection closed unexpectedly')
            line += data
        method, path, version = line.strip().decode().split(' ', 2)


        # 读取请求头
        headers = {}
        while True:
            line = b''
            while not line.endswith(b'\r\n'):
                data = conn.recv(1)
                if not data:
                    raise ConnectionError('Connection closed unexpectedly')
                line += data
            if line == b'\r\n':
                break
            key, value = line.strip().decode().split(': ', 1)
            headers[key] = value


        # 读取请求体
        content_length = int(headers.get('Content-Length', '0'))
        if content_length > 0:
            body = conn.recv(content_length)
        else:
            body = b""


        # 返回请求行、请求头和请求体
        return method, path, version, headers, body



if __name__ == "__main__":
    server = Server("0.0.0.0", 3000)  # 创建服务器实例
    server.run()  # 启动服务器