Python 實現簡單網絡應用程序開發

ICMP Ping

目的

此任務是重新創建第3講(延遲,丟失和吞吐量)中討論的ping客戶端。

Ping 是一個用於在計算機網絡中測量延遲和丟失的工具。

在實際應用中,我們可以通過 ping 命令分析判斷網絡失敗的原因。當然,這類信息也可用於幫助我們選擇性能更佳的IP地址作爲代理服務器。

原理

Ping 通常使用 Internet 控制消息協議 ( ICMP ) 報文來測量網絡中的延遲和丟失:本機在 ICMP 包中發送迴響請求(ICMP類型代碼爲8)給另一個主機。然後,主機解包數據包並提取ICMP類型代碼並匹配請求和回覆之間的ID。如果遠程主機的響應報文ICMP類型代碼爲0,然後我們可以計算髮送請求和接收回復之間經過的時間,進而精確的計算兩臺主機之間網絡的延遲。

注意: IP數據報和ICMP錯誤代碼的結構(ICMP類型代碼爲3)如下所示。因特網校驗和也是數據包的重要部分,但它不是本函數實現的核心。

函數實現

基於上述原理,首先,需要創建一個與協議ICMP關聯的套接字,並設置超時以控制用於接收數據包的時間套接字。

# 運行特權TCP套接字,1是與協議ICMP關聯的套接字模塊常量。
icmp_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, 1)
icmp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, timeout)

創建套接字後,需要實現一個函數來構建,打包並將ICMP數據包發送到目標主機。

如圖所示,如果創建一個32字節大小的數據包,那麼只有四個字節長度來存儲有效負載數據。

因此,以浮點格式(4個字節)存儲當前時間幀是比較好的解決辦法。

但是,由於精度損失,不能使用此數據來計算總網絡延遲。

"!" 但是,由於精度損失,我永遠不會使用此數據來計算總網絡延遲。

構建和打包ICMP數據包源代碼:

def receive_one_ping(icmp_socket, port_id, timeout, send_time):
    while True:
        # 1. 等待套接字並得到回覆。
        wait_for_data = select.select([icmp_socket], [], [], timeout)
        # 2. 一旦接受,記錄當前時間。
        data_received = time.time()
        rec_packet, addr = icmp_socket.recvfrom(1024)
        ip_header = rec_packet[8: 12]
        icmp_header = rec_packet[20: 28]
        payload_size = struct.calcsize("!f")
        # 3. 解壓包首部行查找有用的信息。
        type, code, checksum, id, sequence = struct.unpack("!bbHHh", icmp_header)
        # 4. 檢查收發之間的 ID 是否匹配。
        if type == 0 and id == port_id:  # type should be 0
            ttl = struct.unpack("!b", ip_header[0:1])[0]
            delay_time = data_received - send_time
        # 5. 返回比特大小,延遲率和存活時間。
            return payload_size * 8, delay_time, ttl
        elif type == 3 and code == 0:
            return 0  # 網絡無法到達的錯誤。
        elif type == 3 and code == 1:
            return 1  # 主機無法到達的錯誤。

//進羣:808713721可以獲取Python編程各類入門學習資料!

當從同一主機獲得所有ping測試結果時,需要另一個函數來顯示所有測量的最小時間,平均時間和最大延遲。

def ping_statistics(list):
    max_delay = list[0]
    mini_delay = list[0]
    sum = 0
    for item in list:
        if item >= max_delay:
            max_delay = item
        elif item <= mini_delay:
            mini_delay = item
        sum += item
    avg_delay = int(sum / (len(list)))
    return mini_delay, max_delay, avg_delay

//進羣:808713721可以獲取Python編程各類入門學習資料!

最後一件事是處理異常。需要處理不同的ICMP錯誤代碼和返回值的超時。代碼如下所示:

