第2章:IRCプロトコル
はじめに
IRCプロトコルはRFC 1459で定義されたテキストベースのプロトコルです。本章では、メッセージ形式、コマンド、リプライについて詳しく学びます。
---
1. メッセージ形式
1.1 BNF記法
RFC 1459によるメッセージ形式:
message = [ ":" prefix SPACE ] command [ params ] crlf
prefix = servername / ( nickname [ [ "!" user ] "@" host ] )
command = 1*letter / 3digit
params = *14( SPACE middle ) [ SPACE ":" trailing ]
/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]
middle = nospcrlfcl *( ":" / nospcrlfcl )
trailing = *( ":" / " " / nospcrlfcl )
nospcrlfcl = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
; any octet except NUL, CR, LF, " " and ":"
crlf = %x0D %x0A ; "\\r\\n"
1.2 メッセージ構造
IRCメッセージの構成:
[:<prefix>] <command> [<params>] \\r\\n
┌─────────────────────────────────────────────────┐
│ :Nick!user@host PRIVMSG #channel :Hello World\\r\\n │
└─────────────────────────────────────────────────┘
↑ ↑ ↑ ↑
prefix command param trailing
prefix: オプション、送信元を示す
command: コマンド名または3桁の数字
params: スペース区切りのパラメータ
trailing: ":"以降の残り全て(スペース含む)
1.3 パラメータの最大数
パラメータ:
- 最大15個(command含む)
- trailing は1つのパラメータとしてカウント
例:
MODE #channel +o Alice Bob
↑ ↑ ↑ ↑
param1 2 3 4
PRIVMSG #channel :This is a long message with spaces
↑ ↑
param1 param2(trailing)
---
2. メッセージパーサー
2.1 パーサー実装
struct Message {
std::string prefix;
std::string command;
std::vector<std::string> params;
};
class MessageParser {
public:
static Message parse(const std::string& line) {
Message msg;
std::string::size_type pos = 0;
std::string::size_type end = line.length();
// 末尾の\\r\\n を除去
while (end > 0 && (line[end-1] == '\\r' || line[end-1] == '\\n')) {
end--;
}
// プレフィックス(オプション)
if (pos < end && line[pos] == ':') {
pos++; // ':' をスキップ
std::string::size_type spacePos = line.find(' ', pos);
if (spacePos != std::string::npos && spacePos < end) {
msg.prefix = line.substr(pos, spacePos - pos);
pos = spacePos + 1;
}
}
// 先頭のスペースをスキップ
while (pos < end && line[pos] == ' ') {
pos++;
}
// コマンド
std::string::size_type spacePos = line.find(' ', pos);
if (spacePos == std::string::npos || spacePos >= end) {
msg.command = line.substr(pos, end - pos);
toUpperCase(msg.command);
return msg;
}
msg.command = line.substr(pos, spacePos - pos);
toUpperCase(msg.command);
pos = spacePos + 1;
// パラメータ
while (pos < end) {
// スペースをスキップ
while (pos < end && line[pos] == ' ') {
pos++;
}
if (pos >= end) break;
// trailing パラメータ
if (line[pos] == ':') {
msg.params.push_back(line.substr(pos + 1, end - pos - 1));
break;
}
// 通常のパラメータ
spacePos = line.find(' ', pos);
if (spacePos == std::string::npos || spacePos >= end) {
msg.params.push_back(line.substr(pos, end - pos));
break;
}
msg.params.push_back(line.substr(pos, spacePos - pos));
pos = spacePos + 1;
}
return msg;
}
private:
static void toUpperCase(std::string& str) {
for (size_t i = 0; i < str.length(); i++) {
str[i] = std::toupper(str[i]);
}
}
};
2.2 テストケース
void testParser() {
// 基本的なコマンド
Message m1 = MessageParser::parse("NICK Alice\\r\\n");
assert(m1.command == "NICK");
assert(m1.params[0] == "Alice");
// プレフィックス付き
Message m2 = MessageParser::parse(":Alice!alice@host PRIVMSG #test :Hello\\r\\n");
assert(m2.prefix == "Alice!alice@host");
assert(m2.command == "PRIVMSG");
assert(m2.params[0] == "#test");
assert(m2.params[1] == "Hello");
// trailing にスペース
Message m3 = MessageParser::parse("PRIVMSG #test :Hello World!\\r\\n");
assert(m3.params[1] == "Hello World!");
// 複数パラメータ
Message m4 = MessageParser::parse("MODE #test +o Alice\\r\\n");
assert(m4.params[0] == "#test");
assert(m4.params[1] == "+o");
assert(m4.params[2] == "Alice");
}
---
3. 数値リプライ
3.1 リプライのカテゴリ
数値リプライ:
000-099: 接続関連(ウェルカムメッセージ等)
200-399: コマンド成功
400-599: エラー
リプライ形式:
:<server> <code> <target> [<params>] :<message>
例:
:irc.server.net 001 Alice :Welcome to the IRC Network
:irc.server.net 433 * Alice :Nickname is already in use
3.2 よく使うリプライ
// 接続成功
#define RPL_WELCOME "001" // :Welcome to the IRC Network
#define RPL_YOURHOST "002" // :Your host is <server>
#define RPL_CREATED "003" // :This server was created <date>
#define RPL_MYINFO "004" // <server> <version> <usermodes> <chanmodes>
// チャンネル情報
#define RPL_NOTOPIC "331" // :No topic is set
#define RPL_TOPIC "332" // :<topic>
#define RPL_INVITING "341" // <nick> <channel>
#define RPL_NAMREPLY "353" // = <channel> :<nick list>
#define RPL_ENDOFNAMES "366" // :End of /NAMES list
// エラー
#define ERR_NOSUCHNICK "401" // :No such nick/channel
#define ERR_NOSUCHCHANNEL "403" // :No such channel
#define ERR_CANNOTSENDTOCHAN "404" // :Cannot send to channel
#define ERR_TOOMANYCHANNELS "405" // :You have joined too many channels
#define ERR_UNKNOWNCOMMAND "421" // :Unknown command
#define ERR_NONICKNAMEGIVEN "431" // :No nickname given
#define ERR_ERRONEUSNICKNAME "432" // :Erroneous nickname
#define ERR_NICKNAMEINUSE "433" // :Nickname is already in use
#define ERR_USERNOTINCHANNEL "441" // :They aren't on that channel
#define ERR_NOTONCHANNEL "442" // :You're not on that channel
#define ERR_USERONCHANNEL "443" // :is already on channel
#define ERR_NOTREGISTERED "451" // :You have not registered
#define ERR_NEEDMOREPARAMS "461" // :Not enough parameters
#define ERR_ALREADYREGISTRED "462" // :You may not reregister
#define ERR_PASSWDMISMATCH "464" // :Password incorrect
#define ERR_CHANNELISFULL "471" // :Cannot join channel (+l)
#define ERR_INVITEONLYCHAN "473" // :Cannot join channel (+i)
#define ERR_BADCHANNELKEY "475" // :Cannot join channel (+k)
#define ERR_CHANOPRIVSNEEDED "482" // :You're not channel operator
3.3 リプライ生成
class Reply {
public:
static std::string welcome(const std::string& nick,
const std::string& serverName) {
return ":" + serverName + " 001 " + nick +
" :Welcome to the IRC Network, " + nick + "\\r\\n";
}
static std::string nickInUse(const std::string& nick,
const std::string& serverName) {
return ":" + serverName + " 433 * " + nick +
" :Nickname is already in use\\r\\n";
}
static std::string noSuchNick(const std::string& target,
const std::string& nick,
const std::string& serverName) {
return ":" + serverName + " 401 " + target + " " + nick +
" :No such nick/channel\\r\\n";
}
static std::string needMoreParams(const std::string& nick,
const std::string& command,
const std::string& serverName) {
return ":" + serverName + " 461 " + nick + " " + command +
" :Not enough parameters\\r\\n";
}
static std::string namReply(const std::string& nick,
const std::string& channel,
const std::string& names,
const std::string& serverName) {
return ":" + serverName + " 353 " + nick + " = " + channel +
" :" + names + "\\r\\n";
}
static std::string endOfNames(const std::string& nick,
const std::string& channel,
const std::string& serverName) {
return ":" + serverName + " 366 " + nick + " " + channel +
" :End of /NAMES list\\r\\n";
}
};
---
4. 接続シーケンス
4.1 クライアント登録
クライアント → サーバー:
PASS <password>
NICK <nickname>
USER <username> <mode> <unused> :<realname>
サーバー → クライアント:
:server 001 <nick> :Welcome to the IRC Network
:server 002 <nick> :Your host is <server>, running version <ver>
:server 003 <nick> :This server was created <date>
:server 004 <nick> <server> <version> <usermodes> <chanmodes>
4.2 シーケンス図
Client Server
| |
| PASS secretpass |
| ----------------------------------> |
| |
| NICK Alice |
| ----------------------------------> |
| |
| USER alice 0 * :Alice Smith |
| ----------------------------------> |
| |
| :server 001 Alice :Welcome... |
| <---------------------------------- |
| :server 002 Alice :Your host...|
| <---------------------------------- |
| :server 003 Alice :Created... |
| <---------------------------------- |
| :server 004 Alice ... |
| <---------------------------------- |
| |
| (登録完了) |
4.3 登録状態の管理
class Client {
private:
enum RegState {
REG_NONE = 0,
REG_PASS = 1, // パスワード受信済み
REG_NICK = 2, // ニックネーム設定済み
REG_USER = 4, // ユーザー情報設定済み
REG_COMPLETE = REG_PASS | REG_NICK | REG_USER
};
int _fd;
std::string _nickname;
std::string _username;
std::string _realname;
std::string _hostname;
int _regState;
bool _registered;
public:
void setPassReceived() {
_regState |= REG_PASS;
checkRegistration();
}
void setNickname(const std::string& nick) {
_nickname = nick;
_regState |= REG_NICK;
checkRegistration();
}
void setUser(const std::string& user, const std::string& realname) {
_username = user;
_realname = realname;
_regState |= REG_USER;
checkRegistration();
}
bool isRegistered() const {
return _registered;
}
private:
void checkRegistration() {
if (!_registered && _regState == REG_COMPLETE) {
_registered = true;
// ウェルカムメッセージを送信
}
}
};
---
5. チャンネル操作
5.1 JOINシーケンス
クライアント → サーバー:
JOIN #channel
サーバー → クライアント(参加者全員):
:Alice!alice@host JOIN #channel
サーバー → クライアント:
:server 332 Alice #channel :Channel topic
:server 353 Alice = #channel :@Alice Bob Carol
:server 366 Alice #channel :End of /NAMES list
5.2 PRIVMSGシーケンス
チャンネルメッセージ:
クライアント:
PRIVMSG #channel :Hello everyone!
サーバー(送信者以外の全員に):
:Alice!alice@host PRIVMSG #channel :Hello everyone!
プライベートメッセージ:
クライアント:
PRIVMSG Bob :Hi there!
サーバー(Bobに):
:Alice!alice@host PRIVMSG Bob :Hi there!
5.3 PART/QUITシーケンス
PART:
クライアント:
PART #channel :Goodbye!
サーバー(チャンネルメンバー全員に):
:Alice!alice@host PART #channel :Goodbye!
QUIT:
クライアント:
QUIT :Leaving
サーバー(共有チャンネルのメンバー全員に):
:Alice!alice@host QUIT :Leaving
---
6. モード
6.1 チャンネルモード
ft_ircで必要なモード:
i - invite-only(招待制)
t - topic restriction(オペレータのみトピック変更可)
k - channel key(パスワード)
o - operator(オペレータ権限)
l - user limit(人数制限)
MODE #channel +i → 招待制にする
MODE #channel -i → 招待制を解除
MODE #channel +k secret → パスワード設定
MODE #channel -k → パスワード解除
MODE #channel +o Alice → Aliceをオペレータに
MODE #channel -o Alice → オペレータ権限剥奪
MODE #channel +l 50 → 最大50人
MODE #channel -l → 人数制限解除
複合:
MODE #channel +itk secret → 複数モード同時設定
MODE #channel +ol Alice 50 → オペレータ+人数制限
6.2 MODEパース
struct ModeChange {
bool add; // true: +, false: -
char mode; // モード文字
std::string param; // パラメータ(必要な場合)
};
class ModeParser {
public:
static std::vector<ModeChange> parse(const std::vector<std::string>& params) {
std::vector<ModeChange> changes;
if (params.size() < 2) return changes;
const std::string& modeStr = params[1];
size_t paramIndex = 2;
bool adding = true;
for (size_t i = 0; i < modeStr.length(); i++) {
char c = modeStr[i];
if (c == '+') {
adding = true;
} else if (c == '-') {
adding = false;
} else {
ModeChange change;
change.add = adding;
change.mode = c;
// パラメータが必要なモード
if (needsParam(c, adding)) {
if (paramIndex < params.size()) {
change.param = params[paramIndex++];
}
}
changes.push_back(change);
}
}
return changes;
}
private:
static bool needsParam(char mode, bool adding) {
// +o, -o: 常にパラメータ必要
if (mode == 'o') return true;
// +k: パラメータ必要、-k: 不要
if (mode == 'k') return adding;
// +l: パラメータ必要、-l: 不要
if (mode == 'l') return adding;
return false;
}
};
---
7. ホストマスク
7.1 形式
ホストマスク:
nickname!username@hostname
例:
Alice!alice@192.168.1.100
Bob!~bob@example.com
Guest123!guest@10.0.0.1
生成:
std::string Client::getPrefix() const {
return _nickname + "!" + _username + "@" + _hostname;
}
7.2 メッセージ生成
std::string Client::formatMessage(const std::string& command,
const std::vector<std::string>& params) const {
std::string msg = ":" + getPrefix() + " " + command;
for (size_t i = 0; i < params.size(); i++) {
msg += " ";
// 最後のパラメータでスペースを含む場合
if (i == params.size() - 1 && params[i].find(' ') != std::string::npos) {
msg += ":";
}
msg += params[i];
}
msg += "\\r\\n";
return msg;
}
---
まとめ
本章で学んだこと:
- メッセージ形式: prefix, command, params
- パーサー実装: BNF記法に基づく解析
- 数値リプライ: 成功とエラーのコード
- 接続シーケンス: PASS/NICK/USER
- チャンネル操作: JOIN/PART/PRIVMSG
- モード: チャンネルモードとパース
- ホストマスク: nick!user@host
次章では、サーバーアーキテクチャを学びます。