操做系統:(Operating System,簡稱OS)是管理和控制計算機硬件與軟件資源的計算機程序,是直接運行在「裸機」上的最基本的系統軟件,任何其餘軟件都必須在操做系統的支持下才能運行。html
精簡的說的話,操做系統就是一個協調、管理和控制計算機硬件資源和軟件資源的控制程序。操做系統所處的位置如圖1python
#操做系統位於計算機硬件與應用軟件之間,本質也是一個軟件。操做系統由操做系統的內核(運行於內核態,管理硬件資源)以及系統調用(運行於用戶態,爲應用程序員寫的應用程序提供系統調用接口)兩部分組成,因此,單純的說操做系統是運行於內核態的,是不許確的。
圖1程序員
細說的話,操做系統應該分紅兩部分功能:算法
#一:隱藏了醜陋的硬件調用接口(鍵盤、鼠標、音箱等等怎麼實現的,就不須要你管了),爲應用程序員提供調用硬件資源的更好,更簡單,更清晰的模型(系統調用接口)。應用程序員有了這些接口後,就不用再考慮操做硬件的細節,專心開發本身的應用程序便可。 例如:操做系統提供了文件這個抽象概念,對文件的操做就是對磁盤的操做,有了文件咱們無需再去考慮關於磁盤的讀寫控制(好比控制磁盤轉動,移動磁頭讀寫數據等細節), #二:將應用程序對硬件資源的競態請求變得有序化 例如:不少應用軟件實際上是共享一套計算機硬件,比方說有可能有三個應用程序同時須要申請打印機來輸出內容,那麼a程序競爭到了打印機資源就打印,而後多是b競爭到打印機資源,也多是c,這就致使了無序,打印機可能打印一段a的內容而後又去打印c...,操做系統的一個功能就是將這種無序變得有序。
注:計算機(硬件)->os->應用軟件shell
有關操做系統詳細的介紹和原理請看這裏>>>https://www.cnblogs.com/jin-xin/articles/10078845.html,不是大家如今這個階段須要學習的,仍是老樣子,先大體瞭解一下就行啦。編程
你本身如今徹底能夠寫一些小程序了,可是前面的學習和練習,咱們寫的代碼都是在本身的電腦上運行的,雖然咱們學過了模塊引入,文件引入import等等,我能夠在程序中獲取到另外一個文件的內容,對吧,可是那麼忽然有一天,你的朋友和你說:"把你電腦上的一個文件經過你本身寫的程序發送到個人電腦上",這時候怎麼辦?你是否是會想,what?這怎麼搞?就在此時,忽然靈感來了,我能夠經過qq、雲盤、微信等發送給他啊,但是人家說了,讓你用本身寫的程序啊,嗯,這是個問題,此時又來一個靈感,我給他發送文件確定是經過網絡啊,這就產生了網絡,對吧,那我怎麼讓個人程序可以經過網絡來聯繫到個人朋友呢,而且把文件發送給他呢,那麼查了一下,發現網絡通訊經過socket能夠搞,可是怎麼搞呢?首先,查詢結果是對的,socket就是網絡通訊的工具,也叫套接字,任何一門語言都有socket,他不是任何一個語言的專有名詞,而是你們經過本身的程序與其餘電腦進行網絡通訊的時候都用它。知道爲何要學習socket了吧~~朋友們~~而你使用本身的電腦和別人的電腦進行聯繫併發送消息或者文件等操做就叫作網絡通訊。json
客戶端英文名稱:Client,小程序
瀏覽器英文名稱:Browser.windows
服務端英文名稱:Server,C\S架構就是說的Client\Server架構,B\S架構就是說的Browser\Server架構,。設計模式
a.硬件C\S架構:打印機。
b.軟件C\S架構:QQ、微信、優酷、暴風影音、瀏覽器(IE、火狐,360瀏覽器等)。
其中瀏覽器又比較特殊,不少網站是基於瀏覽器來進行訪問的,瀏覽器和各個網站服務端進行的通信方式又常被成爲B\S架構(B/S架構也是C/S架構的一種)。
詳見網絡通訊原理:https://www.cnblogs.com/jin-xin/articles/10067177.html
看socket以前,先來回顧一下五層通信流程:
但實際上從傳輸層開始以及如下,都是操做系統幫我們完成的,下面的各類包頭封裝的過程,用我們去一個一個作麼?NO!
Socket又稱爲套接字,它是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。當咱們使用不一樣的協議進行通訊時就得使用不一樣的接口,還得處理不一樣協議的各類細節,這就增長了開發的難度,軟件也不易於擴展(就像咱們開發一套公司管理系統同樣,報帳、會議預約、請假等功能不須要單獨寫系統,而是一個系統上多個功能接口,不須要知道每一個功能如何去實現的)。因而UNIX BSD就發明了socket這種東西,socket屏蔽了各個協議的通訊細節,使得程序員無需關注協議自己,直接使用socket提供的接口來進行互聯的不一樣主機間的進程的通訊。這就比如操做系統給咱們提供了使用底層硬件功能的系統調用,經過系統調用咱們能夠方便的使用磁盤(文件操做),使用內存,而無需本身去進行磁盤讀寫,內存管理。socket其實也是同樣的東西,就是提供了tcp/ip協議的抽象,對外提供了一套接口,同過這個接口就能夠統1、方便的使用tcp/ip協議的功能了。
其實站在你的角度上看,socket就是一個模塊。咱們經過調用模塊中已經實現的方法創建兩個進程之間的鏈接和通訊。也有人將socket說成ip+port,由於ip是用來標識互聯網中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程序。 因此咱們只要確立了ip和port就能找到一個應用程序,而且使用socket模塊來與之通訊。
套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 所以,有時人們也把套接字稱爲「伯克利套接字」或「BSD 套接字」。一開始,套接字被設計用在同 一臺主機上多個應用程序之間的通信。這也被稱進程間通信,或 IPC。套接字有兩種(或者稱爲有兩個種族),分別是基於文件型的和基於網絡型的。
基於文件類型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,能夠經過訪問同一個文件系統間接完成通訊
基於網絡類型的套接字家族
套接字家族的名字:AF_INET
(還有AF_INET6被用於ipv6,還有一些其餘的地址家族,不過,他們要麼是隻用於某個平臺,要麼就是已經被廢棄,或者是不多被使用,或者是根本沒有實現,全部地址家族中,AF_INET是使用最普遍的一個,python支持不少種地址家族,可是因爲咱們只關心網絡編程,因此大部分時候我麼只使用AF_INET)
TCP(Transmission Control Protocol)可靠的、面向鏈接的協議(eg:打電話)、傳輸效率低全雙工通訊(發送緩存&接收緩存)、面向字節流。使用TCP的應用:Web瀏覽器;文件傳輸程序。
UDP(User Datagram Protocol)不可靠的、無鏈接的服務,傳輸效率高(發送前時延小),一對1、一對多、多對1、多對多、面向報文(數據包),盡最大努力服務,無擁塞控制。使用UDP的應用:域名系統 (DNS);視頻流;IP語音(VoIP)。
個生活中的場景。你要打電話給一個朋友,先撥號,朋友聽到電話鈴聲後提起電話,這時你和你的朋友就創建起了鏈接,就能夠講話了。等交流結束,掛斷電話結束這次交談。 生活中的場景就解釋了這工做原理。
先從服務器端提及。服務器端先初始化Socket,而後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端鏈接。在這時若是有個客戶端初始化一個Socket,而後鏈接服務器(connect),若是鏈接成功,這時客戶端與服務器端的鏈接就創建了。客戶端發送數據請求,服務器端接收請求並處理請求,而後把迴應數據發送給客戶端,客戶端讀取數據,最後關閉鏈接,一次交互結束
細說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) 因爲 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() 建立一個與該套接字相關的文件
例子:通訊,鏈接循環
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1', 8888)) #0 ~ 65535 1024以前系統分配好的端口 綁定電話卡 phone.listen(5) # 同一時刻有5個請求,可是能夠有N多個連接。 開機。 while 1: # 循環鏈接客戶端 conn, client_addr = phone.accept() print(client_addr) while 1:# 循環收發消息 try: from_client_data = conn.recv(1024) # 一次接收的最大限制 bytes print(from_client_data.decode('utf-8')) conn.send(from_client_data + b'SB') # Exception:萬能異常 ConnectionResetError:[WinError 10054] 遠程主機強迫關閉了一個現有的鏈接。或者ConnectionAbortedError:[WinError 10053] 你的主機中的軟件停止了一個已創建的鏈接。 except Exception as e: print(e) break conn.close() phone.close()
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 買電話 phone.connect(('127.0.0.1', 8888)) # 與客戶端創建鏈接, 撥號 while 1: client_data = input('>>>') phone.send(client_data.encode('utf-8')) from_server_data = phone.recv(1024) print(from_server_data.decode('utf-8')) phone.close() # 掛電話
詳解recv的工做原理
''' 源碼解釋: Receive up to buffersize bytes from the socket. 接收來自socket緩衝區的字節數據, For the optional flags argument, see the Unix manual. 對於這些設置的參數,能夠查看Unix手冊。 When no data is available, block untilat least one byte is available or until the remote end is closed. 當緩衝區沒有數據可取時,recv會一直處於阻塞狀態,直到緩衝區至少有一個字節數據可取,或者遠程端關閉。 When the remote end is closed and all data is read, return the empty string. 關閉遠程端並讀取全部數據後,返回空字符串。 ''' ----------服務端------------: # 1,驗證服務端緩衝區數據沒有取完,又執行了recv執行,recv會繼續取值。 import socket phone =socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.bind(('127.0.0.1',8080)) phone.listen(5) conn, client_addr = phone.accept() from_client_data1 = conn.recv(2) print(from_client_data1) from_client_data2 = conn.recv(2) print(from_client_data2) from_client_data3 = conn.recv(1) print(from_client_data3) conn.close() phone.close() # 2,驗證服務端緩衝區取完了,又執行了recv執行,此時客戶端20秒內不關閉的前提下,recv處於阻塞狀態。 import socket phone =socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.bind(('127.0.0.1',8080)) phone.listen(5) conn, client_addr = phone.accept() from_client_data = conn.recv(1024) print(from_client_data) print(111) conn.recv(1024) # 此時程序阻塞20秒左右,由於緩衝區的數據取完了,而且20秒內,客戶端沒有關閉。 print(222) conn.close() phone.close() # 3 驗證服務端緩衝區取完了,又執行了recv執行,此時客戶端處於關閉狀態,則recv會取到空字符串。 import socket phone =socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.bind(('127.0.0.1',8080)) phone.listen(5) conn, client_addr = phone.accept() from_client_data1 = conn.recv(1024) print(from_client_data1) from_client_data2 = conn.recv(1024) print(from_client_data2) from_client_data3 = conn.recv(1024) print(from_client_data3) conn.close() phone.close() ------------客戶端------------ # 1,驗證服務端緩衝區數據沒有取完,又執行了recv執行,recv會繼續取值。 import socket import time phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) phone.send('hello'.encode('utf-8')) time.sleep(20) phone.close() # 2,驗證服務端緩衝區取完了,又執行了recv執行,此時客戶端20秒內不關閉的前提下,recv處於阻塞狀態。 import socket import time phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) phone.send('hello'.encode('utf-8')) time.sleep(20) phone.close() # 3,驗證服務端緩衝區取完了,又執行了recv執行,此時客戶端處於關閉狀態,則recv會取到空字符串。 import socket import time phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) phone.send('hello'.encode('utf-8')) phone.close()
遠程執行命令的示例:
import socket import subprocess phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.bind(('127.0.0.1',8080)) phone.listen(5) while 1 : # 循環鏈接客戶端 conn, client_addr = phone.accept() print(client_addr) while 1: try: cmd = conn.recv(1024) ret = subprocess.Popen(cmd.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) correct_msg = ret.stdout.read() error_msg = ret.stderr.read() conn.send(correct_msg + error_msg) except ConnectionResetError: break conn.close() phone.close()
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 買電話 phone.connect(('127.0.0.1',8080)) # 與客戶端創建鏈接, 撥號 while 1: cmd = input('>>>') phone.send(cmd.encode('utf-8')) from_server_data = phone.recv(1024) print(from_server_data.decode('gbk')) phone.close() # 掛電話
udp是無連接的,先啓動哪一端都不會報錯
UDP下的socket通信流程
先從服務器端提及。服務器端先初始化Socket,而後與端口綁定(bind),recvform接收消息,這個消息有兩項,消息內容和對方客戶端的地址,而後回覆消息時也要帶着你收到的這個客戶端的地址,發送回去,最後關閉鏈接,一次交互結束
上代碼感覺一下,須要建立兩個文件,文件名稱隨便起,爲了方便看,個人兩個文件名稱爲udp_server.py(服務端)和udp_client.py(客戶端),將下面的server端的代碼拷貝到udp_server.py文件中,將下面cliet端的代碼拷貝到udp_client.py的文件中,而後先運行udp_server.py文件中的代碼,再運行udp_client.py文件中的代碼,而後在pycharm下面的輸出窗口看一下效果。
sever端代碼示例
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) #DGRAM:datagram 數據報文的意思,象徵着UDP協議的通訊方式 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)
import socket BUFSIZE=1024 udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic={ 'taibai':('127.0.0.1',8081), 'Jedan':('127.0.0.1',8081), 'Jack':('127.0.0.1',8081), 'John':('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])# 必須帶着本身的地址,這就是UDP不同的地方,不須要創建鏈接,可是要帶着本身的地址給服務端,不然服務端沒法判斷是誰給我發的消息,而且不知道該把消息回覆到什麼地方,由於咱們之間沒有創建鏈接通道 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()
接下來,給你們說一個真實的例子,也就是實際當中應用的,那麼這是個什麼例子呢?就是咱們電腦系統上的時間,windows系統的時間是和微軟的時間服務器上的時間同步的,而mac本是和蘋果服務商的時間服務器同步的,這是怎麼作的呢,首先他們的時間服務器上的時間是和國家同步的,大家用個人系統,那麼大家的時間只要和我時間服務器上的時間同步就好了,對吧,我時間服務器是否是提供服務的啊,至關於一個服務端,咱們的電腦就至關於客戶端,就是經過UDP來搞的。
自制時間服務器的代碼示例:
from socket import * from time import strftime import time 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) stru_time = time.localtime() #當前的結構化時間 if not msg: time_fmt = '%Y-%m-%d %X' else: time_fmt = msg.decode('utf-8') back_msg = strftime(time_fmt,stru_time) print(back_msg,type(back_msg)) tcp_server.sendto(back_msg.encode('utf-8'), addr) tcp_server.close()
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('當前日期:',str(data,encoding='utf-8'))
講粘包以前先看看socket緩衝區的問題:
每一個 socket 被建立後,都會分配兩個緩衝區,輸入緩衝區和輸出緩衝區。 write()/send() 並不當即向網絡中傳輸數據,而是先將數據寫入緩衝區中,再由TCP協議將數據從緩衝區發送到目標機器。一旦將數據寫入到緩衝區,函數就能夠成功返回,無論它們有沒有到達目標機器,也無論它們什麼時候被髮送到網絡,這些都是TCP協議負責的事情。 TCP協議獨立於 write()/send() 函數,數據有可能剛被寫入緩衝區就發送到網絡,也可能在緩衝區中不斷積壓,屢次寫入的數據被一次性發送到網絡,這取決於當時的網絡狀況、當前線程是否空閒等諸多因素,不禁程序員控制。 read()/recv() 函數也是如此,也從輸入緩衝區中讀取數據,而不是直接從網絡中讀取。 這些I/O緩衝區特性可整理以下: 1.I/O緩衝區在每一個TCP套接字中單獨存在; 2.I/O緩衝區在建立套接字時自動生成; 3.即便關閉套接字也會繼續傳送輸出緩衝區中遺留的數據; 4.關閉套接字將丟失輸入緩衝區中的數據。 輸入輸出緩衝區的默認大小通常都是 8K,能夠經過 getsockopt() 函數獲取: 1.unsigned optVal; 2.int optLen = sizeof(int); 3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen); 4.printf("Buffer length: %d\n", optVal);
import socket server = socket.socket() server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 重用ip地址和端口 server.bind(('127.0.0.1',8010)) server.listen(3) print(server.getsockopt(socket.SOL_SOCKET,socket.SO_SNDBUF)) # 輸出緩衝區大小 print(server.getsockopt(socket.SOL_SOCKET,socket.SO_RCVBUF)) # 輸入緩衝區大小
須知:只有TCP有粘包現象,UDP永遠不會粘包!
發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不一樣的。怎樣定義消息呢?能夠認爲對方一次性write/send的數據爲一個消息,須要明白的是當對方send一條信息的時候,不管底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。 例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束 所謂粘包問題主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的。 此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。 TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無消息保護邊界的。 UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供高效率服務。不會使用塊的合併優化算法,, 因爲UDP支持的是一對多的模式,因此接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。 tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略 udp的recvfrom是阻塞的,一個recvfrom(x)必須對惟一一個sendinto(y),收完了x個字節的數據就算完成,如果y>x數據就丟失,這意味着udp根本不會粘包,可是會丟數據,不可靠 tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端老是在收到ack時纔會清除緩衝區內容。數據是可靠的,可是會粘包。
1,接收方沒有及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包)
import socket import subprocess phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1', 8080)) phone.listen(5) while 1: # 循環鏈接客戶端 conn, client_addr = phone.accept() print(client_addr) while 1: try: cmd = conn.recv(1024) ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) correct_msg = ret.stdout.read() error_msg = ret.stderr.read() conn.send(correct_msg + error_msg) except ConnectionResetError: break conn.close() phone.close()
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 買電話 phone.connect(('127.0.0.1',8080)) # 與客戶端創建鏈接, 撥號 while 1: cmd = input('>>>') phone.send(cmd.encode('utf-8')) from_server_data = phone.recv(1024) print(from_server_data.decode('gbk')) phone.close() # 因爲客戶端發的命令獲取的結果大小已經超過1024,那麼下次在輸入命令,會繼續取上次殘留到緩存區的數據。
2,發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據也很小,會合到一塊兒,產生粘包)
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1', 8080)) phone.listen(5) conn, client_addr = phone.accept() frist_data = conn.recv(1024) print('1:',frist_data.decode('utf-8')) # 1: helloworld second_data = conn.recv(1024) print('2:',second_data.decode('utf-8')) conn.close() phone.close()
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(('127.0.0.1', 8080)) phone.send(b'hello') phone.send(b'world') phone.close() # 兩次返送信息時間間隔過短,數據小,形成服務端一次收取
先介紹一下struct模塊:
該模塊能夠把一個類型,如數字,轉成固定長度的bytes
import struct # 將一個數字轉化成等長度的bytes類型。 ret = struct.pack('i', 183346) print(ret, type(ret), len(ret)) # 經過unpack反解回來 ret1 = struct.unpack('i',ret)[0] print(ret1, type(ret1), len(ret1)) # 可是經過struct 處理不能處理太大 ret = struct.pack('l', 4323241232132324) print(ret, type(ret), len(ret)) # 報錯
方案一:low版。
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把本身將要發送的字節流總數按照固定字節發送給接收端後面跟上總數據,而後接收端先接收固定字節的總字節流,再來一個死循環接收完全部數據。
import socket import subprocess import struct phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1', 8080)) phone.listen(5) while 1: conn, client_addr = phone.accept() print(client_addr) while 1: try: cmd = conn.recv(1024) ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) correct_msg = ret.stdout.read() error_msg = ret.stderr.read() # 1 製做固定報頭 total_size = len(correct_msg) + len(error_msg) header = struct.pack('i', total_size) # 2 發送報頭 conn.send(header) # 發送真實數據: conn.send(correct_msg) conn.send(error_msg) except ConnectionResetError: break conn.close() phone.close() # 可是low版本有問題: # 1,報頭不僅有總數據大小,而是還應該有MD5數據,文件名等等一些數據。 # 2,經過struct模塊直接數據處理,不能處理太大。
import socket import struct phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) while 1: cmd = input('>>>').strip() if not cmd: continue phone.send(cmd.encode('utf-8')) # 1,接收固定報頭 header = phone.recv(4) # 2,解析報頭 total_size = struct.unpack('i', header)[0] # 3,根據報頭信息,接收真實數據 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()
方案二:可自定製報頭版。
整個流程的大體解釋: 咱們能夠把報頭作成字典,字典裏包含將要發送的真實數據的描述信息(大小啊之類的),而後json序列化,而後用struck將序列化後的數據長度打包成4個字節。 咱們在網絡上傳輸的全部數據 都叫作數據包,數據包裏的全部數據都叫作報文,報文裏面不止有你的數據,還有ip地址、mac地址、端口號等等,其實全部的報文都有報頭,這個報頭是協議規定的,看一下 發送時: 先發報頭長度 再編碼報頭內容而後發送 最後發真實內容 接收時: 先手報頭長度,用struct取出來 根據取出的長度收取報頭內容,而後解碼,反序列化 從反序列化的結果中取出待取數據的描述信息,而後去取真實的數據內容
import socket import subprocess import struct import json phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1', 8080)) phone.listen(5) while 1: conn, client_addr = phone.accept() print(client_addr) while 1: try: cmd = conn.recv(1024) ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) correct_msg = ret.stdout.read() error_msg = ret.stderr.read() # 1 製做固定報頭 total_size = len(correct_msg) + len(error_msg) header_dict = { 'md5': 'fdsaf2143254f', 'file_name': 'f1.txt', 'total_size':total_size, } header_dict_json = json.dumps(header_dict) # str bytes_headers = header_dict_json.encode('utf-8') header_size = len(bytes_headers) header = struct.pack('i', header_size) # 2 發送報頭長度 conn.send(header) # 3 發送報頭 conn.send(bytes_headers) # 4 發送真實數據: conn.send(correct_msg) conn.send(error_msg) except ConnectionResetError: break conn.close() phone.close()
import socket import struct import json phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) while 1: cmd = input('>>>').strip() if not cmd: continue phone.send(cmd.encode('utf-8')) # 1,接收固定報頭 header_size = struct.unpack('i', phone.recv(4))[0] # 2,解析報頭長度 header_bytes = phone.recv(header_size) header_dict = json.loads(header_bytes.decode('utf-8')) # 3,收取報頭 total_size = header_dict['total_size'] # 3,根據報頭信息,接收真實數據 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()
FTP上傳下載文件的代碼(簡單版)
import socket import subprocess import json import struct phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1', 8001)) phone.listen(5) file_positon = r'd:\上傳下載' conn, client_addr = phone.accept() # # 1,接收固定4個字節 ret = conn.recv(4) # # 2,利用struct模塊將ret反解出head_dic_bytes的總字節數。 head_dic_bytes_size = struct.unpack('i',ret)[0] # # 3,接收 head_dic_bytes數據。 head_dic_bytes = conn.recv(head_dic_bytes_size) # 4,將head_dic_bytes解碼成json字符串格式。 head_dic_json = head_dic_bytes.decode('utf-8') # 5,將json字符串還原成字典模式。 head_dic = json.loads(head_dic_json) file_path = os.path.join(file_positon,head_dic['file_name']) with open(file_path,mode='wb') as f1: data_size = 0 while data_size < head_dic['file_size']: data = conn.recv(1024) f1.write(data) data_size += len(data) conn.close() phone.close()
import socket import struct import json import os phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 買電話 phone.connect(('127.0.0.1', 8001)) # 與客戶端創建鏈接, 撥號 # 1 制定file_info file_info = { 'file_path': r'D:\lnh.python\pyproject\PythonReview\網絡編程\08 文件的上傳下載\low版\aaa.mp4', 'file_name': 'aaa.mp4', 'file_size': None, } # 2 獲取並設置文件大小 file_info['file_size'] = os.path.getsize(file_info['file_path']) # 2,利用json將head_dic 轉化成字符串 head_dic_json = json.dumps(file_info) # 3,將head_dic_json轉化成bytes head_dic_bytes = head_dic_json.encode('utf-8') # 4,將head_dic_bytes的大小轉化成固定的4個字節。 ret = struct.pack('i', len(head_dic_bytes)) # 固定四個字節 # 5, 發送固定四個字節 phone.send(ret) # 6 發送head_dic_bytes phone.send(head_dic_bytes) # 發送文件: with open(file_info['file_path'],mode='rb') as f1: data_size = 0 while data_size < file_info['file_size']: # f1.read() 不能所有讀出來,並且也不能send所有,這樣send若是過大,也會出問題,保險起見,每次至多send(1024字節) every_data = f1.read(1024) data_size += len(every_data) phone.send(every_data) phone.close()
FTP上傳下載文件的代碼(升級版)(注:我們學完網絡編程就留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() server.py
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() client.py
#=========知識儲備========== #進度條的效果 [# ] [## ] [### ] [#### ] #指定寬度 print('[%-15s]' %'#') print('[%-15s]' %'##') print('[%-15s]' %'###') print('[%-15s]' %'####') #打印% print('%s%%' %(100)) #第二個%號表明取消第一個%的特殊意義 #可傳參來控制寬度 print('[%%-%ds]' %50) #[%-50s] print(('[%%-%ds]' %50) %'#') print(('[%%-%ds]' %50) %'##') print(('[%%-%ds]' %50) %'###') #=========實現打印進度條函數========== import sys import time def progress(percent,width=50): if percent >= 1: percent=1 show_str = ('%%-%ds' % width) % (int(width*percent)*'|') print('\r%s %d%%' %(show_str, int(100*percent)), end='') #=========應用========== data_size=1025 recv_size=0 while recv_size < data_size: time.sleep(0.1) #模擬數據的傳輸延遲 recv_size+=1024 #每次收1024 percent=recv_size/data_size #接收的比例 progress(percent,width=70) #進度條的寬度70
爲何要講socketserver?咱們以前寫的tcp協議的socket是否是一次只能和一個客戶端通訊,若是用socketserver能夠實現和多個客戶端通訊。它是在socket的基礎上進行了一層封裝,也就是說底層仍是調用的socket,在py2.7裏面叫作SocketServer也就是大寫了兩個S,在py3裏面就小寫了。後面咱們要寫的FTP做業,須要用它來實現併發,也就是同時能夠和多個客戶端進行通訊,多我的能夠同時進行上傳下載等。
import socketserver # 引入模塊 class MyServer(socketserver.BaseRequestHandler): # 類名隨便定義,可是必須繼承socketserver.BaseRequestHandler此類 def handle(self): # 寫一個handle方法,固定名字 while 1: # self.request 至關於conn管道 from_client_data = self.request.recv(1024).decode('utf-8') print(from_client_data) to_client_data = input('服務端回信息:').strip() self.request.send(to_client_data) if __name__ == '__main__': ip_port = ('127.0.0.1',8080) # socketserver.TCPServer.allow_reuse_address = True # 容許端口重用 server = socketserver.ThreadingTCPServer(ip_port,MyServer) # 對 socketserver.ThreadingTCPServer 類實例化對象,將ip地址,端口號以及本身定義的類名傳入,並返回一個對象 server.serve_forever() # 對象執行serve_forever方法,開啓服務端
源碼剖析
具體流程分析:
在整個socketserver這個模塊中,其實就幹了兩件事情:一、一個是循環創建連接的部分,每一個客戶連接均可以鏈接成功 二、一個通信循環的部分,就是每一個客戶端連接成功以後,要循環的和客戶端進行通訊。 看代碼中的:server=socketserver.ThreadingTCPServer(('127.0.0.1',8090),MyServer) 還記得面向對象的繼承嗎?來,你們本身嘗試着看看源碼: 查找屬性的順序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer 實例化獲得server,先找ThreadMinxIn中的__init__方法,發現沒有init方法,而後找類ThreadingTCPServer的__init__,在TCPServer中找到,在裏面建立了socket對象,進而執行server_bind(至關於bind),server_active(點進去看執行了listen) 找server下的serve_forever,在BaseServer中找到,進而執行self._handle_request_noblock(),該方法一樣是在BaseServer中 執行self._handle_request_noblock()進而執行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),而後執行self.process_request(request, client_address) 在ThreadingMixIn中找到process_request,開啓多線程應對併發,進而執行process_request_thread,執行self.finish_request(request, client_address) 上述四部分完成了連接循環,本部分開始進入處理通信部分,在BaseServer中找到finish_request,觸發咱們本身定義的類的實例化,去找__init__方法,而咱們本身定義的類沒有該方法,則去它的父類也就是BaseRequestHandler中找.... 源碼分析總結: 基於tcp的socketserver咱們本身定義的類中的 self.server即套接字對象 self.request即一個連接 self.client_address即客戶端地址 基於udp的socketserver咱們本身定義的類中的 self.request是一個元組(第一個元素是客戶端發來的數據,第二部分是服務端的udp套接字對象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>) self.client_address即客戶端地址
簡單分析一下實現方式:
1.字符串操做以及打印 —— 實現上傳下載的進度條功能
2.socketserver —— 實現ftp server端和client端的交互
3.struct模塊 —— 自定製報頭解決文件上傳下載過程當中的粘包問題
4.hashlib或者hmac模塊 —— 實現文件的一致性校驗和用戶密文登陸
5.os模塊 —— 實現目錄的切換及查看文件文件夾等功能
6.文件操做 —— 完成上傳下載文件及斷點續傳等功能
看一下流程圖: