「I/O多重化（システムコールpoll();を使用）」を施したサーバを作成してみた。

# はじめに
どうも。こんにちは。
[42tokyo](https://42tokyo.jp/)といったところで、ソケットプログラミング？？を学んでいます。
前回は、[簡単なエコーサーバーを作成してみた。](https://qiita.com/gu-chi/items/1e2ba4e19902f9e39b5e)を書きました。

今回は、「I/O多重化」を施したサーバーを作成してみます。
「I/O多重化」を行うことで、複数のファイルディスクリプタを扱うことができます。
これによって、複数クライアントの接続を可能にします。

このサーバでは、クライアントが接続すると、サーバ側でメッセージを出力します。

使用した言語は、C++98です。
「I/O多重化」を実現するために使用したシステムコールは、[poll();](https://ja.manpages.org/poll/2)です。
[poll();](https://ja.manpages.org/poll/2)を使用することで、１プロセス、１スレッドで複数クライアントの接続を処理できるようになります。

間違い等があれば、ご指摘ください。
# コード
:::note warn
注意：このコードは、エラー処理が甘いです。バグを大いに含んでいます。
:::
```c++:Server.hpp
#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
```
```c++:Server.cpp
#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);
}
```

# 確認
:::note info
実行環境は、macOS Sonoma バージョン14.2.1です。
:::
コンパイルと実行
```shell:shell
~$ c++ -Wall -Wextra -Werror -std=c++98 -pedantic-errors Server.cpp -o server
~$ ./server&
```
何度か実行し複数のクライアントをサーバに接続させる。
```shell:shell
~$ curl -v telnet://127.0.0.1:8080 &
```
サーバを実行するプロセスが使用しているファイルディスクリプタを確認
```shell:shell
~$ 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)
```
簡単なテスト用シェルスクリプト

:::note warn
注意：　内容を理解した上で使用してください。
:::
```sh:test.sh
#!/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
](https://www.ohmsha.co.jp/book/9784274065194/)
シェルコマンドの使い方を参考にしました。

https://bati11blog.hatenablog.com/entry/2017/03/18/215653