一 操作系統基礎
操作系統:(Operating System,簡稱OS)是管理和控制計算機硬件與軟件資源的計算機程序,是直接運行在“裸機”上的最基本的系統軟件,任何其他軟件都必須在操作系統的支持下才能運行。
精簡的說的話,操作系統就是一個協調、管理和控制計算機硬件資源和軟件資源的控制程序。操作系統所處的位置如圖1
#操作系統位於計算機硬件與應用軟件之間,本質也是一個軟件。操作系統由操作系統的內核(運行於內核態,管理硬件資源)以及系統調用(運行於用戶態,為應用程序員寫的應用程序提供系統調用接口)兩部分組成,所以,單純的說操作系統是運行於內核態的,是不准確的。

圖1
細說的話,操作系統應該分成兩部分功能:
#一:隱藏了丑陋的硬件調用接口(鍵盤、鼠標、音箱等等怎么實現的,就不需要你管了),為應用程序員提供調用硬件資源的更好,更簡單,更清晰的模型(系統調用接口)。應用程序員有了這些接口后,就不用再考慮操作硬件的細節,專心開發自己的應用程序即可。 例如:操作系統提供了文件這個抽象概念,對文件的操作就是對磁盤的操作,有了文件我們無需再去考慮關於磁盤的讀寫控制(比如控制磁盤轉動,移動磁頭讀寫數據等細節), #二:將應用程序對硬件資源的競態請求變得有序化 例如:很多應用軟件其實是共享一套計算機硬件,比方說有可能有三個應用程序同時需要申請打印機來輸出內容,那么a程序競爭到了打印機資源就打印,然后可能是b競爭到打印機資源,也可能是c,這就導致了無序,打印機可能打印一段a的內容然后又去打印c...,操作系統的一個功能就是將這種無序變得有序。
注:計算機(硬件)->os->應用軟件
有關操作系統詳細的介紹和原理請看這里>>>https://www.cnblogs.com/jin-xin/articles/10078845.html,不是你們現在這個階段需要學習的,還是老樣子,先大致了解一下就行啦。
二 為什么學習socket
你自己現在完全可以寫一些小程序了,但是前面的學習和練習,我們寫的代碼都是在自己的電腦上運行的,雖然我們學過了模塊引入,文件引入import等等,我可以在程序中獲取到另一個文件的內容,對吧,但是那么突然有一天,你的朋友和你說:"把你電腦上的一個文件通過你自己寫的程序發送到我的電腦上",這時候怎么辦?你是不是會想,what?這怎么搞?就在此時,突然靈感來了,我可以通過qq、雲盤、微信等發送給他啊,可是人家說了,讓你用自己寫的程序啊,嗯,這是個問題,此時又來一個靈感,我給他發送文件肯定是通過網絡啊,這就產生了網絡,對吧,那我怎么讓我的程序能夠通過網絡來聯系到我的朋友呢,並且把文件發送給他呢,那么查了一下,發現網絡通信通過socket可以搞,但是怎么搞呢?首先,查詢結果是對的,socket就是網絡通信的工具,也叫套接字,任何一門語言都有socket,他不是任何一個語言的專有名詞,而是大家通過自己的程序與其他電腦進行網絡通信的時候都用它。知道為什么要學習socket了吧~~朋友們~~而你使用自己的電腦和別人的電腦進行聯系並發送消息或者文件等操作就叫做網絡通信。
三 CS架構,BS架構
客戶端英文名稱:Client,
瀏覽器英文名稱:Browser.
服務端英文名稱:Server.
C/S架構:基於客戶端與用戶端之間的架構。例如:QQ、微信、優酷、暴風影音等等。
-
優點:C/S架構的界面和操作非常豐富滿足客戶的個性化要求,安全性很容易保證,響應速度較快。
-
缺點:需要開發客戶端和服務器兩套程序,開發成本維護成本較高,兼容性差,用戶群固定等。
B/S架構:基於C/S架構的一種特殊的C/S架構,瀏覽器與服務端之間的架構。
-
優點:分布性強,客戶端幾乎無需維護,開發簡單,共享性強,維護簡單方便。
-
缺點:個性化低,安全性以及響應速度需要花費巨大設計成本。
小結:CS響應速度快,安全性強,一般應用於局域網中,但是開發維護成本高;BS可以實現跨平台,客戶端零維護,但是個性化能力低,響應速度較慢。所以有些單位日常辦公應用BS,在實際生產中使用CS結構。
四 osi七層。
詳見網絡通信原理:https://www.cnblogs.com/jin-xin/articles/10067177.html
五 socket
看socket之前,先來回顧一下五層通訊流程:

但實際上從傳輸層開始以及以下,都是操作系統幫咱們完成的,下面的各種包頭封裝的過程,用咱們去一個一個做么?NO!

