IPUT電子工学研究会による様々な研究結果をおいておくところ

サンドボックスゲーム「Minecraft」のマルチプレイ時におけるサーバ・クライアント間での通信方式について書いておきます。

参考にしているサイトはこちら

プロトコルについて


MinecraftサーバーはTCPによる通信に対応し、デフォルトで25565番ポートを使用します。
サーバーとクライアント同士はパケットを用いて通信しています。

データ型


パケットがどのようなフォーマットで構成されているかを知る前に、
パケットを構成するデータがどのようにbyte配列へエンコードされるかを知っておかなければなりません。
これらの詳細はデータ型?で解説しています。
なお、データはVarIntとVarLong以外全てビッグエンディアンで送信されます。
型名データ長(byte)内容備考
boolean1false / truefalse は0x00、true は0x01
byte1-128 ~ 127
unsigned byte10 ~ 255
short2-32768 ~ 32767
unsigned short20 ~ 65535
int4-2147483648 ~ 2147483647
VarInt1 ~ 5-2147483648 ~ 2147483647可変長のint
long8-9223372036854775808 ~ 9223372036854775807
VarLong1 ~ 10-9223372036854775808 ~ 9223372036854775807可変長のlong
float4IEEE 754 単精度浮動小数点数
double8IEEE 754 倍精度浮動小数点数
String-UTF-8 文字列最初の要素にbyte単位での長さを示すVarInt型の値が含まれている
Position8整数でのブロック位置(X,Y,Z)X, Zは26bit、Yは12bitの符号付整数で表されている
Angle1角度1回転を256分割した角度で、約1.4°刻み
UUID16UUID

パケットの構成


パケットはTCP接続を介して送られる一連のデータです。
パケットの最初の要素には必ず、後に続くデータの長さをbyte単位で表すVarInt型の値が入っています。
その次の要素はVarInt型の「パケットID」が含まれていて、さらにその次の要素は追加のデータを含むbyte配列です。
要素の名前備考
パケットの長さVarIntパケットIDとデータの長さをbyte単位で表したもの
パケットIDVarIntパケットの種類を表すパケットID
データbyte配列接続状態やパケットIDにより異なる

パケットIDは、そのパケットの種類を表しています。ただし、その時々での状態や、
サーバー/クライアントのどちらに送信されるかにより異なる意味を持つものもあります。

例えば、パケットID 0x00 は、
ハンドシェイク状態のサーバーに送信するときはハンドシェイク、
ステータス状態のクライアントに送信するときは状態応答、
ログイン状態のサーバーに送信するときはログイン開始、
プレイ状態のサーバーに送信するときはテレポート確認、
プレイ状態のクライアントに送信するときはエンティティのスポーン
というように、パケットIDには複数の意味があるものがあります。

状態


Minecraftのサーバーとクライアントは、それぞれ「状態」を持っていて、その状態によってパケットの意味が変わることがあります。

状態には、以下の四種類があります。
状態備考
ハンドシェイクハンドシェイクを受信する前のサーバーの状態
ステータスハンドシェイク状態のサーバーにハンドシェイクパケット(ステータス)を送るとこれに遷移する。
ログインハンドシェイク状態のサーバーにハンドシェイクパケット(ログイン)を送るとこれに遷移する。
プレイログイン状態のサーバーからログイン成功パケットが送られるとこれに遷移する。

例 ハンドシェイクパケット(ステータス)をサーバーに送り、応答を解析してみる


ハンドシェイクパケットのパケットIDは0x00で、以下のとおりの構成をしています。
要素の名前備考
プロトコルバージョンVarIntクライアントのバージョンの通し番号。1.15.2 なら 578
サーバーアドレスStringサーバーのホスト名が入る。例えば localhost
サーバーポートunsigned short通常は 25565 。16進数にすると0x63DD
次の状態VarInt1 ならステータス状態に、 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 になります。
サーバーポート

今回は、サーバーポートを 25565 とします。
16進数にすると 0x63DD なので、そのまま 63 DD です。
次の状態

今回はステータスにしたいので、 0x01 を送ります。

データを組み合わせて送信


今までのデータを繋げると、
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)

パケットを送って帰ってきたデータを解析


(TODO)

このページへのコメント

パケットの構成で、
圧縮、非圧縮があるので、表に載っているのは非圧縮の場合と書くといいと思います

1
Posted by koufu193 2022年03月12日(土) 16:19:26 返信

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Menu

メニュー

メンバーのみ編集できます