📌

Internet Rely Chatを作ろう

2024/09/03に公開

IRCとは?

Internet Relay Chat(インターネット・リレー・チャット、略称 : IRC)とは、インターネット上のテキストベースの通信プロトコルです。サーバを介してクライアントとクライアントが会話をする枠組みの名称である。公開または非公開のリアルタイムメッセージングを提供する。文章のみをやり取りして会話を行い、DCCなどを利用することでファイル転送も対応する。ユーザーは直接メッセージを交換したり、グループチャネルに参加したりできる。TCPを通信用のプロトコルとして主に用いる。TLSで暗号化することもできる。

歴史

Internet Relay Chat(以下「IRC」と記述する)は1988年8月にフィンランドの OuluBox というBBSで使われていたMUTと呼ばれるプログラムの代替としてヤルッコ・オイカリネン (Jarkko Oikarinen) によって作られた。BITNETネットワークで運用されていたBitnet Relay Chatに発想を得た。

IRCはその後鉄のカーテンの崩壊に際して東欧の人々が、あるいは湾岸戦争(1991年)の際に現地からIRCで情報発信されたために有名となった。

概要

この課題では、C++98を使用してIRCサーバーを実装します。IRC(Internet Relay Chat)は、リアルタイムのメッセージングプロトコルで、主にチャットルームやプライベートメッセージの送受信に使われます。このプロジェクトの目的は、クライアントからの接続を管理し、チャットメッセージの送受信を行う基本的なIRCサーバーを構築することです。

サーバーは、指定されたポートでTCP/IP接続を受け付け、クライアントが指定したパスワードを使用して認証します。サーバーは複数のクライアントを同時に処理し、以下の機能をサポートします:

  1. ユーザー認証
  2. ニックネームとユーザー名の設定
  3. チャンネルの作成、参加、メッセージの送受信
  4. オペレーターと通常ユーザーの区別
  5. チャンネルオペレーター向けの特別コマンド(KICK, INVITE, TOPIC, MODE)

実装の手順

  1. プロジェクトのセットアップ

    • Makefileの準備: プロジェクト全体をビルドするためのMakefileを作成します。all, clean, fclean, re のターゲットを含めます。
    • 基本的なファイル構造: プロジェクトのヘッダーファイル(.h)、ソースファイル(.cpp)、必要に応じてテンプレートファイル(.tpp, .ipp)を準備します。
  2. ネットワークソケットの設定

    • ソケットの作成: socket() を使用してサーバーソケットを作成します。
    • ソケットオプションの設定: setsockopt() を使用して、ソケットの再利用など必要なオプションを設定します。
    • バインドとリスニング: bind() でソケットを指定のポートにバインドし、listen() で接続を待ち受けます。
    • ノンブロッキングモード: ソケットを fcntl() でノンブロッキングモードに設定します。
  3. クライアント接続の管理

    • 接続の受け入れ: accept() を使って新しいクライアント接続を受け入れます。受け入れたソケットもノンブロッキングモードに設定します。
    • poll() でイベント監視: 1つの poll() 呼び出しで、すべてのI/O操作(読み取り、書き込み、接続待ち受けなど)を監視します。poll() はソケットが読み取り可能、書き込み可能、またはエラーが発生した場合に応答します。
  4. プロトコルの実装

    • クライアント認証: クライアントから送られるパスワードを確認し、適切な応答を返します。
    • IRCコマンドの解析: クライアントから送られるメッセージを解析し、ニックネームの設定、チャンネルへの参加、メッセージ送信などの操作を処理します。
    • チャンネル管理: クライアントがチャンネルに参加したり、チャンネル内の他のクライアントにメッセージを送信する機能を実装します。
  5. チャンネルオペレーター機能の実装

    • KICKコマンド: 特定のクライアントをチャンネルから強制的に退出させます。
    • INVITEコマンド: 指定したクライアントをチャンネルに招待します。
    • TOPICコマンド: チャンネルのトピックを変更または表示します。
    • MODEコマンド: チャンネルのモード(招待制、トピック制限、パスワード設定など)を変更します。
  6. エラーハンドリングとテスト

    • エラーハンドリング: すべてのシステムコールが正しく動作しているかを確認し、エラーが発生した場合は適切に処理します。
    • テスト: nc コマンドや選んだIRCクライアントを使用して、サーバーが正しく動作していることを確認します。特に、部分的なデータの受信や低帯域幅での動作を確認するためのテストを行います。
  7. コードのクリーンアップ

    • メモリ管理: 使用したリソースを適切に解放するため、close()freeaddrinfo() などを使用します。
    • コードレビュー: コードがクリーンであることを確認し、コメントやドキュメントを整備します。

