概述
? ? ? ? 在我們平時(shí)生活工作中,常常會(huì)接觸到網(wǎng)絡(luò)通信的內(nèi)容寻行,不管你是普通的用戶点晴,還是通信行業(yè)內(nèi)的開發(fā)人員感凤,都無法避免與網(wǎng)絡(luò)通信打交道。我在初步學(xué)習(xí)python的過程中粒督,對(duì)python的網(wǎng)絡(luò)通信問題做了總結(jié)陪竿,所以寫下這篇文章作為記錄,也希望能給其他初學(xué)者一些引導(dǎo)和啟發(fā)屠橄。這篇文章的主要內(nèi)容如下:
????1. 在深入講解之前族跛,我們先介紹一些背景信息;
????2. 介紹套接字的概念锐墙;
????3. 展示如何用Python來實(shí)現(xiàn)簡單的網(wǎng)絡(luò)應(yīng)用程序礁哄。
客戶端/服務(wù)器架構(gòu)(C/S架構(gòu))
????????什么是客戶端/服務(wù)器架構(gòu)?總的來說溪北,某一個(gè)特定的對(duì)象桐绒,并沒有嚴(yán)格的客戶端或者服務(wù)器之分,區(qū)分的標(biāo)準(zhǔn)在于之拨,在實(shí)現(xiàn)網(wǎng)絡(luò)通信的過程中茉继,該對(duì)象的行為模式?jīng)Q定了它是客戶端還是服務(wù)端。
服務(wù)器:為一個(gè)或者多個(gè)客戶端(用戶)提供所需“服務(wù)"的一系列硬件或者軟件蚀乔。工作流程可以簡單概括為:等待請(qǐng)求烁竭、響應(yīng)并提供服務(wù)、等待下一個(gè)請(qǐng)求吉挣。
客戶端:因特定請(qǐng)求而聯(lián)系服務(wù)器派撕,接收服務(wù)并處理相關(guān)事務(wù)的一方婉弹。客戶端可以持續(xù)向服務(wù)器發(fā)送請(qǐng)求腥刹,也可以在結(jié)束事務(wù)請(qǐng)求后不再發(fā)出請(qǐng)求马胧。
通信端點(diǎn)與套接字(socket)
????????在服務(wù)器相應(yīng)客戶端的請(qǐng)求之前,雙方需要進(jìn)行一系列的準(zhǔn)備工作衔峰。首先需要?jiǎng)?chuàng)建一個(gè)通信端點(diǎn)佩脊,服務(wù)器通過該通信端點(diǎn)監(jiān)聽客戶端的請(qǐng)求(當(dāng)然,實(shí)際的網(wǎng)絡(luò)通信中垫卤,處理不同類型的消息會(huì)使用不同類型的通信端點(diǎn)威彰,從而進(jìn)行區(qū)分)。
????????在網(wǎng)絡(luò)通信中穴肘,我們常用的一種通信端點(diǎn)為套接字(socket)歇盼,在通信開始之前,網(wǎng)絡(luò)應(yīng)用程序都需要?jiǎng)?chuàng)建套接字评抚。
套接字的分類
? ? 1. 基于文件的套接字
????????UNIX 套接字是我們所講的套接字的第一個(gè)家族豹缀,并且擁有一個(gè)“家族名字”:AF_UNIX(又名 AF_LOCAL,在 POSIX1.g 標(biāo)準(zhǔn)中指定),它代表地址家族(address family):UNIX慨代。
????????包括 Python 在內(nèi)的大多數(shù)受歡迎的平臺(tái)都使用術(shù)語地址家族及其縮寫 AF邢笙;其他比較舊的系統(tǒng)可能會(huì)將地址家族表示成域(domain)或協(xié)議家族(protocol family),并使用其縮寫 PF 而非 AF侍匙。類似地,AF_LOCAL(在 2000~2001 年標(biāo)準(zhǔn)化)將代替 AF_UNIX氮惯。然而,考慮到后向兼容性,很多系統(tǒng)都同時(shí)使用二者想暗,只是對(duì)同一個(gè)常數(shù)使用不同的別名妇汗。Python 本身仍然在使用 AF_UNIX。
? ? 2. 面向網(wǎng)絡(luò)的套接字
????????它也有自己的家族名字 AF_INET说莫,或者地址家族:因特網(wǎng)杨箭。另一個(gè)地址家族 AF_INET6 用于第 6 版因特網(wǎng)協(xié)議(IPv6)尋址。(還有其他的地址家族储狭,這些要么是專業(yè)的互婿、過時(shí)的、很少使用的,要么是仍未實(shí)現(xiàn)的晶密。)在所有的地址家族之中,目前 AF_INET 是使用得最廣泛的。
????????總的來說,Python 只支持 AF_UNIX模她、AF_NETLINK稻艰、AF_TIPC 和 AF_INET 家族。下面的內(nèi)容中,我們將使用 AF_INET侈净。
套接字地址:主機(jī)-端口對(duì)
? ? ? ? 正如我們?cè)诂F(xiàn)實(shí)生活中打電話一樣尊勿,套接字的通信需要一些記號(hào)用作識(shí)別功能僧凤。我們打電話的時(shí)候,可以通過區(qū)號(hào)和電話號(hào)碼的組合找到要呼叫的用戶元扔;在socket通信中躯保,我們通過主機(jī)-端口對(duì)找到通信的對(duì)象。
? ? ? ? 我們都知道澎语,通過url或者ip地址途事,我們可以在互聯(lián)網(wǎng)中找到特定的主機(jī)。在每一臺(tái)主機(jī)中擅羞,不同的端口號(hào)會(huì)用作不同的通信用途尸变。只要確定的主機(jī)和端口號(hào),就可以在網(wǎng)絡(luò)中唯一確定一個(gè)”通信端點(diǎn)“减俏,也就是找到了所要進(jìn)行通信的套接字的地址了召烂。
? ? ? ? 需要說明的是,有效的端口號(hào)范圍是0~65535(小于1024的端口號(hào)預(yù)留給了系統(tǒng))娃承。一般來說奏夫,其余的端口號(hào)我們都可以根據(jù)實(shí)際的需要進(jìn)行使用。
面向連接的套接字與無連接的套接字
? ? 1. 面向連接的套接字(TCP)
? ? ? ? 也稱為虛擬電路或流套接字历筝。主要提供序列化的酗昼、可靠的、不重復(fù)的數(shù)據(jù)漫谷,它可以將消息拆分成多個(gè)片段仔雷,確保每一條片段都順利到達(dá)目的地,然后按照順序組合在一起舔示,最后將完整的消息傳遞給正在等待的應(yīng)用程序碟婆。
? ? ? ? 實(shí)現(xiàn)的主要協(xié)議是傳輸控制協(xié)議(TCP)。創(chuàng)建TCP套接字時(shí)惕稻,必須使用SOCK_STREAM作為套接字類型竖共。
? ? 2. 無連接的套接字(UDP)
????????又稱為數(shù)據(jù)報(bào)類型的套接字。在傳輸過程中無法保證其順序性俺祠、可靠性公给、重復(fù)性,事實(shí)上所發(fā)送的報(bào)文有可能最后并沒有到達(dá)蜘渣,也有可能存在重復(fù)的消息淌铐。
? ? ? ? 我們之所以還繼續(xù)使用數(shù)據(jù)報(bào),這是相對(duì)于面向連接的套接字來看的蔫缸。由于維護(hù)TCP連接需要大量的開銷腿准,發(fā)送數(shù)據(jù)報(bào)能夠保證成本更加”低廉“,所以通常能提供更好的性能拾碌,并且可能更適合某一些類型的應(yīng)用程序吐葱。
? ? ? ? 實(shí)現(xiàn)的主要協(xié)議是用戶數(shù)據(jù)報(bào)協(xié)議(UDP)街望。創(chuàng)建UDP套接字,必須使用SOCK_DGRAM作為套接字類型弟跑。
Python中的網(wǎng)絡(luò)編程
socket()模塊函數(shù)
????????要?jiǎng)?chuàng)建套接字灾前,必須使用socket.socket()函數(shù),它的一般語法如下:
socket(socket_family, socket_type, protocol=0)
????????其中孟辑,socket_family是AF_UNIX或者AF_INET哎甲,socket_type是SOCK_STREAM或者SOCK_DGRAM,protocol通常省略扑浸,默認(rèn)為0 烧给。
????????在python編程中,我們首先在文件最開始的地方引用需要用到的模塊喝噪,在這里础嫡,我們使用“from socket import *",通過這樣的引用酝惧,就可以向下面這樣直接創(chuàng)建套接字:
tcpSock = socket(AF_INET, SOCK_STREAM)
udpSock = socket(AF_INET, SOCK_DGRAM)
????????成功創(chuàng)建套接字后榴鼎,我們就可以調(diào)用套接字對(duì)象的內(nèi)置方法進(jìn)行其他的操作了。具體的內(nèi)容可以查看python的文檔晚唇。
TCP通信
1. 創(chuàng)建TCP服務(wù)器
? ? ? ? 以下是一個(gè)TCP服務(wù)器程序巫财,它接受客戶端發(fā)送的字符串,將其打上時(shí)間戳之后哩陕,再返回給客戶端平项。二話不說,先貼代碼:
第1~4行:
????????聲明運(yùn)行環(huán)境悍及,導(dǎo)入socket模塊和time.ctime()闽瓢。
第6~10行:
? ? ? ? 指定主機(jī)地址、工作端口號(hào)心赶、接收緩存的長度扣讼。HOST和PORT共同組成ADDR,體現(xiàn)的就是上面我們所說的”主機(jī)-端口對(duì)“缨叫。服務(wù)器端的HOST為空椭符,表示它可以使用任意可用的地址。
第12~14行:
? ? ? ? 創(chuàng)建套接字 耻姥,把套接字綁定到服務(wù)器地址销钝,開啟TCP監(jiān)聽。
第16~32行:
? ? ? ? 進(jìn)入服務(wù)器的無限循環(huán)琐簇,不斷等待接收客戶端的連接蒸健。在第18行,我們通過accept()獲取到客戶端的tcpCliSock和addr,于是后續(xù)可以通過這個(gè)tcpCliSock專門處理該客戶端的事務(wù)(從而與其他請(qǐng)求的客戶端區(qū)分開來)纵装。在第22行,使用recv()接收消息据某,如果消息為空橡娄,則跳出循環(huán),關(guān)閉當(dāng)前客戶端的連接癣籽,然后繼續(xù)等待連接挽唉;如果不為空,則把消息解析出來筷狼,添加時(shí)間戳瓶籽,經(jīng)過重新編碼成ASCII字節(jié)后,通過send()發(fā)送回去給客戶端埂材。在這個(gè)程序里塑顺,第32行不會(huì)執(zhí)行,只是用于提醒編寫程序的人俏险,在使用這一套程序時(shí)严拒,必須要考慮到合理科學(xué)的退出方式,正確地調(diào)用close()方法竖独。
? ? ? ? 這里需要強(qiáng)調(diào)的是裤唠,由于python的編碼問題,所以我們的消息在主機(jī)端時(shí)要進(jìn)行解碼后才能正確顯示(如decode())莹痢,進(jìn)行編碼后才能發(fā)送到網(wǎng)絡(luò)端去(如encode()种蘸,bytes())。不管是在服務(wù)器還是客戶端竞膳,我們都需要考慮到這個(gè)情況航瞭。
2. 創(chuàng)建TCP客戶端
? ? ? ? 創(chuàng)建客戶端比服務(wù)器要簡單很多《ゲ拢客戶端與服務(wù)器建立連接沧奴,發(fā)送字符串給服務(wù)器,從服務(wù)器接收添加了時(shí)間戳的消息并打印长窄。發(fā)送空字符串即可關(guān)閉套接字滔吠,退出這次連接。代碼實(shí)例如下:
第1~3行:
????????聲明運(yùn)行環(huán)境挠日,導(dǎo)入socket模塊疮绷。
第5~9行:
????????指定主機(jī)地址、工作端口號(hào)嚣潜、接收緩存的長度冬骚。HOST和PORT共同組成ADDR,體現(xiàn)的就是上面我們所說的”主機(jī)-端口對(duì)“。這里的HOST為服務(wù)器端所在主機(jī)的地址只冻,由于我是在本地進(jìn)行通信測試的庇麦,所以地址設(shè)置為127.0.0.1(localhost)。在實(shí)際網(wǎng)絡(luò)通信的時(shí)候喜德,根據(jù)具體的情況進(jìn)行相應(yīng)的修改山橄。客戶端填寫的PORT必須與服務(wù)器填寫的PORT對(duì)應(yīng)才能正常通信舍悯。
第11~12行:
????????創(chuàng)建套接字 航棱,主動(dòng)調(diào)用并通過connect()連接到服務(wù)器。
第14~25行:
? ? ? ? 進(jìn)行無限循環(huán):客戶端填寫要發(fā)送的消息萌衬,發(fā)送后等待接收服務(wù)器的回復(fù)饮醇,接收到結(jié)果后將結(jié)果打印。當(dāng)客戶端輸入的內(nèi)容為空秕豫,或者服務(wù)器斷開連接朴艰、客戶端接收失敗時(shí),即退出循環(huán)混移,調(diào)用close()函數(shù)呵晚,關(guān)閉客戶端的套接字。
? ? ? ? 同樣的沫屡,發(fā)送消息前饵隙,我們需要對(duì)數(shù)據(jù)進(jìn)行編碼,接收的結(jié)果沮脖,也需要進(jìn)行解碼后才能正常顯示出來金矛。如果我們想要將代碼改成相應(yīng)的ipv6的形式,我們只需要把HOST改成“::1”勺届,sock_family改成AF_INET6即可驶俊。
UDP通信
1. 創(chuàng)建UDP服務(wù)器
? ? ? ? UDP服務(wù)器實(shí)現(xiàn)的功能與TCP基本一致,主要的區(qū)別在于UDP服務(wù)器不是面向連接的免姿,所以只需要等待客戶端的請(qǐng)求饼酿,回復(fù)消息即可,不需要將成功連接的客戶端“轉(zhuǎn)換”到一個(gè)獨(dú)立的套接字的操作胚膊。示例代碼如下:
第1~4行:
????????聲明運(yùn)行環(huán)境故俐,導(dǎo)入socket模塊和time.ctime()。
第6~10行:
? ? ? ? 與TCP相同紊婉,指定主機(jī)地址药版、工作端口號(hào)、接收緩存的長度喻犁。HOST和PORT共同組成ADDR槽片,體現(xiàn)的就是上面我們所說的”主機(jī)-端口對(duì)“何缓。服務(wù)器端的HOST為127.0.0.1,表示它使用本地主機(jī)地址还栓。
第12~13行:
????????創(chuàng)建套接字 碌廓,把套接字綁定到服務(wù)器地址,綁定后直接等待接收剩盒,不需要監(jiān)聽氓皱。
第15~24行:
????????進(jìn)入服務(wù)器的無限循環(huán),不斷等待接收客戶端的連接勃刨。在第17行,使用recvfrom()接收消息股淡,同時(shí)獲取通信客戶端的地址對(duì)信息身隐。緊接著把消息解析出來,添加時(shí)間戳唯灵,經(jīng)過重新編碼成ASCII字節(jié)后贾铝,通過sendto()發(fā)送回去給客戶端,此時(shí)由于服務(wù)器沒有與客戶端維護(hù)連接埠帕,所以要指定發(fā)送的地址對(duì)信息垢揩。同樣的,在這個(gè)程序里敛瓷,第24行不會(huì)執(zhí)行叁巨,只是用于提醒編寫程序的人,在使用這一套程序時(shí)呐籽,必須要考慮到合理科學(xué)的退出方式锋勺,正確地調(diào)用close()方法。
2. 創(chuàng)建UDP客戶端
? ? ? ? 原理非常簡單狡蝶,上面都有提及到庶橱,就不作詳細(xì)說明了。直接貼代碼:
關(guān)于服務(wù)器退出的問題
? ? ? ? 最后再提以下服務(wù)器退出的問題贪惹。在開發(fā)中苏章,一種相對(duì)比較友好的退出方式是:將服務(wù)器的while部分放在一個(gè)try-except的try子句中,并監(jiān)控EOFError和KeyboardInterrupt異常奏瞬,這樣就可以在except或者finally子句中關(guān)閉服務(wù)器的套接字枫绅。
參考內(nèi)容:
《Python核心編程(第三版)》[美] Wesley Chun 著,孫波翔硼端、李斌撑瞧、李晗 譯。