マスターサーバクエリーのプロトコルとアクセス例



・サーバちょうだい
サーバクエリーのプロトコルとアクセス例』では 単一指定のゲームサーバから情報をひっぱってくる方法がわかりました。

では、ゲームのサーバ検索機能を使用した際に情報を問い合わせるべき サーバのアドレスをSteamは一体どっから持ってきてんでしょうか?

答えはマスターサーバでごんす。一般的にゲームサーバからマスターサーバへは定周期で ハートビート信号が送られており、これによりゲームサーバの存在をマスターサーバへ通知しています。
そして、マスターサーバはそれらゲームサーバのリスト(IPアドレス)を提供する機能を持っています。
内臓のサーバブラウザや、GameSpy、ASEはそのマスターサーバから引っ張ってきたリストを 基に各ゲームサーバへ詳細情報取得の問い合わせを行うであります。

それぞれのゲームに対するマスターサーバのアドレスはSteamデフォルトインストール先の場合、
C:\Program Files\Valve\Steam\config\masterservers.vdf

の中に書いてあります。

HL2DMやCS:Sは現在以下がマスターサーバのアドレスです。

68.142.72.250:27011
207.173.177.11:27011
69.28.151.162:27011

・りすと〜、カム〜!
マスターサーバからゲームサーバのリストを取得する際、通信にはUDPを使用します。

  • 要求データフォーマット

  • 要求データのフォーマットはゲームサーバクエリーのフォーマットとは異なり、 先頭に0xFFFFFFFF(-1)の4バイトデータを持っていません(いきなり'1'から始まる)。
    名称 データ型 説明
    リクエストコード byte '1'(0x31)
    リージョンコード byte 下記のコード指定によってサーバの地域を限定できます。
    リージョンコード 該当地域
    0x00 アメリカ東海岸
    0x01 アメリカ西海岸
    0x02 南米
    0x03 ヨーロッパ
    0x04 アジア
    0x05 オーストラリア
    0x06 中東
    0x07 アフリカ
    0xFF その他地域
    IP:Port string Steamは固定で"0.0.0.0:0"を送ってる。
    フィルター string 指定されたフィルタによってピックアップするサーバの条件を指定できます。
    また、フィルタとフィルタの間を'\'キャクタで区切りながら複数 連結させることができます。

    \gamedir\[mod]\map\[map]以外は 固定の記述です、キーワードに続く'1'の部分を0に書換えようともフィルタを無効化できないので注意してください。 有効/無効の基準はフィルタを記述するかしないかで判断されます。

  • \type\d

  • 指定するとDEDICATEDサーバに限定。

  • \secure\1

  • 指定するとVACセキュアのかかったサーバに限定

  • \gamedir\[mod]

  • 指定のゲームフォルダ名のゲームサーバに限定

    例:
    \gamedir\cstrike
    \gamedir\hl2mp

  • \map\[map]

  • 現在のマップが指定マップになっているゲームサーバに限定

    例:
    \map\dm_lockdown

  • \linux\1

  • 指定するとLinux OSのゲームサーバに限定

  • \empty\1

  • 指定すると無人のサーバを除外

  • \full\1

  • 指定すると満員のサーバを除外

  • \proxy\1

  • 指定すると観戦用の代理サーバに限定


    要求の例:
    31 04 30 2E 30 2E 30 2E 30 3A 30 00 5C 74 79 70	1.0.0.0.0:0.\typ
    65 5C 64 5C 73 65 63 75 72 65 5C 31 5C 67 61 6D	e\d\secure\1\gam
    65 64 69 72 5C 68 6C 32 6D 70 5C 65 6D 70 74 79	edir\hl2mp\empty
    5C 31 00					\1.
    


  • 応答データフォーマット

  • 応答データフォーマットはゲームサーバクエリーのフォーマットと同じく、 先頭に0xFFFFFFFF(-1)の4バイトデータを持っています
    Steamの単位パケットサイズは最大1400バイトなのでサーバの情報は232個 (終端データ含まず)まで取れます。
    名称 データ型 説明
    レスポンスコード00 byte 'f'(0x66)
    レスポンスコード01 byte '\n'(0x0A)
    以降データの終端まで以下のデータが連続して格納されてます
    また、最後の1グループは全て0x00のデータが格納されてくるのでデータ終端の判別ができます。
    IPアドレスの第1オクテット byte 0〜255なので符号付きで扱わないこと
    IPアドレスの第2オクテット byte 0〜255なので符号付きで扱わないこと
    IPアドレスの第3オクテット byte 0〜255なので符号付きで扱わないこと
    IPアドレスの第4オクテット byte 0〜255なので符号付きで扱わないこと
    ポート番号 short (注)ここはビックエンディアンで格納されている、符号も無しで扱うこと

    応答の例:
    FF FF FF FF 66 0A C0 A8 01 01 69 87 00 00 00 00 00 00

    ・実際にやってみる
    実際に簡易プログラムを作ってサーバへアクセスしてみました。

    「アジア地域、DEDICATEDサーバ、VAC有効、参加者有り」に合致するHL2DMのサーバリストを マスターサーバから取得し、リスト上のゲームサーバIP/ポート番号から各種情報を取得してます。
    VC++2005 Expressのコンソールアプリケーションです。どーぞ→プロジェクト丸ごとゲット




    先頭の「リージョンコード」と「フィルタ」を適当に書き換えて色々試してみましょう。
    server_info.cpp
    // うんころりんこ〜♪
    
    #include "stdafx.h"
    #include <winsock2.h>
    #include <list>
    
    //------------------------------------------------------------------------
    
    // マスターサーバアドレス
    #define MASTER_SERVER_ADDRESS	"207.173.177.11"
    //#define MASTER_SERVER_ADDRESS	"68.142.72.250"
    //#define MASTER_SERVER_ADDRESS	"69.28.151.162"
    
    // マスターサーバポート
    #define MASTER_SERVER_PORT		27011
    
    // リージョンコード
    #define FILTERING_REGION_CODE	0x04
    
    // フィルタ
    #define SERVER_FILTER			\
    	"\\type\\d"		\
    	"\\secure\\1"		\
    	"\\gamedir\\hl2mp"		\
    /*	"\\map\\dm_lockdown"*/	\
    /*	"\\linux\\1"*/		\
    	"\\empty\\1"		\
    /*	"\\full\\1"	*/	\
    /*	"\\proxy\\1"*/		\
    	""
    //------------------------------------------------------------------------
    
    // 最大パケットサイズ
    #define MAX_OF_PACKET_SIZE		1400
    
    // 最大パケット数
    #define MAX_OF_PACKET_COUNT		15
    
    // 最大パケット番号
    #define MAX_OF_PACKET_NUMBER	(MAX_OF_PACKET_COUNT - 1)
    
    // 受信バッファサイズ
    #define RBUFFER_SIZE			(MAX_OF_PACKET_SIZE * MAX_OF_PACKET_COUNT)
    
    // 非分割データ識別コード
    #define SINGLE_PACKET_CODE		0xFFFFFFFF
    // 分割データ識別コード
    #define DEVIDED_PACKET_CODE		0xFFFFFFFE
    
    // タイムアウト時間
    #define RECEIVE_TIMEOUT		2000
    
    // 通信クラス
    class CA2SCommunicator {
    private:
    	// データバッファ
    	char *m_DataBuffer;
    	// 接続してる?
    	bool m_Connected;
    	// ソケット
    	SOCKET m_Socket;
    	// データサイズ
    	int m_DataSize;
    	// データインデクス
    	int m_DataIndex;
    public:
    	// コンストラクタ
    	CA2SCommunicator() {
    		// つながてない
    		m_Connected = false;
    
    		// データねえよ
    		m_DataSize = 0;
    
    		// 予想されるデータ長だけ領域用意しちゃえ
    		m_DataBuffer = new char[RBUFFER_SIZE];
    	}
    	// デストラクタ
    	~CA2SCommunicator() {
    		if (m_Connected) {
    			// つながってんなら切断
    			Disconnect();
    		}
    		delete m_DataBuffer;
    	}	
    
    	// サーバに接続
    	bool Connect(const char* hostAddress, int port);
    
    	// 切断
    	void Disconnect() {
    		closesocket(m_Socket);
    		WSACleanup();
    		m_Connected = false;
    	}
    
    	// つながってる?
    	bool HasConnected() {
    		return m_Connected;
    	}
    
    	// データあんの?
    	bool IsAvailableData() {
    		return (m_DataSize > 0);
    	}
    
    	// 送信
    	bool SendData(const char *buffer, int size, bool setHeader = true);
    	// 受信
    	bool ReceiveData();
    
    private:
    	// データコピー
    	bool DataCopy(void *target, int size) {
    		if ((m_DataSize - m_DataIndex) < size) {
    			// もう取り出せない
    			return false;
    		}
    		memcpy(target, &m_DataBuffer[m_DataIndex], size);
    		m_DataIndex += size;
    		return true;
    	}
    
    public:
    	// データ取り出し
    	template<class T>
    	bool GetData(T *data) {
    		return DataCopy(data, sizeof(T));
    	}
    
    	// string取り出し
    	bool GetString(char **data) {
    		static char tempChar[255];
    		wchar_t tempWchar[255];
    		memset(tempChar, 0, sizeof(tempChar));
    		memset(tempWchar, 0, sizeof(tempWchar));
    
    		if ((m_DataSize - (m_DataIndex + strlen(&m_DataBuffer[m_DataIndex]) + 1)) < 0) {
    			// もう取り出せない
    			return false;
    		}
    
    		// Unicode変換
    		MultiByteToWideChar(
    			CP_UTF8,
    			0,
    			&m_DataBuffer[m_DataIndex],
    			(signed)strlen(&m_DataBuffer[m_DataIndex]),
    			tempWchar,
    			sizeof(tempWchar)
    		);
    
    		// ANSI変換
    		WideCharToMultiByte(
    						CP_ACP,
    						WC_NO_BEST_FIT_CHARS,
    						tempWchar,
    						(signed)wcslen(tempWchar),
    						tempChar,
    						sizeof(tempChar),
    						NULL,
    						NULL
    						);
    
    		*data = tempChar;
    		m_DataIndex += (signed)(strlen(&m_DataBuffer[m_DataIndex]) + 1);
    		return true;
    	}
    
    	// データ表示
    	template<class T>
    	bool ShowData(char *caption, T *data) {
    		T tempData;
    		bool available = DataCopy(&tempData, sizeof(tempData));
    		if (available) {
    			printf(caption, tempData);
    			printf("\n");
    		}
    		if (data != NULL) {
    			*data = tempData;
    		}
    		return available;
    	}
    
    	// string表示
    	bool ShowString(char *caption, char **data) {
    		char* tempData;
    		bool available = GetString(&tempData);
    		if (available) {
    			printf(caption, tempData);
    			printf("\n");
    		}
    		if (data != NULL) {
    			*data = tempData;
    		}
    		return available;
    	}
    };
    
    // サーバに接続
    bool CA2SCommunicator::Connect(const char* hostAddress, int port) {
    	if (m_Connected) {
    		printf("Connect:もう繋がってんだろバカ!\n");			
    	}
    
    	// みなくていいけどWinSockのバージョン確認してみんよ
    	WORD wVersionRequested = MAKEWORD( 2, 2 );
    	WSADATA wsaData;
    	int err = WSAStartup( wVersionRequested, &wsaData );
    	if(err != 0){
    		printf("Connect:WSAStartupだめじゃん\n");;
    		return false;
    	}
    
    	// Socketくれよ
    	m_Socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    	if(m_Socket == INVALID_SOCKET ){
    		printf("Connect:socketだめじゃん\n");
    		return false;
    	}
    
    	// 受信タイムアウト時間設定
    	long timeout = RECEIVE_TIMEOUT;
    	setsockopt(m_Socket, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));
    
    	// つなげれ
    	sockaddr_in sockAddr;
    	memset(&sockAddr, 0, sizeof(sockAddr));
    	sockAddr.sin_family = AF_INET;
    	sockAddr.sin_addr.s_addr = inet_addr(hostAddress);
    	sockAddr.sin_port = htons(port);
    
    	err = connect(m_Socket, (const sockaddr *)&sockAddr, sizeof(sockAddr)); 
    	if(err == SOCKET_ERROR ){
    		printf("Connect:connectだめじゃん\n");
    		WSACleanup();
    		return false;
    	}
    
    	// (・∀・)つながたよ〜
    	m_Connected = true;
    	return true;
    }
    
    // 送信
    bool CA2SCommunicator::SendData(const char *buffer, int size, bool setHeader) {
    	if (!m_Connected) {
    		printf("SendData:繋がってねえよボケ!\n");			
    	}
    
    	int offset = 0;
    	long packetCode = SINGLE_PACKET_CODE;
    	char tempBuffer[MAX_OF_PACKET_SIZE];
    	memset(tempBuffer, 0, sizeof(tempBuffer));
    
    	// 送信バッファ作成
    	if (setHeader) {
    		// ヘッダ挿入の指示がある場合のみ
    		memcpy(tempBuffer, &packetCode, sizeof(packetCode));
    		offset += sizeof(packetCode);
    	}
    	memcpy(&tempBuffer[offset], buffer, size);
    
    	int err = send(m_Socket, tempBuffer, size + offset, 0);
    	if(err == SOCKET_ERROR ){
    		printf("SendData:sendだめじゃん\n");
    		return false;
    	}
    	return true;
    }
    
    // 受信
    bool CA2SCommunicator::ReceiveData() {
    	if (!m_Connected) {
    		printf("ReceiveData:繋がってねえよボケ!\n");			
    	}
    
    	// サイズ初期化
    	m_DataSize = 0;
    
    	// バッファ初期化
    	memset(m_DataBuffer, 0, RBUFFER_SIZE);
    
    	// 1つ目のパケット受信
    	long packetCode;
    	char tempBuffer[MAX_OF_PACKET_SIZE];
    	int dataSize = recv(m_Socket, tempBuffer, sizeof(tempBuffer), 0);
    	if (dataSize < (signed)sizeof(packetCode)) {
    		// 最低限4バイトないとだめ
    		printf("ReceiveData:受信データないよ\n");
    		return false;
    	}
    
    	// 先頭のコードくれや
    	memcpy(&packetCode, tempBuffer, sizeof(packetCode));
    
    	if (packetCode == SINGLE_PACKET_CODE) {
    		// 非分割データ
    		m_DataSize = dataSize - sizeof(packetCode);
    		memcpy(m_DataBuffer, &tempBuffer[sizeof(packetCode)], m_DataSize);
    
    		// データインデックス初期化
    		m_DataIndex = 0;
    		return true;
    	} else if (packetCode != DEVIDED_PACKET_CODE) {
    		// コードが腐ってる
    		printf("ReceiveData:分割コード腐ってる:%d\n", packetCode);
    		return false;
    	} else if (dataSize < (sizeof(long) * 2 + sizeof(char))) {
    		// 分割ヘッダは5バイトないとだめ
    		printf("ReceiveData:分割ヘッダサイズ不足\n");
    		return false;
    	}
    
    	// 第一分割データの情報コピー
    	long requestID = 0;
    	long totalPacket;
    	long currentPacket;
    
    	// 残りのパケットのデータを取る
    	for (int i = 0; i < MAX_OF_PACKET_COUNT; i++) {
    		if (i > 0) {
    			// 1番以降のパケットを受信(0番はもう取ってある)
    			dataSize = recv(m_Socket, tempBuffer, sizeof(tempBuffer), 0);
    		}
    
    		int offset = 0;
    
    		// 分割コード
    		memcpy(&packetCode, tempBuffer, sizeof(packetCode));
    		offset += sizeof(packetCode);
    
    		// 要求ID
    		long oldRequestID = requestID;
    		memcpy(&requestID, &tempBuffer[offset], sizeof(requestID));
    		offset += sizeof(requestID);
    
    		// 全パケット数
    		totalPacket = (tempBuffer[offset] & 0x0F);
    		// 現在のパケット番号
    		currentPacket = (tempBuffer[offset] & 0xF0) >> 4;
    		offset += sizeof(char);
    
    		if (currentPacket != i) {
    			printf("ReceiveData:パケット番号が狂ってる\n");
    			return false;
    		} else if (totalPacket < 2 || totalPacket <= currentPacket) {
    			printf("ReceiveData:パケット個数が狂ってる\n");
    			return false;
    		} else if ( i > 0 && oldRequestID != requestID) {
    			printf("ReceiveData:要求IDの異なるパケットが割り込んできた\n");
    		}
    		memcpy(&m_DataBuffer[m_DataSize], &tempBuffer[offset], (dataSize - offset));
    		m_DataSize += (dataSize - offset);
    	}
    
    	// データインデックス初期化
    	m_DataIndex = 0;
    	return true;
    }
    
    //------------------------------------------------------------------------
    // MASTER SERVER QUERY
    #define MASTER_SERVER_QUERY {			\
    	'1', FILTERING_REGION_CODE, "0.0.0.0:0\0" SERVER_FILTER 	\
    }
    
    #define MASTER_SERVER_QUERY_RES00		0x66
    #define MASTER_SERVER_QUERY_RES01		0x0A
    
    // A2S_INFO
    #define A2S_INFO {				\
    	'T',"Source Engine Query"		\
    }
    #define A2S_INFO_RES				'I'
    
    //------------------------------------------------------------------------
    
    // グローバル君
    CA2SCommunicator g_Com;
    
    //------------------------------------------------------------------------
    // サーバIP
    typedef struct {
    	unsigned char ip[4]; 
    	int port;
    } GSERVER_IP;
    
    //------------------------------------------------------------------------
    
    // A2S_INFO
    bool GetServerInfo() {
    	bool aborted = false;
    
    	// 送信
    	char request[] = A2S_INFO;
    	aborted = !g_Com.SendData(request, sizeof(request));
    
    	if (!aborted) {
    		// 受信
    		aborted = !g_Com.ReceiveData();
    	}
    	if (!aborted && g_Com.IsAvailableData()) {
    		// レスポンスコード
    		char responseCode = '\0';
    		aborted = !g_Com.GetData(&responseCode);
    		aborted = (aborted || responseCode != A2S_INFO_RES);
    		if (!aborted) {
    			int errCount = 0;
    			// サーバ情報を取り出し、表示
    			char tempChar;
    			short tempShort;
    			char *tempString;
    
    			errCount += !g_Com.ShowData("[バージョン] %02X", &tempChar);
    			errCount += !g_Com.ShowString("[サーバ名] %s", NULL);
    			errCount += !g_Com.ShowString("[マップ名] %s", NULL);
    
    			// 空読み
    			g_Com.GetString(&tempString);	// ゲームディレクトリ
    			g_Com.GetString(&tempString);	// ゲームの説明
    			g_Com.GetData(&tempShort);		// AppID
    
    			// 人数
    			unsigned char player = 0;
    			unsigned char maxPlayer = 0;
    
    			errCount += !g_Com.GetData((char*)&player);	// プレイヤー数
    			errCount += !g_Com.GetData((char*)&maxPlayer);	// プレイヤー数
    
    			printf("[プレイヤー数] %d / %d\n", player, maxPlayer);
    
    			aborted = (errCount > 0);
    		}
    	}
    	return !aborted;
    
    }
    
    
    // めーいーんー
    int _tmain(int argc, _TCHAR* argv[])
    {
    	std::list<GSERVER_IP> serverIPList;
    
    	bool aborted = false;
    	
    	//-------------------------------------
    	// マスターサーバ
    	//-------------------------------------
    	// 接続
    	aborted = !g_Com.Connect(MASTER_SERVER_ADDRESS, MASTER_SERVER_PORT);
    
    	if (!aborted) {
    		// 送信
    		char request[] = MASTER_SERVER_QUERY;
    
    		aborted = !g_Com.SendData(request, sizeof(request), false);
    	}
    	if (!aborted) {
    		// 受信
    		aborted = !g_Com.ReceiveData();
    	}
    	if (!aborted  && g_Com.IsAvailableData()) {
    		// IP取込み
    		char tempChar;
    		long tempLong;
    		unsigned short tempUShort;
    		
    		// レスポンスコード00
    		aborted = !g_Com.GetData(&tempChar);
    		aborted = (aborted || tempChar != MASTER_SERVER_QUERY_RES00);
    
    		// レスポンスコード01
    		aborted = !g_Com.GetData(&tempChar);
    		aborted = (aborted || tempChar != MASTER_SERVER_QUERY_RES01);
    
    		while(!aborted && g_Com.GetData(&tempLong)) {
    			//ip が全部0の場合は終了(データ終端)
    			if (tempLong == 0) {
    				break;
    			}
    
    			GSERVER_IP ip;
    			for (int i = 0; i < sizeof(ip.ip); i++) {
    				memcpy(&ip.ip[i], &tempLong, sizeof(char));
    				tempLong >>= 8;
    			}
    			if (g_Com.GetData((short*)&tempUShort)) {
    				// ポートだけはビッグエンディアン
    				ip.port = tempUShort & 0xFF;
    				ip.port <<= 8;
    				tempUShort >>= 8;
    				ip.port |= (tempUShort & 0xFF);
    
    				serverIPList.insert(serverIPList.end(), ip);
    			} else {
    				aborted = true;
    			}
    		}
    	}
    
    	// 切断
    	g_Com.Disconnect();
    	int index = 0;
    	for (std::list<GSERVER_IP>::iterator i = serverIPList.begin();
    				i != serverIPList.end() && !aborted; ++i, index++) {
    		char addressString[16];
    		GSERVER_IP ip = *i;
    
    		printf("サーバ %04d ------------------------------------------\n", index);
    
    		// アドレス
    		sprintf(addressString, "%d.%d.%d.%d", ip.ip[0], ip.ip[1], ip.ip[2], ip.ip[3]);
    		printf("ADDR=%s:%d\n", addressString, ip.port);
    
    		// 接続
    		aborted = !g_Com.Connect(addressString, ip.port);
    
    		if (!aborted) {
    			// 失敗しても続行させる
    			GetServerInfo();
    		}
    
    		// 切断
    		g_Com.Disconnect();
    	}
    
    	if (aborted) {
    		printf("エラーが発生しています。\n");
    	}
    	return 0;
    }
    

    (´・ω・`)つもどる