実装のヒント

  • ステートマシンの利用: クライアントの状態(接続待ち、認証済み、チャンネル参加中など)を管理するためにステートマシンを使用すると、コードがシンプルで管理しやすくなります。
  • ノンブロッキングI/O: ノンブロッキングモードで動作させるため、recv()send() がデータをすぐに処理できない場合に、次のイベントに進むように設計します。
  • ログとデバッグ: デバッグのためにサーバーの動作をログに記録し、問題が発生した際に迅速に原因を特定できるようにします。

実装(echoServerMain.cpp, echoServer.hpp)

このコードは、基本的なエコーサーバーをC++で実装したものです。エコーサーバーは、クライアントから受信したメッセージをそのままクライアントに返すサーバーです。このコードでは、特定のポートでクライアントの接続を受け付け、メッセージを受信し、それを再びクライアントに送信する機能を提供します。以下に、このコードの各部分について詳しく解説します。

ヘッダーファイル (echoServer.hpp)

3. 定数の定義

#define RCVBUFSIZE 510
  • IRCプロトコルの仕様に従い、受信バッファサイズを510文字に設定しています(最大メッセージ長は512文字で、終端の \r\n を除いた長さ)。

4. グローバル変数

static bool g_sig_flg = false;
  • シグナルハンドリングのためのフラグ。SIGINTSIGQUIT を受信したときに true になります。

5. echoServer クラス

  • メンバー変数

    private:
        short port_;
        std::vector<int> clients_;
        char msg_[RCVBUFSIZE];
    
    • port_: サーバーがリッスンするポート番号を保持。
    • clients_: 接続中のクライアントソケットを管理するためのベクター。
    • msg_: 受信メッセージを格納するためのバッファ。
  • メンバ関数

    • initSocket(): サーバーソケットの初期化を行う。
    • initSelectArgs(): select() に渡すファイルディスクリプタセットを初期化。
    • setReadfds(): select() 用に読み込みファイルディスクリプタを設定。
    • acceptNewClient(): 新しいクライアントの接続を受け入れる。
    • ft_recv(): メッセージの受信処理を行う。
    • ft_send(): メッセージの送信処理を行う。
    • disconnectClient(): クライアントを切断し、ソケットを閉じる。
  • コンストラクタとデストラクタ

    • コンストラクタでサーバーを指定ポートで初期化し、デストラクタでリソースを解放します。
  • startServer()

    • サーバーのメインループを開始し、クライアントからの接続を処理します。

6. ヘルパー関数

void putError(const char *errmsg);
  • エラーメッセージを出力し、例外をスローする関数。

実装 (echoServerMain.cpp)

1. シグナルハンドラ

void sigHandler(int sig)
{
    g_sig_flg = true;
    std::cout << "signal received" << std::endl;
    throw std::exception();
}
  • シグナルが発生した際にフラグを立て、例外を投げます。これにより、サーバーが制御された形で終了することができます。

2. putError() 関数

void putError(const char *errmsg) {
    perror(errmsg);
    throw std::exception();
}
  • システムエラーメッセージを出力し、例外をスローします。

3. is_correct_port() 関数

bool is_correct_port(char *str)
{
    if (strlen(str) > 5)
        return (false);
    int num = 0;
    num = std::atoi(str);
    if (num < 1024 || num > 65535)
        return (false);
    return (true);
}
  • 与えられたポート番号が有効かどうかをチェックする関数です。ポート番号は1024以上65535以下でなければなりません。

main() 関数

int main()
{
    short port = 4242;
    try
    {
        struct sigaction sa;
        sa.sa_flags = SA_RESTART;
        sa.sa_handler = sigHandler;
        if (sigaction(SIGINT, &sa, NULL) < 0)
            putError("sigaction failed");
        if (sigaction(SIGQUIT, &sa, NULL) < 0)
            putError("sigaction faild");

        echoServer serv(port);
        serv.startServer();
    }
    catch(const std::exception& e)
    {
    }
    return 0;
}
  • サーバーはポート4242で動作します。
  • シグナルハンドラを設定し、SIGINTSIGQUIT のシグナルを受け取ると sigHandler が呼び出されます。
  • echoServer インスタンスを作成し、startServer() メソッドでサーバーの動作を開始します。
  • エラーが発生した場合は、例外処理でプログラムが終了します。

