20240416_python手搓http服务器

最近看了有关阻塞、非阻塞、同步、异步、IO多路复用一些知识,所以想着手搓一个极简版本的Http服务器用来练手、以及巩固知识。

相关代码如下:

一、多线程版

 1import random
 2import socket
 3import threading
 4import time
 5
 6# 定义一个自定义的返回页面,通过读取当前文件夹下的index.html来构造web页面的bytes字节流
 7def page_response() -> bytes:
 8    try:
 9        # return b"Hello_world"
10        # 两种实现:
11        # 方法1:打开文件为bytes模式,并进行bytes内容的替换。
12        # with open("index.html",mode="rb") as file : # rb 含义  r->read   b->bytes
13        #     content_bytes= file.read()
14        #     content_bytes:bytes = content_bytes
15        #     random_num: int = random.randint(1, 100)
16        #     new_content = content_bytes.replace(b"{{replace_tag}}",str(random_num).encode("utf-8"))
17        #     return new_content
18        # 方法2:打开文件为str模式。
19        with open("index.html", mode="r", encoding="utf-8") as file:  # rb 含义  r->read ,指定编码模式为utf-8
20            content = file.read()
21            content:str = content
22            # 产生一个随机数用于替换index.html当中的{{replace_tag}} 字段内容,  :int变量注解。
23            random_num:int = random.randint(1,100)
24            modified_content = content.replace("{{replace_tag}}", str(random_num))
25            modified_content_bytes = modified_content.encode("utf-8")
26            return  modified_content_bytes
27    except FileNotFoundError:
28        return b"File not found or unable to open the file."
29
30def page_header(body:bytes) -> bytes  :
31    response_header = """\
32HTTP/1.1 200 OK
33Content-Type: text/html;charset=utf-8
34Content-Length:{}
35
36""".format(len(body)).replace('\n','\r\n').encode()  # encode() 会转化称为字节流 byte
37    return response_header
38
39def print_threadlist():
40    while True:
41        # 获取所有活动线程
42        all_threads = threading.enumerate()
43        # 提取线程名称和线程 ID
44        filtered_threads = [(thread.name, thread.ident) for thread in all_threads]
45        # print(filtered_threads)
46        # 创建一个格式化的字符串,包含线程名称和线程 ID
47        formatted_output = ", ".join(f"{name} ({thread_id})" for name, thread_id in filtered_threads)
48        # 输出所有线程的名称和 ID
49        print(f"输出所有线程: {formatted_output}")
50        time.sleep(5)
51
52
53# 该函数用于产生conn的子线程并拼接字符串返回给该TCP链接
54def fn1(conn:socket.socket): # 这里fn1函数使用参数注解,用以描述conn,后续conn 能够对带出相关的函数出来。
55    try:
56        while True:
57            # 拼装一个http协议的回复报文的字节序列,定义在连接处产生随机字符串,
58            response_byte = page_header(page_response()) + page_response()
59            data = conn.recv(4096)
60            print(type(data),data)  # 打印由client发送过来的二进制数据字节bytes.
61            if not data: # 当clinet侧进行了主动断开连接,则此时conn 主动关闭,该if条件用于判断客户端主动挥手动作。 该处的not data 体现了python的数据等效性原则。
62                print(conn.getpeername() + "Bye~")
63                conn.close()
64            conn.send(response_byte)
65            print(response_byte)
66            # conn.close()
67    except Exception as e :
68        print(e)
69    finally: # 处理最终请求,最终一定conn需要关闭该连接。 使用try except finally 逻辑处理。
70        conn.close()
71
72# 定义一个产生conn连接的函数。
73def fn2(server:socket.socket):
74    thread_number = 1
75    while True:
76        conn, raddr = server.accept()
77        print("Success accept raddr: {}".format(raddr))
78        #  将产生conn连接的对象丢到fn1 函数当中进行处理。
79        threading.Thread(target=fn1,name="conn-{}".format(thread_number),args=(conn,),daemon=True).start() # daemon为True表示为该线程为非陪跑线程。
80        thread_number +=1
81
82if __name__ == '__main__':
83    # 定义一个Server
84    server = socket.socket()
85    server.bind(("0.0.0.0",1234))
86    server.listen(1024)
87
88    # 产生一线程执行fn2函数,该函数用于持续接待新的连接并将其放置到新的线程当中。
89    threading.Thread(target=fn2, name="accept_thread", args=(server,)).start() # 这里是一个一直循环的陪跑线程。
90
91    # 配置一个死循环打印与检测当前整个进程的子线程数。
92    print_threadlist()
93
94    # while True:
95        # time.sleep(5)
96        # print("当前线程状态:{}".format([t.name for t in threading.enumerate()]))# 元组当中取元素,集合推导式的写法,从列表当中获取名字,如果加了*表示对齐解构,获得每一个元素。
97    # server.close()

