20240416_Python手搓Http服务器
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>
- 原文作者:Kid
- 原文链接:https://shuanglu.life/post/20240416_python%E6%89%8B%E6%90%93http%E6%9C%8D%E5%8A%A1%E5%99%A8/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。