第十七篇:WEB服務器之HTTP協議


 

  本篇主要為為了實現WEB服務器,其中包含了HTTP協議的理解,以及TCP的三次握手、四次揮手等方面相關知識,同時還包含了關於web瀏覽器與服務器之間的通信過程。

一、web瀏覽器

  通常在我們上網時會在瀏覽器的地址欄輸入網址,①、瀏覽器首先要對URL進行解析,②、隨后通過HTTP協議定義消息內容和步驟,即規定發送請求的格式;③、根據服務器的域名通過操作系統下的解析器(DNS客戶端)向最近的DNS服務器發送請求獲取目標服務器的IP地址並存儲在指定的內存空間內,,通過操作系統下的協議棧以及socket庫將消息發送出去,④、當服務器接收到請求消息會返回響應消息(該響應消息也是根據HTTP協議定義消息內容的格式)最后經過類似的過程返回給web瀏覽器。接下來我們根據這幾個步驟進行解析:

  1.瀏覽器怎么對URL進行解析?

 通常常用的訪問數據的機制有以下幾種:

  HTTP協議:即 Hypertxt Transfer Protocal 超文本傳送協議)訪問Web服務器,例如:http://www.baidu.com/dir/file1.html

  FTP協議:File Transfer Protocol,文件傳輸協議,主要用於文件的上傳和下載,例如:ftp://ftp.glasscom.com/dir/file1.html

  File協議:本地文件傳輸協議,例如:file://localhost/c:path/file1.zip。

  maito協議:該協議可以創建一個指向電子郵件地址的超級鏈接,通過該鏈接可以在Internet中發送電子郵件。例如:maito.tone@glasscom.com

  等等

 2、根據HTTP協議生成怎樣格式的請求消息和接收的響應消息?

  需要我們需要知道的是:

  HTTP協議:

   我們知道服務端和客戶端之間進行通信過程便是:首先客戶端根據HTTP協議給服務端發送請求消息,隨后服務器給客戶端發送響應消息。那么請求消息和響應消息具體是什么樣的呢?

  請求消息:

 1 # 以下便是瀏覽器給服務器發送的請求消息
 2 
 3 GET / HTTP/1.1
 4 Host: www.baidu.com
 5 Connection: keep-alive
 6 Cache-Control: max-age=0
 7 Upgrade-Insecure-Requests: 1
 8 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
 9 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
10 Accept-Encoding: gzip, deflate, br
11 Accept-Language: zh-CN,zh;q=0.9
12 
13 #接下來對這些信息進行解析:
  第一部分:請求頭行,包含請求類型、URI、HTTP協議版本;
請求信息類型通常有:get、post、put等等;
  第二部分:即緊跟第一行之后的,請求頭部,包含服務器所使用的說明信息;接下來解釋一下這些說明信息的意思:
1、host:請求web服務器的域名地址
2、Connection: 表示是否持久連接;即keep-alive表示持久連接;
3、Cache-Control:指定請求和響應的緩存機制;no-cache(不能緩存)、no-store(在請求消息中發送將使得請求和響應消息都不使用緩存)、
  max-age(客戶機可以接收生存期不大於指定時間(以秒為單位)的響應)、max-stale(客戶機可以接收超出超時期間的響應消息)、min-fresh(客戶機可以接收響應時間小於當前時間加上指定時間的響應)、
  only-if-cached等等
4、User-Agent: HTTP協議運行的瀏覽器類型的詳細信息;比如:谷歌/67.0.3396.99
5、Accept: 指瀏覽器可以接收的內容類型;
6、Accept-Encoding: 客戶端瀏覽器可以支持的web服務器返回內容壓縮編碼類型;
7、Accept-Language:瀏覽器支持的語言類型,
8、Cookie: 某些網站為了辨別用戶身份、進行 session 跟蹤而儲存在用戶本地終端上的數據(通常經過加密);例如當我們上網時,某些網站能准確的推送我們想要的信息。
  第三部分:"\r\n" --> 分割header和body部分的分界線

   響應消息:

 1 # 響應消息
 2 
 3 HTTP/1.1 200 OK
 4 Bdpagetype: 1
 5 Bdqid: 0x8bda58760001baca
 6 Cache-Control: private
 7 Connection: Keep-Alive
 8 Content-Encoding: gzip
 9 Content-Type: text/html
