一.楔子
你現在已經學會了寫python代碼,假如你寫了兩個python檔案a.py和b.py,分別去運行,你就會發現,這兩個python的檔案分別運行的很好,但是如果這兩個程式之間想要傳遞一個資料,你要怎么做呢?
這個問題以你現在的知識就可以解決了,我們可以創建一個檔案,把a.py想要傳遞的內容寫到檔案中,然后b.py從這個檔案中讀取內容就可以了
但是當你的a.py和b.py分別在不同電腦上的時候,你要怎么辦呢?
類似的機制有計算機網盤,qq等等,我們可以在我們的電腦上和別人聊天,可以在自己的電腦上向網盤中上傳、下載內容,這些都是兩個程式在通信,
二.軟體開發的架構
我們了解的涉及到兩個程式之間通訊的應用大致可以分為兩種:
第一種是應用類:qq、微信、網盤、優酷這一類是屬于需要安裝的桌面應用
第二種是web類:比如百度、知乎、博客園等使用瀏覽器訪問就可以直接使用的應用
這些應用的本質其實都是兩個程式之間的通訊,而這兩個分類又對應了兩個軟體開發的架構~
1.C/S架構
C/S即:Client與Server ,中文意思:客戶端與服務器端架構,這種架構也是從用戶層面(也可以是物理層面)來劃分的,
這里的客戶端一般泛指客戶端應用程式EXE,程式需要先安裝后,才能運行在用戶的電腦上,對用戶的電腦作業系統環境依賴較大
2.B/S架構
B/S即:Browser與Server,中文意思:瀏覽器端與服務器端架構,這種架構是從用戶層面來劃分的,
Browser瀏覽器,其實也是一種Client客戶端,只是這個客戶端不需要大家去安裝什么應用程式,只需在瀏覽器上通過HTTP請求服務器端相關的資源(網頁資源),客戶端Browser瀏覽器就能進行增刪改查,

三.網路基礎
網路基礎
1.一個程式如何在網路上找到另一個程式?
首先,程式必須要啟動,其次,必須有這臺機器的地址,我們都知道我們人的地址大概就是國家\省\市\區\街道\樓\門牌號這樣字,那么每一臺聯網的機器在網路上也有自己的地址,它的地址是怎么表示的呢?
就是使用一串數字來表示的,例如:100.4.5.6
IP地址是指互聯網協議地址(英語:Internet Protocol Address,又譯為網際協議地址),是IP Address的縮寫,IP地址是IP協議提供的一種統一的地址格式,它為互聯網上的每一個網路和每一臺主機分配一個邏輯地址,以此來屏蔽物理地址的差異,
IP地址是一個32位的二進制數,通常被分割為4個“8位二進制數”(也就是4個位元組),IP地址通常用“點分十進制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之間的十進制整數,例:點分十進IP地址(100.4.5.6),實際上是32位二進制數(01100100.00000100.00000101.00000110),
什么是IP地址
"埠"是英文port的意譯,可以認為是設備與外界通訊交流的出口,什么是埠
netstat -aon|findstr "49157"在Windows上查看埠占用
因此ip地址精確到具體的一臺電腦,而埠精確到具體的程式,
2.osi七層模型
引子
須知一個完整的計算機系統是由硬體、作業系統、應用軟體三者組成,具備了這三個條件,一臺計算機系統就可以自己跟自己玩了(打個單機游戲,玩個掃雷啥的)
如果你要跟別人一起玩,那你就需要上網了,什么是互聯網?
互聯網的核心就是由一堆協議組成,協議就是標準,比如全世界人通信的標準是英語,如果把計算機比作人,互聯網協議就是計算機界的英語,所有的計算機都學會了互聯網協議,那所有的計算機都就可以按照統一的標準去收發資訊從而完成通信了,
osi七層模型
人們按照分工不同把互聯網協議從邏輯上劃分了層級:

3.socket概念
socket層

理解socket
Socket是應用層與TCP/IP協議族通信的中間軟體抽象層,它是一組介面,在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket介面后面,對用戶來說,一組簡單的介面就是全部,讓Socket去組織資料,以符合指定的協議,
其實站在你的角度上看,socket就是一個模塊,我們通過呼叫模塊中已經實作的方法建立兩個行程之間的連接和通信, 也有人將socket說成ip+port,因為ip是用來標識互聯網中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程式, 所以我們只要確立了ip和port就能找到一個應用程式,并且使用socket模塊來與之通信,
3.套接字(socket)的發展史
套接字起源于 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix, 因此,有時人們也把套接字稱為“伯克利套接字”或“BSD 套接字”,一開始,套接字被設計用在同 一臺主機上多個應用程式之間的通訊,這也被稱行程間通訊,或 IPC,套接字有兩種(或者稱為有兩個種族),分別是基于檔案型的和基于網路型的,
基于檔案型別的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆檔案,基于檔案的套接字呼叫的就是底層的檔案系統來取資料,兩個套接字行程運行在同一機器,可以通過訪問同一個檔案系統間接完成通信
基于網路型別的套接字家族
套接字家族的名字:AF_INET
(還有AF_INET6被用于ipv6,還有一些其他的地址家族,不過,他們要么是只用于某個平臺,要么就是已經被廢棄,或者是很少被使用,或者是根本沒有實作,所有地址家族中,AF_INET是使用最廣泛的一個,python支持很多種地址家族,但是由于我們只關心網路編程,所以大部分時候我么只使用AF_INET)
4.tcp協議和udp協議
TCP(Transmission Control Protocol)可靠的、面向連接的協議(eg:打電話)、傳輸效率低全雙工通信(發送快取&接收快取)、面向位元組流,使用TCP的應用:Web瀏覽器;電子郵件、檔案傳輸程式,
UDP(User Datagram Protocol)不可靠的、無連接的服務,傳輸效率高(發送前時延小),一對一、一對多、多對一、多對多、面向報文,盡最大努力服務,無擁塞控制,使用UDP的應用:域名系統 (DNS);視頻流;IP語音(VoIP),
我知道說這些你們也不懂,直接上圖,

四.套接字(socket)初使用
基于TCP協議的socket
tcp是基于鏈接的,必須先啟動服務端,然后再啟動客戶端去鏈接服務端
server端
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898)) #把地址系結到套接字
sk.listen() #監聽鏈接
conn,addr = sk.accept() #接受客戶端鏈接
ret = conn.recv(1024) #接收客戶端資訊
print(ret) #列印客戶端資訊
conn.send(b'hi') #向客戶端發送資訊
conn.close() #關閉客戶端套接字
sk.close() #關閉服務器套接字(可選)
client端
import socket
sk = socket.socket() # 創建客戶套接字
sk.connect(('127.0.0.1',8898)) # 嘗試連接服務器
sk.send(b'hello!')
ret = sk.recv(1024) # 對話(發送/接收)
print(ret)
sk.close() # 關閉客戶套接字
問題:有的同學在重啟服務端時可能會遇到

解決方法:
#加入一條socket配置,重用ip和埠
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
sk.bind(('127.0.0.1',8898)) #把地址系結到套接字
sk.listen() #監聽鏈接
conn,addr = sk.accept() #接受客戶端鏈接
ret = conn.recv(1024) #接收客戶端資訊
print(ret) #列印客戶端資訊
conn.send(b'hi') #向客戶端發送資訊
conn.close() #關閉客戶端套接字
sk.close() #關閉服務器套接字(可選)
基于UDP協議的socket
udp是無鏈接的,啟動服務之后可以直接接受訊息,不需要提前建立鏈接
簡單使用
server端
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM) #創建一個服務器的套接字
udp_sk.bind(('127.0.0.1',9000)) #系結服務器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr) # 對話(接收與發送)
udp_sk.close() # 關閉服務器套接字
client端
import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)
qq聊天
#_*_coding:utf-8_*_ import socket ip_port=('127.0.0.1',8081) udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server_sock.bind(ip_port) while True: qq_msg,addr=udp_server_sock.recvfrom(1024) print('來自[%s:%s]的一條訊息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8'))) back_msg=input('回復訊息: ').strip() udp_server_sock.sendto(back_msg.encode('utf-8'),addr)server
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic={ '金老板':('127.0.0.1',8081), '哪吒':('127.0.0.1',8081), 'egg':('127.0.0.1',8081), 'yuan':('127.0.0.1',8081), } while True: qq_name=input('請選擇聊天物件: ').strip() while True: msg=input('請輸入訊息,回車發送,輸入q結束和他的聊天: ').strip() if msg == 'q':break if not msg or not qq_name or qq_name not in qq_name_dic:continue udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name]) back_msg,addr=udp_client_socket.recvfrom(BUFSIZE) print('來自[%s:%s]的一條訊息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8'))) udp_client_socket.close()client
時間服務器
# _*_coding:utf-8_*_ from socket import * from time import strftime ip_port = ('127.0.0.1', 9000) bufsize = 1024 tcp_server = socket(AF_INET, SOCK_DGRAM) tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_server.bind(ip_port) while True: msg, addr = tcp_server.recvfrom(bufsize) print('===>', msg) if not msg: time_fmt = '%Y-%m-%d %X' else: time_fmt = msg.decode('utf-8') back_msg = strftime(time_fmt) tcp_server.sendto(back_msg.encode('utf-8'), addr) tcp_server.close()server
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',9000) bufsize=1024 tcp_client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('請輸入時間格式(例%Y %m %d)>>: ').strip() tcp_client.sendto(msg.encode('utf-8'),ip_port) data=tcp_client.recv(bufsize)client
socket引數的詳解
socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)
創建socket物件的引數說明:
| family | 地址系列應為AF_INET(默認值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS, (AF_UNIX 域實際上是使用本地 socket 檔案來通信) |
| type |
套接字型別應為SOCK_STREAM(默認值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一, SOCK_STREAM 是基于TCP的,有保障的(即能保證資料正確傳送到對方)面向連接的SOCKET,多用于資料傳送, SOCK_DGRAM 是基于UDP的,無保障的面向訊息的socket,多用于在網路上發廣播資訊, |
| proto | 協議號通常為零,可以省略,或者在地址族為AF_CAN的情況下,協議應為CAN_RAW或CAN_BCM之一, |
| fileno |
如果指定了fileno,則其他引數將被忽略,導致帶有指定檔案描述符的套接字回傳, 與socket.fromfd()不同,fileno將回傳相同的套接字,而不是重復的, 這可能有助于使用socket.close()關閉一個獨立的插座, |
五.黏包
黏包現象
讓我們基于tcp先制作一個遠程執行命令的程式(命令ls -l ; lllllll ; pwd)
res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) 的結果的編碼是以當前所在的系統為準的,如果是windows,那么res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼 且只能從管道里讀一次結果
同時執行多條命令之后,得到的結果很可能只有一部分,在執行其他命令的時候又接收到之前執行的另外一部分結果,這種顯現就是黏包,
基于tcp協議實作的黏包
#_*_coding:utf-8_*_ from socket import * import subprocess ip_port=('127.0.0.1',8888) BUFSIZE=1024 tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) while True: conn,addr=tcp_socket_server.accept() print('客戶端',addr) while True: cmd=conn.recv(BUFSIZE) if len(cmd) == 0:break res=subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stderr=res.stderr.read() stdout=res.stdout.read() conn.send(stderr) conn.send(stdout)tcp - server
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8888) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) act_res=s.recv(BUFSIZE) print(act_res.decode('utf-8'),end='')tcp - client
基于udp協議實作的黏包
#_*_coding:utf-8_*_ from socket import * import subprocess ip_port=('127.0.0.1',9000) bufsize=1024 udp_server=socket(AF_INET,SOCK_DGRAM) udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) udp_server.bind(ip_port) while True: #收訊息 cmd,addr=udp_server.recvfrom(bufsize) print('用戶命令----->',cmd) #邏輯處理 res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE) stderr=res.stderr.read() stdout=res.stdout.read() #發訊息 udp_server.sendto(stderr,addr) udp_server.sendto(stdout,addr) udp_server.close()udp - server
from socket import * ip_port=('127.0.0.1',9000) bufsize=1024 udp_client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) err,addr=udp_client.recvfrom(bufsize) out,addr=udp_client.recvfrom(bufsize) if err: print('error : %s'%err.decode('utf-8'),end='') if out: print(out.decode('utf-8'), end='')udp - client
注意:只有TCP有粘包現象,UDP永遠不會粘包
黏包成因
TCP協議中的資料傳遞
tcp協議的拆包機制
當發送端緩沖區的長度大于網卡的MTU時,tcp會將這次發送的資料拆成幾個資料包發送出去, MTU是Maximum Transmission Unit的縮寫,意思是網路上傳送的最大資料包,MTU的單位是位元組, 大部分網路設備的MTU都是1500,如果本機的MTU比網關的MTU大,大的資料包就會被拆開來傳送,這樣會產生很多資料包碎片,增加丟包率,降低網路速度,
面向流的通信特點和Nagle演算法
TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務, 收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle演算法),將多次間隔較小且資料量小的資料,合并成一個大的資料塊,然后進行封包, 這樣,接收端,就難于分辨出來了,必須提供科學的拆包機制, 即面向流的通信是無訊息保護邊界的, 對于空訊息:tcp是基于資料流的,于是收發的訊息不能為空,這就需要在客戶端和服務端都添加空訊息的處理機制,防止程式卡住,而udp是基于資料報的,即便是你輸入的是空內容(直接回車),也可以被發送,udp協議會幫你封裝上訊息頭發送過去, 可靠黏包的tcp協議:tcp的協議資料不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容,資料是可靠的,但是會粘包,
基于tcp協議特點的黏包現象成因



發送端可以是一K一K地發送資料,而接收端的應用程式可以兩K兩K地提走資料,當然也有可能一次提走3K或6K資料,或者一次只提走幾個位元組的資料,
也就是說,應用程式所看到的資料是一個整體,或說是一個流(stream),一條訊息有多少位元組對應用程式是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因,
而UDP是面向訊息的協議,每個UDP段都是一條訊息,應用程式必須以訊息為單位提取資料,不能一次提取任意位元組的資料,這一點和TCP是很不同的,
怎樣定義訊息呢?可以認為對方一次性write/send的資料為一個訊息,需要明白的是當對方send一條資訊的時候,無論底層怎樣分段分片,TCP協議層會把構成整條訊息的數據段排序完成后才呈現在內核緩沖區,
socket資料傳輸程序中的用戶態與內核態說明
例如基于tcp的套接字客戶端往服務端上傳檔案,發送時檔案內容是按照一段一段的位元組流發送的,在接收方看了,根本不知道該檔案的位元組流從何處開始,在何處結束
此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的資料后才發送一個TCP段,若連續幾次需要send的資料都很少,通常TCP會根據優化演算法把這些資料合成一個TCP段后一次發送出去,這樣接收方就收到了粘包資料,
UDP不會發生黏包
UDP(user datagram protocol,用戶資料報協議)是無連接的,面向訊息的,提供高效率服務, 不會使用塊的合并優化演算法,, 由于UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了訊息頭(訊息來源地址,埠等資訊),這樣,對于接收端來說,就容易進行區分處理了, 即面向訊息的通信是有訊息保護邊界的, 對于空訊息:tcp是基于資料流的,于是收發的訊息不能為空,這就需要在客戶端和服務端都添加空訊息的處理機制,防止程式卡住,而udp是基于資料報的,即便是你輸入的是空內容(直接回車),也可以被發送,udp協議會幫你封裝上訊息頭發送過去, 不可靠不黏包的udp協議:udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個位元組的資料就算完成,若是y;x資料就丟失,這意味著udp根本不會粘包,但是會丟資料,不可靠,
補充說明:
用UDP協議發送時,用sendto函式最大能發送資料的長度為:65535- IP頭(20) – UDP頭(8)=65507位元組,用sendto函式發送資料時,如果發送資料長度大于該值,則函式會回傳錯誤,(丟棄這個包,不進行發送)
用TCP協議發送時,由于TCP是資料流協議,因此不存在包大小的限制(暫不考慮緩沖區的大小),這是指在用send函式時,資料長度引數不受限制,而實際上,所指定的這段資料并不一定會一次性發送出去,如果這段資料比較長,會被分段發送,如果比較短,可能會等待和下一次資料一起發送,
udp和tcp一次發送資料長度的限制
會發生黏包的兩種情況
情況一 發送方的快取機制
發送端需要等緩沖區滿才發送出去,造成粘包(發送資料時間間隔很短,資料了很小,會合到一起,產生粘包)
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()服務端
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send('egg'.encode('utf-8'))客戶端
情況二 接收方的快取機制
接收方不及時接識訓沖區的包,造成多個包接收(客戶端發送了一段資料,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的資料,產生粘包)
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(2) #一次沒有收完整 data2=conn.recv(10)#下次收的時候,會先取舊的資料,然后取新的 print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()服務端
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello egg'.encode('utf-8'))客戶端
總結
黏包現象只發生在tcp協議中:
1.從表面上看,黏包問題主要是因為發送方和接收方的快取機制、tcp協議面向流通信的特點,
2.實際上,主要還是因為接收方不知道訊息之間的界限,不知道一次性提取多少位元組的資料所造成的
黏包的解決方案
解決方案一
問題的根源在于,接收端不知道發送端將要傳送的位元組流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送資料前,把自己將要發送的位元組流總大小讓接收端知曉,然后接收端來一個死回圈接收完所有資料,

