網絡編程socket

1、客戶端/服務器架構

一、硬件C/S架構(打印機)html

二、軟件C/S架構python

  互聯網到處是C/S架構linux

  好比百度網站是服務端,瀏覽器是客戶端(B/S架構也是C/S架構的一種)算法

  騰訊做爲服務端提供微信服務,須要下載微信安裝包安裝使用才能夠去聊微信。shell

C/S架構與socket的關係:編程

  用socket就是爲了完成C/S架構的開發json

server端(必須遵照的):windows

  一、位置必須固定死,綁定一個固定的地址設計模式

  二、對外一直提供服務,穩定運行瀏覽器

  三、支持併發(讓多個客戶端感受是同時被服務着)

2、OSI七層

一、序

須知一個完整的計算機系統是由硬件、操做系統、應用軟件三者組成,具有了這三個條件,一臺計算機系統就能夠本身跟本身玩了(打個單機遊戲,玩個掃雷)

若是你要跟別人一塊兒玩,那你就須要上網了,什麼是互聯網?

互聯網的核心就是由一堆協議組成,協議就是標準,好比全世界人通訊的標準是英語

若是把計算機比做人,互聯網協議就是計算機界的英語。全部的計算機都學會了互聯網協議,那全部的計算機都就能夠按照統一的標準去收發信息從而完成通訊了。

人們按照分工不一樣把互聯網協議從邏輯上劃分了層級

二、OSI七層協議

互聯網協議按照功能不一樣分爲osi七層或tcp/ip五層或tcp/ip四層

 

每層運行常見物理設備

一、物理層

物理層功能:主要是基於電器特性發送高低電壓(電信號),高電壓對應數字1,低電壓對應數字0

二、數據鏈路層

數據鏈路層由來:單純的電信號0和1沒有任何意義,必須規定電信號多少位一組,每組什麼意思

數據鏈路層的功能:定義了電信號的分組方式

2.1 以太網協議

早期的時候各個公司都有本身的分組方式,後來造成了統一的標準,即以太網協議ethernet

ethernet規定

  • 一組電信號構成一個數據包,叫作‘幀’
  • 每一數據幀分紅:報頭head和數據data兩部分
       head                        data                             

 

head包含:(固定18個字節)

  • 發送者/源地址,6個字節
  • 接收者/目標地址,6個字節
  • 數據類型,6個字節

data包含:(最短46字節,最長1500字節)

  • 數據包的具體內容

head長度+data長度=最短64字節,最長1518字節,超過最大限制就分片發送

2.2 mac地址

head中包含的源和目標地址由來:ethernet規定接入internet的設備都必須具有網卡,發送端和接收端的地址即是指網卡的地址,即mac地址

mac地址:每塊網卡出廠時都被燒製上一個世界惟一的mac地址,長度爲48位2進制,一般由12位16進制數表示(前六位是廠商編號,後六位是流水線號)

2.3 廣播

有了mac地址,同一網絡內的兩臺主機就能夠通訊了(一臺主機經過arp協議獲取另一臺主機的mac地址)

ethernet採用最原始的方式,廣播的方式進行通訊,即計算機通訊基本靠吼

三、網絡層

網絡層由來:有了ethernet、mac地址、廣播的發送方式,世界上的計算機就能夠彼此通訊了,問題是世界範圍的互聯網是由

一個個彼此隔離的小的局域網組成的,那麼若是全部的通訊都採用以太網的廣播方式,那麼一臺機器發送的包全世界都會收到,

這就不只僅是效率低的問題了,這會是一種災難。

 

須要找出一種方法來區分哪些計算機屬於同一廣播域,哪些不是,若是是就採用廣播的方式發送,若是不是,

就採用路由的方式(向不一樣廣播域/子網分發數據包),mac地址是沒法區分的,它只跟廠商有關

網絡層功能:引入一套新的地址用來區分不一樣的廣播域/子網,這套地址即網絡地址。

3.1 IP協議

  • 規定網絡地址的協議叫ip協議,它定義的地址稱之爲ip地址,普遍採用的v4版本即ipv4,它規定網絡地址由32位2進製表示
  • 範圍0.0.0.0-255.255.255.255
  • 一個ip地址一般寫成四段十進制數,例:172.16.10.1

3.2 ip地址分紅兩部分

  • 網絡部分:標識子網
  • 主機部分:標識主機

注意:單純的ip地址段只是標識了ip地址的種類,從網絡部分或主機部分都沒法辨識一個ip所處的子網

例:172.16.10.1與172.16.10.2並不能肯定兩者處於同一子網

3.3 子網掩碼

所謂」子網掩碼」,就是表示子網絡特徵的一個參數。它在形式上等同於IP地址,也是一個32位二進制數字,它的網絡部分所有爲1,主機部分所有爲0。好比,IP地址172.16.10.1,若是已知網絡部分是前24位,主機部分是後8位,那麼子網絡掩碼就是11111111.11111111.11111111.00000000,寫成十進制就是255.255.255.0。

 

知道」子網掩碼」,咱們就能判斷,任意兩個IP地址是否處在同一個子網絡。方法是將兩個IP地址與子網掩碼分別進行AND運算(兩個數位都爲1,運算結果爲1,不然爲0),而後比較結果是否相同,若是是的話,就代表它們在同一個子網絡中,不然就不是。

 

好比,已知IP地址172.16.10.1和172.16.10.2的子網掩碼都是255.255.255.0,請問它們是否在同一個子網絡?二者與子網掩碼分別進行AND運算,

172.16.10.1:10101100.00010000.00001010.000000001

255255.255.255.0:11111111.11111111.11111111.00000000

AND運算得網絡地址結果:10101100.00010000.00001010.000000001->172.16.10.0

 

172.16.10.2:10101100.00010000.00001010.000000010

255255.255.255.0:11111111.11111111.11111111.00000000

AND運算得網絡地址結果:10101100.00010000.00001010.000000001->172.16.10.0

結果都是172.16.10.0,所以它們在同一個子網絡。

總結一下,IP協議的做用主要有兩個,一個是爲每一臺計算機分配IP地址,另外一個是肯定哪些地址在同一個子網絡。

3.4 ip數據包

ip數據包也分爲head和data部分,無須爲ip包定義單獨的欄位,直接放入以太網包的data部分

 

head:長度爲20到60字節

data:最長爲65,515字節。

而以太網數據包的」數據」部分,最長只有1500字節。所以,若是IP數據包超過了1500字節,它就須要分割成幾個以太網數據包,分開發送了。

以太網頭                ip 頭                                     ip數據                                

 

 

 3.5 ARP協議

arp協議由來:計算機通訊基本靠吼,即廣播的方式,全部上層的包到最後都要封裝上以太網頭,而後經過以太網協議發送,在談及以太網協議時候,我門瞭解到

通訊是基於mac的廣播方式實現,計算機在發包時,獲取自身的mac是容易的,如何獲取目標主機的mac,就須要經過arp協議

arp協議功能:廣播的方式發送數據包,獲取目標主機的mac地址

 

協議工做方式:每臺主機ip都是已知的

例如:主機172.16.10.10/24訪問172.16.10.11/24

一:首先經過ip地址和子網掩碼區分出本身所處的子網

場景 數據包地址
同一子網 目標主機mac,目標主機ip
不一樣子網 網關mac,目標主機ip

 

 

 

二:分析172.16.10.10/24與172.16.10.11/24處於同一網絡(若是不是同一網絡,那麼下表中目標ip爲172.16.10.1,經過arp獲取的是網關的mac)

  源mac 目標mac 源ip 目標ip 數據部分
發送端主機 發送端mac FF:FF:FF:FF:FF:FF 172.16.10.10/24 172.16.10.11/24 數據

 

 

三:這個包會以廣播的方式在發送端所處的自網內傳輸,全部主機接收後拆開包,發現目標ip爲本身的,就響應,返回本身的mac

4、傳輸層

傳輸層的由來:網絡層的ip幫咱們區分子網,以太網層的mac幫咱們找到主機,而後你們使用的都是應用程序,你的電腦上可能同時開啓qq,暴風影音,等多個應用程序,

那麼咱們經過ip和mac找到了一臺特定的主機,如何標識這臺主機上的應用程序,答案就是端口,端口即應用程序與網卡關聯的編號。

傳輸層功能:創建端口到端口的通訊

補充:端口範圍0-65535,0-1023爲系統佔用端口

tcp協議:

可靠傳輸,TCP數據包沒有長度限制,理論上能夠無限長,可是爲了保證網絡的效率,一般TCP數據包的長度不會超過IP數據包的長度,以確保單個TCP數據包沒必要再分割。

以太網頭 ip 頭               tcp頭               數據                                                    

 

udp協議:

不可靠傳輸,」報頭」部分一共只有8個字節,總長度不超過65,535字節,正好放進一個IP數據包。

以太網頭 ip頭                      udp頭                            數據                                           

 

tcp三次握手和四次揮手

 

5、應用層

應用層由來:用戶使用的都是應用程序,均工做於應用層,互聯網是開發的,你們均可以開發本身的應用程序,數據多種多樣,必須規定好數據的組織形式 

應用層功能:規定應用程序的數據格式。

例:TCP協議能夠爲各類各樣的程序傳遞數據,好比Email、WWW、FTP等等。那麼,必須有不一樣協議規定電子郵件、網頁、FTP數據的格式,這些應用程序協議就構成了」應用層」。

瞭解上述互聯網協議的目的:

一、基於socket編程,開發C/S架構的軟件

二、C/S架構軟件(軟件屬於應用層)是基於網絡進行通訊的

三、網絡的核心即爲一堆協議,協議即標準,要開發基於網絡通訊的軟件就必須遵照這些標準

3、socket編程

首先socket怎麼去實現網絡通訊的

socket是什麼

咱們常常把socket翻譯爲套接字,socket是在應用層和傳輸層之間的一個抽象層,它把TCP/IP層複雜的操做抽象爲幾個簡單的接口供應用層調用已實現進程在網絡中通訊。

Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。

因此,咱們無需深刻理解tcp/udp協議,socket已經爲咱們封裝好了,咱們只須要遵循socket的規定去編程,寫出的程序天然就是遵循tcp/udp標準的。

 

1、套接字發展史及分類

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

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

套接字家族的名字:AF_UNIX

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

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

套接字家族的名字:AF_INET

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

2、套接字工做流程

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

 

服務端套接字函數

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() 建立一個與該套接字相關的文件

4、基於TCP的套接字

tcp是基於連接的,必須先啓動服務端,而後再啓動客戶端去連接服務端

1、簡單的基於TCP協議的套接字通訊

import socket

#第一步:調用socket產生s對象
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM

#第二步:綁定地址和端口
s.bind(('127.0.0.1',8080)) #ip應該是服務端這個軟件運行那臺機器的ip地址,port(0-65535)

#第三步:監聽
s.listen(5) #半鏈接池:控制的是同一時刻的連接請求個數
print('服務端啓動。。。')

#第四步:等待連接的請求
conn,client_addr=s.accept() #(套接字對象,存放有客戶端的ip和端口的元組)
print(conn,client_addr)

#第五步:收/發消息
data=conn.recv(1024) #1024單位是bytes,表明最大接收1024bytes
print('收到消息',data)
conn.send(data.upper()) #發送大寫的(接收到的結果)

#第六步:關閉連接
conn.close()

#第七步:關閉服務端對象
s.close()
服務端
import socket

#第一步:調用socket產生s對象
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM

#第二步:發起連接請求
s.connect(('127.0.0.1',8080)) #ip和port應該是服務端的ip和端口

#第三步:發/收消息
s.send('hello world'.encode('utf-8')) # 必須是bytes類型
data=s.recv(1024)  #接收消息單位是bytes,表明最大接收1024bytes
print('服務端消息:',data)

#第四步:關閉客戶端對象
s.close()
客戶端

基於TCP協議啓動順序必須是:

  先啓動服務端 ---------->  而後啓動客戶端

 

#重啓服務端時可能會遇到 「Addres already in use」
#這個是因爲你的服務端仍然存在四次揮手的time_wait狀態在佔用地址(若是不懂,請深刻研究
1.tcp三次握手,四次揮手 
2.syn洪水攻擊 
3.服務器高併發狀況下會有大量的time_wait狀態的優化方法)

#加入一條socket配置,重用ip和端口
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加

#==============================================
發現系統存在大量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 時間

2、實現通訊循環TCP的套接字通訊

import socket

#1. 調用socket產生s對象
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM
print(s)

#2. 綁定地址和端口

s.bind(('127.0.0.1',8080)) #ip應該是服務端這個軟件運行那臺機器的ip地址,port(0-65535)

#3. 監聽
s.listen(5) #半鏈接池:控制的是同一時刻的連接請求數

print('服務端啓動。。。')
#4. 等待連接的請求
conn,client_addr=s.accept() #(套接字對象,存放有客戶端的ip和端口的元組)
print(conn,client_addr)

#5. 收\發消息
while True: #通訊循環
    try: #處理異常步驟
        data=conn.recv(1024) #1024單位是bytes,表明最大接收1024bytes
        if len(data) == 0:break #針對linux或者mac。。。
        print('收到消息',data)
        conn.send(data.upper())
    except ConnectionResetError: ##針對windows
        break

#6. 關閉連接
conn.close()

#7. 關閉服務端對象
s.close()
服務端
import socket

#1. 調用socket產生s對象
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM

#2. 發送連接請求
s.connect(('127.0.0.1',8080)) #ip和port應該是服務端的ip和端口

#3,發\收消息
while True:
    msg=input('>>: ').strip()
    s.send(msg.encode('utf-8')) # 必須是bytes類型
    data=s.recv(1024)
    print('服務端消息:',data)

#4. 關閉
s.close()
客戶端 

基於消息的收發加上while循環 即實現了通訊間的循環往復

可是上述代碼問題還有不少

3、TCP的連接循環(內部含通訊循環)

import socket

#1. 調用socket產生s對象
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM
print(s)

#2. 綁定地址和端口

s.bind(('127.0.0.1',8080)) #ip應該是服務端這個軟件運行那臺機器的ip地址,port(0-65535)

#3. 監聽
s.listen(5) #半鏈接池:控制的是同一時刻的連接請求數

print('服務端啓動。。。')
#4. 等待連接的請求
while True:  #連接循環
    conn,client_addr=s.accept() #(套接字對象,存放有客戶端的ip和端口的元組)
    print(conn,client_addr)

    #5. 收\發消息
    while True: #通訊循環
        try: #處理異常步驟
            data=conn.recv(1024) #1024單位是bytes,表明最大接收1024bytes
            if len(data) == 0:break #針對linux或者mac 。。。客戶端強行終止處理異常
            print('收到消息',data)
            conn.send(data.upper())
        except ConnectionResetError: ##針對windows 。。。客戶端強行終止處理異常
            break

    #6. 關閉連接
    conn.close()

#7. 關閉服務端對象
s.close()
服務端
import socket

#1. 調用socket產生s對象
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM

#2. 發送連接請求
s.connect(('127.0.0.1',8080)) #ip和port應該是服務端的ip和端口

#3,發\收消息
while True:
    msg=input('>>: ').strip()
    if  len(msg) == 0:continue  #客戶端的的msg不能爲空
    s.send(msg.encode('utf-8')) # 必須是bytes類型
    data=s.recv(1024)
    print('服務端消息:',data)

#4. 關閉
s.close()
客戶端

此時程序就實現了server端的 條件1和條件2

  一、位置必須固定死,綁定一個固定的地址

  二、對外一直提供服務,穩定運行

暫時實現不了併發的效果

5、基於UDP的套接字

udp是無連接的,先啓動哪一端都不會報錯

UDP服務端

from socket import *

server=socket(AF_INET,SOCK_DGRAM) #SOCK_DGRAM=>數據報協議
server.bind(('127.0.0.1',8080))   #綁定ip和port

print('start....')
while True:
    data,client_addr=server.recvfrom(1024) #接受(b'hello', (客戶端ip和port))
    server.sendto(data.upper(),client_addr)#發送(返回內容,加客戶端ip和port)

server.close()

UDP客戶端

from socket import *

client=socket(AF_INET,SOCK_DGRAM) #SOCK_DGRAM=>數據報協議

while True:
    msg=input('>>: ').strip()
    client.sendto(msg.encode('utf-8'),('127.0.0.1',8080)) #發送內容加服務端ip和port
    data,server_addr=client.recvfrom(1024)  
    print(data)

client.close()

UDP套接字特色:

1

當發送的數據報大於接收數據報的緩衝區大小時,
在windows系統:接收端會拋出異常
在linux系統:接收端不會拋出異常,會丟棄掉多餘的數據
from socket import *

server=socket(AF_INET,SOCK_DGRAM) #SOCK_DGRAM=>數據報協議
server.bind(('127.0.0.1',8081))

server.recvfrom(1)
服務端
from socket import *

client=socket(AF_INET,SOCK_DGRAM) #SOCK_DGRAM=>數據報協議

client.sendto(b'hello',('127.0.0.1',8081))
客戶端

2

udp協議沒有粘包問題
udp協議可以穩定傳輸數據的最大數據量爲512Bytes
from socket import *

server=socket(AF_INET,SOCK_DGRAM) #SOCK_DGRAM=>數據報協議
server.bind(('127.0.0.1',8081))

data1=server.recvfrom(1024)
print('第一次接收: ',data1)
data2=server.recvfrom(1024)
print('第二次接收: ',data2)
服務端
from socket import *

client=socket(AF_INET,SOCK_DGRAM) #SOCK_DGRAM=>數據報協議

client.sendto(b'hello',('127.0.0.1',8081))
client.sendto(b'world',('127.0.0.1',8081))
客戶端

6、模擬ssh遠程執行命令

#=================================服務端============================================
import socket

#1. 調用socket產生s對象
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM
#2. 綁定地址和端口
s.bind(('127.0.0.1',8080)) #ip應該是服務端這個軟件運行那臺機器的ip地址,port(0-65535)
#3. 監聽
s.listen(5) #半鏈接池:控制的是同一時刻的連接請求數
print('服務端啓動。。。')
#4. 等待連接的請求
while True:  #連接循環
    conn,client_addr=s.accept() #(套接字對象,存放有客戶端的ip和端口的元組)
    print(conn,client_addr)

    #5. 收\發消息
    while True: #通訊循環
        try: #處理異常步驟
            data=conn.recv(1024) #1024單位是bytes,表明最大接收1024bytes
            if len(data) == 0:break #針對linux或者mac 。。。客戶端強行終止處理異常
            import subprocess #導入此模塊實現執行操做系統命令
            obj=subprocess.Popen(data.decode('utf-8'),
                                 shell=True,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
            stdout=obj.stdout.read()
            stderr=obj.stderr.read()
            print(len(stdout) + len(stderr))
            conn.send(stdout + stderr)   #發送管道中正確輸出和錯誤輸出
        except ConnectionResetError: ##針對windows 。。。客戶端強行終止處理異常
            break

    #6. 關閉連接
    conn.close()

#7. 關閉服務端對象
s.close()
# ===================================客戶端=================================
import socket

#1. 調用socket產生s對象
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM

#2. 發送連接請求
s.connect(('127.0.0.1',8080)) #ip和port應該是服務端的ip和端口

#3,發\收消息
while True:
    msg=input('>>: ').strip()
    if  len(msg) == 0:continue  #客戶端的的msg不能爲空
    s.send(msg.encode('utf-8')) # 必須是bytes類型
    data=s.recv(1024)
    print('服務端消息:',data.decode('gbk'))#操做系統的輸出,編碼時爲gbk,因此解碼也須要gbk

#4. 關閉
s.close()

 

此時執行windows上的命令,例如dir  、 ping 127.0.0.1  等。發現沒有什麼問題

可是一旦執行例如 tasklist  或 netstat -ano  等  就會發現有問題了

返回的輸出值沒法一次取乾淨。在執行命令時,卻取出了上一次命令的殘留

上述程序是基於tcp的socket,在運行時會發生粘包問題

7、什麼是粘包

前提:只有TCP有粘包現象,UDP永遠不會粘包

上述圖即爲模擬ssh執行命令tasklist 配圖

此時即產生了粘包問題

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

兩種狀況下會發生粘包

一、緩衝區滿

發送端須要等緩衝區滿才發送出去,形成粘包(時間間隔很短,數據量很小,會合到一塊兒,產生粘包)

import  socket
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(ip_port)


s.send('hello'.encode('utf-8'))
s.send('world'.encode('utf-8'))
客戶端發送

二、不及時接收

接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收時仍是從緩衝區拿上次遺留的數據,產生粘包)