まとめ

このコードは、基本的なエコーサーバーを実装するためのフレームワークです。クライアントからの接続を受け入れ、メッセージを受信して再びクライアントに送信するという基本的な機能を提供します。シグナルハンドリングやノンブロッキングI/Oの設定など、サーバーを堅牢に動作させるための多くの技術が含まれています。

1. コンストラクタとデストラクタ

echoServer::echoServer(){}
echoServer::echoServer(short port):port_(port){}
echoServer::~echoServer(){}
echoServer::echoServer(const echoServer &other) { *this = other; }

echoServer &echoServer::operator=(const echoServer &other) {
  if (this != &other) {
    port_ = other.port_;
    size_t i = 0;
    for (i = 0; other.msg_[i] != '\0'; i++)
      this->msg_[i] = other.msg_[i];
    this->msg_[i] = '\0';
    this->clients_ = other.clients_;
  }
  return (*this);
}
  • デフォルトコンストラクタ: 引数なしでechoServerオブジェクトを作成するためのコンストラクタですが、使われていません。
  • 引数付きコンストラクタ: サーバーがリッスンするポート番号を初期化します。
  • デストラクタ: サーバーのリソースを解放しますが、特に何も行っていません(必要に応じて拡張可能)。
  • コピーコンストラクタ: オブジェクトをコピーする際に呼ばれます。*this = other; によって代入演算子が呼び出されます。
  • 代入演算子: オブジェクトのコピーを行います。ポート番号、受信メッセージ、クライアントリストをコピーします。

2. ソケットの初期化

