網絡編程概念
兩大開發架構
C/S B/S
ip地址——互聯網協議地址,為互聯網上的每一個網絡和每一台主機分配一個邏輯地址
以此來屏蔽物理地址的差異。它是一個32位的二進制數
mac地址
內網/外網
交換機:單播,廣播,組播
在一個局域網內的通信
二層/數據鏈路層 - osi七層協議
數據鏈路層的協議:arp協議(通過一台機器的ip地址找到mac地址)
數據鏈路層的網絡設備:交換機、網卡
路由器:
局域網與局域網之間的連接工具
三層/網絡層 - osi七層協議
網絡層的協議:ip協議
網絡層的設備:路由器、三層交換機
網關:
一個局域網要想訪問局域網外部的其他機器,都要通過統一的網關接口
子網掩碼
判斷兩台機器是否在同一個局域網內
端口
定位某一台機器上的某一個服務,可認為是設備與外界通訊交流的出口
如何定位你的網絡中能夠找到的唯一的一台機器+服務?
ip+服務端口號
ip地址精確到具體的一台電腦
端口精確到具體的程序
osi七層協議的傳輸層:tcp/udp協議
這兩個協議標志着我們的數據傳遞的方式都要基於網絡傳輸
tcp協議(好比是語音通話)
必須先接通電話,同一時刻只能和一個人聊天,傳輸比較穩定
可以發送任意長度的消息
udp協議(發微信)
不需要接通,只要互相知道ip,端口就可以通信
同時可以和多個人聊天,傳輸速度快,但是傳輸相對不穩定,不能傳輸過長的消息
tcp:發送文件(郵件)、下載安裝包、上傳下載電影、從網盤上傳、下載文件
udp: qq、微信、小視頻軟件等,即時通信類
tcp協議
為什么傳輸穩定、可靠
先建立連接,然后三次握手
1. 就好比qq給服務器發送一個值1200,問它,“我可以和你通話嗎?”
2. 這時服務器回答,“可以”,並且返回給qq一個值1201來表示接收到了
然后服務器要問qq,“我可以和你通話嗎?”
3. qq回復“可以”
這就是建立連接的三次握手。注意,服務器發送1201和問qq歸於一次握手
數據傳輸
1. qq給服務器發送一條消息
2. 服務器收到后給qq返回一條消息
這兩個過程合在一起才算一個完整的數據傳輸
斷線連接四次揮手
1. qq跟服務器說,“我不想和你說話了”
2. 服務器接收到后,說,“好的”
3. 服務器也說,“我也不想和你說話了”
4. qq回答“好的”
注意2和3不能一起發,因為qq說不想和服務器說話,但是服務器還要和qq說話
tcp協議與udp協議的流程圖:
socket層
所處位置:在應用層和下面所有層之間的位置
有了socket可以以相對簡單的方式進行網絡通信
本質上是幫助我們解決兩個程序之間通信

socket是應用層與tcp/ip協議通信的中間軟件抽象層,是一組接口
下面開始舉例加深對 socket 的理解 建立一個文件夾
比如“tcp協議的socket” 再在里面建立兩個文件,分別命名為“server.py”和“client.py”
tcp是基於鏈接的,必須先啟動服務端,然后再啟動客戶端去鏈接服務端
# server.py import socket # 套接字 sk = socket.socket() sk.bind(("127.0.0.1", 8080)) # 把地址綁定到套接字 # 元組的第一個元素是字符串形式的ip地址,必須是本電腦的。 # ip地址兩種 # 192.168.13.4 (在cmd里面輸入 ipconfig 即可查詢) # 還有一個 “127.0.0.1”,本地回環地址 # 這兩個ip地址不一樣 # 192.168.... 所有的和我在同一個局域網的小伙伴都能訪問 # 127.0.0.1 只有在自己電腦的client才能用 # 第二個元素是端口號,一般在8000-10000之間 # 三次握手 sk.listen() # 監聽鏈接 conn, addr = sk.accept() # 阻塞,直到有一個客戶端來連接 # print(addr) conn.send(b"hello") msg = conn.recv(1024) # 最多接收1024個字節,沒1024時有多少接收多少 print(msg) # 四次揮手 conn.close() # 關閉客戶端套接字 sk.close() # 關閉服務器套接字
# client.py import socket sk = socket.socket() # 創建客戶端套接字 sk.connect(("127.0.0.1", 8080)) # 把三次握手建立起來了,嘗試連接服務器 # 這里connect一次,上面(server.py)就accept一次 msg = sk.recv(1024) # 對話(發送/接收) print(msg) sk.send(b"Goodgoodstudy,Daydayup!") sk.close() # 關閉客戶端套接字
# 然后先運行 server.py, 再運行 client.py,兩個都會有結果
# 注意收發次數要相等,還要一一對應
輸入中文需要編碼解碼
這里涉及到編碼問題
str 字符串數據類型
bytes 字節數據類型
中:gbk bytes: 100100110101
中:uft-8 bytes: 100111100001
send——一定是bytes類型
bytes = recv() 也是bytes類型
我們看,即運行結果要顯示的話,是看str的
我們在發送數據之前是str, 發送的是bytes
就需要對str進行編碼,str.encode("utf-8"),結果是一個bytes
我們在接收數據的時候,收到的也是bytes
要想看懂必須把bytes解碼,bytes.decode("utf-8"),這樣結果就變成字符串了
用一張圖來顯示:
# 服務端輸入中文時編碼,客戶端那邊要解碼,反之亦反 # server.py import socket # 套接字 sk = socket.socket() sk.bind(("127.0.0.1", 8080)) sk.listen() conn, addr = sk.accept() # 給客戶端發送中文時需編碼 conn.send("你好".encode()) # 注意在send里面直接編碼發送出去 recv_msg = conn.recv(1024) decode_recv_msg = recv_msg.decode("utf-8") # 接收客戶端發來的反饋,要解碼 print(decode_recv_msg) conn.close() sk.close()
# client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 8080)) # 接收服務端發來的信息 msg = sk.recv(1024) decode_msg = msg.decode() # 接收上面服務端發來的消息,要解碼 print(decode_msg) # 給服務端發送信息 sk.send("好好學習~".encode()) # 發送給服務端的消息,send里面編碼 sk.close()
帶退出的聊天程序
# server.py import socket sk = socket.socket() sk.bind(("127.0.0.1", 8800)) sk.listen() conn, addr = sk.accept()
# 注意在 accept之后加上循環
# 是為了能夠讓我們和一個客戶端多說幾句話 while 1: send_msg = input("msg: ") # q conn.send(send_msg.encode()) # send(q) if send_msg == "q": break msg = conn.recv(1024).decode() if msg == "q": break print(msg) conn.close() sk.close()
# client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 8800)) while 1: msg = sk.recv(1024).decode() if msg == "q": break print(msg) send_msg = input("msg: ") sk.send(send_msg.encode()) if msg == "q": break sk.close()
小練習
所有的client端都要以server端的時間為基准
client端發送一個時間格式——"%Y-%m-%d %H:%M:%S"
server端根據接收到的時間格式向客戶端返回時間
# server.py import socket # 套接字 import time sk = socket.socket() sk.bind(("127.0.0.1", 9000)) sk.listen() conn, addr = sk.accept() msg = conn.recv(1024) print(msg) format_msg = time.strftime(msg.decode()) conn.send(format_msg.encode()) conn.close() sk.close()
# client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 9000)) sk.send(b"%Y-%m-%d %H:%M:%S") msg = sk.recv(1024).decode() print(msg) sk.close()
讓server同時接收多個client請求的方法
# server.py import socket # 套接字 import time sk = socket.socket() sk.bind(("127.0.0.1", 9000)) sk.listen() # 在 accept之前加上循環,能夠讓我們和多個客戶端進行溝通 # 注意與上面循環的區別 while 1: conn, addr = sk.accept() # 接收一個客戶端的請求 print(addr) msg = conn.recv(1024) # 和這個接通的客戶端進行通信 fmt = time.strftime(msg.decode()) print(msg) conn.send(fmt.encode()) conn.close() # 斷開一個客戶端的請求 sk.close() # client.py內容不變,先運行本文件 # 然后一直重復運行client.py文件 # 運行結果會刷新,不會報錯
# client.py import socket sk = socket.socket() sk.connect(("127.0.0.1", 9000)) sk.send(b"%Y-%m-%d %H:%M:%S") msg = sk.recv(1024).decode() print(msg) sk.close()
粘包
粘包現象的本質
接收端不知道發送端給它發送了多長的數據
注意:只有tcp有粘包現象,udp永遠不會粘包
但是udp會丟失數據,不可靠
對於空消息:tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住
而udp是基於數據報的,即便是你輸入的是空內容(直接回車),也可以被發送,udp協議會幫你封裝上消息頭發送過去。
# 了解粘包之前,先了解 struct 模塊的用法 import struct # pack:固定4個字節 ret = struct.pack("i", 5000000) print(ret, len(ret)) ret1 = struct.pack("i", 123) print(ret1, len(ret1)) ret2 = struct.pack("i", 902730757) # 不能超過2**32個字節 print(ret2, len(ret2)) # b'@KL\x00' 4 # b'{\x00\x00\x00' 4 # b'\x05\x94\xce5' 4 # unpack: res = struct.unpack("i", ret) print(res[0]) res1 = struct.unpack("i", ret1) print(res1[0]) res2 = struct.unpack("i", ret2) print(res2[0]) # 5000000 # 123 # 902730757
# server.py import socket sk = socket.socket() sk.bind(("127.0.0.1", 9001)) sk.listen() conn,addr = sk.accept() conn.send(b"hello") # 粘包現象 conn.send(b"word") conn.close() sk.close()
# 設置時間延遲來觀察粘包現象
import socket import time sk = socket.socket() sk.connect(("127.0.0.1", 9001)) time.sleep(0.1) msg = sk.recv(1024) print(msg) msg2 = sk.recv(1024) print(msg2) # b'helloword' # b''
粘包現象
# 1.發送端的粘包 合包機制 + 緩存區
發送方引起的粘包是由TCP協議本身造成的
TCP為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一個TCP段
若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段
然后一次發送出去,這樣接收方就收到了粘包數據
也就是說,發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據又很小,會合到一起,產生粘包)
# 2.接收端的粘包 延遲接受 + 緩存區
接收方不及時接收緩沖區的包,造成多個包接收
客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包
# 3.流式傳輸 # 電流 高低電壓 # 所以我們說 tcp協議是無邊界的流式傳輸 # 4.拆包機制
總結
1.從表面上看,黏包問題主要是因為發送方和接收方的緩存機制、tcp協議面向流通信的特點
2.實際上,主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的
# 可以通過 struct 模塊來解決粘包問題
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度
解決辦法是,可以借助一個模塊,這個模塊可以把要發送的數據長度轉換成固定長度的字節
這樣客戶端每次接收消息之前只要先接受這個固定長度字節的內容看一看接下來要接收的信息大小
那么最終接受的數據只要達到這個值就停止,就能剛好不多不少的接收完整的數據了
發送時 | 接收時 |
先發送struct轉換好的數據長度4字節 | 先接受4個字節使用struct轉換成數字來獲取要接收的數據長度 |
再發送數據 | 再按照長度接收數據 |
# server.py import struct import socket sk = socket.socket() sk.bind(('127.0.0.1',9000)) sk.listen() conn,addr = sk.accept() send_msg = input('>>>').encode() bytes_len = struct.pack('i',len(send_msg)) # 注意這里是用len()來代表字節長度 conn.send(bytes_len) conn.send(send_msg) conn.send(b'world') conn.close() sk.close()
# client.py import struct import socket sk = socket.socket() sk.connect(('127.0.0.1',9000)) bytes_len = sk.recv(4) # 這里接收到的是一個元組,因此要設置索引來得到想要的結果 msg_len = struct.unpack('i',bytes_len)[0] msg = sk.recv(msg_len) print(msg.decode()) msg2 = sk.recv(5) print(msg2) sk.close()