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

2023-01-31 01:01:24 python 服务器 简单

  • Http协议
    • 请求报文
      • 请求头部字段解析
    • 响应报文
      • 响应头部字段解析
      • 响应状态码
  • HTTP服务器实现

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

首先来介绍下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请求时,能够解析请求,并且返回对应的资源给客户端,客户端解析显示到浏览器上。请求分为两种,一种是简单的静态请求,服务器只需要把对应的静态资源返回即可;另外一种是动态请求,服务器自身无法处理,需要将请求转给服务端其他的程序处理,比如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
如有错误,欢迎指正~

相关文章