day18-網路編程(下)

来源:https://www.cnblogs.com/sbhglqy/p/18150171
-Advertisement-
Play Games

1. OSI 7層模型 OSI的7層模型對於大家來說可能不太好理解,所以我們通過一個案例來講解: 假設,你在瀏覽器上輸入了一些關鍵字,內部通過DNS找到對應的IP後,再發送數據時內部會做如下的事: 應用層:規定數據的格式。 "GET /s?wd=你好 HTTP/1.1\r\nHost:www.bai ...


1. OSI 7層模型

image

image

OSI的7層模型對於大家來說可能不太好理解,所以我們通過一個案例來講解:
image

假設,你在瀏覽器上輸入了一些關鍵字,內部通過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
    
  • 物理層:將二進位數據在物理媒體上傳輸。

    通過網線將二進位數據發送出去
    

image

每一層各司其職,最終保證數據呈現在到用戶手中。

簡單的可以理解為發快遞:將數據外面套了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三次握手和四次揮手

這是一個常見的面試題。
image

image

    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. 粘包

image

image

兩臺電腦在進行收發數據時,其實不是直接將數據傳輸給對方。

  • 對於發送者,執行 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,)

image

示例代碼:

  • 服務端

    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()

image

如果代碼變成了非阻塞,程式運行時一旦遇到 acceptrecvconnect 就會拋出 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操作對象都可以監測 + 文件)。

總結

  1. OSI 7層模型

    應用層、表示層、會話層、傳輸層、網路層、數據鏈路層、物理層。
    
  2. UDP和TCP的區別

    UDP,速度快但無法保證數據的準確性。
    TCP,需要先創建可靠連接,在進行收發數據(ack)。
    
  3. TCP的三次握手和四次揮手

  4. 為什麼會有粘包?如何解決?

  5. 如何讓socket請求變成非阻塞?

  6. IO多路復用的作用?

    監測多個 IO對象 是否發生變化(可讀/可寫)。
    
    • IO多路復用 + 非阻塞 + socket服務端,可以讓服務端同時處理多個客戶端的請求。
    • IO多路復用 + 非阻塞 + socket客戶端,可以向服務端同時發起多個請求。

作業(模塊大作業)

請基於TCP協議實現一個網盤系統,包含客戶端、服務端,各自需求如下:

  • 客戶端

    • 用戶註冊,註冊成功之後,在服務端的指定目錄下為此用戶創建一個文件夾,該文件夾下以後存儲當前用戶的數據(類似於網盤)。

    • 用戶登錄

    • 查看網盤目錄下的所有文件(一級即可),ls命令

    • 上傳文件,如果網盤已存在則重新上傳(覆蓋)。

    • 下載文件(進度條)

      先判斷要下載本地路徑中是否存在該文件。
      - 不存在,直接下載
      - 存在,則讓用戶選擇是否續傳(繼續下載)。
      	- 續傳,在上次的基礎上繼續下載。
      	- 不續傳,從頭開始下載。
      
  • 服務端

    • 支持註冊,併為用戶初始化相關目錄。

      註冊成功之後,將所有用戶信息存儲到特定的Excel文件中
      

      image

    • 支持登錄

    • 支持查看當前用戶網盤目錄下的所有文件。

    • 支持上傳

    • 支持下載

參考代碼見:https://files.cnblogs.com/files/blogs/814578/網盤系統(參考代碼).zip?t=1713753389&download=true


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 發佈訂閱模式是怎樣的? 現在市面上流行的很多消息中間件就是採用的該種模式,這種模式 在實際業務中 將 事件發佈者(Publisher) 與 事件訂閱者 (Subscriber)通過額外的事件通道(Event Channel)來解耦,其基本原理與先前提到的觀察者模式有些許類似,但發佈訂閱模式額外存在了 ...
  • 1 不具備記憶能力的 它是零狀態的,我們平常在使用一些大模型產品,尤其在使用他們的API的時候,我們會發現那你和它對話,尤其是多輪對話的時候,經過一些輪次後,這些記憶就消失了,因為它也記不住那麼多。 2 上下文視窗的限制 大模型對其input和output,也就是它的輸入輸出有數量限制。為了保護它的 ...
  • C++ 構造函數 構造函數是 C++ 中一種特殊的成員函數,當創建類對象時自動調用。它用於初始化對象的狀態,例如為屬性分配初始值。構造函數與類同名,且沒有返回值類型。 構造函數類型 C++ 支持多種類型的構造函數,用於滿足不同的初始化需求: 預設構造函數: 不帶參數的構造函數,通常用於初始化對象的默 ...
  • 單向順序鏈表的創建,增,刪,減,查 /******************************************************************* * * file name: 單向順序鏈表的創建,增,刪,減,查 * author : [email protected] ...
  • 作者:青石路 來源:https://www.cnblogs.com/youzhibing/p/18019399 MyBatis 替換成 MyBatis-Plus 背景介紹 一個老項目,資料庫用的是 MySQL 5.7.36 , ORM 框架用的 MyBatis 3.5.0 , mysql-conne ...
  • C++對象在經過類的封裝後,存取對象中的數據成員的效率是否相比C語言的結構體訪問效率要低下?本篇將從C++類的不同定義形式來一一分析C++對象的數據成員的訪問在編譯器中是如何實現的,以及它們的存取效率如何? ...
  • 只要是 web 項目,程式都會直接或間接使用到線程池,它的使用是如此頻繁,以至於像空氣一樣,大多數時候被我們無視了。但有時候,我們會相當然地認為線程池與其它對象池(如:資料庫連接池)一樣,要用的時候向池子索取,用完後歸還給它即可。然後事實上,線程池獨樹一幟、鶴立雞群,它與普通的對象池就是不同。本文本 ...
  • 本文介紹基於Python語言中ArcPy模塊,實現ArcMap自動批量出圖,並對地圖要素進行自定義批量設置的方法。 1 任務需求 首先,我們來明確一下本文所需實現的需求。 現有通過Python基於Excel數據加以反距離加權空間插值並掩膜圖層所繪製的北京市在2019年05月18日00時至23時(其中 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...