10 Cxy_all: baidu+10412ee70bbb9e9eec33f3dbcb3e2df7
11 Date: Wed, 18 Jul 2018 03:26:13 GMT
12 Expires: Wed, 18 Jul 2018 03:25:42 GMT
13 Server: BWS/1.1
14 Set-Cookie: BDSVRTM=0; path=/
15 Set-Cookie: BD_HOME=0; path=/
16 Set-Cookie: H_PS_PSSID=1435_21118_20929; path=/; domain=.baidu.com
17 Strict-Transport-Security: max-age=172800
18 Vary: Accept-Encoding
19 X-Ua-Compatible: IE=Edge,chrome=1
20 Transfer-Encoding: chunked
21 
22 # 響應消息的解析:
  第一部分:響應頭,包含:HTTP協議版本、狀態碼(1XX-告知請求處理進度和情況,2XX-成功,3XX-表示需要進一步操作,4XX-客戶端錯誤;5XX-服務器錯誤;)
  第二部分:響應頭部,包含服務器發送的附加信息;這里針對幾個重要的進行解析說明:
  https://www.cnblogs.com/mylanguage/p/5689879.html -->有詳細說明。
  第三部分:"\r\n" --分割header和body的分割線
  第四部分:包含服務器向客戶端發送的數據。

  以上就請求消息和響應消息的內容格式,由瀏覽器或者客戶端將信息根據HTTP協議轉換而來。

 

 3、怎么根據域名獲取服務器的IP地址?

  前面便提到了要想實現兩台終端之間的通信必須知道對方的IP地址和端口號,但是我們在瀏覽器地址欄輸入的僅是域名,那么我們怎么獲得目標服務器的IP地址呢?

  首先web瀏覽器會調用操作系統下的解析器即DNS客戶端,隨后由解析器發送請求給最近的DNS服務器(發送過程與C/S架構模型一樣),若所需域名不在最近的DNS服務器,則由該服務器向域的DNS服務器發送詢查消息,若該域名不在根域DNS服務器上,則讓最近DNS服務器向其下級域發送詢查消息,以此遞歸便能查找到該域名所在域的DNS服務器,最后由請求的DNS服務器發送響應消息到最近的DNS服務器,得到該域名的IP地址;

  同時DNS服務器有一個緩存功能,可以記住之前查詢過的域名,如果所查詢的域名和相關信息已經在緩存中,那么久可以直接返回響應。

 


 

