ソケット モデルを使用してネットワーク通信を実装する場合、ソケットの作成、ポートのリッスン、接続の処理、リクエストの読み取りと書き込みなど、複数の手順を実行する必要があります。これらのステップの主要な操作は、ソケット モデルの欠陥を分析するのに役立ちます。
まず、サーバーとクライアントが通信できるようにする必要がある場合、次の 3 つの手順に従って、サーバー側でクライアント接続をリッスンするリスニング ソケット (リスニング ソケット) を作成できます。
listenSocket = socket(); //调用socket系统调用创建一个主动套接字 bind(listenSocket); //绑定地址和端口 listen(listenSocket); //将默认的主动套接字转换为服务器使用的被动套接字,也就是监听套接字 while(1) { //循环监听是否有客户端连接请求到来 connSocket = accept(listenSocket);//接受客户端连接 recv(connSocket);//从客户端读取数据,只能同时处理一个客户端 send(connSocket);//给客户端返回数据,只能同时处理一个客户端 }
listenSocket = socket(); //调用socket系统调用创建一个主动套接字 bind(listenSocket); //绑定地址和端口 listen(listenSocket); //将默认的主动套接字转换为服务器使用的被动套接字,也就是监听套接字 while(1) { //循环监听是否有客户端连接请求到来 connSocket = accept(listenSocket);//接受客户端连接 pthread_create(processData, connSocket);//创建新线程对已连接套接字进行处理 } processData(connSocket){ recv(connSocket);//从客户端读取数据,只能同时处理一个客户端 send(connSocket);//给客户端返回数据,只能同时处理一个客户端 }
int select(int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)
关于刚才提到的第一个问题,即多路复用机制监听的套接字事件有哪些。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 函数后,我们仍然需要遍历每个文件描述符,检测该描述符是否就绪,然后再进行处理。
まず、epoll メカニズムは epoll_event 構造体を使用して、監視対象のファイル記述子と監視対象のイベント タイプを記録します。これは、ポーリング メカニズムで使用される pollfd 構造体に似ています。 。
したがって、epoll_event 構造体の場合、epoll_data_t 共用体変数と整数型の events 変数が含まれます。ファイル記述子を記録する epoll_data_t 共用体にはメンバー変数 fd があり、イベント変数は、epoll_data_t 変数内のファイル記述子が関係するイベント タイプを表すさまざまなマクロ定義値を受け取ります。イベントの種類には次の種類があります。
EPOLLIN: ファイル記述子に対応するソケットに読み取るデータがあることを示す読み取りイベント。
EPOLLOUT: ファイル記述子に対応するソケットに書き込むデータがあることを示す書き込みイベント。
EPOLLERR: ファイル記述子がソケットに対して間違っていることを示すエラー イベント。
select 関数またはpoll関数を使用する場合、ファイル記述子セットまたはpollfd配列を作成した後、監視する必要があるファイル記述子を配列に追加できます。
ただし、epoll メカニズムの場合は、最初に epoll_create 関数を呼び出して epoll インスタンスを作成する必要があります。この epoll インスタンスは、監視対象のファイル記述子と準備完了ファイル記述子を記録する 2 つの構造を内部的に保持しており、準備完了ファイル記述子については、処理のためにユーザー プログラムに返されます。
したがって、epoll メカニズムを使用する場合、選択とポーリングを使用する場合のように、どのファイル記述子が準備ができているかを走査してクエリする必要はありません。したがって、epoll は選択してポーリングするよりも効率的です。
epoll インスタンスを作成した後、epoll_ctl 関数を使用して監視対象のファイル記述子にリスニング イベント タイプを追加し、epoll_wait 関数を使用して準備ができたファイル記述子を取得する必要があります。
これで、epoll 関数の使用方法がわかりました。実際、epoll は監視対象の記述子の数をカスタマイズし、準備完了の記述子を直接返すことができるため、Redis がネットワーク通信フレームワークを設計および実装する際には、epoll メカニズムの epoll_create、epoll_ctl、epoll_wait などの関数に基づいています。書き込みイベントはカプセル化され、ネットワーク通信用のイベント駆動フレームワークを実装するために開発されているため、Redis はシングル スレッドで実行されますが、同時実行性の高いクライアント アクセスを効率的に処理できます。
Reactor モデルは、ネットワーク サーバーが同時実行性の高いネットワーク IO リクエストを処理するために使用するプログラミング モデルです。モデルの機能:
3 種類の処理イベント (接続イベント、書き込みイベント、読み取りイベント)、
3 つの主要な役割 (リアクター、アクセプター、およびハンドラー)。
Reactor モデルは、クライアントとサーバー間の対話プロセスを扱います。これらの 3 種類のイベントは、クライアントとサーバー間の対話中にサーバー側でトリガーされるさまざまな種類のリクエストに対応します。保留中のイベント:
クライアントがサーバーと対話したい場合、クライアントはサーバーに接続要求を送信して、リンクに相当する接続を確立します。イベント
接続が確立されると、クライアントはデータを読み取るために読み取りリクエストをサーバーに送信します。サーバーが読み取りリクエストを処理するときは、サーバー側の書き込みイベントに対応するデータをクライアントに書き戻す必要があります。
クライアントが読み取りまたは書き込みリクエストを送信するかどうかは関係ありません。サーバー、サーバー リクエストの内容はクライアントから読み取る必要があるため、ここでは読み取りまたは書き込みリクエストがサーバー側の読み取りイベントに対応します
3 つの主要な役割:
まず、接続イベントは接続の受信を担当するアクセプターによって処理されます。アクセプターは接続を受信した後、後続の読み取りおよび書き込みイベントを処理するためのハンドラーを作成します。ネットワーク接続;
2 番目に、読み取りおよび書き込みイベントはハンドラーによって処理されます;
最後に、同時実行性の高いシナリオでは、接続イベントと読み取りイベントと書き込みイベントは同時に発生するため、イベントの監視と配布に特化したロールが必要です。これがリアクターのロールです。接続リクエストがある場合、リアクターは生成された接続イベントを処理のためにアクセプターに渡します。読み取りまたは書き込みリクエストがある場合、リアクターは読み取りおよび書き込みイベントを処理のためにハンドラーに渡します。
では、これら 3 つの役割がイベントの監視、転送、処理に関して相互作用することがわかったので、プログラミング時にこれら 3 つをどのように実装できるでしょうか?相互作用についてはどうすればよいでしょうか?これはイベントの運転と切り離せないものです。
Reactor モデルを実装する場合、記述する必要があるコード制御ロジック全体は、イベント駆動型フレームワークと呼ばれます。イベント駆動型フレームワークは、イベントの初期化と、イベントのキャプチャ、オフロード、および処理のメイン ループの 2 つの部分で構成されます。要するに。
イベントの初期化は、サーバー プログラムの起動時に実行され、その主な機能は、監視する必要があるイベントの種類と、この種類のイベントに対応するハンドラーを作成することです。サーバーが初期化を完了すると、それに応じてイベントの初期化も完了し、サーバー プログラムはイベントのキャプチャ、配信、および処理のメイン ループに入る必要があります。
while ループをメイン ループとして使用します。次に、このメイン ループで、発生したイベントをキャプチャし、イベント タイプを決定し、イベント タイプに基づいて、初期化中に作成されたイベント ハンドラーを呼び出して実際にイベントを処理する必要があります。
たとえば、接続イベントが発生すると、サーバー プログラムはアクセプター処理関数を呼び出してクライアントとの接続を作成する必要があります。読み取りイベントが発生すると、読み取りまたは書き込みリクエストがサーバーに送信されたことを示し、サーバー プログラムは特定のリクエスト処理関数を呼び出して、クライアント接続からリクエストの内容を読み取り、読み取りイベントの処理を完了します。
Reactor モデルの基本的な動作メカニズム: クライアントからのさまざまな種類のリクエストにより、サーバー側での接続、読み取り、書き込みという 3 種類のイベントがトリガーされます。これら 3 つの監視、配信、および処理イベントはリアクター、アクセプター、ハンドラーによって実行され、3 種類のロールによって完成され、これら 3 種類のロールによってイベント駆動フレームワークによるインタラクションとイベント処理が実装されます。
以上がRedis のイベント駆動モデルとは何ですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。