《计算机网络——自顶向下方法》课后编程作业,实现web服务器:
开发一个web服务器,一次处理一个HTTP请求。您的web服务器应该接受并解析HTTP请求,从服务器的文件系统中获取请求的文件,创建HTTP响应头和响应体,然后将响应直接发送到客户。如果服务器中不存在请求的文件,则服务器应发送“404 Not Found”消息返回客户端。
创建服务端套接字
指定端口为8888,创建一个服务端TCP套接字,并使用bind()
方法绑定端口(此处bind()
方法的参数应为一个元组)。然后使用listen()方法监听来自客户端的TCP连接请求,参数指定最多与一个客户建立连接。
from socket import *
import os
# 指定端口
port = 8888
# 创建服务端套接字并绑定端口
ServerSocket = socket(AF_INET, SOCK_STREAM)
ServerSocket.bind(('', port))
# 监听端口,指定最多与1个客户建立连接
ServerSocket.listen(1)
定义Content-Type
这里定义了一个包含文件类型对应Content-Type的字典。首先解释一下MIME type——媒体类型,也称为内容类型(content type),是指示文件类型的字符串,与文件一起发送(例如,一个声音文件可能被标记为 audio/x-wav
,一个图像文件可能是 image/png
)。它与传统 Windows 上的文件扩展名有相同目的。媒体类型通常是通过 HTTP 协议,由 Web 服务器告知浏览器的,更准确地说,是通过 Content-Type 来表示的,例如:Content-Type: text/HTML
表示内容是 text/HTML 类型,也就是超文本文件。在超文本传输协议当中,Mime-Type用于指定传输文件的类型。
默认情况下设置为text/html
类型。
# 文件类型对应的mimetype字典
MimeTypes = {
"html": "text/html",
"css": "text/css",
"js": "text/javascript",
"gif": "image/gif",
"ico": "image/x-icon",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"png": "image/png",
"svg": "image/svg+xml",
"json": "application/json",
"pdf": "application/pdf",
"swf": "application/x-shockwave-flash",
"tiff": "image/tiff",
"txt": "text/plain",
"wav": "audio/x-wav",
"wma": "audio/x-ms-wma",
"wmv": "video/x-ms-wmv",
"xml": "text/xml"
}
解析HTTP请求报文
使用accept()
方法接受客户端的TCP连接请求,并返回ConnSocket和ClientAdress两个参数。这里的ConnSocket是一个新的套接字链接,它与前面的ServerSocket套接字不同点在于ServerSocket
是用于服务器端的,用来监听来自客户端的连接请求,并在连接成功后创建一个新的 ConnSocket
对象来处理与该客户端的通信。ServerSocket
只需在服务器端启动一次,然后就可以一直监听客户端的连接请求。ConnSocket
是用于客户端的,用于与服务器建立连接后进行通信。客户端需要在连接服务器之前创建一个 ConnSocket
对象,并指定服务器的地址和端口号。总的来说,ServerSocket
负责监听客户端的连接请求,经过三次握手并创建连接,ConnSocket
则负责与客户端进行通信。三次握手之后,接下来服务端与客户端的数据传输都经过ConnSocket
套接字完成。
之后读取HTTP请求报文,并解析,从报文首行摘取请求方式、资源路径和HTTP版本。
while True:
try:
# 当客户“敲门”时,服务端建立一个新的套接字
ConnSocket, ClientAdress = ServerSocket.accept()
# 从连接套接字获取数据
RequestMsg = ConnSocket.recv(1024)
# 解析http请求
# 将请求分行
RequestLines = RequestMsg.split(b'\r\n')
# 取出第一行
RequestLine = RequestLines[0]
# 将第一行按空格分成三个字符串
# 分别为方法、资源路径和http版本
method, path, version = RequestLine.split()
# 解码
path = path.decode('utf-8')
method = method.decode('utf-8')
从服务端文件系统取出文件
读取文件并获取文件的扩展名,转换为相对应的Mime-Type
# 为get请求
if (method == 'GET'):
# 默认为index.html
if (path == '/'):
path = '/index.html'
# 去除路径的第一个/
path = path[1:]
# 读取文件
with open(path, 'rb') as file:
data = file.read()
# 获取文件的扩展名
FileExtension = os.path.splitext(path)[1]
FileExtension = FileExtension[1:]
# 将文件扩展名转成mime
# 默认是html
try:
MimeType = MimeTypes[FileExtension]
except KeyError:
MimeType = MimeTypes["html"]
发送响应
首先定义响应头,状态码、内容长度和内容类型等等,最后把响应头和响应内容塞入套接字发给客户端。
# 定义响应头
ResponseHeader = "HTTP/1.1 200 OK\r\n"
ResponseHeader += "Connection: close\r\n"
ResponseHeader += f"Content-Type: {MimeType}\r\n"
ResponseHeader += f"Content-Length: {len(data)}\r\n"
ResponseHeader += "\r\n"
ResponseBody = data
'''
send与sendall
send()方法发送的是缓冲区中的一部分数据,如果所有数据都发送成功,send()方法返回发送的字节数
否则,返回-1并且抛出一个错误异常。如果只是发送少量数据,使用send()方法会更加高效。
sendall()方法会尝试将所有数据全部发送,如果所有数据都发送成功,sendall()方法返回None
否则,抛出一个异常。使用sendall()方法时,需要注意,由于sendall()方法会等待所有数据发送完毕,
因此,它可能会占用较长的时间,尤其是当发送的数据较大时。
'''
# 发送响应
ConnSocket.send(ResponseHeader.encode() + ResponseBody)
错误处理
如果发生错误,则返回404的错误提示。
except IOError:
# 返回错误页面
ResponseHeader = "HTTP/1.1 404 Not Found\r\n"
ConnSocket.send(ResponseHeader.encode())
# 关闭TCP连接
ConnSocket.close()
完整代码
from socket import *
import os
# 指定端口
port = 8888
# 文件类型对应的mimetype字典
MimeTypes = {
"html": "text/html",
"css": "text/css",
"js": "text/javascript",
"gif": "image/gif",
"ico": "image/x-icon",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"png": "image/png",
"svg": "image/svg+xml",
"json": "application/json",
"pdf": "application/pdf",
"swf": "application/x-shockwave-flash",
"tiff": "image/tiff",
"txt": "text/plain",
"wav": "audio/x-wav",
"wma": "audio/x-ms-wma",
"wmv": "video/x-ms-wmv",
"xml": "text/xml"
}
# 创建服务端套接字并绑定端口
ServerSocket = socket(AF_INET, SOCK_STREAM)
ServerSocket.bind(('', port))
# 监听端口,指定最多与1个客户建立连接
ServerSocket.listen(1)
print('server running in port 8888')
while True:
try:
# 当客户“敲门”时,服务端建立一个新的套接字
ConnSocket, ClientAdress = ServerSocket.accept()
# 从连接套接字获取数据
RequestMsg = ConnSocket.recv(1024)
# 解析http请求
# 将请求分行
RequestLines = RequestMsg.split(b'\r\n')
# 取出第一行
RequestLine = RequestLines[0]
# 将第一行按空格分成三个字符串
# 分别为方法、资源路径和http版本
method, path, version = RequestLine.split()
# 解码
path = path.decode('utf-8')
method = method.decode('utf-8')
# 为get请求
if (method == 'GET'):
if (path == '/'):
path = '/index.html'
# 去除路径的第一个/
path = path[1:]
# 读取文件
with open(path, 'rb') as file:
data = file.read()
# 获取文件的扩展名
FileExtension = os.path.splitext(path)[1]
FileExtension = FileExtension[1:]
# 将文件扩展名转成mime
# 默认是html
try:
MimeType = MimeTypes[FileExtension]
except KeyError:
MimeType = MimeTypes["html"]
# 定义响应头
ResponseHeader = "HTTP/1.1 200 OK\r\n"
ResponseHeader += "Connection: close\r\n"
ResponseHeader += f"Content-Type: {MimeType}\r\n"
ResponseHeader += f"Content-Length: {len(data)}\r\n"
ResponseHeader += "\r\n"
ResponseBody = data
'''
send与sendall
send()方法发送的是缓冲区中的一部分数据,如果所有数据都发送成功,send()方法返回发送的字节数
否则,返回-1并且抛出一个错误异常。如果只是发送少量数据,使用send()方法会更加高效。
sendall()方法会尝试将所有数据全部发送,如果所有数据都发送成功,sendall()方法返回None
否则,抛出一个异常。使用sendall()方法时,需要注意,由于sendall()方法会等待所有数据发送完毕,
因此,它可能会占用较长的时间,尤其是当发送的数据较大时。
'''
# 发送响应
ConnSocket.send(ResponseHeader.encode() + ResponseBody)
except IOError:
# 返回错误页面
ResponseHeader = "HTTP/1.1 404 Not Found\r\n"
ConnSocket.send(ResponseHeader.encode())
# 关闭TCP连接
ConnSocket.close()
运行效果