天天看點

Python 3 UDP、TCP程式設計

一、UDP 介紹

UDP 是User Datagram Protocol的簡稱, 中文名是使用者資料報協定,是OSI(Open System Interconnection,開放式系統互聯) 參考模型中一種無連接配接的傳輸層協定,提供面向事務的簡單不可靠資訊傳送服務。與TCP協定不同的是,UDP協定是面向非連接配接的,且效率較高,不需要等待伺服器的回應。它類似于現實中的發短信和郵件等,發送方隻管發送資訊即可,無需關注資訊是否丢失。

使用UDP協定時,不需要建立連接配接,隻需要知道對方的IP位址和端口号,就可以直接發資料包。發送消息和接受消息的雙方沒有明确的用戶端和服務端之分,發送方隻負責發送消息,能不能到達就不知道了。雖然用UDP協定傳輸資料不可靠,但它的優點是和TCP協定比,速度快,對于不要求可靠到達的資料,就可以使用UDP協定。

二、收發資料

1.發送資料

下面通過一個簡單的執行個體來模拟udp用戶端發送消息。

使用UDP發送資料

基本代碼如下:

from socket import *
           

上面代碼導入socket子產品,下面代碼建立udp對象

socket_udp = socket(type=SOCK_DGRAM)
           

假設需要向位址192.168.1.101的主機,端口号為5678的程序發送消息,使用下面代碼指定元組資訊:

addr = ('192.168.1.101',5678)
           

下面代碼從鍵盤輸入消息:

msg = input('>')
           

下面代碼就實作資訊的發送,使用udp對象的sendto方法,其中參數包含發送的消息以及對方的IP位址和端口号。

socket_udp.sendto(msg.encode('gbk'),addr)
           

發送消息完畢後把對象關閉:

socket_udp.close()
           

我們使用軟體NetAssist模拟接收放主機,設定IP位址和端口号為192.168.1.10和5678,界面如圖所示:

Python 3 UDP、TCP程式設計

運作上面程式,在鍵盤輸入“你好”,按Enter鍵,則模拟主機将接受到資訊,如圖所示:

Python 3 UDP、TCP程式設計

可以看出,模拟主機已經正常收到了資訊。

上面代碼中也可以綁定發送方的端口号,如下所示:

socket_udp.bind(('',7777))
           

這樣運作的時候,端口号會固定,否則每次運作端口号都是系統随機配置設定。

2.接收資料

上面模拟的是用戶端發送消息,下面模拟服務端接收消息。

udp服務端接收消息

代碼如下:

from socket import *
socket_udp = socket(type=SOCK_DGRAM)
socket_udp.bind(('',7788))
print('等待接收消息...')
data,addr = socket_udp.recvfrom(1024)
print('接收消息成功...')
print('【Receive from %s : %s】:%s'%(addr[0],addr[1],data.decode('gbk')))
socket_udp.close()
           

和上面發送消息基本類似,隻不過這個代碼中使用recvfrom方法,其中參數設定接受最大的資訊緩存數,此處設定為1024個字元,該方法傳回的是元組資訊,第一個值是接收的資訊,第二個值是發送方的IP位址,第三個是發送方的端口。另外此時接收方的端口号必須綁定,這樣用戶端發送消息的時候才能指定向那個主機和端口發送資訊。

運作程式,伺服器端将等待接收資訊,此時使用模拟的發送端發送資訊。

Python 3 UDP、TCP程式設計

如上圖所示,首先輸入IP位址和端口号,然後輸入要發送的資訊,最後單擊“發送”按鈕,此時伺服器端将接收到資訊,并顯示。

Python 3 UDP、TCP程式設計

三、通信過程

上面分别介紹了接收資料的執行個體和發送資料的執行個體,下面介紹即可以接收,又可以發送的執行個體,實作雙方進行通信的基本操作。當接收到資料就回複資料。時間上就是把上面兩部分代碼進行合并即可實作。

代碼如下:

from socket import *

socket_udp = socket(type=SOCK_DGRAM)
socket_udp.bind(('',7788))

while True:
    data,addr = socket_udp.recvfrom(1024)
    print('【Receive from %s : %s】:%s'%(addr[0],addr[1],data.decode('gbk')))
    socket_udp.sendto(data,addr)
socket_udp.close()
           

上面代碼中,使用recvfrom從發送方接收資訊,然後使用sendto把資訊再發送到發送方。

Python 3 UDP、TCP程式設計

使用多線程實作udp收發消息,這段代碼也可以通過多線程的方法進行實作,代碼如下:

from socket import *
from threading import Thread
           

下面代碼定義發送資訊的函數:

def send_msg(data,addr):
    print('【Receive from %s : %s】:%s' % (addr[0], addr[1], data.decode('gbk')))
    msg = input('>').encode('gbk')
    # 發送
    socket_udp.sendto(msg, addr)

socket_udp = socket(type=SOCK_DGRAM)
socket_udp.bind(('',7788))

while True:
    print('主線程等待接收消息...')
    data,addr = socket_udp.recvfrom(1024)
    Thread(target=send_msg,args=[data,addr]).start()

socket_udp.close()
           

運作上面代碼,結果如圖

Python 3 UDP、TCP程式設計

這個執行個體就模拟了QQ聊天的基本實作過程,隻不過這裡編碼還沒有使用圖形視窗界面。

四、udp廣播

基于UDP協定傳輸的特點:通過位址發送消息,無須事先建立連接配接,我們可以實作廣播的功能,例如日常網絡上所見到的視訊廣播、音頻廣播都可以基于UDP協定實作,其基本原理可以使用下圖概述:

Python 3 UDP、TCP程式設計

上圖中機器A如果想機器B1,B2,B3和B4發送消息,傳統的方法是一對一的發送,現在可以通過廣播的形式發送,首先機器A向具有廣播功能的路由器發送資訊,然後路由器再向各個機器發送,即實作廣播發送,這裡有幾點需要注意,第一是機器B1,B2,B3和B4必須在一個網段内,根據前面介紹的IP位址的編碼規則可以,IP位址的前面3個十進制數字可以确定網段,例如192.168.1.10位址中192.168.1标明網段。此外,還要求所有機器B1,B2,B3和B4上以相同端口接收某個任務,如上圖中端口号為7788。另一個需要注意的是具有廣播功能的路由器的廣播的位址可以通過IP位址和子網路遮罩進行計算,下圖是一個計算工具:

Python 3 UDP、TCP程式設計

在上面這個電腦中輸入主機的IP位址和子網路遮罩中1的個數,然後單擊“計算”按鈕,即可以得到對應的廣播位址。

是以,根據前面的發送消息的執行個體,即可得到廣播的程式。

udp廣播,代碼如下:

from socket import *
from threading import Thread

socket_udp = socket(type=SOCK_DGRAM)
           

下面代碼設定可以發送廣播消息

socket_udp.setsockopt(SOL_SOCKET,SO_BROADCAST,1)
addr = ('192.168.1.255',7788)
socket_udp.sendto(input('>').encode('utf-8'),addr)
socket_udp.close()
print('廣播消息發送完畢')
           

上面代碼中使用udp對象的setsockopt方法實作發送廣播消息。其它的代碼和基本發送消息的代碼完全類似。運作代碼,效果如圖:

Python 3 UDP、TCP程式設計

由于位址指定為計算的廣播位址,端口号為7788,這樣所有該網絡段内的機器,隻要其端口号7788運作任務,即可接收消息。

——————————————手動分割線——————————————————
           

7月30号更新

一、tcp介紹

前面一節介紹的UDP協定是是面向非連接配接的,發送的時候主機不需要和其它機器建立連接配接,隻需向指定位址和端口号發送消息即可,至于另外一台機器是否收到資訊,發送端不關心。而TCP(Transmission ControlProtocol 傳輸控制協定)是一種面向連接配接的、可靠的、基于位元組流的傳輸層通信協定。建立TCP連接配接時,主動發起連接配接的叫用戶端,被動響應連接配接的叫伺服器。舉個例子,當我們在浏覽器中通路新浪時,我們自己的計算機就是用戶端,浏覽器會主動向新浪的伺服器發起連接配接。如果一切順利,新浪的伺服器接受了我們的連接配接,一個TCP連接配接就建立起來的,後面的通信就是發送網頁内容了。也就是說,發送資訊之前,主機和用戶端之間必須建立可靠的連接配接。

二、tcp用戶端程式設計

下面通過一個執行個體來認識如何通過TCP協定進行資訊的發送。

首先模拟用戶端發送資訊,此時仍然使用伺服器NetAssist模拟伺服器,用于接收資訊。配置如圖。

Python 3 UDP、TCP程式設計

如上圖所示,在協定類型中選擇“TCP Server”,在本地位址中輸入自己機器的位址,這裡輸入“192.168.1.101”,本地端口号輸入“7788”

用戶端程式如下:

from socket import *   
           

下面代碼建立tcp對象,注意此處和udp不一樣之處在使用udp使用type=SOCK_DGRAM參數,而tcp使用type=SOCK_STREAM,不過這時系統的預設值,可以省略。

socket_tcp = socket()
           

下面代碼給出伺服器的位址:

server_addr = ('192.168.1.101',7788)
           

下面代碼建立與伺服器的連接配接:

socket_tcp.connect(server_addr)
           

注意:如果對應的伺服器沒有啟動,那麼系統就會提示錯誤。

下面代碼實作發送消息:

socket_tcp.send(input('>').encode('gbk'))
           

下面代碼實作tcp對象的關閉:

socket_tcp.close()
print('over...')
           

運作程式,輸入“你好”,如圖:

Python 3 UDP、TCP程式設計

此時伺服器端如圖。

Python 3 UDP、TCP程式設計

可以看到,在“網絡資料接收”下面出現用戶端發送的資訊,同時還可以看出發送方的IP位址和端口号。

從上面程式中也可以看出,用戶端發送消息完畢之後,斷開了與伺服器的連接配接,但是伺服器仍然可以監聽資訊。

三、tcp服務端程式設計

對于伺服器端的程式編碼,需要考慮以下幾個步驟:

1、端口綁定,若是屬于本地位址,IP位址可以不寫,使用tcp對象的bind方法實作;

2、使用tcp對象的listen()方法進行監聽,看是否有資訊傳送;

3、使用tcp對象的accept()方法進行接收,傳回用戶端的端口号和IP位址; 使用tcp對象的recv()方法接收資料;

4、使用tcp對象的send()方法向用戶端發送資訊。

下面是具體代碼:

from socket import *
socket_tcp = socket()
socket_tcp.bind(('',7788))
socket_tcp.listen()
           

上面代碼執行之後,系統處于等待狀态,直到有用戶端連接配接成功,等待狀态解除,下面代碼實作接收資訊:

socket_client,addr_client = socket_tcp.accept()
data = socket_client.recv(1024)
print('%s...%s'%(str(addr_client),data.decode('gbk')))
           

下面代碼實作發送消息

socket_client.send('OK'.encode('gbk'))
           

下面代碼關閉與用戶端的連接配接和TCP對象:

socket_client.close()
socket_tcp.close()

print('over...')
           

上面是伺服器端代碼,同樣使用NetAssist模拟用戶端,配置如下圖

Python 3 UDP、TCP程式設計

在上圖中需要在“協定類型”中選擇“TCP Client”,此時在下部空白處輸入資訊,單擊“發送”按鈕,此時伺服器端将接收到資料;如果伺服器端發送資料,在上部的空白處将顯示接收的資訊。

四、tcp三次握手

為了提供可靠的傳送,TCP在發送新的資料之前,确認用戶端和伺服器都已經準備好,然後才開始發送資料。tcp三次握手主要在于用戶端與伺服器連接配接的時候,即執行connect方法時如何實作的。下圖給出了三次握手的基本過程:

Python 3 UDP、TCP程式設計

如上所示:

第一次握手:建立連接配接時,用戶端發送SYN包(SYN seq=x)到伺服器,并進入SYN_SENT狀态,等待伺服器确認;其中SYN表示同步序列編号(Synchronize Sequence Numbers)。

第二次握手:伺服器收到SYN包,必須确認客戶的SYN(SYN ack=x+1),同時自己也發送一個SYN包(seq=y),即SYN+ACK包,此時伺服器進入SYN_RECV狀态;

第三次握手:用戶端收到伺服器的SYN+ACK包,向伺服器發送确認包ACK(ack=k+1),此包發送完畢,用戶端和伺服器進入ESTABLISHED(TCP連接配接成功)狀态,完成三次握手。

可以用一個形象的比喻,第一次握手的時候,用戶端發送“你在嗎?”,第二次握手的時候,伺服器收到資訊後,回複消息“我在,你還在嗎”;第三次握手。用戶端回複消息“我還在”。這樣雙方建立連接配接。

當服務端和用戶端連接配接之後,就可以在二者之間進行資訊的傳送,此時如果用戶端與伺服器端之間的連接配接中斷,伺服器端必須通過适當的方法進行判斷,否則伺服器端将出現阻塞。

下面通過代碼進行示範:

from socket import *

socket_tcp = socket()
socket_tcp.bind(('',7788))
socket_tcp.listen()
socket_client,addr_client = socket_tcp.accept()
print('%s...連接配接成功'%str(addr_client)) 
           

上面代碼是伺服器偵聽用戶端的資料傳送,當有資料開始傳送的時候,列印出“連接配接成功”,并給出用戶端的位址。下面代碼中判斷接收的資料是否為空(使用b表示空),如果為空就表示用戶端連接配接已經斷開,并列印“斷開了”,否則就列印接收到的資料。

while True:
    data = socket_client.recv(1024)
    if data!=b'':
        data = data.decode('gbk')
        print('%s...%s' % (str(addr_client), data))
    else:
        print('%s...斷開了'%str(addr_client))
        break

socket_client.close()
socket_tcp.close()

print('over...')
           

運作上面這段伺服器代碼,同時使用NetAssist模拟用戶端發送資訊,如下圖所示

Python 3 UDP、TCP程式設計

用戶端配置過程同前面例題,此時在下方空白處輸入資訊,單擊“發送”按鈕,伺服器即可以接收到資訊,當在模拟用戶端單擊“斷開”按鈕,伺服器将會根據斷開資訊,顯示“斷開了”,如下圖所示

Python 3 UDP、TCP程式設計

上面這個例子,伺服器端隻能接收一個用戶端的連接配接和資料發送,如果想同時連接配接多個用戶端,需要使用多線程編碼。

五、tcp四次揮手

在實際協定操作中,TCP斷開連接配接是通過四次揮手實作的。由于TCP連接配接是全雙工的,是以每個方向都必須單獨進行關閉。這原則是當一方完成它的資料發送任務後就能發送一個FIN來終止這個方向的連接配接。收到一個 FIN隻意味着這一方向上沒有資料流動,一個TCP連接配接在收到一個FIN後仍能發送資料。首先進行關閉的一方将執行主動關閉,而另一方執行被動關閉。下圖給出了實作的基本過程:

Python 3 UDP、TCP程式設計

四次揮手的基本過程如下:

(1) TCP用戶端發送一個FIN,用來關閉客戶到伺服器的資料傳送。

(2) 伺服器收到這個FIN,它發回一個ACK,确認序号為收到的序号加1。和SYN一樣,一個FIN将占用一個序号。

(3) 伺服器關閉用戶端的連接配接,發送一個FIN給用戶端。

(4) 用戶端發回ACK封包确認,并将确認序号設定為收到序号加1。

可以用一個形象的比喻來示範:例如用戶端發出資訊“我要退出了”,伺服器收到資訊發出資訊“我知道了”,同時伺服器繼續發出資訊“我也要退出”,用戶端接收到資訊,回複“好的”,這樣經過四次過程,實作了連接配接的斷開。

六、tcp十種狀态

通過上面三次握手和四次揮手的過程,可以知道,共有十種狀态,分别如下:

1)CLOSED:表示關閉狀态(初始狀态)。

2)LISTEN:該狀态表示伺服器端的某個SOCKET處于監聽狀态,可以接受連接配接。

3)SYN_SENT:這個狀态與SYN_RCVD遙相呼應,當用戶端SOCKET執行CONNECT連接配接時,它首先發送SYN封包,随即進入到了SYN_SENT狀态,并等待服務端的發送三次握手中的第2個封包。SYN_SENT狀态表示用戶端已發送SYN封包。

4)SYN_RCVD: 該狀态表示接收到SYN封包,在正常情況下,這個狀态是伺服器端的SOCKET在建立TCP連接配接時的三次握手會話過程中的一個中間狀态,很短暫。此種狀态時,當收到用戶端的ACK封包後,會進入到ESTABLISHED狀态。

5)ESTABLISHED:表示連接配接已經建立。

6)FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2狀态的真正含義都是表示等待對方的FIN封包。差別是: FIN_WAIT_1狀态是當socket在ESTABLISHED狀态時,想主動關閉連接配接,向對方發送了FIN封包,此時該socket進入到FIN_WAIT_1狀态。 FIN_WAIT_2狀态是當對方回應ACK後,該socket進入到FIN_WAIT_2狀态,正常情況下,對方應馬上回應ACK封包,是以FIN_WAIT_1狀态一般較難見到,而FIN_WAIT_2狀态可用netstat看到。

7)FIN_WAIT_2:主動關閉連結的一方,發出FIN收到ACK以後進入該狀态。稱之為半連接配接或半關閉狀态。該狀态下的socket隻能接收資料,不能發。

8)TIME_WAIT: 表示收到了對方的FIN封包,并發送出了ACK封包,等2MSL後即可回到CLOSED可用狀态。如果FIN_WAIT_1狀态下,收到對方同時帶 FIN标志和ACK标志的封包時,可以直接進入到TIME_WAIT狀态,而無須經過FIN_WAIT_2狀态。

9)CLOSE_WAIT: 此種狀态表示在等待關閉。當對方關閉一個SOCKET後發送FIN封包給自己,系統會回應一個ACK封包給對方,此時則進入到CLOSE_WAIT狀态。接下來呢,察看是否還有資料發送給對方,如果沒有可以 close這個SOCKET,發送FIN封包給對方,即關閉連接配接。是以在CLOSE_WAIT狀态下,需要關閉連接配接。

10)LAST_ACK: 該狀态是被動關閉一方在發送FIN封包後,最後等待對方的ACK封包。當收到ACK封包後,即可以進入到CLOSED可用狀态。

七、tcp長連接配接和短連接配接

前面已經認識到,當網絡通信時采用TCP協定時,在真正的讀寫操作之前,server與client之間必須建立一個連接配接,當讀寫操作完成後,雙方不再需要這個連接配接時它們可以釋放這個連接配接,連接配接的建立是需要三次握手的,而釋放則需要4次揮手,是以說每個連接配接的建立都是需要資源消耗和時間消耗的。

1.tcp短連接配接

用戶端向伺服器發起連接配接請求,伺服器接到請求,然後雙方建立連接配接。用戶端向伺服器 發送消息,伺服器回應用戶端,然後一次讀寫就完成了,這時候雙方任何一個都可以發起關閉操作,不過一般都是用戶端先發起 close操作。因為一般的伺服器不會回複完用戶端後立即關閉連接配接的,當然不排除有特殊的情況。從上面的描述看,短連接配接一般隻會在 用戶端/伺服器r間傳遞一次讀寫操作

短連接配接的優點是:管理起來比較簡單,存在的連接配接都是有用的連接配接,不需要額外的控制手段

2.tcp長連接配接

用戶端向伺服器發起連接配接,伺服器接受用戶端連接配接,雙方建立連接配接。用戶端與伺服器完成一次讀寫之後,它們之間的連接配接并不會主動關閉,後續的讀寫操作會繼續使用這個連接配接。

長連接配接和短連接配接的産生在于client和server采取的關閉政策,具體的應用場景采用具體的政策,沒有十全十美的選擇,隻有合适的選擇。

範例:給飛秋發消息

分析:飛秋是一個常用于區域網路的網際網路聊天軟體,飛秋是一個經典的應用程式,通過飛秋,不同使用者可以發送和接收消息,以及檔案,本例設定用戶端,向飛秋發送消息。飛秋使用的是udp協定,并在此基礎上包裝成了IPMSG應用層協定。所謂應用層協定,是指對消息的格式有一定的要求, 其基本格式如下:

版本号:包編号:發送者姓名:發送者機器名:指令字:消息
           

例如下面就符合标準的應用層格式的代碼:

1:12323434:user:machine:32:hello
           

下面是程式代碼:

import socket
import random
           

上面代碼導入所需要的第三方庫函數。

下面代碼使用清單給出使用者名和所使用的機器:

ls1 = ['小王','小張','小李']
ls2 = ['WANG-PC','ZHANG-PC','LI-PC']
           

下面代碼建立socket對象

udpSocket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
           

下面給出飛秋運作程式的電腦的位址和端口号:

destAdress = ('192.168.1.101',2425)
           

下面是輸入的消息内容:

sendMsg = input('>>')
           

下面将輸入資料組成符合要求的格式,其中随機選擇使用者名和機器名稱:

data = '1:12323434:%s:%s:32:%s'%(random.choice(ls1),random.choice(ls2),sendMsg)
data = data.encode('gbk')
           

下面代碼将資料發送給飛秋發送:

udpSocket.sendto(data,destAdress)
           

使用完畢後需要及時關閉socket對象:

udpSocket.close()
print('over......')
           

首先啟動飛秋應用程式,進入監聽狀态,程式如下圖所示:

Python 3 UDP、TCP程式設計

然後運作上面程式,輸入發送的資訊,例如“你好”,此時飛秋将接收到資訊,如下圖所示:

Python 3 UDP、TCP程式設計

實戰:程式設計實作多程序伺服器,運作效果如圖

Python 3 UDP、TCP程式設計

程式設計實作多線程服務,實作效果和上面的實戰一樣。

繼續閱讀