Wenn Sie das Socket-Modell zur Implementierung der Netzwerkkommunikation verwenden, müssen Sie mehrere Schritte ausführen, z. B. das Erstellen eines Sockets, das Abhören von Ports, das Verarbeiten von Verbindungen sowie das Lesen und Schreiben von Anforderungen Schritte, die uns bei der Analyse der Mängel des Socket-Modells helfen.
Wenn wir zunächst zwischen dem Server und dem Client kommunizieren müssen, können wir durch die folgenden drei Schritte auf der Serverseite einen Listening-Socket (Listening Socket) erstellen, der die Client-Verbindung abhört:
Rufen Sie den an Socket-Funktion, Erstellen Sie einen Socket. Im Allgemeinen nennen wir diesen Socket einen aktiven Socket.
Rufen Sie die Bindefunktion auf, um den aktiven Socket an die IP-Adresse und den Überwachungsport des aktuellen Servers zu binden.
Rufen Sie die Listenfunktion auf, um den aktiven Socket zu binden Listening-Socket und beginnt, auf Client-Verbindungen zu lauschen.
Nach Abschluss der oben genannten drei Schritte kann der Server die Verbindungsanfrage des Clients empfangen. Um die Verbindungsanforderung des Clients rechtzeitig zu empfangen, können wir einen Schleifenprozess ausführen, in dem die Akzeptanzfunktion aufgerufen wird, um die Verbindungsanforderung des Clients zu empfangen.
Hier müssen Sie beachten, dass die Akzeptanzfunktion eine Blockierungsfunktion ist. Das heißt, wenn zu diesem Zeitpunkt keine Client-Verbindungsanforderung vorliegt, wird der serverseitige Ausführungsprozess immer in der Akzeptanzfunktion blockiert. Sobald eine Client-Verbindungsanforderung eintrifft, blockiert Accept nicht mehr, sondern verarbeitet die Verbindungsanforderung, stellt eine Verbindung mit dem Client her und gibt den Connected Socket zurück.
Schließlich kann der Server durch Aufrufen der Recv- oder Send-Funktion Lese- und Schreibanforderungen für den gerade zurückgegebenen verbundenen Socket empfangen und verarbeiten oder Daten an den Client senden.
Code:
listenSocket = socket(); //调用socket系统调用创建一个主动套接字 bind(listenSocket); //绑定地址和端口 listen(listenSocket); //将默认的主动套接字转换为服务器使用的被动套接字,也就是监听套接字 while(1) { //循环监听是否有客户端连接请求到来 connSocket = accept(listenSocket);//接受客户端连接 recv(connSocket);//从客户端读取数据,只能同时处理一个客户端 send(connSocket);//给客户端返回数据,只能同时处理一个客户端 }
Anhand des obigen Codes können Sie jedoch möglicherweise feststellen, dass das Programm zwar die Kommunikation zwischen dem Server und dem Client erreichen kann, das Programm jedoch bei jedem Aufruf der Akzeptanzfunktion nur eine Clientverbindung verarbeiten kann. Wenn wir also mehrere gleichzeitige Client-Anfragen bearbeiten möchten, müssen wir Multithreading verwenden, um Anfragen auf mehreren Client-Verbindungen zu verarbeiten, die über die Akzeptanzfunktion hergestellt werden.
Nachdem wir diese Methode verwendet haben, müssen wir einen Thread erstellen, nachdem die Akzeptanzfunktion den verbundenen Socket zurückgegeben hat, und den verbundenen Socket an den erstellten Thread übergeben. Dieser Thread ist für die anschließende Verarbeitung des Datenlesens und -schreibens verantwortlich. Gleichzeitig ruft der serverseitige Ausführungsprozess erneut die Akzeptanzfunktion auf und wartet auf die nächste Clientverbindung.
Multithreading:
listenSocket = socket(); //调用socket系统调用创建一个主动套接字 bind(listenSocket); //绑定地址和端口 listen(listenSocket); //将默认的主动套接字转换为服务器使用的被动套接字,也就是监听套接字 while(1) { //循环监听是否有客户端连接请求到来 connSocket = accept(listenSocket);//接受客户端连接 pthread_create(processData, connSocket);//创建新线程对已连接套接字进行处理 } processData(connSocket){ recv(connSocket);//从客户端读取数据,只能同时处理一个客户端 send(connSocket);//给客户端返回数据,只能同时处理一个客户端 }
Obwohl diese Methode die gleichzeitige Verarbeitungsfähigkeit des Servers verbessern kann, wird der Hauptausführungsprozess von Redis von einem Thread ausgeführt, und Multithreading kann nicht zur Verbesserung der gleichzeitigen Verarbeitungsfähigkeit verwendet werden. Daher funktioniert diese Methode nicht für Redis.
Gibt es andere Methoden, die Redis dabei helfen können, die Verarbeitungsfähigkeiten gleichzeitiger Clients zu verbessern? Dies erfordert die Nutzung der vom Betriebssystem bereitgestellten IO-Multiplexing-Funktion. Im grundlegenden Socket-Programmiermodell kann die Accept-Funktion nur auf Client-Verbindungen an einem Listening-Socket lauschen, und die Recv-Funktion kann nur auf Anforderungen warten, die vom Client an einem verbundenen Socket gesendet werden.
Da das Linux-Betriebssystem in praktischen Anwendungen weit verbreitet ist, untersuchen wir in dieser Lektion hauptsächlich den IO-Multiplexing-Mechanismus unter Linux. Select, Poll und Epoll sind die drei Hauptformen des von Linux bereitgestellten E/A-Multiplexmechanismus. Als nächstes lernen wir die Implementierungsideen und Verwendungsmethoden dieser drei Mechanismen kennen. Lassen Sie uns als Nächstes untersuchen, warum Redis häufig den Epoll-Mechanismus zur Implementierung der Netzwerkkommunikation verwendet.
Lassen Sie uns zunächst das Programmiermodell des Auswahlmechanismus verstehen.
Aber bevor wir im Detail lernen, müssen wir wissen, welche Schlüsselpunkte wir für einen IO-Multiplexing-Mechanismus beherrschen müssen. Dies kann uns helfen, die Zusammenhänge und Unterschiede zwischen verschiedenen Mechanismen schnell zu verstehen. Wenn wir den E/A-Multiplexmechanismus erlernen, müssen wir tatsächlich in der Lage sein, die folgenden Fragen zu beantworten: Erstens, auf welche Ereignisse am Socket wird der Multiplexmechanismus hören? Zweitens: Wie viele Sockets kann der Multiplexmechanismus abhören? Drittens: Wie findet der Multiplexmechanismus den bereiten Socket, wenn ein Socket bereit ist?
Eine wichtige Funktion im Auswahlmechanismus ist die Auswahlfunktion. Zu den Parametern der Select-Funktion gehören die Anzahl der überwachten Dateideskriptoren __nfds, die drei Sammlungen der überwachten Deskriptoren readfds, writefds,exclusedfds und das Timeout-Timeout zum Blockieren des Wartens während der Überwachung. Funktionsprototyp auswählen:
int select(int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)
Was Sie hier beachten müssen, ist, dass Linux für jeden Socket einen Dateideskriptor hat, der eine nicht negative Ganzzahl ist, die zur eindeutigen Identifizierung des Sockets verwendet wird. Unter Linux ist es üblich, Dateideskriptoren als Argumente in Funktionen des Multiplexmechanismus zu verwenden. Die Funktion findet den entsprechenden Socket über den Dateideskriptor, um Vorgänge wie Überwachung, Lesen und Schreiben zu implementieren.
Die drei Parameter der Auswahlfunktion geben den Satz von Dateideskriptoren an, die überwacht werden müssen, was tatsächlich den Satz von Sockets darstellt, die überwacht werden müssen. Warum gibt es also drei Sets?
关于刚才提到的第一个问题,即多路复用机制监听的套接字事件有哪些。select 函数使用三个集合,表示监听的三类事件,分别是读数据事件,写数据事件,异常事件。
我们进一步可以看到,参数 readfds、writefds 和 exceptfds 的类型是 fd_set 结构体,它主要定义部分如下所示。其中,fd_mask类型是 long int 类型的别名,__FD_SETSIZE 和 __NFDBITS 这两个宏定义的大小默认为 1024 和 32。
所以,fd_set 结构体的定义,其实就是一个 long int 类型的数组,该数组中一共有 32 个元素(1024/32=32),每个元素是 32 位(long int 类型的大小),而每一位可以用来表示一个文件描述符的状态。了解了 fd_set 结构体的定义,我们就可以回答刚才提出的第二个问题了。每个描述符集合都可以被 select 函数监听 1024 个描述符。
首先,我们在调用 select 函数前,可以先创建好传递给 select 函数的描述符集合,然后再创建监听套接字。而为了让创建的监听套接字能被 select 函数监控,我们需要把这个套接字的描述符加入到创建好的描述符集合中。
接下来,我们可以使用 select 函数并传入已创建的描述符集合作为参数。程序在调用 select 函数后,会发生阻塞。一旦 select 函数检测到有就绪的描述符,会立即终止阻塞并返回已就绪的文件描述符数。
那么此时,我们就可以在描述符集合中查找哪些描述符就绪了。然后,我们对已就绪描述符对应的套接字进行处理。比如,如果是 readfds 集合中有描述符就绪,这就表明这些就绪描述符对应的套接字上,有读事件发生,此时,我们就在该套接字上读取数据。
而因为 select 函数一次可以监听 1024 个文件描述符的状态,所以 select 函数在返回时,也可能会一次返回多个就绪的文件描述符。我们可以使用循环处理流程,对每个就绪描述符对应的套接字依次进行读写或异常处理操作。
select函数有两个不足
首先,select 函数对单个进程能监听的文件描述符数量是有限制的,它能监听的文件描述符个数由 __FD_SETSIZE 决定,默认值是 1024。
其次,当 select 函数返回后,我们需要遍历描述符集合,才能找到具体是哪些描述符就绪了。这个遍历过程会产生一定开销,从而降低程序的性能。
poll 机制的主要函数是 poll 函数,我们先来看下它的原型定义,如下所示:
int poll(struct pollfd *__fds, nfds_t __nfds, int __timeout)
其中,参数 *__fds 是 pollfd 结构体数组,参数 __nfds 表示的是 *__fds 数组的元素个数,而 __timeout 表示 poll 函数阻塞的超时时间。
pollfd 结构体里包含了要监听的描述符,以及该描述符上要监听的事件类型。从 pollfd 结构体的定义中,我们可以看出来这一点,具体如下所示。pollfd 结构体中包含了三个成员变量 fd、events 和 revents,分别表示要监听的文件描述符、要监听的事件类型和实际发生的事件类型。
pollfd 结构体中要监听和实际发生的事件类型,是通过以下三个宏定义来表示的,分别是 POLLRDNORM、POLLWRNORM 和 POLLERR,它们分别表示可读、可写和错误事件。
了解了 poll 函数的参数后,我们来看下如何使用 poll 函数完成网络通信。这个流程主要可以分成三步:
第一步,创建 pollfd 数组和监听套接字,并进行绑定;
第二步,将监听套接字加入 pollfd 数组,并设置其监听读事件,也就是客户端的连接请求;
第三步,循环调用 poll 函数,检测 pollfd 数组中是否有就绪的文件描述符。
而在第三步的循环过程中,其处理逻辑又分成了两种情况:
如果是连接套接字就绪,这表明是有客户端连接,我们可以调用 accept 接受连接,并创建已连接套接字,并将其加入 pollfd 数组,并监听读事件;
如果是已连接套接字就绪,这表明客户端有读写请求,我们可以调用 recv/send 函数处理读写请求。
其实,和 select 函数相比,poll 函数的改进之处主要就在于,它允许一次监听超过 1024 个文件描述符。但是当调用了 poll 函数后,我们仍然需要遍历每个文件描述符,检测该描述符是否就绪,然后再进行处理。
Zunächst verwendet der epoll-Mechanismus die epoll_event-Struktur, um die zu überwachenden Dateideskriptoren und die zu überwachenden Ereignistypen aufzuzeichnen. Dies ähnelt der im Poll-Mechanismus verwendeten pollfd-Struktur.
Für die epoll_event-Struktur enthält sie also die Unionsvariable epoll_data_t und die Ereignisvariable vom Typ Integer. In der epoll_data_t-Variable gibt es eine Mitgliedsvariable fd, die Dateideskriptoren aufzeichnet, und die Ereignisvariable nimmt verschiedene Makrodefinitionswerte an, um die Ereignistypen darzustellen, um die es in den Dateideskriptoren in der epoll_data_t-Variablen geht Zu den Ereignistypen gehören die folgenden Typen.
EPOLLIN: Leseereignis, das angibt, dass der Socket, der dem Dateideskriptor entspricht, Daten zum Lesen hat.
EPOLLOUT: Schreibereignis, das angibt, dass der Socket, der dem Dateideskriptor entspricht, Daten zum Schreiben hat.
EPOLLERR: Fehlerereignis, das darauf hinweist, dass der Dateideskriptor für den Socket falsch ist.
Wenn wir die Select- oder Poll-Funktion verwenden, können wir nach dem Erstellen des Dateideskriptorsatzes oder Pollfd-Arrays die Dateideskriptoren, die wir überwachen müssen, zum Array hinzufügen.
Aber für den Epoll-Mechanismus müssen wir zuerst die Funktion epoll_create aufrufen, um eine Epoll-Instanz zu erstellen. Diese Epoll-Instanz verwaltet intern zwei Strukturen, die zu überwachende Dateideskriptoren und fertige Dateideskriptoren aufzeichnen und zur Verarbeitung an das Benutzerprogramm zurückgegeben werden.
Wenn wir also den Epoll-Mechanismus verwenden, müssen wir nicht wie bei Select und Poll durchlaufen und abfragen, welche Dateideskriptoren bereit sind. Daher ist Epoll effizienter als Select und Poll.
Nach dem Erstellen der Epoll-Instanz müssen wir die Funktion epoll_ctl verwenden, um den Listening-Ereignistyp zum überwachten Dateideskriptor hinzuzufügen, und die Funktion epoll_wait verwenden, um den fertigen Dateideskriptor abzurufen.
Jetzt verstehen wir, wie man die Epoll-Funktion verwendet. Gerade weil epoll die Anzahl der überwachten Deskriptoren anpassen und fertige Deskriptoren direkt zurückgeben kann, basiert Redis beim Entwerfen und Implementieren des Netzwerkkommunikationsframeworks auf Funktionen wie epoll_create, epoll_ctl und epoll_wait im Epoll-Mechanismus Schreibereignisse wurden gekapselt und entwickelt, um ein ereignisgesteuertes Framework für die Netzwerkkommunikation zu implementieren, sodass Redis zwar in einem einzelnen Thread ausgeführt wird, aber dennoch den Clientzugriff mit hoher Parallelität effizient verarbeiten kann.
Das Reactor-Modell ist ein Programmiermodell, das vom Netzwerkserver zur Verarbeitung von Netzwerk-E/A-Anfragen mit hoher Parallelität verwendet wird:
Drei Arten von Verarbeitungsereignissen, nämlich Verbindungsereignisse, schreiben Ereignisse und Ereignisse lesen ;
Drei Schlüsselrollen, nämlich Reaktor, Akzeptor und Handler.
Das Reactor-Modell befasst sich mit dem Interaktionsprozess zwischen Client und Server. Diese drei Ereignistypen entsprechen den ausstehenden Ereignissen, die durch verschiedene Arten von Anforderungen auf der Serverseite während der Interaktion zwischen Client und Server ausgelöst werden:
Wenn ein Client mit dem Server interagieren möchte, sendet der Client eine Verbindungsanforderung an den Server, um eine Verbindung herzustellen, was einem Verbindungsereignis auf dem Server entspricht. Sobald die Verbindung hergestellt ist, sendet der Client eine Anfrage an den Server senden. Wenn der Server eine Leseanforderung verarbeitet, muss er Daten an den Client zurückschreiben, was dem serverseitigen Schreibereignis entspricht
Unabhängig davon, ob der Client eine Lese- oder Schreibanforderung an den Server sendet, muss der Server lesen der Anforderungsinhalt vom Client, daher entspricht hier die Lese- oder Schreibanforderung dem Leseereignis auf der Serverseite Zum Empfangen der Verbindung wird nach dem Empfangen der Verbindung ein Handler für die Verarbeitung nachfolgender Lese- und Schreibereignisse auf der Netzwerkverbindung erstellt. Zweitens werden Lese- und Schreibereignisse vom Handler verarbeitet In Szenarien mit hoher Parallelität treten Verbindungsereignisse sowie Lese- und Schreibereignisse gleichzeitig auf. Daher benötigen wir eine Rolle, die Ereignisse speziell abhört und verteilt, nämlich die Reaktorrolle. Wenn eine Verbindungsanforderung vorliegt, übergibt der Reaktor das generierte Verbindungsereignis zur Verarbeitung an den Akzeptor. Wenn eine Lese- oder Schreibanforderung vorliegt, übergibt der Reaktor die Lese- und Schreibereignisse zur Verarbeitung an den Handler.
Da wir nun wissen, dass diese drei Rollen rund um die Überwachung, Weiterleitung und Verarbeitung von Ereignissen interagieren, wie realisieren wir dann das Zusammenspiel dieser drei beim Programmieren? Dies ist untrennbar mit dem Eventfahren verbunden.
Die Ereignisinitialisierung wird beim Start des Serverprogramms ausgeführt. Seine Hauptfunktion besteht darin, den zu überwachenden Ereignistyp und den diesem Ereignistyp entsprechenden Handler zu erstellen. Sobald der Server die Initialisierung abgeschlossen hat, ist die Ereignisinitialisierung entsprechend abgeschlossen und das Serverprogramm muss in die Hauptschleife der Ereigniserfassung, -verteilung und -verarbeitung eintreten.
Verwenden Sie eine While-Schleife als Hauptschleife. Dann müssen wir in dieser Hauptschleife das aufgetretene Ereignis erfassen, den Ereignistyp bestimmen und basierend auf dem Ereignistyp den während der Initialisierung erstellten Ereignishandler aufrufen, um das Ereignis tatsächlich zu verarbeiten.
Wenn beispielsweise ein Verbindungsereignis auftritt, muss das Serverprogramm die Akzeptorverarbeitungsfunktion aufrufen, um eine Verbindung mit dem Client herzustellen. Wenn ein Leseereignis auftritt, bedeutet dies, dass eine Lese- oder Schreibanforderung an den Server gesendet wurde. Das Serverprogramm ruft eine bestimmte Anforderungsverarbeitungsfunktion auf, um den Anforderungsinhalt von der Clientverbindung zu lesen und dadurch die Verarbeitung des Leseereignisses abzuschließen.
Der grundlegende Arbeitsmechanismus des Reactor-Modells: Verschiedene Arten von Anforderungen vom Client lösen drei Arten von Ereignissen aus: Verbindung, Lesen und Schreiben auf der Serverseite Diese drei Arten von Ereignissen werden von Reaktoren und Akzeptoren ausgeführt, drei Arten von Rollen, und diese drei Arten von Rollen implementieren dann die Interaktion und Ereignisverarbeitung über das ereignisgesteuerte Framework.
Das obige ist der detaillierte Inhalt vonWas ist das ereignisgesteuerte Modell von Redis?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!