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

基于python的简单HTTP服务器实现(一) 中,我们实现了一个基础的HTTP服务器,这里的服务器只有简单的响应静态请求,以及最基础的动态请求功能,在这里对原来的工程进行完善。首先规范化响应头,规范区分静态和动态请求,并且增加了session部分,能够支持登陆,识别用户身份。

HTTP服务器实现

主要包括三个部分,响应头规范化,请求处理规范化,以及增加SESSION,主要讲解SESSION的实现。

响应头

在第一节中,仅设置了一个响应头,即标识当前返回的html的类型,但在事实中,一个正常的响应报文含有多个响应头,因此使用一个字典来存储响应头,在返回时转成字符串。

class HttpRequest(object):
    def __init__(self):
        self.head = dict()
# 在设置时使用self.head[key]=value 设置响应头部,发送时转成字符串即可

# 将字典转成字符串
def dict2str(d):
    s = ''
    for i in d:
        s = s + i+': '+d[i]+'\r\n'
    return s

解析请求

对于每一个请求,请求的协议可以参照第一篇。首先解析请求行,得到请求的方法以及请求的文件,然后解析第二行的请求头部信息,存储在一个字典中,接下来解析请求的数据部分。以下是我的具体实现:

  1. 首先对于/的请求,自动添加'index.html
  2. 根据请求的方法,如果是POST自动认为是动态请求,解析请求的数据,然后发送到对应的处理文件上。
  3. 如果是GET请求,如果含有参数,则直接认为是动态请求;否则解析请求文件的后缀名,如果是.py文件,则解析成动态请求。
  4. 对于静态请求,直接返回。
  5. 对于动态请求,动态加载模块,并向模块传递请求参数,获得结果后返回。
    def passRequest(self, request):
        if len(request.split('\r\n', 1)) != 2:
            return
        request_line, body = request.split('\r\n', 1)
        request_head = body.split('\r\n\r\n', 1)[0]     # 头部信息
        self.passRequestLine(request_line)   # 解析请求行
        self.passRequestHead(request_head)   # 解析头部

        # 所有post视为动态请求
        # get如果带参数也视为动态请求
        # 不带参数的get视为静态请求,如果是py文件视为动态请求
        if self.method == 'POST':
            self.request_data = {}
            request_body = body.split('\r\n\r\n', 1)[1]
            #print 'request body ', request_body
            parameters = request_body.split('&')   # 每一行是一个字段
            #print 'parameters ', parameters
            for i in parameters:
                #print 'i', i
                if i=='':
                    continue
                key, val = i.split('=', 1)
                self.request_data[key] = val
            self.dynamicRequest(HttpRequest.RootDir + self.url)
        if self.method == 'GET':
            if self.url.find('?') != -1:        # 含有参数的get
                self.request_data = {}
                req = self.url.split('?', 1)[1]
                s_url = self.url.split('?', 1)[0]
                parameters = req.split('&')
                for i in parameters:
                    key, val = i.split('=', 1)
                    self.request_data[key] = val
                # 执行动态请求
                self.dynamicRequest(HttpRequest.RootDir + s_url)
            else:
                # 这里下一步会判断后缀名,如果是py会转向动态请求
                self.staticRequest(HttpRequest.RootDir + self.url)

# 只提供制定类型的静态文件
    def staticRequest(self, path):
        # print path
        if not os.path.isfile(path):
            f = open(HttpRequest.NotFoundHtml, 'r')
            self.response_line = ErrorCode.NOT_FOUND
            self.response_head['Content-Type'] = 'text/html'
            self.response_body = f.read()
        else:
            extension_name = os.path.splitext(path)[1]  # 扩展名
            extension_set = {'.css', '.html', '.js'}
            if extension_name == '.png':
                f = open(path, 'rb')
                self.response_line = ErrorCode.OK
                self.response_head['Content-Type'] = 'text/png'
                self.response_body = f.read()
            elif extension_name in extension_set:
                f = open(path, 'r')
                self.response_line = ErrorCode.OK
                self.response_head['Content-Type'] = 'text/html'
                self.response_body = f.read()
            elif extension_name == '.py':
                self.dynamicRequest(path)
            # 其他文件不返回
            else:
                f = open(HttpRequest.NotFoundHtml, 'r')
                self.response_line = ErrorCode.NOT_FOUND
                self.response_head['Content-Type'] = 'text/html'
                self.response_body = f.read()

SESSION的实现

SESSION与COOKIE机制

