第3章:サーバーアーキテクチャ
はじめに
ft_ircサーバーは、非ブロッキングI/Oとpoll()を使用して複数のクライアントを同時に処理します。本章では、サーバーの設計とイベントループの実装を学びます。
---
1. サーバークラス設計
1.1 メンバー変数
#include <poll.h>
#include <map>
#include <vector>
#include <string>
class Server {
private:
int _port;
std::string _password;
int _serverFd;
std::string _serverName;
std::vector<struct pollfd> _pollfds;
std::map<int, Client*> _clients; // fd → Client
std::map<std::string, Client*> _nicknames; // nickname → Client
std::map<std::string, Channel*> _channels; // channel name → Channel
bool _running;
public:
Server(int port, const std::string& password);
~Server();
void run();
void stop();
// クライアント管理
void addClient(int fd);
void removeClient(int fd);
Client* getClient(int fd);
Client* getClientByNick(const std::string& nick);
// チャンネル管理
Channel* getChannel(const std::string& name);
Channel* createChannel(const std::string& name);
void removeChannel(const std::string& name);
// メッセージ送信
void sendToClient(int fd, const std::string& msg);
void sendToChannel(Channel* channel, const std::string& msg,
Client* exclude = NULL);
void broadcast(const std::string& msg);
// アクセサ
const std::string& getPassword() const { return _password; }
const std::string& getServerName() const { return _serverName; }
};
1.2 コンストラクタ
Server::Server(int port, const std::string& password)
: _port(port),
_password(password),
_serverFd(-1),
_serverName("irc.localhost"),
_running(false) {
// サーバーソケット作成
_serverFd = socket(AF_INET, SOCK_STREAM, 0);
if (_serverFd < 0) {
throw std::runtime_error("Failed to create socket");
}
// SO_REUSEADDR設定
int opt = 1;
if (setsockopt(_serverFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
close(_serverFd);
throw std::runtime_error("Failed to set socket options");
}
// 非ブロッキング設定
if (fcntl(_serverFd, F_SETFL, O_NONBLOCK) < 0) {
close(_serverFd);
throw std::runtime_error("Failed to set non-blocking");
}
// バインド
struct sockaddr_in addr;
std::memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(_port);
if (bind(_serverFd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
close(_serverFd);
throw std::runtime_error("Failed to bind to port " +
std::to_string(_port));
}
// リッスン
if (listen(_serverFd, SOMAXCONN) < 0) {
close(_serverFd);
throw std::runtime_error("Failed to listen");
}
// サーバーソケットをpollfdに追加
struct pollfd pfd;
pfd.fd = _serverFd;
pfd.events = POLLIN;
pfd.revents = 0;
_pollfds.push_back(pfd);
std::cout << "Server listening on port " << _port << std::endl;
}
Server::~Server() {
// クライアントを削除
for (std::map<int, Client*>::iterator it = _clients.begin();
it != _clients.end(); ++it) {
close(it->first);
delete it->second;
}
// チャンネルを削除
for (std::map<std::string, Channel*>::iterator it = _channels.begin();
it != _channels.end(); ++it) {
delete it->second;
}
// サーバーソケットを閉じる
if (_serverFd >= 0) {
close(_serverFd);
}
}
---
2. イベントループ
2.1 メインループ
void Server::run() {
_running = true;
while (_running) {
// poll()でイベント待機
int ready = poll(_pollfds.data(), _pollfds.size(), -1);
if (ready < 0) {
if (errno == EINTR) {
continue; // シグナルで中断
}
throw std::runtime_error("poll() failed");
}
// イベント処理
for (size_t i = 0; i < _pollfds.size(); i++) {
if (_pollfds[i].revents == 0) {
continue;
}
if (_pollfds[i].fd == _serverFd) {
// 新規接続
handleNewConnection();
} else {
// クライアントイベント
handleClientEvent(i);
}
}
}
}
void Server::stop() {
_running = false;
}
2.2 新規接続処理
void Server::handleNewConnection() {
struct sockaddr_in clientAddr;
socklen_t addrLen = sizeof(clientAddr);
int clientFd = accept(_serverFd, (struct sockaddr*)&clientAddr, &addrLen);
if (clientFd < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
std::cerr << "accept() failed: " << strerror(errno) << std::endl;
}
return;
}
// 非ブロッキング設定
if (fcntl(clientFd, F_SETFL, O_NONBLOCK) < 0) {
std::cerr << "Failed to set non-blocking" << std::endl;
close(clientFd);
return;
}
// クライアントを追加
addClient(clientFd);
// ホスト名を取得
char hostBuffer[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &clientAddr.sin_addr, hostBuffer, sizeof(hostBuffer));
Client* client = _clients[clientFd];
client->setHostname(hostBuffer);
std::cout << "New connection from " << hostBuffer
<< " (fd: " << clientFd << ")" << std::endl;
}
void Server::addClient(int fd) {
// Clientオブジェクト作成
Client* client = new Client(fd);
_clients[fd] = client;
// pollfdに追加
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
_pollfds.push_back(pfd);
}
2.3 クライアントイベント処理
void Server::handleClientEvent(size_t index) {
int fd = _pollfds[index].fd;
short revents = _pollfds[index].revents;
Client* client = _clients[fd];
if (!client) {
return;
}
// エラーまたは切断
if (revents & (POLLERR | POLLHUP | POLLNVAL)) {
handleClientDisconnect(fd);
return;
}
// 読み込み可能
if (revents & POLLIN) {
handleClientRead(fd);
}
// 書き込み可能
if (revents & POLLOUT) {
handleClientWrite(fd);
}
}
void Server::handleClientRead(int fd) {
Client* client = _clients[fd];
char buffer[512];
ssize_t bytesRead = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (bytesRead <= 0) {
if (bytesRead < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
return; // 一時的なエラー
}
handleClientDisconnect(fd);
return;
}
buffer[bytesRead] = '\\0';
// 受信バッファに追加
client->appendToRecvBuffer(buffer);
// 完全なメッセージを処理
std::string line;
while (client->extractLine(line)) {
processMessage(client, line);
}
}
void Server::handleClientWrite(int fd) {
Client* client = _clients[fd];
const std::string& buffer = client->getSendBuffer();
if (buffer.empty()) {
// 書き込み完了、POLLOUT を解除
clearWriteInterest(fd);
return;
}
ssize_t bytesSent = send(fd, buffer.c_str(), buffer.size(), 0);
if (bytesSent < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
handleClientDisconnect(fd);
}
return;
}
// 送信済みデータを削除
client->eraseSendBuffer(bytesSent);
if (client->getSendBuffer().empty()) {
clearWriteInterest(fd);
}
}
---
3. クライアント管理
3.1 Clientクラス
class Client {
private:
int _fd;
std::string _nickname;
std::string _username;
std::string _realname;
std::string _hostname;
std::string _recvBuffer;
std::string _sendBuffer;
bool _passReceived;
bool _registered;
std::set<Channel*> _channels;
public:
Client(int fd);
~Client();
// アクセサ
int getFd() const { return _fd; }
const std::string& getNickname() const { return _nickname; }
const std::string& getUsername() const { return _username; }
const std::string& getRealname() const { return _realname; }
const std::string& getHostname() const { return _hostname; }
void setNickname(const std::string& nick) { _nickname = nick; }
void setUsername(const std::string& user) { _username = user; }
void setRealname(const std::string& real) { _realname = real; }
void setHostname(const std::string& host) { _hostname = host; }
// 登録状態
bool isPassReceived() const { return _passReceived; }
bool isRegistered() const { return _registered; }
void setPassReceived() { _passReceived = true; }
void setRegistered() { _registered = true; }
// プレフィックス
std::string getPrefix() const {
return _nickname + "!" + _username + "@" + _hostname;
}
// バッファ操作
void appendToRecvBuffer(const std::string& data) {
_recvBuffer += data;
}
bool extractLine(std::string& line) {
size_t pos = _recvBuffer.find("\\r\\n");
if (pos == std::string::npos) {
pos = _recvBuffer.find("\\n"); // 一部クライアントは\\nのみ
if (pos == std::string::npos) {
return false;
}
}
line = _recvBuffer.substr(0, pos);
_recvBuffer.erase(0, pos + ((_recvBuffer[pos] == '\\r') ? 2 : 1));
return true;
}
void appendToSendBuffer(const std::string& data) {
_sendBuffer += data;
}
const std::string& getSendBuffer() const { return _sendBuffer; }
void eraseSendBuffer(size_t len) {
_sendBuffer.erase(0, len);
}
// チャンネル
void joinChannel(Channel* channel) { _channels.insert(channel); }
void leaveChannel(Channel* channel) { _channels.erase(channel); }
const std::set<Channel*>& getChannels() const { return _channels; }
};
3.2 切断処理
void Server::handleClientDisconnect(int fd) {
Client* client = _clients[fd];
if (!client) {
return;
}
std::cout << "Client disconnected: " << client->getNickname()
<< " (fd: " << fd << ")" << std::endl;
// チャンネルにQUITを通知
std::string quitMsg = ":" + client->getPrefix() + " QUIT :Connection closed\\r\\n";
std::set<Client*> notified;
const std::set<Channel*>& channels = client->getChannels();
for (std::set<Channel*>::iterator it = channels.begin();
it != channels.end(); ++it) {
Channel* channel = *it;
// チャンネルメンバーに通知
const std::set<Client*>& members = channel->getMembers();
for (std::set<Client*>::iterator mit = members.begin();
mit != members.end(); ++mit) {
if (*mit != client && notified.find(*mit) == notified.end()) {
sendToClient((*mit)->getFd(), quitMsg);
notified.insert(*mit);
}
}
// チャンネルからクライアントを削除
channel->removeMember(client);
// 空になったチャンネルを削除
if (channel->isEmpty()) {
removeChannel(channel->getName());
}
}
// ニックネームマップから削除
if (!client->getNickname().empty()) {
_nicknames.erase(client->getNickname());
}
// pollfdから削除
for (size_t i = 0; i < _pollfds.size(); i++) {
if (_pollfds[i].fd == fd) {
_pollfds.erase(_pollfds.begin() + i);
break;
}
}
// クライアントを削除
_clients.erase(fd);
close(fd);
delete client;
}
---
4. メッセージ送信
4.1 送信ヘルパー
void Server::sendToClient(int fd, const std::string& msg) {
Client* client = _clients[fd];
if (!client) {
return;
}
client->appendToSendBuffer(msg);
setWriteInterest(fd);
}
void Server::sendToChannel(Channel* channel, const std::string& msg,
Client* exclude) {
if (!channel) {
return;
}
const std::set<Client*>& members = channel->getMembers();
for (std::set<Client*>::iterator it = members.begin();
it != members.end(); ++it) {
if (*it != exclude) {
sendToClient((*it)->getFd(), msg);
}
}
}
void Server::broadcast(const std::string& msg) {
for (std::map<int, Client*>::iterator it = _clients.begin();
it != _clients.end(); ++it) {
if (it->second->isRegistered()) {
sendToClient(it->first, msg);
}
}
}
4.2 書き込み関心の管理
void Server::setWriteInterest(int fd) {
for (size_t i = 0; i < _pollfds.size(); i++) {
if (_pollfds[i].fd == fd) {
_pollfds[i].events |= POLLOUT;
break;
}
}
}
void Server::clearWriteInterest(int fd) {
for (size_t i = 0; i < _pollfds.size(); i++) {
if (_pollfds[i].fd == fd) {
_pollfds[i].events &= ~POLLOUT;
break;
}
}
}
---
5. コマンドディスパッチ
5.1 メッセージ処理
void Server::processMessage(Client* client, const std::string& line) {
if (line.empty()) {
return;
}
Message msg = MessageParser::parse(line);
std::cout << "[" << client->getFd() << "] " << line << std::endl;
// コマンドディスパッチ
if (msg.command == "PASS") {
cmdPass(client, msg);
} else if (msg.command == "NICK") {
cmdNick(client, msg);
} else if (msg.command == "USER") {
cmdUser(client, msg);
} else if (!client->isRegistered()) {
// 未登録クライアントは他のコマンドを実行できない
sendToClient(client->getFd(),
Reply::notRegistered(client->getNickname(), _serverName));
} else if (msg.command == "JOIN") {
cmdJoin(client, msg);
} else if (msg.command == "PART") {
cmdPart(client, msg);
} else if (msg.command == "PRIVMSG") {
cmdPrivmsg(client, msg);
} else if (msg.command == "KICK") {
cmdKick(client, msg);
} else if (msg.command == "INVITE") {
cmdInvite(client, msg);
} else if (msg.command == "TOPIC") {
cmdTopic(client, msg);
} else if (msg.command == "MODE") {
cmdMode(client, msg);
} else if (msg.command == "QUIT") {
cmdQuit(client, msg);
} else if (msg.command == "PING") {
cmdPing(client, msg);
} else {
sendToClient(client->getFd(),
Reply::unknownCommand(client->getNickname(), msg.command,
_serverName));
}
}
5.2 コマンドハンドラの宣言
// Server.hpp に追加
private:
// コマンドハンドラ
void cmdPass(Client* client, const Message& msg);
void cmdNick(Client* client, const Message& msg);
void cmdUser(Client* client, const Message& msg);
void cmdJoin(Client* client, const Message& msg);
void cmdPart(Client* client, const Message& msg);
void cmdPrivmsg(Client* client, const Message& msg);
void cmdKick(Client* client, const Message& msg);
void cmdInvite(Client* client, const Message& msg);
void cmdTopic(Client* client, const Message& msg);
void cmdMode(Client* client, const Message& msg);
void cmdQuit(Client* client, const Message& msg);
void cmdPing(Client* client, const Message& msg);
// ヘルパー
void welcomeClient(Client* client);
void sendChannelNames(Client* client, Channel* channel);
---
6. シグナル処理
6.1 SIGINT/SIGTERMハンドリング
#include <signal.h>
Server* g_server = NULL;
void signalHandler(int signum) {
(void)signum;
if (g_server) {
std::cout << "\\nShutting down..." << std::endl;
g_server->stop();
}
}
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " <port> <password>" << std::endl;
return 1;
}
int port = std::atoi(argv[1]);
std::string password = argv[2];
if (port <= 0 || port > 65535) {
std::cerr << "Invalid port number" << std::endl;
return 1;
}
if (password.empty()) {
std::cerr << "Password cannot be empty" << std::endl;
return 1;
}
try {
Server server(port, password);
g_server = &server;
// シグナルハンドラ設定
signal(SIGINT, signalHandler);
signal(SIGTERM, signalHandler);
server.run();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
---
7. デバッグとテスト
7.1 ログ出力
#define LOG_DEBUG 0
#define LOG_INFO 1
#define LOG_WARN 2
#define LOG_ERROR 3
int g_logLevel = LOG_INFO;
void log(int level, const std::string& msg) {
if (level < g_logLevel) {
return;
}
static const char* levelNames[] = {"DEBUG", "INFO", "WARN", "ERROR"};
time_t now = time(NULL);
struct tm* tm = localtime(&now);
char timeStr[64];
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", tm);
std::cerr << "[" << timeStr << "] "
<< "[" << levelNames[level] << "] "
<< msg << std::endl;
}
#define LOG_D(msg) log(LOG_DEBUG, msg)
#define LOG_I(msg) log(LOG_INFO, msg)
#define LOG_W(msg) log(LOG_WARN, msg)
#define LOG_E(msg) log(LOG_ERROR, msg)
7.2 テスト用スクリプト
#!/bin/bash
# test_irc.sh
PORT=6667
PASS="test"
# サーバーに接続してコマンド送信
(
sleep 0.5
echo "PASS $PASS"
echo "NICK TestBot"
echo "USER bot 0 * :Test Bot"
sleep 0.5
echo "JOIN #test"
sleep 0.5
echo "PRIVMSG #test :Hello from test script!"
sleep 0.5
echo "QUIT :Bye"
) | nc localhost $PORT
---
まとめ
本章で学んだこと:
- サーバークラス設計: メンバー変数、コンストラクタ
- イベントループ: poll()による多重化
- 接続処理: accept、非ブロッキング設定
- クライアント管理: バッファ、状態管理
- メッセージ送信: バッファリング、POLLOUT
- コマンドディスパッチ: メッセージパース、ハンドラ呼び出し
- シグナル処理: グレースフルシャットダウン
次章では、各コマンドの実装を学びます。