在基于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
解析请求
对于每一个请求,请求的协议可以参照第一篇。首先解析请求行,得到请求的方法以及请求的文件,然后解析第二行的请求头部信息,存储在一个字典中,接下来解析请求的数据部分。以下是我的具体实现:
- 首先对于
/
的请求,自动添加'index.html
。 - 根据请求的方法,如果是
POST
自动认为是动态请求,解析请求的数据,然后发送到对应的处理文件上。 - 如果是
GET
请求,如果含有参数,则直接认为是动态请求;否则解析请求文件的后缀名,如果是.py
文件,则解析成动态请求。 - 对于静态请求,直接返回。
- 对于动态请求,动态加载模块,并向模块传递请求参数,获得结果后返回。
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
请求是没有状态的,这样服务器就无法对客户端判断身份,但在有的时候,我们需要判断客户端的身份,比如这个客户端已经登陆过,那我们就要提供他的账户信息。在这里,使用的是Cookie
和Session
机制来实现的。
当客户端发起请求时,服务器返回的数据会在客户端的浏览器内存储一个Cookie
,可以理解为一个随机的字符串;同时服务器也会在自己内部建立一个与之对应的记录,可以理解为一个文件,当服务器动态脚本执行时,就会解析这个文件的信息,生成一个Session
,这个Session
记录了客户端的信息,可以用于逻辑控制;但客户端再次向服务器请求数据时,就会带着这个Cookie
在头部一起提交,服务器生成Session
,脚本可以用做逻辑控制。当用户注销登陆或者过期时,服务器对应的文件就会被删除或者设置成失效的标志,达到对应的目的。具体的实现如下:
1. 当客户端请求服务器时,服务器查看它是否带有Cookie
头部,如果没有,则为它随机生成一个,如果有,则查找对应的文件解析里面的信息。
2. 当服务器返回给客户端时,就会携带有Cookie
信息,在head
中有一条Set-Cookie
的命令,可以在浏览器上设置Cookie
3. 这样,服务器动态脚本执行时,根据本地的记录文件生成Session
,供脚本逻辑使用。
在这里,服务器的Session
使用一个XML
文件存储,每次解析这个文件生成一个Session
对象供动态脚本调用。
同时,会将Session
,POST/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
- 动态脚本代码
从全局变量得到SESSION
和POST/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
开发的框架就是将session
,post/get
等参数封装起来,脚本直接利用这些信息做逻辑处理。在php
中,就会有全局的$_POST/$_GET
等变量获得提交到这个页面上的数据,道理是一样的。
同时,在路由的选择上,每个框架不尽相同,比如主页是index.py/index.html
,对于Model/Action
这样的路由是怎么定义的,这些就是框架具体做的,把一个url
按照一定的规则映射到对应的脚本上,能够进行更加优雅的开发。
完整代码见github
如有错误,欢迎指正~