鄙人學習筆記
文章目錄
- 套接字介紹
- 定義
- 套接字分類(針對TCP和UDP的分類)
- TCP套接字程式設計
- 服務端流程
- 代碼實作
- 舉個例子
- 用戶端流程
- 代碼實作
- 舉個例子
- TCP套接字資料傳輸特點
- 做個練習
- 網絡收發緩沖區
- 舉個例子
- TCP粘包
套接字介紹
定義
套接字是實作網絡程式設計進行資料傳輸的一種技術手段
套接字分類(針對TCP和UDP的分類)
①流式套接字(SOCK_STREAM): 以位元組流方式(就像是管道中的水流一樣)傳輸資料,實作TCP網絡傳輸方案。
②資料報套接字(SOCK_DGRAM):以資料報形式(就像是用瓶子打包好的水一樣)傳輸資料,實作UDP網絡傳輸方案。
TCP套接字程式設計
服務端流程
先來看一個流程圖:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5CN5IjN1gjY5cTNyUWZ1gTOyYzXwMTN1QTMyAzLcZDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
代碼實作
- 建立套接字
sockfd=socket.socket(socket_family=AF_INET,socket_type=SOCK_STREAM,proto=0)
功能:建立套接字對象
參數:
socket_family 網絡位址類型:AF_INET表示ipv4
socket_type 套接字類型:SOCK_STREAM 流式;SOCK_DGRAM 資料報
proto(在應用層網絡程式設計中用不到) 選擇子協定:通常為0
傳回值: 套接字對象
備注:底層/系統層套接字也針對某種協定,這些協定,有的時候會産生一些分支,也就是子協定。那麼為啥在應用層網絡程式設計中用不到proto這個參數呢?因為TCP協定和UDP協定沒有子協定,是以參數設為0,即不選擇任何子協定。
- 綁定位址
sockfd.bind(addr)
功能: 綁定本機網絡位址(如果不寫本機網絡位址,則會報錯)
參數: 二進制元組 (ip,port) 比如('0.0.0.0',8888)
備注:addr為一個元組,這個元組有兩個元素一個是網絡位址(IP),一個是端口号(port)。
- 設定監聽
sockfd.listen(n)
功能 : 将套接字設定為監聽套接字,确定監聽隊列大小
參數 : 監聽隊列大小
備注1:并不是所有的套接字都具備監聽的功能,即被用戶端連接配接的功能。通過調用listen,我們的套接字對象才能被用戶端連接配接。
備注2:一個服務端套接字對象可以同時連接配接多個用戶端,與用戶端進行連接配接需要進行3次握手,且連接配接的過程需要一個一個來進行。比如說,同時有5個用戶端發起了連接配接請求,這時我們就會形成一個”先來後到”的隊列,對這5個用戶端的連接配接請求,一個一個進行處理。比如說,我們設定監聽隊列大小為5,表示隊列最多容納5個用戶端等待處理。但有10個用戶端同時發起了連接配接請求,這時,服務端會先處理第1個發起請求的那個用戶端,前2~6個用戶端則在等待隊列中,最晚發起請求的4個用戶端,則會被拒絕。
- 等待處理用戶端連接配接請求
connfd,addr = sockfd.accept()
功能: 阻塞等待處理用戶端請求(等待用戶端連接配接,啥時候有用戶端連接配接,才結束阻塞)
傳回值:
connfd : 用戶端連接配接套接字.
用戶端連接配接後,新産生的專門與用戶端進行通信的套接字對象。
則每當有一個用戶端連接配接時,都會有一個新的專用于連接配接的套接字對象産生。
就像對每個用戶端提供一個專屬管家一樣,提升使用者端體驗。
我們如果找到一個連接配接套接字,就可以知道它對應的用戶端是誰
addr :連接配接的用戶端位址
阻塞函數:當程式運作到這個函數時,則程式暫停執行。程式在等待某種條件,當條件滿足後才繼續執行,如:input()、sleep()。阻塞函數在IO操作中很常見。
- 消息收發
data = connfd.recv(buffersize)
功能 : 接受用戶端消息
參數 :每次最多接收消息的大小(最多一次接受多少位元組的消息)
傳回值: 接收到的内容
n = connfd.send(data)
功能 : 發送消息
參數 :要發送的内容-bytes格式(位元組串格式)
傳回值: 發送的位元組數
備注1:所有和網絡相關的消息傳送都得是bytes格式(位元組串格式).發送和接收都得是位元組串。
備注2:我們發送/接收的是位元組串,但平時寫/讀的是字元串,是以我們需要用encode()/dencode()進行轉換。
- 關閉套接字
sockfd.close()
功能:關閉套接字
舉個例子
服務端代碼:
import socket
#建立流式套接字
sockfd = socket.socket(socket.AF_INET, \
socket.SOCK_STREAM)
#綁定位址
sockfd.bind(('127.0.0.1', 8888))
#設定監聽
sockfd.listen(5)
#等待處理用戶端連結
print("Waiting for connect....")
connfd, addr = sockfd.accept()
#收發消息
data = connfd.recv(1024)
print("接收到的消息:", data.decode())
n = connfd.send(b'Receive your message')
print("發送了 %d 個位元組資料" % n)
#關閉套接字
connfd.close()
sockfd.close()
我們運作一下,得到以下結果:
結果說明,程式阻塞在accept,等待用戶端連接配接。是以這時,我們就要找一個用戶端與之連接配接,下一節,我們就學一下用戶端流程。
用戶端流程
先看看流程圖:
代碼實作
-
建立套接字(和用戶端代碼相同)
注意:隻有相同類型的套接字才能進行通信
- 請求連接配接
sockfd.connect(server_addr)
功能:連接配接伺服器
參數:元組 伺服器位址
-
收發消息(和用戶端代碼相同)
注意: 為了防止兩端都阻塞,故recv和send要配合使用。比如,服務端是先send後recv,那麼用戶端則需要先recv後send。否則,若兩端同時recv,則兩端都會阻塞。
- 關閉套接字(和用戶端代碼相同)
舉個例子
用戶端代碼:
服務端代碼:
我們想要運作,但是若先運作用戶端,則會報錯。是以我們要先啟動服務端
①運作服務端
檢視服務端的控制台Console 2/A輸出的結果:
②運作用戶端
我們先看一下服務端的控制台Console 2/A輸出的結果:
我們再看一下用戶端的控制台Console 3/A輸出的結果:
消息收發成功了,很好~
TCP套接字資料傳輸特點
①TCP連接配接中,當一端退出,另一端如果阻塞在recv,此時recv會立即傳回一個空字串。
②TCP連接配接中,如果一端已經不存在,仍然試圖通過send發送資訊,則會産生BrokenPipeError
③一個監聽套接字可以同時連接配接多個用戶端,也能夠重複被連接配接。
做個練習
要求1:一個用戶端退出了,伺服器不會退出,而是連接配接下一個用戶端
要求2:用戶端可以不停的循環發送消息
服務端代碼:
#-*- coding: utf-8 -*-
import socket
#建立流式套接字
sockfd = socket.socket(socket.AF_INET, \
socket.SOCK_STREAM)
#綁定位址
sockfd.bind(('127.0.0.1', 8888))
#設定監聽
sockfd.listen(5)
#等待處理用戶端連結
while True:
print("Waiting for connect....")
try:
connfd, addr = sockfd.accept()
print("Connect from:", addr)
except KeyboardInterrupt:
print("退出服務")
break
# 收發消息
while True:
data = connfd.recv(1024)
# 得到空則退出循環
if not data:
break
print("接收到的消息:", data.decode())
n = connfd.send(b'Receive your message')
print("發送了 %d 個位元組資料" % n)
connfd.close()
#關閉套接字
sockfd.close()
用戶端代碼:
#-*- coding: utf-8 -*-
from socket import *
#建立tcp套接字
sockfd = socket()
#發起連接配接
server_addr = ('127.0.0.1',8888)
sockfd.connect(server_addr)
#收發消息
while True:
data = input("消息:")
if not data:
break
sockfd.send(data.encode())
data = sockfd.recv(1024)
print("From server:",data.decode())
#關閉
sockfd.close()
先運作服務端,再運作用戶端并發送消息。
用戶端結果:
服務端結果:
網絡收發緩沖區
①網絡緩沖區有效的協調了消息的收發速度、緩解收發壓力。
②send和recv實際是向緩沖區發送接收消息,當緩沖區不為空recv就不會阻塞。
網絡緩沖區完成消息傳遞示意圖:
舉個例子
還記得我們上面那個練習麼?其中一段代碼是:
它表示,每次最多可接收消息大小為1024個位元組.
我們來更改一下最大可接受的消息位元組數為5:
用用戶端發送消息:
服務端結果:
用戶端結果:
則說明,用戶端1次發送,服務端分3次接收了,同時傳回給服務端3句’ Receive your message’也就是說,服務端的内層循環(如下圖所示)循環了3次
備注1:是以我們一開始說【函數recv()是阻塞函數,當發送方不發消息,就會阻塞】。但其實更準确的來說,并不是發送方不發消息就會阻塞,接收方其實是先從緩沖區去拿消息,當緩沖區為空時,會阻塞,當緩沖區一直有消息時,則會一直擷取消息,一直不阻塞。
注意! 運作上面的代碼時,用戶端有時候也會出現以下這種情況:
TCP粘包
-
原因
TCP以位元組流方式傳輸,沒有消息邊界。多次發送的消息被一次接收,此時就會形成粘包。
-
影響(分情況,比如:傳一部電影/發送使用者名消息)
①如果發送的内容中,每個資訊都有獨立的含義(比如:發送幾個使用者姓名),需要接收端獨立解析,此時,這時粘包會有影響。
②如果發送的是一個位元組流檔案,是一個連在一起的整體(比如:發送一部電影),接收端無需單獨解析,而是将所有接收到内容最終合成一個整體,這時粘包沒啥影響。
-
處理方法
①人為的添加消息邊界,比如:每次發送一個姓名時,在姓名後加一個特殊符号。
②控制發送速度(因為粘包的産生是因為收發速度不協調),比如用sleep()函數調節。