1. 程式人生 > >網絡讀書筆記-應用層

網絡讀書筆記-應用層

pytho sina 路由器 輸出 pin timestamp example specific 觀察

第一章:計算機網絡和因特網

因特網最初就是基於“一群相互信任的用戶連接到一個透明的網絡上”這樣的模型;身處現代計算機網絡則應當有:”在相互信任的用戶之間的通信是一種例外而不是規則“的覺悟。

介紹一些網絡的背景知識。從網絡的邊緣開始,觀察端系統和應用程序,以及運行在端系統上為應用程序提供的運輸服務。觀察了接入網中能找到的鏈路層技術和物理媒體。進入網絡核心看到分組交換和電路交換技術是通過網絡傳輸數據的兩種基本方法。研究了全球性的因特網(網絡的網絡)結構。

研究了計算機網絡的幾個重要主題。分析分組交換網中的時延、吞吐量和丟包的原因。得到傳輸時延、傳播時延和排隊時延以及用於吞吐量的簡單定量模型。

第二章:應用層

UDP socket 編程

# coding:utf-8
# UDP 客戶端

from socket import socket, AF_INET, SOCK_DGRAM
serverName = 'localhost'
serverPort = 12000
clientSocket = socket(AF_INET, SOCK_DGRAM)
message = input("input: ").encode('utf-8')
clientSocket.sendto(message, (serverName, serverPort))
message, serverAddress = clientSocket.recvfrom(2048)
print(message.decode('utf-8'), "from ", serverAddress)
clientSocket.close()
# coding:utf-8
# UDP 服務器端

from socket import socket, AF_INET, SOCK_DGRAM
serverPort = 12000
serverSocket = socket(AF_INET, SOCK_DGRAM)
serverSocket.bind(('', serverPort))
print("the server is ready to receive")
while True:
    print('waiting... ')
    message, clientAddress = serverSocket.recvfrom(2048)
    print(f"received {message}, from {clientAddress}")

    if message == b'bye':
        serverSocket.sendto(b'I see u.', clientAddress)
        break
    modifiedMessage = message.upper()
    serverSocket.sendto(modifiedMessage, clientAddress)

TCP socket 編程

# coding:utf-8
# TCP 客戶端

from socket import socket, AF_INET, SOCK_STREAM
serverName = ''
serverPort = 12000
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((serverName, serverPort))
# 多傳回一條歡迎信息
print(clientSocket.recv(1024).decode('utf-8'))
while True:
    sentence = input('input: ').encode('utf-8')
    clientSocket.send(sentence)
    if sentence == b'bye':
        break
    message = clientSocket.recv(1024)
    print(message.decode('utf-8'))

clientSocket.close()
# coding:utf-8
# TCP 服務器端

from socket import socket, AF_INET, SOCK_STREAM
serverPort = 12000
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind(('', serverPort))
serverSocket.listen(4)
print("the server is ready to receive")
while True:
    connectionSocket, addr = serverSocket.accept()
    print(f'new connection from {addr}')
    connectionSocket.send(b'Welcome')

    while True:
        sentence = connectionSocket.recv(1024)
        print(f"received {sentence}., from {addr}")
        if sentence == b'bye':
            break
        message = sentence.upper()
        connectionSocket.send(message)

    connectionSocket.close()

作業與實驗

想找配套資源的服務器代碼,沒找到。既然資源如此難找,何不自己做作業,當做困難模式。

Socket編程作業
1. Web Server

題目:編寫一個簡單的Web服務器,一次處理一個請求,如果瀏覽器請求一個不存在的文件,則響應404 Not Found


Web服務器之前通過廖雪峰老師Python實戰博客時有過一些了解,但是不夠深入,只是看著敲,一些東西不夠了解比如HTTP和TCP。這次再學習一下。

最簡單的Web服務器就把上面的TCP服務器拿過來改一下就好了,響應值按照HTTP協議響應報文格式定義的來:

# coding:utf-8
# 服務器

from socket import socket, AF_INET, SOCK_STREAM
host, port = '', 8005
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind((host, port))
serverSocket.listen(1)
print(f"{host}:{port} ready to receive")
while True:
    connection, address = serverSocket.accept()
    print(f'new connection from {address}')
    request = connection.recv(1024)
    print(request)
    # 響應值按HTTP協議響應報文格式來
    response = """HTTP/1.1 200 OK

HEllo
"""
    connection.sendall(response.encode('utf-8'))
    connection.close()
serverSocket.close()

使用WireShark抓包看看HTTP數據:(註:為了抓本地回環包,將host改為了本機IP,或者直接留空即可,註意127.0.0.1抓不到,只能是本機IP)

技術分享圖片

