Gunicorn源码解析

深入浅出Python开发Web中已经实现了一个简单的WSGI服务器,但是在生产应用需要考虑健壮、性能、并发等等问题。

Gunicorn是一个Python的WSGI HTTP服务器实现,它支持多个进程模型,可以自动管理工作进程。在Python web开发中被广泛应用,是最受欢迎的服务器之一,许多著名的Python web框架都支持使用Gunicorn作为服务器。

Start

$ pip install gunicorn
$ cat myapp.py
  def app(environ, start_response):
      data = b"Hello, World!\n"
      start_response("200 OK", [
          ("Content-Type", "text/plain"),
          ("Content-Length", str(len(data)))
      ])
      return iter([data])
$ gunicorn -w 4 myapp:app
[2014-09-10 10:22:28 +0000] [30869] [INFO] Listening at: http://127.0.0.1:8000 (30869)
[2014-09-10 10:22:28 +0000] [30869] [INFO] Using worker: sync
[2014-09-10 10:22:28 +0000] [30874] [INFO] Booting worker with pid: 30874
[2014-09-10 10:22:28 +0000] [30875] [INFO] Booting worker with pid: 30875

gunicorn通过pip安装提供了一个cli入口,可以查看setup.py看到入口函数gunicorn.app.wsgiapp:run

# https://github.com/benoitc/gunicorn/blob/20.1.0/setup.py#L114
entry_points="""
[console_scripts]
gunicorn=gunicorn.app.wsgiapp:run


[paste.server_runner]
main=gunicorn.app.pasterapp:serve
"""

# https://github.com/benoitc/gunicorn/blob/20.1.0/gunicorn/app/wsgiapp.py#L61
def run():
    """\
    The ``gunicorn`` command line runner for launching Gunicorn with
    generic WSGI applications.
    """
    from gunicorn.app.wsgiapp import WSGIApplication
    WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]").run()

实例化了WSGIApplication,然后执行run方法启动。

WSGIApplication

# https://github.com/benoitc/gunicorn/blob/20.1.0/gunicorn/app/base.py#L70

class BaseApplication(object):
    ...
    def run(self):
        try:
            Arbiter(self).run()
        except RuntimeError as e:
            print("\nError: %s\n" % e, file=sys.stderr)
            sys.stderr.flush()
            sys.exit(1)

WSGIApplication继承BaseApplicationrun实现,最终实例化Arbiter执行其run

Arbiter

Arbiter是gunicorn的核心类,负责着整个服务器的运行管理,run方法是服务器启动的核心入口。

# https://github.com/benoitc/gunicorn/blob/20.1.0/gunicorn/arbiter.py#L196

def run(self):
    "Main master loop."
    # 1.初始化实例,并信号监听以及sock
    self.start()
    util._setproctitle("master [%s]" % self.proc_name)

    try:
        # 2.创建worker直到worker数量满足用户的指定条件,如果当前worker数量超过用户指定的条件,则会杀掉创建时间比较长的worker
        self.manage_workers()

        while True:
            # 3.负责判断该进程是否是正真的master,如果是则提升为正真的master(这一块放在最后一部分进行分析)
            self.maybe_promote_master()

            # 4.获取信号
            sig = self.SIG_QUEUE.pop(0) if self.SIG_QUEUE else None
            if sig is None:
                # 5.利用select休眠1小时
                self.sleep()
                # 6.判断worker是否超时,如果是则杀掉worker(将在Worker章节进行分析) 
                self.murder_workers()
                self.manage_workers()
                continue

            if sig not in self.SIG_NAMES:
                self.log.info("Ignoring unknown signal: %s", sig)
                continue

            signame = self.SIG_NAMES.get(sig)
            # 调用对应的信号处理
            handler = getattr(self, "handle_%s" % signame, None)
            if not handler:
                self.log.error("Unhandled signal: %s", signame)
                continue
            self.log.info("Handling signal: %s", signame)
            handler()
            # 7.这样下次循环就不会等待一秒了
            self.wakeup()
    # 8.服务异常,进行退出处理
    except (StopIteration, KeyboardInterrupt):
        # 收到用户的退出信号(按下CTRL+C) 
        self.halt()
    except HaltServer as inst:
        # Worker运行异常的时候
        self.halt(reason=inst.reason, exit_status=inst.exit_status)
    except SystemExit:
        raise
    except Exception:
        # 其它的运行异常
        self.log.info("Unhandled exception in main loop",
                      exc_info=True)
        self.stop(False)
        if self.pidfile is not None:
            self.pidfile.unlink()
        sys.exit(-1)

