Heim > Backend-Entwicklung > Python-Tutorial > Detaillierte Erklärung des Sticky-Packet-Problems bei der Python-Socket-Netzwerkprogrammierung

Detaillierte Erklärung des Sticky-Packet-Problems bei der Python-Socket-Netzwerkprogrammierung

不言
Freigeben: 2018-04-28 13:36:00
Original
2212 Leute haben es durchsucht

Dieser Artikel stellt hauptsächlich die detaillierte Erklärung des Sticking-Problems der Python-Socket-Netzwerkprogrammierung vor. Jetzt teile ich ihn mit Ihnen und gebe ihn als Referenz. Schauen wir uns das gemeinsam an

1. Details zum Sticky-Packet-Problem

1 Nur bei TCP gibt es ein Packet-Sticking-Phänomen , UDP wird niemals Pakete speichern

Ihr Programm hat tatsächlich nicht das Recht, die Netzwerkkarte direkt zu bedienen Schnittstelle, die das Betriebssystem dem Benutzerprogramm zur Verfügung stellt. Jedes Mal, wenn Ihr Programm Daten an einen entfernten Standort senden möchte, kopiert es tatsächlich die Daten vom Benutzerstatus in den Kernelstatus. Dieser Vorgang verbraucht häufig Ressourcen und Zeit Der Kernel-Status und der Benutzerstatus führen zwangsläufig dazu, dass die Effizienz verringert wird. Um die Übertragungseffizienz des Sockets zu verbessern, muss der Absender daher häufig genügend Daten sammeln, bevor er die Daten an die andere Partei sendet einmal. Wenn die Daten, die mehrmals hintereinander gesendet werden müssen, sehr klein sind, fasst der TCP-Socket die Daten normalerweise gemäß dem Optimierungsalgorithmus zu einem TCP-Segment zusammen und sendet sie auf einmal aus, sodass der Empfänger die Sticky-Paketdaten empfängt .

Zuerst müssen Sie das Prinzip des Sendens und Empfangens von Nachrichten über einen Socket beherrschen.

Der Absender kann 1.000, 1.000 gesendete Daten und der Empfänger sein Die Anwendung kann 2k, 2k extrahieren. Natürlich ist es möglich, 3k oder mehr Daten zu extrahieren. Mit anderen Worten, die Anwendung ist unsichtbar, daher ist das TCP-Protokoll das Protokoll für diesen Stream. Dies ist auch der Grund, warum Sticky-Pakete anfällig sind Während es sich bei UDP um ein verbindungsloses Protokoll handelt, ist jedes UDP-Segment eine Nachricht, und die Anwendung muss Daten in Nachrichteneinheiten extrahieren und kann nicht jedes Datenbyte gleichzeitig extrahieren. Dies ist dem TCP sehr ähnlich. Wie definiere ich eine Nachricht? Es wird davon ausgegangen, dass es sich bei den von der anderen Partei gleichzeitig geschriebenen/gesendeten Daten um eine Nachricht handelt. Es muss bekannt sein, dass die TCP-Protokollschicht die Daten sortiert, unabhängig davon, wie Dingcheng sie fragmentiert Segmente, aus denen die gesamte Nachricht besteht, bevor sie im Kernelpuffer erscheinen.

Zum Beispiel lädt ein TCP-basierter Socket-Client eine Datei auf den Server hoch. Beim Senden wird der Dateiinhalt segmentweise als Byte-Stream gesendet. Für den Empfänger, der es nicht weiß, erscheint es noch dümmer Der Bytestrom der Datei. Wo beginnt er und wo endet er.

3. Gründe für Sticky-Packets

3-1 Direkte Gründe

Das sogenannte Sticky-Packet-Problem liegt hauptsächlich daran, dass der Empfänger es nicht weiß der Unterschied zwischen Nachrichten Grenze, verursacht durch Unwissenheit, wie viele Datenbytes gleichzeitig extrahiert werden sollen

3-2 Die Grundursache

Das vom Absender verursachte Sticky Packet wird durch das TCP verursacht Das Protokoll selbst soll die Übertragung verbessern. Aus Effizienzgründen muss der Absender häufig genügend Daten sammeln, bevor er ein TCP-Segment sendet. Wenn die Daten, die mehrmals hintereinander gesendet werden müssen, sehr klein sind, kombiniert TCP normalerweise die Daten gemäß dem Optimierungsalgorithmus zu einem TCP-Segment und sendet sie auf einmal aus, sodass der Empfänger sie empfängt die Sticky-Packet-Daten.

