從零搭建簡易的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()注釋掉之后,這個錯誤就沒有了,但是產生錯誤具體的原因至今未明,如果有知道的同學歡迎在評論區告訴我。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM