網絡編程基礎:網絡基礎之網絡協議、socket模塊
操作系統(簡稱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模塊