run方法的loop中会执行核心的manage_workers管理子进程逻辑。

ManageWorkers

# https://github.com/benoitc/gunicorn/blob/20.1.0/gunicorn/arbiter.py#L545

class Arbiter(object):

    def manage_workers(self):
        if len(self.WORKERS) < self.num_workers:
            self.spawn_workers()
        ...

    def spawn_worker(self):
        self.worker_age += 1
        worker = self.worker_class(self.worker_age, self.pid, self.LISTENERS,
                                   self.app, self.timeout / 2.0,
                                   self.cfg, self.log) # 通过配置获取子进程worker类

        self.cfg.pre_fork(self, worker)
        pid = os.fork()      # 执行fork
        if pid != 0:
            worker.pid = pid
            self.WORKERS[pid] = worker
            return pid

        ...

        # Process Child
        worker.pid = os.getpid()
        try:
            util._setproctitle("worker [%s]" % self.proc_name)
            self.log.info("Booting worker with pid: %s", worker.pid)
            self.cfg.post_fork(self, worker)
            worker.init_process()        # 执行worker的初始化
            sys.exit(0)
        except SystemExit:
            raise


    def spawn_workers(self):

        for _ in range(self.num_workers - len(self.WORKERS)):
            self.spawn_worker()
            time.sleep(0.1 * random.random())

manage_workers中会实例化配置的worker类,然后fork子进程执行workerinit_process

Worker

Worker在gunicorn的pre-fork子进程中负责运行应用处理请求, 大部分worker都是对gunicorn的http、wsgi接口封装,但可以通过自定义协议封装来支持TCP应用。

  • 如果worker使用gunicorn实现的http、wsgi接口,则只需要考虑怎么处理来自客户端的请求即可。Gunicron内置实现了基于thread、gevent、tornado和sync的不同worker,默认gunicorn使用sync的worker,处理请求是单线程、阻塞同步模式。
  • 如果worker在init_process之后,worker不再使用gunicorn实现的http、wsgi接口,仅仅使用共享master进程获得的socket,则可以自定义协议实现TCP应用,这种模式下gunicorn充当一个TCP应用进程管理器。
# src/gunicorn/gunicorn/workers git:(master) $ ls -lh

-rw-rw-r-- 1 arvin arvin 5.6K Jul 11  2022 base_async.py
-rw-rw-r-- 1 arvin arvin 9.0K Mar 24 10:50 base.py
-rw-rw-r-- 1 arvin arvin 6.0K Mar 24 10:50 geventlet.py
-rw-rw-r-- 1 arvin arvin 5.7K Mar 24 10:50 ggevent.py
-rw-rw-r-- 1 arvin arvin  12K Jul 11  2022 gthread.py
-rw-rw-r-- 1 arvin arvin 5.9K Mar 24 10:50 gtornado.py
-rw-rw-r-- 1 arvin arvin  594 Jul 11  2022 __init__.py
-rw-rw-r-- 1 arvin arvin 7.2K Jul 11  2022 sync.py
-rw-rw-r-- 1 arvin arvin 1.7K Jul 11  2022 workertmp.py

SyncWorker

# https://github.com/benoitc/gunicorn/blob/20.1.0/gunicorn/workers/base.py#L29
class Worker(object):

    def init_process(self):
        ....
        self.load_wsgi() # 加载wsgi应用
        ...
        self.run()


