TCP/UDP傳輸的其實是通過套接字來進行傳輸的,那麼我就首先講一下什麼是套接字吧~
什麼是套接字?
Socket(套接字)通信原理 - codedot - 部落格園 (cnblogs.com)

https://www.cnblogs.com/myitnews/p/13790067.html
Socket 就像一個電話插座,負責連通兩端的電話,進行點對點通信,讓電話可以進行通信,端口就像插座上的孔,端口不能同時被其他程序占用。而我們建立連接配接就像把插頭插在這個插座上,建立一個 Socket 執行個體開始監聽後,這個電話插座就時刻監聽着消息的傳入,誰撥通我這個“IP 位址和端口”,我就接通誰。
套接字是網絡上運作的兩個程式之間的雙向通信鍊路的一個端點。套接字機制通過建立發生通信的命名接觸點來提供程序間通信 (IPC) 的方法。就像“管道”用于建立管道,套接字是使用“套接字”系統調用建立的。插座通過網絡提供雙向FIFO通信設施。在通信的每一端建立一個連接配接到網絡的套接字。每個套接字都有一個特定的位址。此位址由 IP 位址和端口号組成。套接字通常用于用戶端伺服器應用程式。伺服器建立一個套接字,将其附加到網絡端口位址,然後等待用戶端與其聯系。用戶端建立一個套接字,然後嘗試連接配接到伺服器套接字。建立連接配接後,将進行資料傳輸。
實際上,Socket 是在應用層和傳輸層之間的一個抽象層,它把 TCP/IP 層複雜的操作抽象為幾個簡單的接口,供應用層調用實作程序在網絡中的通信。Socket 起源于 UNIX,在 UNIX 一切皆檔案的思想下,程序間通信就被冠名為
檔案描述符(file descriptor)
,Socket 是一種“打開—讀/寫—關閉”模式的實作,伺服器和用戶端各自維護一個“檔案”,在建立連接配接打開後,可以向檔案寫入内容供對方讀取或者讀取對方内容,通訊結束時關閉檔案。
Socket類型
世界上有很多種套接字(socket),比如 DARPA Internet 位址(Internet 套接字)、本地節點的路徑名(Unix套接字)、CCITT X.25位址(X.25 套接字)等。我們隻介紹第一種套接字——Internet 套接字,它是最具代表性的,也是最經典最常用的。以後我們提及套接字,指的都是 Internet 套接字。根據資料的傳輸方式,可以将 Internet 套接字分成兩種類型。
流格式套接字(SOCK_STREAM)
流格式套接字(Stream Sockets)也叫“面向連接配接的套接字”,是一種可靠的、雙向的通信資料流,資料可以準确無誤地到達另一台計算機,如果損壞或丢失,可以重新發送。在計算機作業系統中,流套接字是程序間通信套接字或網絡套接字的類型,它提供面向連接配接的、有序的和唯一的資料流,沒有記錄邊界,具有明确定義的機制來建立和銷毀連接配接以及檢測錯誤。它類似于電話。在電話之間建立連接配接(兩端)并進行對話(資料傳輸)。
其特點:
- 資料在傳輸過程中不會消失;
- 資料是按照順序傳輸的;
- 資料的發送和接收不是同步的(有的教程也稱“不存在資料邊界”)。
可以将 SOCK_STREAM 比喻成一條傳送帶,隻要傳送帶本身沒有問題(不會斷網),就能保證資料不丢失;同時,較晚傳送的資料不會先到達,較早傳送的資料不會晚到達,這就保證了資料是按照順序傳遞的。
------------------------------------
Function Call Description
------------------------------------
Create() To create a socket
Bind() It’s a socket identification like a telephone number to contact
Listen() Ready to receive a connection
Connect() Ready to act as a sender
Accept() Confirmation, it is like accepting to receive a call from a sender
Write() To send data
Read() To receive data
Close() To close a connection
-------------------------------------
為什麼流格式套接字可以達到高品質的資料傳輸呢?這是因為它使用了 TCP 協定(The Transmission Control Protocol,傳輸控制協定),TCP 協定會控制你的資料按照順序到達并且沒有錯誤。
你也許見過 TCP,是因為你經常聽說“TCP/IP”。TCP 用來確定資料的正确性,IP(Internet Protocol,網絡協定)用來控制資料如何從源頭到達目的地,也就是常說的“路由”。
TCP協定是端到端的傳輸控制協定,之是以是“端到端”的協定,是因為”路由“是由IP協定負責的,TCP協定負責為兩個通信端點提供可靠性保證,這個可靠性不是指一個端點發送的資料,另一個端點肯定能收到(這顯然是不可能的),而是指,資料的可靠投遞或者故障的可靠通知。
TCP的可靠性通過以下方式來保證:
1.逾時重傳:TCP每發送出一個封包段後,都會啟動一個定時器,對目的端傳回的确認資訊進行确認計時,逾時後便重傳。
2.确認信号:當TCP收到一個來自TCP的封包段後,便會發送回一個确認信号。
3.檢驗和:TCP将始終保持首部和資料的檢驗和,如果收到的封包段的檢驗和有差錯,便将其丢棄,希望發送端逾時重傳。
4.重新排序:由于IP資料報的達到可能失序,是以TCP将會資料進行重新排序,以正确的順序交給應用層。
5.丢棄重複:由于IP資料報有可能重複,是以TCP将會丢棄重複的資料。
6.流量控制:TCP連接配接的兩端都有固定大小的緩沖區空間,TCP接受端隻允許對端發送本端緩沖區能容納的資料。TCP提供流量控制。在雙方進行互動時,會彼此通知自己目前接收緩沖區最多可以接收的資料量(通告視窗),以此確定發送方發送的資料不會溢出接收緩沖區。
那麼,“資料的發送和接收不同步”該如何了解呢?
假設傳送帶傳送的是水果,接收者需要湊齊 100 個後才能裝袋,但是傳送帶可能把這 100 個水果分批傳送,比如第一批傳送 20 個,第二批傳送 50 個,第三批傳送 30 個。接收者不需要和傳送帶保持同步,隻要根據自己的節奏來裝袋即可,不用管傳送帶傳送了幾批,也不用每到一批就裝袋一次,可以等到湊夠了 100 個水果再裝袋。
流格式套接字的内部有一個緩沖區(也就是字元數組),通過 socket 傳輸的資料将儲存到這個緩沖區。接收端在收到資料後并不一定立即讀取,隻要資料不超過緩沖區的容量,接收端有可能在緩沖區被填滿以後一次性地讀取,也可能分成好幾次讀取。
也就是說,不管資料分幾次傳送過來,接收端隻需要根據自己的要求讀取,不用非得在資料到達時立即讀取。傳送端有自己的節奏,接收端也有自己的節奏,它們是不一緻的。
流格式套接字有什麼實際的應用場景嗎?浏覽器所使用的 http 協定就基于面向連接配接的套接字,因為必須要確定資料準确無誤,否則加載的 HTML 将無法解析。
資料報格式套接字(SOCK_DGRAM)
資料報格式套接字(Datagram Sockets)也叫“無連接配接的套接字”。計算機隻管傳輸資料,不作資料校驗,如果資料在傳輸中損壞,或者沒有到達另一台計算機,是沒有辦法補救的。也就是說,資料錯了就錯了,無法重傳。
因為資料報套接字所做的校驗工作少,是以在傳輸效率方面比流格式套接字要高。
有以下特征:
- 強調快速傳輸而非傳輸順序;
- 傳輸的資料可能丢失也可能損毀;
- 限制每次傳輸的資料大小;
- 資料的發送和接收是同步的
衆所周知,速度是快遞行業的生命。用機車發往同一地點的兩件包裹無需保證順序,隻要以最快的速度交給客戶就行。這種方式存在損壞或丢失的風險,而且包裹大小有一定限制。是以,想要傳遞大量包裹,就得配置設定發送。
另外,用兩輛機車分别發送兩件包裹,那麼接收者也需要分兩次接收,是以“資料的發送和接收是同步的”;換句話說,接收次數應該和發送次數相同。
總之,資料報套接字是一種不可靠的、不按順序傳遞的、以追求速度為目的的套接字。
資料報套接字也使用 IP 協定作路由,但是它不使用 TCP 協定,而是使用 UDP 協定(User Datagram Protocol,使用者資料報協定)。
QQ 視訊聊天和語音聊天就使用 SOCK_DGRAM 來傳輸資料,因為首先要保證通信的效率,盡量減小延遲,而資料的正确性是次要的,即使丢失很小的一部分資料,視訊和音頻也可以正常解析,最多出現噪點或雜音,不會對通信品質有實質的影響。
注意:SOCK_DGRAM 沒有想象中的糟糕,不會頻繁的丢失資料,資料錯誤隻是小機率事件。
Socket通信過程
Socket 保證了不同計算機之間的通信,也就是網絡通信。對于網站,通信模型是伺服器與用戶端之間的通信。兩端都建立了一個 Socket 對象,然後通過 Socket 對象對資料進行傳輸。通常伺服器處于一個無限循環,等待用戶端的連接配接。
下面是面向連接配接的 TCP 時序圖:
Client
用戶端的過程比較簡單,建立 Socket,連接配接伺服器,将 Socket 與遠端主機連接配接(注意:隻有 TCP 才有“連接配接”的概念,一些 Socket 比如 UDP、ICMP 和 ARP 沒有“連接配接”的概念),發送資料,讀取響應資料,直到資料交換完畢,關閉連接配接,結束 TCP 對話。
import socket
import sys
if __name__ == '__main__':
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 建立 Socket 連接配接
sock.connect(('127.0.0.1', 8001)) # 連接配接伺服器
while True:
data = input('Please input data:')
if not data:
break
try:
sock.sendall(data)#這裡也可用 send() 方法:不同在于 sendall() 在傳回前會嘗試發送所有資料,并且成功時傳回 None,而 send() 則傳回發送的位元組數量,失敗時都抛出異常。
except socket.error as e:
print('Send Failed...', e)
sys.exit(0)
print('Send Successfully')
res = sock.recv(4096) # 擷取伺服器傳回的資料,還可以用 recvfrom()、recv_into() 等
print(res)
sock.close()
Tips:
套接字send與sendall的差別
send()
使用
send()
進行發送的時候,
Python
将内容傳遞給系統底層的
send
接口,也就是說,
Python
并不知道這次調用是否會全部發送完成,比如
MTU
是1500,但是此次發送的内容是2000,那麼除了標頭等等其他資訊占用,發送的量可能在1000左右,還有1000未發送完畢
但是,
send()
不會繼續發送剩下的包,因為它隻會發送一次,發送成功之後會傳回此次發送的位元組數,如上例,會傳回數字1000給使用者,然後就結束了
如果需要将剩下的1000發送完畢,需要使用者自行擷取傳回結果,然後将内容剩下的部分繼續調用
send()
進行發送
sendall()
sendall()
是對
send()
的包裝,完成了使用者需要手動完成的部分,它會自動判斷每次發送的内容量,然後從總内容中删除已發送的部分,将剩下的繼續傳給
send()
進行發送;是以我們最好使用sendall來發送我們的資料包,這樣才能保證我們發送的資料包完整。
Server
服務端先初始化 Socket,建立流式套接字,與本機位址及端口進行綁定,然後通知 TCP,準備好接收連接配接,調用
accept()
阻塞,等待來自用戶端的連接配接。如果這時用戶端與伺服器建立了連接配接,用戶端發送資料請求,伺服器接收請求并處理請求,然後把響應資料發送給用戶端,用戶端讀取資料,直到資料交換完畢。最後關閉連接配接,互動結束。
import socket
import sys
if __name__ == '__main__':
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 建立 Socket 連接配接(TCP)
print('Socket Created')
try:
sock.bind(('127.0.0.1', 8001)) # 配置 Socket,綁定 IP 位址和端口号
except socket.error as e:
print('Bind Failed...', e)
sys.exit(0)
sock.listen(5) # 設定最大允許連接配接數,各連接配接和 Server 的通信遵循 FIFO 原則
while True: # 循環輪詢 Socket 狀态,等待通路
conn, addr = sock.accept()
try:
conn.settimeout(10) # 如果請求超過 10 秒沒有完成,就終止操作
# 如果要同時處理多個連接配接,則下面的語句塊應該用多線程來處理
while True: # 獲得一個連接配接,然後開始循環處理這個連接配接發送的資訊
data = conn.recv(1024)
print('Get value ' + data, end='\n\n')
if not data:
print('Exit Server', end='\n\n')
break
conn.sendall('OK') # 傳回資料
except socket.timeout: # 建立連接配接後,該連接配接在設定的時間内沒有資料發來,就會引發逾時
print('Time out')
conn.close() # 當一個連接配接監聽循環退出後,連接配接可以關掉
sock.close()
調用
accept()
時,Socket 會進入waiting狀态。用戶端請求連接配接時,建立連接配接并傳回伺服器。
accept()
傳回一個含有兩個元素的元組 (conn, addr)。第一個元素 conn 是新的 Socket 對象,伺服器必須通過它與用戶端通信;第二個元素 addr 是用戶端的 IP 位址及端口。
data = conn.recv(1024)
接下來是處理階段,伺服器和用戶端通過
send()
和
recv()
通信(傳輸資料)。
伺服器調用
send()
,并采用字元串形式向用戶端發送資訊,
send()
傳回已發送的字元個數。
伺服器調用
recv()
從用戶端接收資訊。調用
recv()
時,伺服器必須指定一個整數,它對應于可通過本次方法調用來接收的最大資料量。
recv()
在接收資料時會進入blocked狀态,最後傳回一個字元串,用它表示收到的資料。如果發送的資料量超過了
recv()
所允許的,資料會被截短。多餘的資料将緩沖于接收端,以後調用
recv()
時,會繼續讀剩餘的位元組,如果有多餘的資料會從緩沖區删除(以及自上次調用
recv()
以來,用戶端可能發送的其它任何資料)。傳輸結束,伺服器調用 Socket 的
close()
關閉連接配接。
TCP的信令互動流程
一次完整的TCP通訊包括:建立連接配接、資料傳輸、關閉連接配接
建立連接配接(三次握手):
1.用戶端通過向伺服器端發送一個SYN來建立一個主動打開,作為三路握手的一部分。
2.伺服器端應當為一個合法的SYN回送一個SYN/ACK。
3.最後,用戶端再發送一個ACK。這樣就完成了三路握手,并進入了連接配接建立狀态。
資料傳輸:
1.發送資料端傳輸PSH資料包
2.接收資料端回複ACK資料包
關閉連接配接(四次分手):
1. 一端主動關閉連接配接。向另一端發送FIN包。
2. 接收到FIN包的另一端回應一個ACK資料包。
3. 另一端發送一個FIN包。
4. 接收到FIN包的原發送方發送ACK對它進行确認。
将TCP的握手分手與Socket的傳輸流程結合起來了解:
首先是TCP的三次握手中的Socket的互動流程
- 伺服器調用
、socket()
、bind()
完成初始化後,調用listen()
阻塞等待;accept()
- 用戶端 Socket 對象調用
向伺服器發送了一個 SYN 并阻塞;connect()
- 伺服器完成了第一次握手,即發送 SYN 和 ACK 應答;
- 用戶端收到服務端發送的應答之後,從
傳回,再發送一個 ACK 給伺服器;connect()
- 伺服器 Socket 對象接收用戶端第三次握手 ACK 确認,此時服務端從
傳回,建立連接配接。accept()
然後是四次分手
- 某個應用程序調用
主動關閉,發送一個 FIN;close()
- 另一端接收到 FIN 後被動執行關閉,并發送 ACK 确認;
- 之後被動執行關閉的應用程序調用
關閉 Socket,并也發送一個 FIN;close()
- 接收到這個 FIN 的一端向另一端 ACK 确認。
說明:上面的服務端代碼隻有處理完一個用戶端請求才會去處理下一個用戶端的請求,這樣的伺服器處理能力很弱,而實際中伺服器都需要有并發處理能力,為了達到并發處理,伺服器就需要 fork 一個新的程序或者線程去處理請求。
TCP用戶端、伺服器實時圖像傳輸小Demo
用戶端
#--------------------------------------------------------------
# https://blog.csdn.net/qq_42688495/article/details/108279618
# TCP實時圖像傳輸
# 目前為止找到的時延最低的實作方式
# 能夠實作傳輸完視訊關閉視窗,伺服器不需要重新啟動
#--------------------------------------------------------------
import cv2
import time
import socket
# 服務端ip位址
HOST = '192.168.2.108'
# 服務端端口号
PORT = 8080
ADDRESS = (HOST, PORT)
# 建立一個套接字
tcpClient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 連接配接遠端ip
tcpClient.connect(ADDRESS)
cap = cv2.VideoCapture("clock.mp4")
i = 0
while True:
# 計時
start = time.perf_counter()#用于計算端到端時延的電腦
# 讀取圖像
ref, cv_image = cap.read()
if not ref: #檢查視訊是否讀取完畢,如果讀取完畢則ref=0,那麽退出循環
tcpClient.send(b"over")
break
# 壓縮圖像(如果端到端時延過大,一般是圖像未壓縮,調整至30~60之間的壓縮範圍即可)
img_encode = cv2.imencode('.jpg', cv_image, [cv2.IMWRITE_JPEG_QUALITY, 30])[1]
# 轉換為位元組流
bytedata = img_encode.tostring()
# 标志資料,包括待發送的位元組流長度等資料,用‘,’隔開
flag_data = (str(len(bytedata))).encode() + ",".encode() + " ".encode()
#發送圖檔資料流
tcpClient.send(flag_data)
# 接收服務端的應答
data = tcpClient.recv(1024)
if ("ok" == data.decode()):
# 服務端已經收到标志資料,開始發送圖像位元組流資料
tcpClient.send(bytedata)
# 接收服務端的應答
data = tcpClient.recv(1024)
if ("ok" == data.decode()):
# 計算發送完成的延時
print("延時:" + str(int((time.perf_counter() - start) * 1000)) + "ms")
i = i+1 #自加,計算總的資料幀數
#跳出循環之後意味着視訊全部輸出完畢,這時候要
print('視訊已經全部傳輸完畢')
cap.release()
cv2.destroyAllWindows()
伺服器
#-*- coding: UTF-8 -*-
import socket
import cv2
import numpy as np
HOST = '192.168.2.108'
PORT = 8080
ADDRESS = (HOST, PORT)
# 建立一個套接字
tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 綁定本地ip
tcpServer.bind(ADDRESS)
# 開始監聽
tcpServer.listen(5)
while True:
print("等待連接配接……")
client_socket, client_address = tcpServer.accept()
print("連接配接成功!")
try:
while True:
# 接收标志資料
data = client_socket.recv(1024)
if ("over" == data.decode()):
print("已傳輸完畢!")
break
if data:
# 通知用戶端“已收到标志資料,可以發送圖像資料”
client_socket.send(b"ok")
# 處理标志資料
flag = data.decode().split(",")
# 圖像位元組流資料的總長度
total = int(flag[0])
# 接收到的資料計數
cnt = 0
# 存放接收到的資料
img_bytes = b""
while cnt < total:
# 當接收到的資料少于資料總長度時,則循環接收圖像資料,直到接收完畢
data = client_socket.recv(65535)#256000
img_bytes += data
cnt += len(data)
print("receive:" + str(cnt) + "/" + flag[0])
# 通知用戶端“已經接收完畢,可以開始下一幀圖像的傳輸”
client_socket.send(b"ok")
# 解析接收到的位元組流資料,并顯示圖像
img = np.asarray(bytearray(img_bytes), dtype="uint8")
img = cv2.imdecode(img, cv2.IMREAD_COLOR)
cv2.imshow("img", img)
cv2.waitKey(1)
else:
print("已斷開!")
break
finally:
cv2.destroyAllWindows()
視訊demo
tcp視訊傳輸demo