目录
Web框架本质
我们可以这样理解:所有的Web应用本质上就是一个socket服务端,而用户的浏览器就是一个socket客户端。 这样我们就可以自己实现Web框架了。
socket服务端
- import socket
- sk = socket.socket()
- sk.bind(("127.0.0.1", 80))
- sk.listen()
- while True:
- conn, addr = sk.accept()
- data = conn.recv(8096)
- conn.send(b"OK")
- conn.close()
可以说Web服务本质上都是在这十几行代码基础上扩展出来的。这段代码就是它们的祖宗。
用户在浏览器中输入网址,浏览器会向服务端发送数据,那浏览器会发送什么数据?怎么发?这个谁来定? 你这个网站是这个规定,他那个网站按照他那个规定,那互联网还能玩么?
所以,必须有一个统一的规则,让大家发送消息、接收消息的时候都有个格式依据,不能随便写。
这个规则就是HTTP协议,以后浏览器发送请求信息也好,服务器回复响应信息也罢,都要按照这个规则来。
HTTP协议主要规定了客户端和服务器之间的通信格式,那HTTP协议是怎么规定消息格式的呢?
让我们首先打印下我们在服务端接收到的消息是什么。
- import socket
- sk = socket.socket()
- sk.bind(("127.0.0.1", 80))
- sk.listen()
- while True:
- conn, addr = sk.accept()
- data = conn.recv(8096)
- print(data) # 将浏览器发来的消息打印出来
- conn.send(b"OK")
- conn.close()
输出:
b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3355.4 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: csrftoken=CtHePYARJOKNx5oNVwxIteOJXpNyJ29L4bW4506YoVqFaIFFaHm0EWDZqKmw6Jm8\r\n\r\n'
我们将\r\n替换成换行看得更清晰点:
- GET / HTTP/1.1
- Host: 127.0.0.1:8080
- Connection: keep-alive
- Cache-Control: max-age=0
- Upgrade-Insecure-Requests: 1
- User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3355.4 Safari/537.36
- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
- Accept-Encoding: gzip, deflate, br
- Accept-Language: zh-CN,zh;q=0.9
- Cookie: csrftoken=CtHePYARJOKNx5oNVwxIteOJXpNyJ29L4bW4506YoVqFaIFFaHm0EWDZqKmw6Jm8
然后我们再看一下我们访问博客园官网时浏览器收到的响应信息是什么。
响应相关信息可以在浏览器调试窗口的Network标签页中看到。
点击view source之后显示如下图:
我们发现收发的消息需要按照一定的格式来,这里就需要了解一下HTTP协议了。
HTTP协议对收发消息的格式要求
每个HTTP请求和响应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。
HTTP响应的Header中有一个 Content-Type表明响应的内容格式。它的值如text/html; charset=utf-8。
text/html则表示是网页,charset=utf-8则表示编码为utf-8。
HTTP GET请求的格式:
HTTP响应的格式:
自定义web框架
经过上面的学习,那我们基于socket服务端的十几行代码写一个我们自己的web框架。我们先不处理浏览器发送的请求,先让浏览器能显示我们web框架返回的信息,那我们就要按照HTTP协议的格式来发送响应。
- import socket
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.bind(('127.0.0.1', 8000))
- sock.listen()
- while True:
- conn, addr = sock.accept()
- data = conn.recv(8096)
- # 给回复的消息加上响应状态行
- conn.send(b"HTTP/1.1 200 OK\r\n\r\n")
- conn.send(b"OK")
- conn.close()
我们通过十几行代码简单地演示了web 框架的本质。
接下来就让我们继续完善我们的自定义web框架吧!
根据不同的路径返回不同的内容
这样就结束了吗? 如何让我们的Web服务根据用户请求的URL不同而返回不同的内容呢?
小事一桩,我们可以从请求相关数据里面拿到请求URL的路径,然后拿路径做一个判断...
- """
- 根据URL中不同的路径返回不同的内容
- """
- import socket
- sk = socket.socket()
- sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口
- sk.listen() # 监听
- while True:
- # 等待连接
- conn, add = sk.accept()
- data = conn.recv(8096) # 接收客户端发来的消息
- # 从data中取到路径
- data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串
- # 按\r\n分割
- data1 = data.split("\r\n")[0]
- url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径
- conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
- # 根据不同的路径返回不同内容
- if url == "/index/":
- response = b"index"
- elif url == "/home/":
- response = b"home"
- else:
- response = b"404 not found!"
- conn.send(response)
- conn.close()
根据不同的路径返回不同的内容--函数版
上面的代码解决了不同URL路径返回不同内容的需求。
我们返回的内容是简单的几个字符,那如果我可以将返回的结果封装成一个函数呢?
- """
- 根据URL中不同的路径返回不同的内容--函数版
- """
- import socket
- sk = socket.socket()
- sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口
- sk.listen() # 监听
- # 将返回不同的内容部分封装成函数
- def func(url):
- s = "这是{}页面!".format(url)
- return bytes(s, encoding="utf8")
- while True:
- # 等待连接
- conn, add = sk.accept()
- data = conn.recv(8096) # 接收客户端发来的消息
- # 从data中取到路径
- data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串
- # 按\r\n分割
- data1 = data.split("\r\n")[0]
- url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径
- conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
- # 根据不同的路径返回不同内容,response是具体的响应体
- if url == "/index/":
- response = func(url)
- elif url == "/home/":
- response = func(url)
- else:
- response = b"404 not found!"
- conn.send(response)
- conn.close()
根据不同的路径返回不同的内容--函数进阶版
看起来上面的代码写了一个函数,那肯定可以写多个函数,不同的路径对应执行不同的函数拿到结果,但是我们要一个个判断路径,是不是很麻烦?我们有简单的办法来解决。
- """
- 根据URL中不同的路径返回不同的内容--函数进阶版
- """
- import socket
- sk = socket.socket()
- sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口
- sk.listen() # 监听
- # 将返回不同的内容部分封装成不同的函数
- def index(url):
- s = "这是{}页面XX!".format(url)
- return bytes(s, encoding="utf8")
- def home(url):
- s = "这是{}页面。。!".format(url)
- return bytes(s, encoding="utf8")
- # 定义一个url和实际要执行的函数的对应关系
- list1 = [
- ("/index/", index),
- ("/home/", home),
- ]
- while True:
- # 等待连接
- conn, add = sk.accept()
- data = conn.recv(8096) # 接收客户端发来的消息
- # 从data中取到路径
- data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串
- # 按\r\n分割
- data1 = data.split("\r\n")[0]
- url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径
- conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
- # 根据不同的路径返回不同内容
- func = None # 定义一个保存将要执行的函数名的变量
- for item in list1:
- if item[0] == url:
- func = item[1]
- break
- if func:
- response = func(url)
- else:
- response = b"404 not found!"
- # 返回具体的响应消息
- conn.send(response)
- conn.close()
返回具体的HTML文件
完美解决了不同URL返回不同内容的问题。 但是我不想仅仅返回几个字符串,我想给浏览器返回完整的HTML内容,这又该怎么办呢?
没问题,不管是什么内容,最后都是转换成字节数据发送出去的。 我们可以打开HTML文件,读取出它内部的二进制数据,然后再发送给浏览器。
- """
- 根据URL中不同的路径返回不同的内容--函数进阶版
- 返回独立的HTML页面
- """
- import socket
- sk = socket.socket()
- sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口
- sk.listen() # 监听
- # 将返回不同的内容部分封装成不同的函数
- def index(url):
- # 读取index.html页面的内容
- with open("index.html", "r", encoding="utf8") as f:
- s = f.read()
- # 返回字节数据
- return bytes(s, encoding="utf8")
- def home(url):
- with open("home.html", "r", encoding="utf8") as f:
- s = f.read()
- return bytes(s, encoding="utf8")
- # 定义一个url和实际要执行的函数的对应关系
- list1 = [
- ("/index/", index),
- ("/home/", home),
- ]
- while True:
- # 等待连接
- conn, add = sk.accept()
- data = conn.recv(8096) # 接收客户端发来的消息
- # 从data中取到路径
- data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串
- # 按\r\n分割
- data1 = data.split("\r\n")[0]
- url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径
- conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
- # 根据不同的路径返回不同内容
- func = None # 定义一个保存将要执行的函数名的变量
- for item in list1:
- if item[0] == url:
- func = item[1]
- break
- if func:
- response = func(url)
- else:
- response = b"404 not found!"
- # 返回具体的响应消息
- conn.send(response)
- conn.close()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>index</title>
</head>
<body>
<div>这是index页面</div>
</body>
</html>