# https://github.com/benoitc/gunicorn/blob/20.1.0/gunicorn/workers/sync.py#L25
class SyncWorker(base.Worker):

    def accept(self, listener):
        client, addr = listener.accept()
        client.setblocking(1)
        util.close_on_exec(client)
        self.handle(listener, client, addr) # 处理连接请求

    def run_for_one(self, timeout):
        while self.alive:
            ...
           self.accept(listener)
            ...


    def run_for_multiple(self, timeout):
        while self.alive:
            ...
           self.accept(listener)
            ...

    def run(self): # main loop
        ...
        if len(self.sockets) > 1:
            self.run_for_multiple(timeout) # 进入请求处理loop
        else:
            self.run_for_one(timeout)  # 进入请求处理loop

    def handle(self, listener, client, addr):
         ...
         parser = http.RequestParser(self.cfg, client, addr) # 解析http请求
         req = next(parser)
         self.handle_request(listener, req, client, addr) # 处理wsgi请求
        ...


    def handle_request(self, listener, req, client, addr):
          ...
          resp, environ = wsgi.create(req, client, addr,
                                        listener.getsockname(), self.cfg) # 构建wsgi协议对象
          ...
          respiter = self.wsgi(environ, resp.start_response) # 调用wsgi应用
          ...
  • SyncWorker启动流程为:init_process(初始化)->self.load_wsgi(加载wsgi应用)->run(进入处理请求loop)。
  • SyncWorker处理一个http请求的流程为:accept(接受请求)->handle(解析http)->handle_request(封装wsgi)->self.wsgi(调用wsgi应用)。
阅读全文

深入浅出Python开发Web

深入浅出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()  # 启动服务器
阅读全文

深入浅出Web开发

Web开发

Web开发,通俗一点来说就是做网站。学术一点来说,是做B/S架构的软件,B指Browser浏览器,S指服务端Server。

浏览器(HTML、JavaScript) <—> HTTP协议 <—> 服务器(PHP、Python、Java...)

用户打开网址,服务器返回HTML,浏览器解析HTMl图形化展示给用户内容。
用户提交表单,经过HTTP协议传输,服务收到来自用户的数据,数据交给应用程序处理,返回给用户结果。

移动互联网时代,这个架构也许是这样。

APP(ios,安卓) <—> HTTP协议 <—> 服务器(PHP、Python、Java...)

B(Browser)变成了C(Client),也叫C/S架构。

现在的Web开发,按照这个架构HTTP协议的两头分为前端、后端,本文我们侧重后端。

HTTP协议

我们先从架构中的网络部分开始,即HTTP协议rfc2616

HTTP协议中定义了两个角色客户端和服务端:

  • 客户端,发起请求的实体
  • 服务端,响应请求的实体

在B/S架构中,客户端便是我们的浏览器,服务端是我们服务器软件。

常见的浏览器有:Chrome、Firefox、IE等。
常见的服务器软件有:Apache2、Nginx等。

这些软件已经帮我们实现了,Web体系中面向网络的,网络编程部分工作,比如建立连接、解析HTTP报文、提取数据等。

应用网关协议

在Web后端中,服务器软件档在了前面,处理网络连接和数据接收解析。

我们开发业务系统,其实工作在这些Web服务器的后面,业务系统与Web服务器交互。

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

我们先看一个普通的程序

def get_user_info(user_id):
	user = db.query("select * from user where id=?",user_id)
	return {
		"result": user
	}

问:我们返回的结果如何交付给Web服务器?Web服务器如何执行这段Python代码?

答:应用网关协议!

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

应用网关协议定义了业务系统如何与Web服务器交互,比如Web服务器如何将用户传来的参数传给业务系统,业务系统如何交付输出给Web服务器。

目前为止不同Web服务器,编程语言出现了很多应用网关协议,比如CGI、FastCGI、Python的WSGI,Java的Servlet等等。

我们以最古老和简单的CGI(Common Gateway Interface)为例rfc3875

CGI的工作原理非常简单,业务系统是一个简单的可执行二进制或脚本文件,当请求到来,Web服务器fork子进程执行这个可执行程序,请求数据通过Stdin和环境变量传入,然后接受子进程执行结果的Stdout,最后将结果装入HTTP响应报文返回给客户端。

Browser <—HTTP协议—> Web Server <—CGI—> exec(get_user.py, env,stdin, stdout)

将我们上面的程序封装成CGI Web程序大概这样子。

def get_user_info(user_id):
	user = db.query("select * from user where id=?",user_id)
	return {
		"result": user
	}

# 从环境变量获取请求参数
query_string = get_env("QUERY_STRING")

uid = get_args_from_query(query_string)

# 执行业务逻辑查找用户
user = get_user_info(uid)

# 输出用户数据到stdout
import json
print ("Content-type:application/json")
print ()
print(json.dumps({
     "result":user
 }))

