這次用python做一個(gè)tcp的服務(wù)器和客戶端程序,主要用來(lái)做新建連接數(shù)測(cè)試。
1,新建連接數(shù)測(cè)試的原理
(1)首先tcp建立階段,被測(cè)試設(shè)備需要轉(zhuǎn)發(fā)3個(gè)TCP握手?jǐn)?shù)據(jù)包;
(2)握手成功之后客戶端會(huì)發(fā)送一個(gè)http GET請(qǐng)求給服務(wù)器;
(3)服務(wù)器收到GET請(qǐng)求之后會(huì)回復(fù)一個(gè)200 OK給客戶端;
(4)客戶端收到200 OK之后,就會(huì)發(fā)送一個(gè)rst報(bào)文斷開當(dāng)前連接;
(5)被測(cè)試設(shè)備收到rst報(bào)文就會(huì)刪除當(dāng)前tcp連接跟蹤;
(6)服務(wù)端收到rst報(bào)文就會(huì)關(guān)閉當(dāng)前tcp連接;
(7)重復(fù)上述步驟并在服務(wù)端統(tǒng)計(jì)收到的rst報(bào)文數(shù)量,以此記錄一個(gè)完成的連接過程,統(tǒng)計(jì)單位時(shí)間內(nèi)該數(shù)量就可以對(duì)被測(cè)試設(shè)備新建連接數(shù)進(jìn)行衡量。
此處做的tcp測(cè)試程序主要的細(xì)節(jié)/問題處理在于如何發(fā)出rst報(bào)文,及如何在服務(wù)端統(tǒng)計(jì)每秒通過了多少連接數(shù),涉及的python知識(shí)點(diǎn)有soket編程,全局變量,線程。
2,python TCP客戶端程序
#python client.py
import socket
import struct
import sys
import thread
HOST=sys.argv[1]
PORT=sys.argv[2]
LOOP=sys.argv[3]
print(sys.argv[1], sys.argv[2], sys.argv[3])
def xinjian_test( threadName, threadLoop):
? ? for i in range(1, int(threadLoop), 1):
? ? ? ? s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
? ? ? ? s.connect((HOST,int(PORT)))
? ? ? ? #set reset attr
? ? ? ? s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,struct.pack('ii', 1, 0))
? ? ? ?#http get and recv 200 ok
? ? ? ? s.sendall('Get.')
? ? ? ? data=s.recv(1024)
? ? ? ? #will send tcp reset
? ? ? ? s.close()
try:
? thread.start_new_thread( xinjian_test, ("Thread-1", LOOP, ) )
except:
? print "Error: unable to start thread"
while 1:
? pass
這里主要講下tcp 在應(yīng)用層如何發(fā)送rst報(bào)文:
(1)tcp發(fā)送rst報(bào)文的常規(guī)情況:
1,客戶端嘗試與服務(wù)器未對(duì)外提供服務(wù)的端口建立TCP連接,服務(wù)器將會(huì)直接向客戶端發(fā)送reset報(bào)文(此reset報(bào)文為服務(wù)器主機(jī)內(nèi)核tcp/ip協(xié)議棧發(fā)送,為tcp/ip協(xié)議棧機(jī)制)。
2,客戶端和服務(wù)器的某一方在交互的過程中發(fā)生異常(如程序崩潰等),該方系統(tǒng)將向?qū)Χ税l(fā)送TCP reset報(bào)文,告之對(duì)方釋放相關(guān)的TCP連接(可用ctrl+c模擬,可能在win和linux上表現(xiàn)不一樣,參考:Ctrl+C在Linux平臺(tái)和Windows平臺(tái)下的TCP連接中的不同表現(xiàn))
3,在交互的雙方中的某一方長(zhǎng)期未收到來(lái)自對(duì)方的確認(rèn)報(bào)文,則其在超出一定的重傳次數(shù)或時(shí)間后,會(huì)主動(dòng)向?qū)Χ税l(fā)送reset報(bào)文釋放該TCP連接(同樣是內(nèi)核協(xié)議棧機(jī)制)
4,應(yīng)用開發(fā)者在設(shè)計(jì)應(yīng)用系統(tǒng)時(shí),會(huì)利用reset報(bào)文快速釋放已經(jīng)完成數(shù)據(jù)交互的TCP連接,以提高業(yè)務(wù)交互的效率(不用完成TCP四次揮手)
這次python的tcp客戶端程序正是采用第4種情況來(lái)發(fā)送reset報(bào)文。
我們知道,通常情況,調(diào)用socket的關(guān)閉可以調(diào)用close或shutdown函數(shù),這兩個(gè)函數(shù)正常使用時(shí),是按照tcp關(guān)閉連接的4次揮手過程進(jìn)行的(他們的區(qū)別這里不做討論),那么我們要發(fā)出rst包可能需要額外的處理,這里將要用到socket選項(xiàng):
SO_LINGER套接口選項(xiàng)
A、l_onoff設(shè)置為0,這也是默認(rèn)情況,函數(shù)close()是立即返回的,然后TCP連接雙方是通過FIN、ACK4分組來(lái)終止TCP連接的。當(dāng)然,發(fā)送緩沖區(qū)還有數(shù)據(jù)的話,系統(tǒng)將試著將這些數(shù)據(jù)發(fā)送到對(duì)方。
B、l_onoff非0,l_linger設(shè)置0,函數(shù)close()立即返回,并發(fā)送RST終止連接,發(fā)送緩沖區(qū)的數(shù)據(jù)丟棄。
C、l_onoff非0,l_linger非0,函數(shù)close()不立即返回,而是在
(a)發(fā)送緩沖區(qū)數(shù)據(jù)發(fā)送完并得到確認(rèn)
(b)l_linger延遲時(shí)間到,l_linger時(shí)間單位為微妙。
兩者之一成立時(shí)返回。如果在發(fā)送緩沖區(qū)數(shù)據(jù)發(fā)送完并被確認(rèn)前延遲時(shí)間到的話,close返回EWOULDBLOCK(或EAGAIN)錯(cuò)誤。
(2)python的tcp客戶端將采用B方式發(fā)送rst報(bào)文:
#設(shè)置l_onoff非0,l_linger設(shè)置0
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,struct.pack('ii', 1, 0))
#套接口關(guān)閉時(shí),將發(fā)送rst報(bào)文,終止tcp連接
s.close()
2,python TCP服務(wù)端程序
#!/usr/bin/python3
#python3 main.py
import socketserver
import os,sys
import time
import threading
HOST1="192.168.16.10"
PORT1=8888
#這里省略HOSTn,PORTn定義(多個(gè)服務(wù)線程)
RST_SUM = 0
RST_TIME1 = int(time.time())
def calcu_pkt_rst(flag):
? ? global RST_SUM
? ? global RST_TIME1
? ? RST_SUM += 1
? ? RST_TIME2 = int(time.time())
? ? RST_TIME3 = RST_TIME2 - RST_TIME1
? ? if RST_TIME3 >= 1 :
? ? ? ? print ("RST_SUM:", RST_SUM, "RST_TIME3:", RST_TIME3, "rst" if flag == True else "pkt", " of persecond:", RST_SUM/RST_TIME3)
? ? ? ? RST_TIME1 = RST_TIME2
? ? ? ? RST_SUM = 0
class Myserver(socketserver.BaseRequestHandler):?
? ? def handle(self):
? ? ? ? conn = self.request? ? ?
? ? ? ? while True:
? ? ? ? ? ? try:
? ? ? ? ? ? ? ? #print("conn.recv. ")
? ? ? ? ? ? ? ? ret_bytes = conn.recv(1024)
? ? ? ? ? ? ? ? if not ret_bytes:
? ? ? ? ? ? ? ? ? ? #print ("error.")
? ? ? ? ? ? ? ? ? ? calcu_pkt_rst(False)
? ? ? ? ? ? ? ? ? ? break
? ? ? ? ? ? ? ? #print("ret_bytes ",ret_bytes)
? ? ? ? ? ? except ConnectionResetError as e:
? ? ? ? ? ? ? ? calcu_pkt_rst(True)
? ? ? ? ? ? ? ? break
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? conn.sendall(bytes("200 Ok.",encoding="utf-8"))
? ? ? ? #print ("close.")
? ? ? ? conn.close()
def xinjian_test( threadName, myhost, myport):
? ? print ("host:", myhost, "port:", myport)? ? ? ? ? ? ?
? ? server = socketserver.ThreadingTCPServer((myhost,myport),Myserver)
? ? server.serve_forever()
if __name__ == "__main__":
? ? #這里省略tn(多個(gè)服務(wù)線程的初始化)
? ? t1 = threading.Thread(target=xinjian_test, args=("Thread-1", HOST1, PORT1))
? ? t1.start()
? ? try:
? ? ? ? t1.join()
? ? except KeyboardInterrupt as e:
? ? ? ? print ("KeyboardInterrupt: ", e)
? ? ? ? pass
1)在服務(wù)端,通過套接口異常:ConnectionResetError來(lái)處理rst信息,這個(gè)過程是這樣的:
a:服務(wù)端調(diào)用recv阻塞,等待客戶端發(fā)送的信息
b:客戶端連上服務(wù)器,并發(fā)送Get.信息,然后調(diào)用recv接收服務(wù)端返回的信息,此時(shí)線程將阻塞
c:服務(wù)端recv收到Get.信息,調(diào)用sendall發(fā)送200 Ok.信息,然后循環(huán)又回到a
d:客戶端recv收到服務(wù)器的200 Ok.信息,將往下執(zhí)行close,此時(shí)客戶端將發(fā)送rst報(bào)文(此實(shí)際為內(nèi)核協(xié)議棧發(fā)送)
e:服務(wù)器recv將撲獲ConnectionResetError異常,因?yàn)榉?wù)器端的內(nèi)核協(xié)議棧收到客戶端的rst報(bào)文時(shí),將會(huì)釋放該tcp連接,而應(yīng)用層recv此時(shí)還在等待該連接的信息,因此將觸發(fā)異常
f:對(duì)該異常進(jìn)行統(tǒng)計(jì),到這里將是一個(gè)連接的完整來(lái)回,因此該統(tǒng)計(jì)可以表征中間被測(cè)設(shè)備的新建連接能力(當(dāng)然前提是客戶端和服務(wù)器端本身不是瓶勁)
2)采用python的全局變量機(jī)制進(jìn)行統(tǒng)計(jì),參考:『Python』 多線程 共享變量的實(shí)現(xiàn)
關(guān)鍵點(diǎn)在于:
對(duì)于一個(gè)全局變量,你的函數(shù)里如果只使用到了它的值,而沒有對(duì)其賦值(指a?=?XXX這種寫法)的話,就不需要聲明global。相反,如果你對(duì)其賦了值的話,那么你就需要聲明global。
聲明global的話,就表示你是在向一個(gè)全局變量賦值,而不是在向一個(gè)局部變量賦值。
自己的體會(huì):全局變量首先是應(yīng)該全局聲明的,如在服務(wù)端的程序開頭就定義了全局變量:RST_SUM,在局部和函數(shù)體中需要對(duì)其賦值或改變其值時(shí),需要顯示使用global關(guān)鍵字進(jìn)行聲明,以表示他不是該函數(shù)體的局部變量,關(guān)于python的變量作用域,請(qǐng)參考:Python變量作用域及閉包
另外注意:不能在global聲明語(yǔ)句進(jìn)行賦值,如,global RST_NUM = 0
3)在程序的調(diào)試中碰到的異常:BrokenPipeError: [Errno 32] Broken pipe
關(guān)鍵信息:
File "main.py", line 68, in handle
? ? conn.sendall(bytes("200 Ok.",encoding="utf-8"))
BrokenPipeError: [Errno 32] Broken pipe
我們看到,服務(wù)端在發(fā)送sendall的時(shí)候,出現(xiàn)了Broken pipe異常,通過抓包分析:


