實習期間完成進度
學習與成長
Socket是一個源自於BSD(柏克萊軟體發行版)的技術,又被稱為Berkeley Socket或「行程的門戶」。它用於TCP/IP網路通訊的應用程式介面(API),讓使用者能夠透過這個介面開發網路應用程式,這正是Socket最顯著的特點。
字面上來看,Socket的中譯為「插座」,這個名稱來自於其功能類似電話插座。就像設定電話號碼一樣,只需設定好Socket,任何一台電腦都能透過這個Socket進行通訊。因此,Socket可以被定義為「Socket是網路上的通訊端點,使用者或應用程式只需連接到Socket,就能夠和網路上的任何通訊端點建立連線,Socket之間的通訊方式類似作業系統內的不同程序間的通訊。」。
(一)Socket連線方式
網路由客戶端和伺服器端所組成,客戶端依賴伺服器端提供資源。當程式執行時,同時在這兩端建立進程,這兩個進程之間通過Socket進行資料的讀取(read())和寫入(write())。在這之前,必須建立連接(connect()),而Socket就像是位於協定和進程之間的閘道。開發者可以根據需要對Socket進行一些自訂的設定,例如設定傳送緩衝區(send buffer)和最大區段大小(Maximum Segment Size,MSS),通常情況下,MSS的值為1460位元組(bytes)。
二、Implementation
(一)Socket programming over TCP in Python
Python為進程間的通訊提供Socket,C、Java也同樣支持,不過此次實作使用較熟悉的程式語言Python與平台VS code來完成。
-
Berkeley Socket API庫提供的一些方法概要
socket()-創建一個新的socket,由一整數來標識,並為其分配系統資源。其中需定義要在哪個領域溝通以及傳輸方式,例如下列兩點:
-
AF_INET、AF_INET6 :兩台主機透過「網路」進行資料傳輸,AF_INET使用IPv4,AF_INET6使用IPv6。
-
SOCK_STREAM:SOCK_STREAM提供一個序列化的連接導向位元流,可以做位元流傳輸。對應的protocol為TCP。
-
setsockopt()-子常式,可用來啟用通訊協定層次的除錯、配置緩衝空間、控制逾時等。
-
bind()-通常在伺服器端使用,可將兩端的socket連接起來,用來告訴對方本地位址。
-
listen()-通常在伺服器端使用,用來監聽是否有用戶拜訪,規定有多少用戶可連入伺服器端。
-
connect()-在客戶端使用,為socket分配一個空閒的端口(port),並嘗試建立一TCP連接。
-
accept()-在伺服器端使用,接受來自客戶端嘗試創建的TCP連接。
-
sendall()-用來發送資料,可定義發送多大的資料量。
-
recv()-用來接收發送端的資料,可定義最大緩衝區大小,即最大接收量。
-
close()-使系統Release分配給socket的資源,並終止TCP連接。
-
-
程式編寫說明
TCP SERVER SIDE:
-
由使用者輸入SERVER_IP、SERVER_PORT、RUNNING TIME
-
創建Socket,定義由IPv4進行溝通以及以TCP方式傳輸資料。
-
添加子常式,先設定選項所在的協定層次為SOL_SOCKET,再以SO_SNDBUF來配置緩衝區大小為1000*1024*1024
-
以bind()綁定本地host&port
-
進入循環,不斷accept()接收客戶端嘗試建立的連接。
-
首先recv()接收客戶端準備好了,可以開始傳送檔案的信號。
-
使用sendall()發送開始傳輸信號
-
打開本地文件並讀取,將文件內容encode()成為bytes,使用sendall()發送內容。
-
使用sendall()發送結束傳輸信號
TCP CLIENT SIDE:
-
由使用者輸入SERVER_IP、SERVER_PORT、RUNNING TIME
-
創建Socket,定義由IPv4進行溝通以及以TCP方式傳輸資料。
-
添加子常式,先設定選項所在的協定層次為SOL_SOCKET,再以SO_SNDBUF來配置緩衝區大小為1000*1024*1024
-
以connect()與欲連線的伺服器端host&port嘗試建立連接
-
使用sendall()向伺服器端發送已準備好接收的信號。
-
接收recv()來自伺服器端開始傳輸之信號。
-
開始循環接收recv()資料,decode()後並逐筆加入暫存列表,直到收到特定結束信號。
-
將結束接收的時間減去開始接收的時間,並計算暫存列表中所收到之檔案大小,經下列計算方式可得到Downloads Throughput(吞吐量)
-
計算方式
[說明]
-
TOTAL_SIZE * 8 原先收到的檔案大小單位為Bytes,先將其轉換成bit。
-
((TOTAL_SIZE // 1460) * 54) * 8 取商數,因小數點後值極小,暫且先忽略不計,將商數乘以封包數目可得到最大檔案大小,最後轉換成bit。
-
54 * 8 為上一步暫且忽略的小數點,將其由Bytse轉換為bit。
-
((END_TIME – START_TIME) * 0.977) 求得基礎時間並校正時間。
-
最後將bits轉換成Mbs
-
Multi-threading
TCP連線提供了全雙工服務,這表示當客戶端向服務端發送資料時,服務端也同時可以給客戶端發送資料。Thread,也稱為線程,是Process的一部分,同一個Process中可以包含多個Thread,每個Thread負責不同的功能。在TCP資料傳輸過程中,伺服器端可以進行資料的Downloads,向客戶端發送資料,而客戶端可以進行資料的Uploads,向伺服器端發送資料。這兩種功能可以透過Multi-threading來實現,以充分利用CPU資源,更迅速地完成工作。
在Multi-threading中,多個線程同時訪問或修改Global Variable(全域變數)可能導致Process Synchronization(進程同步)的問題,導致共享資料在不同操作之間出現不一致。在Python中,可以使用Mutual Exclusion(互斥鎖)來解決這個問題。當一個線程需要修改共享數據時,它可以使用acquire()函數來鎖定資源,這樣其他線程就無法同時修改它,直到釋放資源的時候(使用release()),其他線程才能再次鎖定該資源。因此,Mutual Exclusion(互斥鎖)確保了多線程環境下數據的正確性。
(二)Socket programming over UDP in Python
-
UDP & TCP 之差異
UDP是一種無連接且不可靠的傳輸協議,其主要區別於TCP在於不提供三種實現「可靠傳輸」的機制。當使用UDP協議傳輸數據時,發送端不會主動確認接收端是否已成功接收數據,而是持續不斷地發送數據封包。由於UDP不維護連接狀態和數據序列,可能會導致數據包的「遺失」或「亂序」問題。若我們試圖使用確認/重傳機制或封包排序等方法來解決這些問題,則會違反UDP協議的本質,同時失去了其高效性的優勢。因此,我採用了一種基於時間控制的方法,來實現數據傳輸工作,這種方法通過計算傳輸所花費的時間以及實際接收到的檔案大小,來計算吞吐量(Throughput)。
-
Berkeley Socket API庫提供的一些方法概要
socket()-創建一個新的socket,由一整數來標識,並為其分配系統資源。其中需定義要在哪個領域溝通以及傳輸方式,例如下列兩點:
-
AF_INET、AF_INET6 :兩台主機透過「網路」進行資料傳輸,AF_INET使用IPv4,AF_INET6使用IPv6。
-
SOCK_DGRAM: SOCK_DGRAM提供的是一個一個的資料包(datagram),對應的protocol為UDP。
-
bind()-通常在伺服器端使用,可將兩端的socket連接起來,用來告訴對方本地位址。
-
sendto()-用來發送資料,可定義發送多大的資料量。
-
recvfrom()-用來接收發送端的資料,可定義最大緩衝區大小,即最大接收量。
-
close()-使系統Release分配給socket的資源,並終止TCP連接。
-
程式說明
使用UDP協定進行資料傳輸時,無需事先建立連線,只需在兩端啟動Socket後,即可透過sendto()和recvfrom()函數來交換資料。在傳送資料時,必須明確指定對方Socket的位址信息。
UDP SERVER SIDE:
-
由使用者輸入SERVER_IP、SERVER_PORT、RUNNING TIME
-
創建Socket,定義由IPv4進行溝通以及以TCP方式傳輸資料。
-
以bind()綁定本地host&port
-
首先以recvfrom()接收客戶端所要求之接收時間,並記錄發送此消息之客戶地址。
-
設定封包大小為1472,進入循環分塊讀取文檔。
-
進入倒數循環,使用sendto()發送encode()後的資料,直到秒數歸零。
UDP CLIENT SIDE:
-
由使用者輸入SERVER_IP、SERVER_PORT、RUNNING TIME
-
創建Socket,定義由IPv4進行溝通以及以TCP方式傳輸資料。
-
以sendto()傳送使用者指定之接收時間(RUNNING TIME)
-
進入倒數循環,使用recvfrom()循環接收資料,並逐筆存入暫存列表中,直到秒數歸零。
-
計算暫存列表中所收到之檔案大小,經計算可得到Downloads Throughput(吞吐量)
(三)Socket programming over HTTP in Python
-
HTTP請求訊息
[說明]
-
第一行為請求行(request line),三個欄位分別為:方法/要請求的物件URL/HTTP版本欄位,方法欄位可以是GET、POST、HEAD、PUT、DELETE等等。
-
第二行指定開啟的host&port
-
第三行Content-Type指網頁中的內容類型,決定瀏覽器要以什麼形式、編碼讀取此文件,application/x-www-form-urlencoded是表單默認提交數據的格式,表單數據將被編碼為key/value格式發送到伺服器。
-
第四行Transfer-Encoding:chunk,指定接下來的資料將分塊傳輸。
-
第五行為請求訊息中的固定格式,2-4狀態行(header line)與第六行資料主體(entity body)中間必須要空一行(CRLF),空行後皆為實體資料。
-
第六行為資料主體(entity body)
-
補充:每行皆以CRLF做區隔
-
Chunked transfer encoding分塊傳輸編碼
HTTP中存在一種稱為「分塊傳輸」的機制,通常在需要傳輸大量或長度未知的資料時使用,例如文件、動態生成的內容、視訊等等,這種機制允許將資料分成多個不同大小的塊,而當傳輸完最後一個大小為0的塊時,表示整個資料傳輸完成。分塊傳輸方法具備邊讀取資料邊發送的能力,因此有助於提高傳輸效率。此外,它也支持永久性連接,這意味著可以透過單一的TCP連線來傳送所有資料。每個塊的格式通常包括請求訊息、資料本身,以及一個標記為長度0的結束塊(“0\r\n\r\n”)。
-
程式編寫
HTTP SERVER SIDE:
-
由使用者輸入SERVER_IP、SERVER_PORT、RUNNING TIME
-
創建Socket,定義由IPv4進行溝通以及以TCP方式傳輸資料。
-
添加子常式,先設定選項所在的協定層次為SOL_SOCKET,再以SO_SNDBUF來配置緩衝區大小為1000*1024*1024
-
以bind()綁定本地host&port,listen()設定最大連接數。
-
進入循環,不斷accept()接收客戶端嘗試建立的連接。
-
創建HTTP請求訊息,內容如上(1)
-
讀取將要傳輸的文檔。
-
設定分塊讀取大小為1460,進入循環分段讀取。
-
在讀取循環中,發送的資料格式為HEADER + DATA + b’0\r\n\r\n’,使用sendall()將資料發送。
HTTP CLIENT SIDE:
-
由使用者輸入SERVER_IP、SERVER_PORT、RUNNING TIME
-
創建Socket,定義由IPv4進行溝通以及以TCP方式傳輸資料。
-
添加子常式,先設定選項所在的協定層次為SOL_SOCKET,再以SO_SNDBUF來配置緩衝區大小為1000*1024*1024。
-
以connect()與欲連線的伺服器端host&port嘗試建立連接。
-
開始循環接收recv()資料,decode()後並逐筆加入暫存列表。
-
將結束接收的時間減去開始接收的時間,計算暫存列表中所收到之檔案大小,經計算可得到Downloads Throughput(吞吐量)
三、Analysis
由上一小節中所計算出的吞吐量,可以藉由網路封包剖析器Wireshark來驗證。
(一)TCP
在這次的實作中,我使用TCP協定來傳送一個原始大小約為62 KB的檔案。在用戶端進行計算後,得到了一個Throughput(傳輸速率)為778.38 Mbps,並且整個傳輸過程大約花了1.06秒的時間。透過Wireshark抓包分析,獲得了Average Throughput(平均傳輸速率)的圖表。
在這張圖5、6中,我們可以看到藍點代表每個封包接收到的長度,而幾乎每個封包都幾乎填滿了1460 bytes。黃線表示吞吐量,根據資料傳輸結束時的數值估算為8.09(bit/s),轉換成Mbps單位約為771.52 Mbps。這個數值與我實際測得的值非常接近。
根據圖表,我們還可以觀察到整個傳輸過程的總時長約為1.09秒。需要注意的是,Wireshark的時間包括了設定的開始傳輸信號({S})和結束信號({E}),以及回應握手等等,因此時間稍長,而相對的Throughput也略低一些。
一開始,我認為Wi-Fi測試是一個相當複雜且充滿難度的任務。然而,在實際進行測試的過程中,我逐漸體會到所需的不僅僅是技術,更需要豐富的耐心。特別是因為相同的測試步驟需要在三個不同的頻寬帶寬下進行,這三種頻寬分別是2.4GHz、5GHz、和6GHz,或是不同的mod。
在每一步驟完成後,都必須仔細核對結果。根據每個測試項目的描述,我首先須模擬相應的測試環境,例如,在DFS(動態頻譜共享)環境下,Wi-Fi是否能正常運作。這意味著我需要創建一個類似DFS環境的情景,然而,實際測試時很難確保能夠恰巧遇到這種特殊環境。
在這種情況下,我需要倚賴一些特定的命令。這些命令通常是在研發階段開發的功能,能夠幫助我們模擬出所需的測試環境。藉由執行這些命令,我們可以模擬各種特殊環境,並進行測試以確認Wi-Fi在不同情境下的性能表現,並比較測試前後的結果差異。
(三)測試報告撰寫
最後,我需要向客戶和研發團隊提交一份測試報告。這份報告詳細描述了每個測試項目的流程、確認結果的方法以及是否通過了測試。起初,我在第一次測試和報告的撰寫方面並不細心,經常會遺漏測試項目,或者忽略確認步驟,結果差不多全部被退回來。不過,幸運的是,我的同事非常耐心地指導我,他們在每個有問題的測試流程旁邊都寫下了問題所在,這讓我可以根據這些建議來修改我的測試流程和報告撰寫方式。通過這些建議,我才發現我之前漏測了很多細節。因為在某些情況下,同一環境可能需要確認多個事項,但我只注意到其中一個。這個專案讓我對Wi-Fi有了更深入的了解,同時也讓我明白了測試需要極大的耐心。
圖1、Process communicating through TCP sockets
圖2、TCP資料流程示意圖
公式1
圖3、UDP建立流程示意圖
表1
圖4、HTTP建立流程示意圖
圖5、Average Throughtput
圖6、Average Throughtput(放大)
圖7、Wireshark抓包(擷取訊號1)
圖8、Wireshark抓包(擷取訊號2)
圖9、測試項目及步驟