在Ubuntu下以Apache2和Python为例,做一个CGI的例子。

为什么不用Nginx?因为Nginx默认模块不支持CGI了,写本文时候也想用Nginx,发现怪麻烦的。

启用CGI模块

/etc/apache2/mods-enabled
ln -s ../mods-available/cgid.load .
ln -s ../mods-available/cgid.conf .

配置Vhost和CGI脚本路径

# 基于default修改
<VirtualHost *:80>
        ServerAdmin webmaster@localhost
        DocumentRoot /home/www/play

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

<IfModule mod_alias.c> # 添加cgi模块
        <IfModule mod_cgi.c>
                Define ENABLE_USR_LIB_CGI_BIN
        </IfModule>

        <IfModule mod_cgid.c>
                Define ENABLE_USR_LIB_CGI_BIN
        </IfModule>

        <IfDefine ENABLE_USR_LIB_CGI_BIN>
                ScriptAlias /cgi-bin/ /home/www/play/cgi-bin/ # cgi脚本路径
                <Directory "/home/www/play/cgi-bin">
                        AllowOverride None
                        Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
                        Require all granted
                        AddHandler cgi-script .cgi .py .sh # 配置cgi脚本后缀
                </Directory>
        </IfDefine>
</IfModule>

</VirtualHost>

CGI脚本/home/www/play/cgi-bin/index.py

#!/compiler/miniconda3/bin/python

import json
print ("Content-type:application/json")
print ()
print(json.dumps({
    "result":"hello cgi"
}))

访问测试

curl -v http://127.0.0.1/cgi-bin/index.py

< HTTP/1.1 200 OK
< Date: Sun, 29 Aug 2021 03:45:33 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Transfer-Encoding: chunked
< Content-Type: application/json
<
{"result": "hello cgi"}

Web框架

掌握了Web程序整个工作流程,学习Web框架便会轻松很多。框架应该是加速我们实现业务系统的工具,而不是什么难学的技术。

在上面例子中我们,手动处理了很多输入与输出

# 从环境变量获取请求参数
query_string = get_env("QUERY_STRING")

...

# 输出用户数据到stdout
import json
print ("Content-type:application/json")
print ()
print(json.dumps({
     "result":user
 }))

假设现在有一个框架think,也许代码便写成了这样

from think import controller

@controller
def get_user_info(user_id):
	user = db.query("select * from user where id=?",user_id)
	return {
		"result": user
	}

框架帮我们处理了所有对接CGI协议的工作,实际上,目前流行的框架也确实工作在应用网关协议这一层。

总结

画一个点技能的路线图:

Browser  <—    HTTP协议  —>     Web Server         <—        应用网关协议          —>     业务系统(PHP,Python,Java...)
    |                   |                              |                                     |                                              |
 (前端)    (网络协议、路由、DNS)    (操作系统、网络编程 、框架设计、编程语言特性、数据库、中间件、并行/并发、分布式...)
阅读全文

详解HTTP协议

协议内容

HTTP(超文本传输协议)是一种应用层协议,基于TCP/IP协议族。它是一种文本协议,通过文本形式进行数据交换,即传输的TCP报文消息内容是特定格式的文本。

消息格式

HTTP消息分为请求(Request)和响应(Response)两种类型,其格式均符合以下通用格式:

HTTP-message = Request | Response
generic-message = start-line *(message-header CRLF) CRLF [message-body]

其中,消息头部包括General Header、Request Header和Response Header三类,消息体可选,用于携带与请求或响应相关的实体数据。

消息头(Message Header)

格式为

field-name:[field-value]

包括General Header、Request Header和Response Header和Entity Header。

消息体(Message Body)

消息体(Message Body/Entity Body)是可选的,用于携带与请求或响应相关的实体数据。

通用头部(General Header)

通用头部是指在请求和响应消息中都可以使用的头部信息,不会针对特定的资源或实体进行设置,具体字段如:

Cache-Control, Connection, Date, Pragma, Trailer, Transfer-Encoding, Upgrade, Via, Warning等。

实体头部(Entity Header)

实体头部是只能在请求或响应的实体部分中使用的头部。实体是指请求或响应的正文部分,也就是消息体,实体头部包含有关该实体的元数据,如内容长度、内容编码方式,具体字段如:

