1. 程式人生 > 實用技巧 >10.網路程式設計之socket

10.網路程式設計之socket

目錄

一、什麼是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

  1. StreamRequestHandler類支援像操作檔案那樣操作輸入套接字
  2. 客戶端和服務端傳送的訊息都必須加上回車和換行符 /r/n
  3. 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)