因特網最初就是基於「一羣相互信任的用戶鏈接到一個透明的網絡上」這樣的模型;身處現代計算機網絡則應當有:」在相互信任的用戶之間的通訊是一種例外而不是規則「的覺悟。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
題目:編寫一個簡單的Web服務器,一次處理一個請求,若是瀏覽器請求一個不存在的文件,則響應404 Not Found
。web
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)
圖中能夠看到兩次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: breaksocket.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()
進階練習
使用多線程同時處理多個請求
將上文中服務器與客戶端的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個),瀏覽器同樣能夠正常請求,不會被阻塞。
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.
當遠程端關閉並讀取全部數據時, 返回空字符串。
題目:建立一個非標準(但簡單)的基於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,統計信息和往返估計時間都有了,而且正確。
進階練習
第一題是求往返行程以及丟包率,就是上面已經作過的往返估計時間和統計信息。下一題。
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中。
題目:一個簡單的郵件客戶端,能夠向收件人發送郵件。
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)
發送成功:
執行過程: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的編碼。
因此有了Transport Layer Security (TLS) or Secure Sockets Layer (SSL) 。
進階練習
添加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。
如今只能發送文本,修改客戶端,使其能夠發送文本和圖片。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
:
網上有帖子說,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服務,就試了一下,能夠發送成功。下圖右側爲郵件原文。
開發一個小型(且簡單)的 web 代理服務器, 只理解簡單的 get 請求, 但可以處理各類對象:不只是 HTML 頁面, 並且包含圖像。
客戶端經過代理服務器請求對象,代理服務器將客戶端的請求轉發到 web 服務器。而後, web 服務器將生成響應消息並將其傳遞到代理服務器, 而代理服務器又將其發送到客戶端。
進階:
- 增長錯誤處理;
- 除了GET請求,增長POST請求;
- 添加緩存,並驗證緩存是否有效RFC 2068.
遇到makefile問題
書中的框架代碼用了socket.makefile()
,用write
發送數據readlinds()
獲取響應,過了很久很久才能發送請求並收到響應。排查發現是由於 HTTP/1.1
,有多是KEEP-ALIVE
長鏈接緣由致使的timeout
。
使用socket.makefile
和socket.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 書籍配套交互式小程序 能夠看動圖,實驗等功能須要註冊