void echoServer::initSocket(int &sock, struct sockaddr_in &sockaddr) {
  int on = 1;
  if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
    putError("socket failed");

  if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    putError("setsockopt failed");

  std::memset(&sockaddr, 0, sizeof(sockaddr));
  sockaddr.sin_family = AF_INET;
  sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  sockaddr.sin_port = htons(port_);

  if (bind(sock, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0)
    putError("bind failed");

  if (listen(sock, SOMAXCONN) < 0)
    putError("listen failed");

  if (fcntl(sock, F_SETFL, O_NONBLOCK) < 0)
    putError("fcntl failed");
}
  • ソケット作成: socket() でソケットを作成します。PF_INET はIPv4プロトコル、SOCK_STREAM はTCP接続、IPPROTO_TCP はTCPプロトコルを指定しています。
  • ソケットオプション設定: setsockopt()SO_REUSEADDR オプションを設定し、ソケットが閉じられた直後でも同じポートを再利用できるようにしています。
  • アドレス構造体の初期化: sockaddr_in 構造体をゼロクリアし、IPv4アドレス(INADDR_ANY で任意のアドレスを受け入れ)、ポート番号を設定します。
  • バインド: bind() でソケットを指定のアドレスとポートにバインドします。
  • リスン: listen() でソケットをリスニング状態にします。SOMAXCONN はバックログ(保留中の接続要求のキューの最大長)を設定します。
  • ノンブロッキング設定: fcntl() を使用してソケットをノンブロッキングモードに設定します。

3. select() 関連の初期化

void echoServer::initSelectArgs(fd_set &read_fds, fd_set &write_fds, struct timeval &timeout) {
  FD_ZERO(&read_fds);
  FD_ZERO(&write_fds);
  timeout.tv_sec = 100;
  timeout.tv_usec = 0;
}

void echoServer::setReadfds(int sock, fd_set &read_fds) {
  FD_SET(sock, &read_fds);
  for (size_t i = 0; i < clients_.size(); i++)
    FD_SET(clients_[i], &read_fds);
}
  • initSelectArgs()

    • FD_ZERO()fd_set をクリアし、初期化します。
    • timeout を設定し、select() が待機する最大時間を指定します(ここでは100秒)。
  • setReadfds()

    • サーバーソケットとすべてのクライアントソケットを fd_set に追加し、select() で監視するように設定します。

4. クライアント接続の処理

int echoServer::acceptNewClient(int sock) {
  struct sockaddr_in client_addr;
  socklen_t addr_len = sizeof(client_addr);
  int new_sock = 0;

  if ((new_sock = accept(sock, (struct sockaddr *)&client_addr, &addr_len)) < 0)
    putError("accept failed");

  if (fcntl(new_sock, F_SETFL, O_NONBLOCK) < 0)
    putError("fcntl failed");

  std::cout << "connected sockfd: " << new_sock << std::endl;
  clients_.push_back(new_sock);
  return (new_sock);
}
  • acceptNewClient()
    • 新しいクライアント接続を accept() で受け入れ、新しいソケットを作成します。
    • 受け入れたクライアントソケットもノンブロッキングモードに設定します。
    • 新しいクライアントソケットを clients_ ベクターに追加し、接続を管理します。

5. メッセージの受信と送信

void echoServer::ft_recv(size_t i) {
  int recv_size = 0;

  while (g_sig_flg == false) {
    recv_size = recv(clients_[i], msg_, RCVBUFSIZE, 0);
    if (recv_size < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
      continue;
    else if (recv_size < 0 && errno == ECONNRESET) {
      disconnectClient(i);
      return;
    } else if (recv_size < 0)
      putError("recv failed");
    else
      break;
  }
  msg_[recv_size - 1] = '\0';
  msg_[recv_size] = '\n';
  msg_[recv_size + 1] = '\r';
  std::cout << "received: " << msg_ << std::endl;
  ft_send(i, recv_size + 1);
}

void echoServer::ft_send(size_t i, size_t send_size) {
  int send_ret = 0;

  while (g_sig_flg == false) {
    send_ret = send(clients_[i], msg_, send_size, 0);
    if (send_ret < 0 && errno == EAGAIN || errno == EWOULDBLOCK)
      continue;
    else if (send_ret < 0 && errno == ECONNRESET) {
      disconnectClient(i);
      return;
    } else if (send_ret < 0)
      putError("send failed");
    else
      break;
  }
}
  • ft_recv()

    • recv() を使用してクライアントからのメッセージを受信します。ノンブロッキングモードなので、データがまだ届いていない場合は EAGAIN または EWOULDBLOCK が返り、処理を続行します。
    • クライアントが接続をリセットした場合(ECONNRESET)、クライアントを切断します。
    • 正常に受信できたメッセージを改行文字 \n とキャリッジリターン \r を付けてログに表示し、再度クライアントに送り返します。
  • ft_send()

    • send() を使用して受信したメッセージをクライアントに送信します。ノンブロッキングモードなので、送信がまだ完了していない場合は再試行します。
    • ECONNRESET の場合、クライアントを切断します。

6. クライアントの切断

void echoServer::disconnectClient(size_t i) {
  std::cout << "disconnected sockfd : " << clients_[i] <<

 std::endl;
  if (close(clients_[i]) < 0)
    putError("close failed");
  msg_[0] = '\0';
  clients_.erase(clients_.begin() + i);
}
  • disconnectClient()
    • 指定したクライアントを切断し、ソケットを閉じます。
    • クライアントリスト clients_ からクライアントを削除します。

7. サーバーのメインループ

void echoServer::startServer() {
  int sock;
  struct sockaddr_in sockaddr;
  initSocket(sock, sockaddr);

  fd_set read_fds, write_fds;
  struct timeval timeout;
  int sel_ret = 0;
  initSelectArgs(read_fds, write_fds, timeout);
  while (g_sig_flg == false) {
    setReadfds(sock, read_fds);
    sel_ret = select(FD_SETSIZE, &read_fds, &write_fds, NULL, &timeout);
    if (sel_ret < 0)
      putError("select failed");
    if (sel_ret == 0) {
      std::cout << "Time out" << std::endl;
      break;
    }

    if (FD_ISSET(sock, &read_fds))
      acceptNewClient(sock);

    for (size_t i = 0; i < clients_.size(); i++) {
      if (FD_ISSET(clients_[i], &read_fds)) {
        FD_CLR(clients_[i], &read_fds);
        ft_recv(i);
      }
    }
  }
}
  • startServer()
    • サーバーソケットを初期化し、select() を使用してクライアントからの接続とメッセージを監視します。
    • メインループでは、select() によって読み取り可能なソケットを監視し、クライアントからの接続やメッセージの受信を処理します。
    • 新しい接続があれば acceptNewClient() で処理し、既存のクライアントからのメッセージは ft_recv() で処理します。

まとめ

このエコーサーバーのコードは、C++でネットワークプログラミングを行うための基本的な実装を示しています。非ブロッキングI/Oを用いてクライアントとの通信を効率的に処理し、複数のクライアントを同時に扱うことができるように設計されています。各機能がシンプルに実装されているため、学習用や基本的なサーバープログラムのベースとして役立つでしょう。

Discussion