Allow,Content-Encoding,Content-Language,Content-Length,Content-Location,Content-MD5,Content-Range,Content-Type,Expires,Last-Modified,extension-header等。

请求(Request)

请求(Request)由请求行(Request-Line)、请求头部(Request Header)和消息体(Message Body)三部分构成,其中请求行由请求方法(Method)、请求URI(Request-URI)和HTTP版本(HTTP-Version)组成。

Request = Request-Line * ( ( general-header | request-header | entity-header ) CRLF ) CRLF [ message-body ]
Request-Line = Method SP Request-URI SP HTTP-Version CRLF

HTTP协议定义了多种请求方法,常见的包括:

OPTIONS, GET, HEAD, POST, PUT, DELETE, TRANCE, CONNECT等。

请求URI可以是绝对地址(absolute URI)、相对地址(abs_path)或者授权机构(authority)。

请求头部(Request Header)

请求头部(Request Header)包含了请求相关的属性和特性信息,具体字段如:

Accept, Accept-Charset, Accept-Encoding, Accept-Language, Authorization, Expect, Form, Host,If-match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since, Max-Forwards, Proxy-Authorization,Range, Referer, TE, User-Agent等

响应(Response)

响应(Response)由状态行(Status-Line)、响应头部(Response Header)和消息体(Message Body)三部分构成,其中状态行由HTTP版本(HTTP-Version)、状态码(Status-Code)和原因短语(Reason-Phrase)组成。

Response = Status-Line * ( ( general-header | response-header | entity-header ) CRLF ) CRLF [ message-body ]
Status-Line = HTTP-Version SP Status-Code SP Reason-Pharse CRLF

HTTP状态码共分为5大类,包括1XX信息类、2XX成功类、3XX重定向类、4XX客户端错误类和5XX服务端错误类。响应头部以及消息体的信息与请求头部和消息体的内容相似。

响应头部(Response Header)

响应头部(Response Header)包含了响应相关的属性和特性信息,具体字段如:

Accept-Ranges, Age, Etag, Location, Proxy-Authenticate, Retry-After, Server, Vary, WWW-Authenticate等。

HTTP服务器实现

HTTP协议是一种文本协议,实现HTTP服务器,按照协议格式正确处理文本即可。

下面是一个使用Python的socket和多线程,实现的简单HTTP服务器SimpleHTTPServer,支持GET请求,找不到资源返回404,没有实现的方法返回501。

import socket
from multiprocessing.dummy import Pool as ThreadPool
import traceback
import logging
import os


class Server(object):

    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)
        self._logger.addHandler(logging.StreamHandler())

    def run(self):
        # 初始化 socket,绑定地址并开始监听
        self._listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._listen_fd.bind((self._host, self._port))
        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:
            # 关闭监听 socket
            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)
            if method == "GET":
                code = self.try_file(conn, path)
            elif method == "POST":
                code = self.unimplemented(conn)
            else:
                code = self.unimplemented(conn)

            # 记录访问日志
            self._logger.info("{}:{} {} {} {} {}".format(addr[0], addr[1], http_version, method, path, code))
        except Exception as e:
            traceback.print_exc()
        finally:
            # 关闭连接
            conn.close()

    def try_file(self, conn: socket.socket, path: str):
        # 尝试读取静态文件
        here = os.path.abspath(os.path.dirname(__file__))
        target = os.path.join(here, "www", path.strip("/"))
        if not os.path.isfile(target):
            return self.not_found(conn)
        with open(target, "rb") as target_file:
            data = target_file.read()
            conn.sendall(b"HTTP/1.0 200 OK\r\n")
            conn.sendall(self.SERVER_STRING)
            if ".html" in path:
                conn.sendall(b"Content-Type: text/html\r\n")
            else:
                conn.sendall(b"Content-Type: application/octetstream\r\n")
            conn.sendall(bytes("Content-Length: {}\r\n".format(len(data)), "utf-8"))
            conn.sendall(b"\r\n")
            conn.sendall(data)
            return 200

    def not_found(self, conn: socket.socket):
        # 处理404错误
        html = "<html>"
        html += "<head><title>Not Found</title></head>"
        html += "<body>Not Found</body>"
        html += "</html>"
        html = bytes(html, "utf-8")
        conn.sendall(b"HTTP/1.0 404 Not Found\r\n")
        conn.sendall(self.SERVER_STRING)
        conn.sendall(b"Content-Type: text/html\r\n")
        conn.sendall(b"Content-Encoding: utf-8\r\n")
        conn.sendall(bytes("Content-Length: {}\r\n".format(len(html)), "utf-8"))
        conn.sendall(b"\r\n")
        conn.sendall(html)
        return 404

    def unimplemented(self, conn: socket.socket):
        # 处理501错误
        html = "<html>"
        html += "<head><title>Method Not Implemented</title></head>"
        html += "<body>HTTP request method not supported</body>"
        html += "</html>"
        html = bytes(html, "utf-8")
        conn.sendall(b"HTTP/1.0 501 Method Not Implemented\r\n")
        conn.sendall(self.SERVER_STRING)
        conn.sendall(b"Content-Type: text/html\r\n")
        conn.sendall(b"Content-Encoding: utf-8\r\n")
        conn.sendall(bytes("Content-Length: {}\r\n".format(len(html)), "utf-8"))
        conn.sendall(b"\r\n")
        conn.sendall(html)
        return 501

    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 = None

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


