1. 程式人生 > >網絡編程基礎:網絡基礎之網絡協議、socket模塊

網絡編程基礎:網絡基礎之網絡協議、socket模塊

網絡 作用 技術 ast 內存空間 封裝 class 揮手 window

操作系統(簡稱OS)基礎:

應用軟件不能直接操作硬件,能直接操作硬件的只有操作系統;所以,應用軟件可以通過操作系統來間接操作硬件

網絡基礎之網絡協議:

網絡通訊原理:

  連接兩臺計算機之間的Internet實際上就是一系列統一的標準,這些標準稱之為互聯網協議;互聯網的本質就是一系列的協議,總稱為“互聯網協議” (Internet Protocol Suite)

  互聯網協議的功能:定義計算機何如接入Internet,以及接入Internet的計算機通信的標準。

  osi七層協議: 互聯網協議按照功能不同分為OSI七層或TCP/IP五層或TCP/IP四層

技術分享圖片

用戶感知到的只是最上面的一層應用層,自上而下每層都依賴於下一層;每層都運行特定的協議,越往上越靠近用戶,越往下越靠近硬件

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

  數據鏈路層:

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

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

    以太網協議(Ethernet):Ethernet協議規定了:1. 一組電信號構成一個數據包,叫做“幀”;2. 每一數據幀分成“報頭”head和數據data兩部分

       head包含(固定18個字節):1. 發送者/原地址,6個字節; 2. 數據類型,6個字節; 3. 接受者/目標地址,6個字節

       data包含:數據包的具體內容

    Mac地址:head中包含的源、目標地址的由來:Ethernet規定接入Internet的設備都必須具備網卡,發送端和接收端的地址便是指網卡的地址,即Mac地址;

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

    廣播: 有了Mac地址,同一網絡內的兩臺主機就可以通信了;Ethernet采用最原始的廣播的方式進行通信,即計算機通信基本靠吼

  網絡層:有了Ethernet、Mac地址、廣播的發送方式,同一個局域網內的計算機就可以彼此通訊了,但世界範圍內的互聯網是由一個個彼此隔離的小的局域網(子網)組成的,

      所以不能所有的通信都采用以太網的廣播方式

技術分享圖片

    從上圖可以看出:必須找出一種方法來區分哪些計算機屬於同一廣播域、哪些不是,如果是就采用廣播的方式發送;如果不是就采用路由的方式(向不同廣播域/子網分發數據包),

            Mac地址是無法區分的,它只跟廠商有關

    網絡層功能:引入一套新的地址來區分不同的廣播域(子網),這套地址即網絡地址

    IP協議:1. 規定網絡地址的協議叫IP協議,它定義的地址稱為IP地址,廣泛采用的v4版本即ipv4,它規定網咯地址由32位2進制表示;

        2. 範圍0.0.0.0-255.255.255.255

        3. 一個IP地址通常寫成四段十進制數,例如:172.16.10.1

      IP地址分成兩部分: 1. 網絡部分:標識子網; 2. 主機部分:標識主機

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

      子網掩碼:表示子網絡特征的一個參數;知道了“子網掩碼”,我們就能判斷任意兩個IP地址是否處在同一個子網絡。

      網絡層作用總結:IP協議的主要作用有兩個:1. 為每臺計算機分配IP地址;2.確定哪些地址在同一個子網絡

    IP數據包:分為head和data兩個部分,然後直接放入以太包的data部分,如下所示:

技術分享圖片

    ARP協議:由來:計算機通信基本靠吼,即廣播的方式,所有上層的包到最後都要封裝上以太網頭,然後通過以太網協議發送;通信是基於Mac的廣播方式實現,計算機在發包時

            獲取自身的Mac容易,如何獲取目標主機的Mac就需要通過ARP協議。

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

         協議工作方式: 每臺主機IP都是已知的

           1. 首先通過IP地址和子網掩碼區分出自己所處的子網

           2. 分析是否處於同一網絡(如果不是同一網絡。通過ARP獲取的是網關的Mac)

          技術分享圖片

           3. 這個包以廣播的方式在發送端所處的子網內傳輸,所有主機接收後拆開包,發現目標IP是自己的就響應返回自己的Mac(這點還不是很理解,發送端所處的子網??)

    傳輸層:由來:網絡層的IP幫我們區分子網,以太網層的Mac幫我們找到主機,然後大家使用的都是應用程序,那麽我們通過IP和Mac找到了一臺特定的主機;

           然後,標識這臺主機上的應用程序就是端口,端口即應用程序和網卡關聯的編號。

        傳輸層功能:建立端口到端口的通信

        補充:端口範圍0-65535,0-1023為操作系統占用端口

        TCP協議: 可靠傳輸,需要挖雙向“通道“,””3次“握手”和4次“揮手”;流式協議

        UDP協議:不可靠傳輸,不需要挖“通道”

    

    應用層:由來:用戶使用的都是應用程序,均工作於應用層,互聯網是開發的,大家都可以開發自己的應用程序,數據多種多樣,必須規定好數據的組織形式

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

          例如: TCP協議可以為各種各樣的程序傳遞數據,比如Email、www、FTP等;那麽必須有不同協議規定電子郵件、網頁、發圖片數據的格式,這些應用程序協議就構成了“應用層”

          技術分享圖片

Socket:

socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,socket是一個門面模式,他爸復雜的TCP/IP協議族隱藏在socket接口的後面,對於用戶來說,一組簡單的接口就是全部,讓socket去組織數據,以符合指定的協議。

技術分享圖片

所以我們無需深入理解TCP/UDP協議,socket已經為我們封裝好了,我們只需要遵循socket的規定去編程,寫出的程序自然就是遵循TCP、UDP標準的

附:也有人將socket說成IP+port,IP是用來標識互聯網中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程序,IP地址是配置到網卡上的,而port是應用程序開啟的,IP與port的綁定就標識了互聯網中獨一無二的一個應用程序; 而程序的pid是同一臺機器上不同進程或線程的標識

套接字: 套接字有兩種(或者說兩個種族),分別是基於文件型的和基於網絡型的。

基於文件類型的套接字家族: 套接字家族的名字是 : AF_UNIX

基於網絡類型的套接字家族: 套接字家族的名字是:AF_INET  

  還有AF_INET6被用於ivp6;AF_INET是使用最廣泛的一個,python支持很多地址家族,但是由於我們只關心網絡編程,所以大部分時候我們只是用AF_INET

套接字(socket) 工作流程:以打電話為例說明:

客戶端代碼如下:

import socket

# 1. 買“手機”
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
print(phone)
# 打印結果:
# <socket.socket fd=300, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>


# 2. “撥號” (客戶端不需要綁定IP和端口)
phone.connect(("127.0.0.1",8080))
"""
# connect 發起連接, ("127.0.0.1",8080)是服務端的IP和端口; 
# 客戶端的connect對應服務端的accept(),connect()和服務端的accept()底層進行的就是TCP的“三次握手”
# 服務端accept()之後,客戶端的phone就相當於服務端的那個 conn,就是那根“電話線”
"""
print(phone)
# 運行結果:  # 服務端accept()之後客戶端的phone就發生了變化,變得和服務端中的conn對應
# <socket.socket fd=300, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=(‘127.0.0.1‘, 62064), raddr=(‘127.0.0.1‘, 8080)>



# 發、收消息;發收的消息都是bytes類型
phone.send("hello".encode("utf-8"))  #
"""
# 不能直接發字符串, 物理層傳輸的0101,這一步需要發送bytes類型; 
# 字符串轉bytes: string.encode(編碼格式)
# phone.send() 對應服務端的 conn.recv()
"""
data = phone.recv(1024)  #
print(data)

# 關閉
phone.close()


# 先啟動服務端,再啟動客戶端,運行結果如下:
# b‘HELLO‘

服務端代碼如下:

import socket

# 1. 買“手機”
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 基於網絡通訊的、基於TCP協議的套接字;phone就是一個套接字對象  # 這一步得到一個服務端的套接字phone
"""
全稱是: phone = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
socket.socket() # socket下面的socket類;
family=socket.AF_INET # 地址家族(socket的類型)是基於網絡通訊的AF_INET  
type=socket.SOCK_STREAM # 用的是流式的協議,即 TCP協議 
"""
# print(phone)
# 打印結果
# <socket.socket fd=316, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>

# 2. 綁定“手機卡”(服務端的IP地址需要固定下來、並公示給別人;客戶端雖然有IP和端口但是不需要綁定)
phone.bind(("127.0.0.1",8080))
"""
# 服務端需要綁定IP和Port(IP和端口),ip和端口需要以元祖的形式傳進來;
其中第一個參數是字符串形式的IP地址; 127.0.0.1是指本機,專門用於測試的,IP寫成這個就意味著服務端和客戶端都必須在同一臺主機上;
第二個參數是端口;端口範圍是0-65535,其中0-1023是給操作系統使用的,2014以後的你可以使用
"""

# 3. “開機”
phone.listen(5)  # 開始TCP監聽
"""
# 5代表最大掛起的鏈接數; 通常這個數寫在配置文件中
# Enable a server to accept connections. If backlog is specified, it must be at least 0 (if it is lower, it is set to 0); it specifies the number of unaccepted connections that the system will allow before refusing new connections. If not specified, a default reasonable value is chosen.
"""

# 4. 等電話
# res = phone.accept()
# print(res)
# print(phone)
"""
# 等待鏈接; 等待的結果賦值給一個變量 
# 服務端程序啟動後,程序會停在這一步; 
# 服務端的accept()對應客戶端的connect()
# accept()底層建立的就是TCP的“三次握手”,“三次握手”之後會建成一個雙向的鏈接(下面的conn),然後客戶端得到一個對象(新的phone)、服務端得到一個對象(conn),這兩個對象都可以收、發消息
"""
# 客戶端的程序啟動後,服務端的程序也從 res = phone.accept()這一步接著往下運行
# 其中一次的運行結果:
# (<socket.socket fd=356, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=(‘127.0.0.1‘, 8080), raddr=(‘127.0.0.1‘, 56572)>, (‘127.0.0.1‘, 56572))
# 元祖的形式,元祖裏面有2個元素,第一個元素是發送端的鏈接對象(套接字對象),第二個元素是客戶端的IP和端口

# <socket.socket fd=312, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=(‘127.0.0.1‘, 8080)>

"""
由於phone.accept()得到的結果是元祖的形式,裏面有兩個元素:第一個、客戶端的鏈接對象(相當於撥號人的電話線);第二個、客戶端的IP和端口,所以phone.accept()可以寫成如下形式
"""

conn,client_addr = phone.accept()
print(conn)
# 打印結果:
# <socket.socket fd=328, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=(‘127.0.0.1‘, 8080), raddr=(‘127.0.0.1‘, 62064)>


# 5. 收、發消息(基於剛剛建好的那根“電話線”(conn)收發消息),收發的消息都是bytes類型
data = conn.recv(1024)  #
print("客戶端的數據",data)
"""
# conn.recv(1024):接收conn這個發送端對象發來的數據(或者理解成沿著conn這根“電話線”接收消息) 
# 括號內的數字需要註意兩個地方: 1. 數字單位:bytes;2. 數字2014代表最大接收1024個bytes
# conn.recv(1024)接收到的數據賦值給變量 data
"""

conn.send(data.upper())  #
"""
# conn.send(data.upper()):給conn的客戶端發送消息(沿著conn這個“電話線”發送消息)
# .upper() # 把字符串裏面的都變成大寫
"""

# 掛電話(關閉)
conn.close()

# 關機
phone.close()

# 運行結果:
# 客戶端的數據 b‘hello‘


"""
1. 服務端有兩種套接字對象:服務端的phone和conn
     服務端的phone用於:綁定(IP和端口)、監聽TCP和最重要的接收接收客戶端的鏈接和客戶端的IP、端口
     conn用於收發消息
2. 客戶端有一種套接字對象:客戶端的phone(其實客戶端的phone在服務端accept之後也發生了變化),它的作用是:發起建鏈接請求(.connect())和發、收消息 
"""

簡單套接字加上通信循環:

把上面的代碼加上 while True 就變成了循環通信,如下所示

客戶端代碼:

import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",8080))
while True:
    msg = input(">>>").strip()
    phone.send(msg.encode("utf-8"))
    data = phone.recv(1024)
  
print(data) # 也是bytes形式
   """
   如果想要打印正常的形式,可利用利用:
   print(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)

conn,client_addr = phone.accept()
print(client_addr)

while True:
    data = conn.recv(1024)  # 在收到消息之前,程序也“卡”在這一步; 所以,recv()的具體含義是“等待接收消息”
    print("客戶端的數據",data)
    conn.send(data.upper())

conn.close()

phone.close()

重啟服務端的時候可能出現端口仍然被占用的情況,原因是端口被操作系統回收需要時間,解決辦法如下:

為服務端加一句代碼:

import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 在綁定之前加上這句代碼; # reuseaddr表示重新用該端口
phone.bind(("127.0.0.1",8081))
phone.listen(5)

conn,client_addr = phone.accept()
data = conn.recv(1024)
print("客戶端的數據",data)
conn.send(data.upper())
conn.close()
print(phone)
phone.close()

客戶端和服務端代碼bug修復:

客戶端可以發空消息,但服務端卻收不到空消息,如下代碼:

客戶端:

import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",8080))
while True:
    msg = input(">>>").strip()
    """
    客戶端可以發空數據,但是服務端卻收不到空數據
    解決客戶端發空消息可以用如下代碼:
    """
    if not msg:continue  # 如果發的消息為空,則重新發
    phone.send(msg.encode("utf-8"))
    data = phone.recv(1024)
    print(data.decode("utf-8"))

   """
   recv和send都是python(應該說是應用程序)發給操作系統的命令
   收發消息需要通過Internet進行傳輸,而Internet需要通過網卡去發送、接收數據,只有操作系統才能調用網卡這個硬件
   所以,具體執行發送、接收消息動作的是操作系統(就如文件處理中的open(file)一樣),python(應用程序)把發送的消息的內存原封不動地復制給操作系統,然後操作系統去發送消息;

   當客戶端發送空消息時,應用程序會把這個空消息復制給操作系統,正常情況下操作系統會根據TCP協議調用網卡,但由於操作系統收到的是空消息,所以操作系統沒有調用任何硬件,
   也就是說,python(應用程序)發送的空消息只發到了客戶端操作系統這一步,然後客戶端的操作系統並沒有接著往下發這個空消息;所以客戶端的程序就卡在了這一步

   """
phone.close()

服務端:

import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",8080))
phone.listen(5)

conn,client_addr = phone.accept()
print(client_addr)

while True:
    data = conn.recv(1024)
    print("客戶端的數據",data)
    conn.send(data.upper())

conn.close()

phone.close()

還有一種情況:以上面的代碼為例, 由於conn是基於客戶端和服務端建立起來的一個雙向通道,假如客戶端被強行終止掉了,那麽這個雙向通道conn就沒有意義了;在Windows系統下,假如客戶端被強行終止,那麽服務端就會報錯,但在Linux系統下,服務端不會報錯,而是進入了while的死循環,為了防止Linux的這個死循環,可以利用如下方法解決:

import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",8080))
phone.listen(5)

conn,client_addr = phone.accept()
print(client_addr)

while True:
    data = conn.recv(1024)
    if not data:break
    """
    由上面的分析可知:正常情況下服務端不可能收到空消息,因為假如客戶端發了空消息,那麽客戶端的操作系統根本不會把這個空消息發出去;
    所以,假如data變成了空消息,那一定是因為conn這個雙向通道少了一方,也就是客戶端單方面終止了;
    所以 if not data:break  # 就是說,假如客戶端已經終止了,那就結束服務端的這個while True循環
    """
    print("客戶端的數據",data)
    conn.send(data.upper())

conn.close()

phone.close()

上述方法是針對Linux的;Windows下客戶端當方面終止程序,服務端直接報錯,所以應該用 try...except...去解決:

while True:
    try:
        data = conn.recv(1024)

        print("客戶端的數據",data)
        conn.send(data.upper())
    except ConnectionResetError:
        break

conn.close()
phone.close()

服務端為多個客戶端提供服務:

服務端代碼如下:

import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",8080))
phone.listen(5)
"""
這個服務端可以為多個服務端服務,但同一時間只能服務於一個客戶端;
當有其他服務端發來建鏈接請求時就掛起,當正在被服務的服務端退出後,掛起的其他服務端建鏈接的請求就會執行;
5為最大的掛起鏈接數
"""

while True:  # 鏈接循環
    conn,client_addr = phone.accept()
    print(client_addr)

    while True: # 通訊循環
        try:
            data = conn.recv(1024)
            print("客戶端的數據",data)
            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()
phone.close()

模擬ssh遠程執行命令:

關於系統命令的知識點補充:

# 一、系統命令:

# windows:
# dir  # 查看某個文件夾下的子文件名和子文件夾名
# ipconfig # 查看本地網卡的IP信息
# tasklist # 查看運行的進程

# Linux系統對應的是:
# ls
# ifconfig
# ps aux

"""
系統命令不能直接在pycharm上寫,而應該在cmd上輸入(Windows系統);
cmd也是一個程序,它的功能非常單一,就是來接收你輸入的有特殊意義的單詞(命令),然後把你輸入的有特殊意義的單詞(命令)解析成操作系統認識的指令去執行;所以這個程序稱之為“命令解釋器”
如: dir f;\learning
Linux系統中: / 代表c盤
"""

# 二、執行系統命令:
# 1、考慮使用os模塊
# import os
# os.system("dir f;\learning")  # 字符串形式的命令

# 但是這種方法是在服務端的終端上打印了dir f:\learning 的子文件和子文件夾名;而我們想要的結果是把命令結果拿到客戶端然後再客戶端打印

"""
res = os.system("dir f;\learning")  # res 只是 os.system("dir f:\learning") 的執行狀態結果:0或者非0(0代表命令執行成功),並不是命令的查看結果
"""

# 執行系統命令,並拿到命令的結果
# 2. subprocess模塊的Popen
import subprocess
obj = subprocess.Popen("dir f:\learning",shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)  # 命令的結果賦值給obj
"""
# 第一個參數是字符串格式的命令
要寫: shell = True # shell是指命令解釋器 # 啟動一個程序來解析前面的字符串,把這個字符串解析成相應的命令去執行  # 相當於起了一個cmd
這個事例中,不管執行結果正確與否,命令的結果都只有一個,你沒告訴subprocess把命令的結果給誰,它就把結果默認給了終端;但我們想要的是把命令的結果給客戶端,而不是終端
所以我們需要通過某種手段告訴subprocess不要把結果給終端,而是把結果先存到一個地方,等我調用的時候發送給客戶端,所以就用到了“管道”的概念;
把命令的結果放到一個管道裏面(操作系統的內存),等你需要的時候再去管道裏面取
讓subprocess把結果放到管道裏的方法:
stdout = subprocess.PIPE  # stdout是命令的正確執行結果  # 命令的正確執行結果放到一個管道裏面
stderr = subprocess.PIPE # stderr是命令的錯誤執行結果  # 每次的 .PIPE都觸發一次PIPE的功能,從而產生一個新的管道;so 這兩個 PIPE是不一樣的
"""

print(obj)
# 打印結果:
# <subprocess.Popen object at 0x0000007994CEA8D0>

print("stdout---->",obj.stdout.read())   # obj從stdout(正確結果)這個管道裏面讀 (從管道讀取一次之後再取就沒有了)

# 打印結果:(bytes格式)(不管服務端還是客戶端,收、發消息都得是bytes形式)
# stdout----> b‘ \xc7\xfd\xb6\xaf\xc6\xf7 F \xd6\xd0\xb5\xc4\xbe\xed\xc3\xbb\xd3\xd0\xb1\xea\xc7\xa9\xa1\xa3\r\n \xbe\xed\xb5\xc4\xd0\xf2\xc1\xd0\xba\xc5\xca\xc7 BCA5-0E10\r\n\r\n f:\\learning \xb5\xc4\xc4\xbf\xc2\xbc\r\n\r\n2018/01/17  16:19    <DIR>          .\r\n2018/01/17  16:19    <DIR>          ..\r\n2018/01/12  01:04    <DIR>          funny\r\n2018/01/15  14:12    <DIR>          IDLE\xd7\xf7\xd2\xb5\xb2\xe2\xca\xd4\r\n2018/02/05  12:04    <DIR>          pycharm_pro\r\n2018/01/19  09:16    <DIR>          pythontest\r\n2018/03/10  11:05    <DIR>          \xd7\xf7\xd2\xb5\xcc\xe1\xbd\xbb\r\n2018/03/08  11:45    <DIR>          \xb2\xa9\xbf\xcd\xa1\xa2\xb4\xed\xce\xf3\xa1\xa2\xd2\xc9\xce\xca\xbd\xd8\xcd\xbc\r\n2018/01/27  18:01    <DIR>          \xbd\xd8\xcd\xbc\r\n2018/01/16  10:33    <DIR>          \xd7\xd4\xd1\xa7\r\n2018/01/11  14:53    <DIR>          \xc4\xac\xd0\xb4\r\n               0 \xb8\xf6\xce\xc4\xbc\xfe              0 \xd7\xd6\xbd\xda\r\n              11 \xb8\xf6\xc4\xbf\xc2\xbc 115,108,585,472 \xbf\xc9\xd3\xc3\xd7\xd6\xbd\xda\r\n‘


# obj.stdout.read()是bytes格式,如果想看bytes格式裏面具體是什麽內容,則需要 decode();
print("stdout---->",obj.stdout.read().decode("gbk"))
"""
encode()是按照什麽編碼,decode()也需要按照相應的編碼;
subprocess.Popen("dir f;\learning")執行的是系統命令,這個命令是提交給操作系統的,由操作系統執行完後拿到一個結果;
由於沒告訴操作系統命令的結果用什麽格式編碼,所以系統會用它默認的編碼格式;所以:
obj.stdout.read().decode("gbk")
"""

print("stderr--->",obj.stderr.read().decode("gbk"))    # 執行結果不一定正確,所以也要從obj.stderr 讀取

# 打印結果:
# stdout---->
# stderr---> 

模擬ssh遠程執行命令具體代碼:

客戶端:

import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(("127.0.0.1",8080))
while True:
    # 1. 發命令
    cmd = input(">>>").strip()  # 客戶端在這行代碼輸入一條命令
    if not cmd:continue
    phone.send(cmd.encode("utf-8"))

    # 2. 得到命令的結果,並打印
    data = phone.recv(1024)  # data是bytes格式,打印需要解碼  # 1024是個坑,待優化
    print(data.decode("gbk"))
    """
    data 解碼需要是gbk,因為:data是由服務端傳來的(stdout+stderr),而stdout和stderr是由 subprocess.Popen()得到的
    subprocess.Popen("命令")是把命令交給了操作系統去處理,操作系統處理命令後會按照自己默認的編碼把處理結果encode,而Windows的默認編碼是 gbk
    """

phone.close()

服務端:

import subprocess
import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",8080))
phone.listen(5)

while True:  # 鏈接循環
    conn,client_addr = phone.accept()

    while True: # 通訊循環
        try:
            # 1. 接收命令
            cmd = conn.recv(1024)  # cmd是bytes格式

            # 2. 執行命令,拿到執行後的結果
            obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)  # subprocess.Popen()中需要的是字符串格式的命令,所以需要把cmd decode;由於客戶端是按照utf-8進行的encode,所以這步需要decode("utf-8")
            stdout = obj.stdout.read()  # obj.stdout需要read
            stderr = obj.stderr.read()  # stderr和stdout都是bytes格式的

            # 3. 把命令的結果返回給客戶端
            conn.send(stdout+stderr)  # + 會影響效率;因為 + 是重新創建了一份stdout和stderr的新的內存空間(把stdout和stderr的內存空間重新copy了一遍)# 所以+是一個可以優化的點
        except ConnectionResetError:
            break

    conn.close()
phone.close()

網絡編程基礎:網絡基礎之網絡協議、socket模塊