一次python TCP socket編程引發(fā)的知識(shí)點(diǎn)

這次用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
圖2

其中,圖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)?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容