圖中可以看到兩次HTTP請求(有網站圖標請求 GET /favicon.ico HTTP/1.1),兩次請求分別建立了一次TCP連接(進程ID為:49353和49354)。服務器響應體都為HEllo

再通過telnet來建立一條TCP連接研究下:telnet 192.168.10.211回車後則建立了TCP連接,等待請求報文,查看服務器窗口得知TCP端口為:52915,使用命令netstat -ano | findstr "52915"查看該TCP:

技術分享圖片

為什麽是兩條呢?因為TCP連接是全雙工的。telnet頁面回車則得到響應,之後再查詢該端口:

C:\Users\onion>netstat -ano | findstr "52915"
  TCP    192.168.10.211:8005    192.168.10.211:52915   TIME_WAIT       0

沒有按預期的來,本以為兩條都沒了,為什麽留了一條從服務器到客戶端的TCP沒關閉呢?此TCP的狀態為TIME_WAIT,問題就出在這了。3.5.6TCP連接管理小節中有說明。這裏貼一段網上的解釋了解一下:

根據TCP協議定義的3次握手斷開連接規定,發起socket的一方主動關閉,socket將進入TIME_WAIT狀態。TIME_WAIT狀態將持續2個MSL(Max Segment Lifetime),在Windows下默認為4分鐘,即240秒。

說回本題,針對題目要求,只需要訪問特定的接口(通過請求頭中的path判斷),其他接口拋404。

while True:
    connection, address = serverSocket.accept()
    print(f'new connection from {address}')
    request = connection.recv(1024)
    print(request)

    try:
        path = re.findall(r'^\w+ +/([^ ]*)', request.decode('utf-8'))[0]
    except Exception:
        path = None

    if path == 'home':
        response = """HTTP/1.1 200 OK

hello, network.
"""
    elif path == 'index':
        response = """HTTP/1.1 301 Move
Location: home

""" 
    else:
        response = """HTTP/1.1 404 Not Found

<html>
<body><h2>404</h2></body>
</html>
"""

    connection.sendall(response.encode('utf-8'))
    connection.close()

獲取到path之後判斷,訪問/home顯示文字(200),訪問/index跳轉(301重定向)到/home,其他的路由則報錯404

復盤

P.S. 做第二題 UDP Pinger的時候,無意中找到了myk502/Top-Down-Approach, 包含有自頂向下書中配套資源,特別是WireShark Labs多個PDF很有意義。 既然找到了資源,那就拿書中的來復盤一下第一題。

翻到框架代碼示例看了一下,書中(以下書中也包含配套資源)是讀取相應的HTML文件,然後響應,但是發送數據有奇怪的一處不理解:為什麽用循環發送?

for i in range(0, len(outputdata)):
    connectionSocket.send(outputdata[i])

是send和sendall的區別

socket.send(string[, flags])? 發送TCP數據,返回發送的字節大小。這個字節長度可能少於實際要發送的數據的長度。換句話說,這個函數執行一次,並不一定能發送完給定的數據,可能需要重復多次才能發送完成。

data = "something you want to send"
while True:
  len = s.send(data[len:])
  if not len:
      break

socket.sendall(string[, flags])?? 看懂了上面那個,這個函數就容易明白了。發送完整的TCP數據,成功返回None,失敗拋出異常。

python socket函數中,send 與sendall的區別與使用方法

*題外話:這篇短文寫的簡單且清晰,對於只想知道區別的人很受益,然而下面評論中卻出現謾罵的人,舉報需要登錄,登錄卻還要驗證手機便作罷,CSDN越來越沒落了。諷刺的是這個jeck_cen自己掛的幾篇OJ代碼卻全沒有對齊過。

打開文件版Web服務器:

# coding:utf-8
# Web 服務器 v1.1 打開文件響應

from socket import socket, AF_INET, SOCK_STREAM

host, port = 'xxx.xxx.xxx.xxx', 8005
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind((host, port))
serverSocket.listen(2)
print(f"{host}:{port} ready to receive")
while True:
    connection, address = serverSocket.accept()
    print(f'new connection from {address}')

    try:
        request = connection.recv(1024)
        print(request)
        # 獲取文件名,並讀取數據
        filename = request.split()[1][1:]
        with open(filename, encoding='utf-8') as f:
            outputdata = f.read()
        # 發送HTTP響應頭
        header = b"HTTP/1.1 200 OK\r\n\r\n"
        connection.send(header)
        print(outputdata)

        # 沒必要用單字符發,經驗證直接send/sendall都會保證數據傳輸
        # for i in range(0, len(outputdata)):
        #     print(f"send:{outputdata[i]}")
        #     connection.send(outputdata[i].encode())
        connection.send(outputdata.encode())

    except Exception as e:
        print(e)
        header = b"HTTP/1.1 404 Not Found\r\n\r\n"
        connection.send(header)

    connection.close()
serverSocket.close()

進階練習

  1. 使用多線程同時處理多個請求

    將上文中服務器與客戶端的TCP連接封裝一個函數tcpLink(),像這樣:

    from socket import socket, AF_INET, SOCK_STREAM
    
    def tcpLink(sock, addr):
        """ TCP 連接 """
        print(f'new connection from {addr}')
        try:
            request = connection.recv(1024)
            print(request)
            # 獲取文件名,並讀取數據
            filename = request.split()[1][1:]
            with open(filename, encoding='utf-8') as f:
                outputdata = f.read()
            # 發送HTTP響應頭
            header = b"HTTP/1.1 200 OK\r\n\r\n"
            connection.send(header)
            # 發送數據
            connection.send(outputdata.encode())
    
        except Exception as e:
            print(e)
            header = b"HTTP/1.1 404 Not Found\r\n\r\n"
            connection.send(header)
        print(f"{addr} close.")
        sock.close()
    
    host, port = '', 8005
    serverSocket = socket(AF_INET, SOCK_STREAM)
    serverSocket.bind((host, port))
    serverSocket.listen(4)
    print(f"{host}:{port} ready to receive")
    while True:
        connection, address = serverSocket.accept()
        # 新建函數處理TCP連接
        tcpLink(connection, address)
    
    serverSocket.close()

    上面的代碼只是單線程,只能同時處理一個請求。錄個圖:

    技術分享圖片

    瀏覽器請求可以成功,加上telnet請求連接阻塞後,瀏覽器再次請求就阻塞(卡)了,telnet處理完成,瀏覽器又能得到結果。

    在試驗中發現另一個問題,訪問頁面之後經常會自動有一條TCP連接在連接中就導致阻塞了,大概是瀏覽器偷偷請求或者其他原因吧。先不管了,反正要用多線程的。

    多線程版

    怎麽加多線程呢,上面剝離出去的tcpLink()已經做好了工作,只需要加上多線程調用就好了。

    tcpLink(connection, address) 改為:

    # 使用新線程來處理TCP連接
    t = threading.Thread(target=tcpLink, args=(connection, address))
    t.start()

    之前別忘記引入, import threading

    技術分享圖片

    圖中使用telnet發起TCP連接(前面1個,後面4個),瀏覽器一樣可以正常請求,不會被阻塞。

  2. http客戶端

    與其使用瀏覽器, 不如編寫自己的 http 客戶端來測試您的服務器。客戶端將使用 tcp 連接連接到服務器, 向服務器發送 http 請求, 並將服務器響應顯示為輸出。您可以假定發送的 http 請求是 get 方法。

    客戶端應采用命令行參數, 指定服務器 ip 地址或主機名、服務器偵聽的端口以及請求的對象存儲在服務器上的路徑。下面是用於運行客戶端的輸入命令格式:client.py server_host server_port filename

    不深究顯示頁面(只輸出)和資源請求(比如圖片和CSS\JS等不用請求)的話,那就很簡單了。只是發送一個HTTP請求即可。

    # coding:utf-8
    # HTTP客戶端。 格式:client.py server_host server_port filename
    
    from socket import socket, AF_INET, SOCK_STREAM
    import sys
    
    args = sys.argv[1:]
    if len(args) != 3:
        print(r"(參數不足) 格式:.\client.py server_host server_port filename")
        exit()
    
    host, port, filename = args
    
    # 創建Socket, 建立連接
    clientSocket = socket(AF_INET, SOCK_STREAM)
    clientSocket.connect((host, int(port)))
    
    # 發送請求
    request = f"GET {filename} HTTP/1.1\r\n\r\n"
    clientSocket.send(request.encode())
    
    # 接收響應數據
    while True:
        response = clientSocket.recv(1024)
        # 無數據則退出
        if not response:
            break
        print(response.decode(), end='')
    
    clientSocket.close()

    技術分享圖片

    圖中執行三次客戶端,第一次參數不足,不請求。後面兩次都為HTTP請求,一次404,一次請求正確的資源並得到響應。

    在代碼中使用while接收響應數據,直到接收到為空則退出(服務器已關閉,但是用recv()可以獲取到空值,如果不檢測則會無限得到空數據)。正考慮有沒有優雅的辦法從客戶端檢測服務端已關閉狀態,看到函數說明,就釋然了。

    recv(buffersize[, flags]) -> data

    Receive up to buffersize bytes from the socket. For the optional flags argument, see the Unix manual. When no data is available, block until at least one byte is available or until the remote end is closed. When the remote end is closed and all data is read, return the empty string.

    當遠程端關閉並讀取所有數據時, 返回空字符串。

