Python day13--网络编程
一、OSI七層,tcp,udp協議
?
? ? ? ? 首先,我們今天使用的計算機都是要聯網使用的。很少有那種單機走遍天下的情況。那么我們的計算機是如何通過網絡實現通信的。我們先了解一些關于網絡的基礎只是。然后再開始學習一些關于網絡編程的內容。第一個要解釋的名詞叫協議。我們只有明白協議是什么,后面再學習各種各樣的通信規則就容易的多了。
? ? ? ? 官方:網絡協議是通信計算機雙方必須共同遵從的一組約定。那么怎么樣建立連接、怎么樣互相識別等。只有遵守這個約定,計算機之間才能相互通信交流。
? ? ? ? 普通話:兩臺計算機之間約定好,我發送的數據格式是什么,你接收到數據后,使用相同的格式來拿到數據。
舉個栗子:
你和韓國人交流,你說中文,他說韓語,你倆是不能明白對方說什么的,怎么辦?你倆約定好,都說英語,實現交流;這里這個約定就叫做協議。
? ? ? ? 網絡協議:互聯網之間互相傳遞消息的時候使用統一的一系列約定。
? ? ? ? 再今天的互聯網數據傳輸種一般使用的是OSI七層協議。也有簡稱為五層,四層協議。只是對不同網絡層的定義不同。內部原理和作用是一樣的。
????????
????????每一層分別是做什么的?這里涉及到的網絡知識非常的負責。只做簡單介紹,如果想深入研究,請參考大學課本(計算機網絡)。
? ? ? ? 首先是物理層,這一層沒啥說的,作用就是把0101等電信號發送出去,也就是我們常說的數模轉換與模數轉換(數字信號轉換模擬信號,模擬信號轉換數字信號),物理層也是網絡傳輸的基礎,首先要保證物理連接是沒有問題才能進行數據傳輸。
? ? ? ? 數據鏈路層,這一層負責裝配自己和對方主機的MAC地址。MAC地址:每個網絡設備唯一的編碼。全球唯一,由不同廠商直接燒錄再網卡上。作用:在龐大的網絡系統種,你要發送的數據到底是要給誰,由誰發送出來的;這就相當于你寫信時候的信封,上面得寫清楚收信人地址。
? ? ? ? 網絡層:
? ? ? ? 在有了MAC地址其實我們的電腦就可以開始通信了。但是,此時的通信方式是廣播。相當于通信基本靠吼。你發送一個數據出去,會自動的發給當前網絡下的所有計算機。然后每個計算機的網卡會看一眼這個數據是不是發給自己的。像這樣的通信方式,如果計算機的數據量少,是沒有問題的。但是,如果全球所有計算機都按照這樣的方式來傳輸消息,那不僅僅是效率的問題了,絕對是災難性的。那怎么辦,大家就想到了一個新的方案,這個方案叫IP協議,使用IP協議就把不同區域的計算機劃分成一個一個的子網,子網內的通信使用廣播來傳遞消息。廣播外通過路由進行傳遞消息,可以理解為不同快遞公司的分撥中心,我給你寄一個快遞,先看一下你是不是和我一個區域的,是自己區域直接挨家挨戶找就OK了。但是如果你不是我這個區域的,就通過你得分撥中心下發給你,這里IP協議的作用就體現出來了。
網絡層提出了子網(局域網)的概念。通過IP地址和子網掩碼來劃分子網。
????????傳輸層:
????????我們現在解決了外界的數據傳輸問題。使用MAC地址和IP地址可以唯一的定位到一臺計算機了。那么我們知道一臺計算機內是很有可能運行著多個網絡應用和程序的。比如你開著QQ,登陸這微信,還看著芒果TV,還瀏覽的微博。那么此時你得計算機網卡接收到了來自遠方的一條數據,這條數據到底給哪個應用程序呢?說白了,快遞到你公司了,地址沒問題,可是你公司那么多人,這個快遞到底給誰?不能隨便給一個人吧,怎么辦呢?互聯網大佬們想到了一個新詞叫做‘端口’。
傳輸層規定:給每一個應用程序分配一個唯一的端口號,當有數據發送過來后,通過端口號來決定該數據發送的具體應用程序。
但是根據不同的應用程序需求,傳輸層分為2個協議,一個叫TCP,一個叫UDP。TCP可靠,UDP速度快;TCP對系統資源要求較多,UDO對系統資源要求較少。
????????應用層:
????????這一層順理成章,TCP+IP就可以定位到計算機上的某個應用了。但是不同應用傳輸的數據格式可能不是一樣的。就好比快遞,有的是大包裹,有的是小文件,一個要用大麻袋裝,一個要用文件袋裝。到了應用層,我們一般是根據不同類型的應用程序進行的再一次封裝。比如,HTTP協議,SMTP協議,FTP協議等等。
二、初識Socket-TCP編程
在python, 哦不, 是幾乎所有的編程語言中, 我們在編寫網絡程序的時候都要使用到socket. socket翻譯過來叫套接字(很癟嘴. 所以沒人這么叫它). 我們上面也了解到了一次網絡通信的數據需要包裹著mac, ip, port等信息. 但是如果每次我們開發都要程序員去一個一個的去準備數據, 那工作量絕對是絕望的. 所以, 計算機提出了socket. socket幫助我們完成了網絡通信中的絕大多數操作. 我們只需要告訴socket. 我要向哪臺計算機(ip, port)發送數據. 剩下的所有東西都由socket幫我們完成. 所以使用socket完成數據傳輸是非常方便的.
基本的socket-tcp編程
服務器:
import socket# 創建一個socket通道 sk = socket.socket()# 注冊一家洗腳城 sk.bind(("127.0.0.1", 5000)) # 綁定一個ip和端口, 選址 sk.listen() # 開始監聽 開張 print("服務器端準備就緒, 等待連接") conn, address = sk.accept() # 程序會停在這里. 阻塞 等待客人上門 print("有人來連我了, 他的地址是:", address) # 這個address里放著你要的客戶端ip和端口 conn.send("來啊, 大爺, 來玩兒啊".encode("utf-8")) # 發送出去的內容只能是bytes. 給客人服務客戶端:
import socketsk = socket.socket() # 知道有洗腳城 print("客戶端初始化完畢") sk.connect(("127.0.0.1", 5000)) # 建立鏈接 登門拜訪xxxx洗腳城 print("客戶端鏈接成功") print(sk.recv(1024).decode("utf-8")) # 最大接收1024個字節的內容 接收服務此時, 我們可以讓客戶端和服務器端進行基本的網絡通信了. 那如果是想要連續發送數據怎么辦? 加循環啊
服務器:
import socketsk = socket.socket() sk.bind(("127.0.0.1", 5000)) sk.listen() conn, address = sk.accept() while 1: conn.send(input(">>>:").encode("utf-8")) msg = conn.recv(1024).decode("utf-8")print("收到的內容是: ", msg)客戶端:
import socketsk = socket.socket() sk.connect(("127.0.0.1", 5000)) while 1:msg = sk.recv(1024).decode("utf-8")print("收到的內容是:", msg)sk.send(input(">>>:").encode("utf-8"))最基本的TCP編程咱們就先了解這么多. 稍后還會繼續進行拓展的.
三、 初識Socket-UDP編程
使用udp編程
server端:
import socketsk = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) sk.bind(("127.0.0.1", 12123))msg, address = sk.recvfrom(1024) print(msg)sk.sendto(b"i love you", address)client端
import socketsk = socket.socket(type=socket.SOCK_DGRAM)sk.sendto(b'hello', ("127.0.0.1", 12123)) msg, addr = sk.recvfrom(1024)print(msg)四、TCP和UDP的對比
兩種網絡編程的方式最基本的代碼已經OK了. 也能進行通信了. 那么問題來了. TCP和UDP的區別是什么?
TCP是基于鏈接的. 在傳輸數據之前必須保證鏈接的存在. 通信是通過conn來完成的. 所以, TCP通信具有以下特點:
1. 可靠
2. 連續
3. 效率低
三次握手
TCP協議, 為了保障數據傳輸的連續性可靠性. 建立鏈接的時候. 會給服務器發送一個x. 服務器返回客戶端x+1同時返回一個y, 客戶端接收到y, 返回y+1作為回執. 用來保證客戶端和服務器是正常鏈接狀態. 之后才開始進行傳輸數據. 有點兒類似我們打電話的時候,
我: 你好, 是周杰倫么
你: 你好, 我是周杰倫, 你是么林俊杰
我: 我是林俊杰, 咱倆開始聊天吧
目的就是確保在傳輸數據之前我們兩個身份確認成功.
四次揮手:
在傳輸完數據之后, 客戶端會告訴服務器. 我這完事兒了. 給一個狀態x+2,y+1. 服務器收到之后. 返回x+3, 等數據接收完畢之后返回y+1. 告訴客戶端傳輸完畢了. 客戶端最終返回y+2作為回執. 告訴服務器. 我走了.
我: 我要說的就這么多
你: 好的, 我記一下
你: 我記好了. 你可以滾蛋了
我: 好的,我滾了
?
經典問題: 為什么TCP協議是三次握手, 四次揮手?
因為在握手的時候, 兩邊其實還沒有開始發送數據. 只是建立鏈接, 假設數據從Client向Server端發送. 客戶端先發送一個SYN報文告訴服務器. 我要連你, 服務器回客戶端, 我收到了(ACK), 相應的, 服務器也可以給客戶端發送數據啊. 服務器也要發送一個SYN報文. 所以呢, 為了省事兒. 服務器把發送請求的SYN報文和應答的ACK報文就一起發出去了. 最后客戶端發送一個應答(ACK) 這是三次握手. 實際上是4個動作. 只不過中間的服務器的SYN和ACK是一起發的.
那么在揮手的時候為什么是次呢. 梳理一下過程: 客戶端發送完數據. 給服務器放FIN報文. 告訴服務器. 我完事兒了. 服務器先答應一下(ACK), oK 我知道了. 然后此時. 服務器有可能還有數據沒發完呢. 只是你客戶端說的. 你完事兒了. 我這不一定完事兒了啊. 所以, 服務器就只是發送了一個ACK, 告訴客戶端, 我收到了. 等服務器這邊忙完了. 該收的收了. 該發的發了. 服務器給客戶端發送一個FIN報文. 告訴客戶端, 我這也完事兒了. 然后客戶端說: 好的(ACK) . 整個鏈接關閉.
我什么是4次揮手呢. 因為不論哪一方發起的FIN. 對方只能先回應一下(ACK). 得等對方完成了相關操作, 才能返回FIN. 最后在ACK. 所以. 關閉鏈接必須是4步.
五、黏[nián]包現象
在使用TCP協議進行數據傳輸的時候, 會有以下問題出現.
client:
import socketsk = socket.socket() sk.connect(("127.0.0.1", 8101))# 連續發送數據 s = "我愛你" sk.send(s.encode("utf-8")) sk.send(s.encode("utf-8"))print("發送完畢")sk.close()server:
import socketsk = socket.socket() sk.bind(("127.0.0.1", 8101)) sk.listen() conn, addr = sk.accept()msg1 = conn.recv(1024) print(msg1.decode("utf-8"))msg2 = conn.recv(1024) print(msg2.decode("utf-8"))sk.close()運行結果
?
我們發現, 打印來的效果是兩個數據包合在一起了. 為什么會這樣呢? 在數據傳輸的時候客戶端發送兩次數據. 這兩個數據并不是直接發送出去的. 首先會存放在緩沖區. 等緩沖區數據裝滿或者經過一段時間后. 會把緩沖區中的數據一起發送出去. 這就導致了一個很坑的現象. 明明是兩次發送的數據. 被合在了一起. 這就是典型的黏包現象.
注意, 黏包現象只有TCP才會出現. UDP是不會出現黏包的. 因為UDP的不連續性. 每次發送的數據都會立刻打包成數據包然后發出去. 數據包與數據包之間是有邊界隔離的. 你可以認為是一個sendto對應一個recvfrom. 因此UDP不會出現黏包.
那么如何解決黏包問題呢? 很簡單. 之所以出現黏包就是因為數據沒有邊界. 直接把兩個包混合成了一個包. 那么我可以在發送數據的時候. 指定邊界. 告訴對方. 我接下來這個數據包有多大. 對面接收數據的時候呢, 先讀取該數據包的大小.然后再讀取數據. 就不會產生黏包了.
普通話: 發送數據的時候制定數據的格式: 長度+數據 接收的時候就知道有多少是當前這個數據包的大小了. 也就相當于定義了分隔邊界了.
client:
import socketsk = socket.socket() sk.connect(("127.0.0.1", 8101))# 連續發送數據 s = "我愛你" bs = s.encode("utf-8") # 計算數據長度. 格式化成4位數字 bs_len = format(len(bs), "04d").encode("utf-8") # 發送數據之前. 先發送長度 # 整個數據包: 0009\x\x\x\x\x\x... sk.send(bs_len) sk.send(bs)sk.send(bs_len) sk.send(bs)print("發送完畢")sk.close()server:
import socketsk = socket.socket() sk.bind(("127.0.0.1", 8101))sk.listen()conn, addr = sk.accept()# 整個數據包: 0009\x\x\x\x\x\x... # 接收4個字節. 轉換成數字 bs_len = int(conn.recv(4).decode('Utf-8'))# 讀取數據 msg1 = conn.recv(bs_len) print(msg1.decode("utf-8"))bs_len = int(conn.recv(4).decode('Utf-8')) msg2 = conn.recv(bs_len) print(msg2.decode("utf-8"))sk.close()如果每次發送數據都要經過這么一次. 屬實有點兒累. 沒關系. python提供了一個很好用的模塊來幫我們解決這個惡心的問題
import struct# 打包. 把一個數字打包成字節 ret = struct.pack("i", 123456789) print(ret)print(len(ret)) # 4 不論數字大小, 定死了4個字節# 把字節還原回數字 bs = b'\x15\xcd[\x07' num = struct.unpack("i", bs)[0] print(num)六、優雅的解決黏包問題
client:
import socket import structsk = socket.socket() sk.connect(("127.0.0.1", 8123))msg_bs = "我愛你".encode("utf-8") msg_struct_len = struct.pack("i", len(msg_bs))# 發一次 sk.send(msg_struct_len) sk.send(msg_bs)# 發兩次 sk.send(msg_struct_len) sk.send(msg_bs)server:
import socket import structsk = socket.socket()sk.bind(("127.0.0.1", 8123))sk.listen() conn, addr = sk.accept()# 接收一個數據包 msg_struct_len = conn.recv(4) msg_len = struct.unpack("i", msg_struct_len)[0] data = conn.recv(msg_len) print(data.decode('utf-8'))# 接收第二個數據包 msg_struct_len = conn.recv(4) msg_len = struct.unpack("i", msg_struct_len)[0] data = conn.recv(msg_len) print(data.decode('utf-8'))看著還是別扭. 提取一個模塊試試看
my_socket_util
import structdef my_send(sk, msg):msg_bs = msg.encode("utf-8")msg_struct_len = struct.pack("i", len(msg_bs))sk.send(msg_struct_len)sk.send(msg_bs)def my_recv(sk):# 接收一個數據包msg_struct_len = sk.recv(4)msg_len = struct.unpack("i", msg_struct_len)[0]data = sk.recv(msg_len)return data.decode("utf-8")client:
import socket import my_sk_util as msusk = socket.socket() sk.connect(("127.0.0.1", 8123))msu.my_send(sk, "我愛你") msu.my_send(sk, "我愛你")server:
import socket import my_sk_util as msusk = socket.socket()sk.bind(("127.0.0.1", 8123))sk.listen() conn, addr = sk.accept()print(msu.my_recv(conn)) print(msu.my_recv(conn))七、文件上傳
client:
import socket import os import struct import jsonsk = socket.socket() sk.connect(("172.10.1.16", 14399))# 要發送的文件 file_path = "my_sk_util.py"# 拿到文件大小和文件名字 file_size = os.path.getsize(file_path) file_name = os.path.basename(file_path)# 組裝一個字典. file_json = {"file_name": file_name, "file_size": file_size} # 轉化成json字符串, 里面存著數據 file_json_str = json.dumps(file_json) # 把json字符串發送出去. 防止黏包. 需要先發送數據大小 file_json_bs = file_json_str.encode("utf-8") file_len_bs = struct.pack("i", len(file_json_bs)) sk.send(file_len_bs)# 發送json數據 sk.send(file_json_bs)# 發送文件數據 with open(file_path, mode="rb") as f:while file_size > 0:bs = f.read(1024) # 每次最多發送1024個字節sk.send(bs)file_size -= len(bs) # 發一次少一些字節print("上傳完畢") sk.close()server:
import socket import struct import jsonsk = socket.socket() sk.bind(("172.10.1.16", 14399)) sk.listen()conn, address = sk.accept()# 接收json長度, 放黏包 file_json_len_bs = conn.recv(4) file_json_len = struct.unpack("i", file_json_len_bs)[0] # 獲取json字符串 file_json_str = conn.recv(file_json_len).decode('utf-8') # 轉化回字典 file_json = json.loads(file_json_str)with open(f"上傳/{file_json['file_name']}", mode="wb") as f:while file_json['file_size'] > 0:bs = conn.recv(1024)file_json['file_size'] -= len(bs)f.write(bs)print("one part")print("上傳完畢") sk.close()文件下載的邏輯和上傳的邏輯是一樣的.
八、socketserver
由于TCP是連續的. 就我們目前的代碼而言. 服務器端是無法處理多個人的請求的(同時). 就比如. 寫一個這樣的代碼:
server:
import socket import struct import json import subprocess sk = socket.socket() sk.bind(("127.0.0.1", 12233)) sk.listen()conn, address = sk.accept()while 1:shell_len_bs = conn.recv(4)shell_len = struct.unpack("i", shell_len_bs)[0]shell = conn.recv(shell_len).decode("utf-8")# 用來執行shell腳本.ret = subprocess.Popen(shell, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)d = {"msg": ret.stdout.read().decode("utf-8"), "error": ret.stderr.read().decode("utf-8")}msg_json_bs = json.dumps(d).encode("utf-8")msg_json_len_bs = struct.pack("i", len(msg_json_bs))conn.send(msg_json_len_bs)conn.send(msg_json_bs)client:
import socket import struct import json import subprocesssk = socket.socket() sk.bind(("127.0.0.1", 12233)) sk.listen()conn, address = sk.accept()while 1:shell_len_bs = conn.recv(4)shell_len = struct.unpack("i", shell_len_bs)[0]shell = conn.recv(shell_len).decode("utf-8")ret = subprocess.Popen(shell, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)d = {"msg": ret.stdout.read().decode("utf-8"), "error": ret.stderr.read().decode("utf-8")}msg_json_bs = json.dumps(d).encode("utf-8")msg_json_len_bs = struct.pack("i", len(msg_json_bs))conn.send(msg_json_len_bs)conn.send(msg_json_bs)此時我們發現只有一個人能連接到服務器. 因為在服務器端只有accept來接收客戶端的連接. 并且接收客戶端鏈接和接發數據是串行的. 如果我們需要一個能并行執行的server怎么辦呢? python中提供了socketserver來解決這個問題
牛B版本socketserver:
import socketserver import subprocess import struct import jsonclass MyServer(socketserver.BaseRequestHandler):def handle(self):conn = self.requestwhile 1:shell_len_bs = conn.recv(4)shell_len = struct.unpack("i", shell_len_bs)[0]shell = conn.recv(shell_len).decode("utf-8")ret = subprocess.Popen(shell, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)d = {"msg":ret.stdout.read().decode("utf-8"), "error": ret.stderr.read().decode("utf-8")}msg_json_bs = json.dumps(d).encode("utf-8")msg_json_len_bs = struct.pack("i", len(msg_json_bs))conn.send(msg_json_len_bs)conn.send(msg_json_bs)if __name__ == '__main__':sock = socketserver.ThreadingTCPServer(("127.0.0.1", 8991), MyServer)sock.serve_forever()總結
以上是生活随笔為你收集整理的Python day13--网络编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 观《从你的全世界路过》
- 下一篇: 从你的世界路过