「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」。
前言
在我们开发过程中,不会选择使用自己的 Web 框架。但是我们通过实现 Web 框架来来理解 Web 框架的运行原理还是非常有意义的。是现实不管时 Django 还是 Flask 框架,实现的思路都是一样的。今天我们就通过底层的 Socket 开自己实现一个微型的服务器。
Socket 的简单使用
Socket 就是我们常说的套接字,我们所有的网络请求都是通过它实现的。在网络请求中,通常或包含一个服务器一个客户端,在这一节我们就实现一个简单的 CS 架构。
服务器:
import socket
# 创建 Socket 对象
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定 ip 和端口
server.bind(('127.0.0.1', 8000))
# 监听
server.listen(1)
# 接收连接
conn, addr = server.accept()
# 接收数据
data = conn.recvfrom(1024)
# 数据解码
print(data[0].decode('utf-8'))
# 关闭连接
conn.close()
其中 accept 是阻塞式的,它会一直等待客户端发送数据,在接收到数据后才会继续往下执行。在网络传输时,我们的数据都是以二进制形式传输的,所有我们需要对数据进行解码。
客户端:
import socket
# 创建 Socket 对象
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务器
client.connect(('127.0.0.1', 8000))
# 给服务器发送数据
client.send('你好'.encode('utf-8'))
我们先运行服务器,会发现服务器在等待。然后再执行客户端,此时服务器接收到了数据,会输入如下结果:
你好
Web 服务器的原理和上面是一样的。但是因为浏览器为我们实现了客户端的操作,因此我们只需要实现服务端的业务即可。
服务端编写
在实际运行时,我们的服务器是不间断的执行。而上面的程序只接受了一次请求就停止了,下面我们来完善一下服务端的代码:
import socket
# 运行服务器
def run_server():
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
conn, addr = server.accept()
data = conn.recv(1024)
conn.send(b"Do not go gentle into that good night!")
conn.close()
if __name__ == '__main__':
run_server()
这样我们就可以保证服务器一直运行了。现在我们把服务器运行起来,然后打开 IE 浏览器输入我们的 IP + 端口:127.0.0.1:8000 就能看到如下页面:
这样我们的服务器就实现了基本的功能。但是如果你打开两个页面,会发现第二个页面会一直等待。这是因为第一个连接并没有关闭,服务器不会继续连接。所以我们需要使用多线程来让服务器接收多个连接。下面是修改后的代码:
import socket
from threading import Thread
def connect(conn):
data = conn.recv(1024)
conn.send(b'Do not go gentle into that good night!')
conn.close()
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
print("来了一个连接")
conn, addr = server.accept()
t = Thread(target=connect, args=(conn, ))
t.start()
if __name__ == '__main__':
run_server()
我们运行上面的代码,发现问题得到了解决。这样我们服务器的雏形算是出来了。
路由系统
现在我们可以尝试在地址栏输入一些其它东西,比如:http://127.0.0.1:8000/index.html、或者 http://127.0.0.1:8000/login.html。会发现都会显示一个页面。而不同 URL 对应不同页面就是路由系统的工作了。
首先我们的第一个问题是如何拿到用户的 URL,在服务端连接的时候,我们读取了一个 data 参数,我们来看一下它的内容:
b'GET / HTTP/1.1\r\nAccept: text/html, application/xhtml+xml, image/jxr, */*\r\nAccept-Language: zh-Hans-CN,zh-Hans;q=0.5\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko\r\nAccept-Encoding: gzip, deflate\r\nHost: 127.0.0.1:8000\r\nConnection: Keep-Alive\r\n\r\n'
把上面的内容简单整理一下可以看到下面的效果:
b'GET / HTTP/1.1
\r\n
Accept: text/html, application/xhtml+xml, image/jxr, */*
\r\n
Accept-Language: zh-Hans-CN,zh-Hans;q=0.5
\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko
\r\n
Accept-Encoding: gzip, deflate
\r\n
Host: 127.0.0.1:8000
\r\n
Connection: Keep-Alive
\r\n
\r\n'
其中最前面的 b 表示数据是二进制形式。然后我们看一下后面的内容。可以看到每一段都有一个 \n\r 作为分隔符。而且每个数据直接的关系大致如 key:value 形式。这个就是我们常说的请求头,请求头的格式是由网络协议规定的,在我们知道请求头的规范后我们就可以从请求头中解析出我们需要的数据。
import socket
from threading import Thread
def connect(conn):
data = conn.recv(1024)
headers = data.decode('utf-8')
# 解析出请求头的请求地址和请求方式
headers = headers.split('\r\n')
method, url, _ = headers[0].split(' ')
if method == 'GET':
print("处理 get 请求")
elif method == 'POST':
print("处理 post 请求")
print("请求的 url:", url)
conn.send(b'Do not go gentle into that good night!')
conn.close()
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
print("来了一个连接")
conn, addr = server.accept()
t = Thread(target=connect, args=(conn, ))
t.start()
if __name__ == '__main__':
run_server()
在上面的代码中我们解析出了不同的 URL 和请求方式,但是现在还不能对不同的页面进行不同的响应,所以我们需要继续改造。
import socket
from threading import Thread
def index(method):
return "index 页面"
def login(method):
return "登录页面"
def register(method):
return "注册页面"
urls = {
'/index.html': index,
'/login.html': login,
'/register.html': register
}
def connect(conn):
data = conn.recv(1024)
headers = data.decode('utf-8')
headers = headers.split('\r\n')
method, url, _ = headers[0].split(' ')
# 获取返回数据的函数
resp = urls.get(url)(method)
conn.send(resp.encode('utf-8'))
conn.close()
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
print("来了一个连接")
conn, addr = server.accept()
t = Thread(target=connect, args=(conn, ))
t.start()
if __name__ == '__main__':
run_server()
在上面的代码中,我们编写了许多函数用来处理不同的 URL。然后把每个函数放在一个字典中,这样就可以针对不同 URL 返回不同结果。同时把请求方式传了过去,这样我们就可以在函数内对不同请求进行处理。
请求网页
不过这样的响应内容非常单调,我们浏览器访问的都是网页。我们可以写一个 HTML,然后把读取 HTML 文本再作为响应发送给用户,下面是我们的 HTML 页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p style="color: red">我是 index 页面</p>
</body>
</html>
页面十分简单,然后我们针对 index 函数写一个响应页面:
import socket
from threading import Thread
def index(method):
with open('index.html', 'r') as f:
content = f.read()
return content
def login(method):
return "登录页面"
def register(method):
return "注册页面"
urls = {
'/index.html': index,
'/login.html': login,
'/register.html': register
}
def connect(conn):
data = conn.recv(1024)
headers = data.decode('utf-8')
headers = headers.split('\r\n')
method, url, _ = headers[0].split(' ')
# 获取返回数据的函数
resp = urls.get(url)(method)
conn.send(resp.encode('utf-8'))
conn.close()
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
print("来了一个连接")
conn, addr = server.accept()
t = Thread(target=connect, args=(conn, ))
t.start()
if __name__ == '__main__':
run_server()
我们在 index 函数中直接读取内容,然后将内容返回给页面,显示的效果就是一个网页。
模板
上面我们使用本地的 HTML 直接返回给用户,这样就会有一些不足。因为如果我们需要展示一些信息需要在 HTML 中写好。这个时候我们就需要用到模板了。模板是一种含有特殊符号的模板,我们可以把这些特殊符号动态替换,这样就可以更加灵活的显示页面,比如下面这个页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p style="color: red">hello/% name %/</p>
</body>
</html>
在页面中,我们添加了一个 /% name %/ 标记。我们可以在返回给用户前对这些特殊符号进行处理,比如下面这段代码:
import re
import socket
from threading import Thread
def index(method):
with open('index.html', 'r') as f:
content = f.read()
name = "zack"
marks = re.findall('/% (.*?) %/', content)
for mark in marks:
content = content.replace("/% " + mark + " %/", eval(mark))
return content
def login(method):
return "登录页面"
def register(method):
return "注册页面"
urls = {
'/index.html': index,
'/login.html': login,
'/register.html': register
}
def connect(conn):
data = conn.recv(1024)
headers = data.decode('utf-8')
headers = headers.split('\r\n')
method, url, _ = headers[0].split(' ')
# 获取返回数据的函数
resp = urls.get(url)(method)
conn.send(resp.encode('utf-8'))
conn.close()
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
print("来了一个连接")
conn, addr = server.accept()
t = Thread(target=connect, args=(conn, ))
t.start()
if __name__ == '__main__':
run_server()
在我们获取到 HTML 的内容后,通过正则表达式获取页面中的特殊元素,然后再将其替换掉。这样我们就能更加灵活的显示页面。这样我们就实现了一个比较简单的服务器。
静态网站和动态网站
其实我们直接返回的 HTML 页面就是一个静态页面,这个时候的页面比较单调。数据都是本地写好的,而当我们使用模板作为页面的基础然后通过代码来渲染的话。这个时候数据是动态显示的,这时的网站就是动态网站。
因为我们的数据是通过代码获取的,这个时候我们就可以从数据库获取数据,然后再渲染到页面。
Web 框架
上面我们使用 Socket 实现了一个 Web 服务器。也可以算是一个 Web 框架,对比传统的的 Web 框架。我们实现了 Socket 服务端、路由系统、模板引擎三个主要部分。
但是一个完整的 Web 框架还会提供更多的东西。比如 ORM 操作,中间件等。大家可以参照 Django 来继续完善一个 Web 服务器。本次 Chat 到这里就结束了。
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。