2. UDP Pinger

題目:創建一個非標準(但簡單)的基於UDP的客戶ping程序。
書中提供了服務端代碼。使用rand(0,10)<4模擬丟包。

Packet Loss
UDP provides applications with an unreliable transport service. Messages may get lost in the network due to router queue overflows, faulty hardware or some other reasons.

丟包
udp 為應用程序提供了不可靠的傳輸服務。由於路由器隊列溢出、硬件故障或其他一些原因, 消息可能會在網絡中丟失。

Specifically, your client program should

(1) send the ping message using UDP (Note: Unlike TCP, you do not need to establish a connection
first, since UDP is a connectionless protocol.)

(2) print the response message from server, if any

(3) calculate and print the round trip time (RTT), in seconds, of each packet, if server responses

(4) otherwise, print “Request timed out”


Windows ping 了解

雖然不會采用ICMP協議,但是可以模仿Windows的ping顯示信息。先來了解一下

技術分享圖片

Windows的ping程序通過ICMP協議發送32字節數據,內容是abcdefghijklmnopqrstuvwabcdefghi。統計信息不管超時還是正常都會有;往返行程估計時間只有在有成功的情況下才有。每一條信息分析:字節拿到了,時間可以計算到,TTL是什麽呢?和Redis一樣,都是生存時間。

字節代表數據包的大小,時間顧名思義就是返回時間,“TTL”的意思就是數據包的生存時間,當然你得到的這個就是剩余的生存時間。TTL用來計算數據包在路由器的消耗時間,因為現在絕大多數路由器的消耗時間都小於1s,而時間小於1s就當1s計算,所以數據包沒經過一個路由器節點TTL都減一。

我的是系統默認TTL為128 (2^7),經過了一個路由器,所以為上圖中我ping本機IP的TLL是127。

C:\Users\onion>tracert 192.168.10.211
通過最多 30 個躍點跟蹤
到 DESKTOP-VIMN0V8 [192.168.10.211] 的路由:
  1     6 ms     3 ms     4 ms  bogon [192.168.10.1]
  2    95 ms    11 ms    20 ms  DESKTOP-VIMN0V8 [192.168.10.211] # 終點不算
跟蹤完成。

TTL可以先放棄,這個路由器跳數沒有好的思路。往返統計時間中平均為成功的統計,比如上圖(23+39)/2=31

客戶端編寫

# coding:utf-8
# ping程序 客戶端

from socket import socket, AF_INET, SOCK_DGRAM
import time

# 配置
host, port = '192.168.10.211', 12000
times = 10  # 次數
timeout = 1  # 超時時間

# 創建Socket
clientSocket = socket(AF_INET, SOCK_DGRAM)

# 設置超時時間為1s
clientSocket.settimeout(timeout)

print(f"\n正在Ping {host}:{port} 具有 32 字節的數據:(為什麽有端口,因為俺是UDP啊)")

success_ms = []  # 成功接收用時,用於統計
for i in range(1, times+1):
    message = "abcdefghijklmnopqrstuvwabcdefghi"
    clientSocket.sendto(message.encode('utf-8'), (host, port))
    try:
        # 計算時間,由於上面設置timeout,超過會拋time out
        start_ms = int(time.time()*1000)
        rep_message, serverAddress = clientSocket.recvfrom(2048)
        end_ms = int(time.time()*1000)
        gap = end_ms-start_ms
        print(f"{i} 來自 {host} 的回復:字節=32 時間={gap}ms TTL=?")
        success_ms.append(gap)
    except Exception as e:
        print(f"{i} 請求超時。({e})")
        continue

# 輸出統計信息
print(f"\n{host} 的 Ping 統計信息:")
success_times = len(success_ms)
failed_times = times-success_times
lost_scale = failed_times*100//times
print(f"\t數據包:已發送 = {times},已接收 = {success_times},丟失 = {failed_times} ({lost_scale}% 丟失)")

if success_times>0:
    # 往返行程估計時間
    print("往返行程的估計時間(以毫秒為單位):")
    print(f"\t最短 = {min(success_ms)}ms,最長 = {max(success_ms)}ms,平均 = {sum(success_ms)//success_times}ms")

# 關閉Socket
clientSocket.close()

很清晰易懂的。

技術分享圖片

圖中第一次演示5次Ping,得到了100%丟失,只顯示統計信息;第二次10次Ping,統計信息和往返估計時間都有了,並且正確。

進階練習

  1. 第一題是求往返行程以及丟包率,就是上面已經做過的往返估計時間和統計信息。下一題。

  2. UDP 心跳 (UDP Heartbeat)

    Another similar application to the UDP Ping would be the UDP Heartbeat. The Heartbeat can be used to check if an application is up and running and to report one-way packet loss. The client sends a sequence number and current timestamp in the UDP packet to the server, which is listening for the Heartbeat (i.e., the UDP packets) of the client. Upon receiving the packets, the server calculates the time difference and reports any lost packets. If the Heartbeat packets are missing for some specified period of time, we can assume that the client application has stopped. Implement the UDP Heartbeat (both client and server). You will need to modify the given UDPPingerServer.py, and your UDP ping client.

    這道題按著自己的想法做,感覺和Redis的生存時間差不多。除了服務器端(HeartServer)(設置有效心跳為10秒),心跳客戶端(HeartClient) 還加了一個監聽客戶端(HeartClientShow) 用來顯示存活的客戶端(顯示序號和過期時間,每秒發送一次online udp請求,用來監聽存活序號)。

    技術分享圖片

    代碼略,最後放到git中。

3. STMP

題目:一個簡單的郵件客戶端,可以向收件人發送郵件。

You will gain experience in implementing a standard protocol using Python.
Your task is to develop a simple mail client that sends email to any recipient.

驗證的時候使用AUTH LOGIN命令登錄。我這裏使用新浪的smtp郵件服務器,遇到一個小坑就是新浪的手機郵箱登錄,驗證一直提醒535 the email account of mobile is non-active換了字母郵箱就可以了。

打招呼的時候建議使用EHLO而不是舊的ECHO當發出 EHLO 命令以啟動 ESMTP 連接時,服務器的響應指出 SMTP 虛擬服務器支持的功能

When using authentication, EHLO should be used for the greeting to indicate that?Extended SMTP?is in use, as opposed to the deprecated HELO greeting,[10]?which is still accepted?when no extension is used, for backward compatibility.

# coding:utf-8
# SMTP 郵件客戶端

from socket import socket, AF_INET, SOCK_STREAM
import base64

def tcp_send(cli, message, except_code):
    print("C: " + message)
    cli.send((message+'\r\n').encode())
    response = cli.recv(1024)
    response = response.decode()
    print("S: " + response)
    if response[:3] == str(except_code):
        return response
    raise Exception(response[:3])

# Choose a mail server (e.g. Google mail server) and call it mailserver
mailServer = 'smtp.sina.cn'
mailPort = 25
# Create socket called clientSocket and establish a TCP connection with mailserver
clientSocket = socket(AF_INET, SOCK_STREAM)
clientSocket.connect((mailServer, mailPort))
clientSocket.settimeout(5)
# 郵件服務器連接響應信息
response = clientSocket.recv(1024).decode()
print('S: ' + response)

if response[:3] != '220':
    print('220 reply not received from server.')

# Send HELO command and print server response. 打招呼
heloCommand = 'EHLO Alice'
tcp_send(clientSocket, heloCommand, 250)

# mail 驗證
tcp_send(clientSocket, 'AUTH LOGIN', 334)

username = base64.b64encode(b'[email protected]').decode()
password = base64.b64encode(b'password').decode()
tcp_send(clientSocket, username, 334)
tcp_send(clientSocket, password, 235)

# Send MAIL FROM command and print server response.
tcp_send(clientSocket, 'MAIL FROM: <[email protected]>', 250)
# Send RCPT TO command and print server response.
tcp_send(clientSocket, 'RCPT TO: <[email protected]>', 250)
# Send DATA command and print server response.
tcp_send(clientSocket, 'DATA', 354)
# Send message data.
message = '''From: [email protected]
To: [email protected]
Subject: tcp mail client

hello
this is mail client by python tcp.
.'''
tcp_send(clientSocket, message, 250)
# Send QUIT command and get server response.
tcp_send(clientSocket, 'QUIT', 221)

發送成功:

技術分享圖片

執行過程:C代表客戶端,S服務器。

PS F:\py\network\ApplicationLayer\SMTP> python .\client.py
S: 220 smtp-5-121.smtpsmail.fmail.xd.sinanode.com ESMTP

C: EHLO Alice
S: 250-smtp-5-121.smtpsmail.fmail.xd.sinanode.com
250-AUTH LOGIN PLAIN
250-AUTH=LOGIN PLAIN
250-STARTTLS
250 8BITMIME

C: AUTH LOGIN
S: 334 VXNlcm5hbWU6

C: Y293cGVhd2ViQHNpbmEuY24=
S: 334 UGFzc3dvcmQ6

C: ajExMTIyMjU1NTQ0NA==
S: 235 OK Authenticated

C: MAIL FROM: <[email protected]>
S: 250 ok

C: RCPT TO: <[email protected]>
S: 250 ok

C: DATA
S: 354 End data with <CR><LF>.<CR><LF>

