Mini-Web服务器
以下代码和网站模板已经上传github: 代码传送门
一、socket
之前的文章有写过socket的一些东西,这次用来做服务器的底层的也是socket来构建。我们重新回到起点,来谈谈我们应该使用TCP还是UDP来进行通信?
1.1 TCP与UDP的区别
我们知道,UDP是面向无连接的,即发送的信息是在不知道对方是否能接受到消息的情况下。而TCP是面向连接的,即发送信息是在已知对方一定能收到信息的情况下发送的。
并且文件是需要确保每个字节都需要发送到,因此如果选用UDP作为传输协议,那么很可能出现丢包的情况,导致传输过来的文件出现缺失。但是TCP就不会出现这样的情况,它的三次握手会确保客户端与服务器连接,并且确保每个字节都能传到。
下面来整理一下TCP与UDP的区别:
- 面向连接(确认有创建三方交握,连接已创建才作传输。)
- 有序数据传输
- 重发丢失的数据包
- 舍弃重复的数据包
- 无差错的数据传输
- 阻塞/流量控制
1.2 setsockopt
在服务器与客户端断开连接后,通常存在一个时间段,是用来确认被关闭一方能确实收到关闭的信息的,所以这个时候端口是被上一个程序所占用的。因此为了方便,我们需要使用setsockopt来将这个消除。
# 设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,这样就保证了,下次运行程序时 可以立即绑定7890端口
mini_web_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1.3 TCP的创建流程
- 创建套接字
- 绑定ip
- 改主动为被动
- 设置监听
- 接收/发送数据
- 关闭套接字
那接下来,我们就可以根据这个来做一个自己的mini服务器了。当然在写服务器之前,我们需要了解一下HTTP协议。
二、HTTP协议
HTTP协议是网页(客户端)与服务器(服务端)之间的一种连接协议,现在最常用的版本是1.1。具体的在上篇文章有讲,这里就只说一下我们需要用到的两部分,即报文头部和报文主体。报文头部是用来告诉对方自己本次连接自己的一些信息,而报文主体是用来存放传输的数据。
2.1 请求GET格式
GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
每个Header一行一个,换行符是\r\n。
2.2 请求POST格式
POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
当遇到连续两个\r\n时,Header部分结束,后面的数据全部是Body。
2.3 响应格式
200 OK
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
HTTP响应如果包含body,也是通过\r\n\r\n来分隔的。
请再次注意,Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。
当存在Content-Encoding时,Body数据是被压缩的,最常见的压缩方式是gzip,所以,看到Content-Encoding: gzip时,需要将Body数据先解压缩,才能得到真正的数据。压缩的目的在于减少Body的大小,加快网络传输。
三、实现返回固定内容的服务器
首先,我们可以确定需要使用TCP作为文件传输协议,当我们在浏览器上输入127.0.0.1在加上我们绑定的端口,此时就实现了客户端向服务器发送请求报文,当我们的程序收到报文之后,就获得了一个新的socket对象,我们在根据这个对象返回我们想让浏览器显示的内容即可。
3.1代码实现
# -*- coding: utf-8 -*-
import socket
def recv(web_client):
recv_data = web_client.recv(1024).decode("utf-8")
request_header_lines = recv_data.splitlines()
for line in request_header_lines:
print(line)
# 组织相应 头信息(header)
response_headers = "HTTP/1.1 200 OK\r\n" # 200表示找到这个资源
response_headers += "\r\n" # 用一个空的行与body进行隔开
# 组织 内容(body)
response_body = "Worker:summer"
response = response_headers + response_body
web_client.send(response.encode("utf-8"))
web_client.close()
def main():
# 创建套接字
mini_web_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,这样就保证了,下次运行程序时 可以立即绑定7890端口
mini_web_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定端口
mini_web_server.bind(("", 7890))
# 主动改被动
mini_web_server.listen(128)
while True:
# 接受相应
web_client, clint_addr = mini_web_server.accept()
# 返回参数
recv(web_client)
if __name__ == '__main__':
main()
3.2实现页面
3.3收到的请求报文
GET / HTTP/1.1
Host: 127.0.0.1:7890
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36
四、实现返回用户需要的页面
这里主要与上面不同的地方出现在recv的时候,我们知道我们想要的内容是在请求报文的第一段即GET 方法的后面,那我们可以用正则来匹配用户输入的内容即可,然后根据这个内容我们在打开相应的文件然后返回内容即可。如果这个文件存在直接返回即可但是文件不存在,我们可以发送404的响应报文,然后打开404的文件将内容返回即可。
4.1 代码实现
# -*- coding: utf-8 -*-
import re
import socket
# 这里配置服务器
DOCUMENTS_ROOT = "./html"
def recv(clint_socket):
# 获取客户端发送的信息
global f, response_headers
recv_data = clint_socket.recv(1024).decode('utf-8')
# print(recv_data) # 获取该信息的第一行GET / HTTP/1.1
request_header_line_0 = recv_data.splitlines()[0]
# 从客户端发送的信息中获取客户端想要的资源
get_file_name = re.match(r"[^/]+(/[^ ]*)", request_header_line_0).group(1)
# print("clint want file_name is %s" % get_file_name)
if get_file_name == "/":
file_name = DOCUMENTS_ROOT + "/index.html"
else:
file_name = DOCUMENTS_ROOT + get_file_name
try:
# 如果存在资源,将资源发送给对方
f = open(file_name, "rb")
except IOError:
# 如果不存在资源,则发送404响应
response_headers = "HTTP/1.1 404 NOT FOUND\r\n"
page_404 = DOCUMENTS_ROOT + "/404.html"
f = open(page_404, "rb")
else:
response_headers = "HTTP/1.1 200 OK\r\n"
finally:
response_headers += "\r\n"
resopnse_body = f.read()
f.close()
clint_socket.send(response_headers.encode('utf-8'))
# 再发送body
clint_socket.send(resopnse_body)
# 关闭套接字
clint_socket.close()
def main():
# 创建套接字
http_socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,这样就保证了,下次运行程序时 可以立即绑定7890端口
http_socket_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定端口
http_socket_server.bind(("", 7890))
# 改主动为被动
http_socket_server.listen