1、客戶端(client)服務端(sever)架構python
在計算機中有不少常見的C/S架構,例如咱們的瀏覽器是客戶端、而百度網站和其餘的網站就是服務端;視頻軟件是客戶端,提供視頻的騰訊、優酷、愛奇藝就是服務端。程序員
C/S與socket的關係:shell
學習socket就是爲了開發C/S架構。編程
2、OSI七層設計模式
C/S架構的軟件(軟件屬於應用層)是基於網絡進行通訊的,網絡的核心即一堆協議,協議即標準,你想開發一款基於網絡通訊的軟件,就必須遵循這些標準。因此在學習socket以前,先了解一下OSI七層瞭解基本的網絡協議,方便學習socket。瀏覽器
在上面的OSI中好像並無與socket有關的信息,那麼請看下面這圖:緩存
3、socket是什麼服務器
從上面這個圖中能夠看出,socket就是網絡層和運輸層的抽象結合。Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。因此,咱們無需深刻理解tcp/udp協議,socket已經爲咱們封裝好了,咱們只須要遵循socket的規定去編程,寫出的程序天然就是遵循tcp/udp標準的。網絡
固然還有另外一種解釋:網絡上的兩個程序經過一個雙向的通訊鏈接實現數據的交換,這個鏈接的一端稱爲一個socket。創建網絡通訊鏈接至少要一對端口號(socket)。socket本質是編程接口(API),對TCP/IP的封裝,TCP/IP也要提供可供程序員作網絡開發所用的接口,這就是Socket編程接口;HTTP是轎車,提供了封裝或者顯示數據的具體形式;Socket是發動機,提供了網絡通訊的能力。Socket的英文原義是"孔"或"插座"。架構
做爲BSD UNIX的進場通訊機制,取後一種意思。一般也稱做"套接字",用於描述IP地址和端口,是一個通訊鏈的句柄,能夠用來實現不一樣虛擬機或不一樣計算機之間的通訊。在Internet上的主機通常運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,並綁定到一個端口上,不一樣的端口對應於不一樣的服務。Socket正如其英文原意那樣,像一個多孔插座。一臺主機猶如佈滿各類插座的房間,每一個插座有一個編號,有的插座提供220伏交流電, 有的提供110伏交流電,有的則提供有線電視節目。 客戶軟件將插頭插到不一樣編號的插座,就能夠獲得不一樣的服務。
套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 所以,有時人們也把套接字稱爲「伯克利套接字」或「BSD 套接字」。一開始,套接字被設計用在同 一臺主機上多個應用程序之間的通信。這也被稱進程間通信,或 IPC。套接字有兩種(或者稱爲有兩個種族),分別是基於文件型的和基於網絡型的。
基於文件類型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,能夠經過訪問同一個文件系統間接完成通訊
基於網絡類型的套接字家族套接字家族的名字:AF_INET
(還有AF_INET6被用於ipv6,還有一些其餘的地址家族,不過,他們要麼是隻用於某個平臺,要麼就是已經被廢棄,或者是不多被使用,或者是根本沒有實現,全部地址家族中,AF_INET是使用最普遍的一個,python支持不少種地址家族,可是因爲咱們只關心網絡編程,因此大部分時候我麼只使用AF_INET)
5、套接字工做流程
根據鏈接啓動的方式以及本地套接字要鏈接的目標,套接字之間的鏈接過程能夠分爲三個步驟:服務器監聽,客戶端請求,鏈接確認。
(1)服務器監聽:是服務器端套接字並不定位具體的客戶端套接字,而是處於等待鏈接的狀態,實時監控網絡狀態。
(2)客戶端請求:是指由客戶端的套接字提出鏈接請求,要鏈接的目標是服務器端的套接字。爲此,客戶端的套接字必須首先描述它要鏈接的服務器的套接字,指出服務器端套接字的地址和端口號,而後就向服務器端套接字提出鏈接請求。
(3)鏈接確認:是指當服務器端套接字監聽到或者說接收到客戶端套接字的鏈接請求,它就響應客戶端套接字的請求,創建一個新的線程,把服務端套接字的描述發給客戶端,一旦客戶端確認了此描述,鏈接就創建好了。而服務器端套接字繼續處於監聽狀態,繼續接收其餘客戶端套接字的鏈接請求。
生活中的打電話就是一個簡單的套接字工做流程:
5、常見的套接字函數:
服務端套接字函數
s.bind() 綁定(主機,端口號)到套接字
s.listen() 開始TCP監聽
s.accept() 被動接受TCP客戶的鏈接,(阻塞式)等待鏈接的到來
客戶端套接字函數
s.connect() 主動初始化TCP服務器鏈接
s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
公共用途的套接字函數
s.recv() 接收TCP數據
s.send() 發送TCP數據(send在待發送數據量大於己端緩存區剩餘空間時,數據丟失,不會發完)
s.sendall() 發送完整的TCP數據(本質就是循環調用send,sendall在待發送數據量大於己端緩存區剩餘空間時,數據不丟失,循環調用send直到發完)
s.recvfrom() 接收UDP數據
s.sendto() 發送UDP數據
s.getpeername() 鏈接到當前套接字的遠端的地址
s.getsockname() 當前套接字的地址
s.getsockopt() 返回指定套接字的參數
s.setsockopt() 設置指定套接字的參數
s.close() 關閉套接字
面向鎖的套接字方法
s.setblocking() 設置套接字的阻塞與非阻塞模式
s.settimeout() 設置阻塞套接字操做的超時時間
s.gettimeout() 獲得阻塞套接字操做的超時時間
面向文件的套接字的函數
s.fileno() 套接字的文件描述符
s.makefile() 建立一個與該套接字相關的文件
6、基於TCP的套接字
在實現TCP的套接字以前,小編帶你們瞭解一下基於TCP的三次握手,四次揮手。
TCP是面向鏈接的,不管哪一方向另外一方發送數據以前,都必須先在雙方之間創建一條鏈接。在TCP/IP協議中,TCP 協議提供可靠的鏈接服務,鏈接是經過三次握手進行初始化的。三次握手的目的是同步鏈接雙方的序列號和確認號 並交換 TCP窗口大小信息。
1.第一次握手:創建鏈接。
客戶端發送鏈接請求報文段,將SYN位置爲1,Sequence Number爲x;而後,客戶端進入SYN_SEND狀態,等待服務器的確認;
2.第二次握手:服務器收到SYN報文段。
服務器收到客戶端的SYN報文段,須要對這個SYN報文段進行確認,設置Acknowledgment Number爲x+1(Sequence Number+1);同時,本身本身還要發送SYN請求信息,將SYN位置爲1,Sequence Number爲y;服務器端將上述全部信息放到一個報文段(即SYN+ACK報文段)中,一併發送給客戶端,此時服務器進入SYN_RECV狀態;
3.第三次握手:客戶端收到服務器的SYN+ACK報文段。
而後將Acknowledgment Number設置爲y+1,向服務器發送ACK報文段,這個報文段發送完畢之後,客戶端和服務器端都進入ESTABLISHED狀態,完成TCP三次握手。
完成了三次握手,客戶端和服務器端就能夠開始傳送數據。以上就是TCP三次握手的整體介紹。
那四次揮手呢?
當客戶端和服務器經過三次握手創建了TCP鏈接之後,當數據傳送完畢,確定是要斷開TCP鏈接的啊。那對於TCP的斷開鏈接,這裏就有了神祕的「四次揮手」。
1.第一次揮手:主機1(可使客戶端,也能夠是服務器端),設置Sequence Number和Acknowledgment Number,向主機2發送一個FIN報文段;此時,主機1進入FIN_WAIT_1狀態;這表示主機1沒有數據要發送給主機2了;
2.第二次揮手:主機2收到了主機1發送的FIN報文段,向主機1回一個ACK報文段,Acknowledgment Number爲Sequence Number加1;主機1進入FIN_WAIT_2狀態;主機2告訴主機1,我也沒有數據要發送了,能夠進行關閉鏈接了;
3.第三次揮手:主機2向主機1發送FIN報文段,請求關閉鏈接,同時主機2進入CLOSE_WAIT狀態;
4.第四次揮手:主機1收到主機2發送的FIN報文段,向主機2發送ACK報文段,而後主機1進入TIME_WAIT狀態;主機2收到主機1的ACK報文段之後,就關閉鏈接;此時,主機1等待2MSL後依然沒有收到回覆,則證實Server端已正常關閉,那好,主機1也能夠關閉鏈接了。
至此,TCP的四次揮手就這麼愉快的完成了。當你看到這裏,你的腦子裏會有不少的疑問,不少的不懂,感受很凌亂;沒事,咱們繼續總結。
爲何要三次握手?
既然總結了TCP的三次握手,那爲何非要三次呢?怎麼以爲兩次就能夠完成了。那TCP爲何非要進行三次鏈接呢?在謝希仁的《計算機網絡》中是這樣說的:
爲了防止已失效的鏈接請求報文段忽然又傳送到了服務端,於是產生錯誤。
在書中同時舉了一個例子,以下:
"已失效的鏈接請求報文段」的產生在這樣一種狀況下:client發出的第一個鏈接請求報文段並無丟失,
而是在某個網絡結點長時間的滯留了,以至延誤到鏈接釋放之後的某個時間纔到達server。原本這是一
個早已失效的報文段。但server收到此失效的鏈接請求報文段後,就誤認爲是client再次發出的一個新
的鏈接請求。因而就向client發出確認報文段,贊成創建鏈接。假設不採用「三次握手」,那麼只要server
發出確認,新的鏈接就創建了。因爲如今client並無發出創建鏈接的請求,所以不會理睬server的確認,
也不會向server發送數據。但server卻覺得新的運輸鏈接已經創建,並一直等待client發來數據。這樣,
server的不少資源就白白浪費掉了。採用「三次握手」的辦法能夠防止上述現象發生。例如剛纔那種狀況,
client不會向server的確認發出確認。server因爲收不到確認,就知道client並無要求創建鏈接。"
這就很明白了,防止了服務器端的一直等待而浪費資源。
爲何要四次揮手?
那四次揮手又是爲什麼呢?TCP協議是一種面向鏈接的、可靠的、基於字節流的運輸層通訊協議。TCP是全雙工 模式,這就意味着,當主機1發出FIN報文段時,只是表示主機1已經沒有數據要發送了,主機1告訴主機2, 它的數據已經所有發送完畢了;可是,這個時候主機1仍是能夠接受來自主機2的數據;當主機2返回ACK報文 段時,表示它已經知道主機1沒有數據發送了,可是主機2仍是能夠發送數據到主機1的;當主機2也發送了FIN 報文段時,這個時候就表示主機2也沒有數據要發送了,就會告訴主機1,我也沒有數據要發送了,以後彼此 就會愉快的中斷此次TCP鏈接。若是要正確的理解四次揮手的原理,就須要瞭解四次揮手過程當中的狀態變化。
FIN_WAIT_1: 這個狀態要好好解釋一下,其實FIN_WAIT_1和FIN_WAIT_2狀態的真正含義都是表示等 待對方的FIN報文。而這兩種狀態的區別是:FIN_WAIT_1狀態其實是當SOCKET在ESTABLISHED狀態時, 它想主動關閉鏈接,向對方發送了FIN報文,此時該SOCKET即進入到FIN_WAIT_1狀態。而當對方迴應ACK報 文後,則進入到FIN_WAIT_2狀態,固然在實際的正常狀況下,不管對方何種狀況下,都應該立刻迴應ACK 報文,因此FIN_WAIT_1狀態通常是比較難見到的,而FIN_WAIT_2狀態還有時經常能夠用netstat看到。 (主動方)
FIN_WAIT_2:上面已經詳細解釋了這種狀態,實際上FIN_WAIT_2狀態下的SOCKET,表示半鏈接,也即 有一方要求close鏈接,但另外還告訴對方,我暫時還有點數據須要傳送給你(ACK信息),稍後再關閉鏈接。 (主動方)
CLOSE_WAIT:這種狀態的含義實際上是表示在等待關閉。怎麼理解呢?當對方close一個SOCKET後發送FIN 報文給本身,你係統毫無疑問地會迴應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態。接下來呢,實 際上你真正須要考慮的事情是察看你是否還有數據發送給對方,若是沒有的話,那麼你也就能夠 close這個 SOCKET,發送FIN報文給對方,也即關閉鏈接。因此你在CLOSE_WAIT狀態下,須要完成的事情是等待你去關 閉鏈接。(被動方)
LAST_ACK: 這個狀態仍是比較容易好理解的,它是被動關閉一方在發送FIN報文後,最後等待對方的ACK報 文。當收到ACK報文後,也便可以進入到CLOSED可用狀態了。(被動方)
TIME_WAIT: 表示收到了對方的FIN報文,併發送出了ACK報文,就等2MSL後便可回到CLOSED可用狀態了。 若是FINWAIT1狀態下,收到了對方同時帶FIN標誌和ACK標誌的報文時,能夠直接進入到TIME_WAIT狀態,而無 須通過FIN_WAIT_2狀態。(主動方)
CLOSED: 表示鏈接中斷。
7、TCP套接字實踐
服務端:
1 from socket import * 2 3 #對數據進行獨立化方便更改 4 IP_PORT = ("127.0.0.1",8080) 5 BACK_LOG = 5 6 BUFFER_SIZE = 1024 7 8 #創建TCP鏈接 9 sever = socket(AF_INET,SOCK_STREAM) #生成套接字對象 10 sever.bind(IP_PORT) #綁定端口 11 sever.listen(BACK_LOG) #開始監聽(BACK_LOG:表明的是容許監聽的數量) 12 conn,addr = sever.accept() 13 14 #鏈接成功後的數據傳輸 15 data = conn.recv(BUFFER_SIZE) 16 de_data = data.decode("utf-8") 17 conn.send(de_data.upper().encode("utf-8")) 18 19 #測試數據發送成功 20 print("send success") 21 22 #關閉鏈接 23 conn.close() 24 sever.close()
客戶端:
1 from socket import * 2 3 IP_PORT = ("127.0.0.1",8080) 4 BUFFER_SIZE = 1024 5 6 #創建鏈接 7 client = socket(AF_INET,SOCK_STREAM) #生成套接字對象 8 client.connect(IP_PORT) #鏈接到對應的端口 9 10 #數據傳輸 11 data = input(">>:") 12 client.send(data.encode("utf-8")) 13 sever_data= client.recv(BUFFER_SIZE) 14 print("success recv:" ,sever_data.decode("utf-8"))
上面的代碼就實現了一個簡單的基於TCP的套接字服務,可是這僅僅進行了一次簡單的通訊,這與電話通訊的你一句我一句有較大的差別。接下來是socket的進階版:
socket_sever:
1 from socket import * 2 3 #對數據進行獨立化方便更改 4 IP_PORT = ("127.0.0.1",8080) 5 BACK_LOG = 5 6 BUFFER_SIZE = 1024 7 8 sever = socket(AF_INET,SOCK_STREAM) 9 sever.bind(IP_PORT) 10 sever.listen(BACK_LOG) 11 conn,addr = sever.accept() 12 13 try: 14 while True: 15 data = conn.recv(BUFFER_SIZE) 16 print("收到數據:",data.decode("utf-8")) 17 deal_data = data.decode("utf-8").upper() 18 conn.send(deal_data.encode("utf-8")) 19 print("發送數據:",deal_data) 20 except ConnectionResetError as e: 21 print(e) 22 23 sever.close() 24
socket_client:
1 from socket import * 2 3 #對數據進行獨立化方便更改 4 IP_PORT = ("127.0.0.1",8080) 5 BUFFER_SIZE = 1024 6 7 client = socket(AF_INET,SOCK_STREAM) 8 client.connect(IP_PORT) 9 10 while True: 11 data = input(">>:") 12 if not data:continue 13 client.send(data.encode("utf-8")) 14 rv_data = client.recv(BUFFER_SIZE) 15 print(rv_data.decode("utf-8")) 16 17 client.close()
上述的C/S實現了一個簡單的能夠無限收發的簡單功能,可是此時的socket_sever僅實現了一對一的鏈接,咱們打點電話的時候還可能有更多的鏈接進來,而上述的功能在第一 鏈接中斷的時候,第二個服務端也會被中斷,因此咱們對上述功能再次進行了加工:
tcp_socket_sever:
1 from socket import * 2 3 #對數據進行獨立化方便更改 4 IP_PORT = ("127.0.0.1",8080) 5 BACK_LOG = 5 6 BUFFER_SIZE = 1024 7 8 sever = socket(AF_INET,SOCK_STREAM) 9 sever.bind(IP_PORT) 10 sever.listen(BACK_LOG) 11 12 while True: #這個循環是爲了能夠接收多個循環,可是每次只能有一個循環進入鏈接 13 conn, addr = sever.accept() 14 while True: #這個循環是爲了可使客戶端和服務端循環無限次 15 try: #這是爲了解決一個客戶端不正常斷開的時候而拋出的異常 16 data = conn.recv(BUFFER_SIZE) 17 print("收到數據:",data.decode("utf-8")) 18 deal_data = data.decode("utf-8").upper() 19 conn.send(deal_data.encode("utf-8")) 20 print("已發送數據:",deal_data) 21 except ConnectionResetError as e: 22 print(e) 23 break
tcp_socket_client:
1 from socket import * 2 3 IP_PORT = ("127.0.0.1",8080) 4 BUFFER_SIZE = 1024 5 6 client = socket(AF_INET,SOCK_STREAM) 7 client.connect(IP_PORT) 8 9 while True: 10 data = input(">>:") 11 if not data:continue 12 client.send(data.encode("utf-8")) 13 print("已發送:",data) 14 re_data = client.recv(BUFFER_SIZE) 15 print("已接收:",re_data.decode("utf-8"))
接下來運用socket寫一個簡單的C/S,做用是能夠遠程的容許DOS命令,而且返回一些信息,可是沒法作更改刪除等,僅僅查詢功能:
1 from socket import * 2 import subprocess 3 import struct 4 IP_PORT = ("localhost",8080) 5 BACK_LOG = 5 6 BUFFER_SIZE = 1024 7 8 #創建鏈接 9 sever = socket(AF_INET,SOCK_STREAM) 10 sever.bind(IP_PORT) 11 sever.listen(BACK_LOG) 12 13 while True: 14 conn,addr = sever.accept() 15 print("我準備接收連接了:") 16 while True: 17 try: 18 cmd = conn.recv(BUFFER_SIZE) 19 print("接收到指令:",cmd.decode("utf-8")) 20 21 #對接收到的數據進行處理 22 if not cmd:break #這裏的判斷解決客戶端正常斷開時退出鏈接 23 res = subprocess.Popen( 24 cmd.decode("utf-8"),shell=True, 25 stderr=subprocess.PIPE, 26 stdin=subprocess.PIPE, 27 stdout=subprocess.PIPE 28 ) 29 ''' 30 subprocess模塊:shell=True指的是容許將輸出在shell的內容輸入到管道 31 stderr,stdin,stdout:這些都是將輸入流,輸出流接入管道 32 ''' 33 err = res.stderr.read() 34 if err: 35 cmd_res = err 36 else: 37 cmd_res = res.stdout.read() 38 39 #發 40 if not cmd_res: 41 cmd_res = "執行成功".encode("gbk") 42 43 conn.send(cmd_res) 44 except Exception as e: 45 print(e) 46 break
1 from socket import * 2 3 IP_PORT = ("localhost",8080) 4 BUFFER_SIZE = 1024 5 6 client = socket(AF_INET,SOCK_STREAM) 7 client.connect(IP_PORT) 8 9 while True: 10 cmd = input(">>:") 11 print("") 12 if not cmd:continue 13 if cmd == "quit":break 14 client.send(cmd.encode("utf-8")) 15 print("已經發送指令:",cmd) 16 data = client.recv(BUFFER_SIZE) 17 print(data.decode("gbk")) 18 19 client.close()
上述的簡易的C/S服務還有一個重要的問題沒有解決,那就是粘包。
8、粘包問題
在前面的C/S服務咱們輸入ipconfig發送到服務端,接收返回到的消息時,會發現接收到的消息不全,再次輸入其餘命令,接收到的消息仍是原來ipconfig輸出的信息,這就的典型的一種粘包現象。
這是爲何呢,爲何我一個收,一個發爲何會出現粘包現象,這就要從底層提及。
根據上圖咱們模擬一下簡單的TCP套接字工做原理:
第一:啓動服務端和客戶端
第二:客戶端的將指令和請求等數據信息讀取到用戶態內存,而後內核態將用戶態的數據拷貝
第三:而後經過網卡等硬件層發送到服務端的內核態內存
第四:服務端的用戶態再從內核態拷貝而後對拷貝過來的數據進行處理
第五:再按照剛纔來的路線進行返回。
從上面的流程結合TCP套接字的實例能夠得出如下結論:
粘包解決方案一:
1 from socket import * 2 import subprocess 3 import struct 4 IP_PORT = ("localhost",8080) 5 BACK_LOG = 5 6 BUFFER_SIZE = 1024 7 8 sever = socket(AF_INET,SOCK_STREAM) 9 sever.bind(IP_PORT) 10 sever.listen(BACK_LOG) 11 12 while True: 13 conn, addr = sever.accept() 14 print("接收到鏈接:", addr) 15 while True: 16 try: 17 print("開始接收") 18 cmd = conn.recv(BUFFER_SIZE) 19 print("接收到指令:",cmd.decode("utf-8")) 20 if not cmd:break 21 res = subprocess.Popen( 22 cmd.decode("utf-8"),shell=True, 23 stdout=subprocess.PIPE, 24 stdin=subprocess.PIPE, 25 stderr=subprocess.PIPE 26 ) 27 28 res_err = res.stderr.read() 29 if res_err: 30 res_cmd=res_err 31 else: 32 res_cmd = res.stdout.read() 33 34 data_len = len(res_cmd) 35 print(data_len) 36 conn.send(str(data_len).encode("gbk")) #發送長度 37 data2 = conn.recv(BUFFER_SIZE).decode("gbk") #爲了不長度與數據的粘包,先接收一個確認數據 38 #開始發送數據 39 if data2 == "recv_ready": 40 conn.sendall(res_cmd) #這段代碼會循環發送,直到數據被髮送完畢 41 print("發送完畢") 42 except Exception as e: 43 print(e) 44 break
1 from socket import * 2 3 IP_PORT = ("localhost",8080) 4 BUFFER_SIZE = 1024 5 6 client = socket(AF_INET,SOCK_STREAM) 7 client.connect(IP_PORT) 8 9 ''' 10 解決粘包的方法一就是服務端在發送數據的時候,先發一個數據長度,而後讓客戶端循環收,直到收完爲止 11 ''' 12 13 while True: 14 cmd = input(">>:") 15 if not cmd:continue 16 if cmd == "quit":break 17 18 client.send(cmd.encode("utf-8")) #發送命令 19 cmd_data_lenth = int(client.recv(BUFFER_SIZE).decode("gbk")) #接收長度 20 print(cmd_data_lenth) 21 client.send("recv_ready".encode("utf-8")) 22 cmd_data = "" 23 data_lenth= 0 24 while data_lenth < cmd_data_lenth: 25 cmd_data += client.recv(BUFFER_SIZE).decode("gbk") 26 print(cmd_data) 27 data_lenth += len(cmd_data) 28 print("數據接收完畢",cmd_data) 29 30 client.close()
該方案程序的運行速度遠快於網絡傳輸速度,因此在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗
粘包解決方案二: