第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
  • コマンドディスパッチ: メッセージパース、ハンドラ呼び出し
  • シグナル処理: グレースフルシャットダウン

次章では、各コマンドの実装を学びます。