基于python的简单HTTP服务器实现

发布时间:2019-08-11 11:27:12编辑:auto阅读(1314)

    http协议大概是我们接触的最多的协议了,每打开一个网页,浏览器和服务器之间,使用的就是HTTP协议。HTTP协议属于应用层协议,下一层是运输层。这段时间,学习了一些相关的知识,因为对C++的多线程和网络编程不是很熟悉,先用python实现了一遍,后续会用C++实现。

    HTTP协议

    首先来介绍下http协议。http协议分为请求报文和相应报文两个部分组成。

    请求报文

    请求报文如下图所示:
    这里写图片描述

    请求报文由三个部分组成:
    1. 请求行: 使用的请求方法,请求的地址,以及请求的协议
    2. 请求头部,包含一些请求的描述信息,如是否保活connection,来源哪一个地址Referer,客户端信息User-Agent等等,具体可以查相关协议
    3. 请求数据,包含了请求的数据,主要是post数据

    注意:在请求行和请求头部之间没有空行,但在请求头部和请求数据之间是由一个空行的。
    用代码来描述:

    # 请求行
    GET favicon.ico HTTP/1.1
    # POST / HTTP/1.1
    # 请求头部,这里没有空行【每一行结尾一个\r\n】
    Host: 127.0.0.1:9999
    Connection: keep-alive
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
    Accept: image/webp,image/apng,image/*,*/*;q=0.8
    Referer: http://127.0.0.1:9999/
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9
    
    # 请求数据【body部分】,这里有一个空行,传送的是post的数据【2个\r\n】
    use=123
    name=ll

    请求头部字段解析

    Accept             # 能够接受的响应类型
    Cookie             # 上传给服务器的cookie信息
    Referer         # 从哪一个URL过来的
    Cache-Control     # 控制缓存,如希望不要再客户端进行缓存
    Host             # 制定请求URL的主机和端口
    Content-Length     # POST请求时标记长度
    Connection         # 是否可以处理持久链接
    Accept-Encoding # 编码类型,一般是gzip或者compass
    Accept-Charset     # 字符集
    User-Agent         # 请求的客户端信息
    Authorization     # 用于访问密码保护的网页时识别自己的身份

    响应报文

    响应报文于请求报文结构类似。分为三个部分:
    1. 状态行: 对于请求响应的状态,成功失败或者其他
    2. 响应头部:包括返回的报文的一些描述信息
    3. 响应主体:返回的html代码
    用代码来描述:

    # 状态行
    HTTP/1.1 200 OK
    # 响应头部【每一行结尾一个\r\n】
    content-type: text/html; charset=UTF-8
    
    # 返回的数据【body部分】,这里有一个空行,传送的是post的数据【2个\r\n】
    <h1>Hello, World</h1>

    响应头部字段解析

    Allow             # 服务器支持哪些请求方法(如GET、POST等)。
    Content-Encoding     # 文档的编码(Encode)方法。
    Content-Length     # 表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。
    Content-Type     # 表示文档的MIME类型。
    Date             # 当前的GMT时间。
    Expires         # 定义什么时候认为文档过期。
    Last-Modified     # 文档的最后改动时间。
    Location         # 表示客户应当到哪里去提取文档。
    Server             # 服务器名字。
    Refresh         # 表示浏览器应该在多少时间之后刷新文档,以秒计。
    Set-Cookie         # 设置和页面关联的Cookie。
    WWW-Authenticate     # 客户应该在Authorization头中提供什么类型的授权信息。

    响应状态码

    在响应报文中,第一行就包含了响应状态码,标志着整个请求的结果。分为以下几类:

    1**     # 服务器接受到信息,正在处理
    2**     # 成功
    3**     # 重定向,需要进一步操作
    4**     # 客户端错误,如无法找到请求文件
    5**     # 服务端错误,服务器处理请求时发生错误

    HTTP服务器实现

    实现一个http服务器说白了就是实现一个监听程序,当客户端发来对应的http请求时,能够解析请求,并且返回对应的资源给客户端,客户端解析显示到浏览器上。请求分为两种,一种是简单的静态请求,服务器只需要把对应的静态资源返回即可;另外一种是动态请求,服务器自身无法处理,需要将请求转给服务端其他的程序处理,比如python,php,C++等,然后将处理结果返回给客户端,这就是CGI
    这里使用python进行实现,比较简单。实现的功能如下:
    1. 可以响应静态请求getpost
    2. 可以响应图片请求
    3. 可以实现简单的动态请求

    实现主要在两个文件中,一个是主体文件,基于socket的通信程序,一部分是对于http协议的解析,封装和动态请求转发。代码不是很长,全部贴在这,具体可以看注释。

    • TCP通信部分
    # server.py
    # -*- coding=utf-8 -*-
    import socket
    import threading
    from HttpHead import HttpRequest
    
    # 处理每一个请求
    def tcp_link(sock, addr):
        print('Accept new connection from %s:%s...' % addr)
        request = sock.recv(1024)
        http_req = HttpRequest()
        http_req.pass_request(request)
        # 发送数据
        sock.send(http_req.get_response())
        sock.close()
    
    # 开启服务器
    def start_server():
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 监听端口:
        s.bind(('127.0.0.1', 9999))
        s.listen(5)
        # print('Waiting for connection...')
        while True:
            # 接受一个新连接:
            sock, addr = s.accept()
            # 创建新线程来处理TCP连接:
            t = threading.Thread(target=tcp_link, args=(sock, addr))
            t.start()
    
    if __name__ == '__main__':
        start_server()
        pass
    • http协议解析和转发
    • 目前区分动态和静态请求仅根据是否是.py结尾
    • 如果是根目录,会自动加上index.html
    • 目前的头部信息十分简陋,仅仅区别是否是图片
    # -*- coding:utf-8 -*-
    import os
    
    
    # 返回码
    class ErrorCode(object):
        OK = "HTTP/1.1 200 OK\r\n"
        NOT_FOUND = "HTTP/1.1 404 Not Found\r\n"
    
    
    # Content类型
    class ContentType(object):
        HTML = 'Content-Type: text/html\r\n'
        PNG = 'Content-Type: img/png\r\n'
    
    
    class HttpRequest(object):
        RootDir = 'root'   # 根目录
        NotFoundHtml = RootDir+'/'+'404.html'  # 404页面
    
        def __init__(self):
            self.method = None
            self.url = None
            self.protocol = None
            self.host = None
            self.request_data = None
            self.response_line = ErrorCode.OK  # 响应码
            self.response_head = ContentType.HTML # 响应头部
            self.response_body = '' # 响应主题
        # 解析请求,得到请求的信息
        def pass_request(self, request):
            request_line, body = request.split('\r\n', 1)
            header_list = request_line.split(' ')
            self.method = header_list[0].upper()
            self.url = header_list[1]
            # print self.url
            self.protocol = header_list[2]
            # 获得请求参数
            if self.method == 'POST':
                self.request_data = {}
                request_body = body.split('\r\n\r\n', 1)[1]
                parameters = request_body.split('\n')   # 每一行是一个字段
                for i in parameters:
                    key, val = i.split('=')
                    self.request_data[key] = val
                self.handle_file_request(HttpRequest.RootDir + self.url)
            if self.method == 'GET':
                file_name = ''
                # 获取get参数
                if self.url.find('?') != -1:
                    self.request_data = {}
                    req = self.url.split('?', 1)[1]
                    file_name = self.url.split('?', 1)[0]
                    parameters = req.split('&')
                    for i in parameters:
                        key, val = i.split('=', 1)
                        self.request_data[key] = val
                else:
                    file_name = self.url
                    # self.handle_keywords_request()
                if len(self.url) == 1:  # 如果是根目录
                    file_name = '/index.html'
                file_path = HttpRequest.RootDir + file_name
                self.handle_file_request(file_path)
    
        # 处理请求
        def handle_file_request(self, file_path):
            # 如果找不到的话输出404
            if not os.path.isfile(file_path):
                f = open(HttpRequest.NotFoundHtml, 'r')
                self.response_line = ErrorCode.NOT_FOUND
                self.response_head = ContentType.HTML
                self.response_body = f.read()
            else:
                f = None
                self.response_line = ErrorCode.OK
                extension_name = os.path.splitext(file_path)[1] # 扩展名
                # 图片资源需要使用二进制读取
                if extension_name == '.png':
                    f = open(file_path, 'rb')
                    self.response_head = ContentType.PNG
                    self.response_body = f.read()
                # 执行CGI,将请求转发到本地的python脚本
                elif extension_name == '.py':
                    file_path = file_path.split('.', 1)[0]
                    file_path = file_path.replace('/', '.')
                    m = __import__(file_path)
                    self.response_head = ContentType.HTML
                    self.response_body = m.main.app(self.request_data)
                # 其他静态文件
                else:
                    f = open(file_path, 'r')
                    self.response_head = ContentType.HTML
                    self.response_body = f.read()
    
        def get_response(self):
            return self.response_line+self.response_head+'\r\n'+self.response_body

    目前只使用了python简单实现,还有很多问题,后续将先完善。
    完整代码见github
    如有错误,欢迎指正~

关键字