本章目錄
一.什么是socket
二.為什么需要socket
三.socket的發展
四.python中的socket
五.基於TCP的socket
六.基於UDP的socket
六. 粘包問題詳解
七.粘包的解決方案
八.socketserver實現並發通訊
**引入:為什么一定要先學習網絡協議?**
之所以學習網絡編程就是為了讓我們的程序能夠利用網絡來傳輸數據,開發出C/S構架的應用程序
而網絡的核心,就是協議,沒有協議就沒有互聯網,我們要開發出C/S結構程序則必須遵循這些協議的標准!
`就像上帝說可以幫你完成一個願望,然而上帝和你對話時,你說的話上帝完全聽不懂,所有你必須了解上帝使用的語言,說他能聽懂的話!`
# 一.什么是socket
Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。
**socket在OSI模型中的位置**

# 二.為什么需要socket
在標准的OIS模型中並沒有規定說必須有socket層,也就是說不使用socket也能完成通訊,是的,的確如此!
那為什么需要socket呢?一個字 懶,程序員都是懶的!
我們發現還沒有開始實現應用程序邏輯,就需要花大把時間來實現各種協議,太特么費事兒了,就有人專門把協議中一堆復雜的事情進行了封裝,於是socket就誕生了!
有了socket以后,無需自己編寫代碼實現三次握手,四次揮手,ARP請求,打包數據等等,socket已經封裝好了,只需要遵循socket的規定去編程,寫出的程序自然就是遵循tcp/udp標准的。
# 三.socket的發展
套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 因此,有時人們也把套接字稱為“伯克利套接字”或“BSD 套接字”。一開始,套接字被設計用在同 一台主機上多個應用程序之間的通訊。這也被稱進程間通訊,或 IPC。套接字有兩種(或者稱為有兩個種族),分別是基於文件型的和基於網絡型的。
### 基於文件類型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,可以通過訪問同一個文件系統間接完成通信
### 基於網絡類型的套接字家族
套接字家族的名字:AF_INET
(還有AF_INET6被用於ipv6,還有一些其他的地址家族,不過,他們要么是只用於某個平台,要么就是已經被廢棄,或者是很少被使用,或者是根本沒有實現,所有地址家族中,AF_INET是使用最廣泛的一個,python支持很多種地址家族,但是由於大部通訊都是網絡通訊,所以大部分時候使用AF_INET)
# 四.python中的socket
<h3 style="color:red">需明確:關於網絡協議 和socket相關概念,對於所有編程語言都是一致的,區別僅僅是各編程語言的函數名稱不同</h3 >
<h3 style="color:red">需明確:關於網絡協議 和socket相關概念,對於所有編程語言都是一致的,區別僅僅是各編程語言的函數名稱不同</h3 >
```python
# 1.導入socket模塊
import socket
# 2.創建socket對象 函數定義如下
socket.socket(socket_family,socket_type,protocal=0)
#socket_family 可以是 AF_UNIX 或 AF_INET。
#socket_type 可以是 SOCK_STREAM表示TCP協議 或 SOCK_DGRAM表示UDP協議。
#protocol 一般不填,默認值為 0。
# 2.1獲取TCP 套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 或者 后面的參數都有默認值,可以不寫,默認創建的是TCP協議socket
tcpSock = socket.socket()
# 1.導入socket模塊
import socket
# 2.創建socket對象 函數定義如下
socket.socket(socket_family,socket_type,protocal=0)
#socket_family 可以是 AF_UNIX 或 AF_INET。
#socket_type 可以是 SOCK_STREAM表示TCP協議 或 SOCK_DGRAM表示UDP協議。
#protocol 一般不填,默認值為 0。
# 2.1獲取TCP 套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 或者 后面的參數都有默認值,可以不寫,默認創建的是TCP協議socket
tcpSock = socket.socket()
# 2.2獲取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#由於 socket 模塊中有太多的屬性。可以使用'from module import *'語句。使用 'from socket import *',把 socket 模塊里的所有屬性都導入當前命名空間里了,這樣能大幅減短代碼。
#例如:tcpSock = socket(AF_INET, SOCK_STREAM)
#例如:tcpSock = socket(AF_INET, SOCK_STREAM)
```
**要明確一點:無論是客戶端服務器端都使用的都是socket對象**
```python
服務端套接字函數
s.bind() 綁定(主機,端口號)到套接字
s.listen() 開始TCP監聽
s.accept() 被動接受TCP客戶的連接,(阻塞式)等待連接的到來
服務端套接字函數
s.bind() 綁定(主機,端口號)到套接字
s.listen() 開始TCP監聽
s.accept() 被動接受TCP客戶的連接,(阻塞式)等待連接的到來
客戶端套接字函數
s.connect() 主動初始化TCP服務器連接
s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
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.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.setblocking() 設置套接字的阻塞與非阻塞模式
s.settimeout() 設置阻塞套接字操作的超時時間
s.gettimeout() 得到阻塞套接字操作的超時時間
```
## 四.1基於TCP的socket
前言:已經明確socket是別人封裝好的接口,使用接口就變的簡單了
按照通訊流程編寫代碼即可
### 1.TCP通訊流程

**重點是要先接受通話請求 才能建立連接進行通話**
TCP的通訊流程與打電話的過程非常相似
買手機 == socket()
裝進手機卡 == bind()
待機 == listen()
電話來了 接受通話 == accept()
聽 == read()
說 == write()
掛電話 == close()
裝進手機卡 == bind()
待機 == listen()
電話來了 接受通話 == accept()
聽 == read()
說 == write()
掛電話 == close()
### 2.TCP服務端
```python
import socket
ip_port=('127.0.0.1',9000) #電話卡
BUFSIZE=1024 #收發消息的尺寸
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
s.bind(ip_port) #手機插卡
s.listen(5) #手機待機
import socket
ip_port=('127.0.0.1',9000) #電話卡
BUFSIZE=1024 #收發消息的尺寸
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
s.bind(ip_port) #手機插卡
s.listen(5) #手機待機
conn,addr=s.accept() #手機接電話
print('接到來自%s的電話' %addr[0])
msg=conn.recv(BUFSIZE) #聽消息,聽話
print(msg,type(msg))
print(msg,type(msg))
conn.send(msg.upper()) #發消息,說話
conn.close() #掛電話
s.close() #手機關機
服務端
```
```
### 3.TCP客戶端
```python
import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect_ex(ip_port) #撥電話
s.send('linhaifeng nb'.encode('utf-8')) #發消息,說話(只能發送字節類型)
feedback=s.recv(BUFSIZE) #收消息,聽話
print(feedback.decode('utf-8'))
print(feedback.decode('utf-8'))
s.close() #掛電話
```
```
<p style="color:red">注意TCP中必須先啟動服務器再啟動客戶端,否則客戶端由於無法鏈接服務器,直接報錯!</p>
如上就完成了一個最基本的TCP通訊,但是建立是為了傳輸數據,二傳輸數據很多時候並不是一次性就傳輸完成了,需要多次收發過程,所以需要給代碼加上循環
如上就完成了一個最基本的TCP通訊,但是建立是為了傳輸數據,二傳輸數據很多時候並不是一次性就傳輸完成了,需要多次收發過程,所以需要給代碼加上循環
### 4.改進版服務器端
```python
import socket
ip_port=('127.0.0.1',8081)#電話卡
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
s.bind(ip_port) #手機插卡
s.listen(5) #手機待機
import socket
ip_port=('127.0.0.1',8081)#電話卡
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
s.bind(ip_port) #手機插卡
s.listen(5) #手機待機
while True: #新增接收鏈接循環,可以不停的接電話
conn,addr=s.accept() #手機接電話
# print(conn)
# print(addr)
print('接到來自%s的電話' %addr[0])
while True: #新增通信循環,可以不斷的通信,收發消息
msg=conn.recv(BUFSIZE) #聽消息,聽話
print(msg,type(msg))
conn.send(msg.upper()) #發消息,說話
conn.close() #掛電話
s.close() #手機關機
```
conn,addr=s.accept() #手機接電話
# print(conn)
# print(addr)
print('接到來自%s的電話' %addr[0])
while True: #新增通信循環,可以不斷的通信,收發消息
msg=conn.recv(BUFSIZE) #聽消息,聽話
print(msg,type(msg))
conn.send(msg.upper()) #發消息,說話
conn.close() #掛電話
s.close() #手機關機
```
### 5.改進版客戶端
```python
import socket
ip_port=('127.0.0.1',8081)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect_ex(ip_port) #撥電話
import socket
ip_port=('127.0.0.1',8081)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect_ex(ip_port) #撥電話
while True: #新增通信循環,客戶端可以不斷發收消息
msg=input('>>: ').strip()
if len(msg) == 0:continue
s.send(msg.encode('utf-8')) #發消息,說話(只能發送字節類型)
feedback=s.recv(BUFSIZE) #收消息,聽話
print(feedback.decode('utf-8'))
s.close() #掛電話
```
msg=input('>>: ').strip()
if len(msg) == 0:continue
s.send(msg.encode('utf-8')) #發消息,說話(只能發送字節類型)
feedback=s.recv(BUFSIZE) #收消息,聽話
print(feedback.decode('utf-8'))
s.close() #掛電話
```
### 6.常見錯誤:
#### 1.端口占用
在調試過程中,可能會遇見以下錯誤:

問題發生原因:
1.可能是由於你已經啟動了服務器程序,卻又再次啟動了服務器程序,同一個端口不能被多個進程使用導致!
2.三次握手或四次揮手時,發生了異常導致對方程序已經結束而服務器任然處於time_wait狀態導致!
3.在高並發的場景下,由於鏈接的客戶端太多,也會產生大量處於time_wait狀態的鏈接
解決的方案:
第1種原因,很簡單關閉之前運行的服務器即可
第2,3中原因導致的問題,有兩種解決方案:
1.設置服務器重用端口
```python
#加入一條socket配置,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,必須在bind前加
phone.bind(('127.0.0.1',8081))
```
#加入一條socket配置,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,必須在bind前加
phone.bind(('127.0.0.1',8081))
```
2.通過調整linux內核參數解決(了解)
```python
發現系統存在大量TIME_WAIT狀態的連接,通過調整linux內核參數解決,
vi /etc/sysctl.conf
編輯文件,加入以下內容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
然后執行 /sbin/sysctl -p 讓參數生效。
net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉;
net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用於新的TCP連接,默認為0,表示關閉;
net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。
net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間
```
發現系統存在大量TIME_WAIT狀態的連接,通過調整linux內核參數解決,
vi /etc/sysctl.conf
編輯文件,加入以下內容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
然后執行 /sbin/sysctl -p 讓參數生效。
net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉;
net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用於新的TCP連接,默認為0,表示關閉;
net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。
net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間
```
#### 2.強行關閉鏈接
(發生錯誤演示,運行上面的改進版的服務器與客戶端,鏈接成功后直接停止客戶端程序)
當客服端與服務器鏈接成功后,如果一方沒有執行close,而是直接強行終止程序(或是遇到異常被迫終止),都會導致另一方發送問題,
在windows下,接收數據的一方在recv函數處將拋出異常
```python
Traceback (most recent call last):
File "C:/Users/jerry/PycharmProjects/untitled/TCP/server.py", line 9, in <module>
conn.recv(1024)
ConnectionResetError: [WinError 10054] 遠程主機強迫關閉了一個現有的連接。
```
Traceback (most recent call last):
File "C:/Users/jerry/PycharmProjects/untitled/TCP/server.py", line 9, in <module>
conn.recv(1024)
ConnectionResetError: [WinError 10054] 遠程主機強迫關閉了一個現有的連接。
```
linux下,不會拋出異常會導致接收數據的一方,recv方法不斷的收到空消息,造成死循環
要使應用程序能夠在不同平台正常工作,那需要分別處理這兩個問題
解決方案如下:
```python
import socket
ip_port=('127.0.0.1',8081)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(ip_port)
s.listen(5)
while True:
conn,addr=s.accept()
while True:
try:
msg=conn.recv(BUFSIZE)
#linux不會拋出異常,會接收到空消息,這里加以判斷
if not msg:
conn.close()
break
print(msg,type(msg))
conn.send(msg.upper())
except ConnectionResetError:
#只要異常發生則意味着對方以及關閉了,服務器也相應的關閉該鏈接
conn.close()
break
conn.close()
s.close()
```
import socket
ip_port=('127.0.0.1',8081)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(ip_port)
s.listen(5)
while True:
conn,addr=s.accept()
while True:
try:
msg=conn.recv(BUFSIZE)
#linux不會拋出異常,會接收到空消息,這里加以判斷
if not msg:
conn.close()
break
print(msg,type(msg))
conn.send(msg.upper())
except ConnectionResetError:
#只要異常發生則意味着對方以及關閉了,服務器也相應的關閉該鏈接
conn.close()
break
conn.close()
s.close()
```
至此TCP通訊模板程序就完成了,可以不斷的接收新的鏈接,不斷的收發消息,並且不會因為客戶端強制關閉而異常退出!
# 五.基於UDP的socket
### 1.UDP 通訊流程