二、TCP的三次握手和四次揮手

  首先我們需要知道在客戶端與服務器基於TCP協議建立聯系時需要經過三次握手,而在斷開連接時需經歷四次揮手的過程,那么我們來看一下該過程是怎樣的?

  三次握手:

    

 

 解析:

  首先服務器通常是處於監聽的狀態,而客戶端通常是主動建立連接的一方,即

  1. TCP服務器進程先創建傳輸控制塊TCB,時刻准備接受客戶進程的連接請求,此時服務器就進入了LISTEN(監聽)狀態;
  2. TCP客戶進程也是先創建傳輸控制塊TCB,然后向服務器發出連接請求報文,這是報文首部中的同部位SYN=1,同時選擇一個初始序列號 seq=x ,此時,TCP客戶端進程進入了 SYN-SENT(同步已發送狀態)狀態。TCP規定,SYN報文段(SYN=1的報文段)不能攜帶數據,但需要消耗掉一個序號。
  3. TCP服務器收到請求報文后,如果同意連接,則發出確認報文。確認報文中應該 ACK=1,SYN=1,確認號是ack=x+1,同時也要為自己初始化一個序列號 seq=y,此時,TCP服務器進程進入了SYN-RCVD(同步收到)狀態。這個報文也不能攜帶數據,但是同樣要消耗一個序號。
  4. TCP客戶進程收到確認后,還要向服務器給出確認。確認報文的ACK=1,ack=y+1,自己的序列號seq=x+1,此時,TCP連接建立,客戶端進入ESTABLISHED(已建立連接)狀態。TCP規定,ACK報文段可以攜帶數據,但是如果不攜帶數據則不消耗序號。
  5. 當服務器收到客戶端的確認后也進入ESTABLISHED狀態,此后雙方就可以開始通信了。

  

  四次揮手:

   同時我們需要知道的是:通常服務器不會主動斷開連接,而是客戶端主動斷開連接。

    

 

  1、客戶端進程發出連接釋放報文,並且停止發送數據。釋放數據報文首部,FIN=1,其序列號為seq=u(等於前面已經傳送過來的數據的最后一個字節的序號加1),此時,客戶端進入FIN-WAIT-1(終止等待1)狀態。 TCP規定,FIN報文段即使不攜帶數據,也要消耗一個序號。

  1.   2、服務器收到連接釋放報文,發出確認報文,ACK=1,ack=u+1,並且帶上自己的序列號seq=v,此時,服務端就進入了CLOSE-WAIT(關閉等待)狀態。TCP服務器通知高層的應用進程,客戶端向服務器的方向就釋放了,這時候處於半關閉狀態,即客戶端已經沒有數據要發送了,但是服務器若發送數據,客戶端依然要接受。這個狀態還要持續一段時間,也就是整個CLOSE-WAIT狀態持續的時間。
  2.        3、客戶端收到服務器的確認請求后,此時,客戶端就進入FIN-WAIT-2(終止等待2)狀態,等待服務器發送連接釋放報文(在這之前還需要接受服務器發送的最后的數據)。
  3.        4、服務器將最后的數據發送完畢后,就向客戶端發送連接釋放報文,FIN=1,ack=u+1,由於在半關閉狀態,服務器很可能又發送了一些數據,假定此時的序列號為seq=w,此時,服務器就進入了LAST-ACK(最后確認)狀態,等待客戶端的確認。
  4.        5、客戶端收到服務器的連接釋放報文后,必須發出確認,ACK=1,ack=w+1,而自己的序列號是seq=u+1,此時,客戶端就進入了TIME-WAIT(時間等待)狀態。注意此時TCP連接還沒有釋放,必須經過2∗MSL(最長報文段壽命)的時間后,當客戶端撤銷相應的TCB后,才進入CLOSED狀態。
  5.       6、服務器只要收到了客戶端發出的確認,立即進入CLOSED狀態。同樣,撤銷TCB后,就結束了這次的TCP連接。可以看到,服務器結束TCP連接的時間要比客戶端早一些。

 


 

三、實現web靜態服務器

  直接看示例:

import socket
import re

def server_client(server_client_socket):
    """接收來自瀏覽器的數據和發送報文"""

    # 對接收到的請求消息進行解析
    request = server_client_socket.recv(1024).decode("utf-8")
    # 請求消息頭部格式大概為 : GET /index.html HTTP/1.1 
    request_lines = request.splitlines()  # 將接收到的信息按行分割,並返回列表
    ret = re.match(r"[^/]+(/[^ ]*)",request_lines[0])

        # 若正則表達式匹配成功有返回值,則檢測是由為 "/"或者空字符,則設置默認文件為/index.html文件
    if ret:
        filename = ret.group(1)
        if filename == "/" or filename == "":
            filename = "/index.html"
    # print(request)

    respone = "HTTP/1.1 200 OK\r\n"
    respone += "\r\n"
    # respone += "<h2>hello world</h2>"

        # 檢測服務器中是否有該文件,有則發生內容,無則返回錯誤 
    try:
        f = open("html"+ filename,"rb")
    except:
        respone = "HTTP/1.1 404 Not found file \r\n"
        respone += "\r\n"
        respone += "<h1>Not found File</h1>"
        respone = respone.encode("utf-8")
    else:
        file_content = f.read()
        f.close()
        respone = respone.encode("utf-8") + file_content  # 字節之間的拼接
        
    server_client_socket.send(respone)

    server_client_socket.close()


def main():
    # 1、創建套接字對象
    server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 2、綁定本地信息
    server_socket.bind(("",6969))
    # 3、監聽
    server_socket.listen(128)
    while True:
        # 4、等待客戶端連接
        server_client_socket,client_addr =  server_socket.accept()
        # 5、服務客戶端
        server_client(server_client_socket)

    server_socket.close()

if __name__ == "__main__":
    main()

  這樣就實現了一個靜態的web服務器,可以接收web瀏覽器的請求,對請求的數據進行解析,通過正則表達式得到需要瀏覽器請求的頁面的文件名,通過文件的讀取得到數據,並將內容符合給web瀏覽器。

 

 


免責聲明!

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



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