C: From: [email protected]
To: [email protected]
Subject: tcp mail client

hello
this is mail client by python tcp.
.
S: 250 ok queue id 290028394066

C: QUIT
S: 221 smtp-5-121.smtpsmail.fmail.xd.sinanode.com

但這樣傳輸是不安全的,郵箱名和密碼都只是簡單的base64編碼,等於明文。圖中MTIzNDU2則是123456的編碼。

技術分享圖片

所以有了Transport Layer Security (TLS) or Secure Sockets Layer (SSL) 。

進階練習

  1. 添加TLS/SSL來安全傳輸數據。

    Mail servers like Google mail (address: smtp.gmail.com, port: 587) requires your client to add a Transport Layer Security (TLS) or Secure Sockets Layer (SSL) for authentication and security reasons, before you send MAIL FROM command. Add TLS/SSL commands to your existing ones and implement your client using Google mail server at above address and port.

    之前通過EHLO打招呼之後,看到郵件服務器支持STARTTLS( start tls 的意思),新浪smtp是587端口。請求之後,服務器響應220 ready for tls.

    C: STARTTLS
    S: 220 ready for tls

    然後我就是一臉懵逼的,沒有使用過TLS/SSL。

    How to test SMTP Authentication and StartTLS 中得知可以使用openssl s_client -connect smtp.example.com:25 -starttls smtp 連接SMTP服務器,win10下載openssl把命令替換為smtp.sina.cn:587是可以的,但是我需要通過python去訪問,該怎麽辦呢... 怎麽辦呢?

    技術分享圖片

    然後就找了一找, Connect to SMTP (SSL or TLS) using Python 裏發現ssl.wrap_socket函數。

    STARTTLS之後再調用 ssl.wrap_socket(clientSocket),試了一下,果然可以:

    ...
    # Send HELO command and print server response. 打招呼
    heloCommand = 'EHLO Alice'
    tcp_send(clientSocket, heloCommand, 250)
    
    # TLS/SSL 加密傳輸
    tcp_send(clientSocket, 'STARTTLS', 220)
    clientSocket = ssl.wrap_socket(clientSocket)
    
    # mail 驗證
    tcp_send(clientSocket, 'AUTH LOGIN', 334)
    ...

    技術分享圖片

    清晰的看到之後的傳輸加密了。協議為TLSv1.2。

  2. 現在只能發送文本,修改客戶端,使其可以發送文本和圖片。interesting

    使用新浪發送一個帶圖片的郵件,收件箱查看郵件原文:

    MIME-Version: 1.0
    X-Priority: 3
    X-MessageID: 5c9338d62381243a_201903
    X-Originating-IP: [10.41.14.100]
    X-Mailer: Sina WebMail 4.0
    Content-Type: multipart/related; type="multipart/alternative";
      boundary="=-sinamail_rel_ebd2ac5ebf979d4cc71ee25713127299"
    Message-Id: <[email protected]>
    ...
    # 下面為base64的圖片
    --=-sinamail_rel_ebd2ac5ebf979d4cc71ee25713a27299
    Content-ID: <part1.5c9338d62381243a_201903>
    Content-Type: image/gif; name="=?GBK?B?uL28/jEuZ2lm?="
    Content-Disposition: attachment; filename="=?GBK?B?uL28/jEuZ2lm?="
    Content-Transfer-Encoding: base64
    
    R0lGODdh1AADAfcAAAAAABIWCgoXCA4XIwwaCg4aGw4aIxQaAw4bExIbDBIcGxIdJRsdChIeKxMe
    EhMgMxohExohJBshKyghGBsiGyQiKhkkPBskMygkCzMlFxsmRB4qSyMrQSMsTiguMSkuGjQuKTcu
    GikvIy0yPiszUj80KDY7QTg9LTE+VzI/TUFCTkZELUJFYXNGVGRHXXlIW0JKQUNPW1dQOURRUERR
    aXpSallYUFpZQ0tccFRdX2ReQmZec3xecmNfTldgblxgX5FhdVRigGtiRX9ifmJkXW1kTGdqXW5q
    T2JraHRrTnNsUnltUYNtgGtuZ2Fvd5lwgmNxhHRxWnRxh4NxjHZyUnpyT49ylGx0a310VWt1e2x1

    不可避免的要研究一下MIME了。

    發送一個4k的圖片發送完成,可是稍微大一點的文件,就會出錯:

    Traceback (most recent call last):
      File ".\client_send_image.py", line 93, in <module>
        tcp_send(clientSocket, message, 250)
      File ".\client_send_image.py", line 12, in tcp_send
        response = cli.recv(1024)
    ConnectionResetError: [WinError 10054] 遠程主機強迫關閉了一個現有的連接。

    去掉SSL抓包發現,數據TCP傳輸未完成時,變為STMP傳輸,但是攜帶的數據還是圖片中的上面TCP未傳完的數據。之前狀態有 FIN,ACK, 之後就RST

    技術分享圖片

    網上有帖子說,FIN,ACK 就是準備斷開連接了。又發現Ack之前一直是286。再試,有時沒有FIN,有時會有PSH協議,發送短一點的內容則會SMTP|IMF發送成功……耗費好久,重傳?socket緩存?感覺是個小問題,只是沒懂TCP的報文,去讀書。

    續:第二天來看,發現RST之前的SMTP攜帶了126條運輸層報文段,共182448字節。針對上圖就是53882發送到587的所有未接受數據。而且,服務器也一直未確認286號ACK,雙方互相不確認。緩存區滿了?Win是緩存區大小。

    續2:看完第三章來看。好好分析了一波。搜索 smtp tcp data fragment

    Wireshark?decode?SMTP.?The?content?of?an?email?(headers?+?body)?is?sentafter?the?SMTP?DATA?command.?If?that?content?is?larger?than?one?TCPsegment,?Wireshark?will?show?every?packet?that?belongs?to?the?DATA"command"?as?"C:?DATA?fragment"?in?the?Info?column.?So,?those?packets?arebasically?the?content?of?the?email.?You?can?see?the?whole?SMTPcommunication.

    續3:請教群裏的大佬,大佬說包看著沒問題,可能是服務器設置的問題(緩存之類的)。打算換一個服務商看看。用GMAIL吧。python使用socks代理, PySocks OK。然而加了代理(127.0.0.1:1080)之後,包抓不到,放棄。這個問題耗費了太久時間,以後再來挑戰。

    續4:用騰訊企業郵箱的時候,看到也有SMTP服務,就試了一下,可以發送成功。下圖右側為郵件原文。

    技術分享圖片

    4. HTTP Web Proxy Server (多線程Web代理服務器)

    開發一個小型(且簡單)的 web 代理服務器, 只理解簡單的 get 請求, 但能夠處理各種對象:不僅是 HTML 頁面, 而且包含圖像。

    客戶端通過代理服務器請求對象,代理服務器將客戶端的請求轉發到 web 服務器。然後, web 服務器將生成響應消息並將其傳遞到代理服務器, 而代理服務器又將其發送到客戶端。

    進階:

    1. 增加錯誤處理;
    2. 除了GET請求,增加POST請求;
    3. 添加緩存,並驗證緩存是否有效RFC 2068.

    技術分享圖片

    遇到makefile問題

    書中的框架代碼用了socket.makefile(),用write發送數據readlinds()獲取響應,過了好久好久才能發送請求並收到響應。排查發現是因為 HTTP/1.1,有可能是KEEP-ALIVE長連接原因導致的timeout

    使用socket.makefilesocket.send/recv 發送請求和獲取響應對比:

    # 發送請求
    fileobj = c.makefile('wr', encoding='utf-8')
    request = f"GET http://{filename}/ HTTP/1.0\nHost: {hostn}\n\n" # 這裏使用HTTP/1.1協議則會有長連接問題出現
    fileobj.write(request)
    fileobj.flush()
    
    # 接收響應
    response_data = ''
    for line in fileobj.readlines():
     response_data += line
    request = f"GET http://{filename}/ HTTP/1.0\nHost: {hostn}\n\n"
    c.send(request.encode())
    response_data = c.recv(1024)

    之後寫文件就可以了。

    代理方式

    代理有兩種使用方式,一種是使用IP地址+端口將請求定向到代理服務器http://localhost:8888/www.google.com,還有一種是設置瀏覽器代理。這兩種的請求數據是不同的,地址訪問的話是這種:

    訪問 http://192.168.10.120:1081/www.warcraft0.com
    源請求 b'GET /www.warcraft0.com HTTP/1.1\r\nHost: 192.168.10.120:1081\r\nConnection: keep-alive\r\nUpgrade-In...

    瀏覽器設置代理是這種:

    訪問 http://www.warcraft0.com
    源請求: b'GET http://www.warcraft0.com/tools HTTP/1.1\r\nHost: www.warcraft0.com\r\nProxy-Connection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1

    第一種地址訪問只是調試性質,為了完整的獲取請求類型、http/https協議、主機和資源URL,所以針對第二種處理方式開發。而且只考慮HTTP網站(巴特,現在HTTPS很多了找HTTP還不好找,用自己的來)。

    針對 GET http://www.warcraft0.com/tools HTTP/1.1\r\nHost: www.warcraft0.com\r\n請求,

    # 獲取請求的屬性
    http_method = message.split()[0]
    req_path = message.split()[1]
    path_data = re.findall(r'((https?):\/\/([^/]+))?(\/.+)?', req_path)
    
    if path_data[0] and http_method in ['GET', 'POST']:
        _, r_protocal, r_host, r_url = path_data[0]
        print(http_method, r_protocal, r_host, r_url)

    輸出 GET http www.warcraft0.com /tools 分別是請求類型、協議、主機(域名)和 URL。

    將緩存儲存到以每個請求哈希命名的文件上hash(req_path)

    這樣就可以建立一個socket連接到域名的80端口,socket.socket(r_host, 80)。之後就可以發送GET請求和接收響應了。

    處理POST請求

    POST請求需要獲取額外的請求數據,以及長度。下例的變量:message為源請求,獲取部分參數組裝為代理請求request

    # 獲取post參數
    r_post_data = re.findall(r'\r\n\r\n((.|\n)*)', message)[0][0]
    r_content_type = re.findall(r'Content-Type: ([^\r\n]*)', message)[0]
    request = f"""POST http://{r_host}{r_url} HTTP/1.0
    Host: {r_host}
    Content-Type: {r_content_type}
    Content-Length: {len(r_post_data)}
    
    {r_post_data}"""

    在登錄的時候,有時會錯誤(比如密碼無效或者郵箱無效),然後存儲了緩存文件,只有請求正確的賬號,也會去拿緩存(裏面的錯誤響應)。為了只緩存有效數據,所以寫緩存得條件改為:響應狀態碼為200且是GET請求再存儲。

    # 響應200 + GET請求 時,再做存儲
    resp_code = re.findall("^HTTP[^ ]+ +(\d+)", response)[0]
     if int(resp_code) == 200 and http_method == 'GET':
         with open(cache_file, 'w', encoding='utf-8') as f:
             f.write(response_data)

    為了維護登錄狀態,還得設置cookie。Cookie是在登錄後響應頭中的Set-Cookie數據,比如這種:

    Set-Cookie: cowpeas_blog=15389388760310049489E81B9CF4554983F013049CAB3FC000-1554433316-93c1f6e05dd1e16293c87f4442105eb450055470; HttpOnly; Max-Age=86400; Path=/

    不用存儲,直接把響應返回給客戶,瀏覽器會設置Cookie,之後每次請求需要獲取Cookie頭並發送。

    因為可能會取到圖片等二進制數據,所以緩存存儲為字節類型。現在已經可以完整的處理GET訪問、POST登錄了。有的頁面圖片較多,阻塞模式處理的很慢,有一些圖片請求就被丟掉了,需要加多線程。

    HTTPS代理

    在加多線程之前,應該先解決一下HTTPS代理的問題。現在只能代理HTTP請求,對於HTTPS就束手無策。接收到的請求形如:‘CONNECT clients1.google.com:443 HTTP/1.1\r\nHost: clients1.google.com:443\r\nProxy-Connection: keep-alive\r\n SSL該怎麽加呢?隧道又是什麽?

    p.s. HTTPS、多線程以及緩存有效期的問題,以後再補吧,這裏耗費了太久。

    發現有趣的請求:

    在地址欄輸入時,會調用 suggestion.baidu.zbb.df 獲取百度提醒詞。 以bas為例:

    代理請求 b'GET http://suggestion.baidu.com/su?wd=bas&action=opensearch&ie=UTF-8 HTTP/1.0\nHost: suggestion.baidu.com\n\n'
    響應:
    HTTP/1.1 200 OK
    Date: Thu, 04 Apr 2019 02:48:03 GMT
    Server: suggestion.baidu.zbb.df
    Content-Length: 125
    Content-Type: text/javascript; charset=UTF-8
    Cache-Control: private
    Expires: Thu, 04 Apr 2019 03:48:03 GMT
    Connection: Close
    
    ["bas",["base64 解碼","base64","巴薩","base64 轉圖片","bastion","巴塞羅那","bash","base64 加密","basto","base"]]

其他參考

  • 計網6 書籍配套交互式小程序 可以看動圖,實驗等功能需要註冊

  • CNT 5106 C Computer Networks: Fall 2012

  • 一起寫一個 Web 服務器

  • wireshark抓本地回環包

  • 通訊系統經驗談【一】TCP連接狀態分析:SYNC_RECV,CLOSE_WAIT,TIME_WAIT

  • HTTP狀態碼對照表

  • TCP編程-廖雪峰

  • Ping TTL 的值越小越好?不對!

  • Simple Mail Transfer Protocol

  • SMTP_Authentication

  • 利用telnet進行SMTP的驗證

  • Introduction to Network Programming with Python

  • Set-Cookie響應頭

  • HTTP 代理原理及實現(一)

網絡讀書筆記-應用層