二、线程池版

  1import random
  2import socket
  3import threading
  4import time
  5# 引入线程池的包。
  6from concurrent.futures import ThreadPoolExecutor
  7
  8# 构建一个模板,读取文件并返回
  9# respo_body="""\
 10# <!DOCTYPE html>
 11# <html lang="en">
 12# <head>
 13#     <meta charset="UTF-8">
 14#     <title>我的主页</title>
 15# </head>
 16# <body>
 17#     <h1 style="color:red">测试,欢迎页面!Helloworld!!!-{}</h1>
 18# </body>
 19# </html>
 20# """.replace("{{}}",str(format(random.randint(0,100)))).encode()
 21
 22
 23
 24# 定义一个自定义的返回页面,通过读取当前文件夹下的index.html来构造web页面的bytes字节流
 25def page_response() -> bytes:
 26    try:
 27        # 两种实现:
 28        # 方法1:打开文件为bytes模式,并进行bytes内容的替换。
 29        with open("index.html", mode="rb") as file:  # rb 含义  r->read   b->bytes
 30            content_bytes = file.read()
 31            content_bytes: bytes = content_bytes
 32            random_num: int = random.randint(1, 100)
 33            new_content = content_bytes.replace(b"{{replace_tag}}", str(random_num).encode("utf-8"))
 34            return new_content
 35        # 方法2:打开文件为str模式。
 36        # with open("index.html", mode="r", encoding="utf-8") as file:  # rb 含义  r->read ,指定编码模式为utf-8
 37        #     content = file.read()
 38        #     content:str = content
 39        #     # 产生一个随机数用于替换index.html当中的{{replace_tag}} 字段内容,  :int变量注解。
 40        #     random_num:int = random.randint(1,100)
 41        #     modified_content = content.replace("{{replace_tag}}", str(random_num))
 42        #     modified_content_bytes = modified_content.encode("utf-8")
 43        #     return  modified_content_bytes
 44    except FileNotFoundError:
 45        return b"File not found or unable to open the file."
 46
 47#    Content-Type: text/html;charset=gbk
 48def page_header(body: bytes) -> bytes:
 49    response_header = """\
 50HTTP/1.1 200 OK
 51Content-Type: text/html;charset=utf-8
 52Content-Length:{}
 53
 54""".format(len(body)).replace('\n', '\r\n').encode()  # encode() 会转化称为字节流 byte
 55    return response_header
 56
 57def print_threadlist():
 58    while True:
 59        # 获取所有活动线程
 60        all_threads = threading.enumerate()
 61        # 提取线程名称和线程 ID
 62        filtered_threads = [(thread.name, thread.ident) for thread in all_threads]
 63        # print(filtered_threads)
 64        # 创建一个格式化的字符串,包含线程名称和线程 ID
 65        formatted_output = ", ".join(f"{name} ({thread_id})" for name, thread_id in filtered_threads)
 66        # 输出所有线程的名称和 ID
 67        print(f"输出所有线程: {formatted_output}")
 68        time.sleep(5)
 69
 70# 该函数用于产生conn的子线程并拼接字符串返回给该TCP链接
 71def fn1(conn: socket.socket):  # 这里fn1函数使用参数注解,用以描述conn,后续conn 能够对带出相关的函数出来。
 72    try:
 73        while True:
 74            # 拼装一个http协议的回复报文的字节序列,定义在连接处产生随机字符串,
 75            response_byte = page_header(page_response()) + page_response()
 76            data = conn.recv(4096)
 77            print(type(data), data)  # 打印由client发送过来的二进制数据字节bytes.
 78            if not data:  # 当clinet侧进行了主动断开连接,则此时conn 主动关闭,该if条件用于判断客户端主动挥手动作。 该处的not data 体现了python的数据等效性原则。
 79                print(conn.getpeername() + "Bye~")
 80                conn.close()
 81            conn.send(response_byte)
 82            # conn.close()
 83    except Exception as e:
 84        print(e)
 85    finally:  # 处理最终请求,最终一定conn需要关闭该连接。 使用try except finally 逻辑处理。
 86        conn.close()
 87
 88# 定义一个产生conn连接的函数。
 89def fn2(server: socket.socket):
 90    while True:
 91        conn, raddr = server.accept()
 92        print("Success accept raddr: {}".format(raddr))
 93        #  调用fn1的函数,把fn1的函数丢到全局线程池thread_executor当中去执行,其中conn为fn1 函数的参数。
 94        thread_executor.submit(fn1,conn)
 95
 96if __name__ == '__main__':
 97    # 定义一个Server
 98    server = socket.socket()
 99    server.bind(("0.0.0.0", 1234))
