1.定義
又稱爲C/S架構,S 指的是Server(服務端軟件),C指的是Client(客戶端軟件)
本章的中點就是教大寫寫一個c/s架構的軟件,實現服務端軟件和客戶端軟件基於網絡的通訊。
2.互聯網中的c/s架構應用
騰訊做爲服務端爲你提供視頻,你得下個騰訊視頻客戶端才能看它的視頻)
瀏覽網頁,瀏覽器是客戶端軟件,服務端軟件在後端服務器上
3.C/S架構與socket的關係:html
咱們學習socket就是爲了完成C/S架構的開發,其中socket是一個Python給咱們封裝好的,方便開發不須要很是精通各類網絡協議便可進行相關軟件開發。python
1.前言linux
咱們從擁有一臺我的計算機開始,就有意無心的接觸了一臺小型,因此一個完整的計算機系統:其中由硬件,計算機操做系統,應用軟件等組成,git
而咱們要上網看視頻、聊天等,須要使用基於操做系統之上的應用軟件或者各類客戶端。那這些應用軟件是如何鏈接互聯網,與互聯網上的其餘電腦進行通訊的呢?它基層的通訊原理是怎麼樣的呢?請看下面分解。算法
2.互聯網協議由來shell
若是把計算機比做人,互聯網協議就是計算機界的英語。全部的計算機都學會了互聯網協議,那全部的計算機都就能夠按照統一的標準去收發信息從而完成通訊了。編程
因此可以咱們可以鏈接互聯網,核心就是由一堆協議組成,協議就是標準,好比全世界人通訊的標準是英語。json
人們按照分工不一樣把互聯網協議從邏輯上劃分了層級,有國際組織就制訂了統一的標準 7層的OSI 網絡模型,固然還有其餘分類。小程序
3.網絡通訊原理詳解windows
請看並瞭解基本網絡基礎知識,這個很重要。
http://www.javashuo.com/article/p-tmohapqf-p.html
4.學習socket爲什麼要學習網絡協議呢?
1.首先:本節課程的目標就是教會你如何基於socket編程,來開發一款本身的C/S架構軟件
2.其次:C/S架構的軟件(軟件屬於應用層)是基於網絡進行通訊的
3.而後:網絡的核心即一堆協議,協議即標準,你想開發一款基於網絡通訊的軟件,就必須遵循這些標準。
4.最後:就讓咱們從這些標準開始研究,開啓咱們的socket編程之旅
在圖1中,咱們沒有看到Socket的影子,那麼它到底在哪裏呢?仍是用圖來講話,一目瞭然。
Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對 用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。
因此,咱們無需深刻理解tcp/udp協議,socket已經爲咱們封裝好了,咱們只須要遵循socket的規定去編程,寫出的程序天然就是遵循tcp/udp標準的。
也有人將socket說成ip+port,ip是用來標識互聯網中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程序,ip地址是配置到網卡上的,而port是應用程序開啓的,ip與port的綁定就標識了互聯網中獨一無二的一個應用程序 而程序的pid是同一臺機器上不一樣進程或者線程的標識
套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 所以,有時人們也把套接字稱爲「伯克利套接字」或「BSD 套接字」。一開始,套接字 被設計用在同 一臺主機上多個應用程序之間的通信。這也被稱進程間通信,或 IPC。套接字有兩種(或者稱爲有兩個種族),分別是基於文件型的和基於網絡型的。
基於文件類型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,能夠經過訪問同一個文件系統間接完成通訊
基於網絡類型的套接字家族套接字家族的名字:AF_INET
(還有AF_INET6被用於ipv6,還有一些其餘的地址家族,不過,他們要麼是隻用於某個平臺,要麼就是已經被廢棄,或者是不多被使用,或者是根本沒有實現,全部地址家族 中,AF_INET是使用最普遍的一個,python支持不少種地址家族,可是因爲咱們只關心網絡編程,因此大部分時候我麼只使用AF_INET)
1.套接字工做原理介紹
在生活中,你要打電話給你的GrilFriends,先撥號,gf聽到電話鈴聲後是在考慮接電話仍是不接電話,若是接電話,這時候你和你的gf就完成創建了鏈接,就能夠講話了。
若是gf想要結束交流,則告知會告知你她要敷面膜洗澡睡覺了,你也迴應了好的,晚安,而後彼此結束交談。
因此下圖,就是這個原理。
先從服務端提及:服務端初始化Socket,而後與端口綁定(bind()),對端口進行監聽(listen()),調用accept() 阻塞,等待客戶端鏈接。
在這個時候若是有一個客戶端初始化一個Socket,而後鏈接服務器(connect),若是鏈接成功,這是客戶端與服務端的鏈接就創建了。
客戶端發送數據請求,服務端接收請求並處理請求,而後迴應數據發送給客戶端,客戶端讀取數據,最後彼此關閉鏈接,一次交互結束。
2.socket 模塊函數用法
# @Time : 2018/9/6 16:05 # @Author : Jame import socket ''' socket.socket(socket_family,socket_type,protocal=0) socket_family 能夠是AF_UNIX 或AF_INET. socket_type 能夠是SOCK_STREAM 或SOCK_DGRAM. protocal 通常不填寫,默認值爲0 ''' #獲取tcp/ip套接字 tcpSock=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #獲取upd/ip套接字 udpSock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) ''' 因爲socket模塊中有太多的屬性。 因此咱們能夠破例使用 from socket import * 這樣咱們就把socket模塊裏全部的屬性都帶到了咱們的命名空間中,能夠縮減一些重複代碼。 例如:tcpSock=socket(AF_INET,SOCK_STREAM) '''
3.服務端套接字函數、客戶端套接字函數
*服務端:
server.bind() 綁定(主機ip,端口號) 到套接字
server.listen() 開始tcp監聽(1...) 緩衝池數量1到n個
server.accept() 被動接受tcp 客戶端的鏈接,(阻塞式)等待鏈接的到來
*客戶端:
client.connect() 主動初始化tcp服務器鏈接
client.connect_ex() connect() 函數的擴展版本,出錯時候返回錯碼,而不是拋出異常
4.公共用途的套接字函數
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() 關閉套接字
5.面向鎖的套接字方法
s.setblocking() 設置套接字的阻塞與非阻塞模式
s.settimeout() 設置阻塞套接字操做的超時時間
s.gettimeout() 獲得阻塞套接字操做的超時時間
6.面向文件的套接字的函數
s.fileno() 套接字的文件描述符
s.makefile() 建立一個與套接字相關的文件
# @Time : 2018/9/6 16:43 # @Author : Jame # 1:用打電話的流程快速描述socket通訊 # 2:服務端和客戶端加上基於一次連接的循環通訊 # 3:客戶端發送空,卡主,證實是從哪一個位置卡的 # 服務端: from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.bind(('127.0.0.1',8081)) phone.listen(5) conn,addr=phone.accept() while True: data=conn.recv(1024) print('server===>') print(data) conn.send(data.upper()) conn.close() phone.close() # 客戶端: from socket import * phone=socket(AF_INET,SOCK_STREAM) phone.connect(('127.0.0.1',8081)) while True: msg=input('>>: ').strip() phone.send(msg.encode('utf-8')) print('client====>') data=phone.recv(1024) print(data) # 說明卡的緣由:緩衝區爲空recv就卡住,引出原理圖 # # # # 4.演示客戶端斷開連接,服務端的狀況,提供解決方法 # # 5.演示服務端不能重複接受連接,而服務器都是正常運行不斷來接受客戶連接的 #
# 6:簡單演示udp # 服務端 from socket import * phone=socket(AF_INET,SOCK_DGRAM) phone.bind(('127.0.0.1',8082)) while True: msg,addr=phone.recvfrom(1024) phone.sendto(msg.upper(),addr) # 客戶端 from socket import * phone=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ') phone.sendto(msg.encode('utf-8'),('127.0.0.1',8082)) msg,addr=phone.recvfrom(1024) print(msg) # udp客戶端能夠併發演示 # udp客戶端能夠輸入爲空演示,說出recvfrom與recv的區別,暫且不提tcp流和udp報的概念,留到粘包去說 # # 讀者勿看:socket實驗推演流程
Tcp是基於雙向鏈接的,必須先啓動服務端,而後啓動客戶端去鏈接服務端,創建初始鏈接。
1.tcp服務端 和t cp客戶端的主要步驟
ss = socket() #建立服務器套接字 ss.bind() #把地址綁定到套接字 ss.listen() #監聽連接 inf_loop: #服務器無限循環 cs = ss.accept() #接受客戶端連接 comm_loop: #通信循環 cs.recv()/cs.send() #對話(接收與發送) cs.close() #關閉客戶端套接字 ss.close() #關閉服務器套接字(可選)
cs = socket() # 建立客戶套接字 cs.connect() # 嘗試鏈接服務器 comm_loop: # 通信循環 cs.send()/cs.recv() # 對話(發送/接收) cs.close() # 關閉客戶套接字
2.基於tcp的socket實現模擬打電話
# @Time : 2018/9/6 16:55 # @Author : Jame import socket ip_port=('127.0.0.1',8080) #電話卡 BUFSIZE=1024 #收消息的大小 server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機 server.bind(ip_port) #手機插卡 server.listen(2) #手機待機等待接聽 conn,client_addr=server.accept() #手機接聽電話 print('接到來自%s 的電話'%client_addr[0]) msg=conn.recv(BUFSIZE) #聽消息 print(msg,type(msg)) conn.send(msg.upper()) #發消息 conn.close() #掛電話 server.close() #關機,防止騷擾 ''' 接到來自127.0.0.1 的電話 b'jame is boy' <class 'bytes'> '''
# @Time : 2018/9/6 16:55 # @Author : Jame import socket ip_port=('127.0.0.1',8080) #電話卡 BUFSIZE=1024 #收消息的大小 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機 client.connect_ex(ip_port) #撥打電話 client.send('jame is boy'.encode('utf-8')) #發消息,給服務端 msg=client.recv(BUFSIZE) #收消息,來自服務端回覆 print(msg,type(msg)) client.close() #掛電話,不關機繼續打給其餘女孩,哈哈 ''' b'JAME IS BOY' <class 'bytes'> '''
3.基於tcp的鏈接循環和通訊循環的socket
# @Time : 2018/8/27 10:45 # @Author : Jame import socket #1買手機 server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #2 插電話卡 server.bind(('127.0.0.1',8080)) #3.開機 server.listen(3) print('server socket start...') #4.等待電話鏈接中的請求,連接循環 while True: conn,client_addr=server.accept() print(conn,client_addr) #5.收/發信息,通訊循環 while True: try: data=conn.recv(1024) if len(data)==0:break #針對linux max 系統 print(data.decode('utf-8')) conn.send(data.upper()) except ConnectionResetError: #針對windows 系統 break conn.close() server.close()
# @Time : 2018/8/27 10:45 # @Author : Jame import socket #1買手機 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #2 撥號 client.connect(('127.0.0.1',8080)) #3 發/收 信息 while True: msg=input('>>>:').strip() client.send(msg.encode('utf-8')) #必須是bytes類型 data=client.recv(1024) print('服務端發來的消息:',data.decode('utf-8')) client.close()
4.解決服務端Address already in use
這個是因爲你的服務端仍然存在四次揮手的time_wait狀態在佔用地址(若是不懂,請深刻研究1.tcp三次握手,四次揮手 2.syn洪水攻擊 3.服務器高併發狀況下會有大量的time_wait狀態的優化方法)
#加入一條socket配置,重用ip和端口 phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080))
發現系統存在大量TIME_WAIT狀態的鏈接,經過調整linux內核參數解決, vi /etc/sysctl.conf 編輯文件,加入如下內容: net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30 而後執行 /sbin/sysctl -p 讓參數生效。 net.ipv4.tcp_syncookies = 1 表示開啓SYN Cookies。當出現SYN等待隊列溢出時,啓用cookies來處理,可防範少許SYN攻擊,默認爲0,表示關閉; net.ipv4.tcp_tw_reuse = 1 表示開啓重用。容許將TIME-WAIT sockets從新用於新的TCP鏈接,默認爲0,表示關閉; net.ipv4.tcp_tw_recycle = 1 表示開啓TCP鏈接中TIME-WAIT sockets的快速回收,默認爲0,表示關閉。 net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間
UDP是不須要事先進行鏈接的,先啓動哪一端都不會報錯。
1.udp服務端和udp客戶端經常使用套路
服務端:
server=socket() #建立一個服務器的套接字 server.bind() #綁定服務器套接字 inf_loop: cs=server.recvfrom() #對話(接送與發送) #或者 server.sendto() server.close() #關閉服務器套接字
客戶端:
client=socket(...) #建立套接字 comm_loop: #通信循環 client.sendto() /client.recvfrom() #對話(發送、接收) client.close() #關閉客戶端套接字
2.udp套接字簡單實例
# @Time : 2018/9/6 17:50 # @Author : Jame import socket ip_port=('127.0.0.1',9090) BUFSIZE=1024 udp_server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server.bind(ip_port) while True: msg,client_addr=udp_server.recvfrom(BUFSIZE) print(msg,client_addr) udp_server.sendto(msg.upper(),client_addr)
# @Time : 2018/9/6 17:50 # @Author : Jame import socket ip_port=('127.0.0.1',9090) BUFSIZE=1024 udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>:').strip() if not msg:continue udp_client.sendto(msg.encode('utf-8'),ip_port) back_msg,addr=udp_client.recvfrom(BUFSIZE) print(back_msg.decode('utf-8'),addr)
3.模擬qq聊天實例
# @Time : 2018/9/6 18:00 # @Author : Jame import socket ip_port=('127.0.0.1',9091) Buffsize=1024 udp_server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server.bind(ip_port) while True: qq_msg,addr=udp_server.recvfrom(Buffsize) print('來自[%s:%s]的一條信息:%s'%(addr[0],addr[1],qq_msg.decode('utf-8'))) back_msg=input('回覆信息:').strip() udp_server.sendto(back_msg.encode('utf-8'),addr)
# @Time : 2018/9/6 18:04 # @Author : Jame import socket Buffsize=1024 udp_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic={ 'dog':('127.0.0.1',9091), 'cat':('127.0.0.1',9091), 'pig':('127.0.0.1',9091) } while True: qq_name=input('請選擇聊天對象:').strip() while True: msg=input('請輸入消息,回車發送:').strip() if msg=='quit':break if not msg or not qq_name or qq_name not in qq_name_dic:continue udp_client.sendto(msg.encode('utf-8'),qq_name_dic[qq_name]) back_msg,addr=udp_client.recvfrom(Buffsize) print('來自[%s:%s]的一條消息:%s'%(addr[0],addr[1],back_msg.decode('utf-8'))) udp_client.close()
4.應用:ntp時間服務器
#Author http://www.cnblogs.com/Jame-mei 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.bind(ip_port) while True: msg, addr = tcp_server.recvfrom(bufsize) print('===>', msg) if not msg or len(msg)<6: 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()
#Author http://www.cnblogs.com/Jame-mei 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) print(data.decode('utf-8')) tcp_client.close()
1.粘包問題發現tcp
讓咱們基於tcp先製做一個遠程執行命令的程序(1:執行錯誤命令 2:執行ls 3:執行ifconfig)
#Author http://www.cnblogs.com/Jame-mei import socket import subprocess #1 買手機 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) print(phone) #2 插電話卡 phone.bind(('127.0.0.1',8080)) #3 開機 phone.listen(3) print('server 端啓動') #4 等待電話鏈接中 while True: conn,client_addr=phone.accept() print('客戶端鏈接信息:',conn,client_addr) #5接受信息 while True: try: data=conn.recv(1024) if len(data)==0:break #針對linux mac 的異常處理,不能接受空消息,不然處於一直等待中 print('客戶端發送的信息:',data) data=data.decode('utf-8') obj=subprocess.Popen(data,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) #服務端回信息 stdout=obj.stdout.read() #byes類型 stderr=obj.stderr.read() conn.send(stdout+stderr) except ConnectionResetError: #針對windows 系統的異常處理 break #6最後關閉鏈接 conn.close() #7 關機 phone.close()
#Author http://www.cnblogs.com/Jame-mei import socket #1 買手機 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #2 插電話卡,發信息 client.connect(('127.0.0.1',8080)) while True: msg=input('Please input>>:').strip() if len(msg)==0:continue client.send(msg.encode('utf-8')) #3 客戶端收信息 data=client.recv(1024) print('服務端回覆信息:',data.decode('gbk')) #4 關閉客戶端鏈接 client.close()
注意注意注意:
subprocess.Popen 的結果的編碼是以當前所在的系統爲準的。
若是是windows,那麼res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼,且只能從管道里讀一次結果。
注意:命令ls -l ; lllllll ; pwd 的結果是既有正確stdout結果,又有錯誤stderr結果
2.udp爲何發生粘包問題 ?
udp不會發生粘包問題,除了超過udp接收數據的大小,會報錯。
#Author http://www.cnblogs.com/Jame-mei from socket import * import subprocess ip_port=('127.0.0.1',9003) bufsize=1024 udp_server=socket(AF_INET,SOCK_DGRAM) 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+stdout,addr) udp_server.close()
#Author http://www.cnblogs.com/Jame-mei from socket import * ip_port=('127.0.0.1',9003) bufsize=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(bufsize) print(data.decode('gbk'),end='') ''' udp一次接收的數據量較小,於是適合dns解析,網頁聊天等。 OSError: [WinError 10040] 一個在數據報套接字上發送的消息大於內部消息緩衝區或其餘一些網絡限制,或該用戶用於接收數據報的緩衝區比數據報小。 '''
1.前言
只有tcp有粘包現象,udp永遠不會粘包。這是爲何呢,須要瞭解回顧socket收發消息的原理。
2.粘包造成的原理詳解
例如:基於tcp的套接字客戶端往服務端上傳文件,發送文件內容是按照一段一段字節流發送的,在接收方看來,根本不知道該文件的字節流何處開始,何處結束。
發送端能夠是1kb 1kb 地發送數據,而接收端的應用程序能夠2kb 2kb的提走數據,固然也可能一次提走3kb或者6kb數據,或者一次只提走幾個字節。也就是說,應用程序所看到的數據是一 個總體,或者是一個流(stream)。一條消息有多少字節對應用程序來講是不可見的,所以TCP協議是面向流的協議。這也就是容易粘包的緣由。
所謂粘包問題,主要仍是由於接收方不知消息之間的界限,不知道一次性提取多少字節數據形成的。
而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP很不一樣。怎麼定義定義消息呢?能夠認爲對方一次性 write/send的數據爲一個消息,須要明白的是當對方send一條信息時候,不管底層怎麼分段分片,TCP協議層會把構成整條消息的數據段排序完成後呈現到內核的緩衝區。
此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲了提升效率,發送方每每收集足夠多的數據後才發送一個TCP段。若連續幾回都須要send的數據不多,通暢TCP會根據優化算法把這些數據 合成一個TCP段後一次發送出去,這樣接收方就收到了粘在一塊兒的數據流了。
*TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。收發(client/server)兩端都要有一一成對的socket,所以發送端爲了將多個數據發送到接收端的包,更 有效的到對方,使用了優化算法(Nagle算法),將屢次間隔較小且數據量較小的數據,合併成一個大的數據流,而後進行封包。這樣,接收端就很難分辨出從該數據包裏的幾段數據的始末。因此必須提 供科學的拆包機制(定製報頭字典等方法) 。 因此面向流的通訊是無消息保護邊界的。
*UDP(user datagram protocol,用戶數據報協議) 是無鏈接的,面向消息的,提供高效服務。不會使用塊的合併優化算法。因爲UDP支持的是一對多模式,因此接收端的skbuff(套接字緩衝區)採用了鏈 式結構來記錄每個達到UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣對於接收端來講,就容易區分處理了。因此面向消息的通訊是有消息保護邊界的。
總結:
tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息處理機制,防止程序阻塞卡住。而udp時基於數據報的,即使輸入的消息爲空,那也不是空消息,udp會幫你 封裝上消息頭,詳情請九.2 udp ssh模擬實例。
udp的recvfrom是阻塞的,一個recvfrom(x)必須對惟一一個sendinto(y),收完了x字節的數據就算完成,如果y>x數據就丟失,這意味着udp根本不會粘包,可是會丟失數據,不可靠。
tcp的協議數據不會丟失,沒有收完,下次會繼續接收上次的,收取端收到ack纔會清除緩衝區內容。數據是可靠的,可是會產生粘包問題。
3.發生粘包的兩種狀況
1):發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔短,數據量很小,匯合到一塊兒,產生粘包問題)
# @Time : 2018/9/7 15:02 # @Author : Jame from socket import * ip_port=('127.0.0.1',8080) tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(2) conn,client_addr=tcp_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('第一條----->',data1.decode('utf-8')) print('第二條---->',data2.decode('utf-8'))
# @Time : 2018/9/7 15:02 # @Author : Jame from socket import * ip_port=('127.0.0.1',8080) BUFSIZE=1024 tcp_client=socket(AF_INET,SOCK_STREAM) res=tcp_client.connect_ex(ip_port) tcp_client.send('hello'.encode('utf-8')) tcp_client.send('jame'.encode('utf-8')) ''' 第一條-----> hellojame 第二條----> 第一條-----> hello 第二條----> jame 出現這種狀況的緣由就是2條數據量小,時間間隔短 '''
2):接收方不及時收取緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是會從緩衝區拿上次遺留的數據,產生粘包)
# @Time : 2018/9/7 15:02 # @Author : Jame from socket import * ip_port=('127.0.0.1',8081) tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(2) conn,client_addr=tcp_server.accept() data1=conn.recv(2) #第一次設置只收2個字節 不收完 data2=conn.recv(10) #下次收的時候,會先收取舊的數據,而後取新的 print('第一條----->',data1.decode('utf-8')) print('第二條---->',data2.decode('utf-8')) ''' 第一條-----> he 第二條----> llo jame #發生這種粘包問題是,收取放第一次只收取了2個字節,沒有及時收取準確的大小,致使下一次收取了舊數據+新數據 ''' conn.close() tcp_server.close()
# @Time : 2018/9/7 15:02 # @Author : Jame from socket import * ip_port=('127.0.0.1',8081) BUFSIZE=1024 tcp_client=socket(AF_INET,SOCK_STREAM) res=tcp_client.connect_ex(ip_port) tcp_client.send('hello jame'.encode('utf-8'))
拆包的狀況發生:
當發送端緩衝區的長度大於網卡的MTU時候,tcp會將此次發送的數據拆成幾個數據包發送出去。
4.補充tcp/udp send/recv/sendall相關問題
1):爲什麼tcp是可靠的,udp是不可靠的?
基於tcp的傳輸原理請參考:https://www.cnblogs.com/Jame-mei/p/9571728.html
由於tcp在傳輸的時候,發送端會把數據發送到本身的緩存中,而後協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則從新發送數據,所 以tcp是可靠的。
udp發送數據的時候,對端是不會返回確認信息的,所以不可靠。
2):send(字節流)和recv(1024)及sendall
recv裏指定1024意思是從緩存裏一次拿出1024個字節的數據。
send的字節流是先存入已端緩存,而後由協議控制將緩存內容發送對端,若是待發送的字節流大小大於緩存剩餘空間,那麼數據丟失,用sendall就會循環調用send,數據不會丟失。
粘包問題的根源在於,接收端不知道發送端傳送的字節流長度,因此解決粘包問題的方法就是,圍繞如何讓發送端在發送數據的前,把本身將要發送的字節流總大小讓接收端 提早知曉,而後接收端來 一個循環接收完指定長度的字節流便可。
from socket import * import subprocess import struct phone=socket(AF_INET,SOCK_STREAM) phone.bind(('127.0.0.1',8081)) phone.listen(5) print('服務的啓動......') # 鏈接循環 while True: conn,client_addr=phone.accept() print(client_addr) # 通訊循環 while True: try: cmd=conn.recv(1024) if not cmd:break obj=subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() # 一、先發送固定長度的報頭 #目前報頭裏只包含數據的大小 total_size=len(stdout) + len(stderr) conn.send(struct.pack('i',total_size)) # 二、發送真實的數據 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() phone.close()
from socket import * import struct phone=socket(AF_INET,SOCK_STREAM) phone.connect(('127.0.0.1',8081)) while True: cmd=input('>>>: ').strip() if not cmd:continue phone.send(cmd.encode('utf-8')) #一、先收報頭,從報頭裏取出對真實數據的描述信息 header=phone.recv(4) total_size=struct.unpack('i',header)[0] #二、循環接收真實的數據,直到收乾淨爲止 recv_size=0 res=b'' while recv_size < total_size: recv_data=phone.recv(1024) res+=recv_data recv_size+=len(recv_data) print(res.decode('gbk')) phone.close()
import struct obj=struct.pack('q',53112312311231223) print(obj,len(obj)) # # res=struct.unpack('i',obj)[0] # print(res)
思路:先發送指定的報頭及報頭內容,其中爲字節流加上自定義的報頭信息,包括字節流長度,md5值,文件名等等,對端先接受固定長度的報頭,從中讀取真實字節流長度及其餘信息,而後循環收取,直到完全收取完畢。
用到的模塊:struct,能夠把一個類型,如數字,轉換成固定長度bytes。
經常使用的又struct('i',12345678),4個字節,l(long) 4個字節,q(longlong) 8個字節,具體請查閱幫助。
#_*_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的詳細用法
具體步驟:
咱們能夠把報頭作成字典,字典裏包含將要發送的真實數據的詳細信息,而後json序列化,而後用struck將序列化後的數據長度打包成4個字節(4個本身足夠用了)
發送時:
先發報頭長度
再編碼報頭內容而後發送
最後發真實內容
接收時:
先手報頭長度,用struct取出來
根據取出的長度收取報頭內容,而後解碼,反序列化
從反序列化的結果中取出待取數據的詳細信息,而後去取真實的數據內容
from socket import * import subprocess import struct import json phone=socket(AF_INET,SOCK_STREAM) phone.bind(('127.0.0.1',8081)) phone.listen(5) print('服務的啓動......') # 鏈接循環 while True: conn,client_addr=phone.accept() print(client_addr) # 通訊循環 while True: try: cmd=conn.recv(1024) if not cmd:break obj=subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() #製做報頭 header_dic={ 'filename':'a.txt', 'total_size':len(stdout) + len(stderr), 'md5':'xxxxxsadfasdf123234e123' } header_json = json.dumps(header_dic) header_bytes=header_json.encode('utf-8') #一、先發送報頭的長度 conn.send(struct.pack('i',len(header_bytes))) #二、再發送報頭 conn.send(header_bytes) #三、最後發送真實的數據 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() phone.close()
from socket import * import struct import json phone=socket(AF_INET,SOCK_STREAM) phone.connect(('127.0.0.1',8081)) while True: cmd=input('>>>: ').strip() if not cmd:continue phone.send(cmd.encode('utf-8')) #一、先收報頭的長度 obj=phone.recv(4) header_size=struct.unpack('i',obj)[0] #二、再接收報頭 header_bytes=phone.recv(header_size) header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) total_size=header_dic['total_size'] #三、循環接收真實的數據,直到收乾淨爲止 recv_size=0 res=b'' while recv_size < total_size: recv_data=phone.recv(1024) res+=recv_data recv_size+=len(recv_data) print(res.decode('gbk')) phone.close()
思路:在分佈式系統中,簡單的實現一個客戶端認證功能,不像sll那麼複雜的,利用hmac+驗證的方式來實現。
# @Time : 2018/9/7 16:02 # @Author : Jame from socket import * import hmac,os secret_key=b'jia zhuang shi secret key file are you ok?' def con_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 con_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: :param bufsize: :param backlog: :return: ''' tcp_socket=socket(AF_INET,SOCK_STREAM) tcp_socket.bind(ip_port) tcp_socket.listen(backlog) while True: conn,addr=tcp_socket.accept() print('新鏈接[%s :%s]'%(addr[0],addr[1])) data_handler(conn,bufsize) if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 server_handler(ip_port,bufsize)
# @Time : 2018/9/7 16:02 # @Author : Jame from socket import * import hmac,os secret_key=b'jia zhuang shi secret key file are you ok?' 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_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) conn_auth(tcp_client) while True: data=input('>>>>:').strip() if not data:continue if data=='quit':break tcp_client.sendall(data.encode('utf-8')) respone=tcp_client.recv(bufsize) print(respone.decode('utf-8')) tcp_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 client_handler(ip_port,bufsize) ''' server輸出: 新鏈接[127.0.0.1 :8761] 開始驗證新鏈接合法性? 連接合法,開始通訊! client輸出: >>>>:nihao NIHAO >>>>: '''
# @Time : 2018/9/7 16:02 # @Author : Jame from socket import * import hmac,os ''' 非法客戶端:不知道加密方式!!! secret_key=b'jia zhuang shi secret key file are you ok?' 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) tcp_client.connect(ip_port) #conn_auth(tcp_client) while True: data=input('>>>>:').strip() if not data:continue if data=='quit':break tcp_client.sendall(data.encode('utf-8')) respone=tcp_client.recv(bufsize) print(respone.decode('utf-8')) tcp_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 client_handler(ip_port,bufsize) ''' server輸出: 新鏈接[127.0.0.1 :8746] 開始驗證新鏈接合法性? 該連接不合法,請關閉! client輸出: >>>>:nihao Traceback (most recent call last): File "E:/pythonwork/s14/day9/基於認證的客戶端鏈接合法性/非法客戶端.py", line 39, in <module> client_handler(ip_port,bufsize) File "E:/pythonwork/s14/day9/基於認證的客戶端鏈接合法性/非法客戶端.py", line 30, in client_handler print(respone.decode('utf-8')) UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc9 in position 1: invalid continuation byte '''
# @Time : 2018/9/7 16:02 # @Author : Jame from socket import * import hmac,os secret_key=b'bu zhi dao secret key file????' 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_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) conn_auth(tcp_client) while True: data=input('>>>>:').strip() if not data:continue if data=='quit':break tcp_client.sendall(data.encode('utf-8')) respone=tcp_client.recv(bufsize) print(respone.decode('utf-8')) tcp_client.close() if __name__ == '__main__': ip_port=('127.0.0.1',8080) bufsize=1024 client_handler(ip_port,bufsize) ''' server輸出: 新鏈接[127.0.0.1 :8795] 開始驗證新鏈接合法性? 該連接不合法,請關閉! client輸出: >>>>:nihao Traceback (most recent call last): File "E:/pythonwork/s14/day9/基於認證的客戶端鏈接合法性/非法客戶端2.py", line 41, in <module> client_handler(ip_port,bufsize) File "E:/pythonwork/s14/day9/基於認證的客戶端鏈接合法性/非法客戶端2.py", line 31, in client_handler respone=tcp_client.recv(bufsize) ConnectionAbortedError: [WinError 10053] 您的主機中的軟件停止了一個已創建的鏈接。 '''
1):模擬併發效果:鏈接循環+通訊循環
# @Time : 2018/8/27 10:45 # @Author : Jame import socket #1買手機 server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #2 插電話卡 server.bind(('127.0.0.1',8080)) #3.開機 server.listen(3) print('server socket start...') #4.等待電話鏈接中的請求,連接循環 while True: conn,client_addr=server.accept() print(conn,client_addr) #5.收/發信息,通訊循環 while True: try: data=conn.recv(1024) if len(data)==0:break #針對linux max 系統 print(data.decode('utf-8')) conn.send(data.upper()) except ConnectionResetError: #針對windows 系統 break conn.close() server.close()
# @Time : 2018/8/27 10:45 # @Author : Jame import socket #1買手機 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #2 撥號 client.connect(('127.0.0.1',8080)) #3 發/收 信息 while True: msg=input('>>>:').strip() client.send(msg.encode('utf-8')) #必須是bytes類型 data=client.recv(1024) print('服務端發來的消息:',data.decode('utf-8')) client.close()
2):socketserver模塊使用
(1).socketserver原理
基於tcp的套接字,關鍵就是兩個循環,一個連接循環,一個通訊循環
socketserver模塊中分兩大類:server類(解決連接問題)和request類(解決通訊問題)
*server類圖:
*request類圖:
*繼承關係:具體請分析源碼
(2).基於socketserver實例
# @Time : 2018/8/28 11:14 # @Author : Jame import socketserver import subprocess import struct import json class MyTcphandler(socketserver.BaseRequestHandler): def handle(self): #通訊循環 while True: try: cmd=self.request.recv(1024) if len(cmd)==0:break print('收到:%s:%s的命令:%s'%(self.client_address[0],self.client_address[1],cmd.decode('utf-8'))) obj = subprocess.Popen( cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() #1製做報頭 header_dic={ 'total_size':len(stdout)+len(stderr), 'filename':'a.txt' } #轉換成json (str類型),再將str類型轉化成bytes header_json=json.dumps(header_dic) header_bytes=header_json.encode('utf-8') #2 發送報頭長度,報頭的數據 self.request.send(struct.pack('i',len(header_bytes))) self.request.send(header_bytes) #3 最後發送真是數據 self.request.send(stderr) self.request.send(stdout) except ConnectionResetError: break if __name__ == '__main__': server=socketserver.ThreadingTCPServer(('127.0.0.1',8088),MyTcphandler) socketserver.TCPServer.request_queue_size=3 server.serve_forever()
# @Time : 2018/8/28 11:20 # @Author : Jame import socket import struct import json #1 買手機 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM #2 撥號 client.connect(('127.0.0.1',8088)) #3 發 收信息 while True: msg=input('>>: ').strip() if len(msg) == 0:continue client.send(msg.encode('utf-8')) # b'' #3.1 收取報頭長度,報頭的數據 header_size=struct.unpack('i',client.recv(4))[0] header_bytes=client.recv(header_size) header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) #3.2 讀取真是返回數據 total_size=header_dic['total_size'] res = b'' recv_size =0 while recv_size < total_size: data = client.recv(1024) res += data recv_size += len(data) print(res.decode('gbk')) client.close()
一.需求 開發一個支持多用戶在線的FTP程序 要求: 1.用戶加密認證 2.容許同時多個用戶登錄 3.每一個用戶有本身的家目錄,且只能訪問本身的家目錄 4.對用戶進行磁盤配額,每一個用戶的可用空間不一樣 5.運行用戶在ftp server上隨意切換目錄(*) 6.運行用戶查看當前目錄下的文件 7.運行上傳和下載文件,保證文件的一致性 8.文件傳輸過程當中顯示進度條 9.支持文件的斷電續傳功能(*) 二.分析與思路 角色1:用戶consumer 功能: *註冊 *登錄(用hashlib md5進行加密,登錄用正向驗證,由於md5反解難度大) *有本身的家目錄,只能訪問本身的家目錄內的文件及文件夾 *不一樣用戶磁盤空間大小不一樣{ 思路: 初始化的時候分配隨機額度0-50m,而且用戶上傳的時候跟空間額度比對,會員vip充值,充值後能夠提高存儲空間。 } *上傳 put ,下載get ,並顯示進度條,保證文件一致性(md5) { 思路: 上傳:能夠上傳本地路徑存在的文件,到服務端的默認保存位置或者家目錄。 下載:下載當前家目錄裏的某個路徑的文件,到客戶端本地的默認保存位置。 } *斷點續傳( 思路: 建立臨時文件,客戶端上傳時,服務器檢查臨時文件,有就發大小發給客戶端,客戶端seek到文件斷點處給服務器端發送數據。 ) *用戶在ftp server上隨意切換目錄( 思路: 用戶操做使用cd命令,能夠切換到家目錄的任意目錄,再用ls查看會顯示切換後的目錄,下次登錄會記住切換後的地址,直接登錄改地址 ) 角色2:管理員 admin 功能: *查看用戶列表信息 *修改用戶密碼 *刪除用戶(刪除狀態,而不是真正刪除數據) *能夠對用戶的磁盤額度進行調整 *設置黑名單,禁止用於登錄和使用 https://gitee.com/meijinmeng/Ftp_system_v0.1.git