1. OSI 7層模型 OSI的7層模型對於大家來說可能不太好理解,所以我們通過一個案例來講解: 假設,你在瀏覽器上輸入了一些關鍵字,內部通過DNS找到對應的IP後,再發送數據時內部會做如下的事: 應用層:規定數據的格式。 "GET /s?wd=你好 HTTP/1.1\r\nHost:www.bai ...
1. OSI 7層模型
OSI的7層模型對於大家來說可能不太好理解,所以我們通過一個案例來講解:
假設,你在瀏覽器上輸入了一些關鍵字,內部通過DNS找到對應的IP後,再發送數據時內部會做如下的事:
-
應用層:規定數據的格式。
"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n"
-
表示層:對應用層數據的編碼、壓縮(解壓縮)、分塊、加密(解密)等任務。
"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8')
-
會話層:負責與目標建立、中斷連接。
在發送數據之前,需要會先發送 “連接” 的請求,與遠程建立連接後,再發送數據。當然,發送完畢之後,也涉及中斷連接的操作。
-
傳輸層:建立埠到埠的通信,其實就確定雙方的埠信息。
數據:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8') 埠: - 目標:80 - 本地:6784
-
網路層:標記目標IP信息(IP協議層)
數據:"GET /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8') 埠: - 目標:80 - 本地:6784 IP: - 目標IP:110.242.68.3(百度) - 本地IP:192.168.10.1
-
數據鏈路層:對數據進行分組並設置源和目標mac地址
數據:"POST /s?wd=你好 HTTP/1.1\r\nHost:www.baidu.com\r\n\r\n你好".encode('utf-8') 埠: - 目標:80 - 本地:6784 IP: - 目標IP:110.242.68.3(百度) - 本地IP:192.168.10.1 MAC: - 目標MAC:FF-FF-FF-FF-FF-FF - 本機MAC:11-9d-d8-1a-dd-cd
-
物理層:將二進位數據在物理媒體上傳輸。
通過網線將二進位數據發送出去
每一層各司其職,最終保證數據呈現在到用戶手中。
簡單的可以理解為發快遞:將數據外面套了7個箱子,最終用戶收到箱子時需要打開7個箱子才能拿到數據。而在運輸的過程中有些箱子是會被拆開並替換的,例如:
最終運送目標:上海 ~ 北京(中途可能需要中轉站),在中轉站會會打開箱子查看信息,在進行轉發。
- 對於二級中轉站(二層交換機):拆開數據鏈路層的箱子,查看mac地址信息。
- 對於三級中轉站(路由器或三層交換機):拆開網路層的箱子,查看IP信息。
在開發過程中其實只能體現:應用層、表示層、會話層、傳輸層,其他層的處理都是在網路設備中自動完成的。
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('110.242.68.3', 80)) # 向服務端發送了數據包
key = "你好"
# 應用層
content = "GET /s?wd={} http1.1\r\nHost:www.baidu.com\r\n\r\n".format(key)
# 表示層
content = content.encode("utf-8")
client.sendall(content)
result = client.recv(8196)
print(result.decode('utf-8'))
# 會話層 & 傳輸層
client.close()
2. UDP和TCP協議
協議,其實就是規定 連接、收發數據的一些規定。
在OSI的 傳輸層 除了定義埠信息以外,常見的還可以指定UDP或TCP的協議,協議不同連接和傳輸數據的細節也會不同。
-
UDP(User Data Protocol)用戶數據報協議, 是⼀個⽆連接的簡單的⾯向數據報的傳輸層協議。 UDP不提供可靠性, 它只是把應⽤程式傳給IP層的數據報發送出去, 但是並不能保證它們能到達⽬的地。 由於UDP在傳輸數據報前不⽤在客戶和伺服器之間建⽴⼀個連接, 且沒有超時重發等機制, 故⽽傳輸速度很快。
常見的有:語音通話、視頻通話、實時游戲畫面 等。
-
TCP(Transmission Control Protocol,傳輸控制協議)是面向連接的協議,也就是說,在收發數據前,必須和對方建立可靠的連接,然後再進行收發數據。
常見有:網站、手機APP數據獲取等。
2.1 UDP和TCP 示例代碼
UDP示例如下:
-
服務端
import socket server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server.bind(('127.0.0.1', 8002)) while True: data, (host, port) = server.recvfrom(1024) # 阻塞 print(data, host, port) server.sendto("好的".encode('utf-8'), (host, port))
-
客戶端
import socket client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) while True: text = input("請輸入要發送的內容:") if text.upper() == 'Q': break client.sendto(text.encode('utf-8'), ('127.0.0.1', 8002)) data, (host, port) = client.recvfrom(1024) print(data.decode('utf-8')) client.close()
TCP示例如下:
-
服務端
import socket # 1.監聽本機的IP和埠 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) while True: # 2.等待,有人來連接(阻塞) conn, addr = sock.accept() # 3.等待,連接者發送消息(阻塞) client_data = conn.recv(1024) print(client_data) # 4.給連接者回覆消息 conn.sendall(b"hello world") # 5.關閉連接 conn.close() # 6.停止服務端程式 sock.close()
-
客戶端
import socket # 1. 向指定IP發送連接請求 client = socket.socket() client.connect(('127.0.0.1', 8001)) # 2. 連接成功之後,發送消息 client.sendall(b'hello') # 3. 等待,消息的回覆(阻塞) reply = client.recv(1024) print(reply) # 4. 關閉連接 client.close()
2.2 TCP三次握手和四次揮手
這是一個常見的面試題。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
網路中的雙方想要基於TCP連接進行通信,必須要經過:
-
創建連接,客戶端和服務端要進行三次握手。
# 服務端 import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) while True: conn, addr = sock.accept() # 等待客戶端連接 ...
# 客戶端 import socket client = socket.socket() client.connect(('127.0.0.1', 8001)) # 發起連接
客戶端 服務端 1. SYN-SENT --> <seq=100><CTL=SYN> --> SYN-RECEIVED 2. ESTABLISHED <-- <seq=300><ack=101><CTL=SYN,ACK> <-- SYN-RECEIVED 3. ESTABLISHED --> <seq=101><ack=301><CTL=ACK> --> ESTABLISHED At this point, both the client and server have received an acknowledgment of the connection. The steps 1, 2 establish the connection parameter (sequence number) for one direction and it is acknowledged. The steps 2, 3 establish the connection parameter (sequence number) for the other direction and it is acknowledged. With these, a full-duplex communication is established.
-
傳輸數據
在收發數據的過程中,只有有數據的傳送就會有應答(ack),如果沒有ack,那麼內部會嘗試重覆發送。
-
關閉連接,客戶端和服務端要進行4次揮手。
import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) while True: conn, addr = sock.accept() ... conn.close() # 關閉連接 sock.close()
import socket client = socket.socket() client.connect(('127.0.0.1', 8001)) ... client.close() # 關閉連接
TCP A TCP B 1. FIN-WAIT-1 --> <seq=100><ack=300><CTL=FIN,ACK> --> CLOSE-WAIT 2. FIN-WAIT-2 <-- <seq=300><ack=101><CTL=ACK> <-- CLOSE-WAIT 3. TIME-WAIT <-- <seq=300><ack=101><CTL=FIN,ACK> <-- LAST-ACK 4. TIME-WAIT --> <seq=101><ack=301><CTL=ACK> --> CLOSED
3. 粘包
兩臺電腦在進行收發數據時,其實不是直接將數據傳輸給對方。
- 對於發送者,執行
sendall/send
發送消息時,是將數據先發送至自己網卡的 寫緩衝區 ,再由緩衝區將數據發送給到對方網卡的讀緩衝區。 - 對於接受者,執行
recv
接收消息時,是從自己網卡的讀緩衝區獲取數據。
所以,如果發送者連續快速的發送了2條信息,接收者在讀取時會認為這是1條信息,即:2個數據包粘在了一起。例如:
# socket客戶端(發送者)
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8001))
client.sendall('alex正在吃'.encode('utf-8'))
client.sendall('翔'.encode('utf-8'))
client.close()
# socket服務端(接收者)
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
conn, addr = sock.accept()
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))
conn.close()
sock.close()
如何解決粘包的問題?
每次發送的消息時,都將消息劃分為 頭部(固定位元組長度) 和 數據 兩部分。例如:頭部,用4個位元組表示後面數據的長度。
- 發送數據,先發送數據的長度,再發送數據(或拼接起來再發送)。
- 接收數據,先讀4個位元組就可以知道自己這個數據包中的數據長度,再根據長度讀取到數據。
對於頭部需要一個數字並固定為4個位元組,這個功能可以藉助python的struct包來實現:
import struct # ########### 數值轉換為固定4個位元組,四個位元組的範圍 -2147483648 <= number <= 2147483647 ########### v1 = struct.pack('i', 199) print(v1) # b'\xc7\x00\x00\x00' for item in v1: print(item, bin(item)) # ########### 4個位元組轉換為數字 ########### v2 = struct.unpack('i', v1) # v1= b'\xc7\x00\x00\x00' print(v2) # (199,)
示例代碼:
服務端
import socket import struct sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('127.0.0.1', 8001)) sock.listen(5) conn, addr = sock.accept() # 固定讀取4位元組 header1 = conn.recv(4) data_length1 = struct.unpack('i', header1)[0] # 數據位元組長度 21 has_recv_len = 0 data1 = b"" while True: length = data_length1 - has_recv_len if length > 1024: lth = 1024 else: lth = length chunk = conn.recv(lth) # 可能一次收不完,自己可以計算長度再次使用recv收取,直到收完為止。 1024*8 = 8196 data1 += chunk has_recv_len += len(chunk) if has_recv_len == data_length1: break print(data1.decode('utf-8')) # 固定讀取4位元組 header2 = conn.recv(4) data_length2 = struct.unpack('i', header2)[0] # 數據位元組長度 data2 = conn.recv(data_length2) # 長度 print(data2.decode('utf-8')) conn.close() sock.close()
客戶端
import socket import struct client = socket.socket() client.connect(('127.0.0.1', 8001)) # 第一條數據 data1 = 'alex正在吃'.encode('utf-8') header1 = struct.pack('i', len(data1)) client.sendall(header1) client.sendall(data1) # 第二條數據 data2 = '翔'.encode('utf-8') header2 = struct.pack('i', len(data2)) client.sendall(header2) client.sendall(data2) client.close()
案例:消息 & 文件上傳
-
服務端
import os import json import socket import struct def recv_data(conn, chunk_size=1024): # 獲取頭部信息:數據長度 has_read_size = 0 bytes_list = [] while has_read_size < 4: chunk = conn.recv(4 - has_read_size) has_read_size += len(chunk) bytes_list.append(chunk) header = b"".join(bytes_list) data_length = struct.unpack('i', header)[0] # 獲取數據 data_list = [] has_read_data_size = 0 while has_read_data_size < data_length: size = chunk_size if (data_length - has_read_data_size) > chunk_size else data_length - has_read_data_size chunk = conn.recv(size) data_list.append(chunk) has_read_data_size += len(chunk) data = b"".join(data_list) return data def recv_file(conn, save_file_name, chunk_size=1024): save_file_path = os.path.join('files', save_file_name) # 獲取頭部信息:數據長度 has_read_size = 0 bytes_list = [] while has_read_size < 4: chunk = conn.recv(4 - has_read_size) bytes_list.append(chunk) has_read_size += len(chunk) header = b"".join(bytes_list) data_length = struct.unpack('i', header)[0] # 獲取數據 file_object = open(save_file_path, mode='wb') has_read_data_size = 0 while has_read_data_size < data_length: size = chunk_size if (data_length - has_read_data_size) > chunk_size else data_length - has_read_data_size chunk = conn.recv(size) file_object.write(chunk) file_object.flush() has_read_data_size += len(chunk) file_object.close() def run(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # IP可復用 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8001)) sock.listen(5) while True: conn, addr = sock.accept() while True: # 獲取消息類型 message_type = recv_data(conn).decode('utf-8') if message_type == 'close': # 四次揮手,空內容。 print("關閉連接") break # 文件:{'msg_type':'file', 'file_name':"xxxx.xx" } # 消息:{'msg_type':'msg'} message_type_info = json.loads(message_type) if message_type_info['msg_type'] == 'msg': data = recv_data(conn) print("接收到消息:", data.decode('utf-8')) else: file_name = message_type_info['file_name'] print("接收到文件,要保存到:", file_name) recv_file(conn, file_name) conn.close() sock.close() if __name__ == '__main__': run()
-
客戶端
import os import json import socket import struct def send_data(conn, content): data = content.encode('utf-8') header = struct.pack('i', len(data)) conn.sendall(header) conn.sendall(data) def send_file(conn, file_path): file_size = os.stat(file_path).st_size header = struct.pack('i', file_size) conn.sendall(header) has_send_size = 0 file_object = open(file_path, mode='rb') while has_send_size < file_size: chunk = file_object.read(2048) conn.sendall(chunk) has_send_size += len(chunk) file_object.close() def run(): client = socket.socket() client.connect(('127.0.0.1', 8001)) while True: """ 請發送消息,格式為: - 消息:msg|你好呀 - 文件:file|xxxx.png """ content = input(">>>") # msg or file if content.upper() == 'Q': send_data(client, "close") break input_text_list = content.split('|') if len(input_text_list) != 2: print("格式錯誤,請重新輸入") continue message_type, info = input_text_list # 發消息 if message_type == 'msg': # 發消息類型 send_data(client, json.dumps({"msg_type": "msg"})) # 發內容 send_data(client, info) # 發文件 else: file_name = info.rsplit(os.sep, maxsplit=1)[-1] # 發消息類型 send_data(client, json.dumps({"msg_type": "file", 'file_name': file_name})) # 發內容 send_file(client, info) client.close() if __name__ == '__main__': run()
4. 阻塞和非阻塞
預設情況下我們編寫的網路編程的代碼都是阻塞的(等待),阻塞主要體現在:
# ################### socket服務端(接收者)###################
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
# 阻塞
conn, addr = sock.accept()
# 阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))
conn.close()
sock.close()
# ################### socket客戶端(發送者) ###################
import socket
client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))
client.sendall('alex正在吃翔'.encode('utf-8'))
client.close()
如果想要讓代碼變為非阻塞,需要這樣寫:
# ################### socket服務端(接收者)###################
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False) # 加上就變為了非阻塞
sock.bind(('127.0.0.1', 8001))
sock.listen(5)
# 非阻塞
conn, addr = sock.accept()
# 非阻塞
client_data = conn.recv(1024)
print(client_data.decode('utf-8'))
conn.close()
sock.close()
# ################### socket客戶端(發送者) ###################
import socket
client = socket.socket()
client.setblocking(False) # 加上就變為了非阻塞
# 非阻塞
client.connect(('127.0.0.1', 8001))
client.sendall('alex正在吃翔'.encode('utf-8'))
client.close()
如果代碼變成了非阻塞,程式運行時一旦遇到 accept
、recv
、connect
就會拋出 BlockingIOError 的異常。
這不是代碼編寫的有錯誤,而是原來的IO阻塞變為非阻塞之後,由於沒有接收到相關的IO請求拋出的固定錯誤。
非阻塞的代碼一般與IO多路復用結合,可以迸發出更大的作用。
5. IO多路復用
I/O多路復用指:通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。
IO多路復用 + 非阻塞,可以實現讓TCP的服務端同時處理多個客戶端的請求,例如:
# ################### socket服務端 ###################
import select
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False) # 加上就變為了非阻塞
server.bind(('127.0.0.1', 8001))
server.listen(5)
inputs = [server, ] # socket對象列表 -> [server, 第一個客戶端連接conn ]
while True:
# 當 參數1 序列中的socket對象發生可讀時(accetp和read),則獲取發生變化的對象並添加到 r列表中。
# r = []
# r = [server,]
# r = [第一個客戶端連接conn,]
# r = [server,]
# r = [第一個客戶端連接conn,第二個客戶端連接conn]
# r = [第二個客戶端連接conn,]
r, w, e = select.select(inputs, [], [], 0.05)
for sock in r:
# server
if sock == server:
conn, addr = sock.accept() # 接收新連接。
print("有新連接")
# conn.sendall()
# conn.recv("xx")
inputs.append(conn)
else:
data = sock.recv(1024)
if data:
print("收到消息:", data.decode("utf-8"))
else:
print("關閉連接")
inputs.remove(sock)
# 乾點其他事 20s
"""
優點:
1. 乾點其他的事。
2. 讓服務端支持多個客戶端同時來連接。
"""
# ################### socket客戶端 ###################
import socket
client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))
while True:
content = input(">>>")
if content.upper() == 'Q':
break
client.sendall(content.encode('utf-8'))
client.close()
# ################### socket客戶端 ###################
import socket
client = socket.socket()
# 阻塞
client.connect(('127.0.0.1', 8001))
while True:
content = input(">>>")
if content.upper() == 'Q':
break
client.sendall(content.encode('utf-8'))
client.close() # 與服務端斷開連接(四次揮手),預設會向服務端發送空數據。
IO多路復用 + 非阻塞,可以實現讓TCP的客戶端同時發送多個請求,例如:去某個網站發送下載圖片的請求。
import socket
import select
import uuid
import os
client_list = [] # socket對象列表
for i in range(5):
client = socket.socket()
client.setblocking(False)
try:
# 連接百度,雖然有異常BlockingIOError,但向還是正常發送連接的請求
client.connect(('47.98.134.86', 80))
except BlockingIOError as e:
pass
client_list.append(client)
recv_list = [] # 放已連接成功,且已經把下載圖片的請求發過去的socket
while True:
# w = [第一個socket對象,]
# r = [socket對象,]
r, w, e = select.select(recv_list, client_list, [], 0.1)
for sock in w:
# 連接成功,發送數據
# 下載圖片的請求
sock.sendall(b"GET /nginx-logo.png HTTP/1.1\r\nHost:47.98.134.86\r\n\r\n")
recv_list.append(sock)
client_list.remove(sock)
for sock in r:
# 數據發送成功後,接收的返回值(圖片)並寫入到本地文件中
data = sock.recv(8196)
content = data.split(b'\r\n\r\n')[-1]
random_file_name = "{}.png".format(str(uuid.uuid4()))
with open(os.path.join("images", random_file_name), mode='wb') as f:
f.write(content)
recv_list.remove(sock)
if not recv_list and not client_list:
break
"""
優點:
1. 可以偽造出併發的現象。
"""
基於 IO多路復用 + 非阻塞的特性,無論編寫socket的服務端和客戶端都可以提升性能。其中
- IO多路復用,監測socket對象是否有變化(是否連接成功?是否有數據到來等)。
- 非阻塞,socket的connect、recv過程不再等待。
註意:IO多路復用只能用來監聽 IO對象 是否發生變化,常見的有:文件是否可讀寫、電腦終端設備輸入和輸出、網路請求(常見)。
在Linux操作系統化中 IO多路復用 有三種模式,分別是:select,poll,epoll。(windows 只支持select模式)
監測socket對象是否新連接到來 or 新數據到來。
select
select最早於1983年出現在4.2BSD中,它通過一個select()系統調用來監視多個文件描述符的數組,當select()返回後,該數組中就緒的文件描述符便會被內核修改標誌位,使得進程可以獲得這些文件描述符從而進行後續的讀寫操作。
select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點,事實上從現在看來,這也是它所剩不多的優點之一。
select的一個缺點在於單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般為1024,不過可以通過修改巨集定義甚至重新編譯內核的方式提升這一限制。
另外,select()所維護的存儲大量文件描述符的數據結構,隨著文件描述符數量的增大,其複製的開銷也線性增長。同時,由於網路響應時間的延遲使得大量TCP連接處於非活躍狀態,但調用select()會對所有socket進行一次線性掃描,所以這也浪費了一定的開銷。
poll
poll在1986年誕生於System V Release 3,它和select在本質上沒有多大差別,但是poll沒有最大文件描述符數量的限制。
poll和select同樣存在一個缺點就是,包含大量文件描述符的數組被整體複製於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數量的增加而線性增大。
另外,select()和poll()將就緒的文件描述符告訴進程後,如果進程沒有對其進行IO操作,那麼下次調用select()和poll()的時候將再次報告這些文件描述符,所以它們一般不會丟失就緒的消息,這種方式稱為水平觸發(Level Triggered)。
epoll
直到Linux2.6才出現了由內核直接支持的實現方法,那就是epoll,它幾乎具備了之前所說的一切優點,被公認為Linux2.6下性能最好的多路I/O就緒通知方法。
epoll可以同時支持水平觸發和邊緣觸發(Edge Triggered,只告訴進程哪些文件描述符剛剛變為就緒狀態,它只說一遍,如果我們沒有採取行動,那麼它將不會再次告知,這種方式稱為邊緣觸發),理論上邊緣觸發的性能要更高一些,但是代碼實現相當複雜。
epoll同樣只告知那些就緒的文件描述符,而且當我們調用epoll_wait()獲得就緒文件描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的文件描述符即可,這裡也使用了記憶體映射(mmap)技術,這樣便徹底省掉了這些文件描述符在系統調用時複製的開銷。
另一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用一定的方法後,內核才對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。
補充:socket + 非阻塞+ IO多路復用(IO操作對象都可以監測 + 文件)。
總結
-
OSI 7層模型
應用層、表示層、會話層、傳輸層、網路層、數據鏈路層、物理層。
-
UDP和TCP的區別
UDP,速度快但無法保證數據的準確性。 TCP,需要先創建可靠連接,在進行收發數據(ack)。
-
TCP的三次握手和四次揮手
-
為什麼會有粘包?如何解決?
-
如何讓socket請求變成非阻塞?
-
IO多路復用的作用?
監測多個 IO對象 是否發生變化(可讀/可寫)。
- IO多路復用 + 非阻塞 + socket服務端,可以讓服務端同時處理多個客戶端的請求。
- IO多路復用 + 非阻塞 + socket客戶端,可以向服務端同時發起多個請求。
作業(模塊大作業)
請基於TCP協議實現一個網盤系統,包含客戶端、服務端,各自需求如下:
-
客戶端
-
用戶註冊,註冊成功之後,在服務端的指定目錄下為此用戶創建一個文件夾,該文件夾下以後存儲當前用戶的數據(類似於網盤)。
-
用戶登錄
-
查看網盤目錄下的所有文件(一級即可),ls命令
-
上傳文件,如果網盤已存在則重新上傳(覆蓋)。
-
下載文件(進度條)
先判斷要下載本地路徑中是否存在該文件。 - 不存在,直接下載 - 存在,則讓用戶選擇是否續傳(繼續下載)。 - 續傳,在上次的基礎上繼續下載。 - 不續傳,從頭開始下載。
-
-
服務端
-
支持註冊,併為用戶初始化相關目錄。
註冊成功之後,將所有用戶信息存儲到特定的Excel文件中
-
支持登錄
-
支持查看當前用戶網盤目錄下的所有文件。
-
支持上傳
-
支持下載
-
參考代碼見:https://files.cnblogs.com/files/blogs/814578/網盤系統(參考代碼).zip?t=1713753389&download=true