def ping(host, count_num="4", time_out="1"):
    # 1. 查找主機名,將其解析爲IP地址。
    ip_addr = socket.gethostbyname(host)
    successful_list = list()
    lost = 0
    error = 0
    count = int(count_num)
    timeout = int(time_out)
    timedout_mark = False
    for i in range(count):  # i 是序列的值
        # 打印報文首部行
        ......
        try:
            # 2. 調用 doOnePing 函數。
            ping_delay = do_one_ping(ip_addr, timeout, i)
            # 3. 打印出返回的延遲信息。
            if ping_delay == 0 or ping_delay == 1:
                # 獲取本機的 IP 地址。
                ip_addr = socket.gethostbyname(socket.gethostname())
                print("Reply from {ipAdrr}: ".format(ipAdrr = ip_addr), end = "")
                result = "Destination host unreachable." if ping_delay == 0 else \
                         "Destination net unreachable."
                print(result)
                error += 1
            else:
                bytes, delay_time, ttl = ping_delay[0], int(ping_delay[1] * 1000), \
                                         ping_delay[2]
                print("Reply from {ipAdrr}: ".format(ipAdrr = ip_addr), end = "")
                # 如果可以成功接收數據包,
                # 在list裏追加延遲時間。
                successful_list.append(delay_time)
                # 如果延遲時間小於 1 ms,則記爲0。
                ......
        except TimeoutError:  # 超時類型
            lost += 1
            print("Request timed out.")
            # 如果它不總是超時的情況,
            # 我們需要計算最大延遲時間。
            if timedout_mark is False:
                timedout_mark = True
        time.sleep(1)  # 每秒。
    #  4. 繼續執行直到結束。
    ......

輸出結果

C:\Users\asus\Desktop\lab_solution\ICMP Ping>ICMPPing.py>ping www.baidu.com
Pinging www.baidu.com [111.13.100.92] with 32 of data:
Reply from 111.13.100.92: bytes = 32 time = 28ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 35ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 33ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 31ms TTL = 51.

Ping statistics for 111.13.100.92:
    Packet: Sent = 4, Received = 4, lost = 0 (0% loss).
Approximate round trip times in milli - seconds:
    Minimum = 28ms, Maximum = 35ms, Average = 31ms.

C:\Users\asus\Desktop\lab_solution\ICMP Ping>ICMPPing.py>ping google.com
Pinging google.com [172.217.161.174] with 32 of data:
Request timed out.
Request timed out.
Request timed out.
Request timed out.

Ping statistics for 172.217.161.174:
    Packet: Sent = 4, Received = 0, lost = 4 (100% loss).
C:\Users\asus\Desktop\lab_solution\ICMP Ping>ICMPPing.py>ping www.baidu.com -n 6
Pinging www.baidu.com [111.13.100.92] with 32 of data:
Reply from 111.13.100.92: bytes = 32 time = 29ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 46ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 33ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 44ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 36ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 35ms TTL = 51.

Ping statistics for 111.13.100.92:
    Packet: Sent = 6, Received = 6, lost = 0 (0% loss).
Approximate round trip times in milli - seconds:
    Minimum = 29ms, Maximum = 46ms, Average = 37ms.

C:\Users\asus\Desktop\lab_solution\ICMP Ping>ICMPPing.py>ping www.baidu.com -n 6 -w 2
Pinging www.baidu.com [111.13.100.92] with 32 of data:
Reply from 111.13.100.92: bytes = 32 time = 25ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 35ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 20ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 55ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 34ms TTL = 51.
Reply from 111.13.100.92: bytes = 32 time = 37ms TTL = 51.

Ping statistics for 111.13.100.92:
    Packet: Sent = 6, Received = 6, lost = 0 (0% loss).
Approximate round trip times in milli - seconds:
    Minimum = 20ms, Maximum = 55ms, Average = 34ms.

路由追蹤

目的

