【Python】Socket-Part4-TCP值得注意的地方
這一篇依然按照田田田田:【Python】Socket-Part2-TCP模型的流程來,只不過上一篇,server收到數據後直接取upper()返回,這裡server的處理稍微多一點,但也很簡單。
寫代碼可能只花了十分鐘,但是之後的測試-修改-理解-百度,將近兩個小時吧……
這裡主要針對TCP通信中可能發生的特殊情況,提出幾個值得注意的點。
1、client首先發出數據。在正式發出之前,需要對用戶的兩個特殊輸入做考慮:
(1)如果輸入「extit」,代表用戶想退出,而並不是想給伺服器發exit。
(2)如果輸入為空:那麼不能發送空,而要繼續讓用戶輸入,直至不為空。那麼有個問題是: 發送空,會有什麼後果?
經過試驗,現象是這樣:自己send()參數為空,是可以成功發給自己的發送隊列,發送隊列也可以發到網路上;對方的接收緩存也可以成功收到這個空字元串,但是對方的socket讀不到,也就是recv(buffer_size )一直沒有執行到位,俗稱「阻塞」。至於為什麼是這種效果,那就要深入理解recv()的觸發機制了……這裡先放一放,反正現象就是這樣。 總結:自己send()參數為空,對方一直停留在recv(),自然也沒有返回,於是自己的recv()也被阻塞。2、server需要考慮:(1)server同樣不能發送空字元串,原因同上。所以如果server本意要發空字元串,則改寫發送的內容為提示信息,一來避免了阻塞,二來也是給客戶端提示,客戶端收到後就知道自己的輸入請求不到數據。(比如你請求一個不存在的網頁,別人好歹給你返回404這種作為提示,而不是真的什麼都不回)注意:同樣是待發送信息為空的情況,client是continue下一輪循環,讓用戶重新輸入非空,server是改寫發送信息。最終效果都是讓發送非空,但處理方式不一樣,這是功能決定的。
(2)server需要考慮server forever的問題:如果連接非正常中斷(比如客戶端異常關閉,沒有經過正常的四次揮手),需要結束內層的循環,開始下一輪外層循環,以接受一個新的會話。通過在inner loop中使用try -except-break實現,利用conn不存在引發異常。如果不做異常處理,server就會掛掉。3、編碼問題
(1)注意client.recv(buffer_size)執行以後,用的是GB2312而不是utf-8解碼(會出錯),這是因為server處發送的就是經GB2312編碼的數據。這個編碼不是server加工的,而是read()函數做的。當然,如果為了保持客戶端一般用utf-8解碼的習慣,server可以配合修改,即把read()的結果先用GB2312解碼,再用utf8編碼,最後再發送。
(2) buffer_size的大小很關鍵,這裡是1024。中途我做了幾次試驗,將其改小,設為1,2,4,8等,都會報錯。這是因為在GB2312的字符集里,一個漢字是2個位元組,所以如果內容全是漢字,只有2的倍數個位元組讀取,不會有任何問題。但是這裡會出現半形的空格,也佔1byte,這種就會引起錯位:雖然每次讀雙數個位元組,但可能剛好把漢字的高/低拆成兩次的頭和尾。至於解決辦法:能想到的就是buffer_size盡量增大……但感覺依然不能避免。
參考:GB2312區位碼、編碼表與編碼規則 - CSDN博客
下面我來演示「錯位「的現象。
(3)注意到一個現象:在buffer_size很小的時候(比如,假設每次只能recv1個byte),此時你看到cliend每一輪的recv結果其實並不是針對本次send的響應了,而是前一次send的響應。
這種TCP 的APP無法區分多少個位元組真正組成一個消息,可能會把兩個消息解讀為一個消息的現象,叫做粘包。
UDP不會有粘包,因為一發一收(如果收到數據過大,直接丟棄),消息與消息之間的界限很明顯。
TCP的粘包原因,可能有上述分析的接收方APP buffer_size過小(但實際中,並不要求),還可能是TCP的Nagle演算法,在滿足一定條件時會把多個應用小包拼成大包一次性發送,此時就算接收方buffer_size足夠大,socket一次性取到了大包,交給APP,APP根本無法區分這個大包里包含多少小包,以及小包與小包的邊界在哪裡。如果能夠想辦法,讓接收方能夠區分小包與小包的邊界,就可以一起解決上述兩個問題。
這是個很大的問題。至於以上說的Nagel演算法在滿足什麼條件時,會把小包拼接成大包,答案在:TCP之Nagle演算法&&延遲ACK - 某精神病 - 博客園
簡單地說,小包一個個發送,無論從封裝開銷還是網路延遲來說都很不划算,這個很好理解。舉例來說,假如應用分10次給了tcp_socket數據,每個數據1byte,假如tcp收一個發一個,發10次,每次封裝的開銷是100bytes,10次下來封裝的總開銷就是100*10個byte;而如果累計到依次發送10byte,封裝協議開銷總共就只有100個byte。另外,假如粗暴地指定網路傳輸延時固定是10ms,那麼發10次,一個發出了等收到確認再發下一個,這樣總延時是10ms*2*10=200ms;而如果一次性發送,那麼延時只有10ms*2=20ms。
Nagle就是保證每次在網路上的小包最多只有1個(還不算趕盡殺絕),如果要徹底避免小包發送,可以指定 TCP_CORK 選項,除非超時絕不讓小包發出去,更加嚴格。當然,也要考慮如果承載的應用確實實時性要求很高,雖然是小包但確實等不起的情況,可以通過使用TCP_NODELAY選項,禁止TCP開啟Nagle 演算法。
Nagle演算法與CORK演算法區別 - CSDN博客
【附:代碼】
#cmd_server.pyfrom socket import *import subprocessip_port=(127.0.0.1,8080)buffer_size=1024back_log=5cmd_server=socket(AF_INET,SOCK_STREAM)cmd_server.bind(ip_port)cmd_server.listen(back_log)def exe_cmd(cmd): res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) print(res=,res) cmd_res = res.stdout.read() return cmd_reswhile True: print(New Connection--------------) conn,client_ip_port=cmd_server.accept() print(取到一個新的會話,conn) while True: try: #1接收 cmd=conn.recv(buffer_size).decode(utf-8) print(cmd=,cmd) #2以下是處理cmd_recv,生成cmd_res cmd_res=exe_cmd(cmd) print(cmd_res=,cmd_res) #type(cmd_res)已經是bytes,所以不需要編碼直接發送,但這個編碼的工作是由read()完成的,所以編碼方式默認是操作系統的gbk #注意這裡的編碼問題 if not cmd_res: cmd_res=輸入無結果,請重新輸入.encode(GB2312) #3發送 print(before send,conn=,conn) conn.send(cmd_res) print(SEND DONE) except Exception as e: #在windows上,如果client主動斷開了連接,conn就不在了,表現為發生異常,利用這個特點,在服務端檢測到異常就break,不拖累服務端一直等待 print(e) breakcmd_server.close()
#cmd_client.py
from socket import *ip_port=(127.0.0.1,8080)buffer_size=1024cmd_client=socket(AF_INET,SOCK_STREAM)cmd_client.connect(ip_port)while True: print("提示:輸入exit退出") cmd=input(>>:).strip() #特殊情況1:輸入空 if not cmd: continue #特殊情況2:輸入exit elif cmd==exit: break else: cmd_client.send(cmd.encode(utf-8)) #以上發送完畢,以下開始接收…… cmd_res=cmd_client.recv(buffer_size) #注意對方不是按utf8編碼的…… print(cmd_res) print(cmd_res.decode(GB2312))cmd_client.close()
推薦閱讀:
※Python初學者筆記(四):白話講正則的「貪婪與懶惰」,你看不懂算我笨!
※Python環境搭建—安利Python小白的Python和Pycharm安裝詳細教程
※什麼是Python Descriptors
※Kivy中文編程指南:圖形
※Tornado 非同步非阻塞淺析
TAG:Python |