UDP通訊流程與對講機非常類似
買傳呼機 == socket()
固定對講頻道 == bind()
收信號 == recvfrom()
發信號 == sendto()
**由於不需要建立 連接所以省去 TCP的listen()和accept()這兩步**
固定對講頻道 == bind()
收信號 == recvfrom()
發信號 == sendto()
**由於不需要建立 連接所以省去 TCP的listen()和accept()這兩步**
### 2.UDP服務器端
```python
import socket
ip_port=('127.0.0.1',9000) # 固定通訊頻道
BUFSIZE=1024
#在TCP中socket的初始化參數可以省略, 因為默認創建的就是TCP協議的socket
#而UDP則必須手動指定相關參數
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 買對講機
udp_server_client.bind(ip_port)
while True:
msg,addr=udp_server_client.recvfrom(BUFSIZE) #收信息
print(msg,addr)
udp_server_client.sendto(msg.upper(),addr) # 發信息
```
import socket
ip_port=('127.0.0.1',9000) # 固定通訊頻道
BUFSIZE=1024
#在TCP中socket的初始化參數可以省略, 因為默認創建的就是TCP協議的socket
#而UDP則必須手動指定相關參數
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 買對講機
udp_server_client.bind(ip_port)
while True:
msg,addr=udp_server_client.recvfrom(BUFSIZE) #收信息
print(msg,addr)
udp_server_client.sendto(msg.upper(),addr) # 發信息
```
### 3.UDP客戶端
```python
import socket
ip_port=('127.0.0.1',9000) #確定通訊頻道
BUFSIZE=1024
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 買對講機
while True:
msg=input('>>: ').strip()
if not msg:continue
udp_server_client.sendto(msg.encode('utf-8'),ip_port) # 發消息
back_msg,addr=udp_server_client.recvfrom(BUFSIZE) #收消息
```
import socket
ip_port=('127.0.0.1',9000) #確定通訊頻道
BUFSIZE=1024
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 買對講機
while True:
msg=input('>>: ').strip()
if not msg:continue
udp_server_client.sendto(msg.encode('utf-8'),ip_port) # 發消息
back_msg,addr=udp_server_client.recvfrom(BUFSIZE) #收消息
```
udp是無鏈接的,先啟動哪一端都不會報錯,即使對方地址根本不存在也不會報錯,強制關閉任何一方也沒有任何問題
另外,由於無連接的特點,服務器不需要針對摸個客戶端進行循環,只要循環的接收即可,誰發來的消息都可以被處理,基於這個特點我們可以編寫一個UDP程序,實現多個客戶端同時與服務器通訊
### 4.UDP聊天服務器
```python
import socket
ip_port=('127.0.0.1',8081)
udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #買手機
udp_server_sock.bind(ip_port)
import socket
ip_port=('127.0.0.1',8081)
udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #買手機
udp_server_sock.bind(ip_port)
while True:
qq_msg,addr=udp_server_sock.recvfrom(1024)
print('來自[%s:%s]的一條消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8')))
back_msg=input('回復消息: ').strip()
udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
```
qq_msg,addr=udp_server_sock.recvfrom(1024)
print('來自[%s:%s]的一條消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8')))
back_msg=input('回復消息: ').strip()
udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
```
### 5.UDP聊天客戶端
```python
import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
qq_name_dic={
'狗哥':('127.0.0.1',8081),
'天線寶寶':('127.0.0.1',8081),
'巴拉巴拉小魔女':('127.0.0.1',8081),
'王尼瑪':('127.0.0.1',8081),
}
'狗哥':('127.0.0.1',8081),
'天線寶寶':('127.0.0.1',8081),
'巴拉巴拉小魔女':('127.0.0.1',8081),
'王尼瑪':('127.0.0.1',8081),
}
while True:
qq_name=input('請選擇聊天對象: ').strip()
while True:
msg=input('請輸入消息,回車發送: ').strip()
if msg == 'quit':break
if not msg or not qq_name or qq_name not in qq_name_dic:continue
udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])
back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)
print('來自[%s:%s]的一條消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))
udp_client_socket.close()
```
qq_name=input('請選擇聊天對象: ').strip()
while True:
msg=input('請輸入消息,回車發送: ').strip()
if msg == 'quit':break
if not msg or not qq_name or qq_name not in qq_name_dic:continue
udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])
back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)
print('來自[%s:%s]的一條消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))
udp_client_socket.close()
```
客戶端1

客戶端2

服務器:

# 六. 粘包問題
### 什么是粘包?
粘包指的是數據與數據之間沒有明確的分界線,導致不能正確讀取數據!
要理解粘包問題,需要先了解TCP協議傳輸數據時的具體流程,TCP協議也稱之為流式協議(UDP稱為數據報協議)

應用程序無法直接操作硬件,應用程序想要發送數據則必須將數據交給操作系統,而操作系統需要需要同時為所有應用程序提供數據傳輸服務,也就意味着,操作系統不可能立馬就能將應用程序的數據發送出去,就需要為應用程序提供一個緩沖區,用於臨時存放數據,具體流程如下:
應用程序無法直接操作硬件,應用程序想要發送數據則必須將數據交給操作系統,而操作系統需要需要同時為所有應用程序提供數據傳輸服務,也就意味着,操作系統不可能立馬就能將應用程序的數據發送出去,就需要為應用程序提供一個緩沖區,用於臨時存放數據,具體流程如下:
##### 發送方:
當應用程序調用send函數時,應用程序會將數據從應用程序拷貝到操作系統緩存,再由操作系統從緩沖區讀取數據並發送出去
##### 接收方:
對方計算機收到數據也是操作系統先收到,至於應用程序何時處理這些數據,操作系統並不清楚,所以同樣需要將數據先存儲到操作系統的緩沖區中,當應用程序調用recv時,實際上是從操作系統緩沖區中將數據拷貝到應用程序的過程
上述過程對於TCP與UDP都是相同的不同之處在於:
##### UDP:
UDP在收發數據時是基於數據包的,即一個包一個包的發送,包與包之間有着明確的分界,到達對方操作系統緩沖區后也是一個一個獨立的數據包,接收方從操作系統緩沖區中將數據包拷貝到應用程序
這種方式存在的問題:
1.發送方發送的數據長度每個操作系統會有不同的限制,數據超過限制則無法發送
2.接收方接收數據時如果應用程序的提供的緩存容量小於數據包的長度將造成數據丟失,而緩沖區大小不可能無限大

##### TCP:
當我們需要傳輸較大的數據,或需要保證數據完整性時,最簡單的方式就是使用TCP協議了
與UDP不同的是,TCP增加了一套校驗規則來保證數據的完整性,會將超過TCP包最大長度的數據拆分為多個TCP包 並在傳輸數據時為每一個TCP數據包指定一個順序號,接收方在收到TCP數據包后按照順序將數據包進行重組,重組后的數據全都是二進制數據,且每次收到的二進制數據之間沒有明顯的分界

基於這種工作機制TCP在三種情況下會發送粘包問題
1.當單個數據包較小時接收方可能一次性讀取了多個包的數據
2.當整體數據較大時接收方可能一次僅讀取了一個包的一部分內容
3.另外TCP協議為了提高效率,增加了一種優化機制,會將數據較小且發送間隔較短的數據合並發送,該機制也會導致發送方將兩個數據包粘在一起發送
# 七.粘包的解決方案
理解了粘包問題后,接下來就是如何來解決它了
## 1.基礎解決方案
首先明確只有TCP會出現粘包問題,之所以粘包是因為接收方不知道一次該接收的數據長度,那如何才能讓接收方知道數據的長度呢?
解決方案:
在發送數據前先發送數據長度
##### 客戶端:
```python
import socket
import socket
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
cmd = input(">>>:").strip()
c.send(cmd.encode("utf-8"))
c.connect(("127.0.0.1",8888))
while True:
cmd = input(">>>:").strip()
c.send(cmd.encode("utf-8"))
data = c.recv(1024)
length = int(data.decode("utf-8"))
print(length)
size = 0
res = b""
while size < length:
temp = c.recv(1024)
size += len(temp)
res += temp
print(res.decode("gbk"))
```
length = int(data.decode("utf-8"))
print(length)
size = 0
res = b""
while size < length:
temp = c.recv(1024)
size += len(temp)
res += temp
print(res.decode("gbk"))
```
##### 服務器:
```python
import socket
import subprocess
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()
import socket
import subprocess
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()
while True:
client, addr = server.accept()
while True:
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
data = p.stdout.read()+p.stderr.read()
length = str(len(data))
client.send(length.encode("utf-8"))
print(length)
client.sendall(data)
```
client, addr = server.accept()
while True:
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
data = p.stdout.read()+p.stderr.read()
length = str(len(data))
client.send(length.encode("utf-8"))
print(length)
client.sendall(data)
```
上述方案 看起來解決了粘包問題
<p style="color:red">但是由於negle優化機制的存在,長度信息和數據還是有可能會粘包,而接受方並不知道長度信息具體幾個字節,所以現在的問題是如何能夠長度信息做成一個固定長度的bytes數據</p>
我們可以將字符串拼接為一個固定長度的字符 但是這樣太麻煩,struct模塊為我們提供了一個功能,可以將整數類型轉換為固定長度的bytes,此時就派上用場了
```python
# struct模塊使用
import struct
# 整型轉bytes
res = struct.pack("i",100)
print(res)
print(len(res))
# struct模塊使用
import struct
# 整型轉bytes
res = struct.pack("i",100)
print(res)
print(len(res))
# bytes轉整型
res2 = struct.unpack("i",res) # 返回一個元組
print(res2)
print(res2[0])
```
res2 = struct.unpack("i",res) # 返回一個元組
print(res2)
print(res2[0])
```
解決方案修正
##### 客戶端:
```python
import socket
import struct
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
cmd = input(">>>:").strip()
c.send(cmd.encode("utf-8"))
import socket
import struct
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
cmd = input(">>>:").strip()
c.send(cmd.encode("utf-8"))
data = c.recv(4)
length = struct.unpack("i",data)[0]
print(length)
size = 0
res = b""
while size < length:
temp = c.recv(1024)
size += len(temp)
res += temp
print(res.decode("gbk"))
```
length = struct.unpack("i",data)[0]
print(length)
size = 0
res = b""
while size < length:
temp = c.recv(1024)
size += len(temp)
res += temp
print(res.decode("gbk"))
```
##### 服務器:
```python
import socket
import subprocess
import struct
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()
import socket
import subprocess
import struct
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()
while True:
client, addr = server.accept()
while True:
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
data = p.stdout.read()+p.stderr.read()
length = len(data)
len_data = struct.pack("i",length)
client.send(len_data)
client, addr = server.accept()
while True:
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
data = p.stdout.read()+p.stderr.read()
length = len(data)
len_data = struct.pack("i",length)
client.send(len_data)
print(length)
client.send(data)
```
client.send(data)
```
## 2.自定義報頭解決粘包
上述方案已經完美解決了粘包問題,但是擴展性不高,例如我們要實現文件上傳下載,不光要傳輸文件數據,還需要傳輸文件名字,md5值等等,如何能實現呢?
具體思路:
發送端:
1.先將所有的額外信息打包到一個頭中
2.然后先發送頭部數據
3.最后發送真實數據
接收端:
1.接收固定長度的頭部長度數據
2.根據長度數據獲取頭部數據
3.根據頭部數據獲取真實數據
#### CMD程序自定義報頭:
##### 客戶端:
```python
import socket
import struct
import json
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
cmd = input(">>>:").strip()
c.send(cmd.encode("utf-8"))
# 頭部數據
data = c.recv(4)
head_length = struct.unpack("i",data)[0]
head_data = c.recv(head_length).decode("utf-8")
head = json.loads(head_data)
print(head)
import socket
import struct
import json
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
cmd = input(">>>:").strip()
c.send(cmd.encode("utf-8"))
# 頭部數據
data = c.recv(4)
head_length = struct.unpack("i",data)[0]
head_data = c.recv(head_length).decode("utf-8")
head = json.loads(head_data)
print(head)
# 真實數據長度
data_length = head["data_size"]
data_length = head["data_size"]
#接收真實數據
size = 0
res = b""
while size < data_length:
temp = c.recv(1024)
size += len(temp)
res += temp
size = 0
res = b""
while size < data_length:
temp = c.recv(1024)
size += len(temp)
res += temp
print(res.decode("gbk"))
```
##### 服務器:
```python
import socket
import subprocess
import struct
import json
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()
import subprocess
import struct
import json
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()
while True:
client, addr = server.accept()
while True:
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
# 真實數據
data = p.stdout.read() + p.stderr.read()
# 頭部數據
head = {"data_size":len(data),"額外信息":"額外的值"}
head_data = json.dumps(head).encode("utf-8")
#頭部長度
head_len = struct.pack("i",len(head_data))
client, addr = server.accept()
while True:
cmd = client.recv(1024).decode("utf-8")
p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
# 真實數據
data = p.stdout.read() + p.stderr.read()
# 頭部數據
head = {"data_size":len(data),"額外信息":"額外的值"}
head_data = json.dumps(head).encode("utf-8")
#頭部長度
head_len = struct.pack("i",len(head_data))
#逐個發送
client.send(head_len)
client.send(head_data)
client.send(data)
```
client.send(head_len)
client.send(head_data)
client.send(data)
```
至此粘包問題就完美解決了
### 文件上傳下載
# 八.socketserver實現並發通訊