Maison  >  Article  >  développement back-end  >  Explication détaillée du problème des paquets persistants dans la programmation réseau des sockets Python

Explication détaillée du problème des paquets persistants dans la programmation réseau des sockets Python

不言
不言original
2018-04-28 13:36:002168parcourir

Cet article présente principalement l'explication détaillée du problème de blocage de la programmation réseau des sockets Python. Maintenant, je le partage avec vous et le donne comme référence. Venez jeter un œil ensemble

1. Détails du problème des paquets collants

1. , UDP ne collera jamais de paquets

Votre programme n'a en fait pas le droit de faire fonctionner directement la carte réseau. Lorsque vous utilisez la carte réseau, vous utilisez la carte réseau. Interface exposée par le système d'exploitation au programme utilisateur. Ensuite, chaque fois que votre programme souhaite envoyer des données à un emplacement distant, il copie en fait les données de l'état utilisateur vers l'état du noyau. Cette opération consomme des ressources et du temps. l'état du noyau et l'état de l'utilisateur entraîneront inévitablement l'envoi des données. Par conséquent, afin d'améliorer l'efficacité de la transmission du socket, l'expéditeur doit souvent collecter suffisamment de données avant de les envoyer à l'autre partie. une fois. Si les données qui doivent être envoyées plusieurs fois de suite sont très petites, le socket TCP combinera généralement les données dans un segment TCP selon l'algorithme d'optimisation et l'enverra en même temps, afin que le récepteur reçoive les données du paquet collant. .

2. Tout d'abord, vous devez maîtriser le principe d'envoi et de réception de messages via un socket

L'expéditeur peut être 1k, 1k de données envoyées et le destinataire. l'application peut extraire 2k, 2k Bien sûr, il est possible d'extraire 3k ou plus de données. En d'autres termes, l'application est invisible, donc le protocole TCP est le protocole pour ce flux. C'est aussi la raison pour laquelle les paquets collants sont sujets à l'extraction. se produire, alors que UDP signifie protocole sans connexion, chaque segment UDP est un message et l'application doit extraire les données en unités de messages et ne peut extraire aucun octet de données à la fois. Ceci est très similaire à TCP. Comment définir le message ? On considère que les données écrites/envoyées par l'autre partie à un moment donné sont un message. Ce qu'il faut savoir, c'est que lorsque l'autre partie envoie un message, quelle que soit la façon dont Dingcheng le fragmente, la couche de protocole TCP triera les données. segments qui composent l’intégralité du message avant d’apparaître dans le tampon du noyau.

Par exemple, un client socket basé sur TCP télécharge un fichier sur le serveur lors de l'envoi, le contenu du fichier est envoyé sous forme de flux d'octets segment par segment. Cela semble encore plus stupide au destinataire qui ne le sait pas. le flux d'octets du fichier. Où commence-t-il et où se termine-t-il.

3. Raisons des paquets collants

3-1 Raisons directes

Le problème dit des paquets collants est principalement dû au fait que le destinataire ne le sait pas. la différence entre les messages Limite, causée par le fait de ne pas savoir combien d'octets de données extraire en même temps

3-2 La cause première

Le paquet collant provoqué par l'expéditeur est causé par le TCP Le protocole lui-même est destiné à améliorer la transmission. Pour plus d'efficacité, l'expéditeur doit souvent collecter suffisamment de données avant d'envoyer un segment TCP. Si les données qui doivent être envoyées plusieurs fois de suite sont très petites, TCP combinera généralement les données en un seul segment TCP selon l'algorithme d'optimisation et l'enverra en une seule fois, afin que le destinataire reçoive les données collantes.

Résumé 3-3

  1. TCP (protocole de contrôle de transport, Transmission Control Protocol) est orienté connexion et flux, fournissant des services de haute fiabilité. Les extrémités d'envoi et de réception (client et serveur) doivent avoir une paire de sockets. Par conséquent, afin d'envoyer plus efficacement plusieurs paquets à l'extrémité de réception, l'extrémité d'envoi utilise une méthode d'optimisation (algorithme Nagle pour combiner plusieurs données avec de petites données). intervalles et petits volumes de données dans un grand bloc de données, puis regroupez-le. De cette façon, il sera difficile pour le destinataire de faire la distinction et un mécanisme de déballage scientifique doit être fourni. Autrement dit, la communication orientée flux n'a pas de limites de protection des messages.

  2. UDP (protocole de datagramme utilisateur) est sans connexion, orienté message et fournit des services à haute efficacité. L'algorithme d'optimisation de fusion de blocs ne sera pas utilisé. Étant donné qu'UDP prend en charge le mode un-à-plusieurs, le skbuff (tampon de socket) à l'extrémité de réception adopte une structure en chaîne pour enregistrer chaque paquet UDP arrivant. Dans chaque UDP, il y a un en-tête de message (. adresse source du message, port et autres informations) dans le colis, de sorte qu'il soit facile pour le destinataire de le distinguer et de le traiter. Autrement dit, la communication orientée message a des limites de protection des messages.

  3. tcp est basé sur le flux de données, donc les messages envoyés et reçus ne peuvent pas être vides. Cela nécessite l'ajout d'un mécanisme de traitement des messages vides à la fois sur le client et sur le serveur pour empêcher le programme. bloqué Live, et UDP est basé sur des datagrammes. Même si vous entrez du contenu vide (appuyez directement sur Entrée), ce n'est pas un message vide. Le protocole UDP vous aidera à encapsuler l'en-tête du message.

  4. Le recvfrom d'UDP est bloqué. Un recvfrom(x) doit être pour un seul sendinto(y). Il est terminé après la collecte de x octets de données si y>x les données sont perdues, ce qui signifie que udp ne peut pas. Les paquets resteront, mais les données seront perdues, et ce n'est pas fiable

les données du protocole TCP ne seront pas perdues si le paquet n'est pas collecté, la prochaine fois qu'il sera reçu, il continuera à recevoir la dernière fois. . La fin reçoit toujours l'accusé de réception lorsqu'il est reçu. Le contenu du tampon sera effacé. Les données sont fiables, mais peuvent être collantes.

Deuxièmement, le collage se produira dans deux situations :

1. L'expéditeur doit attendre que le tampon local soit plein avant d'envoyer, ce qui entraîne des paquets persistants (l'intervalle de temps pour l'envoi des données est très court, les données sont très petites, Python utilise un algorithme d'optimisation, et ensemble ils produisent des paquets collants)

Client

#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)
s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))

Serveur

#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
conn,addr=tcp_socket_server.accept()
data1=conn.recv(10)
data2=conn.recv(10)
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
conn.close()

2 L'extrémité réceptrice n'accepte pas les paquets dans le tampon à temps, ce qui entraîne plusieurs paquets. les paquets sont acceptés (le client envoie un paragraphe. Le serveur n'a reçu qu'une petite partie des données. Le serveur récupérera toujours les données restantes du tampon la prochaine fois, ce qui entraînera des paquets collants.) Client

#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)
s.send('hello feng'.encode('utf-8'))

Serveur

#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
conn,addr=tcp_socket_server.accept()
data1=conn.recv(2) #一次没有收完整
data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
conn.close()

3. Instance du package Sticky :

Serveur

import socket
import subprocess
din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ip_port=('127.0.0.1',8080)
din.bind(ip_port)
din.listen(5)
conn,deer=din.accept()
data1=conn.recv(1024)
data2=conn.recv(1024)
print(data1)
print(data2)

Client :

import socket
import subprocess
din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ip_port=('127.0.0.1',8080)
din.connect(ip_port)
din.send('helloworld'.encode('utf-8'))
din.send('sb'.encode('utf-8'))

Quatrièmement, que se passe-t-il lors du déballage

Lorsque la longueur du tampon de l'expéditeur est supérieure au MTU de la carte réseau, tcp enverra les données cette fois Divisé en plusieurs paquets de données et les enverra

Première question supplémentaire : Pourquoi TCP est une transmission fiable et UDP est une transmission peu fiable

Quand tcp transmet les données, l'expéditeur envoie d'abord les données à son propre cache, puis le protocole contrôle les données du cache à envoyer au homologue. L'homologue renvoie un accusé de réception = 1. L'expéditeur efface les données du cache. L'homologue renvoie ack=0 et renvoie les données, donc TCP est fiable

Pendant que udp envoie des données, l'homologue ne renverra pas d'informations de confirmation, ce n'est donc pas fiable

Question supplémentaire. deux : send (byte stream), recv(1024) et sendall, que signifient-ils ?

Le 1024 spécifié dans recv signifie que 1024 octets de données sont retirés du cache à la fois

Le flux d'octets d'envoi est d'abord placé dans le cache côté propre, puis le le cache est contrôlé par le protocole. Le contenu est envoyé à l'extrémité opposée. Si la taille du flux d'octets est supérieure à l'espace de cache restant, les données seront perdues. Utilisez sendall pour appeler send en boucle. être perdu.

5. Comment résoudre le problème du sac collant ?

La racine du problème est que l'extrémité réceptrice ne connaît pas la longueur du flux d'octets à transmettre par l'extrémité émettrice, donc la solution aux paquets persistants est de se concentrer sur la façon dont pour que l'extrémité émettrice envoie des données avant de les envoyer. Informez l'extrémité réceptrice de la taille totale du flux d'octets que vous êtes sur le point d'envoyer, puis l'extrémité réceptrice créera une boucle infinie pour recevoir toutes les données.

5-1 Solution simple (solution de surface) :

Ajouter un temps de veille sous l'envoi du client pour éviter que les paquets ne collent. Le serveur doit également effectuer une mise en veille temporelle lors de la réception pour éviter efficacement les paquets persistants.

Client :

#客户端
import socket
import time
import subprocess
din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ip_port=('127.0.0.1',8080)
din.connect(ip_port)
din.send('helloworld'.encode('utf-8'))
time.sleep(3)
din.send('sb'.encode('utf-8'))

Serveur :

#服务端
import socket
import time
import subprocess
din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ip_port=('127.0.0.1',8080)
din.bind(ip_port)
din.listen(5)
conn,deer=din.accept()
data1=conn.recv(1024)
time.sleep(4)
data2=conn.recv(1024)
print(data1)
print(data2)

La solution ci-dessus provoquera certainement beaucoup d'erreurs, car vous ne savez pas quand la transmission est terminée et le temps est mis en pause. Il y aura des problèmes de longueur. S'il est long, l'efficacité est faible, et s'il est court, il est inapproprié, donc cette méthode ne convient pas.

5-2 Solutions courantes (regardez le problème depuis la racine) :

La racine du problème est que l'extrémité réceptrice ne connaît pas le flux d'octets à être transmis par la longueur de l'extrémité d'envoi, donc la façon de résoudre les paquets persistants est de se concentrer sur la façon de laisser l'extrémité d'envoi informer l'extrémité de réception de la taille totale du flux d'octets qu'elle enverra avant d'envoyer des données, puis l'extrémité de réception le fera créez une boucle infinie pour recevoir toutes les données

Ajoutez un en-tête personnalisé de longueur fixe au flux d'octets. L'en-tête contient la longueur du flux d'octets, puis l'envoie à son tour au homologue. le reçoit, il retire d'abord l'en-tête de longueur fixe du cache, puis récupère les données réelles.

Utilisez le module struct pour compresser une longueur fixe de 4 octets ou huit octets. Lorsque le paramètre struct.pack.format est "i", vous ne pouvez compresser que des nombres d'une longueur de 10, vous pouvez donc d'abord le faire. Convertissez la longueur en chaîne json, puis emballez-la.

Client ordinaire

# _*_ coding: utf-8 _*_ 
import socket
import struct
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8880)) #连接服
while True:
 # 发收消息
 cmd = input('请你输入命令>>:').strip()
 if not cmd:continue
 phone.send(cmd.encode('utf-8')) #发送

 #先收报头
 header_struct = phone.recv(4) #收四个
 unpack_res = struct.unpack('i',header_struct)
 total_size = unpack_res[0] #总长度

 #后收数据
 recv_size = 0
 total_data=b''
 while recv_size<total_size: #循环的收
  recv_data = phone.recv(1024) #1024只是一个最大的限制
  recv_size+=len(recv_data) #
  total_data+=recv_data #
 print(&#39;返回的消息:%s&#39;%total_data.decode(&#39;gbk&#39;))
phone.close()

Serveur ordinaire

# _*_ coding: utf-8 _*_ 
import socket
import subprocess
import struct
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
phone.bind((&#39;127.0.0.1&#39;,8880)) #绑定手机卡
phone.listen(5) #阻塞的最大数
print(&#39;start runing.....&#39;)
while True: #链接循环
 coon,addr = phone.accept()# 等待接电话
 print(coon,addr)
 while True: #通信循环

  # 收发消息
  cmd = coon.recv(1024) #接收的最大数
  print(&#39;接收的是:%s&#39;%cmd.decode(&#39;utf-8&#39;))

  #处理过程

  res = subprocess.Popen(cmd.decode(&#39;utf-8&#39;),shell = True,
           stdout=subprocess.PIPE, #标准输出
           stderr=subprocess.PIPE #标准错误
        )
  stdout = res.stdout.read()
  stderr = res.stderr.read()

  #先发报头(转成固定长度的bytes类型,那么怎么转呢?就用到了struct模块)
  #len(stdout) + len(stderr)#统计数据的长度
  header = struct.pack(&#39;i&#39;,len(stdout)+len(stderr))#制作报头
  coon.send(header)

  #再发命令的结果
  coon.send(stdout)
  coon.send(stderr)
 coon.close()
phone.close()


5-3 version optimisée Solution (résoudre le problème à partir de la racine)

L'idée optimisée pour résoudre le problème délicat est que le serveur optimise les informations d'en-tête et utilise un dictionnaire pour décrire le contenu à envoyer. Tout d'abord, le dictionnaire ne peut pas accéder directement. le réseau. Pour la transmission, vous devez la sérialiser et la convertir en chaîne au format json, puis la convertir au format octets pour l'envoyer au serveur. Étant donné que la longueur de la chaîne json au format octets n'est pas fixe, vous devez l'utiliser. le module struct pour compresser la longueur de la chaîne json au format octets en une longueur fixe, envoyée au client, le client l'accepte et la déchiffre pour obtenir le paquet de données complet.

Client version ultime

# _*_ coding: utf-8 _*_ 
import socket
import struct
import json
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect((&#39;127.0.0.1&#39;,8080)) #连接服务器
while True:
 # 发收消息
 cmd = input(&#39;请你输入命令>>:&#39;).strip()
 if not cmd:continue
 phone.send(cmd.encode(&#39;utf-8&#39;)) #发送

 #先收报头的长度
 header_len = struct.unpack(&#39;i&#39;,phone.recv(4))[0] #吧bytes类型的反解

 #在收报头
 header_bytes = phone.recv(header_len) #收过来的也是bytes类型
 header_json = header_bytes.decode(&#39;utf-8&#39;) #拿到json格式的字典
 header_dic = json.loads(header_json) #反序列化拿到字典了
 total_size = header_dic[&#39;total_size&#39;] #就拿到数据的总长度了

 #最后收数据
 recv_size = 0
 total_data=b&#39;&#39;
 while recv_size<total_size: #循环的收
  recv_data = phone.recv(1024) #1024只是一个最大的限制
  recv_size+=len(recv_data) #有可能接收的不是1024个字节,或许比1024多呢,
  # 那么接收的时候就接收不全,所以还要加上接收的那个长度
  total_data+=recv_data #最终的结果
 print(&#39;返回的消息:%s&#39;%total_data.decode(&#39;gbk&#39;))
phone.close()

Serveur version ultime

# _*_ coding: utf-8 _*_ 
import socket
import subprocess
import struct
import json
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind((&#39;127.0.0.1&#39;,8080)) #绑定手机卡
phone.listen(5) #阻塞的最大数
print(&#39;start runing.....&#39;)
while True: #链接循环
 coon,addr = phone.accept()# 等待接电话
 print(coon,addr)

 while True: #通信循环
  # 收发消息
  cmd = coon.recv(1024) #接收的最大数
  print(&#39;接收的是:%s&#39;%cmd.decode(&#39;utf-8&#39;))

  #处理过程
  res = subprocess.Popen(cmd.decode(&#39;utf-8&#39;),shell = True,
           stdout=subprocess.PIPE, #标准输出
           stderr=subprocess.PIPE #标准错误
        )
  stdout = res.stdout.read()
  stderr = res.stderr.read()

  # 制作报头
  header_dic = {
   &#39;total_size&#39;: len(stdout)+len(stderr), # 总共的大小
   &#39;filename&#39;: None,
   &#39;md5&#39;: None
  }
  header_json = json.dumps(header_dic) #字符串类型
  header_bytes = header_json.encode(&#39;utf-8&#39;) #转成bytes类型(但是长度是可变的)

  #先发报头的长度
  coon.send(struct.pack(&#39;i&#39;,len(header_bytes))) #发送固定长度的报头
  #再发报头
  coon.send(header_bytes)
  #最后发命令的结果
  coon.send(stdout)
  coon.send(stderr)
 coon.close()
phone.close()

Six, module struct

了解c语言的人,一定会知道struct结构体在c语言中的作用,它定义了一种结构,里面包含不同类型的数据(int,char,bool等等),方便对某一结构对象进行处理。而在网络通信当中,大多传递的数据是以二进制流(binary data)存在的。当传递字符串时,不必担心太多的问题,而当传递诸如int、char之类的基本数据的时候,就需要有一种机制将某些特定的结构体类型打包成二进制流的字符串然后再网络传输,而接收端也应该可以通过某种机制进行解包还原出原始的结构体数据。python中的struct模块就提供了这样的机制,该模块的主要作用就是对python基本类型值与用python字符串格式表示的C struct类型间的转化(This module performs conversions between Python values and C structs represented as Python strings.)。stuct模块提供了很简单的几个函数,下面写几个例子。

1,基本的pack和unpack

struct提供用format specifier方式对数据进行打包和解包(Packing and Unpacking)。例如:

#该模块可以把一个类型,如数字,转成固定长度的bytes类型
import struct
# res = struct.pack(&#39;i&#39;,12345)
# print(res,len(res),type(res)) #长度是4
res2 = struct.pack(&#39;i&#39;,12345111)
print(res2,len(res2),type(res2)) #长度也是4
unpack_res =struct.unpack(&#39;i&#39;,res2)
print(unpack_res) #(12345111,)
# print(unpack_res[0]) #12345111

代码中,首先定义了一个元组数据,包含int、string、float三种数据类型,然后定义了struct对象,并制定了format‘I3sf',I 表示int,3s表示三个字符长度的字符串,f 表示 float。最后通过struct的pack和unpack进行打包和解包。通过输出结果可以发现,value被pack之后,转化为了一段二进制字节串,而unpack可以把该字节串再转换回一个元组,但是值得注意的是对于float的精度发生了改变,这是由一些比如操作系统等客观因素所决定的。打包之后的数据所占用的字节数与C语言中的struct十分相似。

2,定义format可以参照官方api提供的对照表:

3,基本用法

import json,struct
#假设通过客户端上传1T:1073741824000的文件a.txt
#为避免粘包,必须自定制报头
header={&#39;file_size&#39;:1073741824000,&#39;file_name&#39;:&#39;/a/b/c/d/e/a.txt&#39;,&#39;md5&#39;:&#39;8f6fbf8347faa4924a76856701edb0f3&#39;} #1T数据,文件路径和md5值

#为了该报头能传送,需要序列化并且转为bytes
head_bytes=bytes(json.dumps(header),encoding=&#39;utf-8&#39;) #序列化并转成bytes,用于传输

#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack(&#39;i&#39;,len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度

#客户端开始发送
conn.send(head_len_bytes) #先发报头的长度,4个bytes
conn.send(head_bytes) #再发报头的字节格式
conn.sendall(文件内容) #然后发真实内容的字节格式

#服务端开始接收
head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式
x=struct.unpack(&#39;i&#39;,head_len_bytes)[0] #提取报头的长度
head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
header=json.loads(json.dumps(header)) #提取报头

#最后根据报头的内容提取真实的数据,比如
real_data_len=s.recv(header[&#39;file_size&#39;])
s.recv(real_data_len)


Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn