手写web框架
重新认识HTTP
http请求报文包含三个部分(请求行 + 请求头 + 请求体)
请求行
请求行包含三个内容:
method + request-URI + http-version
-- 例如
GET /icwork/? Search = product HTTP/1.1
请求方法
请求方法 | 作用 |
---|---|
get | 通过请求URI获得资源 |
post | 用于添加新的资源,用于表单提交 |
put | 用于修改某个内容 |
delete | 删除某个内容 |
connect | 用于代理进行传输例如SSL |
options | 询问可以执行那些方法 |
patch | 部分文档更该 |
propfind | 查看属性 |
proppatch | 设置属性 |
mkcol | 创建集合 |
copy | 拷贝 |
move | 移动 |
lock | 加锁 |
unlock | 解锁 |
trace | 用于远程诊断服务器 |
head | 类似于get,用于检查对象是否存在用于得到元数据 |
get方法和post方法
get
方法是在
url
中说明情请求的资源,比如https://www.baidu.com/con?from=self?_t=1466609839126 其中?后的数据就是请求的数据,并且连接用&,get方法也可以提交表单数据,但是提交的数据在url
中,其他人可以通过查看历史记录中的url
来获取你提交的数据,这样很不安全
post
方法传输数据不在
url
中,而在数据段中出现,并且请求头多了Content-Type 和 Content-Length,post提交表单数据的时候比get方法更安全
post方法提交表单和get方法提交表单相比较
- get明文传输,信息附加在
url
上面,get明文传输,post更加安全- get传输有大小限制,应该是
3k
,post需要制定传输类型- get多用于获取数据,根据get变量的不同调用不同的数据,post多用于提交数据,提交用户输入的数据
get方法和post方法区别
Get是向服务器发索取数据的一种请求,而Post是向服务器提交数据的一种请求
Get是获取信息,而不是修改信息,类似数据库查询功能一样,数据不会被修改
Get请求的参数会跟在url后进行传递,请求的数据会附在URL之后,以?分割URL和传输数据,参数之间以&相连,%XX中的XX为该符号以16进制表示的ASCII,如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密。
Get传输的数据有大小限制,因为GET是通过URL提交数据,那么GET可提交的数据量就跟URL的长度有直接关系了,不同的浏览器对URL的长度的限制是不同的。
GET请求的数据会被浏览器缓存起来,用户名和密码将明文出现在URL上,其他人可以查到历史浏览记录,数据不太安全。在服务器端,用Request.QueryString来获取Get方式提交来的数据
Post请求则作为http消息的实际内容发送给web服务器,数据放置在请求体中,Post没有限制提交的数据。Post比Get安全,当数据是中文或者不敏感的数据,则用get,因为使用get,参数会显示在地址,对于敏感数据和不是中文字符的数据,则用post
POST表示可能修改变服务器上的资源的请求,在服务器端,用Post方式提交的数据只能用Request.Form来获取
请求头
请求头成分
Accept:指浏览器或其他客户可以接爱的MIME文件格式。Servlet可以根据它判断并返回适当的文件格式。
User-Agent:是客户浏览器名称
Host:对应网址URL中的Web名称和端口号。
Accept-Langeuage:指出浏览器可以接受的语言种类,如en或en-us,指英语。
connection:用来告诉服务器是否可以维持固定的HTTP连接。http是无连接的,HTTP/1.1使用Keep-Alive为默认值,这样,当浏览器需要多个文件时(比如一个HTML文件和相关的图形文件),不需要每次都建立连
Cookie:浏览器用这个属性向服务器发送Cookie。Cookie是在浏览器中寄存的小型数据体,它可以记载和服务器相关的用户信息,也可以用来实现会话功能。
Referer:表明产生请求的网页URL。如比从网页/icconcept/index.jsp中点击一个链接到网页/icwork/search,在向服务器发送的GET/icwork/search中的请求中,Referer是http://hostname:8080/icconcept/index.jsp。这个属性可以用来跟踪Web请求是从什么网站来的。
User-Agent:是客户浏览器名称。
Content-Type:用来表名request的内容类型。可以用HttpServletRequest的getContentType()方法取得。
Accept-Charset:指出浏览器可以接受的字符编码。英文浏览器的默认值是ISO-8859-1.
Accept-Encoding:指出浏览器可以接受的编码方式。编码方式不同于文件格式,它是为了压缩文件并加速文件传递速度。浏览器在接收到Web响应之后先解码,然后再检查文件格式。
get方法请求头
Accept:image/webp,image/*,*/*;q=0.8
Accept-Encoding:gzip, deflate, sdch, br
Accept-Language:en-US,en;q=0.8
Connection:keep-alive
Cookie:PSTM=1466499789; BAIDUID=D3A617EE01FFA9DB9B7E3E5F0D3A01EE:FG=1; BIDUPSID=4AA34EC11075CB66B8BC9792DD422B6F; BDUSS=VCc1M0cVQtYnFGfmxTUW5kVTUydnBZUmhiWFRXbnRlMnpIdWV2ODVxNHZ1WkZYQVFBQUFBJCQAAAAAAAAAAAEAAADkEA1ZtPO3rMfRt6zH0cfRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC8salcvLGpXdz; BD_HOME=1; BD_UPN=123353; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; BD_CK_SAM=1; H_PS_PSSID=19292_18286_1458_20318_18241_20369_17942_20388_19690_20417_18560_17001_15560_12277_20253; BDSVRTM=0
Host:www.baidu.com
Referer:https://www.baidu.com/s?wd=http%20%E8%AF%B7%E6%B1%82%E6%95%B0%E6%8D%AE%E7%9A%84%E6%95%B0%E6%8D%AE%E5%8C%85%E6%A0%BC%E5%BC%8F&rsv_spt=1&rsv_iqid=0x9b746a8000022af9&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&rqlang=cn&tn=baiduhome_pg&rsv_enter=1&oq=http%20%E8%AF%B7%E6%B1%82%E6%96%B9%E5%BC%8Fpost%20url%E6%A0%BC%E5%BC%8F&rsv_t=59fb7cEn5xgK8JFpqQ7F7coy6k6dn5sGpEMj1cDM4oMoy0TGArJ2l3fxOqy6F9lXoqoi&inputT=7936&rsv_pq=ca5859d100027005&rsv_sug3=73&rsv_sug1=12&rsv_sug7=100&rsv_sug2=0&rsv_sug4=32020
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36
post方法请求头
Accept:*/*
Accept-Encoding:gzip, deflate, br
Accept-Language:en-US,en;q=0.8
Authorization:Basic WkEtMTE0MjcyNjAyMDY=
Connection:keep-alive
Content-Length:666
Content-Type:application/json
Host:zhihu-web-analytics.zhihu.com
Origin:http://www.zhihu.com
Referer:http://www.zhihu.com/question/41690822
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36
Request Payload
view source
相比之下多了content-Type 和 Content-Length
Content-Type:表示的是请求报文体的 MIME 类型
注:GET的请求消息体是空的 所以不需要指定消息体的MIME类型
Content-Length:表示的是 post的数据的长度
举例说明
1 GET/sample.jspHTTP/1.1
2 Accept:image/gif.image/jpeg,*/*
3 Accept-Language:zh-cn
4 Connection:Keep-Alive
5 Host:localhost
6 User-Agent:Mozila/4.0(compatible;MSIE5.01;Window NT5.0)
7 Accept-Encoding:gzip,deflate
8
9 username=ccsvip&password=1234
第一行为
http
请求行,包含方法,URI 和http
版本1-7为请求头,包含浏览器,主机,接受的编码方式和压缩方式
第8行表示一个空行 表示请求头结束 这个空行是必须的
第9行是数据体,比如是需要查询的信息。
请求体
http
响应由三个部分组成分别是状态行,响应头,响应正文
状态行
状态行是由:HTTP-Version + Status-Code + Reason-Phrase
比如:HTTP/1.1 200 ok
分别表示:http版本 + 状态码 + 状态代码的文本描述
响应状态码
状态码类型 | 表达意义 |
---|---|
1xx |
指示信息–表示请求已接收,继续处理 |
2xx |
成功–表示请求已被成功接收、理解、接受 |
3xx |
重定向–要完成请求必须进行更进一步的操作。 |
4xx |
客户端错误–请求有语法错误或请求无法实现。 |
5xx |
服务器端错误–服务器未能实现合法的请求。 |
响应头
响应头:包含服务器类型,日期,长度,内容类型等
Server:Apache Tomcat/5.0.12
Date:Mon,6Oct2003 13:13:33 GMT
Content-Type:text/html
Last-Moified:Mon,6 Oct 2003 13:23:42 GMT
Content-Length:112
响应正文
响应正文响应正文就是服务器返回的HTML页面,HTTP响应头与响应正文之间也必须以空行分隔
正文部分的MIME类型:HTTP请求及响应的正文部分可以是任意格式的数据,如何保证接收方能看得懂发送方发送的正文数据呢?HTTP协议采用MIME协议来规范正文的数据格式。
文件扩展名 | MIME类型 |
---|---|
.bin .exe .o .a .z | application/octet-stream |
application/pdf | |
.zip | application/zip |
.tar | application/x-tar |
.gif | image/gif |
.jpg .jpeg | image/jpeg |
.htm .html | text/html |
.text .c .h .txt .java | text/plain |
.mpg .mpeg | video/mpeg |
.xml | application/xml |
表单上传文件 | multipart/form-data |
<form>标签的enctype属性:用于指定表单数据的MIME类型,取值为multipart/form-data,表示表单数据为复合类型的数据,包含多个子部分。
手写一个web框架
如果需要响应中文字符,需要按下面的格式
f'HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{response_body}'
浏览器作为客户端进行
第一版
from socket import *
while True:
server = socket()
PORT = 8080
IP = 'localhost'
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind((IP, PORT))
server.listen()
conn, addr = server.accept()
try:
content = conn.recv(1024)
print(f"收到来自客户端的消息:\n{content.decode()}")
response_body = "这是来自服务端的消息,你好,我是小满!"
response = f'HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{response_body}'
conn.send(response.encode())
finally:
conn.close()
server.close()
第二版
基于不同的路径地址返回不同的结果
from socket import *
while True:
server = socket()
PORT = 8080
IP = 'localhost'
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind((IP, PORT))
server.listen()
conn, addr = server.accept()
try:
content = conn.recv(1024).decode()
print(f"收到来自客户端的消息:\n{content}")
# 获取到请求的路径
path = content.split()[1]
if path == '/login':
response_body = "你好小满,我是服务器!"
elif path == "/register":
response_body = "小满,您已经成功注册"
else:
response_body = "404 Error"
response = f'HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{response_body}'
conn.send(response.encode())
finally:
conn.close()
server.close()
打印请求信息
GET /login HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
第三版
基于
wsgiref
模块搭建web框架,不需要自己手写,这个模块是python
自带的模块,直接使用就行了。
from wsgiref import simple_server
def register():
return '小满,您已经注册成功啦!'
def login():
return '你好!小满,您已经成功登录啦!'
def other():
return '404 page not found'
path_dict = {
'/login': login,
'/register': register
}
def run(request, response):
"""
:param request: 请求相关的数据
:param response: 响应相关的数据
:return: 返回给客户端的展示数据
"""
# request 得到的是客户端发送来的字典数据
# 路径存放在这个字典的 PATH_INFO 这个键里面,我们把它取到就可以根据不同路径进行不同的处理了
path = request.get('PATH_INFO')
# 此处是固定的写法,不需要纠结
# 列表内如果不这样写,中文会乱码的
response('200 OK', [('Content-Type', 'text/plain; charset=utf-8')])
try:
if path in path_dict:
data = path_dict[path]()
return [data.encode()]
else:
return [other().encode()]
except KeyboardInterrupt:
return ['服务器已断开连接'.encode()]
if __name__ == '__main__':
IP = '127.0.0.1'
PORT = 8088
server = simple_server.make_server(IP, PORT, run)
print('\033[31m服务器已建立,等待客户端连接中。。\033[0m')
server.serve_forever()
第四版
模块化,在当前项目创建两个文件夹
urls
,views
:
urls
:存放功能函数字典,字典里面存放客户端请求的对应路径,以及路径运行的处理函数内存地址
views
:路径对应的功能函数,即业务逻辑的编写
# urls.py
import views
path_dict = {
'/login': views.login,
'/register': views.register
}
# views.py
def register():
return '小满,您已经注册成功啦!'
def login():
return '你好!小满,您已经成功登录啦!'
def other():
return '404 page not found'
# 主函数
from wsgiref import simple_server
import views
import urls
def run(request, response):
path = request.get('PATH_INFO')
response('200 OK', [('Content-Type', 'text/plain; charset=utf-8')])
try:
if path in urls.path_dict:
data = urls.path_dict[path]()
return [data.encode()]
else:
return [views.other().encode()]
except KeyboardInterrupt:
return ['服务器已断开连接'.encode()]
if __name__ == '__main__':
IP = '127.0.0.1'
PORT = 8088
server = simple_server.make_server(IP, PORT, run)
print('\033[31m服务器已建立,等待客户端连接中。。\033[0m')
server.serve_forever()
遇到的问题
第四版已经很好了,不过可能会频繁的使用到不同的
html
页面
模块优化
templates
文件夹
为了避免文件类型的混乱 单独开设一个文件夹存储应用程序的 HTML 模板文件,这个文件夹就是
templates
这些模板文件包含了应用程序的页面结构、布局和内容,使开发者能够更好地组织和维护页面。
static
文件夹
static 文件夹通常用于存放静态资源文件,如
CSS
样式表、JavaScript 脚本文件、图像等。这些静态资源文件不会被您所使用的框架渲染,而是直接提供给浏览器以加载和显示页面。
通过将 HTML 模板文件放置在 templates 文件夹、静态资源文件放置在 static 文件夹,可以使框架应用更加清晰地组织和管理页面结构和静态资源,便于开发和维护。