import socket
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(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(ip_port)


s.send('hello world'.encode('utf-8'))
客戶端發送

拆包的發生狀況

當發送端緩衝區的長度大於網卡的MTU時,tcp會將此次發送的數據拆成幾個數據包發送出去。

 

補充問題一:爲什麼tcp是可靠傳輸,udp是不可靠傳輸

基於tcp的數據傳輸請參考個人另外一篇文章http://www.cnblogs.com/linhaifeng/articles/5937962.html,tcp在數據傳輸時,發送端先把數據發送到本身的緩存中,而後協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則從新發送數據,因此tcp是可靠的

而udp發送數據,對端是不會返回確認信息的,所以不可靠

補充問題二:send(字節流)和recv(1024)及sendall

recv裏指定的1024意思是從緩存裏一次拿出1024個字節的數據

send的字節流是先放入己端緩存,而後由協議控制將緩存內容發往對端,若是待發送的字節流大小大於緩存剩餘空間,那麼數據丟失,用sendall就會循環調用send,數據不會丟失

8、解決粘包問題的方法

爲字節流加上自定義固定長度報頭,報頭中包含字節流長度,而後一次send到對端,對端在接收時,先從緩存中取出定長的報頭,而後再取真實數據

struct 模塊

該模塊能夠把一個類型,如數字,轉成固定長度的bytes

import struct
res=struct.pack('i',123456)
print(res,len(res))
#b'@\xe2\x01\x00' 4

服務端

#=================================服務端============================================
import socket,struct,json

#1. 調用socket產生s對象
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM
#2. 綁定地址和端口
s.bind(('127.0.0.1',8080)) #ip應該是服務端這個軟件運行那臺機器的ip地址,port(0-65535)
#3. 監聽
s.listen(5) #半鏈接池:控制的是同一時刻的連接請求數
print('服務端啓動。。。')
#4. 等待連接的請求
while True:  #連接循環
    conn,client_addr=s.accept() #(套接字對象,存放有客戶端的ip和端口的元組)
    print(conn,client_addr)

    #5. 收\發消息
    while True: #通訊循環
        try: #處理異常步驟
            data=conn.recv(1024) #1024單位是bytes,表明最大接收1024bytes
            if len(data) == 0:break #針對linux或者mac 。。。客戶端強行終止處理異常
            import subprocess #導入此模塊實現執行操做系統命令
            obj=subprocess.Popen(data.decode('utf-8'),
                                 shell=True,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
            stdout=obj.stdout.read()
            stderr=obj.stderr.read()
            # 先製做報頭
            header_dic = {
                'total_size': len(stdout) + len(stderr),
                'filename': 'a.txt'
            }
            header_json = json.dumps(header_dic)  #字典序列化成字符串
            header_bytes = header_json.encode('utf-8') #字符串編碼成bytes類型

            # 先發送報頭的長度(固定4個字節)
            conn.send(struct.pack('i', len(header_bytes)))

            # 再發送報頭
            conn.send(header_bytes)

            # 最後發送真實的數據
            conn.send(stdout)   #發送管道中正確輸出和錯誤輸出
            conn.send(stderr)   #發送管道中正確輸出和錯誤輸出
        except ConnectionResetError: ##針對windows 。。。客戶端強行終止處理異常
            break

    #6. 關閉連接
    conn.close()

#7. 關閉服務端對象
s.close()

客戶端

# ===================================客戶端=================================
import socket,struct,json

#1. 調用socket產生s對象
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM

#2. 發送連接請求
s.connect(('127.0.0.1',8080)) #ip和port應該是服務端的ip和端口

#3,發\收消息
while True:
    msg=input('>>: ').strip()
    if  len(msg) == 0:continue  #客戶端的的msg不能爲空
    s.send(msg.encode('utf-8')) # 必須是bytes類型
    # 先4個bytes,而後提取報頭的長度
    header_size=struct.unpack('i',s.recv(4))[0]

    # 再根據報頭的長度精準地收取報頭,而後從報頭提取報頭字典
    header_bytes=s.recv(header_size)  #收報頭(bytes)
    header_json=header_bytes.decode('utf-8')  #解碼成字符串
    header_dic=json.loads(header_json)  #反序列化獲得字典
    print(header_dic)

    total_size=header_dic['total_size']  #拿到字典中真實數據的長度
    # 最後接收真實數據
    res=b''
    recv_size=0
    while recv_size < total_size:
        data=s.recv(1024)
        res+=data
        recv_size+=len(data)
    print('服務端消息:',res.decode('gbk'))#操做系統的輸出,編碼時爲gbk,因此解碼也須要gbk

#4. 關閉
s.close()

咱們能夠把報頭作成字典,字典裏包含將要發送的真實數據的詳細信息,而後json序列化,而後用struck將序列化後的數據長度打包成4個字節(4個本身足夠用了)

發送時:

  先發報頭長度

  再編碼報頭內容而後發送

  最後發真實內容

接收時:

  先手報頭長度,用struct取出來

  根據取出的長度收取報頭內容,而後解碼,反序列化

  從反序列化的結果中取出待取數據的詳細信息,而後去取真實的數據內容

9、socketserver模塊實現併發

基於tcp的套接字,關鍵就是兩個循環,一個連接循環,一個通訊循環

socketserver模塊中分兩大類:server類(解決連接問題)和request類(解決通訊問題)

#==============================socketserver模塊固定使用套路之服務端========================================
import socketserver

class MyTCPhandler(socketserver.BaseRequestHandler):
    def handle(self): #必須起名handle
        # 通訊循環  能夠本身定義通訊循環
        while True: 
            # try:
            #     data = self.request.recv(1024)  # self.request <==> conn 等同做用 固定用法
            #     if len(data) == 0:break
            #     print('收到%s:%s的數據: %s' % (self.client_address[0],
            #                               self.client_address[1],
            #                               data))
            #     self.request.send(data.upper())
            # except ConnectionResetError:
            #     break

if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('127.0.0.1', 9999), MyTCPhandler)
    socketserver.TCPServer.request_queue_size=10 #設置半連接池
    server.serve_forever() #連接循環
import socket

client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #tcp流式協議=>SOCK_STREAM
client.connect(('127.0.0.1',9999))

while True: #循環發收消息
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    client.send(msg.encode('utf-8')) # b''
    data=client.recv(1024)
    print('服務端消息:',data)

client.close()
客戶端
相關文章
相關標籤/搜索