此任務是重新創建第3講(延遲,丟失和吞吐量)中的路由追蹤工具。這用於測量主機和到達目的地的路徑上的每一跳之間的延遲。在實際應用中,路由追蹤可以找到源主機和目標主機之間的路由器以及到達每個路由器所需的時間。

原理

如上圖所示,源主機使用ICMP echo請求報文,但有一個重要的修改:

存活時間( TTL )的值初始爲1。這可以確保我們從第一跳獲得響應。 一旦報文到達路由器,TTL計數器就會遞減。

當TTL達到0時,報文將返回到源主機,ICMP 類型爲11(已超出TTL且IP數據報尚未到達目標並被丟棄)。

每次增加TTL都會重複此過程,直到我們收到回覆。如果echo回覆ICMP類型爲0,則表示IP數據報已到達目的地。

然後我們就可以停止運行路由追蹤的腳本了。在此過程中,可能會發生異常並且我們需要處理錯誤代碼與 ICMP Ping 相同。

函數實現

基於上述原理,首先,除了創建一個與協議ICMP關聯的套接字並設置超時來控制用於接收數據包的套接字外,還需要通過

socket.setsockopt(level, optname, value) 函數設置套接字的TTL。

client_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, 1) # ICMP
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO , time_out)
client_socket.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, struct.pack('I', ttl))

創建套接字後,需要實現一個函數來構建,打包並將ICMP數據包發送到目標主機。這部分代碼和在 ICMP Ping 中的 構建和打包ICMP數據包源代碼 一致。

下一步是等待並收到回覆。套接字將一直等待,直到收到數據包或達到超時限制。 通過 ICMP 迴響報文發現並報告 無法訪問目標主機 和 無法訪問目標網路 。這部分和 接受數據包源碼 相似但路由追蹤需要額外記錄每次訪問到路由器的IP地址。代碼如下所示:

def receive_one_trace(icmp_socket, send_time, timeout):
    try:
        # 1. 等待套接字並得到回覆。
        ... Similar to Receive Packet Source ...
        # 2. 一旦接受,記錄當前時間。
        rec_packet, retr_addr = icmp_socket.recvfrom(1024)
        # 3. 解壓包首部行查找有用的信息。
        ... Similar to Receive Packet Source ...
        # 4. 通過代號類型檢查數據包是否丟失。
        ... Similar to Receive Packet Source ...
    except TimeoutError :
        print_str = "*   "  # 超時。
    finally:
        ..... # 打印延遲時間。
    # 返回當前路由器的IP地址。
    # 如果超時的話,直接返回字符串。
    try:
        retre_ip = retr_addr[0]
    except IndexError:
        retre_ip = "Request timeout"
    finally:
        return retre_ip

最後一件事是爲每個路由器實現重複測量,解析在對各自主機名的響應中找到的IP地址並處理異常。代碼如下所示:

def trace_route(host, timeout=2):
    # 可配置超時,使用可選參數設置。
    # 1. 查找主機名,將其解析爲IP地址。
    ip_addr = socket.gethostbyname(host)
    ttl = 1
    print("Over a maximum of {max_hop} hops:\n".format(max_hop = MAX_HOP))
    print("Tracing route to " + host + " [{hostIP}]:".format(hostIP = ip_addr))
    for i in range(MAX_HOP):
        sys.stdout.write("{0: >3}".format(str(ttl) + "\t"))
        cur_addr = do_three_trace(ip_addr, ttl, i, timeout)
        try:
            sys.stdout.write("{0:<}".format(" " + socket.gethostbyaddr(cur_addr)[0] + " [" + cur_addr + "]" + "\n"))
        except (socket.herror, socket.gaierror):
            sys.stdout.write("{0:<}".format(" " + cur_addr + "\n"))
        if cur_addr == ip_addr :
            break
        ttl += 1
    sys.stdout.write("\nTrace complete.\n\n")

輸出結果

Over a maximum of 30 hops:
    Tracing route to www.baidu.com [111.13.100.91]:
    1 16 ms 15 ms 15 ms 10.129.0.1
    ...... # All successful
    5 * * * Request timeout
    6 * * * Request timeout
    ...... # All successful
    9 * * * Request timeout
    ...... # All successful
    13 * * * Request timeout
    14 22 ms 22 ms 21 ms 111.13.100.91
    Trace complete.

C:\Users\asus\Desktop\lab_solution\Traceroute>Traceroute.py>tracert 10.129.21.147
Over a maximum of 30 hops:
Tracing route to 10.129.21.147 [10.129.21.147]:
 1 Host unreachable * Host unreachable DESKTOP-6VPPJQ8 [10.129.34.15]
 2        * Host unreachable     *     DESKTOP-6VPPJQ8 [10.129.34.15]
 ...... All situations are the same
29 Host unreachable * Host unreachable DESKTOP-6VPPJQ8 [10.129.34.15]
30        * Host unreachable     *     DESKTOP-6VPPJQ8 [10.129.34.15]
Trace complete.

Web服務器

目的

此任務是構建一個簡單的HTTP Web服務器。根據第4講(Web 和 HTTP)中學習到的知識,Web 服務器是Internet的基礎部分,它們提供我們熟悉的網頁和內容。

網頁由對象組成,這些對象可以是HTML文件,JPEG圖像,Java小程序等。HTTP流量通常綁定到端口80,端口8080是常用的替代方案。因此,虛擬主機(機器)使用本地端口號80和8080。

原理

HTTP/1.1 含有許多類型的 HTTP 請求,在本任務中,只考慮 HTTP GET 請求。

如下圖所示,一個簡單的 web 服務器從前端接受 HTTP 請求報文。收到此請求後,簡單 Web 服務器將從郵件中提取所請求對象的路徑,然後嘗試從硬盤中檢索請求的對象。如果它成功找到硬盤中的對象,它會將對象發送回具有相應首部行的客戶端(其中包含Status-Code 200)。否則,它將使用HTTP響應報文(將包含404 Not Found"狀態將"Not Found"網頁發送到客戶端。

line)". HTTP 請求和響應報文的格式已在下圖5.(a)和5.(b)顯示。

函數實現

基於上述原理,首先,創建一個支持 IPv4 的套接字並將其綁定在高於1024的端口上。Web服務器應該同時監聽5個請求,並具有處理多個併發連接的能力。

Web 服務器運行源碼:

def start_server(server_port , server_address):
    # 將 web 服務器綁定到可配置端口,定義爲可選參數。
    # 1. 常見一個服務器套接字。
    server_socket = socket(AF_INET, SOCK_STREAM) #IPv4
    # 2. 將服務器套接字綁定到服務器地址和服務器端口。
    server_socket.bind(("", server_port))
    # 3. 持續監聽與服務器套接字的連接。
    server_socket.listen(5)
    while True:
        # 4. 當接受連接時,調用 handleRequest 函數,傳遞新的連接套接字。
        connection_socket , (client_ip, client_port) = server_socket.accept()
        # 創建一個多線程服務器實現,能夠處理多個併發連接。
        start_new_thread(handle_request , (connection_socket , client_ip, client_port))
     # 5. 關閉服務器端的套接字。
     server_socket.close() # 不然服務器會一直監聽,不會主動關閉。

 # 從這裏開始運行。
 server_port = int(sys.argv[1])
 server_address = gethostbyname(gethostname())
 start_server(server_port)

創建套接字後,web 服務器需要處理HTTP GET請求。在處理之前,創建了一個StrProcess類來重寫字符串模塊中的split方法。用空格分割一個字符串,只處理請求行。因此,當它找到字符"r"時,它將停止處理並返回結果。

StrProcess 類源碼:

class StrProcess(str):
    def __init__(self, str):
        """用字符型變量 str 初始該屬性"""
        self.str = str
    def split_str(self):
        """實現分割操作"""
        spilt_list = []
        start = 0
        for i in range(len(self.str)):
            if self.str[i] == " ":
                spilt_list.append(self.str[start:i])
                start = i + 1
            if self.str[i] == "\r":
                break
        return spilt_list[1]

最後一件事是處理 HTTP GET 請求和異常。由於非持久性HTTP,不需要在true循環時寫入以接收HTTP請求報文。如果套接字收到空報文,則應該關閉它。否則,web 服務器需要檢查對象是否存在於緩存中。如果對象存在,則使用"HTTP/1.1 200 OK rnrn"將對象發送到客戶端,否則發生"FileNotFoundError"異常,然後對於"未找到"的HTML文件使用"HTTP/1.1 404 Not Foundrnrn"發送到客戶端。 發送HTTP響應報文後,HTTP服務器將關閉TCP連接。代碼如下所示:

def handle_request(tcp_socket, client_ip, client_port):
    print("Client ({ip}: {port}) is coming...".format(ip = client_ip, port = client_port))
    try:
        # 1. 在連接套接字上從客戶端接收請求報文。
        msg = tcp_socket.recv(1024).decode()
        if not msg:
            print("Error! server receive empty HTTP request.")
            tcp_socket.close()
        # 從strProc類創建新對象(handlestr)。
        handle_str = StrProcess(msg)
        # 2. 從報文中提取所請求對象的路徑(HTTP 首部行的第二部分)。
        file_name = handle_str.split_str()[1:]
        # 3. 從磁盤中查找相應的文件。
        # 檢查請求的對象是否存在。
        f = open(file_name)
        f.close()
        # 如果對象存在,準備發送 "HTTP/1.1 200 OK\r\n\r\n" 到套接字。
        status = "200 OK"
    except FileNotFoundError:
        # 否則,準備發送 "HTTP/1.1 404 Not Found\r\n\r\n" 到套接字。
        status = "404 Not Found"
        file_name = "NotFound.html"
    re_header = "HTTP/1.1 " + status + "\r\n\r\n" # 最後一個''\r\n'' 意味着頭報文的結束。
    # 4. 發送正確的HTTP響應。
    tcp_socket.send(re_header.encode())
    # 5. 存儲在臨時緩衝區中
    with open(file_name, 'rb') as f:
        file_content = f.readlines()
    # 6. 將文件的內容發送到套接字。
    for strline in file_content:
        tcp_socket.send(strline)
    # 7. 關閉連接的套接字。
    print("Bye to Client ({ip}: {port})".format(ip = client_ip, port = client_port))
    tcp_socket.close()
////進羣:808713721可以獲取Python編程各類入門學習資料!

編寫了一個單獨的HTTP客戶端來查詢 web 服務器。此客戶端可以發送 HTTP GET 請求並在控制檯上接收HTTP響應報文。該程序的優點是它只需輸入對象的名稱或選擇保留或離開即可查詢對象。

HTTP 客戶端源碼:

from socket import *
import sys

# 1. 設置 web 服務器的地址。
host_port = int(sys.argv[1])
host_address = gethostbyname(gethostname())
# 2. 創建客戶端套接字以啓動與 web 服務器的 TCP 連接。
tcp_client = socket(AF_INET, SOCK_STREAM)
tcp_client.connect((host_address , host_port))
# 3. 輸入要查詢 web服務器的文件客戶端。
print("Hello, which document do you want to query?")
while True:
    obj = input("I want to query: ")
    # 4. 發送 HTTP 請求報文。
    message = "GET /" + obj + " HTTP/1.1\r\n" \
              "Host: " + host_address + ":" + str(host_port) + "\r\n" \
              "Connection: close\r\n" \
              "Upgrade-Insecure-Requests: 1\r\n" \
              "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36\r\n" \
              ...... # 首部行。
              "Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7\r\n\r\n"
    tcp_client.send(message.encode())
    while True:
        # 5. 接收HTTP響應報文並將其打印到控制檯。
        data = tcp_client.recv(1024)
        if not data:
            break
        print("Web server responded to your request:")
        print(data.decode())
    tcp_client.close() # 關閉當前的連接。
    # 6. 詢問客戶是否要繼續。
    ans = input('\nDo you want to cut this connection(y/n) :')
    if ans == 'y' or ans == 'Y':
        break
    elif ans == 'n' or ans == 'N':
        # 重新嘗試。
        print("Anything else I can help you?")
        tcp_client = socket(AF_INET, SOCK_STREAM)
        tcp_client.connect((host_address , host_port))
    else:
        print("Command Error, quit.")
        break

