socket编程实例——实现web服务器

2023-10-21 11:27:40 浏览数 (3)

《计算机网络——自顶向下方法》课后编程作业,实现web服务器:

开发一个web服务器,一次处理一个HTTP请求。您的web服务器应该接受并解析HTTP请求,从服务器的文件系统中获取请求的文件,创建HTTP响应头和响应体,然后将响应直接发送到客户。如果服务器中不存在请求的文件,则服务器应发送“404 Not Found”消息返回客户端。

创建服务端套接字

指定端口为8888,创建一个服务端TCP套接字,并使用bind()方法绑定端口(此处bind()方法的参数应为一个元组)。然后使用listen()方法监听来自客户端的TCP连接请求,参数指定最多与一个客户建立连接。

代码语言:javascript复制
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类型。

代码语言:javascript复制
# 文件类型对应的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版本。

代码语言:javascript复制
while True:
    try:
        # 当客户“敲门”时,服务端建立一个新的套接字
        ConnSocket, ClientAdress = ServerSocket.accept()
        # 从连接套接字获取数据
        RequestMsg = ConnSocket.recv(1024)
        # 解析http请求
        # 将请求分行
        RequestLines = RequestMsg.split(b'rn')
        # 取出第一行
        RequestLine = RequestLines[0]
        # 将第一行按空格分成三个字符串
        # 分别为方法、资源路径和http版本
        method, path, version = RequestLine.split()
        # 解码
        path = path.decode('utf-8')
        method = method.decode('utf-8')

从服务端文件系统取出文件

读取文件并获取文件的扩展名,转换为相对应的Mime-Type

代码语言:javascript复制
# 为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"]

发送响应

首先定义响应头,状态码、内容长度和内容类型等等,最后把响应头和响应内容塞入套接字发给客户端。

代码语言:javascript复制
# 定义响应头
        ResponseHeader = "HTTP/1.1 200 OKrn"
        ResponseHeader  = "Connection: closern"
        ResponseHeader  = f"Content-Type: {MimeType}rn"
        ResponseHeader  = f"Content-Length: {len(data)}rn"
        ResponseHeader  = "rn"
        ResponseBody = data
        '''
        send与sendall

        send()方法发送的是缓冲区中的一部分数据,如果所有数据都发送成功,send()方法返回发送的字节数
        否则,返回-1并且抛出一个错误异常。如果只是发送少量数据,使用send()方法会更加高效。

        sendall()方法会尝试将所有数据全部发送,如果所有数据都发送成功,sendall()方法返回None
        否则,抛出一个异常。使用sendall()方法时,需要注意,由于sendall()方法会等待所有数据发送完毕,
        因此,它可能会占用较长的时间,尤其是当发送的数据较大时。
        '''
        # 发送响应
        ConnSocket.send(ResponseHeader.encode()   ResponseBody)

错误处理

如果发生错误,则返回404的错误提示。

代码语言:javascript复制
except IOError:
        # 返回错误页面
        ResponseHeader = "HTTP/1.1 404 Not Foundrn"
        ConnSocket.send(ResponseHeader.encode())

    # 关闭TCP连接
    ConnSocket.close()

完整代码

代码语言:javascript复制
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'rn')
        # 取出第一行
        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 OKrn"
        ResponseHeader  = "Connection: closern"
        ResponseHeader  = f"Content-Type: {MimeType}rn"
        ResponseHeader  = f"Content-Length: {len(data)}rn"
        ResponseHeader  = "rn"
        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 Foundrn"
        ConnSocket.send(ResponseHeader.encode())

    # 关闭TCP连接
    ConnSocket.close()

运行效果

0 人点赞