#_*_coding:utf-8_*_ import socket,subprocess ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn,addr=s.accept() print('客戶端',addr) while True: msg=conn.recv(1024) if not msg:break res=subprocess.Popen(msg.decode('utf-8'),shell=True,\ stdin=subprocess.PIPE,\ stderr=subprocess.PIPE,\ stdout=subprocess.PIPE) err=res.stderr.read() if err: ret=err else: ret=res.stdout.read() data_length=len(ret) conn.send(str(data_length).encode('utf-8')) data=conn.recv(1024).decode('utf-8') if data =https://www.cnblogs.com/Golanguage/p/= 'recv_ready': conn.sendall(ret) conn.close()服務端
#_*_coding:utf-8_*_ import socket,time s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) length=int(s.recv(1024).decode('utf-8')) s.send('recv_ready'.encode('utf-8')) send_size=0 recv_size=0 data=b'' while recv_size < length: data+=s.recv(1024) recv_size+=len(data) print(data.decode('utf-8'))客戶端
存在的問題:
程式的運行速度遠快于網路傳輸速度,所以在發送一段位元組前,先用send去發送該位元組流長度,這種方式會放大網路延遲帶來的性能損耗
解決方案進階
剛剛的方法,問題在于我們我們在發送
我們可以借助一個模塊,這個模塊可以把要發送的資料長度轉換成固定長度的位元組,這樣客戶端每次接收訊息之前只要先接受這個固定長度位元組的內容看一看接下來要接收的資訊大小,那么最終接受的資料只要達到這個值就停止,就能剛好不多不少的接收完整的資料了,
struct模塊
該模塊可以把一個型別,如數字,轉成固定長度的bytes
>>> struct.pack('i',1111111111111)
struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是范圍

import json,struct
#假設通過客戶端上傳1T:1073741824000的檔案a.txt
#為避免粘包,必須自定制報頭
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T資料,檔案路徑和md5值
#為了該報頭能傳送,需要序列化并且轉為bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并轉成bytes,用于傳輸
#為了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個位元組
head_len_bytes=struct.pack('i',len(head_bytes)) #這4個位元組里只包含了一個數字,該數字是報頭的長度
#客戶端開始發送
conn.send(head_len_bytes) #先發報頭的長度,4個bytes
conn.send(head_bytes) #再發報頭的位元組格式
conn.sendall(檔案內容) #然后發真實內容的位元組格式
#服務端開始接收
head_len_bytes=s.recv(4) #先收報頭4個bytes,得到報頭長度的位元組格式
x=struct.unpack('i',head_len_bytes)[0] #提取報頭的長度
head_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式
header=json.loads(json.dumps(header)) #提取報頭
#最后根據報頭的內容提取真實的資料,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)
#_*_coding:utf-8_*_ #http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html __author__ = 'Linhaifeng' import struct import binascii import ctypes values1 = (1, 'abc'.encode('utf-8'), 2.7) values2 = ('defg'.encode('utf-8'),101) s1 = struct.Struct('I3sf') s2 = struct.Struct('4sI') print(s1.size,s2.size) prebuffer=ctypes.create_string_buffer(s1.size+s2.size) print('Before : ',binascii.hexlify(prebuffer)) # t=binascii.hexlify('asdfaf'.encode('utf-8')) # print(t) s1.pack_into(prebuffer,0,*values1) s2.pack_into(prebuffer,s1.size,*values2) print('After pack',binascii.hexlify(prebuffer)) print(s1.unpack_from(prebuffer,0)) print(s2.unpack_from(prebuffer,s1.size)) s3=struct.Struct('ii') s3.pack_into(prebuffer,0,123,123) print('After pack',binascii.hexlify(prebuffer)) print(s3.unpack_from(prebuffer,0))關于struct的詳細用法
使用struct解決黏包
借助struct模塊,我們知道長度數字可以被轉換成一個標準大小的4位元組數字,因此可以利用這個特點來預先發送資料長度,
| 發送時 | 接收時 |
| 先發送struct轉換好的資料長度4位元組 | 先接受4個位元組使用struct轉換成數字來獲取要接收的資料長度 |
| 再發送資料 | 再按照長度接收資料 |
import socket,struct,json import subprocess phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080)) phone.listen(5) while True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break print('cmd: %s' %cmd) res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err=res.stderr.read() print(err) if err: back_msg=err else: back_msg=res.stdout.read() conn.send(struct.pack('i',len(back_msg))) #先發back_msg的長度 conn.sendall(back_msg) #在發真實的內容 conn.close()服務端(自定制報頭)
#_*_coding:utf-8_*_ import socket,time,struct s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) l=s.recv(4) x=struct.unpack('i',l)[0] print(type(x),x) # print(struct.unpack('I',l)) r_s=0 data=b'' while r_s < x: r_d=s.recv(1024) data+=r_d r_s+=len(r_d) # print(data.decode('utf-8')) print(data.decode('gbk')) #windows默認gbk編碼客戶端(自定制報頭)
我們還可以把報頭做成字典,字典里包含將要發送的真實資料的詳細資訊,然后json序列化,然后用struck將序列化后的資料長度打包成4個位元組(4個自己足夠用了)
| 發送時 | 接收時 |
|
先發報頭長度 |
先收報頭長度,用struct取出來 |
| 再編碼報頭內容然后發送 | 根據取出的長度收取報頭內容,然后解碼,反序列化 |
| 最后發真實內容 | 從反序列化的結果中取出待取資料的詳細資訊,然后去取真實的資料內容 |
import socket,struct,json import subprocess phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080)) phone.listen(5) while True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break print('cmd: %s' %cmd) res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err=res.stderr.read() print(err) if err: back_msg=err else: back_msg=res.stdout.read() headers={'data_size':len(back_msg)} head_json=json.dumps(headers) head_json_bytes=bytes(head_json,encoding='utf-8') conn.send(struct.pack('i',len(head_json_bytes))) #先發報頭的長度 conn.send(head_json_bytes) #再發報頭 conn.sendall(back_msg) #在發真實的內容 conn.close()服務端:定制稍微復雜一點的報頭
from socket import * import struct,json ip_port=('127.0.0.1',8080) client=socket(AF_INET,SOCK_STREAM) client.connect(ip_port) while True: cmd=input('>>: ') if not cmd:continue client.send(bytes(cmd,encoding='utf-8')) head=client.recv(4) head_json_len=struct.unpack('i',head)[0] head_json=json.loads(client.recv(head_json_len).decode('utf-8')) data_len=head_json['data_size'] recv_size=0 recv_data=b'' while recv_size < data_len: recv_data+=client.recv(1024) recv_size+=len(recv_data) print(recv_data.decode('utf-8')) #print(recv_data.decode('gbk')) #windows默認gbk編碼客戶端
FTP作業:上傳下載檔案
import socket import struct import json import subprocess import os class MYTCPServer: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 server_dir='file_upload' def __init__(self, server_address, bind_and_activate=True): """Constructor. May be extended, do not override.""" self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: try: self.server_bind() self.server_activate() except: self.server_close() raise def server_bind(self): """Called by constructor to bind the socket. """ if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) self.server_address = self.socket.getsockname() def server_activate(self): """Called by constructor to activate the server. """ self.socket.listen(self.request_queue_size) def server_close(self): """Called to clean-up the server. """ self.socket.close() def get_request(self): """Get the request and client address from the socket. """ return self.socket.accept() def close_request(self, request): """Called to clean up an individual request.""" request.close() def run(self): while True: self.conn,self.client_addr=self.get_request() print('from client ',self.client_addr) while True: try: head_struct = self.conn.recv(4) if not head_struct:break head_len = struct.unpack('i', head_struct)[0] head_json = self.conn.recv(head_len).decode(self.coding) head_dic = json.loads(head_json) print(head_dic) #head_dic={'cmd':'put','filename':'a.txt','filesize':123123} cmd=head_dic['cmd'] if hasattr(self,cmd): func=getattr(self,cmd) func(head_dic) except Exception: break def put(self,args): file_path=os.path.normpath(os.path.join( self.server_dir, args['filename'] )) filesize=args['filesize'] recv_size=0 print('----->',file_path) with open(file_path,'wb') as f: while recv_size < filesize: recv_data=self.conn.recv(self.max_packet_size) f.write(recv_data) recv_size+=len(recv_data) print('recvsize:%s filesize:%s' %(recv_size,filesize)) tcpserver1=MYTCPServer(('127.0.0.1',8080)) tcpserver1.run() #下列代碼與本題無關 class MYUDPServer: """UDP server class.""" address_family = socket.AF_INET socket_type = socket.SOCK_DGRAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' def get_request(self): data, client_addr = self.socket.recvfrom(self.max_packet_size) return (data, self.socket), client_addr def server_activate(self): # No need to call listen() for UDP. pass def shutdown_request(self, request): # No need to shutdown anything. self.close_request(request) def close_request(self, request): # No need to close anything. pass服務端
import socket import struct import json import os class MYTCPClient: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 def __init__(self, server_address, connect=True): self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if connect: try: self.client_connect() except: self.client_close() raise def client_connect(self): self.socket.connect(self.server_address) def client_close(self): self.socket.close() def run(self): while True: inp=input(">>: ").strip() if not inp:continue l=inp.split() cmd=l[0] if hasattr(self,cmd): func=getattr(self,cmd) func(l) def put(self,args): cmd=args[0] filename=args[1] if not os.path.isfile(filename): print('file:%s is not exists' %filename) return else: filesize=os.path.getsize(filename) head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize} print(head_dic) head_json=json.dumps(head_dic) head_json_bytes=bytes(head_json,encoding=self.coding) head_struct=struct.pack('i',len(head_json_bytes)) self.socket.send(head_struct) self.socket.send(head_json_bytes) send_size=0 with open(filename,'rb') as f: for line in f: self.socket.send(line) send_size+=len(line) print(send_size) else: print('upload successful') client=MYTCPClient(('127.0.0.1',8080)) client.run()客戶端
六.socket的更多方法介紹
服務端套接字函式
s.bind() 系結(主機,埠號)到套接字
s.listen() 開始TCP監聽
s.accept() 被動接受TCP客戶的連接,(阻塞式)等待連接的到來
客戶端套接字函式
s.connect() 主動初始化TCP服務器連接
s.connect_ex() connect()函式的擴展版本,出錯時回傳出錯碼,而不是拋出例外
公共用途的套接字函式
s.recv() 接收TCP資料
s.send() 發送TCP資料
s.sendall() 發送TCP資料
s.recvfrom() 接收UDP資料
s.sendto() 發送UDP資料
s.getpeername() 連接到當前套接字的遠端的地址
s.getsockname() 當前套接字的地址
s.getsockopt() 回傳指定套接字的引數
s.setsockopt() 設定指定套接字的引數
s.close() 關閉套接字
面向鎖的套接字方法
s.setblocking() 設定套接字的阻塞與非阻塞模式
s.settimeout() 設定阻塞套接字操作的超時時間
s.gettimeout() 得到阻塞套接字操作的超時時間
面向檔案的套接字的函式
s.fileno() 套接字的檔案描述符
s.makefile() 創建一個與該套接字相關的檔案
更多方法
官方檔案對socket模塊下的socket.send()和socket.sendall()解釋如下: socket.send(string[, flags]) Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Returns the number of bytes sent. Applications are responsible for checking that all data has been sent; if only some of the data was transmitted, the application needs to attempt delivery of the remaining data. send()的回傳值是發送的位元組數量,這個數量值可能小于要發送的string的位元組數,也就是說可能無法發送string中所有的資料,如果有錯誤則會拋出例外, – socket.sendall(string[, flags]) Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Unlike send(), this method continues to send data from string until either all data has been sent or an error occurs. None is returned on success. On error, an exception is raised, and there is no way to determine how much data, if any, was successfully sent. 嘗試發送string的所有資料,成功則回傳None,失敗則拋出例外, 故,下面兩段代碼是等價的: #sock.sendall('Hello world\n') #buffer = 'Hello world\n' #while buffer: # bytes = sock.send(buffer) # buffer = buffer[bytes:]send和sendall方法
七.驗證客戶端鏈接的合法性
如果你想在分布式系統中實作一個簡單的客戶端鏈接認證功能,又不像SSL那么復雜,那么利用hmac+加鹽的方式來實作
#_*_coding:utf-8_*_ from socket import * import hmac,os secret_key=b'linhaifeng bang bang bang' def conn_auth(conn): ''' 認證客戶端鏈接 :param conn: :return: ''' print('開始驗證新鏈接的合法性') msg=os.urandom(32) conn.sendall(msg) h=hmac.new(secret_key,msg) digest=h.digest() respone=conn.recv(len(digest)) return hmac.compare_digest(respone,digest) def data_handler(conn,bufsize=1024): if not conn_auth(conn): print('該鏈接不合法,關閉') conn.close() return print('鏈接合法,開始通信') while True: data=conn.recv(bufsize) if not data:break conn.sendall(data.upper()) def server_handler(ip_port,bufsize,backlog=5): ''' 只處理鏈接 :param ip_port: :return: ''' tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(backlog) while True: conn,addr=tcp_socket_server.accept() print('新連接[%s:%s]' %(addr[0],addr[1])) data_handler(conn,bufsize) if __name__ == '__main__': ip_port=('127.0.0.1',9999) bufsize=1024 server_handler(ip_port,bufsize)服務端
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' from socket import * import hmac,os secret_key=b'linhaifeng bang bang bang' def conn_auth(conn): ''' 驗證客戶端到服務器的鏈接 :param conn: :return: ''' msg=conn.recv(32) h=hmac.new(secret_key,msg) digest=h.digest() conn.sendall(digest) def client_handler(ip_port,bufsize=1024): tcp_socket_client=socket(AF_INET,SOCK_STREAM) tcp_socket_client.connect(ip_port) conn_auth(tcp_socket_client) while True: data=input('>>: ').strip() if not data:continue if data =https://www.cnblogs.com/Golanguage/p/= 'quit':break tcp_socket_client.sendall(data.encode('utf-8')) respone=tcp_socket_client.recv(bufsize) print(respone.decode('utf-8')) tcp_socket_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',9999) bufsize=1024 client_handler(ip_port,bufsize)客戶端(合法)
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' from socket import * def client_handler(ip_port,bufsize=1024): tcp_socket_client=socket(AF_INET,SOCK_STREAM) tcp_socket_client.connect(ip_port) while True: data=input('>>: ').strip() if not data:continue if data =https://www.cnblogs.com/Golanguage/p/= 'quit':break tcp_socket_client.sendall(data.encode('utf-8')) respone=tcp_socket_client.recv(bufsize) print(respone.decode('utf-8')) tcp_socket_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',9999) bufsize=1024 client_handler(ip_port,bufsize)客戶端(非法:不知道加密方式)
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' from socket import * import hmac,os secret_key=b'linhaifeng bang bang bang1111' def conn_auth(conn): ''' 驗證客戶端到服務器的鏈接 :param conn: :return: ''' msg=conn.recv(32) h=hmac.new(secret_key,msg) digest=h.digest() conn.sendall(digest) def client_handler(ip_port,bufsize=1024): tcp_socket_client=socket(AF_INET,SOCK_STREAM) tcp_socket_client.connect(ip_port) conn_auth(tcp_socket_client) while True: data=input('>>: ').strip() if not data:continue if data =https://www.cnblogs.com/Golanguage/p/= 'quit':break tcp_socket_client.sendall(data.encode('utf-8')) respone=tcp_socket_client.recv(bufsize) print(respone.decode('utf-8')) tcp_socket_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',9999) bufsize=1024 client_handler(ip_port,bufsize)客戶端(非法:不知道secret_key)
八.socketserver
解讀socketserver原始碼 —— http://www.cnblogs.com/Eva-J/p/5081851.html
import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): self.data = self.request.recv(1024).strip() print("{} wrote:".format(self.client_address[0])) print(self.data) self.request.sendall(self.data.upper()) if __name__ == "__main__": HOST, PORT = "127.0.0.1", 9999 # 設定allow_reuse_address允許服務器重用地址 socketserver.TCPServer.allow_reuse_address = True # 創建一個server, 將服務地址系結到127.0.0.1:9999 server = socketserver.TCPServer((HOST, PORT),Myserver) # 讓server永遠運行下去,除非強制停止程式 server.serve_forever()server端
import socket HOST, PORT = "127.0.0.1", 9999 data = "hello" # 創建一個socket鏈接,SOCK_STREAM代表使用TCP協議 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect((HOST, PORT)) # 鏈接到客戶端 sock.sendall(bytes(data + "\n", "utf-8")) # 向服務端發送資料 received = str(sock.recv(1024), "utf-8")# 從服務端接收資料 print("Sent: {}".format(data)) print("Received: {}".format(received))client
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/188616.html
標籤:Python
