前言
不管是哪一种语言的web框架,其核心都是一致的,那就是以http协议为核心,围绕着http请求和http响应这两方面做文章。至于衍生的数据持久化(cookie session 数据库 等)只是存储手段罢了。
如果要理解好http协议,没有什么比实现一个自定义的http client客户端和http server服务器更好的方法了。
这篇博客博主就打算使用多种语言来做同一件事情:实现一个Http server,并且可以像apache那样实现静态资源的访问。
1、Http的请求过程
1.1 HTTP是一个基于TCP/IP通信协议来传递数据
这意味着可以使用建立socket的方式来监听某一端口,比如像tomcat一样监听8080端口,来实现这个web服务器。基于socket来实现client 和 server的交流。
1.2 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,通过http请求可以访问服务器上的HTML 文件, 图片文件, 查询结果等。
可以放几个简单的静态网页作为访问使用
1.3 HTTP三点注意事项:
- HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
- HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。
- HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
socket在处理完一个请求之后应该主动断开连接,然后方便处理下一请求
服务器端向客户端发送数据时,要通过MiME-type指定数据的类型
http不保存事务的处理状态,当然具体的业务逻辑可以使用session或者cookie来保存状态
1.4 HTTP请求图示
这是相对复杂的结构,HTTPServer、CGI我们可以合在一起,为了简单起见,就不需要访问database。数据可以用cache暂存。
大概就是这种结构:
Http server兼任 web server和CGI program的作用。
2、HTTP请求消息
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
这个数据是很有格式的,我们可以通过对
\r\n
的切分就可以简单的在server端取出client端的数据
3、HTTP响应数据
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
2、3 引用自菜鸟教程
使用Python实现一个Http server
第一步,实现一个监听8080端口的socket server,这个server只会连接一次就断开
# -*- coding:utf-8 -*-
import socket
if __name__ == '__main__':
# family: 套接字家族可以使AF_UNIX或者AF_INET
# type: 套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。
s.bind(('localhost', 8080))
while(True):
# 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。
# 该值至少为1,大部分应用程序设为5就可以了。
s.listen(3)
# 被动接受TCP客户端连接,(阻塞式)等待连接的到来
conn, addr = s.accept()
# 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。
# flag提供有关消息的其他信息,通常可以忽略。
request = conn.recv(1024)
# 打印请求内容
print request
# 给客户端返回内容
conn.sendall('welcome!')
# 关闭连接
conn.close()
运行:
测试一下是否跑起来了:
上面是一个简单的脚本,模拟了客户端
服务器成功返回了信息welcome
同时服务器也接收到了信息:
接下来继续扩展
使用浏览器访问server尝试:这是打印出来的request数据
按照
对数据进行切分,新的代码:
# -*- coding:utf-8 -*-
import socket
if __name__ == '__main__':
# family: 套接字家族可以使AF_UNIX或者AF_INET
# type: 套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。
s.bind(('localhost', 8080))
while(True):
# 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。
# 该值至少为1,大部分应用程序设为5就可以了。
s.listen(3)
# 被动接受TCP客户端连接,(阻塞式)等待连接的到来
conn, addr = s.accept()
# 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。
# flag提供有关消息的其他信息,通常可以忽略。
request_content = conn.recv(1024)
# 没有内容的连接,防止keep-alive导致错误断开
if not request_content:
conn.close()
request_split = request_content.split('\r\n')
# 请求行
method, url, http_version = request_split[0].split(' ')
# 请求头
request_headers = {}
for i in range(1, len(request_split)):
if request_split[i] == '':
break
else:
key, value = request_split[i].split(': ')
request_headers[key] = value
# 请求数据
request_body = []
for i in range(2+len(request_headers), len(request_split)):
request_body.append(request_split[i])
request_body = '\r\n'.join(request_body)
# 打包成请求字典
request = {
'addr': addr,
'method': method,
'url': url,
'http_version': http_version,
'headers': request_headers,
'body': request_body
}
print request
# 给客户端返回内容
conn.sendall('welcome!')
# 关闭连接
conn.close()
为了模拟客户端post数据,使用postman进行测试:
结果:
可以看到一个简单的request就被切分出来了
返回response
这个简单,按照格式写就是了:
common_response = '''
HTTP/1.1 200 OK
Content-Type: text/html
<head>
<title>Hello world!</title>
</head>
<html>
<p>this is a easy python http server~</p>
<p>welcome~<p>
</html>
'''.replace('\n', '\r\n')
然后返回回去:
# 给客户端返回内容
conn.sendall(common_response)
使用浏览器访问127.0.0.1:8080
成功了!
写到现在,一个最基础的,能获取请求,能返回信息的httpServer就做好了。但是它现在的功能还是太简单,下一阶段,我们要再加个功能:
- 静态资源的访问
在继续写之前,先把解析请求信息的部分抽离成函数,方便使用:
def parseRequest(request_content):
request_split = request_content.split('\r\n')
# 请求行
method, url, http_version = request_split[0].split(' ')
# 请求头
request_headers = {}
for i in range(1, len(request_split)):
if request_split[i] == '':
break
else:
key, value = request_split[i].split(': ')
request_headers[key] = value
# 请求数据
request_body = []
for i in range(2+len(request_headers), len(request_split)):
request_body.append(request_split[i])
request_body = '\r\n'.join(request_body)
# 打包成请求字典
request = {
'addr': addr,
'method': method,
'url': url,
'http_version': http_version,
'headers': request_headers,
'body': request_body
}
return request
建立一个static目录用来存储静态文件:
index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<link rel="stylesheet" href="all.css">
</head>
<body>
<img src="img.jpg" alt="">
<h1 class="red">首页</h1>
</body>
</html>
all.css:
.red {
color: red;
}
img.jpg
我们首先要能把这些static目录下的文件都读取:
def get_files(files_dir='.'):
""" 获取某个路径所有的文件路径
"""
files_dir = os.path.join(os.getcwd(), files_dir)
files_all = []
def get_files_(files_dir='.', r_path=''):
if files_dir[-1:] != '/':
files_dir += '/'
files = os.listdir(files_dir)
for file in files:
file_path = os.path.join(files_dir, file)
if os.path.isdir(file_path):
get_files_(file_path, file)
else:
if r_path:
files_all.append('%s/%s' % (r_path, file))
else:
files_all.append(file)
get_files_(files_dir)
return files_all
def loadStatic(static_path='static'):
statics = get_files(static_path)
static_path = os.path.join(os.getcwd(), static_path)
statics_dict = {}
# 设置下列文件后缀使用二进制读取
byte_files_suf = ('jpg', 'png')
for file_name in statics:
file_suf = file_name.split('.')[-1]
print file_suf
file_path = os.path.join(static_path, file_name)
if file_suf in byte_files_suf:
file = open(file_path, 'rb')
else:
file = open(file_path, 'r')
statics_dict['/'+file_name] = file.read()
file.close()
return statics_dict
这边做了一个判断,将图片类型的文件使用二进制来进行了读写。
然后会将stataic目录下的所有文件读取,并按照文件相对static的路径-文件内容的键值对返回。
这样我们要做的就很简单了,根据之前获取的request.url,来匹配路由,如果匹配到了静态文件路径,就将其内容返回。
在浏览器中访问http://127.0.0.1:8080/index.html
控制台中打印出了request信息:
根据要求修改__main__
if __name__ == '__main__':
# 加载静态文件
statics = loadStatic()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('localhost', 8080))
while(True):
s.listen(3)
conn, addr = s.accept()
request_content = conn.recv(1024)
# 没有内容的连接,防止keep-alive导致错误断开
try:
request = parseRequest(request_content)
except e:
conn.close()
break
mime_type = {
'jpg': 'image/jpeg',
'png': 'image/png',
'html': 'text/html'
}
file_suf = request['url'].split('.')[-1]
if file_suf in mime_type:
content_type = mime_type[file_suf]
else:
content_type = 'text/html'
response = 'HTTP/1.1 200 OK\r\nContent-Type:%s\r\n\r\n' % content_type
print request['url']
if request['url'] in statics:
print 'match static'
response += statics[request['url']]
# 给客户端返回内容
conn.sendall(response)
# 关闭连接
conn.close()
再次访问http://127.0.0.1:8080/index.html
浏览器显示:
查看f12
到这里一个静态服务器就大功告成啦,哈哈哈哈
我们可以随便往static目录下放文件,然后重启server, 并访问对应的路径,比如,再新建个/img/img_1.jpg:
这张图片长这样:
重新运行easy_http_server_1.py,浏览器访问http://127.0.0.1:8080/img/img_1.jpg
访问成功。
总结
洋洋洒洒,一篇博客接近一万字了。
事实证明,一个简单的http server的实现并不复杂(当然是不追求性能和安全的前提下)。
代码仅仅100行左右。
下一篇博主将会进一步实现一个动态服务器。敬请期待。
博客代码github 链接:https://github.com/numb-men/easy-http-server/blob/master/easy_http_server_1.py
ps:请随便转载
ps:发现错误请评论
ps:python 2.7.15