OpenSSLとselect

OpenSSL を使用しているときに複数のソケットの入力状況を管理する場合,通常の socket と同じように select や epoll を用いて制御を行うとうまくいかないときがあります.

SSL のような暗号通信の場合、送信は「垂れ流し」では済まず、ハンドシェークを行なう必要があるからだが、この時、受信しようと思っていなかったデータ、つまり通信相手が送信したデータまで読み込んでしまう場合がある。

すると、SSL_write を呼んでいるのに、 OpenSSL の受信バッファに、意図せずデータが溜まってしまう。こうなってしまうと、select(2) や epoll(2) では検知できない。 select(2) や epoll(2) は、 I/O レベルでの受信データの有無を調べるシステムコールであり、それより上のレベルである OpenSSL ライブラリの受信バッファのことは関知しないからだ。

仙石浩明の日記: SSL_pending

したがって,select で入力にパケットが到着しているかどうかを判定し,パケットが到着していれば対応する socket に受信操作を行わせると言う制御を行う場合,select で判定すると同時にそれぞれの (SSL) socket の受信バッファにデータが存在しているかどうかを判定する必要があります.コードにすると以下のような形になります.

fd_set sysrfds;
FD_ZERO(&sysrfds);

/*
 * rfds_ は [socket, handler] の map.
 * socket: ソケットクラス.socket() メソッドでディスクリプタを取得している.
 * handler: 入力にパケットが到着したときに行う処理.
 */
for (iterator pos = rfds_.begin(); pos != rfds_.end(); ++pos) {
    FD_SET(pos->first->socket(), &sysrfds);
}

struct timeval tv;
tv.tv_sec = timeout; // 適当なタイムアウト時間
tv.tv_usec = 0;

int n = select(FD_SETSIZE, &sysrfds, NULL, NULL, &tv);
if (n < 0) throw std::runtime_error("select");

for (iterator pos = rfds_.begin(); pos != rfds_.end(); ++pos) {
    if (SSL_pending(pos->ssl()) > 0 || FD_ISSET(pos->socket(), &sysrfds)) {
        pos->second->run(*pos->first);
    }
}

上記のような形で,SSL ソケットで複数のソケットの入力状況を監視するクラスが ssl::sockmanager となります.