其中,圖1是產(chǎn)生Broken pipe異常的交互流,圖2是無(wú)異常的交互流,我們看到在圖1,在“此時(shí)應(yīng)該是RST”報(bào)文處,發(fā)送了[FIN,ACK]報(bào)文,即192.168.1.230(客戶端)告訴192.168.16.13(服務(wù)器端)這個(gè)TCP連接已經(jīng)關(guān)閉,但是我們看到服務(wù)器端任然在該連接回[PSH,ACK],就是還在向該連接寫數(shù)據(jù),從tcp的四次揮手來(lái)講,遠(yuǎn)端已經(jīng)發(fā)送了FIN序號(hào),告訴你我這個(gè)管道已經(jīng)關(guān)閉,這時(shí)候,如果你繼續(xù)往管道里寫數(shù)據(jù),第一次,你會(huì)收到一個(gè)遠(yuǎn)端發(fā)送的RST信號(hào)(我們看到接下來(lái)就是RST信號(hào),這個(gè)信號(hào)不是客戶端close觸發(fā)的,是因?yàn)榭蛻舳税l(fā)了[FIN,ACK]而服務(wù)器端任然在該連接回[PSH,ACK]),如果你繼續(xù)往管道里write數(shù)據(jù),操作系統(tǒng)就會(huì)給你發(fā)送SIGPIPE的信號(hào),并且將errno置為Broken pipe(32)(這個(gè)繼續(xù)寫數(shù)據(jù)的數(shù)據(jù)包并沒有出現(xiàn)在鏈路上被我們抓到),這是Broken pipe產(chǎn)生的原因。
那么,為什么客戶端的close調(diào)用本應(yīng)該產(chǎn)生的RST報(bào)文哪里去了?圖1和圖2的不同在于,客戶端的運(yùn)行環(huán)境,圖2時(shí)在物理機(jī)上運(yùn)行(win和Linux效果一樣,我剛開始以為是Linux系統(tǒng)的問題),圖1是在虛擬機(jī)上運(yùn)行(Linux系統(tǒng)),不知道虛擬機(jī)的網(wǎng)絡(luò)棧及接口為什么把我的RST報(bào)文變成了[FIN,ACK],而我們看到接下來(lái)虛擬機(jī)本身是能夠發(fā)出RST報(bào)文的,這里的原因還沒有進(jìn)行深入分析。
另外,在調(diào)試這個(gè)問題的過程中,發(fā)現(xiàn)了另外一個(gè)問題:在服務(wù)端收到[FIN,ACK]到Broken pipe產(chǎn)生的過程中,python ?conn.recv(1024)一直不停的返回空字符串,也就是當(dāng)python的TCP通道因?yàn)閷?duì)方的[FIN,ACK]斷開后,本來(lái)應(yīng)該阻賽的recv一直收到空字符串,這就是為什么在服務(wù)器端會(huì)有這端代碼的原因:
?if not ret_bytes:
? ? ? ? ? ? ? ? ? ? #print ("error.")
? ? ? ? ? ? ? ? ? ? calcu_pkt_rst(False)
? ? ? ? ? ? ? ? ? ? break
這段代碼判斷收到空串,則退出while循環(huán),關(guān)閉該套接口。因此不會(huì)再走到sendall函數(shù)調(diào)用中去,這樣不再觸發(fā)Broken pipe錯(cuò)誤,同時(shí)也可以完成程序設(shè)計(jì)的功能。
這里,有1個(gè)技術(shù)點(diǎn)澄清,還有一個(gè)疑問:
1)recv為什么收到空串:因?yàn)閜ython的網(wǎng)絡(luò)編程API是基于標(biāo)準(zhǔn)的 BSD Sockets API,可以訪問底層操作系統(tǒng)Socket接口的全部方法。我們可以查看C語(yǔ)言recv的man page得到答案,其中:
RETURN VALUE
? ? ? These? calls? return the number of bytes received, or -1 if an error occurred.? In the event of an error, errno is set to?indicate the error.? The return value will be 0 when the peer has performed an orderly shutdown.
回應(yīng)該篇文章:python socket.recv() 一直不停的返回空字符串,客戶端怎么判斷連接被斷開?
2)從圖1中可以看到客戶端最后回了RST,為什么服務(wù)端程序沒有響應(yīng)到該異常,從程序代碼執(zhí)行順序,按理recv先于sendall,為何感覺Broken pipe先于RST到來(lái)?