由于HTTP请求是没有状态的,这样服务器就无法对客户端判断身份,但在有的时候,我们需要判断客户端的身份,比如这个客户端已经登陆过,那我们就要提供他的账户信息。在这里,使用的是CookieSession机制来实现的。
当客户端发起请求时,服务器返回的数据会在客户端的浏览器内存储一个Cookie,可以理解为一个随机的字符串;同时服务器也会在自己内部建立一个与之对应的记录,可以理解为一个文件,当服务器动态脚本执行时,就会解析这个文件的信息,生成一个Session,这个Session记录了客户端的信息,可以用于逻辑控制;但客户端再次向服务器请求数据时,就会带着这个Cookie在头部一起提交,服务器生成Session,脚本可以用做逻辑控制。当用户注销登陆或者过期时,服务器对应的文件就会被删除或者设置成失效的标志,达到对应的目的。具体的实现如下:
1. 当客户端请求服务器时,服务器查看它是否带有Cookie头部,如果没有,则为它随机生成一个,如果有,则查找对应的文件解析里面的信息。
2. 当服务器返回给客户端时,就会携带有Cookie信息,在head中有一条Set-Cookie的命令,可以在浏览器上设置Cookie
3. 这样,服务器动态脚本执行时,根据本地的记录文件生成Session,供脚本逻辑使用。

在这里,服务器的Session使用一个XML文件存储,每次解析这个文件生成一个Session对象供动态脚本调用。
同时,会将SessionPOST/GET的数据作为全局变量发送到动态脚本上运行。


class Session(object):
    def __init__(self):
        self.data = dict()
        self.cook_file = None

    def getCookie(self, key):
        if key in self.data.keys():
            return self.data[key]
        return None

    def setCookie(self, key, value):
        self.data[key] = value

    def loadFromXML(self):
        import xml.dom.minidom as minidom
        root = minidom.parse(self.cook_file).documentElement
        for node in root.childNodes:
            if node.nodeName == '#text':
                continue
            else:
                self.setCookie(node.nodeName, node.childNodes[0].nodeValue)        
    # 写入文件
    def write2XML(self):
        import xml.dom.minidom as minidom
        dom = xml.dom.minidom.getDOMImplementation().createDocument(None, 'Root', None)
        root = dom.documentElement
        for key in self.data:
            node = dom.createElement(key)
            node.appendChild(dom.createTextNode(self.data[key]))
            root.appendChild(node)
        print self.cook_file
        with open(self.cook_file, 'w') as f:
            dom.writexml(f, addindent='\t', newl='\n', encoding='utf-8')


    def dynamicRequest(self, path):
        # 如果找不到或者后缀名不是py则输出404
        if not os.path.isfile(path) or os.path.splitext(path)[1] != '.py':
            f = open(HttpRequest.NotFoundHtml, 'r')
            self.response_line = ErrorCode.NOT_FOUND
            self.response_head['Content-Type'] = 'text/html'
            self.response_body = f.read()
        else:
            # 获取文件名,并且将/替换成.
            file_path = path.split('.', 1)[0].replace('/', '.')
            self.response_line = ErrorCode.OK
            m = __import__(file_path)
            # 设置脚本的session
            m.main.SESSION = self.processSession()            
            # 设置脚本的post/get参数
            if self.method == 'POST':
                m.main.POST = self.request_data
                m.main.GET = None
            else:
                m.main.POST = None
                m.main.GET = self.request_data
            self.response_body = m.main.app()            
            self.response_head['Content-Type'] = 'text/html'
            self.response_head['Set-Cookie'] = self.Cookie
  • 动态脚本代码
    从全局变量得到SESSIONPOST/GET参数
SESSION = None
POST = None
GET = None


def app():
    if POST is not None:    # post请求
        print 'POST ', POST
        if ('name' in POST.keys()) and ('password' in POST.keys()) and (POST['name'] == '123') and (POST['password']=='123'):
            SESSION.setCookie('name', '123')
            SESSION.write2XML()
            return 'login success'
        else:
            with open('root/login.html', 'r') as f:
                data = f.read()
            return data
    else:                   # get请求
        if SESSION.getCookie('name') is not None:
            return 'hello, '+SESSION.getCookie('name')
        with open('root/login.html', 'r') as f:
            data = f.read()
        return data
  • 第一次请求
    这里写图片描述

  • 登陆成功
    这里写图片描述

  • 再次刷新,已经登陆,获得用户名
    这里写图片描述

思考

现在想想,一个web开发的框架就是将sessionpost/get等参数封装起来,脚本直接利用这些信息做逻辑处理。在php中,就会有全局的$_POST/$_GET等变量获得提交到这个页面上的数据,道理是一样的。
同时,在路由的选择上,每个框架不尽相同,比如主页是index.py/index.html,对于Model/Action这样的路由是怎么定义的,这些就是框架具体做的,把一个url按照一定的规则映射到对应的脚本上,能够进行更加优雅的开发。

完整代码见github
如有错误,欢迎指正~

猜你喜欢

转载自blog.csdn.net/hu694028833/article/details/80991708