Socket又稱為套接字,它是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。當我們使用不同的協議進行通信時就得使用不同的接口,還得處理不同協議的各種細節,這就增加了開發的難度,軟件也不易於擴展(就像我們開發一套公司管理系統一樣,報賬、會議預定、請假等功能不需要單獨寫系統,而是一個系統上多個功能接口,不需要知道每個功能如何去實現的)。於是UNIX BSD就發明了socket這種東西,socket屏蔽了各個協議的通信細節,使得程序員無需關注協議本身,直接使用socket提供的接口來進行互聯的不同主機間的進程的通信。這就好比操作系統給我們提供了使用底層硬件功能的系統調用,通過系統調用我們可以方便的使用磁盤(文件操作),使用內存,而無需自己去進行磁盤讀寫,內存管理。socket其實也是一樣的東西,就是提供了tcp/ip協議的抽象,對外提供了一套接口,同過這個接口就可以統一、方便的使用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和 UDP兩個協議)
6.1 TCP和UDP對比
TCP(Transmission Control Protocol)可靠的、面向連接的協議(eg:打電話)、傳輸效率低全雙工通信(發送緩存&接收緩存)、面向字節流。使用TCP的應用:Web瀏覽器;文件傳輸程序。
UDP(User Datagram Protocol)不可靠的、無連接的服務,傳輸效率高(發送前時延小),一對一、一對多、多對一、多對多、面向報文(數據包),盡最大努力服務,無擁塞控制。使用UDP的應用:域名系統 (DNS);視頻流;IP語音(VoIP)。
6.2 TCP協議下的socket
個生活中的場景。你要打電話給一個朋友,先撥號,朋友聽到電話鈴聲后提起電話,這時你和你的朋友就建立起了連接,就可以講話了。等交流結束,掛斷電話結束此次交談。 生活中的場景就解釋了這工作原理。

先從服務器端說起。服務器端先初始化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() 創建一個與該套接字相關的文件
第一版,單個客戶端與服務端通信(low版)
# 網絡通信與打電話(諾基亞)是一樣的。 import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 買電話 phone.bind(('127.0.0.1',8080)) # 0 ~ 65535 1024之前系統分配好的端口 綁定電話卡 phone.listen(5) # 同一時刻有5個請求,但是可以有N多個鏈接。 開機。 conn, client_addr = phone.accept() # 接電話 print(conn, client_addr, sep='\n') from_client_data = conn.recv(1024) # 一次接收的最大限制 bytes print(from_client_data.decode('utf-8')) conn.send(from_client_data.upper()) conn.close() # 掛電話 phone.close() # 關機
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 買電話 phone.connect(('127.0.0.1',8080)) # 與客戶端建立連接, 撥號 phone.send('hello'.encode('utf-8')) from_server_data = phone.recv(1024) print(from_server_data) phone.close() # 掛電話
第二版,通信循環
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() print(conn, client_addr, sep='\n') while 1: # 循環收發消息 try: from_client_data = conn.recv(1024) print(from_client_data.decode('utf-8')) conn.send(from_client_data + b'SB') 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: # 循環收發消息 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() # 掛電話
第三版, 通信,連接循環
import socket 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: from_client_data = conn.recv(1024) print(from_client_data.decode('utf-8')) conn.send(from_client_data + b'SB') 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: 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() # 掛電話
6.3UDP協議下的socket
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); socket緩沖區解釋
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實現並發
為什么要講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這個模塊中,其實就干了兩件事情:1、一個是循環建立鏈接的部分,每個客戶鏈接都可以連接成功 2、一個通訊循環的部分,就是每個客戶端鏈接成功之后,要循環的和客戶端進行通信。 看代碼中的: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即客戶端地址
九. 網絡編程的作業
2. 用戶登陸,加密認證
3. 上傳/下載文件,保證文件一致性
4. 傳輸過程中現實進度條
5. 不同用戶家目錄不同,且只能訪問自己的家目錄
6. 對用戶進行磁盤配額、不同用戶配額可不同
7. 用戶登陸server后,可在家目錄權限下切換子目錄
8. 查看當前目錄下文件,新建文件夾
9. 刪除文件和空文件夾
10. 充分使用面向對象知識
11. 支持斷點續傳
簡單分析一下實現方式:
1.字符串操作以及打印 —— 實現上傳下載的進度條功能
2.socketserver —— 實現ftp server端和client端的交互
3.struct模塊 —— 自定制報頭解決文件上傳下載過程中的粘包問題
4.hashlib或者hmac模塊 —— 實現文件的一致性校驗和用戶密文登錄
5.os模塊 —— 實現目錄的切換及查看文件文件夾等功能
6.文件操作 —— 完成上傳下載文件及斷點續傳等功能
看一下流程圖:


