1. 程式人生 > 其它 >網路程式設計之socket套接字

網路程式設計之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()