SOCKET套接字
來自專欄 編程學習筆記
之前在另一篇里總結了網路傳輸的協議,主機,IP,域名,埠的具體內容。具體看網路傳輸。
我們知道TCP UDP,但協議只是規定,沒有具體的實施方案,那麼在python編程中如何實現這些協議提供的服務呢?在編程時要用到SOCKET套接字這個東西。
套接字,這個比較抽象,沒有特別的含義,一開始創建的人按照他們的文化定義的東西,再被翻譯過來的。是網路間進行通信的方式的名稱。
最開始,套接字是一種方法,在linux中演化為一種文件類型,socket文件,是一種特殊的文件,是臨時文件,用ls命令通常捕捉不到它。linux系統下文件有七種,管道,b(二進位文件)c(字元設備文件)d(目錄文件)-(普通文件)s(套接字文件)p(管道文件)
無論用什麼樣的編程語言和操作系統,幾乎網路通信的編程實現都是用socket
套接字分類:三類(常用前兩類)
流式套接字:表示傳輸層使用tcp協議提供面向連接的可靠的傳輸服務
數據報套接字:表示傳輸層使用UDP協議提供面向連接的可靠的傳輸服務
原始套件字:一般用作底層協議測試(一般用不到)
為什麼叫流式,為什麼叫數據報?
指的是數據在傳輸時的形式,以位元組流形式還是以消息結構體(將消息打包封裝為一種結構)。好比說水的傳輸,前者是水流分不開,後者是變為一個個冰塊。
基於tcp協議的socket編程,網路傳輸是兩端的傳輸。
服務端
下面做個類比,假如你(伺服器)在等一個電話(一個請求),必要條件是
1.要有一個電話(創建一個tcp流式套接字)
2.然後得有電話號碼,要有卡號(綁定本機的IP和埠號,IP和埠號決定伺服器的位置)
3.這個卡得有錢才能用(將套接字變為可監聽套接字,就可以接收請求)
4.等電話(套接字等待客戶端請求)
5.接聽電話,互通信息(消息的收發)
6.掛斷電話(關閉套接字不等待請求)最後一個類比不是特別恰當,以現實中的案例來模擬能幫助我們理解,但是並不一定能完全對上。
在編程時基本是按照這個流程,每一步都有相應的函數介面,以圖為例
那這些函數介面都在哪裡呢,都封裝在一個模塊socket中,需要import進來。
創建一個文件夾
~$ mkdir pythonweb
~$ cd pythonweb
~/pythonweb $ mkdir day1
創建好後在sublime編輯器中打開day1文件夾,新建一個文件命名為tcp_server.py
在這個文件里編寫基於tcp協議的服務端通信過程
1.import socket #引入python網路套接字模塊
2.#創建一個tcp流式套接字
3.socket.socket() #用模塊中的socket函數,這個函數常用有三個參數
#socket(family=AF_INET,type=SOCK_STREM,proto=0),三個形參都有默認值。
一個函數有功能,參數,和返回值。
功能:創建一個套接字對象,第一個參數family是協議族類型,AF_INET表示的是IPv4地址類型,最常用,當然也有表示IPv6的類型,是AF_INET6。type表示要創建的套接字類型,SOCK_STREM是流式套接字,還可以是SOCK_DGRAM,表示要創建UDP數據報套接字。proto表示子協議選項,通常為0,沒有子協議。
返回值是套接字對象,這裡定義為sockfd。
3.綁定本機的IP和埠號,使用bind()函數
bind(address)
參數:一個包含兩個元素的元組的類型,第一個元素是本機主機,是字元串類型,第二個是使用的埠號,是數字類型。沒有返回值。
可以寫成(『 』,8888),用空字元串表示本機。(『localhost』,8888),或者是(『127.0.0.1』,8888),這三個地址只能用於本機的測試。
(『0.0.0.0』,8888)和(『192.168.0.109』,8888)用這兩個地址(也是本機地址)來綁定到伺服器端的話,可以讓區域網內的其他IP來訪問這個服務端程序。
8888這個埠號可以隨便取。
既然函數的參數是元組,那我就可以事先把參數給一個元組變數
HOST=「127.0.0.1」
POST=8888
ADDR=(HOST,POST),ADDR就是一個元組變數
sockfd.bind(ADDR)
3.將套接字變為監聽套接字
sockfd.listen(5)
listen(n)功能:將套接字設置為監聽套接字(讓套接字可以接收客戶端的連接,這是tcp特有的,因為需要建立連接),並且設置一個連接等待隊列,參數
是一個正整數,無返回值。
連接等待隊列是什麼?以圖為例;
套接字變為可監聽套接字時,這個套接字可以連接多個客戶端,客戶端連接進來需要先經歷一個過程,因為是tcp連接,所以需要三次握手過程,在同一時刻只能處理一個客戶端的連接請求。建立一個請求隊列,先處理一個,其他排隊等候。
參數n的值表示設置的這個隊列同一時刻最多能夠容納幾個客戶端的連接請求,而套接字本身可以接收很多客戶端的請求,只是在同一時刻需要一個隊列。
n的取值自己可以定義,在linux操作系統中,這個n沒有什麼用,因為linux內核會自動設置這個隊列的大小。
4.套接字等待接收客戶端請求
conn,addr=sockfd.accept()
accept()功能:阻塞等待客戶端的連接,當沒有客戶端連接時會一直阻塞在這裡,直到有客戶端連接就結束阻塞。這一步里就實現了三次握手。
參數無。難點在有兩個返回值,第一個conn,為和客戶端交互的新的套接字,第二字返回值addr,為連接進來的客戶端的地址。
之前提到的套接字sockfd是用來處理客戶端的連接請求,一個伺服器可以連接很多客戶端,再用sockfd這個套接字和客戶端交互就不清楚了,每有一個客戶端連接進來就創建一個新的套接字進行交互,維護和客戶端的關係。sockfd就只用負責連接,不負責交互。addr是連進來的那個客戶端的地址。
5.消息的收發
作為伺服器,一般先收消息。接下來的操作用的是conn這個套接字的函數,方法和屬性。
conn.recv()
recv(buffer)函數功能:接收網路消息
參數buffer的值是一個正整數,表示一次接收從緩衝區拿到的消息的位元組數,返回值是接收到的消息。
客戶端在向伺服器端發送消息,通過網路,我們知道網路是個不太穩定的東西,發的消息,服務端可能一次接收不完。可能發的快,接收的慢(也可能發的慢,接收的快),總之會存在時間差,客戶端發送的消息並不是直接給伺服器端程序,而是系統自動創建一個叫做緩衝區的空間(大小系統自定),也叫網路緩衝區,把消息放到緩衝區中,recv從這個緩衝區中拿到伺服器端進程中,一個拿多少位元組的內容呢?這就是設置參數的作用了。
設置BUFFERSIZE=1024,一次拿1024位元組,也就是1k。
data=conn.recv(BUFFERSIZE)
收到消息後,我們回發一下消息
conn.send()
send()函數
功能:發送網路消息
參數:要發送的內容
返回值:實際發送的位元組數(注意這裡的**實際**二字)
**python3**中要求send的內容必須是bytes格式,同樣之前接收的也是bytes格式的數據。
n=conn.send(「Recv your message」),這樣是不行的,因為這是字元串信息,不是bytes格式,要進行轉化。轉化的方法有兩種:
一是在信息後加入encode()進行轉化,如(「Recv your message」.encode())
二是前面加一個b,如(b「Recv your message」.encode(),但是加b不是萬能的,當這個信息存儲在一個變數中,如data1=「hello world」,n=conn.send(data1.encode()),只能用encode(),而不能用b了。
也就是當參數是字元串常量時用b,是字元串變數時用.decode()
6收發完結後關閉套接字
.conn.close() #表示和客戶端斷開連接
sockfd.close() #清除套接字,不能再使用套接字了。
為了將實驗,有一些現象在代碼中加一些列印信息。
比如在監聽後加入
print(「wait for connect..」)
在accept之後加入
print(「connect from」,addr) #列印輸出連進來的那個客戶端地址,這個地址也是元組的形式,包括IP和埠。
在接收到消息後recv之後加入
print(「接收到:」,data.decode())#data是bytes格式,要進行轉化
回發信息後列印
print(「發送了%d位元組個數據」%n)
如果沒有列印信息,整個程序運行起來沒有現象.
**decode(),encode()默認的是在字元串和bytes之間進行轉換**
整個程序完成,運行一下。
進入終端,切換到程序所在目錄
$ ls
tcp_server.py
$python3 tcp_server.py
sockfd=socket.socket(AF_INET,SOCK_STREAM)
SyntaxError:invaid character in identifier,在標識符中有無效的字元。
判斷應該是加入了一個中文字元。在全英輸入法下再寫一遍就對了。
$python3 tcp_server.py
NameError:name』AF_INET』 is not defined
解決辦法,將import socket改為from socket import ×
這樣在AF_INET就不用寫成socket.AF_INET才能使用。
$python3 tcp_server.py
Waiting for connect…..
運行到接收連接之前,阻塞在這裡等待一個客戶端連接進來。
那這個程序的服務端地址是本機,我也只有本機。讓本機也成為客戶端。
在linux命令下有telnet命令可以使用
新建一個終端,進入程序目錄
$ telnet 127.0.0.1 8888 #這個是伺服器端地址.
就可以實現客戶端和伺服器端的連接了
看一下之前的終端埠,運行到
connect from(「127.0.0.1」,53644) #這裡的53644是由系統自動分配給客戶端的埠。
誰明目前accept已經衝破阻塞,但是沒有繼續運行,說明又阻塞了。是阻塞在recv這裡了。因為現在網路緩衝區里沒有東西,是空的,所以會阻塞。那我們在客戶端這個終端埠那發送些信息。
在客戶端輸入:
hello zhangjihong按下回車Recv your messageConnection closed by foreign host.#連接已經被伺服器端關閉
觀察兩個終端:
伺服器端:
接收到:hello zhangjihong
發送了17個位元組數
伺服器端接收到了發送的信息並列印,回發信息給客戶端,客戶端收到了回發的信息。
伺服器端列印共回發了多少個位元組數。按enter鍵關閉連接。
另外,客戶端這邊Recv your messageConnection closed by foreign host希望可以換行列印輸出。
可以在send(「Recv your message
」)的內容後加入換行字元,就解決了。
通過上面例子可以看到,除了accept()是阻塞函數,recv()也是阻塞函數,當網路緩衝中沒有內容時會阻塞。
如果將HOST=「127.0.0.1」改為網路上的IP地址,「192.168.0.109」,那麼在同一區域網內的其他主機也可以進行通信,互發信息。
操作步驟,其他主機在終端上輸入,$ telnet 192.168.0.109 8888
就可以訪問我的主機了
一個應用程序必須要有一個對應的埠號。
現在有一個服務端程序,用telnet雖然也可以通信,但是收發不太自由
我們也可以自己寫一個客戶端
客戶端創建的套接字必須是相同類型
bind綁定是可選的,客戶端如果要是綁定了IP和埠,那就始終使用固定的地址,如果不綁定的話,IP就是你的IP,但埠,系統會隨機分配(結合之前的案例),所以客戶端訪問伺服器,伺服器有固定的埠,客戶端是訪問別人的,埠變也沒事,通常不綁定。
客戶端連接伺服器端使用什麼套接字,是不是只用socket生成的套接字就可以了。
一個客戶端只能連接一個伺服器,不用創建新的套接字。
connect()函數是用來向伺服器發起請求的,其他的send/recv,close和伺服器端基本一樣。
下面來寫一下客戶端代碼:
新建文件tcp_client.py
from socket import *
#創建客戶端套接字,要和訪問的伺服器的套接字類型相同,所以這邊也要是流式的
connfd=socket(AF_INET,SOCK_STREAM),這個套接字對象名字隨便取,fd是文件描述符的意思。
連接伺服器,用創建的套接字的屬性函數connfd.connect(),我們先來說一下connect()這個函數的用法
connect(address)
功能:向伺服器發起連接請求
參數:是一個元組,即為要連接的伺服器的地址
沒有返回值,類似終端命令telnet。
伺服器的IP和埠可以事先寫好,再把ADDR傳進去
#要連接的伺服器的地址信息
HOST=「192.168.0.109」
PORT=8888
ADDR=(HOST,PORT)
connfd.connect(ADDR)
這個發起請求函數和伺服器端的accept()請求相呼應,accept()等待接收請求。
這兩個函數的呼應共同完成了三次握手的過程(具體細節不做描述)
連接完成後就可以進行發送信息了
通信還是用connfd這個套接字
客戶端一般是先send,伺服器端是先recv
connfd.send(b「hello server」)#網路交互都需要send一個bytes類型
data=connfd.recv(1024) #或者設置一個buffersize變數也可以,也可以直接寫數字,緩衝區一次能拿多少位元組。
列印一下收到的信息
print(「客戶端收到:」,data)
關閉套接字,和伺服器斷開連接。
connfd.close()
注意點:1.客戶端要和伺服器端的套接字類型相同
2.客戶端就使用創建的套接字和伺服器交互
3.recv和send要與伺服器配合,避免recv死阻塞,雙方都recv,一直阻塞下去。
4.如果是虛擬機,需要在虛擬機上打開ifconfig查看IP地址。
在寫完客戶端之後就可以運行了,打開終端,進如程序所在目錄,先運行伺服器程序
$python3 tcp_server.py
wait for connect…
再運行客戶端
$python3 tcp_client.py
可以接收。
下面附上伺服器端和客戶端的代碼截圖,大家可以試一下。
可以看到兩端都有關閉套接字的代碼,誰會先斷開呢,都有可能,也就是誰先執行斷開操作不一定。
那麼,如果一邊突然斷開,會對另一邊產生什麼影響呢?下面,我們通過一個sleep的延遲來演示一下斷開程序。
在客戶端程序里,import time,加入time.sleep(),在哪裡加這一句呢?在建立連接之後把send和recv注釋掉。伺服器端不變。運行一下,先在終端運行伺服器,
$python3 tcp_server.py
waiting for connect….
此時阻塞在accept這裡。
運行客戶端
$python3 tcp_client.py
然後連接上了,在伺服器端埠可以看到
$python3 tcp_server.pywaiting for connect….connect from (「192.168.0.115」,41656)
2秒後,都斷開,同時伺服器端列印輸出
$python3 tcp_server.pywaiting for connect….connect from (「192.168.0.115」,41656)接收到:發送了 17位元組的數據
客戶端執行到第7行時建立連接,伺服器端的第10行accept就會執行,並列印連接信息。但是因為客戶端沒有send,緩衝區內沒有內容,所以伺服器端在recv這一步就阻塞了。而客戶端睡了2秒後就斷開了和伺服器的連接,同時伺服器端也不再阻塞,直接列印了接收到信息,返回一個空字元串,該發送還發送。只是,發送過後,客戶端這邊沒去接收了。
我們之前說過,recv()是阻塞函數,當接收的網路緩衝區中沒有內容時會阻塞,通過上面這個例子,說明recv的這個阻塞是有條件的,即雙方還在連接上。當連接斷開,會自動衝破阻塞,返回一個空字元串。
同樣的,伺服器端先斷開,在客戶端的recv也會衝破阻塞。
補充知識點:send()和sendall(),
功能上都是:發送網路消息。
參數也一樣:要發送的內容,要求為bytes格式
區別:sendall的返回值,如果成功發送返回None,發送失敗報異常。
而send()的返回值是返回實際發送的位元組數。
sendall()不管數據多大,都嘗試將數據一次發送完。send()如果發送的數據特別大,那麼實際發送的數可能會比要發送的小,那麼會再次的提起發送。
以上只是小測試,程序發送接收一次就結束,實際情況下,伺服器端需要持續的運行來不斷的接收客戶端的請求。
在伺服器端加入循環語句。要求這個循環語句可以實現,當一個客戶端進來後收發消息可以進行多次。將收發部分進行while循環(次數不確定),並且如果一個退出後我還能接收下一個客戶端的連接(if邏輯判斷)。
第一個訴求的實現步驟:
將伺服器收發部分進行while循環,同樣客戶端也應該改成多次收發。那麼這種循環,發送接收太快,而且每次發送內容都一樣,如果想讓慢一點怎麼辦,可以讓客戶端這邊sleep一下。想要改變內容也可以在循環中加入input(),從終端輸入什麼就發送什麼。這樣每次循環就自動阻塞在input()這裡,等待輸入。
data=input(「發送信息」)
while循環里得有跳出循環的語句。那麼繼續在客戶端里循環語句里加入break語句,
if not data:
break;
如果終端輸入為空,就跳出循環,執行,循環外的close()語句,當客戶端執行了關閉連接時,此時伺服器的循環里的recv就衝破阻塞,繼續執行死循環。
因此,在伺服器端的循環里也寫入:
if not data:
break;
因為客戶端為空時,斷開連接,伺服器端這邊recv返回空字元串,data為空。
運行一下,可以實現隨發隨收,多次收發,當摁一下回車,客戶端和伺服器端都自動退出循環,並且關閉連接。
那要實現一個客戶端結束連接,伺服器還能接收另一個連接要怎麼做?
步驟:
在接收請求部分再寫一個循環,把後面的連接,收發,關閉連接的套接字語句都包含進來。但是
不要把sockfd.close()給包含進來,因為break後conn這個套接字已經沒用了,可以關閉和當前的套接字的連接了。繼續執行等待連接語句,等待下一個連接。
運行試一下,會發現,建立一個連接後,當客戶端按enter鍵後,客戶端退出連接,服務端執行等待連接,在客戶端再次運行,又能重新連上。那麼怎麼結束伺服器端的循環呢?直接按ctrl+c結束,因為伺服器端通常不需要頻繁退出。
以上循環代碼如下圖:
那麼,想想這個程序還有什麼問題呢?
現在的操作是一個客戶端必須退出後,另一個才能連接上。
運行伺服器端,再運行一個客戶端,在連接中時,再另起一個終端,運行客戶端,但是新起的這第二個客戶端發送的信息,在伺服器端那邊是接收不到的。因為伺服器端循環里的conn是上一個客戶端的套接字。第二個客戶端在排隊等待。它的send()這會兒也在阻塞,當上一個退出後,第二個和伺服器端自動就連接上了。之前發送的信息伺服器也收到了。
也就是說目前循環這種方式的缺陷是伺服器只能同時處理一個客戶端的請求。當有多個客戶端發來請求時是無法同時處理的。
TCP循環服務有缺陷,怎麼去解決呢?
用多進程多線程嗎,具體解決方案會在下一篇中進行。
推薦閱讀:
※關於技術奇點的一些探討
※共享充電寶盈利了沒?又發現一個新貢共享充電樁了,手機共享充電樁,你們看好么?
※對時下正火的「全面屏」概念,普通消費者的真實看法究竟是怎樣的,它能引領真的換機潮么?
※醫生會被人工智慧取代嗎?