ソケットレベルのプログラミングを行っている場合は、メッセージタイプごとに開くポートの数に関係なく、何らかのヘッダーが必要です。メッセージの残りの長さだけであっても。メッセージに単純なヘッダーとテールの構造を追加するのは簡単だと言っています。クライアント側のポートを1つだけ処理する方が簡単だと思います。
現代のMMORPG(そしておそらく古いものでさえ)には2つのレベルのサーバーがあったと思います。有料クライアントであることを確認するログインサーバー。確認されると、これらはすべてのゲームワールド情報を含むゲームサーバーに渡されます。それでも、クライアントが1つのソケットを開いている必要があるだけですが、それ以上のソケットを開くことはできません。
さらに、ほとんどのMMORPGは、すべてのデータも暗号化します。あなたが楽しみのための練習としてこれを書いているなら、これはそれほど重要ではありません。
一般的なプロトコルの設計/作成について、私が心配していることは次のとおりです。
エンディアン
クライアントとサーバーは常に同じエンディアンを持つことが保証されていますか?そうでない場合は、シリアル化コードでそれを処理する必要があります。エンディアンを処理する方法は複数あります。
- 無視する-明らかに悪い選択
- プロトコルのエンディアンを指定します。これは、古いプロトコルが行った/行ったことであり、したがって、常にビッグエンディアンであったネットワーク順序という用語です。どちらか一方を指定するだけで、どちらのエンディアンを指定するかは実際には重要ではありません。ビッグエンディアンを使用しない場合、一部の無愛想な古いネットワークプログラマーは腕を組むでしょうが、サーバーとほとんどのクライアントがリトルエンディアンである場合、プロトコルをビッグエンディアンにすることで余分な作業以外のものを購入することはありません。
- 各ヘッダーにエンディアンをマークする-クライアント/サーバーのエンディアンを通知するCookieを追加し、必要に応じて各メッセージを適宜変換することができます。残業!
- プロトコルにとらわれないようにする-すべてをASCII文字列として送信する場合、エンディアンは無関係ですが、はるかに非効率的です。
4つのうち通常は2つを選択し、エンディアンをクライアントの大多数のエンディアンになるように指定します。これは現在、リトルエンディアンになります。
前方互換性と後方互換性
プロトコルは下位互換性と下位互換性が必要ですか?ほとんどの場合、答えは「はい」です。この場合、これにより、バージョン管理の観点からプロトコル全体をどのように設計するか、および実際にはバージョン管理の一部であってはならない小さな変更を処理するために個々のメッセージをどのように作成するかが決まります。これをパントしてXMLを使用することはできますが、効率が大幅に低下します。
全体的なバージョン管理のために、私は通常、単純なものを設計します。クライアントは、バージョンXYを話すことを指定するバージョン管理メッセージを送信します。ただし、サーバーがそのバージョンをサポートできる限り、クライアントはクライアントのバージョンを確認するメッセージを送り返し、すべてが先に進みます。それ以外の場合は、クライアントをナックして接続を終了します。
メッセージごとに、次のようなものがあります。
+-------------------------+-------------------+-----------------+------------------------+
| Length of Msg (4 bytes) | MsgType (2 bytes) | Flags (4 bytes) | Msg (length - 6 bytes) |
+-------------------------+-------------------+-----------------+------------------------+
長さは、メッセージの長さを示しており、長さ自体は含まれていません。MsgTypeは、メッセージのタイプです。65356はアプリケーション用のメッセージタイプが豊富であるため、これには2バイトしかありません。フラグは、メッセージでシリアル化されているものを通知するためにあります。このフィールドと長さの組み合わせにより、下位互換性と下位互換性が得られます。
const uint32_t FLAG_0 = (1 << 0);
const uint32_t FLAG_1 = (1 << 1);
const uint32_t FLAG_2 = (1 << 2);
...
const uint32_t RESERVED_32 = (1 << 31);
次に、逆シリアル化コードは次のようなことを行うことができます。
uint32 length = MessageBuffer.ReadUint32();
uint32 start = MessageBuffer.CurrentOffset();
uint16 msgType = MessageBuffer.ReadUint16();
uint32 flags = MessageBuffer.ReadUint32();
if (flags & FLAG_0)
{
// Read out whatever FLAG_0 represents.
// Single or multiple fields
}
// ...
// read out the other flags
// ...
MessageBuffer.AdvanceToOffset(start + length);
これにより、プロトコル全体を改訂しなくても、メッセージの最後に新しいフィールドを追加できます。また、古いサーバーとクライアントが知らないフラグを無視することも保証します。新しいフラグとフィールドを使用する必要がある場合は、プロトコル全体のバージョンを変更するだけです。
フレームワークを使用するかどうか
ビジネスアプリケーションに使用することを検討するさまざまなネットワークフレームワークがあります。特にスクラッチする必要がない限り、標準のフレームワークを使用します。あなたの場合、ソケットレベルのプログラミングを学びたいので、これはすでにあなたのために答えられた質問です。
フレームワークを使用する場合は、上記の2つの懸念事項に対応していることを確認してください。または、これらの領域でフレームワークをカスタマイズする必要がある場合は、少なくとも邪魔になりません。
私は第三者と取引していますか
多くの場合、通信する必要のあるサードパーティのサーバー/クライアントを扱っている可能性があります。これは、いくつかのシナリオを意味します。
- 彼らはすでにプロトコルを定義しています-単に彼らのプロトコルを使用してください。
- あなたはすでにプロトコルを定義しています(そして彼らはそれを喜んで使用します)-再び簡単に定義されたプロトコルを使用します
- 標準のフレームワーク(WSDLベースなど)を使用します-フレームワークを使用します。
- どちらの当事者にもプロトコルが定義されていません-手元にあるすべての要因(ここで言及したすべての要因)とその能力のレベル(少なくともあなたが知る限り)に基づいて、最良の解決策を決定するようにしてください。いずれにせよ、双方がプロトコルに同意し、理解していることを確認してください。経験から、これは苦痛または楽しいことがあります。それはあなたが誰と働いているかによります。
どちらの場合も、サードパーティとは連携しないため、これは完全を期すために追加されたものです。
これについてもっともっと書けるような気がしますが、もうかなり長いです。これがお役に立てば幸いです。具体的な質問がある場合は、Stackoverflowで質問してください。
knoopxの質問に答えるための編集: