サンドボックスゲーム「Minecraft」のマルチプレイ時におけるサーバ・クライアント間での通信方式について書いておきます。
参考にしているサイトはこちら
パケットがどのようなフォーマットで構成されているかを知る前に、
パケットを構成するデータがどのようにbyte配列へエンコードされるかを知っておかなければなりません。
これらの詳細はデータ型?で解説しています。
なお、データはVarIntとVarLong以外全てビッグエンディアンで送信されます。
パケットはTCP接続を介して送られる一連のデータです。
パケットの最初の要素には必ず、後に続くデータの長さをbyte単位で表すVarInt型の値が入っています。
その次の要素はVarInt型の「パケットID」が含まれていて、さらにその次の要素は追加のデータを含むbyte配列です。
パケットIDは、そのパケットの種類を表しています。ただし、その時々での状態や、
サーバー/クライアントのどちらに送信されるかにより異なる意味を持つものもあります。
例えば、パケットID 0x00 は、
ハンドシェイク状態のサーバーに送信するときはハンドシェイク、
ステータス状態のクライアントに送信するときは状態応答、
ログイン状態のサーバーに送信するときはログイン開始、
プレイ状態のサーバーに送信するときはテレポート確認、
プレイ状態のクライアントに送信するときはエンティティのスポーン
というように、パケットIDには複数の意味があるものがあります。
Minecraftのサーバーとクライアントは、それぞれ「状態」を持っていて、その状態によってパケットの意味が変わることがあります。
状態には、以下の四種類があります。
ハンドシェイクパケットのパケットIDは0x00で、以下のとおりの構成をしています。
このパケットを手作業で組み立ててみましょう。
今回はプロトコルバージョン578としてパケットを送ります。
まずは578をVarIntで表してみましょう。
VarIntでは、それぞれのbyteが 7bit の値と1bit のストップビットで構成されています。
javaっぽい擬似コードで表すと、以下のようにしてエンコードできます。
まず、 578 を二進数に変換すると、 001001000010(2)となる。
ここから下位7ビットを取り出して byte にキャストすると、 01000010(2) が得られる。
001001000010(2) を、下位7ビットを取り出した分ずらす(7ビットシフト)と、00100(2) が残る。
残った値は 0 ではないので、01000010(2) の最上位ビットを1にする。
そうすると、最初の 1バイト は11000010(2) = 0xC2 になる。
残った値 00100(2) から下位7ビットを取り出して byte にキャストすると、 00000100(2) が得られる。
00100(2) を、下位7ビットを取り出した分ずらす(7ビットシフト)と、0が残る。
残った値は 0 なので、次の 1バイト は 00000100(2) = 0x04 になる。
これで終わり。
よって、578を VarInt で表すと、 C2 04 になります。
今回は、サーバーアドレスを "localhost" とします。
まず、"localhost" をUTF-8でエンコードすると、 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x68, 0x6F, 0x73, 0x74 という配列になります。
文字数は 9 で、VarIntにしてもそのまま 09 となるので、それを最初につけると文字列全体の配列は 09 6C 6F 63 61 6C 68 6F 73 74 になります。
今までのデータを繋げると、
C2 04 09 6C 6F 63 61 6C 68 6F 73 74 63 DD 01 となります。これに接頭辞としてパケットID(0x00)をつけると
00 C2 04 09 6C 6F 63 61 6C 68 6F 73 74 63 DD 01 となります。
これの長さは 16 byte なので、 これを16進数に直し接頭辞としてつけると、最終的に
0F 00 C2 04 09 6C 6F 63 61 6C 68 6F 73 74 63 DD 01
が得られます。これが実際に送られるパケットのデータです。
(TODO)
参考にしているサイトはこちら
パケットがどのようなフォーマットで構成されているかを知る前に、
パケットを構成するデータがどのようにbyte配列へエンコードされるかを知っておかなければなりません。
これらの詳細はデータ型?で解説しています。
なお、データはVarIntとVarLong以外全てビッグエンディアンで送信されます。
型名 | データ長(byte) | 内容 | 備考 |
boolean | 1 | false / true | false は0x00、true は0x01 |
byte | 1 | -128 ~ 127 | |
unsigned byte | 1 | 0 ~ 255 | |
short | 2 | -32768 ~ 32767 | |
unsigned short | 2 | 0 ~ 65535 | |
int | 4 | -2147483648 ~ 2147483647 | |
VarInt | 1 ~ 5 | -2147483648 ~ 2147483647 | 可変長のint |
long | 8 | -9223372036854775808 ~ 9223372036854775807 | |
VarLong | 1 ~ 10 | -9223372036854775808 ~ 9223372036854775807 | 可変長のlong |
float | 4 | IEEE 754 単精度浮動小数点数 | |
double | 8 | IEEE 754 倍精度浮動小数点数 | |
String | - | UTF-8 文字列 | 最初の要素にbyte単位での長さを示すVarInt型の値が含まれている |
Position | 8 | 整数でのブロック位置(X,Y,Z) | X, Zは26bit、Yは12bitの符号付整数で表されている |
Angle | 1 | 角度 | 1回転を256分割した角度で、約1.4°刻み |
UUID | 16 | UUID |
パケットはTCP接続を介して送られる一連のデータです。
パケットの最初の要素には必ず、後に続くデータの長さをbyte単位で表すVarInt型の値が入っています。
その次の要素はVarInt型の「パケットID」が含まれていて、さらにその次の要素は追加のデータを含むbyte配列です。
要素の名前 | 型 | 備考 |
パケットの長さ | VarInt | パケットIDとデータの長さをbyte単位で表したもの |
パケットID | VarInt | パケットの種類を表すパケットID |
データ | byte配列 | 接続状態やパケットIDにより異なる |
パケットIDは、そのパケットの種類を表しています。ただし、その時々での状態や、
サーバー/クライアントのどちらに送信されるかにより異なる意味を持つものもあります。
例えば、パケットID 0x00 は、
ハンドシェイク状態のサーバーに送信するときはハンドシェイク、
ステータス状態のクライアントに送信するときは状態応答、
ログイン状態のサーバーに送信するときはログイン開始、
プレイ状態のサーバーに送信するときはテレポート確認、
プレイ状態のクライアントに送信するときはエンティティのスポーン
というように、パケットIDには複数の意味があるものがあります。
Minecraftのサーバーとクライアントは、それぞれ「状態」を持っていて、その状態によってパケットの意味が変わることがあります。
状態には、以下の四種類があります。
状態 | 備考 |
ハンドシェイク | ハンドシェイクを受信する前のサーバーの状態 |
ステータス | ハンドシェイク状態のサーバーにハンドシェイクパケット(ステータス)を送るとこれに遷移する。 |
ログイン | ハンドシェイク状態のサーバーにハンドシェイクパケット(ログイン)を送るとこれに遷移する。 |
プレイ | ログイン状態のサーバーからログイン成功パケットが送られるとこれに遷移する。 |
ハンドシェイクパケットのパケットIDは0x00で、以下のとおりの構成をしています。
要素の名前 | 型 | 備考 |
プロトコルバージョン | VarInt | クライアントのバージョンの通し番号。1.15.2 なら 578 |
サーバーアドレス | String | サーバーのホスト名が入る。例えば localhost |
サーバーポート | unsigned short | 通常は 25565 。16進数にすると0x63DD |
次の状態 | VarInt | 1 ならステータス状態に、 2 ならログイン状態になる |
このパケットを手作業で組み立ててみましょう。
今回はプロトコルバージョン578としてパケットを送ります。
まずは578をVarIntで表してみましょう。
VarIntでは、それぞれのbyteが 7bit の値と1bit のストップビットで構成されています。
javaっぽい擬似コードで表すと、以下のようにしてエンコードできます。
void writeVarInt (int value) { for (;;) { byte temp = (byte) ( value & 0x7F ) ; // 値から下位7ビットを取り出して byte にキャストする value = value >>> 7 ; // 値を7ビット詰める。符号ビットはずらさないこと。 if (value != 0) // 値が0ではない、つまり残りがあるなら最上位ビットを1にする temp = temp |= 0x80 ; writeByte( temp ) ; // 値を1byte 送信する if (value == 0) // 残りがなくなったら終わり break; } }
まず、 578 を二進数に変換すると、 001001000010(2)となる。
ここから下位7ビットを取り出して byte にキャストすると、 01000010(2) が得られる。
001001000010(2) を、下位7ビットを取り出した分ずらす(7ビットシフト)と、00100(2) が残る。
残った値は 0 ではないので、01000010(2) の最上位ビットを1にする。
そうすると、最初の 1バイト は11000010(2) = 0xC2 になる。
残った値 00100(2) から下位7ビットを取り出して byte にキャストすると、 00000100(2) が得られる。
00100(2) を、下位7ビットを取り出した分ずらす(7ビットシフト)と、0が残る。
残った値は 0 なので、次の 1バイト は 00000100(2) = 0x04 になる。
これで終わり。
よって、578を VarInt で表すと、 C2 04 になります。
今回は、サーバーアドレスを "localhost" とします。
まず、"localhost" をUTF-8でエンコードすると、 0x6C, 0x6F, 0x63, 0x61, 0x6C, 0x68, 0x6F, 0x73, 0x74 という配列になります。
文字数は 9 で、VarIntにしてもそのまま 09 となるので、それを最初につけると文字列全体の配列は 09 6C 6F 63 61 6C 68 6F 73 74 になります。
今までのデータを繋げると、
C2 04 09 6C 6F 63 61 6C 68 6F 73 74 63 DD 01 となります。これに接頭辞としてパケットID(0x00)をつけると
00 C2 04 09 6C 6F 63 61 6C 68 6F 73 74 63 DD 01 となります。
これの長さは 16 byte なので、 これを16進数に直し接頭辞としてつけると、最終的に
0F 00 C2 04 09 6C 6F 63 61 6C 68 6F 73 74 63 DD 01
が得られます。これが実際に送られるパケットのデータです。
(TODO)
このページへのコメント
パケットの構成で、
圧縮、非圧縮があるので、表に載っているのは非圧縮の場合と書くといいと思います