網絡通訊

1.socket

看socket以前,先來回顧一下五層通信流程:程序員

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

  Socket又稱爲套接字,它是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。當咱們使用不一樣的協議進行通訊時就得使用不一樣的接口,還得處理不一樣協議的各類細節,這就增長了開發的難度,軟件也不易於擴展(就像咱們開發一套公司管理系統同樣,報帳、會議預約、請假等功能不須要單獨寫系統,而是一個系統上多個功能接口,不須要知道每一個功能如何去實現的)。因而UNIX BSD就發明了socket這種東西,socket屏蔽了各個協議的通訊細節,使得程序員無需關注協議自己,直接使用socket提供的接口來進行互聯的不一樣主機間的進程的通訊。這就比如操做系統給咱們提供了使用底層硬件功能的系統調用,經過系統調用咱們能夠方便的使用磁盤(文件操做),使用內存,而無需本身去進行磁盤讀寫,內存管理。socket其實也是同樣的東西,就是提供了tcp/ip協議的抽象,對外提供了一套接口,同過這個接口就能夠統1、方便的使用tcp/ip協議的功能了。shell

  其實站在你的角度上看,socket就是一個模塊。咱們經過調用模塊中已經實現的方法創建兩個進程之間的鏈接和通訊。也有人將socket說成ip+port,由於ip是用來標識互聯網中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程序。 因此咱們只要確立了ip和port就能找到一個應用程序,而且使用socket模塊來與之通訊。編程

2.套接字發展史及分類

套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 所以,有時人們也把套接字稱爲「伯克利套接字」或「BSD 套接字」。一開始,套接字被設計用在同 一臺主機上多個應用程序之間的通信。這也被稱進程間通信或 IPC。套接字有兩種(或者稱爲有兩個種族),分別是基於文件型的和基於網絡型的。json

2.1基於文件類型的套接字家族

套接字家族的名字:AF_UNIXwindows

unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,能夠經過訪問同一個文件系統間接完成通訊設計模式

2.2基於網絡類型的套接字家族

套接字家族的名字:AF_INET瀏覽器

(還有AF_INET6被用於ipv6,還有一些其餘的地址家族,不過,他們要麼是隻用於某個平臺,要麼就是已經被廢棄,或者是不多被使用,或者是根本沒有實現,全部地址家族中,AF_INET是使用最普遍的一個,python支持不少種地址家族,可是因爲咱們只關心網絡編程,因此大部分時候我麼只使用AF_INET)緩存

3.套接字的工做流程(基於TCP和 UDP兩個協議)

3.1 TCP和UDP對比

TCP(Transmission Control Protocol)可靠的、面向鏈接的協議(eg:打電話)、傳輸效率低全雙工通訊(發送緩存&接收緩存)、面向字節流。使用TCP的應用:Web瀏覽器;文件傳輸程序。

UDP(User Datagram Protocol)不可靠的、無鏈接的服務,傳輸效率高(發送前時延小),一對1、一對多、多對1、多對多、面向報文(數據包),盡最大努力服務,無擁塞控制。使用UDP的應用:域名系統 (DNS);視頻流;IP語音(VoIP)。

3.2 TCP協議下的socket

生活中的場景。你要打電話給一個朋友,先撥號,朋友聽到電話鈴聲後提起電話,這時你和你的朋友就創建起了鏈接,就能夠講話了。等交流結束,掛斷電話結束這次交談。 生活中的場景就解釋了這工做原理。

  先從服務器端提及。服務器端先初始化Socket,而後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端鏈接。在這時若是有個客戶端初始化一個Socket,而後鏈接服務器(connect),若是鏈接成功,這時客戶端與服務器端的鏈接就創建了。客戶端發送數據請求,服務器端接收請求並處理請求,而後把迴應數據發送給客戶端,客戶端讀取數據,最後關閉鏈接,一次交互結束

3.2.1細說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監聽 backlog 半鏈接池
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()        建立一個與該套接字相關的文件

3.2.2初版,單個客戶端與服務端通訊(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個請求,一個在通訊,一共6個.可是能夠有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()  # 掛電話

3.2.3第二版,通訊循環

服務端

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()  # 掛電話

3.2.4第三版, 通訊,鏈接循環

服務端

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()  # 掛電話

3.2.5詳解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()

3.2.6遠程執行命令的示例

服務端

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()  # 掛電話

3.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下面的輸出窗口看一下效果。

3.3.1sever端代碼示例

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()                         # 關閉服務器套接字

3.3.2client端代碼示例

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)

3.3.3相似於qq聊天的代碼示例:

server端

#_*_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)

client端

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來搞的。

自制時間服務器的代碼示例:

server端

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()

client端

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'))

4.粘包

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時纔會清除緩衝區內容。數據是可靠的,可是會粘包。

4.1兩種狀況下會發生粘包

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')) # 2:b''

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()  

# 兩次返送信息時間間隔過短,數據小,形成服務端一次收取

4.2粘包的解決方案

先介紹一下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))  # 報錯

4.2.1方案一: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()

4.2.2方案二:可自定製報頭版

'''
整個流程的大體解釋:
咱們能夠把報頭作成字典,字典裏包含將要發送的真實數據的描述信息(大小啊之類的),而後json序列化,而後用struck將序列化後的數據長度打包成4個字節。
咱們在網絡上傳輸的全部數據 都叫作數據包,數據包裏的全部數據都叫作報文,報文裏面不止有你的數據,還有ip地址、mac地址、端口號等等,其實全部的報文都有報頭,這個報頭是協議規定的,看一下

發送時:
先發報頭長度
再編碼報頭內容而後發送
最後發真實內容

接收時:
先手報頭長度,用struct取出來
根據取出的長度收取報頭內容,而後解碼,反序列化
從反序列化的結果中取出待取數據的描述信息,而後去取真實的數據內容
'''

4.2.3總體的流程解釋

服務端

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()

4.3FTP上傳下載文件的代碼(簡單版)

server端

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()

client端

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()
相關文章
相關標籤/搜索