一.客戶端/服務器架構
1.C/S架構:
(1)硬件C/S架構(打印機)
(2)軟件C/S架構(web服務)
2.生活中的C/S架構:
飯店是S端,所有食客是C端
3.C/S架構與socket的關系:
socke就是為了完成C/S架構的開發
二.互聯網協議osi七層
1.一個完整的計算機系統由硬件,操作系統,應用軟件三者組成,具備了這三個條件,一台計算機系統就可以自己跟自己玩了,如果要跟別人一起玩,就需要上網,互聯網的核心就是由一堆協議組成,協議就是標准,全世界人通信的標准是英語,如果把計算機比作人,互聯網協議就是計算機界的英語。所有的計算機都學會了互聯網協議,那所有的計算機都可以按照統一的標准去收發信息從而完成通信了。人們按照分工不同把互聯網協議從邏輯上划分了層級。
第一層物理層:目的是傳輸的連接介質(光纜,雙絞線,無線電波),物理層就可以發送電信號010101
第二層數據鏈路層:負責把發送的電信號010101分組后電信號就有意義了,以太網協議,包含報頭(原mac和目標mac)和數據部分,有了原mac和目標mac就可以基於廣播的方式發送(mac標識在這個局域網的哪一個位置)
第三層網絡層:IP協議標識一個網絡的(IP在標識不同的子網在哪里),發送方式首先通過IP地址跟子網掩碼地址計算得到一個自己的網絡地址和目標網絡地址,如果在一個子網里面可直接通信,如果不在一個子網里這個包就會送給網關,網關在轉發到一樣的子網里
第四層傳輸層:基於TCP和UDP協議端口方式
TCP協議三次握手和四次揮手
(1)三次握手:是你得讓對方知道你已經知道他知道,確定我和對方都准備好了再傳輸數據
發起一次握手:客戶端與服務器發生連接首先要發syn包請求到達服務器端(syn包:原IP,目標IP,原端口,目標端口)
收到一次握手:當服務器端服務器收到syn之后,他的狀態就會轉變成SYN_RECV(指的是某一個連接) 服務器會創建一個socket_buff
發起二次握手:服務器會回一個包syn+ack
收到二次握手:客戶端收到包之后,客戶端已經相應了,客戶端回第三個包ack發起三次握手
收到三次握手:當服務器收到第三個包之建立好3次握手狀態就會變成establi
(2)傳輸上層數據:
當兩端只有建立establi之后,客戶端才能傳輸上層數據
session建立TCP/IP會話
發起一個請求要index.html文件----有數據服務器返回數據
(3)四次斷開:四次斷開時客戶端和服務器都可以發起斷開,因為會牽扯到數據傳輸所以要四次斷開(誰先發完包誰就主動發起斷開連接)
發起一次斷開:客戶端發起了一個fin請求給服務端,客戶端會進入fin_wait_1狀態(主動斷開連接請求)
發起二,三次斷開:服務器收到fin他會把自己的狀態設置成CLOSE_WAIT,他給客戶端回一個ack,代表收到fin這個事(被動斷開一端會出現CLOSE_WAIT)
發起四次斷開:客戶端收到后會把狀態設置成FIN_WAIT2,等待服務器最后發送的FIN,等收到最后的FIN,狀態會變成TIME_WAIT(主動斷開的一端會出現TIME_WAIT),當客戶收到fin回一個ack(TIME_WAIT默認停留一分鍾 等ack數據包)到此整個通訊結束
第五層會話層:解除或建立與其他接點的聯系
第六層表示層:數據格式化,代碼轉換,數據加密
第七層應用層:本機開啟一個軟件會監聽這個端口(跑TCP或UDP協議),端口會跟IP地址還有mac信息相綁定標識了哪一個子網當中一個程序
三.socket
1.socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口在設計模式中,socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在socket接口的后面,對於用戶來說,一組簡單的接口就是全部,讓socket去組織數據,以符合指定的協議,所以我們無需深入理解TCP/udp協議,socket已經為我們封裝好了,我們只需要遵循socket的規定去編程,寫出的程序自然就是遵循tcp/udp標准的
2.socket的五個協議:組成一個邏輯上的文件
(1)原端口-sport
(2)原IP-sip
(3)目標端口-dport:對端主機交給哪個進程的
(4)目標IP-dip
(5)協議-tcp/ip
只要其中任意一個發生改變就不是同一個socket
四.套接字發展及分類
1.一開始,套接字被設計用在同一台主機上多個應用程序之間通訊,這也被稱進程間通訊或IPC。套接字有倆種(或者稱為有倆個種族),分別是基於文件型的和基於網絡型的
2.基於文件類型的套接字家族:AF_UNIX
unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,倆個套接字進程運行在同一機器,可以通過訪問同一個文件系統間接完成通信
3.基於網絡類型的套接字家族:AF_INET
AF_INET是一種廣泛的一個,python支持多種地址家族,但是由於無名指關心網絡編程,所以大部分時候我們只使用AF_INET
五.TCP/IP 套接字
1.結合現實生活的工作流程
TCP服務端 TCP客戶端 socket()--買手機 socket() bind() --綁定一個手機卡 lister()--開機 accept()--等電話,拿到一個電話鏈接 connect()撥電話把請求給服務端accept() read() --收消息 write()發消息給服務端read進行一系列處理 write() --發消息 服務端回應數據給客戶端read(),再執行write close() --斷開電話鏈接 close() --關機 客戶端關閉鏈接close()
2.socket()模塊函數用法
import socket socket.socket(socket_family,socket_type,protocal=0) #socket_family可以是AF_UNIX或AF_INET。socket_type可以是SOCK_STREAM或SOCK_DGRAM。protocol一般不填,默認值為0 #獲取tcp/ip套接字 tcpSock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #獲取udp/ip套接字 udpSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
(1)服務端套接字函數:
s.bind():綁定(主機,端口號)到套接字
s.listen():開始TCP監聽
saccept():被動接受TCP客戶端的連接,(阻塞式)等待連接的到來
(2)客戶端套接字函數
s.connect():主動初始化TCP服務器連接
s.connect_ex():connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
(3)公共用途的套接字函數
s.recv():接收TCP數據
s.send():發送TCP數據(send在待發送數據量大於己端緩存區剩余空間時,數據丟失,不會發完)
s.sendall():發送完整的TCP數據(本質就是循環調用send,sendall在待發數據量大於己端緩存區剩余空間時,數據不丟失,循環調用send直到發完)
s.recvfrom():接收UDP數據
s.sendto():發送UDP數據
s.getpeername():連接到當前套接字的遠端的地址
s.getsockname():當前套接字的地址
s.getsockopt():返回指定套接字的參數
s.setsockopt():設置指定套接字的參數
s.close():關閉套接字
(4)面向鎖的套接字方法
s.setblocking()設置套接字的阻塞與非阻塞模式
s.settimeout()設置阻塞套接字操作的超時時間
s.gettimeout()得到阻塞套接字操作的超時時間
(5)面向文件的套接字的函數
s.fileno():套接字的文件描述符
s.makefile():創建一個與該套接字相關的文件
3.用socket方式創建一個服務端和一個客戶端使兩者進行通信
服務端:
import socket #導入socket模塊 #建立三次握手 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #第一步:socket.socket產生一個對象傳倆個參數(socket.AF_INET基於網絡通訊,socket.SOCK_STREAM表TCP協議)給phone創建套接字 phone.bind(('127.0.0.1',8000)) #第二步:把IP地址和訪問和端綁定到套接字 phone.listen(5) #第三步:監聽鏈接listen(5)最多可以有五個建立好三次握手后的backlog(半連接池)等着,后面的需要排隊等着 conn,addr=phone.accept() #第四步:phone.accept()相當於拿到了TCP三次握手的結果是個元祖解壓給給conn(三次握手的連接)和addr #數據傳輸,基於TCP三次握手建立好的雙向連接才能完成數據傳輸,收發消息基於網絡方式二進制方式 msg=conn.recv(1024) #第八步:收消息 print('客戶端發來的消息是: ',msg) conn.send(msg.upper()) #第九步:服務端給客戶端回一個大寫的msg #四次揮手 conn.close() #第十一步:關閉三次握手連接,觸發的是四次揮手 phone.close() #第十二步:關閉socket,把這個程序關掉
客戶端:
import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #第五步:socket.socket產生一個對象傳倆個參數(socket.AF_INET基於網絡通訊,socket.SOCK_STREAM表TCP協議)給phone phone.connect(('127.0.0.1',8000)) #第六步:找到服務端的IP和端口 phone.send('hello'.encode('utf-8')) #第七步:客戶端發要把消息hello進行二進制編碼給服務端的msg data=phone.recv(1024) #第十步:客戶端接收服務端消息打印 print('客戶端收到服務端的發來的消息:',data)
服務端打印:
客戶端發來的消息是: b'hello'
客戶端打印:
客戶端收到服務端的發來的消息: b'HELLO'
4.基於TCP套接字的通訊流程:創建服務端循環鏈接請求倆台客戶度收發發消息
服務端:
#import socket from socket import * #由於socket模塊中有太多的屬性。所以使用from module import *語句。就可以把socket模塊里的所有屬性都帶到我們的命名空間里,這樣能大幅減少我們的代碼
#把變量提取出來 ip_port=('127.0.0.1',8080) back_log=5 #半連接池最多可以有5個建立好三次握手后的連接等待 buffer_size=1024 #1024代表接收字節 tcp_server=socket(AF_INET,SOCK_STREAM) #第一步:產生一個對象傳倆個參數(socket.AF_INET基於網絡通訊,socket.SOCK_STREAM表TCP協議)給tcp_server創建服務器套接字 tcp_server.bind(ip_port) #第二步:把IP地址和訪問和端口號綁定到套接字 tcp_server.listen(back_log) #第三步:監聽鏈接,listen(5)最多可以有五個建立好三次握手后的backlog(半連接池)等着,后面的需要排隊等着 while True: #第四步:服務端做連接循環的接,可以做到接收多個人發的連接 print('服務端開始運行了') conn,addr=tcp_server.accept() #第五步:tcp_server.accept()相當於拿到了TCP三次握手的結果是個元祖解壓給給conn(三次握手的連接)和addr服務端阻塞 print('雙向鏈接是',conn) #打印conn: print('客戶端地址',addr) #打印addr: while True: #第六步:給收消息和發消息加上通訊循環就可以多次收發消息 try: #做一個異常處理 data=conn.recv(buffer_size) #第七步:服務端收客戶端消息,recv是用戶態應用程序發起的(網卡來接收消息交給操作系統到內核態內存,應用程序從里面取,如果是空會卡住) print('客戶端發來的消息是',data.decode('utf-8')) conn.send(data.upper()) #第八步:服務端發送data字節格式通過upper()轉大寫回給客戶端 except Exception: #當客戶端斷掉不要影響程序的中斷 break #跳出當前客戶端收發消息這個循環,跳到服務端循環接消息的位置 conn.close() #第九步:關閉三次握手連接,觸發的是四次揮手,關閉客戶端套接字 tcp_server.close() #第十步:關閉socket,把這個程序關掉,關閉服務器套接字
客戶端1:
#import socket from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) #第一步:客戶端產生一個對象傳倆個參數(socket.AF_INET基於網絡通訊,socket.SOCK_STREAM表TCP協議)給tcp_client創建客戶端套接字 tcp_client.connect(ip_port) #第二步:客戶端連接服務器端的IP和端口 while True: #第三步:給發消息和收消息加上循環可以循環發收消息 msg=input('>>: ').strip() #第四步:客戶端讓用戶輸入方式發消息 if not msg:continue #第五步:客戶端做判斷如果輸入為空從新輸入 tcp_client.send(msg.encode('utf-8')) #第六步:客戶端把用戶輸入的消息進行二進制編碼給服務端的msg(socket發消息會從用戶態內存send給內核態內存,發到內核態的內存由操作系統接收,操作系統操作網卡發送出去) print('客戶端已經發送消息') data=tcp_client.recv(buffer_size) #第七步:客戶端接收服務端字節格式 print('收到服務端發來的消息',data.decode('utf-8')) #通過解碼看服務端發送的消息 tcp_client.close() #第八步:關閉客戶端套接字
客戶端2:
#import socket from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) #第一步:客戶端產生一個對象傳倆個參數(socket.AF_INET基於網絡通訊,socket.SOCK_STREAM表TCP協議,流式套接字)給tcp_client創建客戶端套接字 tcp_client.connect(ip_port) #第二步:客戶端連接服務器端的IP和端口 while True: #第三步:給發消息和收消息加上循環可以循環發收消息 msg=input('>>: ').strip() #第四步:客戶端讓用戶輸入方式發消息 if not msg:continue #第五步:客戶端做判斷如果輸入為空從新輸入 tcp_client.send(msg.encode('utf-8')) #第六步:客戶端把用戶輸入的消息進行二進制編碼給服務端的msg(socket發消息會從用戶態內存send給內核態內存,發到內核態的內存由操作系統接收,操作系統操作網卡發送出去) print('客戶端已經發送消息') data=tcp_client.recv(buffer_size) #第七步:客戶端接收服務端字節格式 print('收到服務端發來的消息',data.decode('utf-8')) #通過解碼看服務端發送的消息 tcp_client.close() #第八步:關閉客戶端套接字
當客戶端1輸入>>: xiaoxi
客戶端1返回:
客戶端已經發送消息
收到服務端發來的消息 XIAOXI
服務端返回:
服務端開始運行了
雙向鏈接是 <socket.socket fd=372, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 60931)>
客戶端地址 ('127.0.0.1', 60931)
客戶端發來的消息是 xiaoxi
當客戶端1斷開連接且客戶端2輸入:>>: daxi
客戶端2返回:
客戶端已經發送消息
收到服務端發來的消息 DAXI
服務端返回:
服務端開始運行了
雙向鏈接是 <socket.socket fd=368, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 60932)>
客戶端地址 ('127.0.0.1', 60932)
客戶端發來的消息是 daxi
六.基於UDP的套接字通訊流程(由於udp無連接,所以可以同時多個客戶端去跟服務端通訊)
服務端:
from socket import * #導入模塊 ip_port=('127.0.0.1',8080) buffer_size=1024 udp_server=socket(AF_INET,SOCK_DGRAM) #第一步:服務端產生套接字對象傳倆個參數(AF_INET基於網絡,SOCK_DGRAM數據報套接字類型) udp_server.bind(ip_port) #第二步:綁定服務器套接字IP和端口 while True: #第三步:服務器通訊循環 data,addr=udp_server.recvfrom(buffer_size) #第四步:服務端收客戶端消息,addr是給我發消息的客戶端IP和端口,recvfrom收的時候緩沖區如果沒有的話拿到空 print('接收客戶端發來的IP和端口',addr) #打印客發來消息的客戶端的端口和IP print('接收客戶端發來的消息內容',data) #打印客戶端收來的消息 udp_server.sendto(data.upper(),addr) #第五步:服務端發送data字節格式通過upper()轉大寫回給客戶端ddr的IP和端口 udp_server.close() #第六步:關閉服務器套接字
客戶端1:
from socket import * ip_port=('127.0.0.1',8080) buffer_size=1024 udp_client=socket(AF_INET,SOCK_DGRAM) #第一步:創建客戶端產生套接字對象傳倆個參數(AF_INET基於網絡,SOCK_DGRAM數據報套接字類型) while True: #第二步:通訊循環 msg=input('>>: ').strip() #客戶端讓用戶輸入方式發消息 udp_client.sendto(msg.encode('utf-8'),ip_port) #第三步:客戶端把用戶輸入的消息進行二進制編碼給服務端的,udp協議發包沒有連接,只能每次發包指定sendto要發給服務端的地址跟端口 data,addr=udp_client.recvfrom(buffer_size) #第四步:客戶端接收服務端字節格式 print('客戶端接收到服務端返回數據',data)
客戶端2:
from socket import * ip_port=('127.0.0.1',8080) buffer_size=1024 udp_client=socket(AF_INET,SOCK_DGRAM) #第一步:創建客戶端產生套接字對象傳倆個參數(AF_INET基於網絡,SOCK_DGRAM數據報套接字類型) while True: #第二步:通訊循環 msg=input('>>: ').strip() #客戶端讓用戶輸入方式發消息 udp_client.sendto(msg.encode('utf-8'),ip_port) #第三步:客戶端把用戶輸入的消息進行二進制編碼給服務端的,udp協議發包沒有連接,只能每次發包指定sendto要發給服務端的地址跟端口 data,addr=udp_client.recvfrom(buffer_size) #第四步:客戶端接收服務端字節格式 print('客戶端接收到服務端返回數據', data)
客戶端1輸入:>>: xiaoxi
返回:
客戶端接收到服務端返回數據 b'XIAOXI'
客戶端2輸入:>>: daxi
返回:
客戶端接收到服務端返回數據 b'DAXI'
服務端返回:
接收客戶端發來的IP和端口 ('127.0.0.1', 61907)
接收客戶端發來的消息內容 b'xiaoxi'
接收客戶端發來的IP和端口 ('127.0.0.1', 61908)
接收客戶端發來的消息內容 b'daxi'
基於udp實現ntp服務
服務端:
from socket import * import time ip_port=('127.0.0.1',8080) buffer_size=1024 udp_server=socket(AF_INET,SOCK_DGRAM) # udp_server.bind(ip_port) while True: data,addr=udp_server.recvfrom(buffer_size) print(data) if not data: #判斷客戶端發來的的空的情況下 fmt='%Y-%m-%d %X' #返回默認時間 else: fmt=data.decode('utf-8') #如果客戶端輸入格式就把傳的值發來 back_time=time.strftime(fmt) udp_server.sendto(back_time.encode('utf-8'),addr) #把服務端的時間以encode字符串形式返回給客戶端
客戶端:
from socket import * ip_port=('127.0.0.1',8080) buffer_size=1024 udp_client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) #客戶端發給服務端一個消息 data,addr=udp_client.recvfrom(buffer_size) #服務端返回的時間 print('ntp服務器的標准時間是',data.decode('utf-8'))
客戶端輸入:>>: %Y
客戶端返回:
ntp服務器的標准時間是 2018
服務端返回:
b'%Y'
客戶端輸入空>>:
客戶端返回:
ntp服務器的標准時間是 2018-11-07 13:39:37
服務端返回:
b''
b''
七.recv與recvfrom的區別
tcp:send發消息,recv收消息
udp:sendto發消息,recvfrom收消息是元祖的形式
發消息二者類似,收消息確實有區別
1.tcp協議:
(1)如果收消息緩沖區里的數據為空,那么recv就會堵塞
(2)tcp基於鏈接通信,如果一端端口鏈接,那另外一端的鏈接也跟着完蛋recv將會堵塞,收到的是空
2.udp協議:
(1)如果收消息緩沖區里的數據為空,recvfrom不會堵塞
(2)recvfrom收的數據小於sendinto發送的數據時,數據丟失
(3)只有sendinto發送數據沒有recvfrom收數據,數據丟失
注意:
(1)單獨運行udp的客戶端,並不會報錯,相反tcp會報錯,因為udp協議只負責把包發出去,對方收不收,根本不管,而tcp基於鏈接的,必須有一個服務端先運行着,客戶端去跟服務端建立鏈接然后依托於鏈接才能傳遞消息,任何一方試圖把鏈接摧毀都會導致對方程序崩潰
(2) udp程序,注釋任何一條客戶端的sendinto,服務端都會卡住,因為服務端有幾個recvfrom就要對應幾個sendinto,哪怕是sendinto(b")那也要有
3.總結
(1)udp的sendinto不用管是否是一個正在運行的服務端,可以己端一個勁的發消息
(2)udp的recvfrom是阻塞的,一個recvfrom(x)必須對一個一個sendinto(y),收完了x個字節的數據就算完成,若y>x數據就丟失,這意味udp根本不會粘包,但是會丟數據,不可靠
(3)tcp的協議數據不會丟,己端總是在收到ack時才會清楚緩沖區內容。數據是可靠的,但是會粘包
八.粘包
1.什么是粘包:由於收發消息都是在操作自己的緩沖區,自己的緩沖區根本不歸你的應用程序管,而是由操作系統控制的,基於TCP協議工作的話,收消息會收一堆放在自己緩沖區,客戶端第一次收到的少了,第二次收還會從緩沖區里接收上一次沒收完的,這就是粘包,所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據造成的。此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠的數據后才發送一個TCP段,若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段后一次發送出去,這樣接收方就收到了粘包數據。
2.TCP(transport control protocol,傳輸控制協議):是面向連接的,面向流的,提高可靠性服務。收發倆端(客戶端和服務端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。即面向流的通信是無消息保護邊界的。
3.UDP(user datagram protocol,用戶數據報協議):是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。即面向消息的通信是有消息保護邊界的。
4.tcp是基於數據流的,於是收發消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭。
5.倆種情況下會發生粘包
(1)發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據量很小,會合到一起,產生粘包)
模擬客戶端一次發送三條很小量的數據產生粘包
客戶端:
from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) tcp_client.send('wang'.encode('utf-8')) tcp_client.send('xixi'.encode('utf-8')) tcp_client.send('good'.encode('utf-8'))
服務端:
from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(back_log) conn,addr=tcp_server.accept() data1=conn.recv(buffer_size) print('第1次數據',data1) data2=conn.recv(buffer_size) print('第2次數據',data2) data3=conn.recv(buffer_size) print('第3次數據',data3)
客戶端運行后服務端接收信息:
第1次數據 b'wangxixigood'
第2次數據 b''
第3次數據 b'
(2)接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
模擬客戶端一次發送一條很大量的數據,接收端接收很小產生粘包
客戶端:
from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) tcp_client.send('wangxixigood'.encode('utf-8'))
服務端:
from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(back_log) conn,addr=tcp_server.accept() data1=conn.recv(2) #一次收倆字節 print('第1次數據',data1) data2=conn.recv(2) #一次收倆個字節 print('第2次數據',data2)
客戶端運行后服務端接收信息:
第1次數據 b'wa'
第2次數據 b'ng'
6.解決粘包的處理方法
(1)粘包問題根源在於,接收端不知道發送端將要傳送的字節流長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據
(2)為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然后一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然后再取真實數據。
通過解決粘包問題基於tcp實現客戶端遠程向服務端執行命令
客戶端:
from socket import * import struct #解決粘包問題 ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 ###啟動客戶端連接服務端 tcp_client=socket(AF_INET,SOCK_STREAM) #客戶端產生一個對象傳倆個參數(socket.AF_INET基於網絡通訊,socket.SOCK_STREAM表TCP協議)給tcp_server tcp_client.connect(ip_port) #客戶端連接服務器端的IP和端口 ###客戶端給服務端發消息 while True: #第一步:給發消息和收消息加上循環可以循環發收消息 cmd=input('>>: ').strip() #第二步:用戶輸入的命令賦值給cmd if not cmd:continue #客戶端不可以發空 if cmd == 'quit':break #給客戶端加上退出功能 tcp_client.send(cmd.encode('utf-8')) #第三步:客戶端把用戶輸入的命令send發給服務端 #第二步:struct解決粘包 length_data = tcp_client.recv(4) #客戶端就收recv4個字節包含服務端發來的長度的數據是byte類型 length = struct.unpack('i', length_data)[0] #用struct.unpack解碼收到的byte類型是元祖的形式,元祖的第一個元素就是數據的長度,加上'i'代表解的是整型賦值給length收到的長度 recv_size = 0 #定一個接收的尺寸默認值0 recv_msg=b'' #最后得到的結果recv_msg while recv_size < length: #有了數據的長度客戶端循環從自己的緩沖區一直接收直到把數據完整接收完 recv_msg += tcp_client.recv(buffer_size) #客戶端接收recv_msg尺寸加上tcp_client.recv(buffer_size)內容 recv_size=len(recv_msg) #recv_size收了多少真實數據等於len(recv_msg)真實長度=1024字節 #recv_msg = ''.join(iter(partial(tcp_client.recv, buffer_size), b'')) #partial把buffer_size傳給tcp_client.recv函數的第一個參數,iter無窮執行(partial(tcp_client.recv, buffer_size)直達運行結果遇到緩沖區為空時候停掉,通過.join轉換成字符串形式 print('命令的執行結果是 ',recv_msg.decode('gbk')) tcp_client.close()
服務端:
from socket import * import subprocess #subprocess模塊執行命令 import struct #解決粘包問題 ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 ####啟動服務端后 tcp_server=socket(AF_INET,SOCK_STREAM) #第一步:產生一個對象傳倆個參數(socket.AF_INET基於網絡通訊,socket.SOCK_STREAM表TCP協議)給tcp_server tcp_server.bind(ip_port) #第二步:綁定IP地址和訪問和端口號 tcp_server.listen(back_log) #第三步:listen(5)最多可以有五個建立好三次握手后的backlog(半連接池)等着,后面的需要排隊等着 ####客戶端連接服務端后 while True: #做連接循環 conn,addr=tcp_server.accept() #第一步:tcp_server.accept()拿到了TCP三次握手的結果是個元祖解壓給給conn(三次握手的連接)和addr服務端阻塞 print('打印出接收過來的客戶端鏈接',addr) while True: #第二步:服務端做通信循環的接,可以做到接收多個人發的連接 ###開始收第一個客戶端發來的消息 try: #第一步:做異常處理防止客戶端非正常斷開 cmd=conn.recv(buffer_size) #第二步:服務端收客戶端消息,recv是用戶態應用程序發起的 if not cmd:break #第三步:如果收到的cmd為空的話跳出通訊循環(解決客戶端tcp_client.close斷開服務端造成死循環問題) print('打印出客戶端所發出的命令',cmd) #執行命令,得到命令的運行結果cmd_res res=subprocess.Popen(cmd.decode('utf-8'),shell=True, #第四步:把接過來的字節命令轉碼decode('utf-8') stderr=subprocess.PIPE, #subprocess把stderr標准錯誤輸出的結果交給管道PIPE,res拿到的是subprocess.Popen的對象 stdout=subprocess.PIPE, #stdout標准輸出 stdin=subprocess.PIPE) #stdin標准輸入 #有了subprocess.Popen的對象,就可以通過stdout.read讀取管道的內容獲取cmd的運行結果,讀取后管道PIPE里的內容就空了 err=res.stderr.read() #從錯誤的管道里讀信息賦值給err if err: #如果讀取err有值代表出錯 cmd_res=err #cmd_res讀取錯誤信息 else: cmd_res=res.stdout.read() #如果err沒有值,cmd_res讀取管道里的正確的值 ###服務端發消息回給客戶端 if not cmd_res: #第一步:當命令正常執行且cmd_res沒有返回值的情況下防止接收到空 cmd_res='執行成功'.encode('gbk') #賦值一個返回值 #第二步:解決粘包: 第一步發數據長度,第二步發數據內容,把數據長度封裝在固定的大小范圍內后返回給客戶端 length = len(cmd_res) #計算cmd_res的長度賦值給length data_length = struct.pack('i', length) #struct.pack獲取的長度length值直接打成整型byte形式,'i'是固定長度是4個字節的賦值給data_length #相當於給cmd_res這個數據流封裝了一個報文頭叫data_length,因為cmd_res基於TCP的字節流,只要是字節流代表沒有消息的邊界,沒有邊界定一個邊界data_length數據頭(cmd_res的長度) conn.send(data_length) #服務端send,把data_length數據長度發給客戶端 conn.send(cmd_res) #服務端send,把得到的運行結果cmd_res通過conn.send發給客戶端 except Exception as e: #收到異常處理錯誤 print(e) break #斷開通訊循環
九.socketserver模塊實現TCP和UDP的並發
socketserver有倆大類:
1.第一個類:server類基於基本的socket幫你處理連接的
(1)BaseServer類:祖宗類
(2)TCPServer類繼承BaseServer類:處理TCP連接
UnixStremServer類繼承TCPServer:處理TCP連接用在Unix系統上
(3)UDPServer類繼承TCPServer:處理UDP連接
UnixDatagramServer類繼承UDPServer:處理UDP連接用在Unix系統上
2.第二個類:request類幫你處理通信的
(1)BaseRequestHandler類:數據通信,每一個請求來都會呼叫handle()方法,繼承一個類定義handle()方法
(2)StreamRequestHandler類繼承BaseRequestHandler類:數據流
(3)DatagramRequestHand類繼承BaseRequestHandler類:數據報
3.進程並發
(1)ForkingUDPServer類進程優先繼承ForkingMixIn類沒有繼承UDPServer類
(2)ForkinTCPServer類進程優先繼承ForkingMixIn類沒有繼承TCPServer類
4.線程並發
(1)ThreadingUDPServer類線程優先繼承ThreadingMixIn類沒有繼承UDPServer類
(2)ThreadingTCPServer類線程優先繼承ThreadingMixIn類沒有繼承TCPServer類
5.利用socketserver模塊實現TCP多客戶端連接並發
服務端代碼:
import socketserver #客戶端連接到服務端會進入通訊循環,一進入通訊循環就是調MyServer里的__init__函數里的handle方法接收conn和addr ''' def __init__(self, request, client_address, server): self.request = request self.client_address = client_address self.server = server self.setup() try: self.handle() finally: self.finish() ''' class MyServer(socketserver.BaseRequestHandler): #定一個類MyServer繼承socketserver下面的BaseRequestHandler類(客戶端每來一個新的連接用MyServer類實例化得到一個實例,然后跟你進行通信) def handle(self): #定義handle方法收發消息,handle屬於MyServer類的函數屬性,一次連接的self是實例(包含倆個信息request和client_address) print('conn is: ',self.request) #conn:接收的連接 print('addr is: ',self.client_address) #addr:是給我發消息的客戶端IP和端口 while True: #給發消息和收消息加上通信循環可以循環收發消息 try: #在通信循環做異常處理防止客戶端非正常斷開 ###收消息 data=self.request.recv(1024) #self.request相當於conn.recv if not data:break #解決不斷的收 print('收到客戶端的消息是',data,self.client_address) #client_address是到底那個客戶端發的 ###發消息 self.request.sendall(data.upper()) #self.request相當於conn.sendall except Exception as e: #收到異常處理錯誤 print(e) break #斷開通訊循環 if __name__ == '__main__': s=socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer) #調socketserver模塊下面的ThreadingTCPServer(多線程的TCP服務端)類處理連接,這個類傳倆個參數IP端口元祖形式,第二個參數是MyServer類,ThreadingTCPServer類加上括號實例化得到結果賦值給s # s=socketserver.ForkingTCPServer(('127.0.0.1',8080),MyServer) #多進程TCP服務端(linux系統實現) #s里包含了以下信息 print(s.server_address) print(s.RequestHandlerClass) print(MyServer) print(s.socket) print(s.server_address) # s.serve_forever() #socketserver.ThreadingTCPServer內置的s.serve_forevery方法完成連接循環,連接循環里面套一個通信循環,MyServer這個類進行實例化得到的實例跟客戶端進行通訊
客戶端1代碼:
from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: msg=input('>>: ').strip() if not msg:continue if msg == 'quit':break tcp_client.send(msg.encode('utf-8')) data=tcp_client.recv(buffer_size) print('收到服務端發來的消息:',data.decode('utf-8')) tcp_client.close()
客戶端2代碼:
from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: msg=input('>>: ').strip() if not msg:continue if msg == 'quit':break tcp_client.send(msg.encode('utf-8')) data=tcp_client.recv(buffer_size) print('收到服務端發來的消息:',data.decode('utf-8')) tcp_client.close()
服務端客戶端啟動后:
服務端返回:
('127.0.0.1', 8080)
<class '__main__.MyServer'>
<class '__main__.MyServer'>
<socket.socket fd=368, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)>
('127.0.0.1', 8080)
conn is: <socket.socket fd=392, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 50129)>
addr is: ('127.0.0.1', 50129)
conn is: <socket.socket fd=424, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 50130)>
addr is: ('127.0.0.1', 50130)
客戶端1執行>>: xixi
客戶端1返回:
收到服務端發來的消息: XIXI
客戶端2執行:>>: yaoyao
客戶端2返回:
收到服務端發來的消息: YAOYAO
服務端返回:
收到客戶端的消息是 b'xixi' ('127.0.0.1', 50129)
收到客戶端的消息是 b'yaoyao' ('127.0.0.1', 50130)
6.利用hmac+加鹽的方式來實現認證客戶端的鏈接合法性
客戶端:
from socket import * import struct #解決粘包問題 import hmac,os secret_key=b'wang xi xi' def conn_auth(conn): msg=conn.recv(32) h=hmac.new(secret_key,msg) digest=h.digest() conn.sendall(digest) def client_handler(ip_port,bufsize=1024): ###啟動客戶端連接服務端 tcp_client=socket(AF_INET,SOCK_STREAM) #客戶端產生一個對象傳倆個參數(socket.AF_INET基於網絡通訊,socket.SOCK_STREAM表TCP協議)給tcp_server tcp_client.connect(ip_port) #客戶端連接服務器端的IP和端口 conn_auth(tcp_client) ###客戶端給服務端發消息 while True: #第一步:給發消息和收消息加上循環可以循環發收消息 cmd=input('>>: ').strip() #第二步:用戶輸入的命令賦值給cmd if not cmd:continue #客戶端不可以發空 if cmd == 'quit':break #給客戶端加上退出功能 tcp_client.send(cmd.encode('utf-8')) #第三步:客戶端把用戶輸入的命令send發給服務端 #第二步:struct解決粘包 length_data = tcp_client.recv(4) #客戶端就收recv4個字節包含服務端發來的長度的數據是byte類型 length = struct.unpack('i', length_data)[0] #用struct.unpack解碼收到的byte類型是元祖的形式,元祖的第一個元素就是數據的長度,加上'i'代表解的是整型賦值給length收到的長度 recv_size = 0 #定一個接收的尺寸默認值0 recv_msg=b'' #最后得到的結果recv_msg while recv_size < length: #有了數據的長度客戶端循環從自己的緩沖區一直接收直到把數據完整接收完 recv_msg += tcp_client.recv(bufsize) #客戶端接收recv_msg尺寸加上tcp_client.recv(buffer_size)內容 recv_size=len(recv_msg) #recv_size收了多少真實數據等於len(recv_msg)真實長度=1024字節 #recv_msg = ''.join(iter(partial(tcp_client.recv, buffer_size), b'')) #partial把buffer_size傳給tcp_client.recv函數的第一個參數,iter無窮執行(partial(tcp_client.recv, buffer_size)直達運行結果遇到緩沖區為空時候停掉,通過.join轉換成字符串形式 print('命令的執行結果是 ',recv_msg.decode('gbk')) tcp_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',9999) bufsize=1024 client_handler(ip_port,bufsize)
服務端:
#_*_coding:utf-8_*_ from socket import * import subprocess #subprocess模塊執行命令 import struct #解決粘包問題 import hmac,os import socketserver #第一步:客戶端驗證 secret_key=b'wang xi xi' def conn_auth(conn): #定義認證客戶端鏈接函數 print('開始驗證新鏈接的合法性') msg=os.urandom(32) #產生位32字節的隨機數 conn.sendall(msg) #發送給客戶端 h=hmac.new(secret_key,msg) #把secret_key自定義的鹽和msg產生的32位隨機數添加到hmac里得到的對象是h digest=h.digest() #拿到對象h用digest()得到數字形式賦值(32位隨機字符串和加鹽得到值)給digest respone=conn.recv(len(digest)) #服務端conn會收跟客戶端發過來跟respone長度一樣的字節 return hmac.compare_digest(respone,digest) #hmac.compare_digest比較respone和digest這倆個數字是不是一樣結果產生布爾值交給通訊里的if判斷 #第三步:處理通訊 def data_handler(conn,bufsize=1024): if not conn_auth(conn): #判斷鏈接是否合法把conn(收發消息)鏈接傳給定義的認證函數 print('該鏈接不合法,關閉') conn.close() return print('鏈接合法,開始通信') while True: #收發消息做通訊循環 try: cmd = conn.recv(bufsize) # 第二步:服務端收客戶端消息,recv是用戶態應用程序發起的 if not cmd:break #如果收到的cmd為空的話跳出通訊循環(解決客戶端tcp_client.close斷開服務端造成死循環問題) print('打印出客戶端所發出的命令', cmd) # 執行命令,得到命令的運行結果cmd_res res = subprocess.Popen(cmd.decode('utf-8'), shell=True, # 第四步:把接過來的字節命令轉碼decode('utf-8') stderr=subprocess.PIPE, # subprocess把stderr標准錯誤輸出的結果交給管道PIPE,res拿到的是subprocess.Popen的對象 stdout=subprocess.PIPE, # stdout標准輸出 stdin=subprocess.PIPE) # stdin標准輸入 # 有了subprocess.Popen的對象,就可以通過stdout.read讀取管道的內容獲取cmd的運行結果,讀取后管道PIPE里的內容就空了 err = res.stderr.read() # 從錯誤的管道里讀信息賦值給err if err: # 如果讀取err有值代表出錯 cmd_res = err # cmd_res讀取錯誤信息 else: cmd_res = res.stdout.read() # 如果err沒有值,cmd_res讀取管道里的正確的值 ###服務端發消息回給客戶端 if not cmd_res: # 第一步:當命令正常執行且cmd_res沒有返回值的情況下防止接收到空 cmd_res = '執行成功'.encode('gbk') # 賦值一個返回值 # 第二步:解決粘包: 第一步發數據長度,第二步發數據內容,把數據長度封裝在固定的大小范圍內后返回給客戶端 length = len(cmd_res) # 計算cmd_res的長度賦值給length data_length = struct.pack('i', length) # struct.pack獲取的長度length值直接打成整型byte形式,'i'是固定長度是4個字節的賦值給data_length # 相當於給cmd_res這個數據流封裝了一個報文頭叫data_length,因為cmd_res基於TCP的字節流,只要是字節流代表沒有消息的邊界,沒有邊界定一個邊界data_length數據頭(cmd_res的長度) conn.send(data_length) # 服務端send,把data_length數據長度發給客戶端 conn.send(cmd_res) # 服務端send,把得到的運行結果cmd_res通過conn.send發給客戶端 except Exception as e: #收到異常處理錯誤 print(e) break #斷開通訊循環 #第二步:處理鏈接 def server_handler(ip_port,bufsize,backlog=5): tcp_socket_server=socket(AF_INET,SOCK_STREAM) #得到socket對象 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) #調用data_handler通訊循環把conn和bufsize傳進去 if __name__ == '__main__': ip_port=('127.0.0.1',9999) bufsize=1024 server_handler(ip_port,bufsize)