从零搭建简易的Web服务器

本文将使用python套接字编程从零搭建一个简易的web服务器,对应于教材《计算机网络:自顶向下方法》第二章后面套接字编程作业,我们先来看一看客户机(浏览器)和服务器交互的过程中在服务器端发生了哪些事情:

  1. 当一个客户(浏览器)联系服务器时创建一个连接套接字;
  2. 服务器从这个连接接受HTTP请求;
  3. 解释该请求以确定所请求的特定文件;
  4. 从服务器的文件系统获得请求的文件;
  5. 创建一个由请求的文件组成的HTTP响应报文,报文前有首部行;
  6. 经TCP连接向请求的浏览器发送响应;如果文件不存在,则返回404 Not Found差错报文。

1.Web服务器

假设我们通过浏览器向服务器请求的文件是HelloWorld.html,文件内容自定(我这里写的内容就是一句话:太棒了,服务器正常工作!),我们需要将该文件放在与服务器同级的目录下,然后通过浏览器向服务器发起请求,服务器按照上述步骤进行响应。服务器端的全部代码如下:

from socket import *

serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind(('', 6789))
serverSocket.listen(1)
while True:
    print('服务器已就位')
    connectionSocket, addr = serverSocket.accept()
    try:
        message = connectionSocket.recv(1024).decode()
        filename = message.split()[1]
        f = open(filename[1:], encoding='utf-8')
        outputdata = f.read()
        header = 'HTTP/1.1 200 OK\nConnection: close\nContent-Type: text/html\nContent-Length: %d\n\n' % (len(outputdata)+24)
        connectionSocket.send(header.encode())
        for i in range(0, len(outputdata)):
            connectionSocket.send(outputdata[i].encode())
        connectionSocket.send("\r\n".encode())
        connectionSocket.close()
    except IOError:
        header = 'HTTP/1.1 404 Not Found'
        connectionSocket.send(header.encode())
        connectionSocket.close()

bind(('', 6789))指定了套接字与端口号6789绑定,如果代码是运行在本地的,那么只需在浏览器中输入http://localhost:6789/HelloWorld.html即可访问页面;如果代码是部署在云服务器上的,就需要将IP改为服务器的公网IP,通过这种方式,我们可以很容易地在服务器上部署类似个人简介这样的静态网页。

listen(1)指定了服务器在同一时刻只接受一个请求,后续我们将通过多线程编码来同时处理多个请求。

在构造的头部信息中,我们通过Content-Length指定了实体(封装的TCP报文)的长度,即等于数据长度 + TCP头部信息长度,通常TCP的头部信息长度是20字节,但是通过实际观察网页源代码我发现少了四个字节,不难猜测这是因为TCP的选项字段占用了四个字节,因此这里头部信息长度就是24字节。我们甚至不需要自己指定报文长度,只需要返回最基本的HTTP/1.1 200 OK即可。

这里插入一个题外话,关于Content-Length的使用,通过实践我发现存在以下四种情况:

  1. 不显式指定Content-Length,前端页面显示完好,数据完整;
  2. 显示指定Content-Length且小于实体的长度,前端页面显示不完好,数据缺失;
  3. 显示指定Content-Length且等于实体的长度,前端页面显示完好,数据完整;
  4. 显示指定Content-Length且大于实体的长度,前端页面不显示,浏览器控制台报错ERR_CONTENT_LENGTH_MISMATCH

也就是说,最糟糕的情况是指定的长度大于实体的长度,由于长度不匹配,浏览器会报错且前端不显示任何东西。如果指定的长度小于实体长度,浏览器只取消息实体的前面一部分,则前端页面显示不完好。在效果上,不显示指定和显式指定为实体长度都是一样的,如果怕麻烦可以不指定。

2.多线程Web服务器

参照上面的代码,一个最基本的简易Web服务器就搭建好了,但是它在同一时刻只能处理一个请求,现在我们给它升下级,我们使用多线程的方式让它能够同时处理多个请求。具体代码如下:

from socket import *
import threading


def tcp_process(connectionSocket):
    print(threading.current_thread())
    try:
        message = connectionSocket.recv(1024).decode()
        print(repr(message))
        print(message)
        filename = message.split()[1]
        f = open(filename[1:], encoding='utf-8')
        outputdata = f.read()
        header = 'HTTP/1.1 200 OK\nConnection: close\nContent-Type: text/html\nContent-Length: %d\n\n' % (len(outputdata)+24)
        connectionSocket.send(header.encode())
        for i in range(0, len(outputdata)):
            connectionSocket.send(outputdata[i].encode())
        connectionSocket.send("\r\n".encode())
        connectionSocket.close()
    except IOError:
        header = 'HTTP/1.1 404 Not Found'
        connectionSocket.send(header.encode())
        connectionSocket.close()


if __name__ == "__main__":
    serverSocket = socket(AF_INET, SOCK_STREAM)
    serverSocket.bind(('', 6789))
    serverSocket.listen(10)
    while True:
        print('服务器已就位')
        connectionSocket, addr = serverSocket.accept()
        thread = threading.Thread(target=tcp_process, args=(connectionSocket, ))
        thread.start()

从上面可以看出,我们只是将connectionSocket交给一个具体的线程来执行,该线程负责为具体的客户服务,而主进程不必等待它服务完这个用户就可以接受下一个用户的请求,这样就大大提高了服务器的工作效率。

3.客户端

最后我们来看看客户端的代码,通过客户端可以不经过浏览器直接向服务器发起请求。

from socket import *

clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect(('localhost', 6789))
while True:
    header = 'GET /HelloWorld.html HTTP/1.1\nHost: localhost:6789\nConnection: keep-alive\nUser-Agent: Mozilla/5.0\n\n'
    clientSocket.send(header.encode())
    message = clientSocket.recv(1024)
    print(message.decode())

当我通过运行客户端代码向服务器发起请求时,虽然数据成功获取了,但是在客户端也收到了以下错误:

ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。

这个错误一般出现在爬虫过程中,因为抓取信息太过频繁,而被服务器认定为恶意攻击。但是这里显然不是这个原因,在我将服务器代码中的connectionSocket.close()注释掉之后,这个错误就没有了,但是产生错误具体的原因至今未明,如果有知道的同学欢迎在评论区告诉我。

posted @ 2021-08-22 18:42  Marvin-wen  阅读(1872)  评论(0编辑  收藏  举报