python网络编程项目_python网络编程详解
最近在看《UNIX網絡編程 卷1》和《FREEBSD操作系統設計與實現》這兩本書,我重點關注了TCP協議相關的內容,結合自己后臺開發(fā)的經驗,寫下這篇文章,一方面是為了幫助有需要的人,更重要的是方便自己整理思路,加深理解。
理論基礎
OSI網絡模型
OSI模型是一個七層模型,實際工程中,層次的劃分沒有這么細致。一般來說,物理層和數據層對應著硬件和設備驅動程序,例如網卡和網卡驅動。傳輸層和網絡層由操作系統內核實現,當用戶進程需要通過網絡傳輸數據,通過系統調用的方式讓內核將數據封裝為相應的協議格式,進而調用網卡驅動傳輸數據。頂上三層對應具體的網絡應用協議:FTP、HTTP等,這些應用層協議不需要知道具體的通信細節(jié)。
傳輸層
在實際工程中,我們常用的應用層服務(例如:HTTP服務、數據庫服務、緩存服務)通信的直接底層就是傳輸層,下圖是一些常用命令涉及的通信協議。
IPv4(Internet Protocol version 4)全稱是網際協議版本4,它使用32地址,平時常說的IP協議就是指IPv4,類似于192.168.99.100的地址可以看成4位256進制數據,也就是32網絡地址。但隨著網絡設備爆炸式增長,32地址面臨這用完的風險,IPv6(Internet Protocol version 6)應運而生。IPv6使用128位地址,但IPv4地址耗盡的問題有了新的解決方案,目前普遍使用的還是IPv4,IPv6全面取代IPv4還有很長的距離。
UDP (User Datagram Protocol),全稱用戶數據報協議。UDP提供面向無連接的服務,客戶端和服務端不存在任何長期的關系。UDP不提供可靠的通信,它不保證數據報一定送達,也不保證數據包送達的先后順序,也不保證每份數據報只送達一次。雖然UDP可靠性差,但是消耗資源少,適用在網絡環(huán)境較好的局域網中,例如不需要精確統計的監(jiān)控服務(eg: Statsd)。由于使用了UDP,客戶端每次打點統計只需要一次發(fā)送UDP數據報的IO開銷,服務性能損失很小,而且在內網環(huán)境數據包一般都能正常到達服務端,也能保證較高的可行度。
TCP(Transmission Control Protocl),全稱傳輸控制協議。和UDP相反,TCP提供了面向連接的服務,而且提供了可靠性保障。平常我們使用的應用層協議,例如HTTP,FTP等,幾乎都是建立在TCP協議之上,深入了解TCP的細節(jié)對于開發(fā)高質量的后臺開發(fā)和客戶端開發(fā)都有很好的借鑒意義。下面開始重點介紹TCP協議的細節(jié)。
TCP協議
狀態(tài)轉換
為了提供可靠的通信服務,TCP通過三次分節(jié)建立連接,四次分節(jié)關閉連接,心跳檢查判斷連接是否正常,因此需要記錄連接的狀態(tài),TCP一共定義了11種不同的狀態(tài)。
通過netstat命令可以查看所有的tcp狀態(tài)。
三路握手
在三路握手之前,服務器必須準備好接收外來的連接。這通常通過調用bind和listen完成被動打開,此時服務進程有一個套接字處于LISTEN狀態(tài)。在客戶端發(fā)通過調用connect送一個SYN分節(jié)后,服務進程必須確認(ACK)此分節(jié),同時也發(fā)送一個SYN分節(jié),這兩步在同一分節(jié)中完成,通過上面的轉臺扭轉圖,可以知道服務進程中會生成一個處于SYN_RCVD狀態(tài)的套接字。當再次收到客戶端的ACK分節(jié)后,服務端的套接字狀態(tài)轉變?yōu)镋STABLISHED。
客戶端通過connect函數發(fā)起主動打開,在此之前客戶端套接字狀態(tài)為CLOSED。調用connect導致客戶TCP發(fā)送一個SYN分節(jié),此時套接字狀態(tài)有CLOSED變?yōu)镾YN_SENT,在收到服務器的SYN和ACK后,客戶端socket再發(fā)送ACK分節(jié),套接字狀態(tài)變?yōu)镋STABLISHED,此時connect返回。
備注:SYN分節(jié)中除了有序列號之外,還會有最大分節(jié)大小、窗口規(guī)模選項、時間戳等TCP參數,具體可以參考協議詳細規(guī)定。
終止連接
上圖展示了客戶端執(zhí)行主動關閉的情形,實際上無論客戶端還是服務器,都可以執(zhí)行主動關閉。一般情況下客戶端執(zhí)行主動關閉較多,所以使用客戶端主動關閉為例講解。
客戶端調用close,執(zhí)行主動關閉時,發(fā)送FIN分節(jié),此時客戶端套接字狀態(tài)由ESTABLISED變?yōu)镕IN_WAIT_1。服務器收到這個FIN,會執(zhí)行被動關閉,并向客戶端發(fā)送ACK,FIN的接受也作為一個文件結束符傳遞給服務進程,如果此時服務進程調用套接字的方法,無論緩存區(qū)是否有數據都會返回EOF,服務端套接字狀態(tài)由ESTABLISED變?yōu)闉镃LOSE_WAIT。客戶端接收到ACK后,客戶端套接字狀態(tài)由FIN_WAIT_1變?yōu)镕IN_WAIT_2。
一段時間后,當服務進程調用close或者shutdown時,也會發(fā)生送FIN分節(jié),服務端套接字狀態(tài)由CLOSE_WAIT變?yōu)長AST_ACK。客戶端在接收到FIN分節(jié)后,發(fā)送ACK分節(jié),客戶端套接字狀態(tài)由FIN_WAIT_2變?yōu)門IME_WAIT。服務器段接收到客戶端的ACK分節(jié),狀態(tài)變成CLOSED。
在某些情況下,第二和第三分節(jié)可能會合并發(fā)送。調用close可能會觸發(fā)主動關閉,當進程正常或者非正常退出時,內核會將該進程所使用的文件描述符對應的打開次數執(zhí)行減一操作,當某個文件打開次數為0時,也就是說所有的進程都沒有使用此文件時,也會觸發(fā)TCP的主動關閉操作。
TIME_WAIT狀態(tài)
在終止連接的過程中,主動關閉方套接字最終的狀態(tài)是TIME_WAIT,在經過2MSL(maximun segment lifetime,每個IP數據報都包含一個跳限的字段,表明數據報能經過的路由最大個數,因此默認每個數據報在因特網中有一個最大存活時間)時間后狀態(tài)才變?yōu)镃LOSED,為什么這樣設計呢?
這樣的設計出于兩個考慮:
可靠地實現TCP全雙工連接的終止。上圖的四次分節(jié)關閉連接是在正常流程,實際情況中,任何一次分節(jié)都可能出現發(fā)送失敗的情況。主動關閉方最后的一個ACK分節(jié)可能會因為路由問題發(fā)送失敗,為了保證可靠性,需要重新發(fā)送保證另一方正確關閉套接字,因此此時的狀態(tài)不能為CLOSED。
允許老的重復分界在網絡中消失。加入10.10.89.9的3400端口和206.168.12.12的80端口建立了一個TCP連接,此連接中斷后,之前發(fā)送的TCP分節(jié)可能因為路由循環(huán)的問題還在因特網中游蕩,而此時這兩個機器相同的端口再建立起新的連接后,原來在網絡中游蕩的分解會對新的連接造成干擾。為了避免這種情況,設置一個2MSL的超時時間,保證之前還在網絡中游蕩的數據包完全消失。
套接字編程
下圖是C語言的套接字函數,考慮Python的socket庫只是底層C庫的簡單封裝,接口參數大同小異,而且Python方便上手調試,語法上也更通俗易懂,所以本文使用Python的socket庫作為講解實例。
socket
socket是python套接字類,通過構造函數生成套接字對象,構造函數簽名如下
其中family參數指協議族;type參數指套接字類型;protocol值協議類型,或者設置為0,以選擇所給定family和type組合的系統默認值;fileno指文件描述符(我從來沒用過)。
family
說明
AF_INET
IPv4協議
AF_INET6
IPv6協議
AF_LOCAL
Unix域協議
AF_ROUTE
路由套接字
AF_KEY
密鑰套接字
type
說明
SOCK_STREAM
字節(jié)流套接字
SOCK_DGRAM
數據包套接字
SOCK_SEQPACKET
有序分組套接字
SOCK_RAW
原始套接字
protocol
說明
IPPROTO_TCP
TCP傳輸協議
IPPROTO_UDP
UDP傳輸協議
并非所有套接字family和type的組合都是有效的,下表給出了一些有效的組合和對應的協議,其中標是的項也是有效的,但是沒有找到便捷的縮略詞,而空白項是無效組合。
connect
connect用于客戶端和服務器建立連接,函數簽名如下:
客戶端在調用connect之前不必非得調用bind函數,內核會確定源IP地址,并選擇一個臨時端口作為源端口。如果使用TCP協議,connect將激發(fā)TCP的三路握手過程,TCP狀態(tài)由CLOSED變?yōu)镾YN_SENT,最終變?yōu)镋STABLISHED,在三路握手的過程中,可能會出現下面幾種情況導致connect報錯。connect失敗則套接字不可用,必須關閉,不能對這樣的套接字再次調connect函數。
TCP客戶端沒有是收到SYN分節(jié)響應,一般發(fā)生在服務端backlog隊列已滿的情況下,服務器會對收到的SYN分節(jié)不做任何處理。客戶端等待一段時間后會重新發(fā)送SYN分節(jié),直到等待時間超過上限,才會拋出ETIMEDOUT錯誤(對應的python異常是TimeoutError)。
對客戶端SYN的響應是RST,表明服務端在指定的端口上沒有進程在等待與之連接,客戶端馬上會拋出ECONNRFUSED錯誤。下圖是用python連接一個未使用的端口,拋出異常ConnectionRefusedError,該異常錯誤號碼111,errno中查找正是ECONNRFUSED對應的錯誤碼。
如果發(fā)出的SYN在中間的嗎某個路由器上引發(fā)了目的地不可達錯誤,客戶端會等待一段時間后重新發(fā)送,直到等待時間超過上限(和第一種情況類似),此時會拋出ENETUNREACH或者EHOSTUNREACH錯誤。下圖為關閉本機網絡后,用python調用connect,由于網絡不可達,異常的錯誤碼為101,errno中查找正是ENETUNREACH錯誤碼。
bind
bind方法把一個本地協議地址賦予給一個套接字,方法簽名如下:
在不調用bind的情況下,內核會確定IP地址,并分配臨時端口,這種情況很適合客戶端,因此客戶端在調用connect之前不調用bind方法。而服務端需要一個確定的ip和端口,因此需要調用bind指定地址和端口。一般情況下,服務器都有多個ip地址,除了環(huán)路地址127.0.0.1外,還有局域網和公網地址,如果bind綁定的是環(huán)路地址127.0.0.1,則只有本機通過環(huán)路地址才能訪問,如果需要通過任一ip地址都能訪問到,可以綁定通配地址0.0.0.0。當指定的端口為0時,內核會分配一個臨時端口。
如果端口已經在使用,會拋出EADDRINUSE(errno對應錯誤碼是98)異常,可以通過設置SO_REUSEADDR和SO_REUSEPORT這兩個套接字參數讓多個進程使用同一個TCP連接。
listen
當創(chuàng)建一個套接字時,默認為主動套接字,也就是說,是一個將調用connect發(fā)起連接的客戶套接字。listen方法把一個未連接的套接字轉換為一個被動套接字,指示內核應接受指向該套接字的狀態(tài)請求。根據TCP狀態(tài)轉換圖,調用listen導致套接字從CLOSED狀態(tài)轉換到LISTEN狀態(tài)。此方法參數規(guī)定了內核應該為相應套接字排隊的最大連接個數,在bind之后,并在accept之前調用。
為了理解backlog參數,我們必須認識到內核為其中任何一個給定的監(jiān)聽套接字維護兩個隊列:
未完成連接隊列,每個這樣的SYN分節(jié)對應其中一項:已由某個客戶發(fā)出并到達服務器,而服務器正在等待完成相應的TCP三路握手過程,這些套接字處于SYN_RCVD狀態(tài)。
已完成連接隊列,每個已完成TCP三路握手過程的客戶對應其中一項,這些套接字處于ESTABLISHED狀態(tài)。
RTT指的是未連接隊列中的任何一項在隊列中的存活時間。linux下的backlog指的是已完成連接隊列的容量,如果服務器長時間未調用accept從此隊列中取走數據,當新的客戶端通過三路握手重新建立連接時,服務器不會處理收到的SYN分節(jié),而客戶端會一直等待并不斷重試直到超時。在服務器負載很大的情況下,就會造成客戶端連接時間長,所以需要合理設置backlog大小。
accept
accept用于從已完成連接隊列頭返回下一個已完成連接,如果已完成連接隊列為空,那么進程會被投入睡眠(套接字為阻塞方式)。
accept會自動生成一個全新的文件描述符,代表與所返回客戶的TCP連接。需要注意的是,此處有兩個套接字對象,一個是監(jiān)聽套接字,一個返回的已連接套接字。區(qū)分這兩個套接字很重要,一個服務器通常僅僅創(chuàng)建一個監(jiān)聽套接字,它在該服務器的生命周期內一直存在,內核為每個由服務器進程接受的客戶連接創(chuàng)建一個已連接套接字(也就是說TCP三路握手已經完成),當服務器完成對某個給定客戶的服務時,相應的已連接套接字會被關閉。
close
close方法用來關閉套接字,方法簽名如下:
需要注意的是,close方法并不一定會觸發(fā)TCP的四分組連接終止序列,當一個已連接套接字被多個進程打開時,關閉套接字只會導致此進程相應描述符的計數值減1,只有所有進程都將該套接字關閉后,套接字的引用計數值小于1以后,系統內核才會開始終止連接操作,這一點在多進程開發(fā)過程中需要格外注意。如果確實想在某個TCP連接上發(fā)送FIN觸發(fā)主動關閉,可以調用shutdown方法。
send
send方法用于TCP發(fā)送數據,方法簽名如下:
每一個TCP套接字都有一個發(fā)送緩沖區(qū),默認大小通過socket.SO_SNDBUF查看,當某個進程調用send時,內核從該應用進程的緩沖區(qū)復制所有數據到所寫套接字的發(fā)送緩沖區(qū),如果該套接字的發(fā)送緩沖區(qū)容不下該應用進程的所有數據(或是應用進程的緩沖區(qū)大小大于套接字的發(fā)送緩沖區(qū),或是套接字的發(fā)送緩沖區(qū)已有其他數據),該應用進程將被投入睡眠(套接字阻塞的情況),內核將不從系統調用返回,直到應用進程緩沖區(qū)的所有數據都復制到套接字發(fā)送緩存區(qū)。當對端確認收到數據后,會發(fā)送ACK分節(jié),隨著對端ACK的不斷到達,本端TCP才能從套接字發(fā)送緩存區(qū)中丟棄已確認的數據。
在類似于HTTP的應用層協議中,客戶端在發(fā)送完請求數據之后,可以調用s.shutdown(socket.SHUT_WR)告訴服務端所有的數據已經發(fā)送完成,服務端通過recv會讀取到空字符串,之后就可以處理請求數據了。
recv
recv方法用于TCP接收數據,方法簽名如下:
每一個TCP套接字也都有一個接受緩存區(qū),默認大小通過socket.SO_RCVBUF查看。當某個進程調用recv而且緩存區(qū)沒有數據時,該進程會被投入睡眠(套接字阻塞的情況),內核將不從系統調用返回。
在《Unix網絡編程》中,所有C語言調用accept,read, write函數都會檢查errno是否等于EINTR,這是因為進程在執(zhí)行這些系統調用的時候可能會被信號打斷,導致系統調用返回。而我自己用python2.7嘗試的時候發(fā)現并沒有此問題,猜測是python針對系統調用被信號打斷的情況,自動重新執(zhí)行系統調用,stackoverflow上也證實了這一點: http://stackoverflow.com/questions/16094618/python-socket-recv-and-signals。
IO多路復用
在做服務器開發(fā)的時候,經常會碰到處理多個套接字的情形,此時可以通過多進程或這多線程的模型解決此問題。用一個主進程或者主線程負責監(jiān)聽套接字,其它每個進程或線程負責一個已連接套接字,這樣還可以利用操作系統的線程切換實現多并發(fā),提高機器利用率。但是機器資源有限,不可能無限制的生成新線程或進程,IO多路復用應運而生。當內核一旦發(fā)現進程指定的一個或者多個IO條件就緒,它就通知進程。
IO模型
Unix下有5中IO模型:
阻塞式IO
非阻塞式IO
IO復用
信號驅動IO
異步IO
已讀取數據為例,講解這物種IO模型的區(qū)別。每次讀取數據包括以下兩個階段,而這五種模型的不同之處也體現在這兩個階段不同的處理。
等待數據準備好
從內核想進程復制數據
阻塞式IO
socket套接字默認就是阻塞式IO。以recvfrom為例,用戶進程通過系統調用獲取TCP數據,如果套接字緩存區(qū)沒有數據,系統調用不會返回,造成用戶進程一直阻塞。直到緩存區(qū)有可用數據,內核將緩存區(qū)數據拷貝至用戶進程空間,系統調用才會返回。
非阻塞式IO
python可以通過調用s.setblocking(False)或者s.settimeout(0.0)將一個套接字設置為非阻塞式IO。以recvfrom為例,當沒有可用的數據時,用戶進程不會阻塞,而是馬上拋出EWOULDBLOCK錯誤(或者EAGAIN,對應的errno錯誤碼都是11),只有當數據復制到內核空間后,才會正確返回數據。
IO多路復用
在有多個IO操作時,先阻塞于select調用,等待數據報套接字變?yōu)榭勺x,然后再通過recvfrom把緩存區(qū)數據復制到用戶進程空間。和阻塞是IO相比,當處理的套接字個數較少的時候,多路復其實沒有性能上的優(yōu)勢,它的優(yōu)勢在于可以方便操作很多套接字。
信號驅動式IO
通過信號處理的方式讀取數據。
異步IO
當數據包被復制到用戶進程后,用戶通過callback的方式獲取數據。
模型對比
可以發(fā)現,前四種IO模型——阻塞式IO、非阻塞式IO、IO復用、信號驅動IO都是同步IO模型,因為真正的IO操作(recvfrom)將阻塞進程,只有異步IO模型才不會導致用戶進程阻塞。
python使用
較早的時候使用的多路復用是select函數,但是由于時間復雜度較高,很快就被其他的函數替代:linux下的epoll,unix下的kqueue,windows下的iocp。為了屏蔽不同系統下的不同實現,跨平臺的第三方庫出現:libuv、libev、libevent等,這些庫根據平臺的不同,調用不同的底層代碼。
如果想直接使用底層的epoll或者select,它們封裝在python的select庫中;libuv、libev都有相應的python封裝,庫名叫做pyuv、pyev,通過pip安裝后即可使用。
python示例
一般情況下,為了提升服務的承載量,都會采用進程+IO多路復用或者線程+IO多路復用的開發(fā)模式。IO多路復用是為了一個并發(fā)單位管理多個套接字,而多進程或者多線程是為了充分利用多核。由于GIL的存在,python多線程模型并不能充分多核,因此我們常見的wsgi server,例如:gunicorn、uwsgi、tornado等都是使用的多進程+IO多路服用開發(fā)模式。
tornado使用epoll管理多個套接字,gunicorn和uwsgi都可以使用gevent,gevent是一個python網絡庫,用greenlet做協程切換,每個協程管理一個套接字,主協程通過libevent輪詢查找可用的套接字。因為gevent可以通過monkey patch將socket設置為非阻塞模式,因此當服務器有數據庫、緩存或者其他網絡請求的時候,相比tornado,uwsgi和gunicorn可以充分利用這部分的阻塞時間。和gunicorn相比,uwsgi是c語言實現,直觀感覺這三個server的性能應該是:uwsgi > gunicorn > tornado,和網上的benchmark大致匹配。
django的作者在github上實現了一個wsgi server,項目地址: https://github.com/jonashaag/bjoern,使用C語言實現,代碼量很少,性能據說比uwsgi還好,十分適合網絡開發(fā)進階學習。參考這份代碼,我用python實現了一個thrift server,項目地址:https://github.com/LiuRoy/dracula,和thriftpy的TThreadedServer做了一個簡單的性能對比。
50
100
150
200
250
300
350
400
450
libev
92
181
269.9
355.2
362.6
367.1
373.8
378.5
315(3%)
thread
88.9
180.5
266.1
354.8
428.9
460.2
486.5(2%)
477.9(7%)
486.5(22%)
橫坐標是連接個數,縱坐標是qps,括號內的數字表示錯誤率。在連接數較少的情況下,使用libev管理socket和多線程性能相差不大,在連接數超過200后,libev模型的請求耗時會增加,導致qps增加的并不多,但是線程模型在連接數很多的情況下,會導致部分請求一直得不到處理,在連接個數350的時候就會出現部分請求超時,而libev模型在450的時候才會出現。
總結
以上是生活随笔為你收集整理的python网络编程项目_python网络编程详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AppScan api登录接口 post
- 下一篇: python3 x默认使用的编码_pyt