10.網路程式設計之socket
目錄
- 一、什麼是socket?
- 二、面向連線的套接字和為無連線的套接字
- 三、python中socket
- 四、基於TCP的socket
- 五、基於UDP的socket
- 六、粘包問題
- 七、SocketServer模組
- 八、補充:基礎網路協議
- 九、例子
一、什麼是socket?
1.1 套接字簡介
套接字(socket):最初是應用於計算機兩個程序之間的通訊。
兩種型別的套接字:基於檔案的和麵向網路的
- 基於檔案的套接字:UNIX套接字,套接字的一個家族,並且擁有一個“家族名字”,AF_UNIX(又名:AF_LOCAL),代表地址家族:UNIX。python用的是AF_UNIX。因為兩個程序執行在同一臺計算機上,所以,這是基於檔案的套接字。
- 基於網路的套接字:他的家族名字是:AF_INET,代表家族地址:因特網。另一個地址家族AF_INET6用ipv6定址。
1.2 套接字地址:主機-埠對
主機名和埠號類似區號和電話號碼的組合,有效的埠號範圍為0~~65535(儘管小於1024的埠號預留給了系統)。使用POSIX相容系統(如:Linux,Mac OS等),可以在/etc/services檔案中找到預留埠號的列表。
二、面向連線的套接字和為無連線的套接字
2.1 面向連線的套接字
面向連線的套接字:在通訊前必須先建立一個連結,也成為虛擬電路或者流套接字。它提供序列化的、可靠的、不可重複的資料交付,沒有記錄邊界,這表示每條訊息可以拆分成多個片段,在每條訊息片段都能到達的前提下,按照一定順序組合,最後將完整的訊息傳遞給正在等待的應用程式。
這是基於傳輸控制協議(TCP協議),建立TCP套接字,必須使用SOCKET_STREAM
2.2 無連線的套接字
這是資料報型別的套接字。這是一種無連線的套接字,無法保證傳輸過程中的順序性、可靠性、重複性,且訊息是以整體傳送的。
這是基於使用者資料報協議(UDP),建立UDP套接字,必須使用SOCKET_DGRAM作為套接字。
三、python中socket
3.1 socket()模組函式
socket(socket_family,socket_type,protocol=0)
# socket_family是AF_UNIX或AF_INET
# socket_type是SOCKET_STRRAM或SOCKET_DGRAM
# protocol通常省略,預設為0,這是與特定的地址家族相關的協議,如果是0,則系統就會根據地址格式和套接類別,自動選擇一個合適的協議。
# 使用from socket import *傳入引數不會報錯,直接imort socket傳入引數會報錯,顯示沒有這個AF_INET這個族,因為這個AF_INET這個值在socket的名稱空間裡,from socket import *是把所有名字都引入當前的名稱空間下
3.2 套接字物件(內建)方法
名稱 | 描述 |
---|---|
伺服器socket方法 | |
s.bind() | 將地址(主機名、埠號對)繫結到套接字上 |
s.listen() | 設定並啟動TCP監聽器 |
s.accept() | 被動接受TCP客戶端的連線,一直等待直到連線到達(阻塞) |
客戶端socket方法 | |
s.connect() | 主動發起TCP連線(阻塞) |
s.connect_ex() | connext()的擴充套件版本,以錯誤碼形式返回問題,不是丟擲一個異常 |
普通socket方法 | |
s.recv() | 接受TCP訊息,recv(1024)不代表一定要收到1024個位元組,而是一次最多隻能收這麼多。(阻塞) |
s.recv_into() | 接受TCP訊息到指定的緩衝區 |
s.send() | 傳送TCP訊息 |
s.sendall() | 完整的傳送TCP訊息(本質就是迴圈呼叫send,sendall在待發資料量),待發資料量大於緩衝區的剩餘空間,資料不丟失,直到呼叫send發完 |
s.recvfrom() | 接受UDP訊息和地址(阻塞) |
s.recvfrom_into() | 接受UDP訊息到指定的緩衝區 |
s.sendto() | 傳送UDP訊息(需要寫訊息和地址) |
s.close() | 關閉套接字 |
面向阻塞(鎖)的socket | |
s.setblocking() | 設定套接字的阻塞或非阻塞模式 |
s.settimeout() | 設定阻塞套接字操作的超時時間 |
s.gettimeout() | 獲取阻塞套接字操作超時時間 |
會造成阻塞的方法,accept,recv,recvfrom,connect
3.3 Socket中的一些引數
listen(n) # n表示允許排隊個數,socket允許的最大連線數=伺服器正在處理的socket連線數+排隊的個數。
send不需要寫地址,sendto需要寫地址。
四、基於TCP的socket
寫在這裡,不管是伺服器還是客戶端,在接收訊息時先解碼,在傳送訊息時編碼。
4.1 建立TCP伺服器
# 虛擬碼
ss = scoket() # 建立伺服器套接字物件
ss.bind() # 套接字與地址繫結
ss.listen() # 監聽連線
inf_loop: # 伺服器無限迴圈
sc = ss.accept() # 接受客戶端連線
comm_loop: # 通訊迴圈
cs.recv()/sc.send() # 對話(接受/傳送)
cs.close() # 關閉客戶端套接字
ss.close() # 關閉服務端套接字,一般不用
SocketServer模組是一個以socket為基礎的高階套接字模組,支援客戶端請求的執行緒和多程序處理。
4.2 建立TCP客戶端
cs = socket() # 建立客戶端套接字
cs.connect() # 嘗試連線伺服器
somm_loop: # 通訊迴圈
cs.send()/cs.recv() # 對話(傳送/接受)
cs.close() # 關閉客戶端套接字
傳送的資料相關的內容組成json,先發json的長度,再發json,json中存了接下來要傳送的資料長度,再發資料。
五、基於UDP的socket
5.1 建立UDP伺服器
UDP伺服器除了等待連線外,不需要像TCP那樣做一些額外的工作。
ss = socket() # 建立伺服器套接字
ss.bind() # 繫結伺服器套接字
inf_loop: # 伺服器無限迴圈
cs = ss.recvfrom()/ss.sendto() # 關閉(接受/傳送)
ss.close() # 關閉伺服器套接字
OSError: [WinError 10057] 由於套接字沒有連線並且(當使用一個 sendto 呼叫傳送資料報套接字時)沒有提供地址,傳送或接收資料的請求沒有被接受。解決:socket.socket(type=socket.SOCK_DGRAM)
5.2 建立UDP客戶端
cs = socket() # 建立客戶端
comm_loop: # 通訊迴圈
cs.sento()/cs.recvfrom() # 對話(傳送/接受)
cs.close() # 關閉客戶端套接字
六、粘包問題
6.1 粘包問題
send()要傳送的資料合併成一條傳送了,產生原因:傳送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle演算法),將多次間隔較小且資料量小的資料,合併成一個大的資料塊,然後進行封包。還有一個機制是拆包,在傳送端,因為受到網絡卡MTU限制,會將大的超過MTU
限制的資料,進行拆分,拆分成多個小的資料,進行傳輸,當傳輸到目標主機
的作業系統層時,會將多個小資料合併成原本的資料。
網路最大頻寬限制MTU=1500位元組,
導致粘包問題的本質:TCP傳輸的是流式傳輸,資料與資料之間沒有邊界
解決粘包:自定義協議,規定傳送資料的位元組大小,等於設定邊界,即客戶端傳送一條訊息,服務端接收一條訊息,前提是服務端知道客戶端傳送的一條訊息的大小,這需要客戶端提前告知,需要在正式的傳送訊息前傳送一條類似於驗證訊息,這條訊息必須固定大小,否則服務端不知道這條訊息多大,怎麼接收,還是有可能發生粘包現象。
- 傳送端:計算要傳送的資料的長度,通過struct模組轉換為固定長度的4位元組,傳送4個位元組的長度
- 接收端:接收4個位元組,在使用struct.unpack把4個位元組轉換成數字,這個數字就是要接收資料的長度,再更據長度接收真實的資料就不會發生粘包現象了。
6.2 自定義協議解決粘包問題
# 客戶端
# 設定要傳送訊息的長度
num = str(len(msg))
# 前面補0填充到四個位元組,由於是數字不會改變值的大小
ret = num.zfill(4) # 把ret先發送過去,這個長度是固定的4個位元組
sk.send(ret.encode('utf-8'))
# 服務端
# 接收上面的ret
length = conn.recv(4).decode('utf-8')
# 按照客戶端傳送過來的長度進行接收
msg = conn.recv(length)
6.3 使用struct模組解決粘包問題
struct模組:
import struct
num = 125478568
# 轉換成4個位元組
ret = struct.pack('i', num)
print(ret, len(ret)) # b'\xa8\xa6z\x07' 4
print(struct.unpack('i', ret)) # 返回的是一個元組(125478568,)
4個位元組差不多可以表示1G的大小的資料。
七、SocketServer模組
SocketServer的封裝度較高,但是效率比較固定,處理併發的客戶端請求,只改變server端的程式碼,client端的程式碼不變。
SocketServer請求處理程式是預設行為是接收連線,獲取請求,然後關閉連線。因此每次向服務端傳送訊息時都必須重新建立一個新的套接字。
類 | 描述 |
---|---|
BaseServer | 包含核心伺服器功能和mix-in類的鉤子:僅用於推導,這樣不會建立這個類的例項;可以用TCPServer或UDPServer建立類的例項。 |
TCPServer/UDPServer | 基礎網路同步TCP/UDP伺服器 |
BaseRequestHandler | 包含處理服務請求的核心功能:僅用於推導,這樣不會建立這個類的例項;可以用StreamRequestHandler或DatagramRequestHandler建立類的例項。 |
StreamRequestHandler/DatagramRequesthandler | 實現TCP/UDP伺服器的服務處理器 |
7.1 TCPServer+StreamRequestHandler
- StreamRequestHandler類支援像操作檔案那樣操作輸入套接字
- 客戶端和服務端傳送的訊息都必須加上回車和換行符 /r/n
- SockerServer請求處理程式的預設行為是接收連線、獲取請求、關閉連線。
# SocketServer服務端
from socketserver import (TCPServer as TCP, StreamRequestHandler as SRH)
from time import ctime
HOST = '127.0.0.1'
PORT = 9001
ADDR = (HOST, PORT)
class MyRequestHandler(SRH):
def handle(self):
# 列印連線的客戶端的地址
print('...connected from:', self.client_address)
# SocketServer傳送訊息一定是\r\n結尾的,由於這裡接收到客戶端發來的訊息就是以\r\n結尾的,所以不需要加
data = '[%s] %s ' %(ctime(), self.rfile.readline().decode(('utf-8')))
# 傳送訊息
self.wfile.write(data.encode('utf-8'))
print('傳送完畢')
tcpServ = TCP(ADDR, MyRequestHandler)
print('waiting for connection...')
tcpServ.serve_forever()
# SocketServer客戶端
from socket import *
HOST = '127.0.0.1'
PORT = 9001
ADDR = (HOST, PORT)
BUFSIZ = 1024
while True:
tcpCliSock = socket(AF_INET, SOCK_STREAM)
tcpCliSock.connect(ADDR)
data = input('>>>')
if not data:
break
# SocketServer傳送訊息一定是\r\n結尾的
tcpCliSock.send(('%s \r\n'%data).encode('utf-8'))
print('傳送完畢')
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
print(data.strip().decode('utf-8'))
tcpCliSock.close()
7.2 TCPServer+BaseRequestHandler
這與TCPServer+StreamRequestHandler的區別是:通過self.request.recv()和self.request.send()兩個函式來接受和傳送訊息而不是self.rfile.readline和self.rfile.write()。
八、補充:基礎網路協議
九、例子
9.1 TCP檔案上傳
# 服務端
import socket
import json
import struct
sk = socket.socket()
sk.bind(('127.0.0.1', 9001))
sk.listen()
conn , addr = sk.accept()
msg_len = conn.recv(4)
# 4個位元組,去掉前面的0,拿到字典的長度
dic_len = struct.unpack('i', msg_len)[0]
# 接收字典並解碼
jdic = conn.recv(dic_len).decode('utf-8')
# json轉換成普通字典
dic = json.loads(jdic)
print(dic)
with open(dic['filename'],mode='wb')as f:
while dic['filesize']>0:
data = conn.recv(1024)
dic['filesize']-=len(data)
f.write(data)
conn.close()
sk.close()
# 客戶端
import socket
import json
import struct
sk = socket.socket()
sk.bind(('127.0.0.1', 9001))
sk.listen()
conn , addr = sk.accept()
msg_len = conn.recv(4)
# 4個位元組,去掉前面的0,拿到字典的長度
dic_len = struct.unpack('i', msg_len)[0]
# 接收字典並解碼
jdic = conn.recv(dic_len).decode('utf-8')
# json轉換成普通字典
dic = json.loads(jdic)
print(dic)
with open(dic['filename'],mode='wb')as f:
while dic['filesize']>0:
data = conn.recv(1024)
dic['filesize']-=len(data)
f.write(data)
conn.close()
sk.close()
9.2 驗證客戶端的合法性
生成隨機字串
import os
# 生成32位的隨機字串
ret = os.urandom(32)
print(ret)
# 服務端
import os
import socket
import hashlib
secret_key = b'alex_sb'
sk = socket.socket()
sk.bind(('127.0.0.1',9001))
sk.listen()
conn,addr = sk.accept()
# 建立一個隨機的字串,bytes型別
rand = os.urandom(32)
# 傳送隨機字串
conn.send(rand)
# 根據傳送的字串 + secrete key 進行摘要
sha = hashlib.sha1(secret_key)
sha.update(rand)
res = sha.hexdigest()
# 等待接收客戶端的摘要結果
res_client = conn.recv(1024).decode('utf-8')
# 做比對
if res_client == res:
print('是合法的客戶端')
# 如果一致,就顯示是合法的客戶端
# 並可以繼續操作
conn.send(b'hello')
else:
conn.close()
# 如果不一致,應立即關閉連線
# 客戶端
import socket
import hashlib
secret_key = b'alex_sb979'
sk = socket.socket()
sk.connect(('127.0.0.1',9001))
# 接收客戶端傳送的隨機字串
rand = sk.recv(32)
# 根據傳送的字串 + secret key 進行摘要
sha = hashlib.sha1(secret_key)
sha.update(rand)
res = sha.hexdigest()
# 摘要結果傳送回server端
sk.send(res.encode('utf-8'))
# 繼續和server端進行通訊
msg = sk.recv(1024)
print(msg)