一個由端口號引起的奇怪事件
引子
最近在寫畢業論文,我的研究方向與計算機網絡相關,實驗部分還差點數據仿真測試。一般,網絡的仿真,一般用iperf這個工具進行帶寬測試。iperf的使用方法挺簡單的,以下面這個圖為例,兩個終端分別是兩個由Mininet模擬出來的用戶,h1的IP地址是10.0.0.1,h3的IP地址是10.0.0.3。在h3上運行iperf服務器端(參數-s),監聽UDP(參數-u),每秒輸出一次(參數-i),在h1上運行iperf客戶端(參數-c),指定服務器端IP地址為10.0.0.3(即h3),每秒輸出一次(參數-i),指定UDP帶寬為50M(參數-b)。
這里可以提供一個先驗知識,iperf服務器端開啟的默認端口是5001,客戶端沒有固定端口(比如上圖h1的端口是48921)
場景復現
跑仿真、跑數據,肯定不可能手動開啟一個個終端再一個個去執行命令,最好是寫一個自動化的腳本去模擬我要做的事。
比如現在我想同時運行h1 iperf h3與h2 iperf h3,如果要手動操作,得開四個終端,其中兩個終端是h3,運行iperf服務器端,另外兩個終端分別是h1與h2,運行iperf客戶端,這樣肯定效率非常低下,而且切換終端是有時間差的,數據可能不精準,于是我考慮在Mininet源碼中加入一些命令,方便我測試,下面的截圖就是我在Mininet CLI中執行自定義的iperfdouble命令的效果圖(在本例中,都是h3作為iperf服務器端)
mininet> iperfdouble h1 h2 h3 50m 60
*** Iperf: testing bandwidth between h1 and h3
***start server***
***start client***
***client ping server***
*** Iperf: testing bandwidth between h2 and h3
***start server***
***start client***
mininet> 【光標閃爍】
自定義的iperfdouble實現代碼如下,前三個主機參數必須有,后面兩個分別是帶寬與持續時間,因為下游函數iperf_single中對這兩個參數設置了默認值,故可忽略
def do_iperfdouble( self, line ):
"""Multi iperf UDP test between two specified nodes
$iperfdouble h1 h2 h3 50m 10"""
args = line.split()
hosts1 = []
hosts2 = []
now = strftime("%Y%m%dT%H%M%S", localtime())
for i in range(3):
if args[i] not in self.mn:
err = True
error( "node '%s' not in network\n" % args[i] )
else:
if i == 0:
hosts1.append( self.mn[ args[i] ] )
elif i == 1:
hosts2.append( self.mn[ args[i] ] )
else:
hosts1.append( self.mn[ args[i] ] )
hosts2.append( self.mn[ args[i] ] )
err = False
if len(args) == 3:
if not err:
self.mn.iperf_single(now, hosts1 )
self.mn.iperf_single(now, hosts2 )
elif len(args) == 5:
udpBw = args[ 3 ]
period = args[ 4 ]
err = False
if not err:
self.mn.iperf_single(now, hosts1, udpBw, float(period))
self.mn.iperf_single(now, hosts2, udpBw, float(period))
else:
error('invalid number of args: iperfdouble node1 node2 node3 udpBw period\n' +
'examples: iperfdouble h1 h2 h3 [50M] [10]\n')
iperfdouble會提取兩對主機,分別運行iperf_single函數,該函數定義如下,iperf服務器端的輸出會附帶當前時間戳存儲到log文件夾中:
def iperf_single( self, now, hosts=None, udpBw='10M', period=10, port=5001):
"""Run iperf between two hosts using UDP.
hosts: list of hosts; if None, uses opposite hosts
returns: results two-element array of server and client speeds"""
if not hosts:
return
else:
assert len( hosts ) == 2
client, server = hosts
iperf_output = 'h' + client.name[1:] + 'iperf' + 'h' + server.name[1:] + '.' + now
# ping_output = 'h' + client.name[1:] + 'ping' + 'h' + server.name[1:] + '.' + now
output( '*** Iperf: testing bandwidth between ' )
output( "%s and %s\n" % ( client.name, server.name ) )
iperfArgs = 'iperf -u '
bwArgs = '-b ' + udpBw + ' '
print "***start server***"
server.cmd(iperfArgs + '-s -P 1 -i 1' +
' > /home/liaoss/network/log/' + iperf_output + '&')
print "***start client***"
client.cmd(iperfArgs + '-t '+ str(period) + ' -c ' + server.IP() + ' ' + bwArgs + '&')
# print "***client ping server***"
# client.cmd('ping -c' + str(period) + ' ' + server.IP() +
# ' ' + ' > /home/liaoss/network/log/' + ping_output + '&')
于是,我就得到了這樣的log文件,有什么發現奇怪的地方嗎?
看看左邊文件紅線框出來的,文件名是h1iperfh3(從iperf_single的代碼可以看出來,log文件命名格式是:[client]iperf[server].timestamp
),但是根據iperf輸出,這些數據來自于h2!右邊log文件正好相反,文件名中client是h2,但是數據來自于h1!
是不是很奇怪?讀者讀到這里,可以再仔細看看iperfdoube與iperf_single的代碼,看看能不能發現到底哪里出了問題。
問題原因
其實,本文的標題已經給出了答案,問題的根源在于端口號,注意到,iperf服務器端的默認端口號是5001,iperf客戶端如果不指定端口號,默認發送的服務器端端口號也會5001,觀察上圖兩個log文件,h3作為iperf服務器端開了兩個進程,效果等同于下圖兩個iperf服務器端進程,pid分別為1676與1916
假設這時h1作為iperf客戶端向h3發起UDP流量,到底h3哪個iperf服務器端進程會建立連接呢?下圖給出了答案,pid為1916的被選擇了(我也不太清楚為啥選擇它,可能是隨機的?經測試,兩個h3iperf服務器端開啟先后順序不影響結論,讀者如果有想法可以告訴筆者)
寫到這里,問題的根源應該有些眉目了,在iperf_single函數中,iperf并未指定端口號,所以h3iperf服務器端的輸出并不與函數的client參數一致,我們再以一個無序的例子佐以證明,下圖的左上、右上運行h3iperf服務器端,左下與右下分別是h1與h2的iperf客戶端,四個命令均不指定端口號,在h1與h2上運行iperf客戶端,發現都是由右上的h3iperf服務器端接收,看起來這真的挺隨機的。
解決辦法
在iperf_single中,指定端口號即可,因為在本場景中,都是h1與h2作為iperf客戶端,所以這里把'hx'中的數字'x'提取出來,拼在'500'后面即可,這樣h1連接的就是端口5001,h2連接的就是端口5002,所以有點hard code之嫌,但這里僅提供一個解決方法而已
最終,log日志正確輸出了
總結
有時候,問題的根源不像表面那般清晰,得掌握一定的知識才能深入分析,從而解決問題。