3-3 Zusammenfassung

  1. TCP (Transport Control Protocol, Transmission Control Protocol) ist verbindungsorientiert und stromorientiert und bietet hochzuverlässige Dienste. Sowohl die sendende als auch die empfangende Seite (Client und Server) müssen über ein Socket-Paar verfügen. Um mehrere Pakete effizienter an die empfangende Seite zu senden, verwendet die sendende Seite eine Optimierungsmethode (Nagle-Algorithmus). Intervalle und kleine Datenmengen in einen großen Datenblock packen und dann verpacken. Auf diese Weise ist es für den Empfänger schwierig zu unterscheiden, und es muss ein wissenschaftlicher Auspackmechanismus bereitgestellt werden. Das heißt, die stromorientierte Kommunikation kennt keine Nachrichtenschutzgrenzen.

  2. UDP (User Datagram Protocol) ist verbindungslos, nachrichtenorientiert und bietet hocheffiziente Dienste. Der Block-Merging-Optimierungsalgorithmus wird nicht verwendet. Da UDP den Eins-zu-Viele-Modus unterstützt, verwendet der Skbuff (Socket-Puffer) auf der Empfangsseite eine Kettenstruktur, um jedes ankommende UDP-Paket aufzuzeichnen. In jedem UDP gibt es einen Nachrichtenheader (). (Adresse der Nachrichtenquelle, Port und andere Informationen) im Paket, sodass es für den Empfänger leicht zu unterscheiden und zu verarbeiten ist. Das heißt, nachrichtenorientierte Kommunikation hat Nachrichtenschutzgrenzen.

  3. TCP basiert auf dem Datenfluss, sodass die gesendeten und empfangenen Nachrichten nicht leer sein dürfen. Dies erfordert das Hinzufügen eines leeren Nachrichtenverarbeitungsmechanismus sowohl auf dem Client als auch auf dem Server, um das Programm zu verhindern Stuck Live, und UDP basiert auf Datagrammen. Auch wenn Sie leeren Inhalt eingeben (drücken Sie direkt die Eingabetaste), hilft Ihnen das UDP-Protokoll dabei, den Nachrichtenheader leicht zu kapseln

  4. Ein recvfrom(x) darf nur für einen sendinto(y) sein. Wenn y>x Daten verloren gehen, kann dies nicht der Fall sein Pakete bleiben hängen, aber Daten gehen verloren und es ist unzuverlässig.

TCP-Protokolldaten gehen nicht verloren. Wenn das Paket beim nächsten Empfang nicht erfasst wird, wird es beim letzten Mal weiterhin empfangen . Das Ende erhält immer die Bestätigung, wenn sie empfangen wird. Der Pufferinhalt wird gelöscht. Die Daten sind zuverlässig, können jedoch klebrig sein.

Zweitens kommt es in zwei Situationen zum Festkleben:

1. Das sendende Ende muss vor dem Senden warten, bis der lokale Puffer voll ist, was zu Sticky-Paketen führt (das Zeitintervall für das Senden von Daten ist sehr kurz, die Daten sind sehr klein, Python verwendet einen Optimierungsalgorithmus usw.). sie erzeugen klebrige Pakete)

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'))
Nach dem Login kopieren

Server

#_*_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()
Nach dem Login kopieren

2. Der Empfänger akzeptiert die Pakete im Puffer nicht rechtzeitig, was dazu führt, dass mehrere Pakete akzeptiert werden ( Der Client sendet ein Datenstück, der Server hat beim nächsten Mal nur einen kleinen Teil der Daten gesammelt, was zu Sticky-Paketen führt.) 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'))
Nach dem Login kopieren

Server

#_*_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()
Nach dem Login kopieren

Drei, Beispiel für eine klebrige Tasche:

Server

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)
Nach dem Login kopieren

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'))
Nach dem Login kopieren

Viertens das Auftreten des Entpackens

Wenn die Länge des Absenderpuffers größer als die MTU der Netzwerkkarte ist, teilt TCP die diesmal gesendeten Daten in mehrere Datenpakete auf und Senden Sie sie rüber

Ergänzungsfrage 1: Warum TCP eine zuverlässige Übertragung und UDP eine unzuverlässige Übertragung ist

Wenn TCP Daten überträgt, sendet der Absender die Daten zunächst an seinen eigenen Cache , und dann steuert das Protokoll die Daten im Cache an den Peer, der Peer gibt eine Bestätigung = 1 zurück, der Absender löscht die Daten im Cache, der Peer gibt Bestätigung = 0 zurück und sendet die Daten dann erneut, sodass TCP zuverlässig ist

Wenn UDP Daten sendet, gibt der Peer keine Bestätigungsinformationen zurück, daher ist es unzuverlässig

Ergänzungsfrage 2: Was senden (Byte-Stream), recv(1024 ) und sendall meinen?

