今日學習內容總結3.1
今日學習內容總結
昨日我們學習了網路程式設計,對軟體開發結構有了一個明確的瞭解。並且知道了計算機的生產過程中都必須有的相同功能,OSI七層協議,也可以總結成五層。而今天的主要學習內容就是通過程式碼,實現客戶端與伺服器的資訊互動。
socket
socket套接字簡介
什麼是套接字?套接字就是網路間進行通訊的方式的名稱,行內人一般都稱之為套接字通訊。比如說HTTP協議,需要具體的程式設計去實現,或者現在我們做前後端分離專案的時候需要遵循RESTful協議,那麼實現此協議的方法就是RESTful API。那麼傳輸層的兩種傳輸服務分別遵循了TCP、UDP協議,實現這兩種協議的方法就是套接字。
套接字的表示方法:
1.套接字Socket=(IP地址:埠號),套接字的表示方法是點分十進位制的lP地址後面寫上埠號,中間用冒號或逗號隔開。
2.每一個傳輸層連線唯一地被通訊兩端的兩個端點(即兩個套接字)所確定。例如:如果IP地址192.168.0.1,而埠號是23,那麼得到套接字就是(192.168.0.1:23)
套接字工作流程
1.通過網際網路進行通訊,至少需要一對套接字,其中一個運行於客戶端,我們稱之為 Client Socket,另一個運行於伺服器端,我們稱之為 Server Socket。
2.根據連線啟動的方式以及本地套接字要連線的目標,套接字之間的連線過程可以分為三個步驟:
1.伺服器監聽:指伺服器端套接字並不定位具體的客戶端套接字,而是處於等待連線的狀態,實時監控網路狀態。
2.客戶端請求:指由客戶端的套接字提出連線請求,要連線的目標是伺服器端的套接字。為此,客戶端的套接字必須首先描述它要連線的伺服器的套接字,指出伺服器端套接字的地址和埠號,然後就向伺服器端接字提出連線請求。
3.連線確認:當伺服器端套接字監聽到或者說接收到客戶端套接字的連線請求,就會響應客戶端套接字的請求,建立一個新的執行緒,並把伺服器端套接字的描述傳送給客戶端。一旦客戶端確認了此描述,連線就建立好了。而伺服器端套接字繼續處於監聽狀態,接收其他客戶端套接字的連線請求。
我們今天的學習內容是要編寫一個cs架構的程式,實現資料的互動。由於操作OSI七層是所有cs架構的程式都需要經歷的過程,所以有固定的模組。這個模組就是socket模組。
socket模組
socket模組的基本用法:
服務端
import socket
server = socket.socket() # 建立例項
server.bind(('127.0.0.1', 8080)) # 繫結監聽
"""
服務端應該具備的特徵
固定的地址
...
127.0.0.1是計算機的本地迴環地址 只有當前計算機本身可以訪問
"""
server.listen(5) # 監聽
"""
半連線池(暫且忽略 先直接寫 後面講)
"""
sock, addr = server.accept() # 獲取從客戶端發過來的資料 沒有資料就原地等待(程式阻塞)
"""
listen和accept對應TCP三次握手服務端的兩個狀態
"""
print(addr) # 客戶端的地址
data = sock.recv(1024) # recv就是接收資料 用一個最大位元組數來作為引數,如果不確定,使用1024比較好
print(data.decode('utf8'))
sock.send('你好啊'.encode('utf8')) # send傳送資料,用字串作為引數
"""
recv和send接收和傳送的都是bytes型別的資料
"""
sock.close() # 主動關閉連線
server.close() # 關機
這段程式碼的意思是開啟一個socket服務,客戶端傳送過來訊息後。經過服務端的處理後。再返回給客戶端,然後斷開連線。接下來看客戶端的程式碼。
客戶端
import socket
client = socket.socket() # 建立一個socket物件
client.connect(('127.0.0.1', 8080)) # 根據服務端的地址連結
client.send(b'hello sweet heart!!!') # 給服務端傳送訊息
data = client.recv(1024) # 接收服務端回覆的訊息
print(data.decode('utf8'))
client.close() # 關閉客戶端
客戶端的程式碼的意思是,開啟連線,連線到指定埠,使用者輸入資料傳送到服務端,然後接受服務端返回的資料。最後再關閉這個連線。
這樣,我們就簡單的實現了一個客戶端與服務端的首次互動了,雖然只能互動一次就結束。所以接下來的通訊迴圈就是實現服務端與客戶端的連續訊息傳送了。
通訊迴圈
迴圈通訊的實現
上面兩個檔案最後都關閉了連線,我們怎麼保持訊息的連續傳送呢?僅僅是不做關閉就可以了嗎?答案是不行。我們怎麼實現一次連線,就可以持續傳送呢,我們可以在一次連線成功後做一個while true的迴圈,這樣我們就可以持續傳送訊息了。下面是對程式碼的進一步改寫。
服務端
import socket
server = socket.socket() # 建立例項
server.bind(('127.0.0.1', 8080)) # 繫結監聽
"""
服務端應該具備的特徵
固定的地址
...
127.0.0.1是計算機的本地迴環地址 只有當前計算機本身可以訪問
"""
server.listen(5) # 監聽
"""
半連線池(暫且忽略 先直接寫 後面講)
"""
sock, addr = server.accept() # 獲取從客戶端發過來的資料 沒有資料就原地等待(程式阻塞)
"""
listen和accept對應TCP三次握手服務端的兩個狀態
"""
print(addr) # 客戶端的地址
while True:
data = sock.recv(1024) # recv就是接收資料 用一個最大位元組數來作為引數,如果不確定,使用1024比較好
print(data.decode('utf8'))
msg = input('請回復訊息>>>:').strip()
sock.send(msg.encode('utf8')) # send傳送資料,用字串作為引數
"""
recv和send接收和傳送的都是bytes型別的資料
"""
sock.close() # 主動關閉連線
server.close() # 關機
客戶端
import socket
client = socket.socket() # 建立一個socket物件
client.connect(('127.0.0.1', 8080)) # 根據服務端的地址連結
while True:
msg = input('請輸入你需要傳送的訊息>>>:').strip()
client.send(msg.encode('utf8')) # 給服務端傳送訊息
data = client.recv(1024) # 接收服務端回覆的訊息
print(data.decode('utf8'))
client.close() # 關閉客戶端
迴圈通訊的程式碼優化及連線迴圈
優化問題:
1.傳送訊息不能為空
2.反覆重啟服務端可能會報錯
3.連線迴圈:
1.如果是windows 客戶端異常退出之後服務端會直接報錯
2.如果是mac或linux 服務端會接收到一個空訊息
優化方式:
1.統計長度並判斷即可
2.在bind前加 from socket import SOL_SOCKET,SO_REUSEADDR
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
3.1 用異常處理解決
3.2用len判斷
目前我們的服務端只能實現一次服務一個人,不能做到同事服務多個 ,學了併發才可以實現。
半連線池
當伺服器在響應了客戶端的第一次請求後會進入等待狀態,會等客戶端傳送的ack資訊,這時候這個連線就稱之為半連線。而半連線池其實就是一個容器,系統會自動將半連線放入這個容器中,可以避免半連線過多而保證資源耗光。所以我們的半連線池可以設定最大等待人數。節約資源,提高效率。
寫法:listen(3) 。引數可以設定最大的半連線數,最大3個.
黏包問題
TCP作為常用的網路傳輸協議,資料流解析是網路應用開發人員永遠繞不開的一個問題。TCP資料傳輸是以無邊界的資料流傳輸形式,所謂無邊界是指資料傳送端傳送的位元組數,在資料接收端接受時並不一定等於傳送的位元組數,可能會出現粘包情況。
TCP黏包情況:
1. 傳送端傳送了數量比較大的資料,接收端讀取資料時候資料分批到達,造成一次傳送多次讀取;通常網路路由的快取大小有關係,一個數據段大小超過快取大小,那麼就要拆包傳送。
2. 傳送端傳送了幾次資料,接收端一次性讀取了所有資料,造成多次傳送一次讀取;通常是網路流量優化,把多個小的資料段集滿達到一定的資料量,從而減少網路鏈路中的傳輸次數。
問題產生的原因其實是因為recv括號內我們不知道即將要接收的資料到底多大,如果每次接收的資料我們都能夠精確的知道它的大小,那麼肯定不會出現黏包。
解決黏包問題
因為困擾我們的核心問題是不知道即將要接收的資料多大,如果能夠精準的知道資料量多大,那麼黏包問題就自動解決了。所以我們解決的方向就是精確獲取資料的大小。
# 可以使用struct模組
import struct
data1 = 'hello world!'
print(len(data1)) # 12
res1 = struct.pack('i', len(data1)) # 第一個引數是格式 寫i就可以了
print(len(res1)) # 4
ret1 = struct.unpack('i', res1)
print(ret1) # (12,)
data2 = 'hello baby baby baby baby baby baby baby baby'
print(len(data2)) # 45
res2 = struct.pack('i', len(data2))
print(len(res2)) # 4
ret2 = struct.unpack('i', res2)
print(ret2) # (45,)
pack可以將任意長度的數字打包成固定長度,unpack可以將固定長度的數字解包成打包之前資料真實的長度。解決思路:
1.先將真實資料打包成固定長度的包
2.將固定長度的包先發給對方
3.對方接收到包之後再解包獲取真實資料長度
4.接收真實資料長度