简陋的不能再简陋的 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,访问一切👌,什么?你不信,有图有真相的:

分析

实现功能小节中提到了,本文实现的非阻塞方式相当低级,是对套接字列表轮询的机制,这种方式有两个大问题:

  1. 套接字列表中元素增多,对于一个操作系统来说,这个列表是在用户层的,每次轮询都需要用户层和系统层之间数据的拷贝,才能让系统正常的操作硬件;当套接字数量到足够大时,可能会因为拷贝而造成时间的浪费;
  2. 当列表中套接字数量足够多,如果其中只有极少部分的套接字需要数据处理,那全部轮询就会造成不必要的时间浪费;

而上文提到的 epoll 恰恰能解决这两个问题,所以十里用 epoll 的方式也实现了这个服务,只是为了理解原理,所以实现同样很简单,可参考:

smslit/tools_of_python/simpe_server


python

1325 字

2018-11-27 10:48 +0800