一、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,界面如圖所示:
運作上面程式,在鍵盤輸入“你好”,按Enter鍵,則模拟主機将接受到資訊,如圖所示:
可以看出,模拟主機已經正常收到了資訊。
上面代碼中也可以綁定發送方的端口号,如下所示:
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位址,第三個是發送方的端口。另外此時接收方的端口号必須綁定,這樣用戶端發送消息的時候才能指定向那個主機和端口發送資訊。
運作程式,伺服器端将等待接收資訊,此時使用模拟的發送端發送資訊。
如上圖所示,首先輸入IP位址和端口号,然後輸入要發送的資訊,最後單擊“發送”按鈕,此時伺服器端将接收到資訊,并顯示。
三、通信過程
上面分别介紹了接收資料的執行個體和發送資料的執行個體,下面介紹即可以接收,又可以發送的執行個體,實作雙方進行通信的基本操作。當接收到資料就回複資料。時間上就是把上面兩部分代碼進行合并即可實作。
代碼如下:
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把資訊再發送到發送方。
使用多線程實作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()
運作上面代碼,結果如圖
這個執行個體就模拟了QQ聊天的基本實作過程,隻不過這裡編碼還沒有使用圖形視窗界面。
四、udp廣播
基于UDP協定傳輸的特點:通過位址發送消息,無須事先建立連接配接,我們可以實作廣播的功能,例如日常網絡上所見到的視訊廣播、音頻廣播都可以基于UDP協定實作,其基本原理可以使用下圖概述:
上圖中機器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位址和子網路遮罩進行計算,下圖是一個計算工具:
在上面這個電腦中輸入主機的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方法實作發送廣播消息。其它的代碼和基本發送消息的代碼完全類似。運作代碼,效果如圖:
由于位址指定為計算的廣播位址,端口号為7788,這樣所有該網絡段内的機器,隻要其端口号7788運作任務,即可接收消息。
——————————————手動分割線——————————————————
7月30号更新
一、tcp介紹
前面一節介紹的UDP協定是是面向非連接配接的,發送的時候主機不需要和其它機器建立連接配接,隻需向指定位址和端口号發送消息即可,至于另外一台機器是否收到資訊,發送端不關心。而TCP(Transmission ControlProtocol 傳輸控制協定)是一種面向連接配接的、可靠的、基于位元組流的傳輸層通信協定。建立TCP連接配接時,主動發起連接配接的叫用戶端,被動響應連接配接的叫伺服器。舉個例子,當我們在浏覽器中通路新浪時,我們自己的計算機就是用戶端,浏覽器會主動向新浪的伺服器發起連接配接。如果一切順利,新浪的伺服器接受了我們的連接配接,一個TCP連接配接就建立起來的,後面的通信就是發送網頁内容了。也就是說,發送資訊之前,主機和用戶端之間必須建立可靠的連接配接。
二、tcp用戶端程式設計
下面通過一個執行個體來認識如何通過TCP協定進行資訊的發送。
首先模拟用戶端發送資訊,此時仍然使用伺服器NetAssist模拟伺服器,用于接收資訊。配置如圖。
如上圖所示,在協定類型中選擇“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...')
運作程式,輸入“你好”,如圖:
此時伺服器端如圖。
可以看到,在“網絡資料接收”下面出現用戶端發送的資訊,同時還可以看出發送方的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模拟用戶端,配置如下圖
在上圖中需要在“協定類型”中選擇“TCP Client”,此時在下部空白處輸入資訊,單擊“發送”按鈕,此時伺服器端将接收到資料;如果伺服器端發送資料,在上部的空白處将顯示接收的資訊。
四、tcp三次握手
為了提供可靠的傳送,TCP在發送新的資料之前,确認用戶端和伺服器都已經準備好,然後才開始發送資料。tcp三次握手主要在于用戶端與伺服器連接配接的時候,即執行connect方法時如何實作的。下圖給出了三次握手的基本過程:
如上所示:
第一次握手:建立連接配接時,用戶端發送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模拟用戶端發送資訊,如下圖所示
用戶端配置過程同前面例題,此時在下方空白處輸入資訊,單擊“發送”按鈕,伺服器即可以接收到資訊,當在模拟用戶端單擊“斷開”按鈕,伺服器将會根據斷開資訊,顯示“斷開了”,如下圖所示
上面這個例子,伺服器端隻能接收一個用戶端的連接配接和資料發送,如果想同時連接配接多個用戶端,需要使用多線程編碼。
五、tcp四次揮手
在實際協定操作中,TCP斷開連接配接是通過四次揮手實作的。由于TCP連接配接是全雙工的,是以每個方向都必須單獨進行關閉。這原則是當一方完成它的資料發送任務後就能發送一個FIN來終止這個方向的連接配接。收到一個 FIN隻意味着這一方向上沒有資料流動,一個TCP連接配接在收到一個FIN後仍能發送資料。首先進行關閉的一方将執行主動關閉,而另一方執行被動關閉。下圖給出了實作的基本過程:
四次揮手的基本過程如下:
(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......')
首先啟動飛秋應用程式,進入監聽狀态,程式如下圖所示:
然後運作上面程式,輸入發送的資訊,例如“你好”,此時飛秋将接收到資訊,如下圖所示:
實戰:程式設計實作多程序伺服器,運作效果如圖
程式設計實作多線程服務,實作效果和上面的實戰一樣。