Die in recv angegebene Zahl 1024 bedeutet, dass jeweils 1024 Byte Daten aus dem Cache entnommen werden.

Der gesendete Bytestrom wird zuerst in den Cache auf der eigenen Seite gestellt und dann Der Cache wird vom Protokoll gesteuert. Der Inhalt wird an das andere Ende gesendet. Wenn die Größe des Byte-Streams größer ist als der verbleibende Cache-Speicherplatz, gehen die Daten verloren. Verwenden Sie sendall, um send in einer Schleife aufzurufen verloren gehen.

5. Wie lässt sich das Problem der klebrigen Beutel lösen?

Die Ursache des Problems liegt darin, dass die empfangende Seite die Länge des vom sendenden Ende zu übertragenden Bytestroms nicht kennt. Daher besteht die Lösung für Sticky-Pakete darin, sich darauf zu konzentrieren, wie Damit das sendende Ende Daten sendet, bevor es gesendet wird, teilen Sie dem empfangenden Ende die Gesamtgröße des Byte-Streams mit, den Sie senden möchten. Anschließend erstellt das empfangende Ende eine Endlosschleife, um alle Daten zu empfangen.

5-1 Einfache Lösung (Oberflächenlösung):

Fügen Sie eine Ruhezeit unter Client-Senden hinzu, um ein Anhaften von Paketen zu vermeiden. Auch beim Empfangen durch den Server ist eine Ruhezeit erforderlich, um Packet Sticking effektiv zu vermeiden.

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'))
Nach dem Login kopieren

Server:

#服务端
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)
Nach dem Login kopieren

Die obige Lösung wird auf jeden Fall viele Fehler enthalten, da Sie nicht wissen, wann die Übertragung abgeschlossen ist, und Die Länge der Pause variiert. Das Problem besteht darin, dass sie ineffizient ist, wenn sie lang ist, und ungeeignet, wenn sie kurz ist. Daher ist diese Methode ungeeignet.

5-2 Gängige Lösungen (betrachten Sie das Problem von der Wurzel):

Die Wurzel des Problems besteht darin, dass das empfangende Ende den Bytestream nicht kennt Die Methode zum Lösen von Sticky-Paketen besteht also darin, sich darauf zu konzentrieren, wie das sendende Ende dem empfangenden Ende die Gesamtgröße des zu sendenden Bytestroms mitteilen kann, bevor Daten gesendet werden, und dann wird das empfangende Ende dies tun Erstellen Sie eine Endlosschleife, um alle Daten zu empfangen.

Fügen Sie dem Byte-Stream einen benutzerdefinierten Header mit fester Länge hinzu. Der Header enthält die Länge des Byte-Streams und sendet ihn dann nacheinander an den Peer Wenn es empfangen wird, entnimmt es zunächst den Header mit fester Länge aus dem Cache und ruft dann die tatsächlichen Daten ab.

Verwenden Sie das Strukturmodul, um eine feste Länge von 4 Bytes oder acht Bytes zu packen. Wenn der Parameter struct.pack.format „i“ ist, können Sie nur Zahlen mit einer Länge von 10 packen, also können Sie zuerst Konvertieren Sie die Länge in einen JSON-String und verpacken Sie ihn dann.

Gewöhnlicher Client

# _*_ 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()
Nach dem Login kopieren

Gewöhnlicher Server

# _*_ 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()
Nach dem Login kopieren


5-3 Optimierte Version der Lösung (von: Lösen Sie das Problem grundlegend )

Die Idee der Optimierung zur Lösung des Sticky-Problems besteht darin, dass der Server die Header-Informationen optimiert und ein Wörterbuch verwendet, um den zu sendenden Inhalt zu beschreiben. Erstens kann das Wörterbuch nicht wird direkt über das Netzwerk übertragen und muss in eine JSON-formatierte Zeichenfolge konvertiert und dann zum Senden in das Byte-Format an den Server konvertiert werden. Da die Länge der JSON-Zeichenfolge im Byte-Format nicht festgelegt ist, ist dies beim Strukturmodul der Fall Wird verwendet, um die Länge der JSON-Zeichenfolge im Byte-Format auf eine feste Länge zu komprimieren. Senden Sie sie an den Client, der Client akzeptiert sie und erhält das vollständige Datenpaket durch Dekodierung.

Ultimate-Version-Client

# _*_ 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()
Nach dem Login kopieren

Ultimate-Version-Server

# _*_ 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()
Nach dem Login kopieren

6. Strukturmodul

了解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
Nach dem Login kopieren

代码中,首先定义了一个元组数据,包含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)
Nach dem Login kopieren


Das obige ist der detaillierte Inhalt vonDetaillierte Erklärung des Sticky-Packet-Problems bei der Python-Socket-Netzwerkprogrammierung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage