最近在学习Flask源码的时候,发现了python有一个内置的http,可以用来搭建http服务器,所以花时间研究了一番。

httpserver

基于python的http包构建一个简易http服务器。

使用到的两个类

from http.server import BaseHTTPRequestHandler, HTTPServer

源码

首先查看BaseHTTPRequestHandler处理请求的部分的源码,才知道继承它后要怎么样才能接收处理请求。

    # 跳过前面负责的逻辑,直接来到
    # handle_one_request
    # 这个方法就是BaseHTTPRequestHandler处理一个请求的方法。
    def handle_one_request(self):
        try:
            # 读取请求
            self.raw_requestline = self.rfile.readline(65537)
            if len(self.raw_requestline) > 65536:
                self.requestline = ''
                self.request_version = ''
                self.command = ''
                self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
                return
            if not self.raw_requestline:
                self.close_connection = True
                return
            
            # 这一句是解析http头的(这时的http请求在raw_requestline中,也就是解析raw_requestline)
            # 就是将类似b'GET /test-request HTTP/1.1\r\n...'的请求头解释出来
            # An error code has been sent, just exit
            if not self.parse_request(): 
                return
            
            # self.command 是解释出来的command,如'GET', 'POST'
            # 注意这里没有使用lower函数
            mname = 'do_' + self.command 
            if not hasattr(self, mname):
                self.send_error(
                    HTTPStatus.NOT_IMPLEMENTED,
                    "Unsupported method (%r)" % self.command)
                return
            method = getattr(self, mname)
            # 所以这个do_command需要是一个可以调用的对象,可以是一个方法。
            # 每次有相应的请求到来,就会调用相应的do_command()
            # 所以我们实现这些方法就可以了:
            # 如果想要处理get,就在继承中实现do_GET方法, 如果处理post就实现do_POST方法等等。
            method()
            self.wfile.flush() #actually send the response if not already done.
        except socket.timeout as e:
            #a read or a write timed out.  Discard this connection
            self.log_error("Request timed out: %r", e)
            self.close_connection = True
            return

读了源码之后就大概知道如何实现自己的Http服务器了:

思路是继承BaseHTTPRequestHandler写一个处理类,然后用它初始化一个HTTPServer

继承BaseHTTPRequestHandler

继承BaseHTTPRequestHandler,然后实现相应的方法如do_GET,do_POST,do_DELETE等。(注意大小写一定要对)

class MyHttpRequestHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        # 处理逻辑
        pass
    
    def do_POST(self):
        pass
    
    ...

创建Server

创建一个server,这里的server是HTTPServer类,设置监听地址和端口,将MyHttpRequestHandler传递给它。

ts = HTTPServer(('127.0.0.1', 8899), MyHttpRequestHandler)

然后需要让服务器开始工作

ts.serve_forever()

这时服务器就开始工作了。

所以我们该在BaseHTTPRequestHandler中准备一些简单的逻辑,否则无输出的话我们也不知道服务器是否在运行中。

do_GET

因为GET方法容易测试,所以选择GET方法测试。

do_GET中添加一些逻辑,在do_GET返回响应头和数据。

    def do_GET(self):
        # 响应头
        headers = """HTTP/1.1 200 OK
Server: YouFather
Accept-Ranges: bytes
Content-Length: {data-length}
Vary: Accept-Encoding
Content-Type: text/html
""".replace('\n', '\r\n') + '\r\n'
        
        # 响应数据
        data = "<a href='http://www.baidu.com'>百度</a>".encode('gbk')

        # 设置一下这个Content-Length参数,告诉客户端数据的长度。
        headers = headers.format_map({'data-length': len(data)})

        # 写入响应头和数据
        # 这里的wfile是HTTPServer的基类TCPServer为我们准备的一个写入对象。
        # wfile = socket.make_file('wb')
        # 和open(filename,'w')是同一类型。
        self.wfile.write(headers.encode())
        self.wfile.write(data)

执行一下

浏览器结果

如果方法命名不对如do_get,或请求到没有的方法如没有do_GET,都会引发异常

控制台:

127.0.0.1 - - [05/Aug/2021 17:35:52] code 501, message Unsupported method ('GET')
127.0.0.1 - - [05/Aug/2021 17:35:52] "GET / HTTP/1.1" 501 -
127.0.0.1 - - [05/Aug/2021 17:35:52] code 501, message Unsupported method ('GET')
127.0.0.1 - - [05/Aug/2021 17:35:52] "GET /favicon.ico HTTP/1.1" 501 -

浏览器端:

浏览器的返回

获取Http的请求信息

do_GET中可能要用到Http相关的请求信息,这些大部分都由BaseHTTPRequestHandler为我们准备好了。

来看看BaseHTTPRequestHandler封装了哪些消息给我们:

# 修改MyHttpRequestHandler
class MyHttpRequestHandler(BaseHTTPRequestHandler):
    is_first_request = True

    def do_GET(self):
        if self.is_first_request:
            self.is_first_request = False
            for k in dir(self):
                v = getattr(self, k)
                # 魔术方法,私有变量和大部分的callable并不是为我们提供的,所以排除
                if k.startswith('_') or callable(v): 
                    continue
                print('---arg: ', k, ' type:', v.__class__.__name__, '\n', v, '\n\n', sep='')

        headers = """HTTP/1.1 200 OK
Server: YouFather
Accept-Ranges: bytes
Content-Length: {data-length}
Vary: Accept-Encoding
Content-Type: text/html
""".replace('\n', '\r\n') + '\r\n'

        data = "<a href='http://www.baidu.com'>百度</a>".encode('gbk')

        headers = headers.format_map({'data-length': len(data)})

        self.wfile.write(headers.encode())
        self.wfile.write(data)

再运行,浏览器访问一下

---arg: client_address type:tuple
('127.0.0.1', 59205)

---arg: close_connection type:bool
True

---arg: command type:str
GET

---arg: connection type:socket
<socket.socket fd=888, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8899), raddr=('127.0.0.1', 59205)>

---arg: default_request_version type:str
HTTP/0.9

---arg: disable_nagle_algorithm type:bool
False

---arg: error_content_type type:str
text/html;charset=utf-8

---arg: error_message_format type:str
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
        <title>Error response</title>
    </head>
    <body>
        <h1>Error response</h1>
        <p>Error code: %(code)d</p>
        <p>Message: %(message)s.</p>
        <p>Error code explanation: %(code)s - %(explain)s.</p>
    </body>
</html>


---arg: headers type:HTTPMessage
Host: localhost:8899
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9



---arg: is_first_request type:bool
False

---arg: monthname type:list
[None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

---arg: path type:str
/

---arg: protocol_version type:str
HTTP/1.0

---arg: raw_requestline type:bytes
b'GET / HTTP/1.1\r\n'

---arg: rbufsize type:int
-1

---arg: request type:socket
<socket.socket fd=888, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8899), raddr=('127.0.0.1', 59205)>

---arg: request_version type:str
HTTP/1.1

---arg: requestline type:str
GET / HTTP/1.1

---arg: responses type:dict
{<HTTPStatus.CONTINUE: 100>: ('Continue', 'Request received, please continue'), <HTTPStatus.SWITCHING_PROTOCOLS: 101>: ('Switching Protocols', 'Switching to new protocol; obey Upgrade header'), <HTTPStatus.PROCESSING: 102>: ('Processing', ''), <HTTPStatus.OK: 200>: ('OK', 'Request fulfilled, document follows')...省略节省篇幅}

---arg: rfile type:BufferedReader
<_io.BufferedReader name=888>

---arg: server type:HTTPServer
<http.server.HTTPServer object at 0x0000022A1A1011C0>

---arg: server_version type:str
BaseHTTP/0.6

---arg: sys_version type:str
Python/3.8.6

---arg: timeout type:NoneType
None

---arg: wbufsize type:int
0

---arg: weekdayname type:list
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

---arg: wfile type:_SocketWriter
<socketserver._SocketWriter object at 0x0000022A335CCE20>
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