Python ネットワーク プログラミングに関する高度な知識
概要:
- OSI7層モデル
- TCPとUDP
- 粘着性のあるパッケージ
- ブロッキングとノンブロッキング
- I/O多重化
1. OSI7層モデル
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 (ユーザー データ プロトコル) は、シンプルなコネクションレス型のデータグラム指向のトランスポート層プロトコルです。UDP は信頼性を提供しません。アプリケーションによって送信されたデータグラムを IP 層に送信するだけですが、宛先に到達することは保証されません。UDP はデータグラムを送信する前にクライアントとサーバーの間で接続を確立する必要がなく、タイムアウトによる再送などの仕組みがないため、送信速度が非常に高速です。
常见的有:语音通话、视频通话、实时游戏画面 等。
-
TCP(Transmission Control Protocol、伝送制御プロトコル)は接続指向のプロトコルです。つまり、データを送受信する前に、相手との信頼できる接続を確立してからデータを送受信する必要があります。
常见有:网站、手机APP数据获取等。
2.1UDPとTCPのサンプルコード
UDP の例は次のとおりです。
- サーバ
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(("127.0.0.1", 8001))
while True:
data, (host, port) = server.recvfrom(1024) # 阻塞
print(data.decode("utf-8"), 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", 8001))
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.2TCP 3 ウェイ ハンドシェイクと 4 ウェイ ウェーブ
ネットワーク内の 2 者が TCP 接続に基づいて通信したい場合は、以下を通過する必要があります。
- 接続を作成するには、クライアントとサーバーは 3 ウェイ ハンドシェイクを実行する必要があります。
# 服务端
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)) # 发起连接
- データを転送する
在收发数据的过程中,只有有数据的传送就会有应答(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() # 关闭连接
3. 粘着パッケージ
2 台のコンピュータがデータを送受信する場合、相互にデータを直接送信するわけではありません。
- 送信側は、
sendall/send
メッセージを送信する際、まず自分のネットワーク カードの書き込みバッファにデータを送信し、次にバッファから相手のネットワーク カードの読み取りバッファにデータを送信します。 - 受信側では、
recv
メッセージを受信するときに、自身のネットワーク カードの読み取りバッファからデータを取得します。
したがって、送信者が 2 つの情報を連続して送信すると、受信者はそれを読み取るときにこれを 1 つの情報、つまり2 つのデータ パケットが貼り付けられていると認識します。例えば:
# socket客户端(发送者)
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8001))
client.sendall('达莱正在吃'.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()
パッケージの粘着性の問題を解決するにはどうすればよいですか?
メッセージが送信されるたびに、メッセージはヘッダー (固定バイト長) とデータの 2 つの部分に分割されます。例: ヘッダー。次のデータの長さを表すために 4 バイトを使用します。
- データを送信するには、まずデータの長さを送信してから、データを送信します (またはデータを結合して送信します)。
- データを受信するには、まず 4 バイトを読み取り、データ パケット内のデータの長さを確認し、次にその長さに応じてデータを読み取ります。
ヘッダーには数値が必要で、4 バイトに固定されています。この機能は、Python の struct パッケージを使用して実現できます。
import struct
# ########### 数值转换为固定4个字节,四个字节的范围 -2147483648 <= number <= 2147483647 ###########
v1 = struct.pack('i', 199) # i是int固定4个字节
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 = '达莱正在吃'.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()
コードをノンブロッキングにしたい場合は、次のように記述する必要があります。
- client.setblocking(False) # 追加するとノンブロッキングになります
# ################### 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('达莱正在吃羊肉'.encode('utf-8'))
client.close()
コードが非ブロッキングになると、プログラムで 、 が発生すると、accept
BlockingIOErrorの例外がスローされます。recv
connect
これはコード作成時のエラーではなく、元の 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)
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() # 与服务端断开连接(四次挥手),默认会向服务端发送空数据。
IO 多重化 + ノンブロッキングにより、TCP クライアントに複数のリクエストを同時に送信させることができます。たとえば、Web サイトにアクセスして、写真をダウンロードするリクエストを送信します。
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 多重化 + ノンブロッキング機能に基づいて、ソケットを書き込むサーバーとクライアントの両方のパフォーマンスを向上させることができます。の
- IO 多重化、ソケット オブジェクトが変更されたかどうか (接続が成功したかどうか、データが来ているかどうかなど) を監視します。
- ノンブロッキングなので、ソケットの connect プロセスと recv プロセスは待機しなくなりました。
注: IO 多重化は、IO オブジェクトが変更されたかどうかを監視するためにのみ使用できます。一般的なものは、ファイルの読み取りおよび書き込み可能かどうか、コンピュータ端末装置の入出力、ネットワーク要求 (共通) です。
Linux オペレーティング システムには、select、poll、epoll という 3 つの IO 多重化モードがあります。(Windows は選択モードのみをサポートします)
- 新しい接続の到着または新しいデータの到着についてソケット オブジェクトを監視します。
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()时便得到通知。
補足: ソケット + ノンブロッキング + IO 多重化 (すべての IO 操作オブジェクトを監視可能 + ファイル)。