入門篇¶ 官方文檔:https://docs.python.org/3/library/ipc.html(進程間通信和網路) 實例代碼:https://github.com/lotapp/BaseCode/tree/master/python/6.net 1.概念¶ 1.1.Python方向¶ 已經 ...
入門篇¶
官方文檔:https://docs.python.org/3/library/ipc.html(進程間通信和網路)
實例代碼:https://github.com/lotapp/BaseCode/tree/master/python/6.net
1.概念¶
1.1.Python方向¶
已經講了很多Python的知識了,那Python能幹啥呢?這個是我眼中的Python:
Python方向:
- 早期方向
- Web全棧
- 擅長專欄
- 爬蟲系列
- 數據分析
- 人工智慧
物聯網系
(lot
萬物互聯)- 自動化運維(
安全
與測試
)
- 其他系列
- 游戲開發(最近很火)
如果想專攻Web
、爬蟲
、物聯網
、游戲
等等方向,網路
這塊是硬條件,So ==> 不要不急,咱們繼續學習~
多句嘴,一般情況下需要什麼庫就去官方看下,沒有再考慮第三方:https://docs.python.org/3/library
1.2.拙見一點點¶
技術前景:(註意加粗方向)
- Python:
- 最常用:
Data
- 最看好:
LoT
- 最火是:
AI
- 經典是:
Web
- 壟斷是:
System
- 最常用:
- Web:
- 最看好:
小程式
- 最常見:
移動端
- 最火是:
Web端
- 最看好:
- Go(
高併發
、區塊鏈
)、C(基礎
) NetCore
(WebAPI
、EFCore
)
總的來說:Python最吃香,Go最有潛力,Web必不可少,NetCore性價比高
現在基本沒有單一方向的程式員了,如果有可以默默思考幾分鐘,一般都是JS
and Python
and (Go
or NetCore
)【二選一】
其他行業:(僅代表逆天個人看法)
- 設計師:
影視製作
(剪輯師、合成師、特效師)【目前最火,性價比很高】修圖師
(商業修片、影樓後期)【大咖特別多,創業很吃香】UI|UE
(最容易找工作)平面設計
(最常見)室內設計
(高手很吃香)
- 教育:
幼兒編程
和中醫課
最火琴棋書畫武
+國學
需求頗高英語
一直是出國必學
- 營銷:
新媒體+短視頻
- 旅游:
出國游
1.2.分層模型¶
1.OSI
7層模型¶
- 物理層:物理設備標準,主要作用就是傳輸比特流(數據為bit)eg:網線介面、光纖介面、各種傳輸介質的傳輸速率
- 雙絞線,光纖(硬體)
- 數據鏈路層:對物理層的校驗(是否有丟失、錯誤)
- 數據的傳輸和數據檢測(網卡層)
- 網路層:指定傳輸過程中的路徑。eg:IP
- 為數據包選擇路由(保證數據傳達)
- 傳輸層:定義了傳輸數據的協議和埠號(主要就是攜帶了埠號,這樣可以找到對應的進程)
- 提供端對端的介面,eg:TCP、UDP
- 會話層:通過傳輸層,在端與端之間(埠)建立數據傳輸通道(設備之間可以通過IP、Mac、主機名相互認識)
- 解除或者建立和別的節點之間的聯繫
- 表示層:保證一個系統應用發的消息可以被另一個系統應用讀取到。eg:兩個應用發送的消息格式不同(eg:UTF和ASCII各自表示同一字元),有必要時會以一種通用格式來實現不同數據格式之間的轉換
- 數據格式化、代碼轉化、數據加密
- 應用層:為用戶的應用程式提供網路服務
- 文件傳輸、電子郵箱、文件服務、虛擬終端
我用PPT畫了個圖:(物
數
網
傳
會
表
應
)
2.TCP/IP
4層模型¶
- 網路介面層:(
物、數
)- eg:乙太網幀協議
- 網路層:
- eg:IP、ARP協議
- 傳輸層:
- eg:TCP、UDP協議
- 應用層:(
會、表、應
)我們基本上都是關註這個- eg:FTP、SSH、HTTP協議...
1.3.協議相關¶
電腦和電腦網路通信前達成的一種約定,舉個例子:以漢語為交流語言
再舉個發送文件的例子,PPT做個動畫:(自定義協議-文件傳輸演示)
B/S
基本上都是HTTP
協議,C/S
開發的時候有時會使用自己的協議,比如某大型游戲,比如很多框架都有自己的協議:
- Redis的
redis://
- Dubbo的
dubbo://
協議
總的來說,基本上都是HTTP
協議,對性能要求高的就使用TCP
協議,更高性能要求就自己封裝協議了,比如騰訊在UDP
基礎上封裝了自己的協議來保證通信的可靠性
數據包的封裝¶
先看一個老外
的動畫(忽略水印廣告):https://v.qq.com/x/page/w01984zbrmy.html
以TCP/IP四層協議為例:數據包的逐層封裝
和解包
都是操作系統
來做的,我們只管應用層
發送過程:
- 發送消息
- 應用層添加了協議頭
- 傳輸層添加
TCP
段首 - 網路層添加
IP
報頭 - 網路介面層(鏈路層)添加幀頭和幀尾
PPT動畫示意:
接收過程:
- 去除鏈路層的幀頭和幀尾
- 去除網路層
IP
的報頭 - 去除傳輸層
TCP
的段首 - 去除應用層的協議頭
- 獲取到數據
PPT動畫示意:
我們下麵按照解包順序簡單說說各種格式:
1.乙太網幀格式¶
先看一下這個是啥?用上面動畫內容表示:
乙太網幀協議:根據MAC
地址完成數據包傳遞
如果只知道IP,並不知道MAC
地址,可以使用ARP
請求來獲取:
ARP
數據報:根據IP
獲取MAC
地址(網卡編號)ARP
只適合IPv4
,IPv6
用ICMPV6
來代替ARP
- 在
TCP/IP
模型中,ARP
協議屬於IP
層;在OSI
模型中,ARP
協議屬於鏈路層
PPT畫一張圖:1bit = 8byte
(1位元組=8位)
課後思考:根據ARP原理想想ARP欺騙
到底扎回事?(IP進行ARP請求後會緩存,緩存失效前不會再去ARP請求)
擴展:
RARP 是反向地址轉換協議,通過 MAC 地址確定 IP 地址
- 真實IP在網路層的IP協議之中,乙太網幀中的IP是下一跳的IP地址(路由)
- 每到一個路由都要解網路層的包(知道到底需要獲取哪個IP)
MAC
地址就是硬體地址,廠商向全球組織申請唯一編號(類似於身份證)- 最後附上手畫的ARP數據報圖示:(一般都不是一步得到MAC的,多數都是經過一個個路由節點最終獲取到MAC)
2.IP段格式¶
先貼一IP段格式圖片(網路):
我們在這不去詳細講解,擴展部分有課後拓展,我就說一個大多數人困惑的點:
查看IP
信息的時候經常會看到192.168.36.235/24
,這個/24
一直爭議很大
我們來簡單解釋一下:IP為192.168.36.235
192.168.36
:網路標識235
:主機標識/24
:標識從頭數到多少位為止屬於網路標識(剩下的就是可分配的主機數了)- 二進位表示為:
11111111 11111111 11111111 00000000
(24個1) - 翻譯成子網掩碼就是:
255.255.255.0
(/多少
就數多少個1,然後轉化) - 表示可以有255個ip用來自行分配(記得去除路由之類的占用)
- 二進位表示為:
擴展:IP屬於面向無連接行(IP
協議不保證傳輸的可靠性,數據包在傳輸過程中可能丟失,可靠性可以在上層協議或應用程式中提供支持)
面向連接
和面向無連接
區別如圖:(圖片來自網路)
預告¶
關於TCP和UDP的內容下次繼續~
課外拓展:
圖解TCP/IP第五版
鏈接: https://pan.baidu.com/s/1C4kpNd2MvljxfwTKO082lw 提取碼: 7qce
Python網路編程第三版
Code:https://github.com/brandon-rhodes/fopnp
PDF:鏈接: https://pan.baidu.com/s/1jhW-Te-GCEFKrZVf46S_Tw 提取碼: d7fw
網路基礎-含書簽(網路文檔)
鏈接: https://pan.baidu.com/s/1WZ1D4BthA4qBk2QXBAjm4w 提取碼: jmdg
老外講解網路數據包解析:
下載:https://pan.baidu.com/s/1uUjahs_b05y9Re9ROtzzIw
中文:http://video.tudou.com/v/XMjE3MTg0NzkzNg==.html
英文:http://video.tudou.com/v/XMTkyNjU5NDYwOA==.html
2.UDP¶
實例代碼:https://github.com/lotapp/BaseCode/tree/master/python/6.net/1.UDP
UDP
是無連接的傳輸協議,不保證可靠性。使用UDP
協議的應用程式需要自己完成丟包重發、消息排序等工作(有點像寄信)
2.1.UDP發送消息¶
引入案例¶
看個UDP的簡單案例:
import socket def main(): # AF_INET ==> IPV4;SOCK_STREAM ==> 類型是TCP,stream 流 # SOCK_DGRAM ==> 類型是UDP,dgram 數據報、數據報套接字 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_sock: udp_sock.sendto("大兄弟,你好啊".encode("utf-8"), ("192.168.36.235", 8080)) print("over") if __name__ == '__main__': main()
接收到的消息:這時候埠是隨機的
看起來代碼還挺麻煩,我稍微分析下你就知道對比其他語言真的太簡單了:
標識:
AF_INET
==>IPV4
SOCK_DGRAM
==> 類型是UDP
SOCK_STREAM
==> 類型是TCP
代碼三步走:
- 創建
udp_sock=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- 發送
udp_sock.sendto(Bytes內容,(IP,Port))
接收:udp_sock.recvfrom(count)
- 關閉
udp_sock.close()
埠綁定¶
藉助調試工具
(點我下載)可以知道:上面程式每次運行,埠都不固定
那怎麼使用固定埠呢?==> udp_socket.bind(('', 5400))
import socket def main(): with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_socket: # 綁定固定埠 udp_socket.bind(('', 5400)) # 發送消息 udp_socket.sendto("小明,你知道小張的生日嗎?\n".encode("utf-8"), ("192.168.36.235", 8080)) print("over") if __name__ == '__main__': main()
消息圖示:nc -ul 8080
(nc -l
是監聽TCP)
調試工具:
2.2.UDP接收消息¶
先看一個簡單版本的:udp_socket.recvfrom(1024)
from socket import socket, AF_INET, SOCK_DGRAM def main(): with socket(AF_INET, SOCK_DGRAM) as udp_socket: # 綁定埠 udp_socket.bind(('', 5400)) while True: # 發送消息 udp_socket.sendto("你可以給我離線留言了\n".encode("utf-8"), ("192.168.36.235", 8080)) # 接收消息(data,(ip,port)) data, info = udp_socket.recvfrom(1024) print(f"[來自{info[0]}:{info[1]}的消息]:\n{data.decode('utf-8')}") if __name__ == '__main__': main()
圖示:接收消息(data,(ip,port))
題外話(Nmap)¶
其實如果你使用Nmap
來掃描的話並不能發現nc
打開的UDP
埠:
稍微解釋一下:掃描其實就是發了幾個空消息過去
-sU
代表掃描UDP,-sT
代表掃描TCP-Pn
這個主要是針對有些伺服器禁用ping的處理(ping不通也嘗試)-p
指定埠號,如果是所有埠可以使用-p-
sudo
是因為在Ubuntu
下沒許可權,kali
下可以直接使用nmap
可能有人對nc
輸出的你可以給離線留意了
有疑惑,其實就是在給5400埠發空消息的時候~True迴圈了兩次
來張對比圖:
掃描TCP和UDP埠:sudo nmap -sTU 192.168.36.235 -Pn
課後擴展:
NC命令擴展:https://www.cnblogs.com/nmap/p/6148306.html
Nmap基礎:https://www.cnblogs.com/dunitian/p/5074784.html
收放自如¶
如果還是用True迴圈來實現:
from socket import socket, AF_INET, SOCK_DGRAM def main(): with socket(AF_INET, SOCK_DGRAM) as udp_socket: # 綁定埠 udp_socket.bind(('', 5400)) while True: msg = input("請輸入發送的內容:") if msg == "dotnetcrazy": break else: udp_socket.sendto( msg.encode("utf-8"), ("192.168.36.235", 8080)) data, info = udp_socket.recvfrom(1024) print(f"[來自{info[0]}:{info[1]}的消息]:\n{data.decode('utf-8')}") if __name__ == '__main__': main()
你會發現,消息不能輪流發送,只能等對方方式後再發,雖然有處理方式,但太麻煩,這時候就可以使用我們之前說的多線程來改寫一下了:
from socket import socket, AF_INET, SOCK_DGRAM from multiprocessing.dummy import Pool as ThreadPool def send_msg(udp_socket): while True: msg = input("輸入需要發送的消息:\n") udp_socket.sendto(msg.encode("utf-8"), ("192.168.36.235", 8080)) def recv_msg(udp_socket): while True: data, info = udp_socket.recvfrom(1024) print(f"[來自{info[0]}:{info[1]}的消息]:\n{data.decode('utf-8')}") def main(): # 創建一個Socket with socket(AF_INET, SOCK_DGRAM) as udp_socket: # 綁定埠 udp_socket.bind(('', 5400)) # 創建一個線程池 pool = ThreadPool() # 接收消息 pool.apply_async(recv_msg, args=(udp_socket, )) # 發送消息 pool.apply_async(send_msg, args=(udp_socket, )) pool.close() # 不再添加任務 pool.join() # 等待線程池執行完畢 print("over") if __name__ == '__main__': main()
輸出:(就一個註意點~socket在pool之後關閉
)
2.3.手寫UDP網路調試工具¶
調試工具功能比較簡單,我們手寫一個UDP
版的:
from socket import socket, AF_INET, SOCK_DGRAM from multiprocessing.dummy import Pool as ThreadPool def get_port(msg): """獲取用戶輸入的埠號""" while True: port = input(msg) try: port = int(port) except Exception as ex: print(ex) else: return port # 沒有錯誤就退出死迴圈 def recv_msg(udp_socket): """接收消息""" while True: data, info = udp_socket.recvfrom(1024) print(f"[來自{info[0]}:{info[1]}的消息]:\n{data.decode('utf-8')}") def send_msg(udp_socket): """發送消息""" ip = input("請輸入對方IP:") port = get_port("請輸入對方埠號:") while True: msg = input("請輸入發送的消息:\n") udp_socket.sendto(msg.encode("utf-8"), (ip, port)) def main(): with socket(AF_INET, SOCK_DGRAM) as udp_socket: # 綁定埠 udp_socket.bind(('', get_port("請輸網路助手的埠號:"))) # 創建一個線程池 pool = ThreadPool() # 接收消息 pool.apply_async(recv_msg, args=(udp_socket, )) # 發送消息 pool.apply_async(send_msg, args=(udp_socket, )) pool.close() pool.join() if __name__ == '__main__': main()
CentOSIP
和Port
(192.168.36.123:5400
)
演示:(多PC演示)
簡單說下本機IP的綁定:
Net裡面習慣使用localhost
,很多人不知道到底是啥,其實你打開host
文件就可以看到 ==> 127.0.0.1
被重定向為localhost
,在Linux裡面也是這樣的,每個PC對應的都是lo
迴環地址:
本機通信時,對方ip就可以使用127.0.0.1
了,當然了綁定本機ip的時候也可以使用127.0.0.1
(bind(('',))
中的空其實填的就是這個)(很多地方也會使用0.0.0.0
)
_LOCALHOST = '127.0.0.1' # 看這 _LOCALHOST_V6 = '::1' def socketpair(family=AF_INET, type=SOCK_STREAM, proto=0): if family == AF_INET: host = _LOCALHOST # 看這 elif family == AF_INET6: host = _LOCALHOST_V6 .... lsock = socket(family, type, proto) try: lsock.bind((host, 0)) # 看這 lsock.listen() ...
2.4.NetCore版¶
快速實現一下:
using System.Net; using System.Text; using System.Net.Sockets; namespace netcore { class Program { static void Main(string[] args) { // UDP通信 using (var udp_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) { var ip_addr = IPAddress.Parse("192.168.36.235"); // 綁定本地埠 udp_socket.Bind(new IPEndPoint(ip_addr, 5400)); // UDP發送消息 int i = udp_socket.SendTo(Encoding.UTF8.GetBytes("小明你好啊~"), new IPEndPoint(ip_addr, 8080)); Console.WriteLine($"發送計數:{i}"); } Console.WriteLine("over"); } } }
3.TCP¶
示例代碼:https://github.com/lotapp/BaseCode/tree/master/python/6.net/2.TCP
TCP
是一種面向連接的、可靠的協議,TCP
傳輸的雙方需要首先建立連接,之後由TCP
協議保證數據收發的可靠性,丟失的數據包自動重發,上層應用程式收到的總是可靠的數據流,通訊之後關閉連接(有點像打電話)
用過下載軟體的可能遇到過一種‘Bug’
==> 很多人為了防止自己本地文件納入共用大軍,一般都是直接把網路上傳給禁了,然後發現文件經常出問題?
其實這個就是TCP
的一個應用,文件一般都很大,所以進行分割後批量下載,那少量的網路上傳其實是為了校驗一下文件 ==> 正確做法是限制上傳速度而不是禁止(學生時代那會還經常蛋疼這個問題,現在想想還挺好玩的O(∩_∩)O
)
大多數連接都是可靠的TCP連接。創建TCP連接時,主動發起連接的叫客戶端,被動響應連接的叫伺服器
上面那個例子里,我們的下載工具就是客戶端,每一小段文件接收完畢後都會向伺服器發送一個完成的指令來保證文件的完整性
3.1.TCP客戶端¶
來看一個簡單的入門案例:
from socket import socket def main(): # 預設就是創建TCP Socket with socket() as tcp_socket: # 連接伺服器(沒有返回值) tcp_socket.connect(("192.168.36.235", 8080)) # 發送消息(返回發送的位元組數) tcp_socket.send("小張生日快樂~".encode("utf-8")) # 接收消息 msg = tcp_socket.recv(1024) print(f"伺服器:{msg.decode('utf-8')}") if __name__ == '__main__': main()
輸出:(socket()
預設就是創建TCP Socket
)
概括來說:
- TCP,有點像打電話,先撥號連通了(
connect
)才能通信(send
,recv
),之後的通信不用再撥號連通了 - UDP,有點像寄信封,每次寄過去都不確定能不能收到,每次通信都得寫地址(
ip
+port
)
代碼四步走:(TCP客戶端其實創建Socket
之後connect
一下伺服器就OK了)
- 創建:
tcp_sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- 連接:
tcp_sock.connect((IP, Port))
- 發送:
tcp_sock.send(Bytes內容)
接收:tcp_sock.recv(count)
- 關閉:
tcp_sock.close()
模擬HTTP¶
from socket import socket def get_buffer(tcp_socket): buffers = b'' while True: b = tcp_socket.recv(1024) if b: buffers += b else: break # 返回bytes return buffers def main(): with socket() as tcp_socket: # 連接伺服器 tcp_socket.connect(("dotnetcrazy.cnblogs.com", 80)) # 發送消息(模擬HTTP) tcp_socket.send( b'GET / HTTP/1.1\r\nHost: dotnetcrazy.cnblogs.com\r\nConnection: close\r\n\r\n' ) # 以"\r\n\r\n"分割一次 header, data = get_buffer(tcp_socket).split(b"\r\n\r\n", 1) print(header.decode("utf-8")) with open("test.html", "wb") as f: f.write(data) print("over") if __name__ == '__main__': main()
輸出:(test.html
就是頁面源碼)
HTTP/1.1 200 OK
Date: Thu, 01 Nov 2018 03:10:48 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 20059
Connection: close
Vary: Accept-Encoding
Cache-Control: private, max-age=10
Expires: Thu, 01 Nov 2018 03:10:58 GMT
Last-Modified: Thu, 01 Nov 2018 03:10:48 GMT
X-UA-Compatible: IE=10
X-Frame-Options: SAMEORIGIN
over
註意\r\n
和Connection:close
;split("",分割次數)
3.2.TCP服務端¶
服務端代碼相比於UDP,多了一個監聽和等待客戶端,其他基本上一樣:
客戶端Code:(如果你想固定埠也可以綁定一下Port
)
from socket import socket def main(): # 預設就是創建TCP Socket with socket() as tcp_socket: # 連接伺服器(沒有返回值) tcp_socket.connect(("192.168.36.235", 8080)) print("Connected TCP Server...") # 連接提示 # 發送消息(返回發送的位元組數) tcp_socket.send("小張生日快樂~\n".encode("utf-8")) # 接收消息 msg = tcp_socket.recv(1024) print(f"伺服器:{msg.decode('utf-8')}") if __name__ == '__main__': main()
服務端Code:
from socket import socket def main(): with socket() as tcp_socket: # 綁定埠(便於客戶端找到) tcp_socket.bind(('', 8080)) # 變成被動接收消息(監聽) tcp_socket.listen() # 不指定連接最大數則會設置預設值 print("TCP Server is Running...") # 運行後提示 # 等待客戶端發信息 client_socket, client_addr = tcp_socket.accept() with client_socket: # 客戶端連接提示 print(f"[來自{client_addr[0]}:{client_addr[1]}的消息]\n") # 接收客戶端消息 data = client_socket.recv(1024) print(data.decode("utf-8")) # 回覆客戶端 client_socket.send("知道了".encode("utf-8")) if __name__ == '__main__': main()
輸出:(先運行服務端,再運行客戶端。客戶端發了一個生日快樂的祝福,服務端回覆了一句)
3.2.TCP服務端調試助手¶
如果像上面那般,並不能多客戶端通信
這時候可以稍微改造一下:
客戶端:¶
from time import sleep from socket import socket from multiprocessing.dummy import Pool def send_msg(tcp_socket): with tcp_socket: while True: try: tcp_socket.send("小明同志\n".encode("utf-8")) sleep(2) # send是非阻塞的 print("向伺服器問候了一下") except Exception as ex: print("服務端連接已斷開:", ex) break def recv_msg(tcp_socket): with tcp_socket: while True: # 這邊可以不捕獲異常: # 服務端關閉時,send_msg會關閉,然後這邊也就關閉了 try: data = tcp_socket.recv(1024) if data: print("服務端回覆:", data.decode("utf-8")) except Exception as ex: print("tcp_socket已斷開:", ex) break def main(): with socket() as tcp_socket: # 連接TCP Server tcp_socket.connect(("192.168.36.235", 8080)) print("Connected TCP Server...") # 連接提示 pool = Pool() pool.apply_async(send_msg, args=(tcp_socket,)) pool.apply_async(recv_msg, args=(tcp_socket,)) pool.close() pool.join() if __name__ == '__main__': main()
服務端¶
伺服器需要同時響應多個客戶端的請求,那麼每個連接都需要一個新的進程或者線程來處理
from socket import