即C/S架構,包括html
1.硬件C/S架構(打印機)python
2.軟件C/S架構(web服務)linux
美好的願望:web
最經常使用的軟件服務器是 Web 服務器。一臺機器裏放一些網頁或 Web 應用程序,而後啓動 服務。這樣的服務器的任務就是接受客戶的請求,把網頁發給客戶(如用戶計算機上的瀏覽器),然 後等待下一個客戶請求。這些服務啓動後的目標就是「永遠運行下去」。雖然它們不可能實現這樣的 目標,但只要沒有關機或硬件出錯等外力干擾,它們就能運行很是長的一段時間。 算法
生活中的C/S架構:shell
飯店是S端(服務端),全部的食客是C端(客戶端)編程
互聯網中到處是C/S架構(商城網站是服務端,你的瀏覽器是客戶端;騰訊做爲服務端爲你提供視頻,你得下個騰訊視頻客戶端才能看視頻)json
C/S架構與socket的關係:windows
咱們學習socket就是爲了完成C/S架構的開發設計模式
一個完整的計算機系統是由硬件、操做系統、應用軟件三者組成,具有了這三個條件,一臺計算機系統就能夠本身跟本身玩了(打個單機遊戲,玩個掃雷啥的)
若是你要跟別人一塊兒玩,那你就須要上網了,互聯網的核心就是由一堆協議組成,協議就是標準,全世界人通訊的標準是英語,若是把計算機比做人,互聯網協議就是計算機界的英語。全部的計算機都學會了互聯網協議,那全部的計算機都就能夠按照統一的標準去收發信息從而完成通訊了。人們按照分工不一樣把互聯網協議從邏輯上劃分了層級。
詳見網絡通訊原理:http://www.cnblogs.com/xuyaping/p/7670198.html
爲什麼學習socket必定要先學習互聯網協議:
1.首先:本節課程的目標就是教會你如何基於socket編程,來開發一款本身的C/S架構軟件
2.其次:C/S架構的軟件(軟件屬於應用層)是基於網絡進行通訊的
3.而後:網絡的核心即一堆協議,協議即標準,你想開發一款基於網絡通訊的軟件,就必須遵循這些標準。
4.最後:就讓咱們從這些標準開始研究,開啓咱們的socket編程之旅。
TCP/IP協議族包括運輸層、網絡層、鏈路層。如今你知道TCP/IP與UDP的關係了吧。
在圖1中,咱們沒有看到Socket的影子,那麼它到底在哪裏呢?仍是用圖來講話,一目瞭然。
socket一般也稱做"套接字",用於描述IP地址和端口,應用程序一般經過"套接字"向網絡發出請求或者應答網絡請求。
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)。
一個生活中的場景。你要打電話給一個朋友,先撥號,朋友聽到電話鈴聲後提起電話,這時你和你的朋友就創建起了鏈接,就能夠講話了。等交流結束,掛斷電話結束這次交談。生活中的場景就解釋了這工做原理。
先從服務器端提及。服務器端先初始化Socket,而後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端鏈接。在這時若是有個客戶端初始化一個Socket,而後鏈接服務器(connect),若是鏈接成功,這時客戶端與服務器端的鏈接就創建了。客戶端發送數據請求,服務器端接收請求並處理請求,而後把迴應數據發送給客戶端,客戶端讀取數據,最後關閉鏈接,一次交互結束。
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) 因爲 socket 模塊中有太多的屬性。咱們在這裏破例使用了'from module import *'語句。使用 'from socket import *',咱們就把 socket 模塊裏的全部屬性都帶到咱們的命名空間裏了,這樣能 大幅減短咱們的代碼。 例如tcpSock = socket(AF_INET, SOCK_STREAM)
服務端套接字函數
s.bind() 綁定(主機,端口號)到套接字
s.listen() 開始TCP監聽
s.accept() 被動接受TCP客戶的鏈接,(阻塞式)等待鏈接的到來
客戶端套接字函數
s.connect() 主動初始化TCP服務器鏈接
s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
公共用途的套接字函數
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() 關閉套接字
面向鎖的套接字方法
s.setblocking() 設置套接字的阻塞與非阻塞模式
s.settimeout() 設置阻塞套接字操做的超時時間
s.gettimeout() 獲得阻塞套接字操做的超時時間
面向文件的套接字的函數
s.fileno() 套接字的文件描述符
s.makefile() 建立一個與該套接字相關的文件
tcp服務端
ss = socket() #建立服務器套接字 ss.bind() #把地址綁定到套接字 ss.listen() #監聽連接 inf_loop: #服務器無限循環 cs = ss.accept() #接受客戶端連接 comm_loop: #通信循環 cs.recv()/cs.send() #對話(接收與發送) cs.close() #關閉客戶端套接字 ss.close() #關閉服務器套接字(可選)
tcp客戶端
cs = socket() # 建立客戶套接字 cs.connect() # 嘗試鏈接服務器 comm_loop: # 通信循環 cs.send()/cs.recv() # 對話(發送/接收) cs.close() # 關閉客戶套接字
socket通訊流程與打電話流程相似,咱們就以打電話爲例來實現一個low版的套接字通訊
#TCP服務端 import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #AF_INET基於網絡類型的套接字家族;SOCK_STREAM,TCP協議。「買手機」 phone.bind(("127.0.0.1",8080)) #綁定ip地址,綁定(主機,端口號)到套接字,127.0.0.1,8080參數ip地址和端口號。「綁定手機」 phone.listen(5) #開始TCP監聽,同時掛起5個客戶端帳戶,通常定義參數,寫入配置文件中,可進行修改。「開機」 print("start...") conn,addr=phone.accept() #接收客戶端的連接和IP地址;被動接受TCP客戶端的鏈接,(阻塞式)等待鏈接的到來。「等待電話的連接」 print("客戶端的信息是(電話線路是)",conn) print("客戶端的ip地址和端口是(客戶端手機號是)",addr) data=conn.recv(1024) #接收的客戶端TCP數據。此時1024爲最大設置,收消息來自緩存,內存有限,參數太大也沒什麼卵用。 print("客戶端發來的消息是",data) conn.send(data.upper()) # 發送給客戶端TCP數據(send在待發送數據量大於己端緩存區剩餘空間時,數據丟失,不會發完) conn.close() #關閉客戶端套接字,"掛電話" phone.close() #關閉服務端套接字,"關機"
運行服務端
start... #程序停滯,沒有接收到客戶端消息
#TCP客戶端 import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #AF_INET基於網絡類型的套接字家族;SOCK_STREAM,TCP協議。「買手機」 phone.connect(("127.0.0.1",8080)) #連接服務器,127.0.0.1,8080爲服務端的ip地址和端口 phone.send("hello".encode("utf8")) #客戶端發送消息須要是bytes格式。由於發送消息是在應用層上,應用層發送給本機網絡層-->鏈路層-->物理層,物理層是經過電信號二進制來交換信息 data=phone.recv(1024) #接收到的服務端消息 print(data) phone.close() #關閉客戶端套接字
運行客戶端
b'HELLO' #輸出b'HELLO',客戶端程序結束。
再次看服務端運行結果,服務端程序執行完畢。
start... 客戶端的信息是(電話線路是) <socket.socket fd=244, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 52088)> 客戶端的ip地址和端口是(客戶端手機號是) ('127.0.0.1', 52088) 客戶端發來的消息是 b'hello'
由於服務端和客戶端都在本機,因此ip地址相同,可是端口號不一樣。
上述流程的問題是,服務端只能接受一次連接,而後就完全關閉掉了,實際狀況應該是,服務端不斷接受連接,而後循環通訊,通訊完畢後只關閉連接,服務器可以繼續接收下一次連接,下面是修改版。
#服務端 import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #AF_INET基於網絡類型的套接字家族;SOCK_STREAM,TCP協議。「買手機」 phone.bind(("127.0.0.1",8080)) #綁定ip地址,綁定(主機,端口號)到套接字,127.0.0.1,8080參數ip地址和端口號。「綁定手機」 phone.listen(5) #開始TCP監聽,同時掛起5個客戶端帳戶,通常定義參數,寫入配置文件中,可進行修改。「開機」 print("start...") conn,addr=phone.accept() #接收客戶端的連接和IP地址;被動接受TCP客戶端的鏈接,(阻塞式)等待鏈接的到來。「等待電話的連接」 print("客戶端的信息是(電話線路是)",conn) print("客戶端的ip地址和端口是(客戶端手機號是)",addr) while True: #通訊循環 data=conn.recv(1024) #接收的客戶端TCP數據。此時1024爲最大設置,收消息來自緩存,內存有限,參數太大也沒什麼卵用。「收消息」 print("客戶端發來的消息是",data) conn.send(data.upper()) # 發送給客戶端TCP數據(send在待發送數據量大於己端緩存區剩餘空間時,數據丟失,不會發完) conn.close() #關閉客戶端套接字 phone.close() #關閉服務端套接字
#客戶端 import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #AF_INET基於網絡類型的套接字家族;SOCK_STREAM,TCP協議。「買手機」 phone.connect(("127.0.0.1",8080)) while True: #通訊循環 msg=input(">>:").strip() phone.send(msg.encode("utf8")) #客戶端發送消息須要是bytes格式。由於發送消息是在應用層上,應用層發送給本機網絡層-->鏈路層-->物理層,物理層是經過電信號二進制來交換信息 data=phone.recv(1024) #接收的服務端消息 print(data) phone.close() #關閉客戶端套接字
可是上述又有問題了
1.當客戶端發送中文時,顯示的不是中文而是二進制。
解決方式:在客戶端和服務端打印輸出消息時將消息用utf8解碼。
2.當客戶端發送的內容爲空時,客戶端卡掉。由於客戶端發送給服務端消息等待服務端反饋,發送更改成大寫字母打印內容。但客戶端發送的爲空,服務端一直接收不了,也沒法反饋給客戶端,客戶端也一直在等待,因此客戶端卡掉了。
解決方法:客戶端加一條判斷語句if not msg:continue。
3.當客戶端停止操做程序時,window下服務端報錯,linux下服務器一直收空。
解決方法:服務端加上try...except Exception:break來避免報錯。可是錯誤避免了,服務端也跟着結束程序了。
服務端啓動後不能停止程序,因此加上while True: 語句來實現不一樣的客戶端帳號再次訪問時能正常工做,由於可能訪問的客戶端ip地址不一樣,因此while True: 語句應該放置在 接收客戶端的連接和IP地址conn,addr=phone.accept()的語句上方。
最終程序優化以下:
#服務端 import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #AF_INET基於網絡類型的套接字家族;SOCK_STREAM,TCP協議。「買手機」 #phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #REUSEADDR,從新使用地址。 phone.bind(("127.0.0.1",8080)) #綁定ip地址,綁定(主機,端口號)到套接字,127.0.0.1,8080參數ip地址和端口號。「綁定手機」 phone.listen(5) #開始TCP監聽,同時掛起5個客戶端帳戶,通常定義參數,寫入配置文件中,可進行修改。「開機」 print("start...") while True: conn,addr=phone.accept() #接收客戶端的連接和IP地址;被動接受TCP客戶端的鏈接,(阻塞式)等待鏈接的到來。「等待電話的連接」 print("客戶端的信息是(電話線路是)",conn) print("客戶端的ip地址和端口是(客戶端手機號是)",addr) while True: #通訊循環 try: data=conn.recv(1024) #接收的客戶端TCP數據。此時1024爲最大設置,收消息來自緩存,內存有限,參數太大也沒什麼卵用。「收消息」 print("客戶端發來的消息是",data.decode("utf8")) conn.send(data.upper()) # 發送給客戶端TCP數據並大寫(send在待發送數據量大於己端緩存區剩餘空間時,數據丟失,不會發完) except: break conn.close() #關閉客戶端套接字 phone.close() #關閉服務端套接字
#客戶端 import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #AF_INET基於網絡類型的套接字家族;SOCK_STREAM,TCP協議。「買手機」 phone.connect(("127.0.0.1",8080)) while True: #通訊循環 msg=input(">>:").strip() if not msg:continue phone.send(msg.encode("utf8")) #客戶端發送消息須要是bytes格式。由於發送消息是在應用層上,應用層發送給本機網絡層-->鏈路層-->物理層,物理層是經過電信號二進制來交換信息 data=phone.recv(1024) #接收的服務端消息 print(data.decode("utf8")) phone.close() #關閉客戶端套接字
當重啓服務端程序時會出現錯誤:OSError: [WinError 10048] 一般每一個套接字地址(協議/網絡地址/端口)只容許使用一次。
這個是因爲你的服務端仍然存在四次揮手的time_wait狀態在佔用地址(若是不懂,請深刻研究1.tcp三次握手,四次揮手
2.syn洪水攻擊 3.服務器高併發狀況下會有大量的time_wait狀態的優化方法)
解決方法一:在服務端phone.bind(("127.0.0.1",8080))語句前加phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
解決方法二:
發現系統存在大量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 時間
#服務端 import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_port=("127.0.0.1",8080) phone.bind(ip_port) phone.listen(5) con,addr=phone.accept() data1=con.recv(1024) data2=con.recv(1024) print("第一次接收到客戶端發送的信息是",data1) print("第二次接收到客戶端發送的信息是",data2) con.close() phone.close()
#客戶端 import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(("127.0.0.1",8080)) phone.send("hello world".encode("utf8")) phone.send("haha".encode("utf8")) phone.close()
運行服務端結果:
第一次接收到客戶端發送的信息是 b'hello worldhaha' 第二次接收到客戶端發送的信息是 b''
客戶端第一次發送的hello world和第二次發送的haha粘在一塊兒了。
粘包緣由:
發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。
基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束。
所謂粘包問題主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的。
此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。
TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無消息保護邊界的。
tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端老是在收到ack時纔會清除緩衝區內容。數據是可靠的,可是會粘包。
兩種狀況下會發生粘包:
一、發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包)
#服務端 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()
#客戶端 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('xuyaping'.encode('utf-8'))
運行程序結果:
-----> helloxuyap -----> ing
二、接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包)
#服務端 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(20)#下次收的時候,會先取舊的數據,而後取新的 print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()
#客戶端 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 xuyaping'.encode('utf-8')) s.send('haha'.encode('utf-8'))
服務端運行結果:
-----> he -----> llo xuyapinghaha
拆包的發生狀況
當發送端緩衝區的長度大於網卡的MTU時,tcp會將此次發送的數據拆成幾個數據包發送出去。
簡陋方法:加time.sleep() 睡眠一段時間比網絡延遲時間長,先傳送第一次發送給服務端的信息。
#服務端 import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ip_port = ("127.0.0.1", 8080) phone.bind(ip_port) phone.listen(5) con, addr = phone.accept() data1 = con.recv(1024) data2 = con.recv(1024) print("第一次接收到客戶端發送的信息是", data1) print("第二次接收到客戶端發送的信息是", data2) con.close() phone.close()
#客戶端 import socket import time phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(("127.0.0.1", 8080)) phone.send("hello world".encode("utf8")) time.sleep(3) phone.send("haha".encode("utf8")) phone.close()
服務端運行結果:
第一次接收到客戶端發送的信息是 b'hello world' 第二次接收到客戶端發送的信息是 b'haha'
睡3s,比網絡延遲長,hello world先被送走,haha再次被送走。解決了粘包問題。
可是程序運行速度太慢...
優化版:根據每次收到的消息字節長度來進行處理。
#服務端 import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ip_port = ("127.0.0.1", 8080) phone.bind(ip_port) phone.listen(5) con, addr = phone.accept() data1 = con.recv(10) #客戶端第一次發送10個字節的消息 data2 = con.recv(1024) print("第一次接收到客戶端發送的信息是", data1) print("第二次接收到客戶端發送的信息是", data2) con.close() phone.close()
#客戶端 import socket import time phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(("127.0.0.1", 8080)) phone.send("helloworld".encode("utf8")) phone.send("haha".encode("utf8")) phone.close()
運行服務器結果:
第一次接收到客戶端發送的信息是 b'helloworld' 第二次接收到客戶端發送的信息是 b'haha'
你本身發送的到達對方後你的軟件不知道數據內容。開發程序須要你本身定義協議,可是以上都沒有本身定義協議,只是把數據發送出去而已。因此須要模仿協議格式來爲你的應用數據封裝一個應用數據的頭。
報頭:1.固定長度,本身知道本身的數據長度,規避粘包問題。
2.包含對將要發送數據的描述信息。
基於tcp的套接字實現遠程執行命令的操做
上述詳細代碼:http://www.cnblogs.com/xuyaping/p/6803732.html
在以上程序的基礎上解決粘包問題。
#服務端運行在linux系統下,固然也能夠在Windows系統下 #encoding:utf8 import socket import subprocess import struct phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_port=("192.168.85.129",8081) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(ip_port) phone.listen(5) while True: conn,addr=phone.accept() print("client addr:",addr) while True: try: cmd=conn.recv(1024) res=subprocess.Popen(cmd.decode("utf8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) #客戶端收到服務端發出的命令結果時,不知道接收多少長度的命令結果。只有服務端知道,因此服務端需發送給客戶數據長度和數據信息。 out_res=res.stdout.read() err_res=res.stderr.read() data_size=len(out_res)+len(err_res) #發送報頭 conn.send(struct.pack("i",data_size)) #仍是不知道數據長度,借用struct模塊,將數據長度打成固定長度爲4的bytes #發送數據部分 conn.send(out_res) conn.send(err_res) #一連發3個數據信息,間隔不少,內容又不多,確定會發生粘包問題,可是是你本身定的協議,報頭協議就是4個字節 except Exception: break conn.close() phone.close()
#客戶端 import socket import struct phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(("192.168.85.129",8081)) while True: cmd=input(">>:").strip() if not cmd:continue phone.send(cmd.encode("utf8")) phone.send('recv_ready'.encode('utf-8')) #收報頭 baotou=phone.recv(4) data_size=struct.unpack("i",baotou)[0] #將報頭數據長度解出來,解出來的結果是元組 #收數據 recv_size=0 recv_data= b"" while recv_size < data_size: #不斷的收數據,直到收到的數據長度總和大於等於data_size data=phone.recv(1024) recv_size+=len(data) recv_data+=data print(recv_data.decode("utf8")) #當服務端運行在windows系統下,utf8改爲gbk phone.close()
但上述解決粘包問題一樣不完善,多了中間的網絡延遲,卡着收確認信息。程序的運行速度遠快於網絡傳輸速度,因此在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗。
最終優化版:自定義報頭解決粘包問題
爲字節流加上自定義固定長度報頭,報頭中包含字節流長度,而後一次send到對端,對端在接收時,先從緩存中取出定長的報頭,而後再取真實數據。
上述涉及到struct模塊的使用。
struct:
該模塊能夠把一個類型,如數字,轉成固定長度的bytes
struct.pack('i',1111111111111)
struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是第二個參數的範圍。
詳細用法可參考:http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
使用struct模塊解決粘包的問題,定製本身的報頭:
使用struct模塊,對數據大小長度是有限制的,超過這個數據範圍,報錯。單純的把數據長度打成bytes格式極可能會超出範圍,並且報頭不僅有數據長度一個內容,還有各類信息等等...
此時須要將數據信息寫成字典的形式來避免數據大小超出範圍,而且須要json做爲中介來實現struct模塊的使用。
head_dir={'data_size':111111111111111111111111111,'hash':None,'filename':'a.txt'}
1.使用json模塊轉成json格式的字符串
import json head_json=json.dumps(head_dic) print(head_json) --->{'data_size':111111111111111111111111111,'hash':None,'filename':'a.txt'} #json格式的字符串
2.json字符串格式轉成bytes格式
head_bytes=head_json.encode('utf8') print(head_bytes) --->b{'data_size':111111111111111111111111111,'hash':None,'filename':'a.txt'}
此時bytes格式就是報頭,struct.pack將報頭bytes格式的數據大小打包成固定大小爲4個字節:
head_len=struct.pack('i',len(head_bytes))
最終優化程序:
#服務端 #運行在linux環境下。也可自行選擇在windows系統下,改一下客戶端的解碼方式爲gbk便可 #encoding:utf8 import socket import subprocess import struct import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip_port=("192.168.85.128",8080) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(ip_port) phone.listen(5) while True: conn,addr=phone.accept() print("client addr:",addr) while True: try: cmd=conn.recv(1024) res=subprocess.Popen(cmd.decode("utf8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out_res=res.stdout.read() err_res=res.stderr.read() data_size=len(out_res)+len(err_res) head_dir = {"data_size": data_size} # 將目前知道的信息數據長度寫入字典中 head_json = json.dumps(head_dir) # 字典不能直接struct.pack打包,使用json轉化爲json字符串格式 head_bytes = head_json.encode("utf8") # 將json字符串格式轉化成bytes格式 head_len=len(head_bytes) #part1:發送報頭的長度 conn.send(struct.pack("i",head_len)) #part2:再發送報頭 conn.send(head_bytes) #part3:最後發送數據部分 conn.send(out_res) conn.send(err_res) except Exception: break conn.close() phone.close()
#客戶端 import socket import struct import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(("192.168.85.128",8080)) while True: cmd=input(">>:").strip() if not cmd:continue phone.send(cmd.encode("utf8")) phone.send('recv_ready'.encode('utf-8')) #part1:收報頭長度 head_struct=phone.recv(4) #接收4個字節,服務端發送過來的報頭的長度 head_len=struct.unpack("i",head_struct)[0] #struct解包爲元組格式,元組索引號爲1的對呀的value值爲報頭長度 #part2:收報頭 head_bytes=phone.recv(head_len) #客戶端接收報頭,head_len個字節的長度的字節 head_json=head_bytes.decode("utf8") #將報頭字節格式解碼成字符串格式 print(head_json) #part3:收數據 head_dir = json.loads(head_json) # 將json字符串格式反序列成元組格式 data_size = head_dir["data_size"] recv_size=0 recv_data= b"" while recv_size < data_size: #不斷的收數據,直到收到的數據長度總和大於等於data_size data=phone.recv(1024) recv_size+=len(data) recv_data+=data print(recv_data.decode("utf8")) phone.close()
使用模塊socket同一時間下只能實現一個客戶端與服務端的交互,無法實現二個客戶端與服務端的交互。
基於該博客 七 基於TCP的套接字 的內容,最終優化版程序運行服務端,運行客戶端1,再運行一次相同代碼的客戶端2發現客戶端2
不能運行,輸入msg後就卡住不能運行。是由於服務端的通訊循環一直和客戶端1創建連接,客戶端2不能和服務端進行通訊循環。
socketserver模塊兩大類解決兩個問題:
server類:解決一直運行提供服務(鏈接循環)
request類:解決基於一個連接的通訊循環
使用socketserver模塊
#服務端,此時服務端程序不完整,只是爲了查看部分參數究竟是什麼 import socketserver class FTPserver(socketserver.BaseRequestHandler): #繼承的類socketserver.BaseRequestHandler是固定的 def handle(self): #定義的方法handle也是固定的 print("self:",self) print("self.request:",self.request) #self.request即一個連接,至關於conn print("self.server:",self.server) #self.server即套接字對象 print("self.client_address:",self.client_address) #即客戶端地址 if __name__=="__main__": obj=socketserver.ThreadingTCPServer(("127.0.0.1",8080),FTPserver) #ThreadingTCPServer線程,自動觸發類FTPserver下的handle函數的運行。類的實例化:詳細的過程請查看源碼,各類繼承。 obj.serve_forever() #連接循環,對象的調用 #下面爲運行服務端而後再運行客戶端後輸出的結果: --->self: <__main__.FTPserver object at 0x0000000001E3A8D0> self.request: <socket.socket fd=204, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 53195)> self.server: <socketserver.ThreadingTCPServer object at 0x0000000001E2D5C0> self.client_address: ('127.0.0.1', 53195)
#客戶端 import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(("127.0.0.1",8080)) while True: msg=input(">>:").strip() if not msg:continue phone.send(msg.encode("utf8")) data=phone.recv(1024) print(data) phone.close()
使用socketserver模塊實現併發
#服務端: import socketserver class MyFTP(socketserver.BaseRequestHandler): def handle(self): while True: data = self.request.recv(1024) self.request.send(data.upper()) if __name__ == '__main__': obj = socketserver.ThreadingTCPServer(('127.0.0.1',8484),MyFTP) #完成了bind綁定ip+port和listen監聽的操做。 obj.serve_forever() #永遠接收連接。完成了accept的操做
#每一個客戶端: import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(('127.0.0.1',8484)) #服務器ip地址和端口號 while True: #通訊循環 msg = input('>>:').strip() if msg == '':continue phone.send(msg.encode('utf-8')) #發送消息 data = phone.recv(1024) #接收消息,從緩存中每次最大取字節爲1024個字節 print(data) phone.close() #結束通訊
用戶數據報協議,無鏈接,面向消息的,自帶報頭(發空沒事,不會粘包)。
tcp和udp差異:
1.tcp是可靠傳輸,udp是不可靠傳輸。udp是無連接的,發消息根本無論對方收不收到,發完就結束。當udp客戶端先啓動再啓動服
務端也不會報錯,可是信息就會丟失。
2.tcp可靠是由於有連接,發包有迴應。udp沒連接,因此不須要listen,那麼也不要接收連接accept。
3.udp能夠收空而且不報錯。由於udp表面上收的是空,可是是報頭,實質上不爲空。
基於udp套接字的示例
#服務端: import socket host_ip = '127.0.0.1' host_port = 8080 udp_server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server.bind((host_ip,host_port)) while True: conn, addr = udp_server.recvfrom(1024) print(conn, addr) udp_server.sendto(conn.upper(), addr)
#客戶端: import socket host_ip = '127.0.0.1' host_port = 8080 udp_client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg = input('>>:').strip() udp_client.sendto(msg.encode('utf-8'),(host_ip,host_port)) conn, addr = udp_client.recvfrom(1024) print(conn.decode('utf-8'))