網路程式設計之socket套接字
socket套接字簡介
由於操作OSI七層是所有C/S架構的程式都需要經歷的過程,而操作OSI七層相當的複雜,所以這時候就出現了一門技術——socket套接字。
socket套接字可以向網路發出請求或者應答網路請求,使主機間或者一臺計算機上的程序間可以通訊,而python語言提供了socket模組來使用這門技術。
socket模組
C/S架構的軟體無論是在編寫還是執行,都應該先考慮服務端,所以我們先編寫服務端的程式碼。
服務端(Server)
import socket # 建立套接字物件,相當於買手機 server = socket.socket() # 將ip地址和埠號繫結到套接字,相當於插電話卡 server.bind(('127.0.0.1', 8080)) # 監聽,後面詳細講解,相當於開機 server.listen(5) # 等待客戶端的訊息,獲取客戶端的物件和地址,相當於等待並接聽電話 sock, addr = server.accept() # 沒有訊息來就原地等待(程式阻塞) # 獲取客戶端的訊息 data = sock.recv(1024) # 獲取的訊息是bytes型別,需要解碼 print(data.decode('utf8')) # 給客戶端發訊息,需要轉成bytes型別 sock.send('來自服務端的訊息'.encode('utf8')) # 斷開與客戶端的連線,相當於掛電話 sock.close() # 關閉服務端,相當於電話關機 server.close()
客戶端(Client)
import socket
# 產生一個socket物件
client = socket.socket()
# 根據服務端的地址和埠連線
client.connect(('127.0.0.1', 8080))
# 給服務端發訊息
client.send('來自客戶端的訊息'.encode('utf8'))
# 接收來自服務端的訊息
data = client.recv(1024)
# 解碼並輸出
print(data.decode('utf8'))
# 關閉客戶端
client.close()
服務端與客戶端首次互動,一邊是recv那麼另一邊必須是send,兩邊不能相同,否則兩邊都在等待對方發來的訊息,程式就卡住了。
通訊迴圈
上面的程式碼已啟動就結束了,無法讓服務端一直執行,為了能讓服務端和客戶端一直可以互相傳送訊息,我們可以用迴圈的方式實現服務端和客戶端一直可以互動,可以互相發訊息。
服務端(Server)
import socket # 服務端啟動 server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) # 建立與客戶端連線 sock, addr = server.accept() while True: data = sock.recv(1024) print(data.decode('utf8')) msg = input('需要傳送給客戶端的訊息:').strip() sock.send(msg.encode('utf8')) # 斷開連線 sock.close() server.close()
客戶端(Client)
import socket
# 客戶端建立連線
client = 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()
程式碼優化
在實現了通訊迴圈後,還是有很多小問題,比如當服務端或者客戶端傳送的訊息為空時,程式會卡住,無法獲取空的資料。
解決方法:加一個判斷條件判斷輸入的資料是否為空。
# 客戶端
msg = input('需要傳送給服務端的訊息:').strip()
if len(msg) == 0:
print('不能傳送空訊息')
continue
# 服務端
msg = input('需要傳送給客戶端的訊息:').strip()
if len(msg) == 0:
msg = '服務端給你傳送了空訊息'
有些時候重啟服務端可能會報錯:Address already in use
解決方法:在服務端的bind方法前加一串程式碼
from socket import SOL_SOCKET,SO_REUSEADDR
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # 在bind前加
連線迴圈
在windows系統中,如果客戶端異常退出,那麼服務端會引起報錯,所以我們要讓程式碼可以在客戶端異常退出後可以重新回到accept等待新的客戶端,這裡可以使用異常處理的方法。
服務端(Server)
import socket
# 服務端啟動
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
# 建立與客戶端連線
sock, addr = server.accept()
while True:
try:
data = sock.recv(1024)
print(data.decode('utf8'))
msg = input('需要傳送給客戶端的訊息:').strip()
if len(msg) == 0:
msg = '服務端給你傳送了空訊息'
sock.send(msg.encode('utf8'))
except ConnectionResetError:
# 重新建立與客戶端連線
sock, addr = server.accept()
# 斷開連線
sock.close()
server.close()
客戶端(Client)
import socket
# 客戶端建立連線
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
# 與服務端互動
msg = input('需要傳送給服務端的訊息:').strip()
if len(msg) == 0:
print('不能傳送空訊息')
continue
client.send(msg.encode('utf8'))
data = client.recv(1024)
print(data.decode('utf8'))
# 斷開連線
client.close()
PS:目前我們的服務端只能實現一個服務端對應一個客戶端,不能做到一個服務端對應多個客戶端,這個功能需要學了併發程式設計才可以實現。
半連線池
在建立服務端的時候,我們需要建立半連線池,server.listen()這個方法就是建立半連線池的。
半連線池的作用就是設定的最大等待的客戶端的數量,可以有效節省資源,提高效率。listen(5)就是可以讓最多有5個客戶端進行等待。
與當前客戶端斷開連線後,就會去等待區與下一個客戶端連線。
黏包問題
我們先來看一段程式碼:
服務端(Server)
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
sock, addr = server.accept()
data1 = sock.recv(1024)
print(data1.decode('utf8'))
data2 = sock.recv(1024)
print(data2.decode('utf8'))
data3 = sock.recv(1024)
print(data3.decode('utf8'))
sock.close()
server.close()
客戶端(Client)
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
client.send(b'one')
client.send(b'two')
client.send(b'three')
client.close()
首先啟動服務端,然後啟動客戶端,按照之前的理解,服務端應該是輸出三段資料,但是並不是,而是把三段資料合在第一個send一起傳送了,後面兩個send傳送的是空字元。
服務端輸出內容:
b'onetwothree'
b''
b''
這個就是黏包問題!因為TCP協議的特點:會將資料量比較小並且時間間隔比較短的資料整合到一起傳送,並且還會受制於recv括號內的數字大小。
我們可以更改服務端的recv括號內的大小來防止黏包問題:
data1 = sock.recv(3)
data2 = sock.recv(3)
data3 = sock.recv(5)
但這隻能在我們知道傳送的資料大小才能這樣使用,如果我們不知道即將要接收的資料到底多大呢?
解決黏包問題
解決黏包問題,我們可以使用python中的struct模組,這個模組可以把長度任意的資料打包成固定長度的資料。
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 world world world '
print(len(data2)) # 24
# 資料打包
res2 = struct.pack('i', len(data2))
print(len(res2)) # 4
# 資料解包
ret2 = struct.unpack('i', res2)
# 返回的是元組
print(ret2) # (24,)
結合C/S架構:
服務端(Server)
import socket
import struct
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
sock, addr = server.accept()
# 先獲取打包的資料
msg1 = sock.recv(4)
# 解包獲取真實資料長度
data1_len = struct.unpack('i', msg1)[0]
# 在獲取真實資料
data1 = sock.recv(data1_len)
print(data1)
# 獲取第二段資料
msg2 = sock.recv(4)
data2_len = struct.unpack('i', msg2)[0]
data2 = sock.recv(data2_len)
print(data2)
# 獲取第三段資料
msg3 = sock.recv(4)
data3_len = struct.unpack('i', msg3)[0]
data3 = sock.recv(data3_len)
print(data3)
sock.close()
server.close()
客戶端(Client)
import socket
import struct
client = socket.socket()
client.connect(('127.0.0.1', 8080))
data1 = b'one'
data2 = b'two'
data3 = b'three'
# 資料打包
msg1 = struct.pack('i', len(data1))
# 先發送打包好的資料,服務端解包獲取長度
client.send(msg1)
# 在傳送真實的資料
client.send(data1)
msg2 = struct.pack('i', len(data2))
client.send(msg2)
client.send(data2)
msg3 = struct.pack('i', len(data3))
client.send(msg3)
client.send(data3)
client.close()
黏包問題特殊情況(檔案過大)
recv括號內的數字儘量不要寫太大,1024、2048、4096足夠了,如果要傳送的資料大小過大,我們可以使用字典的方式。
1.先接收固定長度的字典包
2.解析出字典的真實長度
3.接收字典資料
4.從字典資料中解析出各種資訊
5.接收真實的資料
比如客戶端給服務端傳輸檔案的資訊:
服務端(Server)
import socket
import struct
import json
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
sock, addr = server.accept()
# 獲取打包的資料
data_json_pack = sock.recv(4)
# 解包
data_json_len = struct.unpack('i', data_json_pack)[0]
# 獲取json資料
data_json = sock.recv(data_json_len)
# json轉字典
data_dict = json.loads(data_json)
print(data_dict)
# 接收檔案
size = 0
while size < data_dict['file_size']:
data = sock.recv(1024)
print(data.decode('utf8'))
size += len(data)
sock.close()
server.close()
客戶端(Client)
import socket
import os
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1', 8080))
data_dict = {
'file_name': r'main.py', # 檔名
'file_size': os.path.getsize(r'main.py') # 檔案大小
}
# 字典轉json
data_json = json.dumps(data_dict)
# 打包json,併發送
data_json_pack = struct.pack('i', len(data_json))
client.send(data_json_pack)
# 傳送json資料
client.send(data_json.encode('utf8'))
# 傳送檔案
with open(data_dict['file_name'], 'rb') as f:
for line in f:
client.send(line)
client.close()