//進羣:808713721可以獲取Python編程各類入門學習資料!

輸出結果

WebServer.py

you can test the web server by accessing: http://10.129.34.15:8899/hello.html
Wait for TCP clients...
Client (10.129.34.15: 6123) is coming...
Bye to Client (10.129.34.15: 6123)
Client (10.129.34.15: 6135) is coming...
Bye to Client (10.129.34.15: 6135)

C:\User\asus\Desktop\lab_solution\Web Server>python Client.py 8899
Hello, which document do you want to query?
I want to query: hello.html
//進羣:808713721可以獲取Python編程各類入門學習資料!

Client.py

Web server responded to your request:
HTTP/1.1 200 OK
Web server responded to your request:
<!DOCTYPE html>
<html>
<head>
<title>Hello World HTML</title>
</head>
<body>
<h1>Hello World</h1>
</body>
Web server responded to your request:
</html>

Do you want to cut this connection(y/n) :n
Anything else I can help you?
I want to query: index.html
Web server responded to your request:
HTTP/1.1 404 Not Found
Do you want to cut this connection(y/n) :y
Process finished with exit code 0
//進羣:808713721可以獲取Python編程各類入門學習資料!

Web代理服務器

目的

此任務是構建一個簡單的 web 代理服務器。根據第4講(Web 和 HTTP)中所學到的知識, web 代理服務器充當客戶端和服務器,這意味着它具有 web 服務器和客戶端的所有功能。它和 web 服務器之間最顯着的區別是發送的請求報文

和響應報文都要通過 web 服務器傳遞。web 代理服務器在任何地方(大學,公司和住宅ISP)使用,以減少客戶請求和流量的響應時間。

原理

web 代理服務器的原理基於 web 服務器。web 代理服務器從客戶端接收 HTTP 請求報文,並從請求行中提取方法類型。

  • 如果請求類型是 GET ,從同一行獲取URL並檢查請求的對象是否存在於緩存中。否則,web 代理服務器會將客戶端的請求轉發給 web 服務器。然後,web 服務器將生成響應報文並將其傳遞給 web 代理服務器,web 代理服務器又將其發送到客戶端併爲將來的請求緩存副本。
  • 如果請求類型是 DELETE ,代理服務器會首先確認請求,如果對象存在緩存中,j只是從緩存中刪除它併發送帶有 Status-Code 200 的 HTTP 響應報文。否則,web 代理服務器發送帶有 Status-Code 404 的 HTTP 響應報文。
  • 如果請求類型是 POST , 處理過程比上述方法類型更容易,web 代理服務器只是以二進制格式將對象寫入磁盤,然後發送帶有 Status-Code 200 的 HTTP 響應報文(輸入在實體行中上傳)。如果方法是 PUT,則只需要在實體行中返回 true。
  • 如果請求類型是 HEAD , web 代理服務器僅返回 HTTP 響應報文的報文首部行和狀態行。

簡化過程如下所示。

函數實現

基於上述原理,首先,創建一個支持 IPv4 的套接字並將其綁定在高於1024的端口上。web 代理服務器和 web 服務器非常類似,唯一區別是它是單線程的。

我在 StrProcess 類中添加了其他方法使 web 代理服務器獲取對象或連接到 web 服務器的效率更高。

StrProcess 類源碼:

class StrProcess(str):

    def __init__(self, str):
        """用字符型變量 str 初始該屬性"""
        self.str = str

    def split_str(self):
        """實現分割操作"""
        spilt_list = []
        start = 0
        for i in range(len(self.str)):
            if self.str[i] == " ":
                spilt_list.append(self.str[start:i])
                start = i + 1
            if self.str[i] == "\r":
                break
        try:
            return spilt_list[0],spilt_list[1]
        except IndexError:
            return None

    def get_cmd_type(self):
        """從 HTTP 請求行中提取請求方法"""
        return self.split_str()[0]

    def get_body(self):
        """從 HTTP 請求報文實體行中提取數據"""
        body_start = self.str.find('\r\n\r\n') + 4
        return self.str[body_start:]

    def get_referer(self):
        """從 HTTP 請求行中提取引用"""
        ref_pos = self.str.find('Referer: ') + 9
        ref_stop = self.str.find('\r\n', ref_pos+1)
        get_ref = self.str[ref_pos:ref_stop]
        get_ref_start = get_ref.find('9/') + 2
        get_ref_path = self.str[ref_pos+get_ref_start:ref_stop]
        return get_ref_path

    def get_path(self):
        """從 HTTP 請求請求行中提取URL"""
        original_path = self.split_str()[1]
        for i in range(len(original_path)):
            if original_path[i] == "/":
                original_path = original_path[i+1:]
                return original_path

    def change_name(self):
        """將所有特殊符號轉換爲 "-"。"""
        original_name = self.get_path()
        for i in range(len(original_name)):
            if original_name[i] == "/" or original_name[i] == "?" \
                    or original_name[i] == "=" or original_name[i] == "&" \
                    or original_name[i] == "%":
                original_name = original_name[:i] + "-" + original_name[i+1:]
        return original_name

    def get_hostname(self):
        """從URL中提取主機名"""
        whole_URL = self.get_path()
        for i in range(len(whole_URL)):
            if whole_URL[i] == "/":
                    host_name = whole_URL[:i]
                    return host_name
        return whole_URL
//進羣:808713721可以獲取Python編程各類入門學習資料!

創建套接字後,web 代理服務器需要處理不同的 HTTP 請求類型和異常。在這部分中,文件處理應該以二進制格式使用,我們必須考慮對象類型(可以是.jpg,.svg,.ico等)。

處理 HTTP 請求源碼:

def start_listen(tcp_socket, client_ip, client_port):

    # 1. 在連接套接字上從客戶端接收請求報文。
    message = tcp_socket.recv(1024).decode()
    # 從strProc類創建新對象(handlestr)。
    handle_str = StrProcess(message)
    print("client is coming: {addr}:{port}".format(addr = client_ip, port = client_port))
    file_error = False
    global host
    try:
        command = handle_str.get_cmd_type()
        # 2. 從報文中提取所請求對象的路徑(HTTP 首部行的第二部分)。
        filename = handle_str.change_name()
        # 3. 找到特定的方法類型並處理請求。
        if command == "DELETE" :
            # 刪除緩存中存在的對象
            os.remove("./Cache/" + filename)
            tcp_socket.send(b"HTTP/1.1 200 OK\r\n\r\n")
            print("File is removed.")
        elif command == "GET" or command == "HEAD":
            print("Client want to {c} the {o}.".format(c=command, o=filename))
            # 檢查請求的對象是否存在。
            f = open("./Cache/" + filename, "rb")
            file_content = f.readlines()
            f.close()
            if command == "GET":
                print("File in cache!")
                # 從磁盤中查找相應的文件(如果存在)。
                for i in range(0, len(file_content)):
                    tcp_socket.send(file_content[i])  # 發送 HTTP 響應報文。
            else:  # "HEAD" 方法。
                list_to_str = ""
                for i in range(0, len(file_content)):
                    list_to_str += file_content[i].decode()
                HTTP_header_end = list_to_str.find("\r\n\r\n")
                # 僅發送 HTTP 響應報文首部行。
                tcp_socket.send(list_to_str[:HTTP_header_end+4].encode())
        elif command == "PUT" or command == "POST":  # 只實現上傳文件。
            f = open("./Cache/" + filename, "ab")
            f.write(b"HTTP/1.1 200 OK\r\n\r\n" + handle_str.get_body().encode())
            f.close()
            print("Update successfully!")
            tcp_socket.send(b"HTTP/1.1 200 OK\r\n\r\n")
            body_re = b"true" if command == "PUT" else handle_str.get_body().encode()
            tcp_socket.send(body_re)
        else:
            tcp_socket.send(b"HTTP/1.1 400 Bad Request\r\n\r\n")
    # 4. 如果緩存中不存在該文件,則處理異常。
    except (IOError, FileNotFoundError):
        if command == "GET":
            # 在代理服務器上創建套接字。
            c = socket(AF_INET, SOCK_STREAM)
            hostname = handle_str.get_hostname()
            file = handle_str.split_str()[1]
            print("The file isn't in the cache!")
            try:
                # 連接到端口80的套接字。
                c.connect((hostname, 80))
                host = hostname  # 記錄真實的主機名。
                request = "GET " + "http:/" + file + " HTTP/1.1\r\n\r\n"
            except:
                try:
                    # 需要使用全局主機或引用主機名。
                    new_host = handle_str.get_referer() if host == "" else host
                    c.connect((new_host, 80))
                    request = "GET " + "http://" + new_host + file + " HTTP/1.1\r\n\r\n"
                except:
                    tcp_socket.send(b"HTTP/1.1 404 Not Found\r\n\r\n")
                    with open("./Cache/NotFound.html", 'rb') as f:
                        file_content = f.readlines()
                    for strline in file_content:
                        tcp_socket.send(strline)
                    file_error = True
            if file_error is False:
                c.sendall(request.encode())
                # 將響應讀入緩衝區。
                print("The proxy server has found the host.")
                # 在緩存中爲請求的文件創建一個新文件。
                # 此外,將緩衝區中的響應發送到客戶端套接字和緩存中的相應文件。
                writeFile = open("./Cache/" + filename, "wb")
                print("The poxy server is receiving data...")
                # 接受 HTTP 響應報文直到所有報文都被接收。
                while True:
                    data = c.recv(4096)
                    if not data:
                        break
                    sys.stdout.write(">")
                    # 將文件的內容發送到套接字。
                    tcp_socket.sendall(data)
                    writeFile.write(data)
                writeFile.close()
                sys.stdout.write("100%\n")
            c.close()
        elif command == "DELETE":
            tcp_socket.send(b"HTTP/1.1 204 Not Content\r\n\r\n")
    except (ConnectionResetError, TypeError):
        print("Bye to client: {addr}:{port}".format(addr = client_ip, port = client_port))
    # 關閉客戶端的套接字。
    print("tcp socket closed\n")
    tcp_socket.close()

//進羣:808713721可以獲取Python編程各類入門學習資料!

輸出結果

瀏覽器測試

WebProxy.py

C:\Users\asus\Desktop\lab_solution\Web Proxy>python WebProxy.py 8899
Wait for TCP clients...
wait for request:
client is coming: 127.0.0.1:4596
Client want to GET the s-wd-facebook-rsv_bp-0-ch-tn-baidu-bar-rsv_spt-3-ie-utf-8-rsv_enter-1-oq-face-f-3-inputT-3356.
The file is not in the cache!
The proxy server has found the host.
Poxy server is receiving data...
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>100%
tcp socket closed

源碼

如果我的文章可以幫到您,勞煩您點進源碼點個 ★ Star 哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章