100    server.listen(1024)
101
102    # 使用线程池方式实现,多线程web页面。
103    # 定义懒加载的全局线程池,容量为4个,这里不包括主线程,由于fn2为常驻状态,因此,client 最多能够同时连接3个进来。
104    # 如果有第四个client连接进来,则会卡住,此时放入到队列当中,直到某个client退出后等待队列的线程才会正常处理网络请求。
105    pool_size= 4
106    thread_executor = ThreadPoolExecutor(pool_size)
107    # 把fn2的函数丢到线程池当中取执行。server为fn2的参数,这里不想 threading.Thread 使用元组传递参数。
108    thread_executor.submit(fn2,server)
109
110    # 配置一个死循环打印与检测当前整个进程的子线程数。
111    print_threadlist()

三、IO多路复用版

  1import socket
  2import selectors
  3
  4def page_response():
  5    try:
  6        with open("index.html",mode="rb") as file :
  7            content = file.read()
  8            return  content
  9    except FileNotFoundError:
 10        return b"File not found or unable to open the file."
 11# Content-Type: text/html;charset=gbk
 12response_header = """\
 13HTTP/1.1 200 OK
 14Content-Type: text/html;charset=utf-8
 15Content-Length:{}
 16
 17""".format(len(page_response())).replace('\n','\r\n').encode()  # encode() 会转化称为字节流 byte
 18
 19# 拼装一个http协议的回复报文的字节流。
 20response_byte = response_header + page_response()
 21
 22def get_response() -> bytes:
 23    return  response_byte
 24
 25def fn1(server:socket.socket):
 26    conn,raddr = server.accept()
 27    print("type of conn :{}".format(type(conn)))
 28    # 需要将conn的阻塞状态也配置为一个非阻塞状态。
 29    conn.setblocking(False)
 30    print("conn是:{} , raddr是:{} ".format(conn,raddr))
 31    k=myselector.register(conn, selectors.EVENT_READ,fn2)
 32    print(k)
 33def fn2(conn:socket.socket,raddr:tuple):
 34    # 配置该链接的超时时间 为10秒,如果没有数据过来则关闭
 35    conn.settimeout(10)
 36    try:
 37        while True:
 38            data = conn.recv(1024)
 39            print("收到数据:",data)
 40            conn.send(get_response())
 41            print("数据已经发")
 42            if not data:
 43               # print("bye",conn.getpeername())
 44               ip_address, port = raddr
 45               print("bye",f"{ip_address}:{port}")
 46               myselector.unregister(conn)
 47               conn.close()
 48               return
 49    except socket.timeout:
 50        print("10 秒内没有接收到数据。正在关闭套接字。")
 51        myselector.unregister(conn)
 52        conn.close()
 53    except Exception as e:
 54        print(f"发生错误:{e}")
 55        myselector.unregister(conn)
 56        conn.close()
 57    finally:
 58        print("Finally unregister and conn.close !")
 59        # 不能够直接关闭,关闭前需要反向注册Selector当中的连接。
 60
 61if __name__ == '__main__':
 62    server = socket.socket()
 63    server.bind(("0.0.0.0",1234))
 64    server.setblocking(False) # 注意:如果使用 io 多路复用的模式则需要将server 设置为非阻塞状态。
 65    server.listen(1024)
 66    # 打印输出server的类型,类型为socket.socket
 67    print("type of server:{}".format(type(server)))
 68
 69    # 实例化一个IO多路复用Selector
 70    myselector = selectors.DefaultSelector()  # 实例化io多路复用类,其中python的多路复用会默认选择最佳的IO多路复用选择器,windows 只能够用Selector 模式。 linux 则会变为epoll模型。
 71
 72    # 将server注册到 myselector当中,此外对于需要配置需要观测的事件,这里对于读事件进行关注,data字段是注册的执行函数,返回值为MySelectKey为注册的内容。
 73    # 查注册的IO多路复用器当中的内容:
 74    MySelectKey = myselector.register(server,selectors.EVENT_READ| selectors.EVENT_WRITE,fn1)
 75    print("MySelectKey 全部内容:", MySelectKey)
 76    print("MySelectKey 类型:", type(MySelectKey))
 77    print("MySelectKey.fileobj:", MySelectKey.fileobj)
 78    print("MySelectKey.fileobj 类型:", type(MySelectKey.fileobj))
 79    print("MySelectKey.fd:", MySelectKey.fd)
 80    print("MySelectKey.event:", MySelectKey.events)
 81    print("MySelectKey.data:", MySelectKey.data)
 82
 83    # 使用Selector 进行阻塞进程,等待进程进来。使用什么多路复用模型由python的default自己选择,返回元组由2个元素组成。
 84    while True : # 配置一个死循环,让IO多路复用器一直处于等待状态
 85        for key,mask in myselector.select() :
 86            print("key1:",key)
 87            print("type key1:",type(key))
 88            print("key1.fileobj:",key.fileobj)
 89            print("key1.fd:", key.fd)
 90            print("key1.event:", key.events)
 91            print("key1.data:", key.data)
 92            # 判断key.obj的类型进行分别传入两种参数,fn1传入1个对象,fn2 传入2个对象。
 93            if key.data == fn1:
 94                key.data(key.fileobj)  # 单一入参: fn1(server:socket.socket)
 95            if  key.data == fn2:
 96                keyobj: socket.socket = key.fileobj
 97                print("key.fileobj.raddr:", keyobj.getpeername() )
 98                ip_address, port = keyobj.getpeername()
 99                print("type of kobj.getpeername():",type(keyobj.getpeername()))
100                print(f"IP 地址: {ip_address}")
101                print(f"端口号: {port}")
102                # key1.data(key1.fileobj)
103                key.data(key.fileobj,keyobj.getpeername())  # fn2 2个参数入参 fn2(conn:socket.socket,raddr:tuple)

四、html模板

 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4    <meta charset="UTF-8">
 5    <title>我的主页</title>
 6</head>
 7<body>
 8    <h1 style="color:red">测试,欢迎页面!Helloworld!!!-{{replace_tag}}</h1>
 9</body>
10</html>