はじめに
どうも。こんにちは。
42tokyoといったところで、ソケットプログラミング??を学んでいます。
前回は、簡単なエコーサーバーを作成してみた。を書きました。
今回は、「I/O多重化」を施したサーバーを作成してみます。
「I/O多重化」を行うことで、複数のファイルディスクリプタを扱うことができます。
これによって、複数クライアントの接続を可能にします。
このサーバでは、クライアントが接続すると、サーバ側でメッセージを出力します。
使用した言語は、C++98です。
「I/O多重化」を実現するために使用したシステムコールは、poll();です。
poll();を使用することで、1プロセス、1スレッドで複数クライアントの接続を処理できるようになります。
間違い等があれば、ご指摘ください。
コード
注意:このコードは、エラー処理が甘いです。バグを大いに含んでいます。
#ifndef SERVER_HPP
# define SERVER_HPP
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <poll.h>
#include <iostream>
#include <cstring>
class Server {
private:
int socketFd_;
socklen_t socketAddressLen_; // socklen_tはコンパイラによってエラー。intの場合もある。
const int maxClients_;
struct sockaddr_in socketAddress_;
struct pollfd fds_[6];
public:
explicit Server(unsigned short port);
~Server();
void run();
};
#endif // SERVER_HPP
#include "./Server.hpp"
static void errorExit(const std::string &title) {
std::cerr << title << strerror(errno) << std::endl;
exit(EXIT_FAILURE);
}
Server::Server(unsigned short port) :
socketFd_(0), socketAddressLen_(sizeof(this->socketAddress_)), maxClients_(5) {
// サーバーソケットの作成
this->socketFd_ = socket(AF_INET, SOCK_STREAM, 0);
if (this->socketFd_ < 0) {
errorExit("socket: ");
}
this->socketAddress_.sin_family = AF_INET;
this->socketAddress_.sin_addr.s_addr = INADDR_ANY;
this->socketAddress_.sin_port = htons(port);
// サーバーソケットをアドレスにバインド
if (bind(this->socketFd_, reinterpret_cast<struct sockaddr *>(&this->socketAddress_), sizeof(this->socketAddress_)) < 0) {
errorExit("bind: ");
}
// サーバーソケットをリスニング
if (listen(this->socketFd_, 3) < 0) {
errorExit("listen: ");
}
std::cout << "サーバーが " << port << " ポートでリスニングしています..." << std::endl;
// サーバーソケットを初期化
this->fds_[0].fd = this->socketFd_;
this->fds_[0].events = POLLIN;
// 各クライアントに対応したサーバーソケットを保存する配列を初期化
for (int i = 1; i <= this->maxClients_; ++i) {
this->fds_[i].fd = -1;
this->fds_[i].events = POLLIN;
}
}
Server::~Server() {
close(this->socketFd_);
}
void Server::run() {
while (1) {
// poll()を使用して待機
int result = poll(this->fds_, this->maxClients_ + 1, -1);
int newSocket = -1;
if (result == -1) {
errorExit("poll: ");
}
if (result == 0) {
continue;
}
// サーバーソケットに新しい接続があるか確認
if (this->fds_[0].revents & POLLIN) {
if ((newSocket = accept(this->socketFd_, reinterpret_cast<struct sockaddr *>(&this->socketAddress_), &this->socketAddressLen_)) < 0) {
close(newSocket);
errorExit("accept: ");
}
// 新しいクライアントソケットを検出し、fdsに追加
for (int i = 1; i <= this->maxClients_; ++i) {
if (this->fds_[i].fd == -1) {
this->fds_[i].fd = newSocket;
std::cout << "新しいクライアントが接続しました。ソケット " << newSocket << std::endl;
break;
}
}
}
}
}
int main() {
Server Server(8080);
Server.run();
return (0);
}
確認
実行環境は、macOS Sonoma バージョン14.2.1です。
コンパイルと実行
~$ c++ -Wall -Wextra -Werror -std=c++98 -pedantic-errors Server.cpp -o server
~$ ./server&
何度か実行し複数のクライアントをサーバに接続させる。
~$ curl -v telnet://127.0.0.1:8080 &
サーバを実行するプロセスが使用しているファイルディスクリプタを確認
~$ lsof -i:8080
出力例
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 11958 USER 3u IPv4 0x9c94c1832eab7691 0t0 TCP *:http-alt (LISTEN)
server 11958 USER 4u IPv4 0x9c94c1832eab53f9 0t0 TCP localhost:http-alt->localhost:50840 (ESTABLISHED)
server 11958 USER 5u IPv4 0x9c94c1832eab8219 0t0 TCP localhost:http-alt->localhost:50841 (ESTABLISHED)
server 11958 USER 6u IPv4 0x9c94c1832eabb039 0t0 TCP localhost:http-alt->localhost:50842 (ESTABLISHED)
server 11958 USER 7u IPv4 0x9c94c1832f4ff691 0t0 TCP localhost:http-alt->localhost:50843 (ESTABLISHED)
curl 11960 USER 6u IPv4 0x9c94c1832eab6b09 0t0 TCP localhost:50840->localhost:http-alt (ESTABLISHED)
curl 11962 USER 6u IPv4 0x9c94c1832f501929 0t0 TCP localhost:50841->localhost:http-alt (ESTABLISHED)
curl 11964 USER 6u IPv4 0x9c94c1832eaba4b1 0t0 TCP localhost:50842->localhost:http-alt (ESTABLISHED)
curl 11966 USER 6u IPv4 0x9c94c1832eab5f81 0t0 TCP localhost:50843->localhost:http-alt (ESTABLISHED)
簡単なテスト用シェルスクリプト
注意: 内容を理解した上で使用してください。
#!/bin/sh
c++ -Wall -Wextra -Werror -std=c++98 -pedantic-errors Server.cpp -o server
./server&
server_pid=$!
for i in {0..4}
do
sleep 1
curl -v telnet://127.0.0.1:8080 2>> result &
done
cat result
lsof -i:8080
sleep 1
kill $server_pid
rm result server
最後に
処理の流れを把握するのに、少し時間が掛かりました。
シェルスクリプトを書いて、テストの実行を簡略化させることができ嬉しかったです。
次回は、ノンブロッキングなファイルディスクリプタを用いて「I/O多重化」を施すか、
「I/O多重化」を施したエコーサーバを作成したいと思います。
ありがとうございました。
参考
本書のコード(システムコールselect();を使用)を参考に実装しました。
(本書のソースコードはサイトからダウンロードできます。)
『TCP/IP ソケットプログラミング C言語編』5.5節 多重化 P.108 ~ 113
シェルコマンドの使い方を参考にしました。