Python基礎之網絡併發編程

目錄

  • Python基礎之網絡併發編程
  • 一、黏包現象
  • 1、什麼是黏包
  • 2、產生黏包的原因
  • 3、如何避免黏包
  • 二、struct模塊
  • 1、模塊簡介
  • 2、常用方法
  • 3、常見格式
  • 三、黏包問題實戰
  • 1、解決思路
  • 2、代碼實戰
  • 四、UDP協議
  • 1、UDP協議特點
  • 2、代碼實現

一、黏包現象

1、什麼是黏包

黏包是指,當我們基於TCP協議,客户端可服務端進行數據傳輸時,會自動將多個小部份的數據打包成一個大的數據進行發送,例如,在客户端給服務端發送數據時,我們分開發送了 ABC , 123 這兩段信息,按照常理來説,客户端收到的消息也應該兩段分開的消息,而服務端實際收到的消息是 ABC123,可是這並不是我們想要的結果,因為在實際中,我們收到這種消息後很難將多條連接在一起的消息區分開來

代碼表現

1、客户端
    # 導入socket
    import socket

    # 產生socket對象
    server = socket.socket()
    # 連接主機地址
    server.connect(('192.168.1.188', 8899))
    # 向主機發送信息
    server.send(b'hello tom')
    server.send(b'hello jason')
    server.send(b'hello jerry')

2、服務端
	# 產生socket對象
    server = socket.socket()
    # 設置主機IP地址
    server.bind(('192.168.1.188', 8899))
    # 設置半連接池
    server.listen(5)
    # 獲取客户端接入
    sock, addre = server.accept()
    sock = sock.recv(1024).decode('utf8')
    # 打印客户端信息
    print(sock)

---------------------------------------------------------------
hello tomhello jasonhello jerry

2、產生黏包的原因

  • 1、在使用socket的模塊,接收消息時 需要設置 recv 也就是接收的字節數,可是我們並不知道對方發送來的消息具體大小是多少,所以就會默認接收recv設置的字節以內的多段的消息
  • 2、TCP也稱為流式協議:數據像水流一樣綿綿不絕沒有間隔(TCP會針對數據量較小且發送間隔較短的多條數據一次性合併打包發送)

3、如何避免黏包

核心思路:

1、明確即將接收的消息具體有多大

2、針對消息的大小設置recv的字節數

解決辦法:

1、藉助struct模塊

二、struct模塊

1、模塊簡介

可以將一組數據打包為固定的大小,也可將打包的數據進行解包

2、常用方法

1、struct
	功能:導入模塊

1、pack('模式', '待打包的數據')
	功能:將一組數據按照指定模式進行打包,打包後的數據為Bytes類型
 
2、unpack(bytes_data)	
	功能:解析打包後的數據

3、常見格式

格式

python類型

標準尺寸

c

1個字節長度

1

ns

n個字符

n

i

整數

4

f

浮點

4

三、黏包問題實戰

1、解決思路

上述,我們瞭解了產生黏包的原因,並且知道了大致解決思路,下面我們將針對黏包問題指定詳細解決辦法

第一步:

分別在客户端和服務端導入struct模塊,通過了解struct模塊,我們知道在pack方法的‘i’模式下,會固定加打包的數據大小壓縮為4個字節

第二步:

產生一個‘字典’類型,在字典內存入我們需要發送的目標文件詳細信息,並將目標文件的大小存入鍵值對,將信息設置為鍵值對格式

第三步:

使用‘len’方法,獲得字典編碼後的字節個數,將字典的個數用'struct'模塊的下'pack'方法進行打包,接收端將'recv'設置為4個字節,並接收打包後的‘字典字節個數’,將‘recv’設置為字典的大小,接收字典後,獲取字典內目標文件大小,再將‘recv’設置為目標文件大小,進行接收目標文件

第四步:

將打包的'字典'使用'unpack'方法解析,並讀取字典信息,獲取到需要接收的數據詳細大小,在將'recv'設置為需要接收的文件大小,進行接收數據

2、代碼實戰

模擬客户端向服務端發送信息,將存有數據的'txt'格式文本發送給服務端

1、服務端
import socket
import struct
import json


server = socket.socket()
server.bind(('127.0.0.1', 8081))
server.listen(5)

sock, addr = server.accept()
# 1.接收固定長度的字典報頭
data_dict_head = sock.recv(4)
# 2.根據報頭解析出字典數據的長度
data_dict_len = struct.unpack('i', data_dict_head)[0]
# 3.接收字典數據
data_dict_bytes = sock.recv(data_dict_len)
data_dict = json.loads(data_dict_bytes)  # 自動解碼再反序列化
# 4.獲取真實數據的各項信息
# total_size = data_dict.get('file_size')
# with open(data_dict.get('file_name'), 'wb') as f:
#     f.write(sock.recv(total_size))
'''接收真實數據的時候 如果數據量非常大 recv括號內直接填寫該數據量 不太合適 我們可以每次接收一點點 反正知道總長度'''
# total_size = data_dict.get('file_size')
# recv_size = 0
# with open(data_dict.get('file_name'), 'wb') as f:
#     while recv_size < total_size:
#         data = sock.recv(1024)
#         f.write(data)
#         recv_size += len(data)
#         print(recv_size)


2、客户端
import socket
import os
import struct
import json

client = socket.socket()
client.connect(('127.0.0.1', 8081))

'''任何文件都是下列思路 圖片 視頻 文本 ...'''
# 1.獲取真實數據大小
file_size = os.path.getsize(r'/Users/jiboyuan/PycharmProjects/day36/xx老師合集.txt')
# 2.製作真實數據的字典數據
data_dict = {
    'file_name': '有你好看.txt',
    'file_size': file_size,
    'file_desc': '內容很長 準備好吃喝 我覺得營養快線挺好喝',
    'file_info': '這是我的私人珍藏'
}
# 3.製作字典報頭
data_dict_bytes = json.dumps(data_dict).encode('utf8')
data_dict_len = struct.pack('i', len(data_dict_bytes))
# 4.發送字典報頭
client.send(data_dict_len)  # 報頭本身也是bytes類型 我們在看的時候用len長度是4
# 5.發送字典
client.send(data_dict_bytes)
# 6.最後發送真實數據
with open(r'/Users/jiboyuan/PycharmProjects/day36/xx老師合集.txt', 'rb') as f:
    for line in f:  # 一行行發送 和直接一起發效果一樣 因為TCP流式協議的特性
        client.send(line)
import time
time.sleep(10)

四、UDP協議

1、UDP協議特點

UDP協議是面向無連接的通訊協議,UDP是不可靠協議,發送方所發送的數據對方不一定能夠接收到

2、代碼實現

服務端:

import socket

# UDP客户端、創建一個服務器端的Socket
socket_server = socket(AF_INET, SOCK_DGRAM)

# 2、定義服務器端的ip地址和端口號,元組形式
host_port = ('192.168.108.43', 8090)

# 3、服務器端的Socket來綁定地址和端口,只有綁定了地址和端口,才能稱為服務器的Socket
socket_server.bind(host_port)

# 4、接收客户端發送過來的數據,每次接收1kb的數據,如果一直沒收到數據會阻塞
# 收到的每一個數據報,裏面是一個元組,第一個值是數據內容,第二個值是源地址
data = socket_server.recvfrom(1024)

# 解碼
print(data[0].decode('utf-8'))
print(data)

# 5、關閉套接字、釋放資源
socket_server.close()

服務器端的結構:
(1) 使用函數socket(),生成套接字描述符;
(2) 通過host_post 結構設置服務器地址和監聽端口;
(3)使用bind()函數綁定監聽端口,將套接字文件描述符和地址類型變(host_post) 進行綁定;
(4)接收客户端的數據,使用recvfrom()函數接收客户端的網絡數據;
(5)關閉套接字,使用close()函數釋放資源;

客户端:

impo

# 客户端發送一個請求也需要端口,端口是隨機分配的
# 創建一個UDP協議的套接字,然後發送一條數據到網絡上的另外一個進程

# UDP客户端、創建套接字
client_socket = socket(AF_INET, SOCK_DGRAM)  # SOCK_DGRAM:UDP協議

# 2、定義一個接收消息的目標,8080是一個目標服務器的端口,127.0.0.1是目標服務器地址
server_host_port = ('192.168.108.43', 8080)
# 3、準備即將發送的數據,encode表示按照一種編碼格式把數據變成字節數組bytes
# 數據一定是字節數據才能發送
datas = input('請輸入:').encode('utf-8')

# 4、發送數據,標識一個進程是通過ip+端口+協議
client_socket.sendto(datas, server_host_port)

print('發送完成')

# 5、關閉套接字,其實就是釋放了系統資源
client_socket.close()