简陋的不能再简陋的 HTTP1.1 Server
今天实现了一个超级简陋的 HTTP/1.1 服务,使用 python3 实现,主要为了练习 TCP 通信和理解浏览器与服务器之间的交互过程。
知识基础
- python3 的 tcp 网络通信: socket 的使用;
- 客户端与服务器之间的通信过程;
- TCP 通信中的三次握手与四次挥手;
- 网络的长连接与短连接;
- 正则表达式;
- python 的基本知识应用:文件读、Shebang运行、命令参数解析
实现功能
为指定的路径提供简陋的 HTTP/1.1 服务,可以通过浏览器访问,符合 HTTP/1.1 的长连接支持,并且实现的服务是单进程、单线程、非阻塞的。网络服务为了提高响应速度,多以多进程、多线程和协程方式实现服务,一般情况下实现效率从高到低是:协程 > 多线程 > 多进程,主要还是因为资源开销的问题,实际应用过程中大多数情况下主要使用 epoll 技术实现高并发,简单来说是一种高级的单进程、单线程、非阻塞技术,依赖于:内存映射和通知机制。这里实现的非阻塞形式相当简单,与 epoll 相比差很大一截。
这里服务默认开启在 8080 端口,也可以指定端口。
实现代码
因为只是练习,以后应该也不会维护加功能,废话不多说,粗暴地展示代码了:
#!/usr/bin/env python3
import re
import socket
import argparse
def get_args():
"""get_args"""
parser = argparse.ArgumentParser(description="开启指定网站服务")
parser.add_argument("-p", "--port", type=int, default=8080, help="指定网服务端口")
parser.add_argument("site", help="指定网站路径")
return parser.parse_args()
def response_to(client_socket: socket.socket, request, site_path: str):
# 处理请求
req_str: str = request.decode("utf-8")
req_lines = req_str.splitlines()
ret = re.match(r"[^/]+([^(?| )]*)", req_lines[0])
file_name = ""
if ret:
file_name = ret.group(1)
if file_name.endswith("/"):
file_name += "index.html"
file_name = site_path + file_name
print("+" * 50 + "\n" + file_name + "\n" + "+" * 50)
try:
f_req = open(file_name, "rb")
except Exception:
# 生成响应
# 响应内容
response_body = "file is not available!"
# 响应头
response_header = "HTTP/1.1 404 NOT FOUND\r\n"
response_header += f"Content-Length:{len(response_body)}\r\n"
response_header += "\r\n"
response = (response_header + response_body).encode("utf-8")
# 发送响应
client_socket.send(response)
print("Not found!")
else:
html_content = f_req.read()
f_req.close()
# 生成响应
# 响应内容
response_body = html_content
# 响应头
response_header = "HTTP/1.1 200 OK\r\n"
response_header += f"Content-Length:{len(response_body)}\r\n"
response_header += "\r\n"
response = response_header.encode("utf-8") + response_body
# 发送响应
client_socket.send(response)
print(req_str)
def main():
# 获取命令参数
args = get_args()
if args.site:
if args.site.endswith("/"):
args.site = args.site[:-1]
if args.port >= 65535:
args.port = 8080
# 创建套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 保证服务器先关闭套接字时,重启程序可以立马重复使用上一次的配置端口
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_socket.setblocking(False)
# 绑定套接字
tcp_socket.bind(("", args.port))
# 监听
tcp_socket.listen(128)
print(f"{args.site} 的 http 服务已开启")
print(f"访问地址: http://127.0.0.1:{args.port}")
client_socket_list = list()
while True:
try:
# 等待
client_socket, client_addr = tcp_socket.accept()
except Exception as e:
pass
else:
client_socket.setblocking(False)
client_socket_list.append(client_socket)
for client_socket in client_socket_list:
try:
request = client_socket.recv(1024)
except Exception as e:
pass
else:
if request:
response_to(client_socket, request, args.site)
else:
client_socket.close()
client_socket_list.remove(client_socket)
# 关闭套接字
tcp_socket.close()
if __name__ == "__main__":
main()
代码保存到文件 simple_server
中,为源文件添加运行权限:
$ chmod +x simple_server
使用方法
命令行执行命令获取帮助信息
$ ./simple_server -h
usage: simple_server [-h] [-p PORT] site
开启指定网站服务
positional arguments:
site 指定网站路径
optional arguments:
-h, --help show this help message and exit
-p PORT, --port PORT 指定网服务端口
开启服务,我这里使用的测试对象是本人静态博客的路径,如此简陋,居然能用!
$ ./simple_server /Users/5km/smslit/public/ -p 7788
/Users/5km/smslit/public 的 http 服务已开启
访问地址: http://127.0.0.1:7788
这里使用了端口 7788,访问一切👌,什么?你不信,有图有真相的:
分析
实现功能小节中提到了,本文实现的非阻塞方式相当低级,是对套接字列表轮询的机制,这种方式有两个大问题:
- 套接字列表中元素增多,对于一个操作系统来说,这个列表是在用户层的,每次轮询都需要用户层和系统层之间数据的拷贝,才能让系统正常的操作硬件;当套接字数量到足够大时,可能会因为拷贝而造成时间的浪费;
- 当列表中套接字数量足够多,如果其中只有极少部分的套接字需要数据处理,那全部轮询就会造成不必要的时间浪费;
而上文提到的 epoll 恰恰能解决这两个问题,所以十里用 epoll 的方式也实现了这个服务,只是为了理解原理,所以实现同样很简单,可参考: