本文先用一个实例详细讲解了web请求与响应的具体过程并说明了web应用的本质,然后带大家由浅入深地手写了几个不同需求下的web服务端程序,帮助大家从底层理解服务端对web请求的处理过程以及web服务的运行原理,最后介绍了如何使用Python的wsgiref模块实现web请求与响应的处理。
web应用的本质
客户端-服务器模型
其实,对于所有的Web应用来说,从本质上讲我们运行web应用程序的地方就是一个socket服务端,而用户的浏览器就是一个socket客户端,我们可以使用Python的socket
模块自己实现一个简单的带并发效果的web服务端:
1 | import socket |
这里,我们可以看到,在接收到浏览器的请求127.0.0.1:8990
后,这个服务器端首先给浏览器(客户端)发送了一个200 OK
的HTTP响应信息,然后发送了字符串Hello WHW!
。
我们先运行这个程序然后在浏览器输入:localhost:8990
,就可以看到服务器发送出的这个“Hello WHW!”字符串:
1 | Hello WHW! |
这里解释一下我们给浏览器发送Hello WHW
之前:conn.sendall(b'HTTP/1.1 200 OK \r\n\r\n')
的意思:
socket是应用层和传输层之间的抽象层,每一层都有协议,所谓的协议协议其实就是固定的消息格式,传输层的消息格式socket已经帮我们封装好了,但是应用层的协议还是需要开发者遵守的,所以在给浏览器发送消息的时候,如果没有按照应用层的消息格式来写,那么你返回给浏览器的信息,浏览器是没法识别的。
而我们web开发用到的应用层的协议都是HTTP协议,所以我们按照HTTP协议规定的消息格式来给浏览器返回消息时浏览器是可以识别的!也就是说,“200 OK”那一行的数据是在告诉浏览器,“请接收并输出我下面发来的数据”,而其实这句话也可以合在一起写:
1 | conn.sendall(b'HTTP/1.1 200 OK \r\n\r\nHello WHW!') |
其实上面这些还涉及到HTTP协议的版本
以及报文格式
的问题;还有我们上面的程序其实还打印了浏览器的请求信息:
1 | GET / HTTP/1.1 |
由于本文篇幅有限这里就不一一详细展开说明了,大家有兴趣的话请参考这篇文章:关于HTTP协议,一篇就够了
接着,我们在函数run_server
的第二个conn中加上有样式效果
的字符串:
1 | conn.sendall(bytes('<h1 style="background-color:red;">Hello WHW!',encoding='utf-8')) |
再看看浏览器的返回结果,上面的样式生效了:
也就是说,浏览器自动将服务器发送给它的字符串按照一定的规则呈现出对应的效果!
web应用本质揭示
(1)当浏览器作为客户端与运行web程序的服务器端进行交互的时候,服务器给浏览器返回的是“字符串”; (2)如果这些“字符串”中有浏览器能够识别的格式,那么浏览器会自动的将这些包含在字符串中的格式解析成用户看着舒服的“效果”;
(3)而要想在浏览器实现我们想要的效果,我们就必须去学习浏览器都有哪些规则;
(4)我们可以将服务器端send的内容先写进一个文件里,然后将这个文件的内容读出来再发给浏览器,而这个文件,大家“约定俗成”的将其命名成后缀为.html的文件,也就是大家熟悉的html文件。所以从web开发者的角度讲,我们需要做的事情大致有以下两点:
(1)按照Html的规则编写Html文件——充当模板
(2)从数据库中获取数据,然后替换到Html文件的数据位置——需要学习web框架
手写web服务
了解了web应用的本质后,接下来带大家一步步地手写web服务!
返回html文件的web框架
上面说到了,如果返回的内容比较多的话,在服务端我们可以将一个html文件返回给浏览器。
准备工作:新建一个html文件:index.html,从本地找到一个图片1.jpg与一个图标文件favicon.ico,并且创建一个css文件存放css样式。
index.html文件的内容如下:
1 |
|
server端的代码如下:
1 | import socket |
whw.css文件中的内容如下:
1 | .content{color:red;} |
我们在浏览器中输入127.0.0.1:8991
看一下结果:alert弹窗与h1标签自带的效果都有,但是网页中没有显示图片与ico图标,css定制的样式也没有呈现!
这是因为:弹窗与h1标签的效果我们随着html文件发送给了浏览器,但是图片、图标与css文件还在server本地,并没有发送给浏览器,浏览器渲染不出来!
其实针对html文件“引用”的静态文件,浏览器会额外发送相应的请求的,看一下Network中的信息大家就明白了:
也就是说:获取index页面浏览器发送请求127.0.0.1:8991
,但是想要获取index页面中的静态文件的话,浏览器会在前面的请求的基础上加上静态文件的名字再向服务器“索取”对应位置的静态文件!
其实这些和标签的属性有有关系,css文件是link标签的href属性:<link rel="stylesheet" href="test.css">
,js文件是script标签的src属性:<script src="test.js"></script>
,图片文件是img标签的src属性:<img src="meinv.png" alt="" width="100" height="100">
,图标ico文件是link标签的属性:<link rel="icon" href="whw.ico">
,其实这些属性都会在页面加载的时候,单独到自己对应的属性值里面取请求对应的文件数据,而且我们如果在值里面写的都是自己本地的路径,那么都会来自己的本地路径来找,如果我们写的是相对路径,就会到我们自己的网址+文件名称,这个路径来找它需要的文件,所以我们只需要在服务接收到这些请求后做出对应的响应,就可以将相应的文件发送给浏览器了!
返回静态文件的web应用
既然浏览器可以根据link标签的href
、img标签的src
、script标签的src
后面的值向服务器端请求对应的文件,那我们完全可以根据这些请求信息将对应为文件发送给浏览器,这样浏览器拿到我们发给它的文件后进行渲染,就可以展现出对应的效果了!对应的服务端程序我们可以这样来写:
1 | import socket |
这时我们再在浏览器中输入127.0.0.1:8991
就可以展示图、图标与效果了!
优化一
我们可以使用函数
与映射
的方式优化一下上面的代码:
1 | # -*- coding:utf-8 -*- |
优化二
当然可以专门为每个传文件的函数开多线程提高效率,代码如下:
1 | # -*- coding:utf-8 -*- |
优化三
在优化二
中我们实现了多线程上传文件
,结合前面实现的多线程接收请求
,两者可以结合起来写,但是考虑到线程安全的问题,不建议大家这样来写:
多线程接收请求结合多线程上传文件
import socket
from threading import Thread
def run_server(conn,opt_lst):
msg = conn.recv(65105).decode(‘utf-8’)
# print(msg)
path = msg.split(‘\r\n\r\n’)[0].split()[1]
print(path)
# 先发响应协议
conn.sendall(b’HTTP/1.1 200 OK \r\n\r\n’)
# 开多线程执行文件操作
t = Thread(target=handle,args=(opt_lst,path,conn))
t.start()
def index(conn):
with open(‘index.html’,’rb’)as f:
data = f.read()
conn.sendall(data)
conn.close()
def ico(conn):
with open(‘favicon.ico’,’rb’)as f:
data = f.read()
conn.sendall(data)
conn.close()
def img(conn):
with open(‘1.jpg’,’rb’)as f:
data = f.read()
conn.sendall(data)
conn.close()
def handle(opt_lst,path,conn):
for i in opt_lst:
if path == i[0]:
i[1](conn)
if name == ‘main‘:
# 处理的函数列表
opt_lst = [
(‘/‘, index),
(‘/favicon.ico’, ico),
(‘/1.jpg’, img),
]
# 初始化socket
server = socket.socket()
#设置端口重复利用
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind((‘127.0.0.1’,8765))
server.listen()
#连接循环
while 1:
conn,addr = server.accept()
# 开线程处理连接
t = Thread(target=run_server, args=(conn,opt_lst))
t.start()
根据不同路径返回独立的页面
根据上面介绍的根据不同的路径返回相应的文件
,我们也可以根据不同的路径返回独立的页面。
之前用到的index.html文件我们不考虑外部文件引入的情况,另外再新建一个home.html文件,同样不考虑外部文件引入的情况。
服务端的代码如下:
1 | # -*- coding:utf-8 -*- |
这样,我们在浏览器中输入127.0.0.1:8765/index
与127.0.0.1:8765/home
就可以看到对应的不同网页了。当然~需要其他的静态文件的话再另外做判断就OK了!
返回“动态”页面的web应用的实现
前面我们返回的都是静态网页
,实际中的网页都是 动态
的——其实所谓的动态网页
是里面有可变的数据!
这里我们用字符串的替换方式来实现这个动态
的需求——利用时间戳来模拟动态的数据。
代码如下:
1 | # -*- coding:utf-8 -*- |
我们需要在index页面中加入这个标签:<h2>123</h2>
,在home页面中加入下面的标签:<h2>456</h2>
,这样的话可以用当前的时间戳代替页面中对应的字符串,实现一下动态
的效果 - -!
最后我们在浏览器中输入127.0.0.1:8765/index
与127.0.0.1:8765/home
并不断刷新页面就可以看到动态
的效果了!
wsgiref版web服务
经过上面的讲解与代码实现,大家肯定感受到了,对于一个web应用来说,浏览器作为客户端是已经成型的了,我们需要自己去实现一个web服务端来处理浏览器的请求并返回正确的响应!
而接下来要介绍的wsgiref
是世界上最nice的框架——Django
(个人认为- -!)内置的一个web服务端,它的作用就是将浏览器的请求进行封装——所有的请求信息都封装到了request
对象中!使用request.path
就能获取到用户这次请求的路径,request.method
就能获取到本次用户请求的请求方式(get还是post)等,使用wsgiref模块
极大的简化了我们写web应用的工作!
对于web后端开发者来说,有了这样的一个wen服务端模块我们不用再去过度的关注浏览器中纷繁复杂的请求信息与厚重的HTTP协议规范了——这样可以将绝大多数的时间放在业务逻辑的处理上!
其实wsgiref
只是基于WSGI
协议下的一个性能比较低的web服务端
,实际生产中部署Django项目的时候我们都会选择性能更好的Uwsgi
模块,当然个人调试的时候wsgiref
是足够了的。
关于WSGI
协议有兴趣的老铁可以参考这篇文章简单看看:Python进阶:何为WSGI协议
利用wsgiref实现一个简单的web服务端程序
接下来我们使用Python的wsgiref
模块实现一个简单的web server程序:
1 | from wsgiref.simple_server import make_server |
启动程序后,在浏览器中输入http://127.0.0.1:8080
就可以看到响应的字符串了:
1 | Hello Web |
关于wsgiref的深入理解建议大家看这篇文章:wsgiref 源码解析
结束语
由于篇幅有限,本文只带大家介绍一下web程序的流程以及服务端是如何处理浏览器请求并将响应正确的返回给浏览器端的,并且最后引出了Python自带的一个web服务端模块——wsgiref
。
下一篇文章将利用wsgiref模块实现一个简单的web框架,带大家深入理解web框架的本质,敬请期待!