if __name__ == "__main__":
    server = Server("0.0.0.0", 3000)
    server.run()
阅读全文

内存管理

write code

任何类型的指针变量都可以用来存储内存地址,通常用u_char *表示一个内存地址。

(void*)-1是将有符号整数-1强制转换为指向void类型的指针,这个指针的值为-1,它通常用于表示一个无效的指针或错误指针。

malloc

标准C库中的malloc函数维护了一张内存块链表(或者堆)来管理动态内存分配和释放。当程序调用malloc函数申请内存时,malloc会在链表中查找大小合适的空闲内存块,如果找到则使用该内存块;否则就会向操作系统请求更多的内存,并把这个新申请的内存块加入到链表中,以备后续使用。在这个过程中,malloc会把每个内存块的大小等信息记录下来,以便在后续的内存管理操作中使用。

free

调用free函数时,C库会根据该指针,找到相应的内存块,并把它标记为可用状态,以便下次又有其他程序需要使用该内存时可以再次被分配出去。因此,在使用free函数时,我们必须确保传入的指针是通过malloc或者realloc函数申请得到的,否则可能会发生内存泄漏或者非法操作的情况。

syscall

#include<unistd.h>

void *sbrk(intptr_t increment);
int brk(void *addr);

sbrk函数返回指向新分配内存的起始地址的指针,并且自动将堆末地址增加increment字节,brk函数将堆末指针移动到addr处。两个函数都返回0表示操作成功,返回-1表示失败并设置errno变量。

#include <unistd.h>

int main() {
    void* ptr1 = sbrk(10 * sizeof(int)); // 分配10个整型变量的内存空间
    if (ptr1 == (void*) -1) {
        // 内存分配失败
        return -1;
    }

    void* ptr2 = sbrk(20 * sizeof(int)); // 再次分配20个整型变量的内存空间
    if (ptr2 == (void*) -1) {
        // 内存分配失败
        brk(ptr1); // 释放ptr1之前分配的内存
        return -1;
    }

    // 使用ptr1和ptr2进行一些操作

    brk(ptr2); // 释放ptr2之前分配的内存
    brk(ptr1); // 释放ptr1之前分配的内存

    return 0;
}

malloc会调用brk/sbrk来增加堆区大小以满足分配需求,分配/回收完成进程的堆顶指针(brk指针)将被更新,指向新的堆顶位置。

kernel

即使调用brk()sbrk()函数移动了进程数据段的位置,Linux内核也不会立即为进程分配新的物理内存。Linux内核只有在进程需要访问这些新分配的虚拟地址时,才会将虚拟地址映射到物理内存。

在进程需要访问某个虚拟地址时,Linux内核首先会检查该虚拟地址是否已经被映射到物理内存,如果没有则会进行页面缺失(page fault)处理。这时,Linux内核会评估进程请求的内存块大小,并查找可用的物理内存块来满足进程的请求。如果有足够的可用物理内存,则Linux内核会将该内存块映射到进程的虚拟地址空间中,并允许进程对其进行读写操作。

阅读全文