網絡讀書筆記-應用層

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

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

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

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

第二章:應用層

UDP socket 編程node

# 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 編程python

# 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()

做業與實驗git

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

Socket編程做業
1. Web Server

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


Web服務器以前經過廖雪峯老師Python實戰博客時有過一些瞭解,可是不夠深刻,只是看着敲,一些東西不夠了解好比HTTP和TCP。此次再學習一下。spring

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

# 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)

web_http_wireshark.png

圖中能夠看到兩次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_port_52915.png

爲何是兩條呢?由於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()

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

    single_web_server.gif

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

    在試驗中發現另外一個問題,訪問頁面以後常常會自動有一條TCP鏈接在鏈接中就致使阻塞了,大概是瀏覽器偷偷請求或者其餘緣由吧。先無論了,反正要用多線程的。

    多線程版

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

    tcpLink(connection, address) 改成:

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

    以前別忘記引入, import threading

    thread_web_server.gif

    圖中使用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_client.png

    圖中執行三次客戶端,第一次參數不足,不請求。後面兩次都爲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.png

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()

很清晰易懂的。

udppinger.png

圖中第一次演示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請求,用來監聽存活序號)。

    udp_heartbeat.gif

    代碼略,最後放到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'xxxxx@sina.cn').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: <xxxxx@sina.cn>', 250)
# Send RCPT TO command and print server response.
tcp_send(clientSocket, 'RCPT TO: <xxxxx@qq.com>', 250)
# Send DATA command and print server response.
tcp_send(clientSocket, 'DATA', 354)
# Send message data.
message = '''From: xxxxx@sina.cn
To: xxxxx@qq.com
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)

發送成功:

tcp_mail.png

執行過程: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: <xxxxx@sina.cn>
S: 250 ok

C: RCPT TO: <xxxxx@qq.com>
S: 250 ok

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

C: From: xxxxx@sina.cn
To: xxxxx@qq.com
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的編碼。

tcp_nosafe.png

因此有了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去訪問,該怎麼辦呢... 怎麼辦呢?

    win_openssl.gif

    而後就找了一找, 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)
    ...

    tls_smtp_client.png

    清晰的看到以後的傳輸加密了。協議爲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: <20190321071014.F10372000091@webmail.sinamail.sina.com.cn>
    ...
    # 下面爲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

    FINACK.png

    網上有帖子說,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服務,就試了一下,能夠發送成功。下圖右側爲郵件原文。

    smtp_success.png

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

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

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

    進階:

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

    proxy_server.png

    遇到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"]]

其餘參考

相關文章
相關標籤/搜索