最后一次更新于
2019/07/10
ICMP Ping
目的
此任務(wù)是重新創(chuàng)建第3講(延遲,丟失和吞吐量)中討論的ping客戶端。
Ping 是一個(gè)用于在計(jì)算機(jī)網(wǎng)絡(luò)中測(cè)量延遲和丟失的工具。
在實(shí)際應(yīng)用中,我們可以通過(guò)
ping
命令分析判斷網(wǎng)絡(luò)失敗的原因。當(dāng)然,這類信息也可用于幫助我們選擇性能更佳的IP地址作為代理服務(wù)器。
原理
Ping 通常使用 Internet 控制消息協(xié)議 ( ICMP ) 報(bào)文來(lái)測(cè)量網(wǎng)絡(luò)中的延遲和丟失:本機(jī)在 ICMP 包中發(fā)送回響請(qǐng)求(ICMP類型代碼為8)給另一個(gè)主機(jī)。然后,主機(jī)解包數(shù)據(jù)包并提取ICMP類型代碼并匹配請(qǐng)求和回復(fù)之間的ID。如果遠(yuǎn)程主機(jī)的響應(yīng)報(bào)文ICMP類型代碼為0,然后我們可以計(jì)算發(fā)送請(qǐng)求和接收回復(fù)之間經(jīng)過(guò)的時(shí)間,進(jìn)而精確的計(jì)算兩臺(tái)主機(jī)之間網(wǎng)絡(luò)的延遲。
注意
: IP數(shù)據(jù)報(bào)和ICMP錯(cuò)誤代碼的結(jié)構(gòu)(ICMP類型代碼為3)如下所示。因特網(wǎng)校驗(yàn)和也是數(shù)據(jù)包的重要部分,但它不是本函數(shù)實(shí)現(xiàn)的核心。
函數(shù)實(shí)現(xiàn)
基于上述原理,首先,需要?jiǎng)?chuàng)建一個(gè)與協(xié)議ICMP關(guān)聯(lián)的套接字,并設(shè)置超時(shí)以控制用于接收數(shù)據(jù)包的時(shí)間套接字。
# 運(yùn)行特權(quán)TCP套接字,1是與協(xié)議ICMP關(guān)聯(lián)的套接字模塊常量。
icmp_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, 1)
icmp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, timeout)
創(chuàng)建套接字后,需要實(shí)現(xiàn)一個(gè)函數(shù)來(lái)構(gòu)建,打包并將ICMP數(shù)據(jù)包發(fā)送到目標(biāo)主機(jī)。
如圖所示,如果創(chuàng)建一個(gè)32字節(jié)大小的數(shù)據(jù)包,那么只有四個(gè)字節(jié)長(zhǎng)度來(lái)存儲(chǔ)有效負(fù)載數(shù)據(jù)。
因此,以浮點(diǎn)格式(4個(gè)字節(jié))存儲(chǔ)當(dāng)前時(shí)間幀是比較好的解決辦法。
但是,由于精度損失,不能使用此數(shù)據(jù)來(lái)計(jì)算總網(wǎng)絡(luò)延遲。
"!" 但是,由于精度損失,我永遠(yuǎn)不會(huì)使用此數(shù)據(jù)來(lái)計(jì)算總網(wǎng)絡(luò)延遲。
構(gòu)建和打包ICMP數(shù)據(jù)包源代碼:
def receive_one_ping(icmp_socket, port_id, timeout, send_time):
while True:
# 1. 等待套接字并得到回復(fù)。
wait_for_data = select.select([icmp_socket], [], [], timeout)
# 2. 一旦接受,記錄當(dāng)前時(shí)間。
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. 檢查收發(fā)之間的 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. 返回比特大小,延遲率和存活時(shí)間。
return payload_size * 8, delay_time, ttl
elif type == 3 and code == 0:
return 0 # 網(wǎng)絡(luò)無(wú)法到達(dá)的錯(cuò)誤。
elif type == 3 and code == 1:
return 1 # 主機(jī)無(wú)法到達(dá)的錯(cuò)誤。
當(dāng)從同一主機(jī)獲得所有ping測(cè)試結(jié)果時(shí),需要另一個(gè)函數(shù)來(lái)顯示所有測(cè)量的最小時(shí)間,平均時(shí)間和最大延遲。
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
最后一件事是處理異常。需要處理不同的ICMP錯(cuò)誤代碼和返回值的超時(shí)。代碼如下所示:
def ping(host, count_num="4", time_out="1"):
# 1. 查找主機(jī)名,將其解析為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 是序列的值
# 打印報(bào)文首部行
......
try:
# 2. 調(diào)用 doOnePing 函數(shù)。
ping_delay = do_one_ping(ip_addr, timeout, i)
# 3. 打印出返回的延遲信息。
if ping_delay == 0 or ping_delay == 1:
# 獲取本機(jī)的 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 = "")
# 如果可以成功接收數(shù)據(jù)包,
# 在list里追加延遲時(shí)間。
successful_list.append(delay_time)
# 如果延遲時(shí)間小于 1 ms,則記為0。
......
except TimeoutError: # 超時(shí)類型
lost += 1
print("Request timed out.")
# 如果它不總是超時(shí)的情況,
# 我們需要計(jì)算最大延遲時(shí)間。
if timedout_mark is False:
timedout_mark = True
time.sleep(1) # 每秒。
# 4. 繼續(xù)執(zhí)行直到結(jié)束。
......
輸出結(jié)果
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.
路由追蹤
目的
此任務(wù)是重新創(chuàng)建第3講(延遲,丟失和吞吐量)中的路由追蹤工具。這用于測(cè)量主機(jī)和到達(dá)目的地的路徑上的每一跳之間的延遲。在實(shí)際應(yīng)用中,路由追蹤可以找到源主機(jī)和目標(biāo)主機(jī)之間的路由器以及到達(dá)每個(gè)路由器所需的時(shí)間。
原理
如上圖所示,源主機(jī)使用ICMP echo請(qǐng)求報(bào)文,但有一個(gè)重要的修改:
存活時(shí)間
(
TTL
)的值初始為1。這可以確保我們從第一跳獲得響應(yīng)。 一旦報(bào)文到達(dá)路由器,TTL計(jì)數(shù)器就會(huì)遞減。
當(dāng)TTL達(dá)到0時(shí),報(bào)文將返回到源主機(jī),ICMP 類型為11(已超出TTL且IP數(shù)據(jù)報(bào)尚未到達(dá)目標(biāo)并被丟棄)。
每次增加TTL都會(huì)重復(fù)此過(guò)程,直到我們收到回復(fù)。如果echo回復(fù)ICMP類型為0,則表示IP數(shù)據(jù)報(bào)已到達(dá)目的地。
然后我們就可以停止運(yùn)行路由追蹤的腳本了。在此過(guò)程中,可能會(huì)發(fā)生異常并且我們需要處理錯(cuò)誤代碼與 ICMP Ping 相同。
函數(shù)實(shí)現(xiàn)
基于上述原理,首先,除了創(chuàng)建一個(gè)與協(xié)議ICMP關(guān)聯(lián)的套接字并設(shè)置超時(shí)來(lái)控制用于接收數(shù)據(jù)包的套接字外,還需要通過(guò)
socket.setsockopt(level, optname, value)
函數(shù)設(shè)置套接字的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))
創(chuàng)建套接字后,需要實(shí)現(xiàn)一個(gè)函數(shù)來(lái)構(gòu)建,打包并將ICMP數(shù)據(jù)包發(fā)送到目標(biāo)主機(jī)。這部分代碼和在 ICMP Ping 中的 構(gòu)建和打包ICMP數(shù)據(jù)包源代碼 一致。
下一步是等待并收到回復(fù)。套接字將一直等待,直到收到數(shù)據(jù)包或達(dá)到超時(shí)限制。 通過(guò) ICMP 回響報(bào)文發(fā)現(xiàn)并報(bào)告 無(wú)法訪問(wèn)目標(biāo)主機(jī) 和 無(wú)法訪問(wèn)目標(biāo)網(wǎng)路 。這部分和 接受數(shù)據(jù)包源碼 相似但路由追蹤需要額外記錄每次訪問(wèn)到路由器的IP地址。代碼如下所示:
def receive_one_trace(icmp_socket, send_time, timeout):
try:
# 1. 等待套接字并得到回復(fù)。
... Similar to Receive Packet Source ...
# 2. 一旦接受,記錄當(dāng)前時(shí)間。
rec_packet, retr_addr = icmp_socket.recvfrom(1024)
# 3. 解壓包首部行查找有用的信息。
... Similar to Receive Packet Source ...
# 4. 通過(guò)代號(hào)類型檢查數(shù)據(jù)包是否丟失。
... Similar to Receive Packet Source ...
except TimeoutError :
print_str = "* " # 超時(shí)。
finally:
..... # 打印延遲時(shí)間。
# 返回當(dāng)前路由器的IP地址。
# 如果超時(shí)的話,直接返回字符串。
try:
retre_ip = retr_addr[0]
except IndexError:
retre_ip = "Request timeout"
finally:
return retre_ip
最后一件事是為每個(gè)路由器實(shí)現(xiàn)重復(fù)測(cè)量,解析在對(duì)各自主機(jī)名的響應(yīng)中找到的IP地址并處理異常。代碼如下所示:
def trace_route(host, timeout=2):
# 可配置超時(shí),使用可選參數(shù)設(shè)置。
# 1. 查找主機(jī)名,將其解析為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")
輸出結(jié)果
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服務(wù)器
目的
此任務(wù)是構(gòu)建一個(gè)簡(jiǎn)單的HTTP Web服務(wù)器。根據(jù)第4講(Web 和 HTTP)中學(xué)習(xí)到的知識(shí),Web 服務(wù)器是Internet的基礎(chǔ)部分,它們提供我們熟悉的網(wǎng)頁(yè)和內(nèi)容。
網(wǎng)頁(yè)由對(duì)象組成,這些對(duì)象可以是HTML文件,JPEG圖像,Java小程序等。HTTP流量通常綁定到端口80,端口8080是常用的替代方案。因此,虛擬主機(jī)(機(jī)器)使用本地端口號(hào)80和8080。
原理
HTTP/1.1 含有許多類型的 HTTP 請(qǐng)求,在本任務(wù)中,只考慮 HTTP GET 請(qǐng)求。
如下圖所示,一個(gè)簡(jiǎn)單的 web 服務(wù)器從前端接受 HTTP 請(qǐng)求報(bào)文。收到此請(qǐng)求后,簡(jiǎn)單 Web 服務(wù)器將從郵件中提取所請(qǐng)求對(duì)象的路徑,然后嘗試從硬盤中檢索請(qǐng)求的對(duì)象。如果它成功找到硬盤中的對(duì)象,它會(huì)將對(duì)象發(fā)送回具有相應(yīng)首部行的客戶端(其中包含Status-Code 200)。否則,它將使用HTTP響應(yīng)報(bào)文(將包含404 Not Found"狀態(tài)將"Not Found"網(wǎng)頁(yè)發(fā)送到客戶端。
line)". HTTP 請(qǐng)求和響應(yīng)報(bào)文的格式已在下圖5.(a)和5.(b)顯示。
函數(shù)實(shí)現(xiàn)
基于上述原理,首先,創(chuàng)建一個(gè)支持 IPv4 的套接字并將其綁定在高于1024的端口上。Web服務(wù)器應(yīng)該同時(shí)監(jiān)聽(tīng)5個(gè)請(qǐng)求,并具有處理多個(gè)并發(fā)連接的能力。
Web 服務(wù)器運(yùn)行源碼:
def start_server(server_port , server_address):
# 將 web 服務(wù)器綁定到可配置端口,定義為可選參數(shù)。
# 1. 常見(jiàn)一個(gè)服務(wù)器套接字。
server_socket = socket(AF_INET, SOCK_STREAM) #IPv4
# 2. 將服務(wù)器套接字綁定到服務(wù)器地址和服務(wù)器端口。
server_socket.bind(("", server_port))
# 3. 持續(xù)監(jiān)聽(tīng)與服務(wù)器套接字的連接。
server_socket.listen(5)
while True:
# 4. 當(dāng)接受連接時(shí),調(diào)用 handleRequest 函數(shù),傳遞新的連接套接字。
connection_socket , (client_ip, client_port) = server_socket.accept()
# 創(chuàng)建一個(gè)多線程服務(wù)器實(shí)現(xiàn),能夠處理多個(gè)并發(fā)連接。
start_new_thread(handle_request , (connection_socket , client_ip, client_port))
# 5. 關(guān)閉服務(wù)器端的套接字。
server_socket.close() # 不然服務(wù)器會(huì)一直監(jiān)聽(tīng),不會(huì)主動(dòng)關(guān)閉。
# 從這里開(kāi)始運(yùn)行。
server_port = int(sys.argv[1])
server_address = gethostbyname(gethostname())
start_server(server_port)
創(chuàng)建套接字后,web 服務(wù)器需要處理HTTP GET請(qǐng)求。在處理之前,創(chuàng)建了一個(gè)StrProcess類來(lái)重寫字符串模塊中的split方法。用空格分割一個(gè)字符串,只處理請(qǐng)求行。因此,當(dāng)它找到字符"r"時(shí),它將停止處理并返回結(jié)果。
StrProcess 類源碼:
class StrProcess(str):
def __init__(self, str):
"""用字符型變量 str 初始該屬性"""
self.str = str
def split_str(self):
"""實(shí)現(xiàn)分割操作"""
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 請(qǐng)求和異常。由于非持久性HTTP,不需要在true循環(huán)時(shí)寫入以接收HTTP請(qǐng)求報(bào)文。如果套接字收到空?qǐng)?bào)文,則應(yīng)該關(guān)閉它。否則,web 服務(wù)器需要檢查對(duì)象是否存在于緩存中。如果對(duì)象存在,則使用"HTTP/1.1 200 OK rnrn"將對(duì)象發(fā)送到客戶端,否則發(fā)生"FileNotFoundError"異常,然后對(duì)于"未找到"的HTML文件使用"HTTP/1.1 404 Not Foundrnrn"發(fā)送到客戶端。 發(fā)送HTTP響應(yīng)報(bào)文后,HTTP服務(wù)器將關(guān)閉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. 在連接套接字上從客戶端接收請(qǐng)求報(bào)文。
msg = tcp_socket.recv(1024).decode()
if not msg:
print("Error! server receive empty HTTP request.")
tcp_socket.close()
# 從strProc類創(chuàng)建新對(duì)象(handlestr)。
handle_str = StrProcess(msg)
# 2. 從報(bào)文中提取所請(qǐng)求對(duì)象的路徑(HTTP 首部行的第二部分)。
file_name = handle_str.split_str()[1:]
# 3. 從磁盤中查找相應(yīng)的文件。
# 檢查請(qǐng)求的對(duì)象是否存在。
f = open(file_name)
f.close()
# 如果對(duì)象存在,準(zhǔn)備發(fā)送 "HTTP/1.1 200 OK\r\n\r\n" 到套接字。
status = "200 OK"
except FileNotFoundError:
# 否則,準(zhǔn)備發(fā)送 "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" # 最后一個(gè)''\r\n'' 意味著頭報(bào)文的結(jié)束。
# 4. 發(fā)送正確的HTTP響應(yīng)。
tcp_socket.send(re_header.encode())
# 5. 存儲(chǔ)在臨時(shí)緩沖區(qū)中
with open(file_name, 'rb') as f:
file_content = f.readlines()
# 6. 將文件的內(nèi)容發(fā)送到套接字。
for strline in file_content:
tcp_socket.send(strline)
# 7. 關(guān)閉連接的套接字。
print("Bye to Client ({ip}: {port})".format(ip = client_ip, port = client_port))
tcp_socket.close()
編寫了一個(gè)單獨(dú)的HTTP客戶端來(lái)查詢 web 服務(wù)器。此客戶端可以發(fā)送 HTTP GET 請(qǐng)求并在控制臺(tái)上接收HTTP響應(yīng)報(bào)文。該程序的優(yōu)點(diǎn)是它只需輸入對(duì)象的名稱或選擇保留或離開(kāi)即可查詢對(duì)象。
HTTP 客戶端源碼:
from socket import *
import sys
# 1. 設(shè)置 web 服務(wù)器的地址。
host_port = int(sys.argv[1])
host_address = gethostbyname(gethostname())
# 2. 創(chuàng)建客戶端套接字以啟動(dòng)與 web 服務(wù)器的 TCP 連接。
tcp_client = socket(AF_INET, SOCK_STREAM)
tcp_client.connect((host_address , host_port))
# 3. 輸入要查詢 web服務(wù)器的文件客戶端。
print("Hello, which document do you want to query?")
while True:
obj = input("I want to query: ")
# 4. 發(fā)送 HTTP 請(qǐng)求報(bào)文。
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響應(yīng)報(bào)文并將其打印到控制臺(tái)。
data = tcp_client.recv(1024)
if not data:
break
print("Web server responded to your request:")
print(data.decode())
tcp_client.close() # 關(guān)閉當(dāng)前的連接。
# 6. 詢問(wèn)客戶是否要繼續(xù)。
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
輸出結(jié)果
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
Client.py
Web server responded to your request:
HTTP/1.1 200 OK
Web server responded to your request:
Hello World HTML
Hello World
Web server responded to your request:
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
Web代理服務(wù)器
目的
此任務(wù)是構(gòu)建一個(gè)簡(jiǎn)單的 web 代理服務(wù)器。根據(jù)第4講(Web 和 HTTP)中所學(xué)到的知識(shí), web 代理服務(wù)器充當(dāng)客戶端和服務(wù)器,這意味著它具有 web 服務(wù)器和客戶端的所有功能。它和 web 服務(wù)器之間最顯著的區(qū)別是發(fā)送的請(qǐng)求報(bào)文
和響應(yīng)報(bào)文都要通過(guò) web 服務(wù)器傳遞。web 代理服務(wù)器在任何地方(大學(xué),公司和住宅ISP)使用,以減少客戶請(qǐng)求和流量的響應(yīng)時(shí)間。
原理
web 代理服務(wù)器的原理基于 web 服務(wù)器。web 代理服務(wù)器從客戶端接收 HTTP 請(qǐng)求報(bào)文,并從請(qǐng)求行中提取方法類型。
- 如果請(qǐng)求類型是 GET ,從同一行獲取URL并檢查請(qǐng)求的對(duì)象是否存在于緩存中。否則,web 代理服務(wù)器會(huì)將客戶端的請(qǐng)求轉(zhuǎn)發(fā)給 web 服務(wù)器。然后,web 服務(wù)器將生成響應(yīng)報(bào)文并將其傳遞給 web 代理服務(wù)器,web 代理服務(wù)器又將其發(fā)送到客戶端并為將來(lái)的請(qǐng)求緩存副本。
- 如果請(qǐng)求類型是 DELETE ,代理服務(wù)器會(huì)首先確認(rèn)請(qǐng)求,如果對(duì)象存在緩存中,j只是從緩存中刪除它并發(fā)送帶有 Status-Code 200 的 HTTP 響應(yīng)報(bào)文。否則,web 代理服務(wù)器發(fā)送帶有 Status-Code 404 的 HTTP 響應(yīng)報(bào)文。
- 如果請(qǐng)求類型是 POST , 處理過(guò)程比上述方法類型更容易,web 代理服務(wù)器只是以二進(jìn)制格式將對(duì)象寫入磁盤,然后發(fā)送帶有 Status-Code 200 的 HTTP 響應(yīng)報(bào)文(輸入在實(shí)體行中上傳)。如果方法是 PUT,則只需要在實(shí)體行中返回 true。
- 如果請(qǐng)求類型是 HEAD , web 代理服務(wù)器僅返回 HTTP 響應(yīng)報(bào)文的報(bào)文首部行和狀態(tài)行。
函數(shù)實(shí)現(xiàn)
基于上述原理,首先,創(chuàng)建一個(gè)支持 IPv4 的套接字并將其綁定在高于1024的端口上。web 代理服務(wù)器和 web 服務(wù)器非常類似,唯一區(qū)別是它是單線程的。
我在 StrProcess 類中添加了其他方法使 web 代理服務(wù)器獲取對(duì)象或連接到 web 服務(wù)器的效率更高。
StrProcess 類源碼:
class StrProcess(str):
def __init__(self, str):
"""用字符型變量 str 初始該屬性"""
self.str = str
def split_str(self):
"""實(shí)現(xiàn)分割操作"""
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 請(qǐng)求行中提取請(qǐng)求方法"""
return self.split_str()[0]
def get_body(self):
"""從 HTTP 請(qǐng)求報(bào)文實(shí)體行中提取數(shù)據(jù)"""
body_start = self.str.find('\r\n\r\n') + 4
return self.str[body_start:]
def get_referer(self):
"""從 HTTP 請(qǐng)求行中提取引用"""
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 請(qǐng)求請(qǐng)求行中提取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):
"""將所有特殊符號(hào)轉(zhuǎn)換為 "-"。"""
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中提取主機(jī)名"""
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
創(chuàng)建套接字后,web 代理服務(wù)器需要處理不同的 HTTP 請(qǐng)求類型和異常。在這部分中,文件處理應(yīng)該以二進(jìn)制格式使用,我們必須考慮對(duì)象類型(可以是.jpg,.svg,.ico等)。
處理 HTTP 請(qǐng)求源碼:
def start_listen(tcp_socket, client_ip, client_port):
# 1. 在連接套接字上從客戶端接收請(qǐng)求報(bào)文。
message = tcp_socket.recv(1024).decode()
# 從strProc類創(chuàng)建新對(duì)象(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. 從報(bào)文中提取所請(qǐng)求對(duì)象的路徑(HTTP 首部行的第二部分)。
filename = handle_str.change_name()
# 3. 找到特定的方法類型并處理請(qǐng)求。
if command == "DELETE" :
# 刪除緩存中存在的對(duì)象
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))
# 檢查請(qǐng)求的對(duì)象是否存在。
f = open("./Cache/" + filename, "rb")
file_content = f.readlines()
f.close()
if command == "GET":
print("File in cache!")
# 從磁盤中查找相應(yīng)的文件(如果存在)。
for i in range(0, len(file_content)):
tcp_socket.send(file_content[i]) # 發(fā)送 HTTP 響應(yīng)報(bào)文。
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")
# 僅發(fā)送 HTTP 響應(yīng)報(bào)文首部行。
tcp_socket.send(list_to_str[:HTTP_header_end+4].encode())
elif command == "PUT" or command == "POST": # 只實(shí)現(xiàn)上傳文件。
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":
# 在代理服務(wù)器上創(chuàng)建套接字。
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 # 記錄真實(shí)的主機(jī)名。
request = "GET " + "http:/" + file + " HTTP/1.1\r\n\r\n"
except:
try:
# 需要使用全局主機(jī)或引用主機(jī)名。
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())
# 將響應(yīng)讀入緩沖區(qū)。
print("The proxy server has found the host.")
# 在緩存中為請(qǐng)求的文件創(chuàng)建一個(gè)新文件。
# 此外,將緩沖區(qū)中的響應(yīng)發(fā)送到客戶端套接字和緩存中的相應(yīng)文件。
writeFile = open("./Cache/" + filename, "wb")
print("The proxy server is receiving data...")
# 接受 HTTP 響應(yīng)報(bào)文直到所有報(bào)文都被接收。
while True:
data = c.recv(4096)
if not data:
break
sys.stdout.write(">")
# 將文件的內(nèi)容發(fā)送到套接字。
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))
# 關(guān)閉客戶端的套接字。
print("tcp socket closed\n")
tcp_socket.close()
輸出結(jié)果
瀏覽器測(cè)試
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.
The proxy server is receiving data...
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>100%
tcp socket closed
源碼
如果我的文章可以幫到您,勞煩您點(diǎn)進(jìn)源碼點(diǎn)個(gè) ★ Star 哦!
https://github.com/Hephaest/C...
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元

