多任务版web服务器程序的实现:
web服务器基于TCP服务端开发,其基本构成都是相同的,但有最大一点的差异是:
客户端请求及服务端响应的内容,必须符合html协议,否则将无法获取数据。
- 客户端请求报文格式示例:
# 请求行(还有POST请求方式)
GET / HTTP/1.1\r\n
# 请求体
Host: www.itcast.cn\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n
Cookie: pgv_pvi=1246921728; \r\n
# 空行(不能省略)
\r\n
- 服务端响应报文格式示例:
# 响应行
HTTP/1.1 200 OK\r\n
# 响应体
Server: Tengine\r\n
Content-Type: text/html; charset=UTF-8\r\n
Transfer-Encoding: chunked\r\n
Connection: keep-alive\r\n
Date: Fri, 23 Nov 2018 02:01:05 GMT\r\n
# 空行(不能省略)
\r\n
# 响应体(正文)
…
特别说明:
每项数据之后都要使用:\r\n(见格式示例)
以上内容为web传输与普通TCP传输代码的差异。
本文以多线程web客户端作为代码示例。
当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的请求,防止主线程阻塞。
把创建的子线程设置成为守护主线程,防止主线程无法退出。
-
代码示例
注:代码中使用的html源文件是提前放在代码文件同级目录中static目录下的,你可以在网上找到用于练习的html文件。
import threading
import socket
def handle_recv(new_socket, ip_port):
print("客户端已连接:", ip_port)
# 获取浏览器发送的http请求报文数据并解码
recieve_content = new_socket.recv(4096).decode("utf-8")
print(recieve_content)
# 获取用户请求资源的路径(根据浏览器发送的请求行提取)
path = recieve_content.split(" ",maxsplit=3)[1]
# 指定如果访问根目录时,返回index.html内容
if path == "/":
path = "/index.html"
# 响应行
response_line = "HTTP/1.1 200 OK\r\n"
# 响应头
response_headers = "Content-Type: text/html;charset=utf-8\r\n"
# 加入判断,如请求的路径存在,则正常返回,当客服端请求的路径不存在时,返回错误页面
try:
with open("./static%s" % path, "rb") as f:
response_body = f.read()
except FileNotFoundError:
response_line = "HTTP/1.1 404 NOT FOUND\r\n"
path = "/error.html"
with open("./static%s" % path, "rb") as f:
response_body = f.read()
# 服务端发送符合html要求的数据给客服端
new_socket.send((response_line + response_headers + "\r\n").encode("utf-8") + response_body)
new_socket.close()
if __name__ == '__main__':
# 创建服务端tcp socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 程序退出端口号立即释放
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定端口号
server_socket.bind(("", 8090))
# 设置监听
server_socket.listen(128)
# 循环等待接受客户端的连接请求
while True:
# 主线程负责创建新的套接字接口
new_socket, ip_port = server_socket.accept()
# 当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的请求,防止主线程阻塞。
sub_thread = threading.Thread(target=handle_recv,args=(new_socket, ip_port))
# 把创建的子线程设置成为守护主线程,防止主线程无法退出。
sub_thread.setDaemon(True)
# 启动线程执行对应的任务
sub_thread.start()
运行呜呜段程序,在浏览器中访问http://127.0.0.1:8090/index2.html
服务端窗口返回接收到的浏览器请求数据如下,提取的请求文件路径即是通过第一行的请求行提取:
客户端已连接: ('127.0.0.1', 53729)
客户端已连接: ('127.0.0.1', 53730)
GET /index2.html HTTP/1.1 #请求行
Host: 127.0.0.1:8090
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
GET /web.jpg HTTP/1.1
Host: 127.0.0.1:8090
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
Accept: image/webp,image/apng,image/,/*;q=0.8
Referer: http://127.0.0.1:8090/index2.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
客服端浏览器返回的页面如下:
当输入不存在的页面时:
将返回设定好的404错误页面,可以看到,页面的响应体和状态信息和我们代码中设置的一致。
将代码改写为面向对象编程结构
构造面向对象的代码块,将可以在复用时非常方便的创建新服务器对象。
import threading
import socket
class HTTPWebServer(object):
def __init__(self,port):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(("", port))
server_socket.listen(128)
self.server_socket = server_socket
@staticmethod
def handle_recv(new_socket, ip_port):
print("客户端已连接:", ip_port)
recieve_content = new_socket.recv(1024).decode("utf-8")
print(recieve_content)
path = recieve_content.split(" ",maxsplit=2)[1]
if path == "/":
path = "/index.html"
response_line = "HTTP/1.1 200 OK\r\n"
response_headers = "Content-Type: text/html;charset=utf-8\r\n"
try:
with open("./static%s" % path, "rb") as f:
response_body = f.read()
except FileNotFoundError:
response_line = "HTTP/1.1 404 NOT FOUND\r\n"
path = "/error.html"
with open("./static%s" % path, "rb") as f:
response_body = f.read()
new_socket.send((response_line + response_headers + "\r\n").encode("utf-8") + response_body)
new_socket.close()
def start(self):
while True:
new_socket, ip_port = self.server_socket.accept()
sub_thread = threading.Thread(target=self.handle_recv,args=(new_socket, ip_port))
sub_thread.setDaemon(True)
sub_thread.start()
if __name__ == '__main__':
port = 8080
server = HTTPWebServer(port)
server.start()
小结
这样就完成了一个简单的web服务器,可用于浏览器访问。
- 当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的请求,防止主线程阻塞。
- 使用.setDaemon(True)设置主线程守护,防止主线程无法退出。