在Python实现web服务器入门学习笔记(1)——HTTP协议简介与一次基于HTTP协议的请求应答初体验部分,我们体会了如何通过网络调试助手mNetAssist作为TCP服务器,使用Chrome浏览器作为客户端来发起一次简单的HTTP请求与获取响应的过程。接下来,我们将通过Python代码,使用socket模块,编写一个简单的web服务器,使得其可以根据浏览器的请求返回应答。
一、返回固定页面的HTTP服务器
由于浏览器和服务器之间的数据通过TCP协议传输,因此可以使用socket模块中的socket类来实现一个简单的HTTP服务器。
为简单起见,这里实现的HTTP服务器仅在浏览器发起请求后,返回固定数据给浏览器。
具体代码如下所示,需要注意的是,在准备服务器回复浏览器的数据response时,就像Python实现web服务器入门学习笔记(1)——HTTP协议简介与一次基于HTTP协议的请求应答初体验中提及的一样,应答报文头和报文体之间必须空一行,这在代码中通过字符串"\r\n"来表示。
import socket
def serve_client(new_client_socket):
"""为这个客户端返回数据"""
# 1.接收浏览器发送过来的http请求
request = new_client_socket.recv(1024)
print(request)
# 2.返回http格式的数据给浏览器
# 2.1 准备发送给浏览器的数据-->header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
# 2.2 准备发送给浏览器的数据-->body
response += "<h1>TakingCoding4Granted</h1>"
new_client_socket.send(response.encode("utf-8"))
new_client_socket.close()
def main():
"""用来完成程序整体控制"""
# 1.创建套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.绑定端口
tcp_server_socket.bind(("", 7890))
# 3.变为监听套接字
tcp_server_socket.listen(128)
while True:
# 4.等待新客户端连接
new_client_socket, client_addr = tcp_server_socket.accept()
# 5.为连接上的客户端服务
serve_client(new_client_socket)
# 关闭监听套接字
tcp_server_socket.close()
if __name__ == "__main__":
main()
二、返回需要页面的HTTP服务器
在实际应用中,服务器需要按照浏览器的请求返回对应文件,下面使用一套网上写好的文件
,来实现这一需求。
1. 实现简析
实际上,我们可以在上述代码的基础上,对其进行完善来实现该需求:
- 在函数serve_client()中,当接收到浏览器发来的请求时,获取浏览器希望被返回的资源名称;
- 在服务器上尝试打开该文件,进而读取文件内容;
- 根据服务器组织HTTP应答报文的要求拼装应答报文头和报文体(从文件中读出的数据);
- 将上述拼装好的数据作为应答回复给浏览器。
2. 代码实现
import socket
import re
def serve_client(new_client_socket):
"""为这个客户端返回数据"""
# 6.接收浏览器发送过来的http请求
request = new_client_socket.recv(1024).decode("utf-8")
# 7.将请求报文分割成字符串列表
request_lines = request.splitlines()
print(request_lines)
# 8.通过正则表达式提取浏览器请求的文件名
file_name = None
ret = re.match(r"^[^/]+(/[^ ]*)", request_lines[0])
if ret:
file_name = ret.group(1)
print("file_name:", file_name)
if file_name == "/":
file_name = "/index.html"
# 9.返回http格式的应答数据给浏览器
try:
f = open("./Charisma" + file_name, "rb")
except Exception:
response = "HTTP/1.1 404 NOT FOUND\r\n"
response += "\r\n"
response += "-----file not found-----"
new_client_socket.send(response.encode("utf-8"))
else:
# 9.1 读取发送给浏览器的数据-->body
html_content = f.read()
f.close()
# 9.2 准备发送给浏览器的数据-->header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
# 将response header发送给浏览器--先以utf-8格式编码
new_client_socket.send(response.encode("utf-8"))
# 将response body发送给浏览器--直接是以字节形式发送
new_client_socket.send(html_content)
# 10. 关闭此次服务的套接字
new_client_socket.close()
def main():
"""用来完成程序整体控制"""
# 1.创建套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 通过设定套接字选项解决[Errno 98]错误
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2.绑定端口
tcp_server_socket.bind(("", 7899))
# 3.变为监听套接字
tcp_server_socket.listen(128)
# 定义计数器,记录浏览器发起的请求次数
request_counter = 0
while True:
# 4.等待新客户端连接
new_client_socket, client_addr = tcp_server_socket.accept()
# 成功连接一次,计数器加一
request_counter += 1
print("%d request(s) made!" % request_counter)
# 5.为连接上的客户端服务
serve_client(new_client_socket)
# 关闭监听套接字
tcp_server_socket.close()
if __name__ == "__main__":
main()
3. 代码分析
(1)main()函数中死循环的作用
实际上,通常在浏览器向服务器发出请求(如请求index.html文件)并得到服务器的应答后,浏览器又会自动触发一系列后续请求。因为在该场景下,浏览器是以同步的形式来解析服务器应答的index.html文件,即该文件需要被正确解析,又需要额外的资源,如图片、.css、.js等文件才能完成。
因此,该死循环保证主程序一直保持运行,直到浏览器完成所有请求。
(2)serve_client()函数中的正则的作用
该函数是为了使用一个socket来对浏览器的此次请求进行服务,在浏览器发来的请求报文中,会在报文头第一行包含所需资源的名称和路径,该正则即是为了能够从报文头第一行的字符串中获取浏览器此次请求希望获取的资源文件路径和名称。
- 为了理解正则re.match(r"^[^/]+(/[^ ]*)", request_lines[0])的含义,假设浏览器此次请求的报文头第一行request_lines[0]为’GET /img/logo20.png HTTP/1.1’,则:
- ^[^/]+表示从字符串开始处,匹配到不是/的位置,即该部分可先匹配出’GET ';
- (/[^ ]*)表示先匹配/,然后匹配0个或多个非空格字符,并为此部分匹配使用()进行了分组,则又匹配出了’/img/logo20.png’;
后续通过该正则返回值match object的group()方法,并指定分组参数1,即可取出字符串’/img/logo20.png’。
需要注意的是,正如在文章Python爬虫、后端,使用正则表达式,看这一篇就够了!指出的一样:^除了表示从待匹配字符串开始处进行匹配外,还可以表示取反,如上述^/和^ 分别表示匹配非/和非空格。