自宅で叀兞的なRTSをれロから䜜成するストヌリヌパヌト2「埩掻」蚘事の終わりネットワヌク







箄1幎前、私の蚘事が出おきたした。これはこの蚘事の「 最初の郚分 」ず呌ぶこずができたす。 最初の郚分では、できる限り、熱心な開発者の困難な道を敎理したした。 これらの努力の結果、自宅で゚ンゞン、デザむナヌ、その他の最新の開発ツヌルなしで䜜成されたRTSゞャンル「 Land of Onimode 」のゲヌムができたした。 プロゞェクトでは、 C ++ずAssemblerが䜿甚され、メむンツヌルずしお私自身の頭が䜿甚されたした。



この蚘事では、私がどのように「蘇生者」の圹割を匕き受け、このプロゞェクトを「埩掻させる」こずを決めたかに぀いおお話したす。 独自のゲヌムサヌバヌの䜜成には、倚くの泚意が払われたす。



これで蚘事の終わりです 。始たりはこちらです。



→ 蚘事の冒頭ゲヌムの埩掻

→ 蚘事の続きGUI



ネットワヌク



添付の䟋を分析しなければ、私が話そうずしおいるこずの耇雑さを理解できるずは思いたせん。 しかし、いずれにせよ、この䞻題に関する䞀般的な考えを䌝えるこずができるこずを願っおいたす。 たた、私は自分でそのような解決策を考え出すこずを別に泚意する必芁がありたす。そのため、問題に察する私の解決策の䞀郚が次善ずなるリスクが垞にありたす。 私はlibuvネットワヌクのラむブラリを䞻に「科孊的な突く」方法で研究したした。たた、ネットワヌクプログラミングに垞に携わっおいる人々が私の解釈の䞀郚を修正できるこずも認めおいたす。 そしお今、おそらく、私は蚘事のトピックに戻りたす...



最初は、 WinSockラむブラリをネットワヌクに䜿甚したかったのです。 しかし、圌はすぐに考えを倉えたした。この方法で私が再び完党にWindowsに接続されるこずは明らかだったからです。 それで、私はむンタヌネットを調べお、 libuvず呌ばれる興味深い解決策を芋぀けたした。 このラむブラリは無料です。 Windows 、 Unix 、 Mac OS 、 Androidで 動䜜したす 。 たた、最新のC ++蚀語暙準を䜿甚する芁件など、最新の技術革新を匕き付けたせん。 ずにかく、それは玔粋なCで曞かれおおり、これは開発者にずっおさらなる利点になるず考えおいたす。



最初は、2぀の問題がありたした。



  1. サヌバヌをれロから䜜成したこずはないため、䞀般的なプログラム構造を考え出す必芁がありたした。



  2. 私の意芋では、 libuvのドキュメントには倚くの芁望がありたす。 䞀般的に、私はい぀もあなたが非垞にたずもな補品を䜜り、倚かれ少なかれその機胜を説明するのが面倒だず思うのです。 しかし、残念ながら、ほずんどすべおの開発者がこれに苊しんでいたす。 この分野の怠の最埌の段階は、 Doxygenナヌティリティなどを䜿甚した自動ドキュメント生成の䜿甚です。これにより、コメントがコヌドに倉換され、クラスず構造間のリンクが自動的に生成されたす。 私はおそらく人生に遅れをずっおいたかもしれたせんが、自分のラむブラリに害を及がすより良い方法は、あらゆる皮類の構造や図から自動的にガベヌゞビンを生成するこずよりもよくありたせん。


私の2番目の問題は、 WinSockも䜿甚したこずがないずいう事実によっお郚分的に補われたため、 WinSockたたはlibuvをどのように扱うかはそれほど重芁ではありたせんでした。 しかし、 libuvは䜕かを曞き換える必芁なくいく぀かのプラットフォヌムを玄束しおくれたので、私の目には間違いなく勝ちたした。



珟圚、ゲヌム開発者はロヌカルネットワヌクでプレむする機胜を削陀するこずが倚いずいう事実にもかかわらず、私はこのバヌゞョンのネットワヌクゲヌムも保持するこずにしたした。 蚘事の最初の郚分で、ロヌカルネットワヌクでRTSをプレむする堎合、ゲヌムは各コンピュヌタヌで独立しお動䜜し、調敎センタヌは必芁ないため、サヌバヌはたったく必芁ないず個人的に説明したした。 そしお、本圓にそうです。 しかし、私の堎合、ネットワヌクゲヌムの2぀のオプションを取埗したいず考えたした。むンタヌネット経由ずロヌカルネットワヌク経由です。 ロヌカルネットワヌクでコンピュヌタヌ盞互䜜甚のピアツヌピア方匏を䜿甚し始めた堎合、このような゜リュヌションはコヌドの2番目のブランチを䜜成したす。 そのような゜リュヌションはより速く動䜜したすか おそらくそうですが、LANではデヌタは通垞すぐにタヌゲットに到達するため、プレむダヌが違いを感じるこずはたずありたせん。



その結果、ロヌカルネットワヌクにもサヌバヌを䜿甚するず論理的に刀断したした。



アドレスずポヌトに関する少しの理論



倚くの人がおそらく知っおいるように、むンタヌネットに接続されおいる䞖界の各コンピュヌタヌには䞀意のアドレスを割り圓おる必芁がありたすもちろん、NATが䜿甚されおいるか、同様の堎合を陀きたす。 このアドレスはIPず呌ばれたす。 IPアドレスは4桁で構成され、「人間の圢」では次のようになりたす234.123.34.18



これらのアドレスで、コンピュヌタヌはお互いを芋぀けたす。 ただし、通垞、倚くのプログラムが同じコンピュヌタヌで同時に実行されおいるため、「ポヌト」ずいう抂念が远加されおいたす。 プログラムはこれらのポヌトを怜出し、それらを介しお盞互䜜甚を確立できたす。 より明確にするために... IPアドレスは、ロシア、ブハロフスカダ州、ボリショむゎロドゥヒノ村、ulです。 「New Russian Descent」、d.18、および「port」は、特定の人が䜏んでいるお金のあるアパヌトの番号です。 アパヌトポヌト番号がないず、特定の人プログラムに「手玙」を届けるこずができたせん。したがっお、「ポヌト」の抂念は非垞に重芁です。 通垞、ポヌトはIPアドレスの埌にコロンを介しお曞き蟌たれたす䟋234.123.34.18:57。



メッセヌゞは、プロトコルず呌ばれる特別なプログラムによっおコンピュヌタヌ間で配信されたす。 むンタヌネット党䜓が眮かれおいる最も有名なプロトコルはTCPず呌ばれたす 。 UDPず呌ばれる別の非垞に重芁なプロトコルがありたすが、それを䜿甚するこずはやや困難です。



このトピックにあたり詳しくない人のために、その違いを簡単に説明しおください。



UDPを䜿甚するず、デヌタをバッチで転送できたす。 このデヌタの断片はデヌタグラムず呌ばれたす。 デヌタグラムは、指定されたIPアドレスずポヌトに送信できたす。 しかし... UDPプロトコルは 「䜕も玄束したせん」、぀たり 送信されたデヌタグラムは途䞭で簡単に倱われる可胜性があり、この堎合、受信者は䜕も受信したせん。 そしお、いく぀かのデヌタグラムが送信された堎合、それらは異なる順序で来るか、たたはいく぀かは来ないかもしれたせん。 ここでは任意のオプションが可胜であり、これはUDPの完党に正圓な動䜜です。 UDPを保蚌する唯䞀のこずは、デヌタグラムがただ到着した堎合、完党に到着したずいう事実です。 デヌタグラムの「半分」は来るこずができたせん。



TCPは完党に異なる原理で機胜したす。 圌は、理解できないデヌタグラムを運呜に翻匄されお未知のネットワヌク空間に送信するこずはありたせん。最初に受信者ずの通信チャネルを確立し、このチャネルを通じおデヌタを明確に送信したす。 TCPは、すべおの送信デヌタが受信者に届き、送信された正確な順序で到着するこずを保蚌したす。 デヌタ自䜓は、固定長のデヌタグラムの圢匏ではなく、バむトストリヌムの圢匏で提䟛されたすファむルにバむトデヌタを蚘録するのが良い䟋えです。 このアプロヌチでは、 TCPは送信されたメッセヌゞを必芁に応じお郚分に分割できるこずに泚意しおください。 受信者は最初に送信されたデヌタの䞀郚のみを受信でき、しばらくするず残りの半分が時間内に到着したす。



プログラムずポヌト間の通信には、いわゆる゜ケットが䜿甚されたす。 ゜ケットはポヌトに接続され、ポヌトずのすべおの通信はこの゜ケットを介しお行われたす。 ゜ケットはポヌトず察話するこず以倖は䜕も凊理しないため、ポヌトのない゜ケットの存圚はあたり意味がありたせん。



プロトコル遞択



初期段階では、盞互䜜甚のためのプロトコルを遞択する必芁がありたした。 独自の゜リュヌションを䜜成しない堎合、 TCPずUDPの 2぀のオプションしかありたせん。 UDPは、ゲヌムプレむの遅延を蚱可できない堎合に適しおいたす。 通垞、ゲヌム自䜓はサヌバヌで実行されたすが、クラむアントはゲヌムの状況の倉化に関するデヌタのみをサヌバヌから受信したす。 このアプロヌチにより、接続が䞍十分なプレむダヌを文字通り「無芖」できたす。 サヌバヌは遅れおいるゲヌムを埅぀ためにゲヌムを停止しないため、他のすべおのプレむダヌは非垞に快適にプレむを続けたす。 そのようなゲヌムの䟋はCounter Strikeです。



RTSの堎合、ゲヌムプロセスは倚くの蚈算を必芁ずし、したがっお各コンピュヌタヌで実行されるため、このアプロヌチはほずんどの堎合䞍適切です。これらのコンピュヌタヌはすべお同じ方法で実行する必芁がありたす。 したがっお、他の各コンピュヌタヌの各コンピュヌタヌは、1぀の「ネットワヌクタクト」でプレヌダヌが実行したアクションのリストを垞に受信する必芁がありたす。 リストが遅れおいる堎合は、受信されるたで埅぀必芁がありたす。 ぀たり UDPを䜿甚する堎合でも、メッセヌゞの配信を自分で制埡する必芁がありたす。 したがっお、 TCPが遞択されたした。



私が聞いたように、Starcraft2はRTSゞャンルに属しおいるずいう事実にもかかわらず、䟝然ずしおサヌバヌ䞊で動䜜したす。 珟代の鉄は、この方法を䜿甚するためにそのようなレベルに達しおいる可胜性がありたす。 しかし、私の堎合、ネットワヌクはRTSの 「クラシック」になりたす。



サヌバヌは䜕をしたすか



実際、圌はクラむアントからメッセヌゞを受け取るたでほずんど䜕もしたせん。 TCPの堎合、サヌバヌタスクはポヌトを開いおリッスンするこずです。 クラむアントがこのポヌトぞのCONNECT接続を確立する芁求を受信した堎合、サヌバヌはACCEPTを実行する必芁がありたす。ACCEPTは新しいランダムポヌトを開き、クラむアントに報告したす。 サヌバヌからランダムなポヌトを受け取ったクラむアントは、独自のランダムなポヌトを開き、これらのポヌトでサヌバヌずクラむアントの間に接続が確立されたす。 接続は、圓事者の1人が接続を閉じるたで、たたは技術的な理由で接続が切断されるたで存圚したす。 サヌバヌは、各クラむアントず1぀の接続を確立したす。 クラむアントが互いに送信するすべおのデヌタはサヌバヌを通過したすが、ピアツヌピアネットワヌクの堎合のように、クラむアントからクラむアントに盎接送信されるこずはありたせん。



私のサヌバヌは次のタスクを実行したす。





クラむアントは䜕をしたすか



ゲヌム自䜓はクラむアントで行われたす。 クラむアントは、自分がゲヌムに䞀人ではないこずを知っおおり、プレヌダヌの行動に関する他の参加者メッセヌゞを送信し、同じメッセヌゞを受信する必芁がありたす。





むンタヌネットサヌバヌずLANサヌバヌの違い



逆説的に聞こえるかもしれたせんが、LANサヌバヌはむンタヌネットサヌバヌよりも耇雑です。 なぜそう



むンタヌネットサヌバヌは別のプログラムずしお動䜜したす。このプログラムは䞀床どこかで起動され、理想的には氞久に動䜜するはずです。 ロヌカルサヌバヌは、ホストずしお機胜するコンピュヌタヌ䞊に䞀時的に䜜成されたす。 このようなサヌバヌは、別個のプログラムの圢ではなく、メむンアプリケヌションのデヌタにアクセスする別個のストリヌムの圢でのみ存圚したす。これは、スレッドの同期によっお凊理される小芏暡な問題を䌎いたす。 たた、「マルチスレッド」は通垞、デバッグの独立したセクションであり、開発者の生掻を長く損なう可胜性がありたす。



さらに、クラむアントをむンタヌネットサヌバヌずロヌカルサヌバヌに接続する原理は完党に異なるこずに泚意しおください。 クラむアントがむンタヌネットサヌバヌに接続できるようにするには、クラむアントはIPアドレスずポヌトを知っおいる必芁がありたす。 そしお、このデヌタは、プレヌダヌによっおテキストフィヌルドに瀺される必芁がありたす。 たた、ロヌカルネットワヌクでは、IPアドレスを指定せずに既存のゲヌムセッションを怜出する必芁がありたす。 このアクションは、IPが255.255.255.255である、いわゆる「ブロヌドキャストリク゚スト」によっお実珟されたす。 このアドレスは「ロヌカルネットワヌク党䜓」を意味したすが、 TCPの堎合は最も玔粋な圢匏のUDP機胜であるため機胜したせん。 TCPがブロヌドキャストアドレスで動䜜しないのはなぜですか さお、䞊で説明したように、 TCPは通信チャネルを確立し、厳密に䞀察䞀で通信する必芁がありたす。 そしおここで、「ねえ、ここに誰かいる」ずいう原則に基づいお、ロヌカルネットワヌク党䜓を「呌びかける」必芁がありたす。 答えおくれ」 さお、「いる」人は応答し、ゲヌムセッションに関する申請者情報を枡さなければなりたせん。 したがっお、ロヌカルサヌバヌでUDPを䜿甚する必芁がありたす。



さお、デザヌトに぀いおは...ゲヌム䞭にロヌカルサヌバヌを実行しおいるホストが突然倱われた、たたは䜕らかの理由でゲヌムを終了したず想像しおください。 あなたはどうなりたすか ゲヌムを䜜成した人が残りの前にゲヌムセッションを終了するこずにしたずき、ゲヌムDiablo2で発生する同じこずが起こりたす。 Diablo2では 、残りの䞍幞なこずに、「ホストが利甚できなくなりたした」ずいうスタむルでメッセヌゞが黒い画面にポップアップ衚瀺されたす。実際、それだけです...プレむダヌはゲヌムから远い出されたす。 この動䜜の理由は、ホストがセッションを離れるず、ホストがロヌカルサヌバヌを閉じ、すべおのプレヌダヌが接続されるためです。 このい動䜜に察凊するため、 DirectPlayにはか぀おホスト移行ず呌ばれる玠晎らしい機胜がありたした。 䞀蚀で蚀えば、このように芋えたす...その突然の死に関する情報がロヌカルサヌバヌから来るず、残りのクラむアントは新しいサヌバヌを起動しお再接続するこずを決定したす。 ロヌカルサヌバヌの起動はクラむアントで実行できたす。これは、セッション内のプレヌダヌのリストの䞀番最初です。 その埌、サヌバヌは、以前にゲヌムに参加しおいた党員が接続できるようになるたで埅機する必芁がありたす。



ただし、むンタヌネットサヌバヌには独自の特性もありたす。 䞻な機胜は、開発者たたはOSの゚ラヌにより、サヌバヌがクラッシュたたはフリヌズする危険性が垞にあるこずです。 この堎合、珟圚ゲヌムに参加しおいるすべおのプレむダヌは非垞にむラむラしたす。 しかし、この状況では、リリヌス前にできるだけ倚くのバグを特定しようずするこずを陀いお、䜕もするこずはほずんど䞍可胜です。 しかし、それでも、゚ラヌは耇雑なプログラムに残っおおり、唯䞀の問題はそれらの発生の可胜性です。



しかし、サヌバヌが突然䜿甚されお「死亡」し、プレヌダヌが切断されたず仮定したす。 開発者が「曲がった手」に察する呪いをやめた埌、プレヌダヌは通垞、サヌバヌに再び接続しようずしたす。 しかし...サヌバヌが「死んだ」堎合、誰かが再起動するたで動䜜したせん。サヌバヌを監芖しおいる人がいない堎合、これはすぐに起こりたす。 その結果、プレヌダヌの怒りが自然に増倧し始め、深刻な心理的結果に぀ながる可胜性がありたす。 圌はひどく萜ち始め、テストは急激に悪化し、実際には、その人は神経衰匱に近くなりたす。 個人的には、そのような責任を負いたくないので、事前に安党にプレむするこずにしたした。



むンタヌネットサヌバヌは独立しお動䜜するべきではありたせん。サヌバヌを起動しおその動䜜を監芖しようずするコントロヌラヌが必芁です。 これには、サヌバヌに定期的に信号を送信し、応答を受信する必芁がありたす。 信号に応答がない堎合、これはサヌバヌが「クラッシュ」しおいるこずを意味したす。 この堎合、最初にサヌバヌプロセスを完党に匷制終了しおから、サヌバヌを再起動する必芁がありたす。 サヌバヌが実際にどれほど効果的かはわかりたせんが、このアプロヌチは䜕らかの圢でサヌバヌを保護するはずですが、これが正しい方向ぞの動きであるこずは間違いありたせん。



さらに、コントロヌラヌプログラムはいく぀かの远加アクションを実行できたす。 たずえば、サヌバヌを曎新するように圌女に指瀺できたす。 私の堎合、次のようになりたす。



1たずえば、サヌバヌ䞊で䜕かを修正しお新しいバヌゞョンのサヌバヌを䜜成するこずにしたしたが、誰かがゲヌムに興味を持っおいる堎合は、誰かがサヌバヌ䞊で垞にプレむしおいたす。 サヌバヌを再起動するず、プレむダヌはゲヌムから远い出されたすが、これには垞に感情的な苊痛が䌎いたす。



2これを知っお、サヌバヌず監芖ナヌティリティに「゜フトモヌド」でサヌバヌを曎新するこずを教えたす。これは次のように実行されたす。 管理ナヌティリティを介しお、新しいサヌバヌは通垞のファむルの圢匏で転送され、メむンサヌバヌが実行されおいるコンピュヌタヌに届きたす。 その埌、メむンサヌバヌはリッスンポヌトを閉じたすが、シャットダりンしたせん。぀たり、実際には新しいプレヌダヌは参加できなくなり、「すでにここにいる」ナヌザヌは萜ち着いおプレむし続けたす。 さらに、このサヌバヌは、すべおのプレむダヌがサヌバヌから切断する状況を定期的にチェックし、その埌静かに䜜業を完了したす。 この時点で、コントロヌラヌプログラムは新しいサヌバヌの送信ファむルを怜出し、叀いサヌバヌず䞊行しお実行したす。 リスニングポヌトは叀いサヌバヌから既に解攟されおいるため、新しいサヌバヌのリッスンを開始したす。これにより、すべおの新しいプレヌダヌが接続されたす。 しばらくするず、叀いサヌバヌは自動的にオフになり、サヌバヌの新しいバヌゞョンが1぀だけ動䜜したす。



これはかなり良いように聞こえたすが、残念ながら、この蚘事を曞いおいる時点では、実際の負荷がかかっおいる状態でサヌバヌをチェックできたせんでした。 これは、熱心にプロゞェクトを開発する際の兞型的な問題です。問題が発生したこずを単に報告しお報告するだけのテスタヌチヌムがいない堎合です。 しかし、垞識は私が正しい方向にすべおをしおいるこずを教えおくれるので、このテヌマに関する私の考えを蚘事に含めたした。



Libuvラむブラリ関数



このセクションでは、ネットワヌキングの開発䞭に察凊しなければならなかったlibuvラむブラリの機胜に぀いお簡単に説明したす。 良い蚘憶は私の匷い品質ではないので、少なくずも、この情報は私にずっおも圹立぀参考になるこずがありたす。



libuvずナヌザヌ間の盞互䜜甚は 、コヌルバック関数たたはコヌル バック関数の䜿甚に完党に基づいおいたす。 どのように機胜したすか たずえば、ナヌザヌがTCPを介しおメッセヌゞを送信したいずしたす 。 このために、 uv_write関数が䜿甚されたす。これは、もちろん、「送信するもの」および「どの゜ケットを経由するか」をパラメヌタヌずしお受け取りたす。 ただし、これに加えお、送信が正垞に完了したずきに呌び出されるナヌザヌ定矩関数のアドレスも指定する必芁がありたす。 発生するむベントを制埡できるのは、これらの関数です。 メッセヌゞの受信にも同じこずが圓おはたりたすが、これにはuv_read_start関数が䜿甚されたす。これは、デヌタの別の郚分を受信した埌に呌び出されるナヌザヌ定矩関数も瀺したす。



最も重芁な関数はuv_run関数です。これは基本的にWindowsのメッセヌゞ凊理ルヌプのようなものです。ラむブラリは、uv_run内でのみコヌルバック関数を呌び出したす。これは、libuv自䜓が䜕らかの皮類のメッセヌゞをネットワヌク経由で受信した堎合でも、ナヌザヌはuv_runが呌び出されるたでそのこずを知らないこずを意味したす。uv_run



関数は次のように宣蚀されたす。



int uv_run(uv_loop_t* loop, uv_run_mode mode);
      
      





最初のパラメヌタヌuv_loop_t * loopは、 libuvが個人的なニヌズのいく぀かに䜿甚する構造䜓ぞのポむンタヌです。 この倉数を䞀床䜜成する必芁があり、二床ず觊れないでください。 たずえば、次のように䜜成できたす。



 uv_loop_t loop; memset(&loop, 0, sizeof(loop)); uv_loop_init(&loop);
      
      





これがルヌプパラメヌタのすべおであり、 libuv内でどのように䜿甚されるかを知る必芁は特にありたせん。 しかし... 1぀の倧きなニュアンスがありたす。 マルチスレッドプログラムを䜿甚しおいる堎合、スレッドごずに独自のルヌプを䜿甚する必芁がありたす 。 私の堎合、ゲヌム自䜓に1぀のルヌプを䜜成し、別のスレッドで実行されるロヌカルサヌバヌに2぀目のルヌプを䜜成したす。 それに応じお、各スレッドで独自のuv_runが呌び出されたす。



2番目のパラメヌタヌuv_run_mode modeは、 uv_run関数が機胜するモヌドを決定したす。 サヌバヌの堎合は倀UV_RUN_DEFAULTを䜿甚し、クラむアントの堎合はUV_RUN_NOWAITを䜿甚したす 。 理由を理解しおみたしょう。



UV_RUN_DEFAULTパラメヌタヌにより、少なくずも䜕らかの䜜業が行われおいる限り、 uv_run関数が実行されたす。 そしお、そのような仕事は、䟋えば、ポヌトを聞くタスクです。 ぀たり ポヌトをリッスンする゜ケットが最初に䜜成された堎合、 uv_runはその゜ケットが存圚しおいる間は終了したせん。 そしお、これはサヌバヌのメむンタスクです-クラむアントからの接続を埅っおそれを確立する。 したがっお、 UV_RUN_DEFAULTを䜿甚したオプションはサヌバヌにずっお非垞に適切であり、次の行です。



 uv_run(&loop, UV_RUN_DEFAULT)
      
      





倚くの堎合、プログラムの最埌の行です。このサむクルを終了するず、サヌバヌが単玔にシャットダりンするためです。



ナヌザヌがリスニング゜ケットを砎棄するず、 uv_runloop、UV_RUN_DEFAULT関数は自動的に終了したす。



uv_runloop、UV_RUN_DEFAULT関数からの緊急終了には、 uv_stop 関数が䜿甚されたす。これは、パラメヌタヌず同じルヌプを取りたす。 このような呌び出しの埌、 uv_runは終了したすが、゚ラヌが返されたす。これは、すぐに䞭断され、ただ䜕かするこずがあるこずを意味したす。 ちなみに、この堎合、誰もuv_runを再床呌び出す必芁はありたせん。



UV_RUN_NOWAITパラメヌタヌは、 uv_run関数がこれたでに発生したむベントのみを凊理するように匷制したす。 ぀たり、ネットワヌクメッセヌゞが受信された堎合、コヌルバック関数が呌び出されたす。 その埌、 uv_run関数が完了したす。 クラむアントは、ネットワヌクメッセヌゞの亀換に加えお、ゲヌム自䜓も凊理するため、この動䜜はクラむアントに最適です。 私の堎合、 uv_runloop、UV_RUN_NOWAITをゲヌムメゞャヌの開始時に1回、終了時に1回クロック呚波数玄60 Hz呌び出したす。 これは、ビヌトの開始前に受信したメッセヌゞを凊理し、ビヌトの盎埌に独自のメッセヌゞを送信できるようにするために行われたす。



前述のように、 TCPには接続が必芁です。 接続芁求は、クラむアントによっお垞に特定のIPアドレスずサヌバヌポヌトに送信されたす。 このために、 uv_tcp_connect関数が䜿甚されたす。



uv_tcp_connect関数は次のように宣蚀されたす。



 int uv_tcp_connect(uv_connect_t* req, uv_tcp_t* handle, const sockaddr* addr, uv_connect_cb cb);
      
      





最初のパラメヌタヌuv_connect_t * reqは、䜕らかの構造䜓ぞのポむンタヌです 。明らかに、 libuvは䜕かのために本圓に必芁です 。 ナヌザヌのタスクは、この構造を䜜成しお関数に枡すこずです。 構造の䜜成は単玔ではありたせん



 uv_connect_t connect_data;
      
      





念のため、れロを曞き蟌みたすが、これは必芁ではないようです



 memset(&connect_data, 0, sizeof(connect_data));
      
      





たた、この倉数は、アドレスがコヌルバック関数で䜿甚されるため、 uv_tcp_connectが呌び出された埌も存圚し続ける必芁があるこずに泚意しおください。



2番目のパラメヌタヌuv_tcp_t *ハンドルは、事前に䜜成する必芁があるが、どのポヌトにもバむンドされおいないTCP゜ケットです。 TCP゜ケットの䜜成はuv_tcp_init関数によっお実行されたす。これに぀いおは埌で説明したす。



3番目のパラメヌタヌconst sockaddr * addrは、接続が芁求されおいるサヌバヌのIPアドレスずポヌトです。 Libuvには、この構造にデヌタを取り蟌むのに圹立぀uv_ip4_addr関数がありたす。



 sockaddr_in dest; uv_ip4_addr("234.123.34.18", 57, &dest);
      
      





4番目のパラメヌタヌuv_connect_cb cbは、カスタムコヌルバック関数です。 たた、この機胜では、ナヌザヌは接続が確立されたかどうかを刀断し、䜕らかの圢でこの事実に反応するこずができたす。



私の堎合、コヌルバック関数は次のようになりたす。



 void OnConnect(uv_connect_t* req, int status) { if (status==0) { //   ... ... ... } else { //    ... ... ... } }
      
      





゜ケットを䜜成する



TCP゜ケットは 、 uv_tcp_t構造䜓によっお蚘述されたす 。 たず、この構造にメモリを割り圓おる必芁がありたす。



 uv_tcp_t* tcp_socket=malloc(sizeof(uv_tcp_t));
      
      





垌望する堎合は、割り圓おられたメモリをれロでクリアできたすが、これはオプションの操䜜です。



 memset(tcp_socket, 0, sizeof(uv_tcp_t));
      
      





次に、゜ケット自䜓を䜜成できたす。



 uv_tcp_init(&loop, tcp_socket);
      
      





here ルヌプは、 uv_runで提䟛される同じ苊痛のルヌプです。



そしお今、ゲヌム䜜家にずっお非垞に重芁なポむント



 uv_tcp_nodelay(tcp_socket, true);
      
      





この蚭定が䜕であるかを説明しようずしたす。 実際には、 TCPプロトコルはそれ自䜓を非垞にスマヌトなプロトコルず芋なし、送信されるデヌタの最小サむズはいずれにしおもパケットサむズず等しくなるため、小さな郚分でデヌタを送信するこずは有益ではないこずを知っおいたす。 蚀い換えるず、1バむトを送信するず、結果ずしおパケット党䜓が送信されたす。この堎合、1バむトの有甚なデヌタのみが送信され、残りはゎミになりたす。 したがっお、デフォルトでは、スマヌトTCPは、ナヌザヌが送信するデヌタを远加しおすべおを䞀床に転送するたで200ミリ秒埅機したす。 この埅機メカニズムは「 Nagle Algorithm 」ず呌ばれ、ゲヌムにはたったく適しおいたせん。 したがっお、この蚭定は、そのたたで、 TCPプロトコルに指瀺したす-「聞いお、芪愛なる、賢くなくお、すぐにデヌタを送信したしょう」。 実際には、この蚭定はTCPプロトコルがネむルアルゎリズムを䜿甚するこずを犁止したす 。



これがサヌバヌの堎合、メむン゜ケットはすぐにポヌトにバむンドする必芁がありたす。



 sockaddr_in address; uv_ip4_addr("0.0.0.0",   , &address); uv_tcp_bind(tcp_socket, (const struct sockaddr*)&address, 0);
      
      





この堎合、「0.0.0.0」がアドレスずしお瀺されたす。これは、゜ケットが1぀だけでなく、コンピュヌタヌ䞊に存圚するすべおのネットワヌクアダプタヌにバむンドされるこずを意味したす。 サヌバヌポヌト番号は独立しお遞択する必芁がありたす。ここでの䞻なこずは、他のプログラムず競合しないようにするための䞀意性です。



次に、サヌバヌは゜ケットのポヌトリッスンを有効にする必芁がありたす。



 uv_listen((uv_stream_t*)tcp_socket, 1024, OnAccept);
      
      





もちろん、最初のパラメヌタヌtcp_socketは゜ケット自䜓ですが、2番目のパラメヌタヌはより具䜓的です。 これは、むンラむンで埅機できる接続芁求の最倧数です。 非垞に人気のあるサヌバヌがあり、プレむダヌがそのサヌバヌでプレむしようずしおいるず想像しおください。 クラむアントから倚くの接続芁求がありたす。 サヌバヌがそれらに応答する時間がない堎合、サヌバヌはそれらをキュヌに入れたす。 この堎合、この数倀1024はこのキュヌの最倧サむズです。 キュヌに入れられおいない人のために、サヌバヌは3぀の単語で応答したす。



3番目のOnAcceptパラメヌタヌは、クラむアントがuv_tcp_connect関数を䜿甚するクラむアントからCONNECT接続芁求を受信したずきに呌び出されるコヌルバック関数です。



OnAccept関数は次のようになりたす。



 void OnAccept(uv_stream_t *server, int status) { if (status<0) { //    return; } //    ,        uv_tcp_t* tcp_socket=malloc(sizeof(uv_tcp_t)); memset(tcp_socket, 0, sizeof(uv_tcp_t)); uv_tcp_init(&loop, tcp_socket); if (uv_accept(server, (uv_stream_t*)tcp_socket)==0) //     { //   uv_read_start((uv_stream_t*)tcp_socket, OnAllocBuffer, OnReadTCP); //      } else { //     uv_close((uv_handle_t*) tcp_socket, OnCloseSocket); //  ,      } }
      
      





OnAccept関数の内郚をさらに詳しく調べおみたしょう。



  1. 最初に、新しい゜ケットが䜜成されたす。これは、接続を確立する芁求の送信元であるクラむアントに接続するために䜿甚されたす。 クラむアントはuv_tcp_connectを䜿甚しお接続を確立したす。



  2. Uv_acceptが呌び出され、サヌバヌずクラむアント間の接続が実行されたす。 uv_accept関数により、クラむアントは、 uv_tcp_connectで最埌のパラメヌタヌずしお指定されたコヌルバック関数を呌び出したす。 䞊蚘の䟋では、 OnConnect関数でした。



  3. 接続が正垞に確立された堎合、サヌバヌは䜜成された゜ケットがこの接続からデヌタを読み取るこずを蚱可する必芁がありたす。 uv_read_start関数は、新しく䜜成されたtcp_socket゜ケットのデヌタの読み取りを有効にしたす。 「デヌタの読み取り」ず「゜ケットのリッスン」は異なる操䜜であるこずに泚意しおください。 「デヌタの読み取り」は、ファむルからのバむトの読み取りず同様に、文字通り「読み取り」であり、「゜ケットをリッスン」は、接続を確立するためのCONNECT芁求を埅機しおいたす。



    uv_read_start関数は、2぀の敎数コヌルバック関数を䜿甚したす。



    -OnAllocBufferは、デヌタを読み取る前に呌び出され、デヌタを受信するためのメモリを指定するようナヌザヌに芁求したす。



    関数自䜓は次のように定矩されたす。



     void OnAllocBuffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf)
          
          





    最初のパラメヌタヌuv_handle_t * handleは、メモリを必芁ずする゜ケットぞのポむンタヌです。



    2番目のパラメヌタヌsize_t suggest_sizeは、必芁なバッファヌサむズです。



    3番目のパラメヌタヌuv_buf_t * bufは、ナヌザヌがバッファヌ情報サむズずアドレスをlibuvラむブラリに返すための構造です。



     buf->len=; buf->base=;
          
          





    ぀たり、バッファ自䜓はナヌザヌが䜜成し、ナヌザヌも削陀する必芁がありたす。 たた、 libuvはこのバッファヌぞのデヌタのみを受け入れたす。 同じバッファを耇数の゜ケットに䜿甚できたす。



    私の堎合、䜕らかの理由でlibuvは垞に65536バむトのバッファヌを芁求したした。 私にずっおは、これは少し奇劙ですが、このメモリを1回割り圓おるので、問題はないようです。



    -OnReadTCPはOnAllocBufferの埌に呌び出され、゜ケットがバッファで受信したデヌタをナヌザヌに枡したす。



    関数自䜓は次のように定矩されたす。



     void OnReadTCP(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf)
          
          





    最初のパラメヌタヌuv_handle_t *ハンドルは、デヌタを受信した゜ケットぞのポむンタヌです。



    2番目のssize_t nreadパラメヌタヌは、受信したバむト数です。 nreadがれロ以䞋の堎合、これは接続が切断されたこずを瀺しおいたす。 ぀たり これはデヌタを読み取るのではなく、2番目の偎がこの接続に関連する゜ケットを取り倖したずいう事実、たたは「ケヌブル砎損」などのハヌドりェアの理由により発生する可胜性のある切断に関する情報です。 この状況を監芖し、それに応じお察応する必芁がありたす。



    3番目のパラメヌタヌはconst uv_buf_t * buf-読み取りデヌタのあるバッファヌのアドレスを含みたす。 これは、ナヌザヌがOnAllocBuffer関数で指定したものず同じアドレスになりたす。



    デヌタを読み取る最も重芁な瞬間。 前述したように、 TCPは送信メッセヌゞを郚分的に配信できたす。たずえば、送信者が「Hello」ずいうフレヌズを送信し、次に「Server」ずいうフレヌズを送信した堎合、これら2぀のメッセヌゞを受信したずきに情報がたったく同じように芋える必芁はありたせん配送圢態で。 たず、䞡方のメッセヌゞを1぀にたずめるず、受信偎は「Hello、Server」ずいうメッセヌゞ党䜓を受信したす。最初に「When」、次に「Vet、Ser」、「Ver」などのメッセヌゞを受信したす。 ぀たり 理論䞊のメッセヌゞは、任意の数の郚分に断片化できたす。 したがっお、ネットワヌクを介しお送信されるすべおのメッセヌゞには、垞に、あるメッセヌゞを他の゜ケットからのバむトストリヌムから分離できるヘッダヌが必芁です。 これはすべお、 TCP゜ケットからデヌタを読むずいう非垞に䞍快な機胜に぀ながりたす 。 実際には、そのような゜ケットにはそれぞれ次のメッセヌゞの着信バむトを栌玍する独自のバッファヌが必芁です。メッセヌゞが郚分的にしか受信されない堎合、完党に受信されるたで埅機する必芁があり、その堎合にのみ䜕らかの方法で応答できるからです



  4. 接続を確立できなかった堎合、その接続甚の゜ケットを削陀する必芁がありたす。 これを行うには、 uv_close関数を䜿甚したす。 しかし...この関数は、最埌のパラメヌタヌずしおOnCloseSocketコヌルバック関数も受け入れるこずに泚意しおください。 たた、この関数を呌び出した時点で、libuvは゜ケットをメモリから物理的に削陀できるようになったこずをナヌザヌに䌝えたす。



     void OnCloseSocket(uv_handle_t* handle) { free(handle); //     malloc(),      free() }
          
          





メッセヌゞを送信する



゜ケットを介しおメッセヌゞを送信するには、 uv_write関数を䜿甚したす。



 int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb);
      
      





最初のパラメヌタヌuv_write_t * reqは、 libuvがデヌタを転送するために必芁なある皮の倉数です。 関数パラメヌタヌに存圚する意味を掘り䞋げるこずは䟡倀がありたせんが、たずえば次のように䜜成する必芁がありたす。



 uv_write_t write_data;
      
      





珟圚、この倉数はメッセヌゞの連続送信に繰り返し䜿甚できたすが、耇数のメッセヌゞの送信に同時に䜿甚するこずはできたせん。



2番目のパラメヌタヌuv_stream_t * handleは、送信偎゜ケットです。



3番目のパラメヌタヌconst uv_buf_t bufs []は、送信するメッセヌゞの配列です。 uv_buf_t型の芁玠で構成されたす。これらの芁玠にはlenフィヌルドずbaseフィヌルドがあり、それぞれSIZEずADDRESSが含たれおいる必芁がありたす。



4番目のパラメヌタヌunsigned int nbufsは、メッセヌゞ配列内の芁玠の数です。 私の堎合、送信するメッセヌゞは垞に1぀だけでした。



5番目のパラメヌタヌuv_write_cb cbは、メッセヌゞが送信されたずきに呌び出されるコヌルバック関数です。 なぜ必芁なのですか 実際、ナヌザヌが送信するメッセヌゞは、送信されるたでメモリに保存する必芁がありたす。 ぀たり このコヌルバック関数がトリガヌされるず、送信されるメッセヌゞを含むデヌタバッファヌがlibuvで䞍芁になるこずを意味したす。 そしお今、このバッファは再びナヌザヌ制埡䞋にあり、新しいデヌタで満たされ、新しいメッセヌゞを送信できたす。



私の堎合、デヌタバッファヌず、あいたいだが必芁なuv_write_t write_dataの䞡方を1぀の構造に入れおいたす。 したがっお、同じ構造の䞀郚ずしおペアで機胜したす。



libuv構造からより䟿利なデヌタ型ぞの移行



メッセヌゞを受け入れるuv_tcp_tのような倚くの゜ケットがあるず想像しおください。 少し䞊で述べたように、デヌタはフロヌの原則に埓っお読み取られ、メッセヌゞは任意の郚分に分割できるため、着信デヌタを栌玍および分析するために各゜ケットにバッファが远加で必芁になりたす。 そしお、 OnReadTCPコヌルバック関数をもう䞀床芋おみたしょう。



 void OnReadTCP(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf)
      
      





たた、 uv_stream_t *ストリヌムパラメヌタヌを介しお、デヌタを受信した゜ケットが衚瀺されるこずに泚意しおください。 しかし... ...゜ケットを目的のデヌタバッファにどのように関連付けたすか 1000個の゜ケットがあり、誰もがそこに䜕かを受け入れるずしたしょう。 ゜ケットごずに、受信したデヌタを独自のバッファヌに入れる必芁がありたす。 しかし、結局のずころ、そのバッファヌは゜ケットによっお決定されるわけではありたせん-゜ケット構造内には、䜿甚可胜な数千のバッファヌのどれに察応するかの兆候のないナンセンスがありたす。



したがっお、ネットワヌキングの原理は完党に異なるはずです。次に、この問題をどちらの偎で解決できるかを説明しようずしたす。



ずりあえず、 libuvの存圚を忘れお、共通のサヌバヌ構造を䜜成しおみたしょう。



GNetServerずいうサヌバヌクラスがあるずしたす 。 このクラスのオブゞェクトは垞に単䞀のむンスタンスに存圚し、サヌバヌ機胜を完党に想定しおいたす。 私の堎合、サヌバヌには2぀のメむンアレむが必芁です。





これら2぀の配列の間には密接な関係が確立されおいる必芁がありたす。 プレむダヌは自分が所属しおいるセッションを知っおいる必芁がありたすすでにセッションに参加しおいる堎合。たた、どのセッションもそのプレむダヌがコンポゞションに含たれおいるこずを知っおいる必芁がありたす。



プレヌダヌずセッションの芁件は䜕ですか 䞻な芁件は、個人の䞀意の識別子による目的のプレヌダヌたたはセッションぞの迅速なアクセスです。 そしお、この識別子を単玔に配列内のプレヌダヌたたはセッションのむンデックスにするよりも高速な方法を考え出すこずはおそらく䞍可胜です。 そのため、すべおのプレヌダヌにはIDがありたす。これは、プレヌダヌの合蚈配列内のIDず単玔に同じです。 そしお今、別のプレヌダヌが識別子5、10、21、および115のプレヌダヌにデヌタを送信する堎合、サヌバヌは、識別子をむンデックスずしお䜿甚するだけで、これらの受信者をすぐに識別できたす。



次に、サヌバヌの芳点から「プレヌダヌ」が䜕であるかを刀断したしょう。 実際、「プレヌダヌ」は「゜ケット」であり、いく぀かの远加情報しかありたせん。 远加情報には次のデヌタが含たれたす。





この情報はすべおGNetSocketクラスに保存されたす。 ただし、 libuvからのデヌタはないこずに泚意しおください。 これは、必芁に応じおlibuvを別のものに眮き換えるこずができるように行われたす。



私の堎合、 GNetSocketを継承するGNetSocketLibUVクラスがありたす。 実際にはGNetSocketLibUVクラスのオブゞェクトのみが䜜成される堎合、なぜ䞭間GNetSocketクラスが必芁なのですか 事実、私の仕事は、可胜な限り䞀般的なネットワヌク構造からlibuvラむブラリを分離するこずでした。 その結果、メむンサヌバヌ/クラむアントファむルは7000行以䞊を占有し、 libuv固有のファむルは 600行かかりたす。 たた、 libuvを眮き換える必芁がある堎合は、比范的簡単に行うこずができたす。 たた、キヌクラスのオブゞェクトがnewを介しお盎接䜜成されるのではなく、仮想関数を介しお䜜成される堎合にも原則を䜿甚したす。たずえば、これによりサヌバヌの゜ケットオブゞェクトが䜜成されたす。



 GNetSocket* GNetServerLibUV::NewSocket(GNet* net) { return new GNetSocketLibUV(net); }
      
      





぀たり 1぀の堎所で亀換しお、 新しいGNetSocketLibUVnetを返す必芁がありたす。 他のタむプのオブゞェクトに察しおは、プログラム内ではこのタむプのみが䜜成されたす。



GNetSocketLibUVクラスは、基本クラスのすべおの抜象関数をオヌバヌラむドしたす。 次のようになりたす。



 class GNetSocketLibUV : public GNetSocket { public: void* sock; public: GNetSocketLibUV(GNet* net); virtual ~GNetSocketLibUV(); //  UDP  TCP- ,   listaen=true,     virtual bool Create(bool udp_tcp, int port, bool listen); //     TCP-    virtual bool SetConnectedSocketToReadMode(); //   virtual void Destroy(); //  IP-   TCP- // own_or_peer           virtual bool GetIP(CMagicString& addr, bool own_or_peer); //      (  TCP-) virtual bool Connect(NET_ADDRESS* addr); //     (  TCP-) virtual bool Accept(); virtual void SendTCP(NET_BUFFER_INDEX* buf); virtual void SendUDP(NET_BUFFER_INDEX* buf); virtual void ReceiveTCP(); virtual void ReceiveUPD(); };
      
      





1぀の倉数void * sockのみがGNetSocketLibUVクラスに远加されたした。これは、libuv゜ケットぞのほずんどのポむンタヌになりたすが、泚意が必芁です。私たちは、すぐに゜ケットぞの胜力が必芁libuv察応する゜ケットタむプを決定GNetSocketを単にバッファを読むためにどの嘘、そしおプレむダヌのID。どうやっおやるの䞭間構造を远加したした







 struct NET_SOCKET_PTR { GNetSocket* net_socket; }; struct TCP_SOCKET : public NET_SOCKET_PTR, public uv_tcp_t { }; struct UDP_SOCKET : public NET_SOCKET_PTR, public uv_udp_t { };
      
      





だから... ...今、私たちはのための新しい構造を持っおいるTCP゜ケットず呌ばTCP_SOCKETずのための新構造UDP゜ケットず呌ばれるUDP_SOCKETを。しかし...これらの構造は䞡方ずも゜ケット構造の前に新しいフィヌルドを持ちたす。これはGNetSocketクラスの芪オブゞェクトぞのポむンタです。



もう1぀重芁な点です。プログラムは、どこにも「ネむティブ」なlibuv゜ケットを䜜成するべきではなく、TCP_SOCKETやUDP_SOCKETなどの゜ケットのみを䜜成する必芁がありたす。䜜成盎埌に、その䞀郚ずしお䜜成されたGNetSocketオブゞェクトのアドレスをnet_socketフィヌルドに蚘録する必芁がありたすTCP_SOCKETたたはUDP_SOCKET。



実際には、゜ケットの䜜成は次のようになりたす。



 //  UDP  TCP- ,   listen=true,     bool GNetSocketLibUV::Create(bool udp_tcp, int port, bool listen) { GNetSocket::Create(udp_tcp, port, listen); uv_loop_t* loop=GetLoop(net); if (udp_tcp) { sock=malloc(sizeof(TCP_SOCKET)); memset(sock, 0, sizeof(TCP_SOCKET)); ((TCP_SOCKET*)sock)->net_socket=this; ... ... ... }
      
      





さお、ずきに我々のボむド*靎䞋はアドレスですTCP_SOCKETたたはUDP_SOCKET、ず私たちは垞に構造の最初は垞に根底にある゜ケットぞのポむンタになるこずを知っおいるGNetSocket * Net_Socket、「迅速なむンストヌル適合性」のタスクはほずんど解決したした。



必芁なデヌタを取埗するのに圹立぀関数をいく぀か远加したす。sockがTCP_SOCKETの



堎合、次の関数にsockアドレスを枡すず、libuv TCP゜ケットが簡単に抜出されたす。



 uv_tcp_t* GetPtrTCP(void* ptr) { return (uv_tcp_t*)(((char*)ptr)+sizeof(void*)); }
      
      





sockがUDP_SOCKETの堎合、次の関数にsockアドレスを枡すず、libuv UDP゜ケットが簡単に抜出されたす。



 uv_udp_t* GetPtrUDP(void* ptr) { return (uv_udp_t*)(((char*)ptr)+sizeof(void*)); }
      
      





GetNetSocketPtr関数゜ケットのuv_tcp_tたたはuv_udp_tのアドレスを䜿甚するず、この゜ケットに察応するGNetSocketタむプのメむン゜ケットのアドレスを取埗できたす。



 GNetSocket* GetPtrSocket(void* ptr) { return *((GNetSocket**)ptr); } GNetSocket* GetNetSocketPtr(void* uv_socket) { return GetPtrSocket(((char*)uv_socket)-sizeof(void*)); }
      
      





実際に䜿甚する方法はたずえば、TCP゜ケットを読み取りモヌドにする必芁がありたす。



 //     TCP-    bool GNetSocketLibUV::SetConnectedSocketToReadMode() { if (udp_tcp) { uv_tcp_t* tcp=GetPtrTCP(sock); int r=uv_read_start((uv_stream_t*)tcp, OnAllocBuffer, OnReadTCP); return (r==0); } return false; }
      
      





靎䞋はGetPtrTCPsockを䜿甚しおuv_tcp_t * tcpに倉わり、すでにuv_read_start関数に枡すこずができるこずに泚意しおください。さお、私の堎合、OnReadTCPコヌルバック関数は次のようになりたす。







 void OnReadTCP(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { GNetSocket* socket=GetNetSocketPtr(stream); if (nread>0) { NET_BUFFER* recv_buffer=socket->net->GetRecvBuffer(); assert(buf->base==(char*)recv_buffer->GetData()); recv_buffer->SetLength(nread); socket->ReceiveTCP(); } else { //  ,    socket->net->OnLostConnection(socket); } }
      
      





最初の行



 GNetSocketLibUV* socket=(GNetSocketLibUV*)GetNetSocketPtr(stream);
      
      





GNetSocketオブゞェクトのアドレスを取埗したす。このオブゞェクトに察しお、関数socket- > ReceiveTCPが実行され、゜ケットが受信したメッセヌゞの受信を実珟したす。圌女はすでにこのデヌタを自分の゜ケットバッファヌに入れ、メッセヌゞが完党に受信されたこずを確認しおから、サヌバヌたたはクラむアントに送信しお凊理したすサヌバヌずクラむアント間のコヌドの倧郚分は䞀臎しおいたす。



たた、゜ケットを削陀する䟋を瀺したす。



 //   void GNetSocketLibUV::Destroy() { if (sock) { if (udp_tcp) { uv_tcp_t* tcp=GetPtrTCP(sock); uv_close((uv_handle_t*)tcp, OnCloseSocket); ((TCP_SOCKET*)sock)->net_socket=NULL; } else { uv_udp_t* udp=GetPtrUDP(sock); int r=uv_read_stop((uv_stream_t*)udp); assert(r==0); uv_close((uv_handle_t*)udp, OnCloseSocket); ((UDP_SOCKET*)sock)->net_socket=NULL; } sock=NULL; } GNetSocket::Destroy(); }
      
      





ここの行に泚意しおください。



 ((TCP_SOCKET*)sock)->net_socket=NULL;
      
      





、぀たりlibuv゜ケットはここでは削陀されたせんが、メむンのGNetSocketオブゞェクトはこの゜ケットずの接続を持たなくなるため、䞀般的にそれ自䜓で削陀されたす。ただし、libuvが゜ケットでのすべおの䜜業を完了するず、OnCloseSocketコヌルバック関数が必然的に機胜し、freeが実行されたす。したがっお、メモリリヌクは発生したせん。



これで、libuvラむブラリに関する䌚話を終了できるず思いたす。私は、その䜜業の根底にある原則の本質を明確にしようずしたしたが、これらの原則は、WinSockを含むほずんどの゜リュヌションで実質的に同じであるず思いたす。おそらく私の解釈では十分なコヌド䟋はありたせんが、関数名でむンタヌネット䞊で芋぀けるのはそれほど難しくありたせん。これらの関数で䜕をする必芁があるのか​​、そしおそれらが互いにどのように盞互䜜甚するのかを説明しようずしたした。私の目暙はゲヌムを完成させるこずであり、ネットワヌクプログラミングの分野の専門家になるこずではなかったため、私の理解では䞍正確な点がある可胜性がありたす。そのため、「必芁か぀十分」に基づいおこのケヌスを芋぀けたした。



GNetClientネットワヌククラむアントの構造



クラむアントは異なる時点で完党に異なる動䜜をする必芁がありたす。たずえば、最初にサヌバヌに接続しおセッションを䜜成たたは参加する必芁があり、ゲヌムが既に開始されおいる堎合、マりスずキヌボヌドから受信したデヌタを他のプレヌダヌず亀換する必芁がありたす。



ステヌゞでクラむアントの䜜業の論理を砎り、各ステヌゞの動䜜を個別に蚘述するこずができたす。



私の堎合、次の段階がありたす



。GNetStadyEnumSession / NET_STADY_ENUM_SESSION-既存のゲヌムセッションのリストを取埗する段階。

GNetStadyJoinSession / NET_STADY_JOIN_SESSION-ゲヌムセッションに接続し、独自のプレヌダヌをセットアップする段階。

GNetStadyCreateSession / NET_STADY_CREATE_SESSION-セッションを䜜成し、それを蚭定し、自分のプレヌダヌを蚭定できるステヌゞ。

GNetStadyStartGame / NET_STADY_START_GAME-オンラむンゲヌムを起動する段階。

GNetStadyGame / NET_STADY_GAME-ゲヌムのステヌゞ。

GNetStadyMigrationHost / NET_STADY_MIGRATION_HOST-ホスト移行の段階。



ステヌゞむンデックスは次のように宣蚀されたす。



 enum NET_STADY {NET_STADY_ENUM_SESSION, NET_STADY_JOIN_SESSION, NET_STADY_CREATE_SESSION, NET_STADY_START_GAME, NET_STADY_GAME, NET_STADY_MIGRATION_HOST, NET_STADY_NO=-1};
      
      





各ステヌゞのオブゞェクトは事前に䜜成され、GNetClient内のステヌゞの配列に保存されたす。



 // ,     class GNetClient : public GNet { protected: NET_STADY net_stady; //   int k_net_stady; //   GNetStady** m_net_stady; //    ... ... ... } GNetClient::GNetClient() : GNet() { net_stady=NET_STADY_NO; k_net_stady=6; m_net_stady=new GNetStady*[k_net_stady]; m_net_stady[NET_STADY_ENUM_SESSION]=new GNetStadyEnumSession(this); m_net_stady[NET_STADY_JOIN_SESSION]=new GNetStadyJoinSession(this); m_net_stady[NET_STADY_CREATE_SESSION]=new GNetStadyCreateSession(this); m_net_stady[NET_STADY_START_GAME]=new GNetStadyStartGame(this); m_net_stady[NET_STADY_GAME]=new GNetStadyGame(this); m_net_stady[NET_STADY_MIGRATION_HOST]=new GNetStadyMigrationHost(this); ... ... ... }
      
      





すべおのステヌゞの基本クラスはGNetStadyクラスであり、そこから他のすべおのステヌゞが継承されたす。



 // ,      class GNetStady { protected: GNetClient* owner; //    - GNetClient unsigned int stady_period; unsigned int stady_tick; public: GNetStady(GNetClient* owner); virtual ~GNetStady(){} virtual bool OnStart(NET_STADY previous, void* init); virtual void OnFinish(NET_STADY next){} virtual void OnUpdate(); //     ,    stady_period (    0) virtual void OnPeriod(){} //      virtual bool IsMessageCorrected(int message_type){return false;} };
      
      





ステヌゞの機胜に぀いお簡単に説明したしょう。





ネットワヌククラむアントステヌゞ管理



クラむアントがただサヌバヌに接続しおいない堎合を陀き、䞀郚のステヌゞは垞に最新です。



ネットワヌクでの䜜業を開始するには、次の機胜を䜿甚したす。



 //    virtual bool GGame::StartNet(const char* ip);
      
      





ip = NULLの堎合、独自のロヌカルサヌバヌを起動したす。それ以倖の堎合、ipはむンタヌネットサヌバヌのIPアドレス234.123.34.18:57などである必芁がありたす。



ネットワヌクでの䜜業を完了するには、次の機胜がありたす。



 //    virtual void StopNet();
      
      





この関数は、ネットワヌクのすべおの兆候、぀たりロヌカルネットワヌクサヌバヌが存圚する堎合は停止し、GNetClientクラむアントオブゞェクトを砎棄したす。



ネットワヌクの操䜜を開始するには、関数bool GGame :: StartNetconst char * ipを呌び出す必芁がありたす。 IPアドレスが指定されおいる堎合、関数はネットワヌクを初期化し、指定されたアドレスにあるサヌバヌずの接続を確立しようずしたす。サヌバヌが応答しお接続を蚱可した堎合、関数はtrueを返し、そうでない堎合はfalseを返したす。サヌバヌぞのすべおのさらなる呌び出しは確立されたTCP接続を通しお実行されるでしょう。ip = NULLの堎合、関数はサヌバヌぞの接続を実行したせん。代わりに、UDP゜ケットを䜜成したす。ブロヌドキャスト芁求を介しおロヌカルネットワヌク䞊のサヌバヌを怜玢するために䜿甚したす。StartNet



関数の実行時に問題がなかった堎合、ネットワヌクはNET_STADY_ENUM_SESSIONステヌゞの実行を開始したす。䜜成されたゲヌムセッションを怜玢したす。これは次のように行われたす...関数GNetStadyEnumSession :: OnPeriodで、0.5秒ごずに自動的に呌び出され、タむプMESSAGE_TYPE_ENUM_SESSIONのメッセヌゞが送信されたす。メッセヌゞは、事前に確立された接続を介しおむンタヌネットサヌバヌにのみ送信されるか、ロヌカルネットワヌクぞのブロヌドキャスト芁求によっお送信されたす。いずれの堎合も、むンタヌネットサヌバヌたたはロヌカルネットワヌク䞊のサヌバヌがこのメッセヌゞを受信するず、同じ方法で応答したす。぀たり、サヌバヌで利甚可胜なすべおのセッションをリストするメッセヌゞMESSAGE_TYPE_ENUM_SESSION_REPLYの圢匏で質問者に回答を送信したす。各セッションはの簡単な説明があるセッションの䜜成時間、IDホスト、名刺に、プレむダヌの数ずその特性、セッションにおける症状のパスワヌドだけでなく、サヌバヌのIPアドレスをこの堎合、クラむアントはリク゚ストブロヌドキャストを送信し、接続を確立するためにクラむアントが応答を送信したサヌバヌのアドレスを知る必芁があるため、クラむアントが接続できるようにするこずはロヌカルネットワヌクにずっお非垞に重芁です。



芋぀かったセッションに関する情報を受け取った埌、クラむアントは仮想関数virtual void GGame :: OnEnumSessionGSessionList * sessions、int count_general_sessionを呌び出したす。この関数は、実際にはナヌザヌにセッションのリストを衚瀺するこずがタスクなので、GGameクラスでは定矩されおいたせん。GGameは普遍的な基本クラスであるため、䜿甚できる特定のゲヌムに぀いお䜕も知らないため、GGameはそのようなタスクを匕き受けたせん。したがっお、私の堎合、この関数はクラスで再定矩されおいたすGGameOnimodLandず受信したセッションを䜜成時間で䞊べ替え、GListBoxコンポヌネントに行を远加しおナヌザヌに衚瀺するのは圌女です。



ナヌザヌがセッションを遞択しお「参加」をクリックするず、次のようになりたす。クラむアントは単玔にNET_STADY_JOIN_SESSIONステヌゞを開始したす。これはGNetStadyJoinSession :: OnStart関数でセッションぞの接続を詊行したす。この詊みはさたざたな理由で倱敗する堎合がありたす。たずえば、ゲヌムが開始された、別のプレヌダヌが接続された、セッションにスペヌスがなくなったなどです。いずれの堎合も、接続の蚱可はホストによっお発行されたすサヌバヌではありたせん。



これは実際にはどのように起こりたすか



ロヌカルネットワヌクの堎合、最初にサヌバヌに接続する必芁がありたす。これを行うには、ブヌル関数GNetClient :: ConnectToServerconst char * ipを䜿甚したす。trueの堎合はtrueを返したす。サヌバヌのIPアドレスは、セッション情報から取埗されたす。接続を確立する前に、UDP゜ケットは最初に匷制終了されたす。これは、䞍芁になり、代わりにTCP゜ケットが䜜成されるためです。これにより、接続が確立されたす。クラむアントはCONNECTを䜿甚しおサヌバヌにアクセスし、サヌバヌは芁求を受け入れおACCEPTを介しお応答する必芁がありたす。接続が確立されたら、「ハンドシェむク」を亀換する必芁がありたす。



これは䜕のためですか実際には、別のプログラムのアクセスから誰でもサヌバヌに接続できたす。 「ハンドシェむク」は、サヌバヌから芋おクラむアントを識別したす。「ハンドシェむク」が成功するず、サヌバヌがクラむアントにメッセヌゞの送信を蚱可したす。 「ハンドシェむク」自䜓は、メッセヌゞMESSAGE_TYPE_HELLOを介しお実行されたす。メッセヌゞMESSAGE_TYPE_HELLOには、クラむアントバヌゞョン、ラむセンスキヌなどのサヌビス情報が远加されたす。サヌバヌはこの情報を確認し、MESSAGE_TYPE_HELLO_REPLYメッセヌゞをクラむアントに返したす。メッセヌゞには䞀意のクラむアントIDが含たれおいたす。クラむアントは、これを介しおサヌバヌおよび他のプレヌダヌずのすべおのさらなる通信を実行したすID。サヌバヌがID = 0を返した堎合、これはクラむアントがサヌバヌに受け入れられなかったこずを意味したす。この堎合、サヌバヌはクラむアントに障害の理由を説明するテキストを远加で送信したす。このテキストは、たずえばGMessageBoxの圢匏でクラむアント偎に衚瀺できたす。むンタヌネットサヌバヌぞの接続も同様の方法で実行されるこずに泚意しおください。



クラむアントがサヌバヌに接続されおいる堎合、セッションぞの接続芁求をすぐにホストに送信する必芁がありたす。ただし、ホストはセッションをパスワヌドで保護できるため、「自分の」だけが接続できたす。セッションに関する情報から、そのようなパスワヌドの事実が刀断されたす。この堎合、ナヌザヌは最初にパスワヌドの入力を求められたす。次に、関数bool GNetClient :: ConnectSessionNET_JOIN_DATA * join。これは、タむプMESSAGE_TYPE_CONNECTINGのメッセヌゞをホストに送信したす。メッセヌゞはホストIDに送信され、セッションIDから再び取埗されたす。たず、サヌバヌがメッセヌゞを受信したす。サヌバヌは受信者のリストを分析し、メッセヌゞをホストに枡したす。



メッセヌゞを受信した埌、ホストはセッションぞのプレヌダヌの受け入れが可胜かどうかを確認し、パスワヌド存圚する堎合を確認しお決定したす。答えが「はい」の堎合、ホストは最初に仮想関数virtual int GGame :: GetIndexOfConnectedPlayerを䜿甚しお新しいプレヌダヌのゲヌムスロットを遞択し、セッションにプレヌダヌを含めたす。さらに、ホストには機胜がありたすvirtual void GGame :: OnNewPlayerint index。これにより、新しいメンバヌの出珟により䜕らかのアクションを実行できたす。次に、ホストはメッセヌゞMESSAGE_TYPE_CONNECT_REPLYの圢匏でクラむアントに応答を送信したす。肯定的な回答の堎合、セッションでクラむアントに割り圓おられたスロットむンデックス、セッション内の他のプレヌダヌのステヌタスに関するすべおの情報、およびいく぀かの远加情報を送信したす。メッセヌゞMESSAGE_TYPE_CONNECT_REPLYを受信するず、クラむアントはスロットを蚘憶し、サヌバヌから受信したデヌタでセッション倉数を初期化したす。



これで、新しいプレヌダヌはセッションの特性を調敎し、チャットメッセヌゞを亀換できたす。ホストがゲヌムを開始できるように、セッションの各参加者は「準備ができおいたす」ボタンをクリックする必芁がありたす「これがないず、ホストの[スタヌト ]ボタンは無効になりたす。



ゲヌムセッション情報



セッション情報は、特定のゲヌムに䟝存しないように敎理する必芁がありたす。たずえば、突然「黄金の子䟛時代を芚えお」ず蚀い、ゲヌムをやり盎したい堎合、シェルの構造を倉曎する必芁はありたせん。そのため、セッションの説明のフィヌルドが任意であり、特定のゲヌムのみが分析に関䞎し、サヌバヌたたはクラむアントが関䞎しおいないこずを確認する必芁がありたす。ただし、䞀郚のフィヌルドは明確に定矩する必芁がありたす。これは、サヌバヌがセッションに぀いお名前を知る必芁があるためです。たた、明らかな「の数や状態などのセッションに含た遞手、に関するいく぀かの情報を解釈する必芁が誰である」人、コンピュヌタ、空きスロット、クロヌズドスロットを



この情報はすべおNET_SESSION_INFO構造䜓に入れたす。この構造の䞀郚ずしお、プレむダヌNET_PLAYER ** m_playerの配列がありたす。およびその番号int k_player;



しかし、最も重芁なのは、任意の情報を栌玍できるフィヌルドがありたす



 int length_info; //  .  char* info; //  . 
      
      





同じフィヌルドがNET_PLAYER構造にあり、任意のプレヌダヌの任意の情報を保存するこずもできたす。



次に、次の関数ず挔算子をNET_SESSION_INFO構造に远加したす。



 virtual void Serialize(CMagicStream& stream); // /    NET_SESSION_INFO& operator=(const NET_SESSION_INFO& si); //   bool operator==(const NET_SESSION_INFO& si); //     bool operator!=(const NET_SESSION_INFO& si) //    
      
      





なぜこれがすべお必芁なのですか



重芁なこずを1぀説明しようず思いたす。私の意芋では、オペレヌタヌの䜿甚はかなり危険なビゞネスです。問題は、開発䞭に頻繁に新しい倉数が構造に远加されるこずです。そしお倚くの堎合、コンストラクタでそれらを初期化するこずを忘れるこずさえできたす。そしお、これらの倉数は、比范挔算子ず平等に远加する必芁があるずいう事実を忘れお-それは䞀回以䞊私に起こった、そしお圌女ず私は時々非垞に怒っおいるの埌に「tがオフに頭upuyu「物忘れのために。だから...結果ずしお、私は次の決定に来たした。通垞、重芁な構造にはシリアル化が含たれたす。より単玔には、ストリヌムからデヌタを読み取り、ストリヌムに曞き蟌むこずができたす。ほずんどの堎合、ストリヌムはバむトが曞き蟌たれる通垞のファむルですが、メモリ領域もストリヌムにできる堎合ははるかに優れおいたす。私の堎合、私はクラス曞いCMagicStreamをしお生成されたクラスからCMagicStreamFileずCMagicStreamMemoryを。したがっお、関数virtual void SerializeCMagicStreamstream;実際にどのクラスオブゞェクトストリヌムであるかに応じお、ファむルずRAMを操䜜する方法を知っおいたす。



ずころで、もう1皮類のクラスがありたすCMagicStreamVirtualFileは、私の「シェル」の䞀郚であり、仮想ディスクで動䜜するように蚭蚈されおいたす。仮想ディスクは、独自のファむルシステムを含むいく぀かのファむルです。仮想ディスクを䜿甚しお、その内郚にゲヌムリ゜ヌスを配眮したした。仮想ディスクは、CMagicString GPlatform :: OpenVirtualDriveconst char * pathを介しお、仮想ディスクファむルのパスを指定するこずで開くこずができたす。その結果、タむプ0のパスが返されたす。このパスはシェルで䜿甚され、仮想ディスク内のファむルを操䜜できたす。ここで重芁なこずは、ファむルシステムで機胜する「シェル」関数がこの方法を理解し、必芁に応じお芁求をファむルにリダむレクトするこずです。たずえば、CMagicStream * GPlatform :: OpenStreamconst char * file、int mode関数このような普遍的な原則で動䜜し、ファむルが察応するパスを指しおいる堎合、仮想ディスクに正しくアクセスしたす。同じこずが「珟圚のフォルダヌ」の状況にも圓おはたりたす。誰も仮想ディスク䞊に珟圚のフォルダヌを䜜成する必芁はありたせん。



だから...ポむントに戻りたす。私は、物忘れのせいでオペレヌタヌが私にずっおどのように危険になるかに぀いお話したした。リスクを最小限に抑えるため、これを行いたす。バむナリ圢匏で構造のシリアル化関数を䜜成し、この関数にすべおの挔算子を枡したす。぀たりたずえば、割り圓お挔算子を蚘述する必芁がある堎合、構造フィヌルドを1぀ず぀コピヌする代わりに、曞き蟌み甚にRAMにCMagicStreamMemory型のストリヌムを䜜成し、コピヌした構造オブゞェクトのSerializeを実行したす。次に、読み取り甚ず同じストリヌムを䜜成したす。結果の構造オブゞェクトは、同じメモリ䜍眮からSerializeも実行したす。そのような保存+負荷が刀明したすRAMを介しお。比范挔算子を䜿甚しおも同じこずができたす。比范するオブゞェクトのデヌタを2぀の異なるストリヌムに曞き蟌み、それらをバむト単䜍で比范し始めたす。もちろん、この方法は、オペレヌタヌがそれぞれのフィヌルドを蚈算する堎合よりも遅くなりたす。ただし、ボトルネックの堎合は、垞に埓来のバヌゞョンを䜿甚できたす。シリアル化の利点は、通垞の保存 / 読み蟌みデヌタにも䜿甚されるこずです。そしお、少なくずも私の堎合、デヌタの砎損や保存されおいないフィヌルドの方がはるかに印象的であるため、忘れっぜさがすぐに珟れたす。



さらに、シリアル化は、クリップボヌドを介しおデヌタをコピヌするのに最適です。そしお、ネットワヌクの堎合...ネットワヌクの盞互䜜甚は、「タむプ」、「サむズ」、「任意のデヌタ」のフィヌルドであるメッセヌゞの送信に限定されるこずに泚意しおください。たた、バむナリシリアル化を䜿甚するず、あらゆるタむプのデヌタをネットワヌク経由で送信可胜なバむトストリヌムに倉換し、受信偎で元のデヌタに戻すこずができたす。



これで、ようやくネットワヌクセッションの蚭定が完了したした。ネットワヌククラむアントは、ゲヌムに぀いお䜕も知らないようにし、プレヌダヌの蚭定のほずんどに぀いおは知らないようにしたす。たずえば、「準備ができおいたす」ずいうフラグ「それは確かに必芁はありたせん- 。圌はそれゲヌム必芁があり、ネットワヌククラむアントは、ネットワヌクを介しお送信メッセヌゞにありたす



。私はそのようにしたGGameは、クラスで定矩されなければならない4぀の空virutalnye機胜を発衚したしたGGameOnimodLandを。



 virtual bool GGame::GetSessionInfoStruct(NET_SESSION_INFO* si); //    NET_SESSION_INFO* si   . virtual void GGame::SetSessionInfoStruct(NET_SESSION_INFO* si); //      NET_SESSION_INFO* si  . virtual bool GGame::GetSessionPlayerStruct(int index, NET_PLAYER* np); //     NET_PLAYER* np      .  index   . virtual void GGame::SetSessionPlayerStruct(int index, NET_PLAYER* np); //       NET_PLAYER* np    .
      
      





実際には、これらの機胜は、ネットワヌククラむアントからゲヌムぞ、たたはその逆にデヌタを普遍的に転送したす。同時に、GetSessionInfoStruct / SetSessionInfoStruct関数は、プレヌダヌに関するデヌタを含むセッション党䜓のデヌタを凊理したす。たた、GetSessionPlayerStruct / SetSessionPlayerStruct関数は、特定のプレヌダヌのデヌタを凊理したす。NET_SESSION_INFOにはNET_PLAYERオブゞェクトの配列が含たれおいるため、圓然、プレヌダヌの関数はセッション関数によっお䜿甚されたす。



このアプロヌチの埌、ネットワヌククラむアントのゲヌム自䜓は「ブラックボックス」に倉わり、そこから「䜕か」が生たれ、「䜕か」が受信されたす。そしお今、プレヌダヌを蚭定するプロセスを簡玠化するために必芁な重芁なポむント。プレヌダヌには倚くの蚭定があり、倉曎できるこずを想像しおください。たずえば、チヌムの色を倉曎したり、レヌスを遞択したりできたす。しかし、将来䜕が考えられるかは決しおわからないので、埌でプログラム構造を修正する必芁はありたせん。NET_STADY_JOIN_SESSIONの



段階で、タむマヌでvoid GNetStadyJoinSession :: OnUpdate関数が呌び出され、普遍性の問題を解決する1組の行を実行したす。GNetStadyJoinSession



ステヌゞには倉数がありたすNET_SESSION_INFO * copy_session;最埌のメゞャヌのセッション状態のコピヌを保存したす。それにもかかわらず、コヌド党䜓のかなりの郚分を提䟛したす。



 void GNetStadyJoinSession::OnUpdate() { GNetStady::OnUpdate(); //       NET_PLAYER* current_player=current_session->m_player[index_player]; NET_PLAYER* copy_player=copy_session->m_player[index_player]; owner->game->GetSessionPlayerStruct(index_player, current_player); if (*copy_player!=*current_player) { //        *copy_player=*current_player; if (current_player->type==PLAYER_MAN) { GMemWriter* wr1=owner->wr1; wr1->Start(); (*wr1)<<index_player; current_player->Serialize(*wr1); MEM_DATA buf; wr1->Finish(buf); //    ,    int k_receiver=owner->RefreshReceiverList(); NET_BUFFER_INDEX* result=owner->PrepareMessageForPlayers(MESSAGE_TYPE_PLAYER_INFO, buf.length, buf.data, k_receiver, owner->m_receiver); owner->GetMainSocket()->SendMessage(result); } } }
      
      





実際には、次のこずが発生したす。倉数index_playerは、セッション内のプレヌダヌに属するスロット番号です。



String owner-> game-> GetSessionPlayerStructindex_player、current_player;ゲヌムから同じプレヌダヌの珟圚の蚭定を



取埗 し、クラむアントが蚘憶しおいる蚭定ず単玔に比范したすif* copy_player= * current_player

そしお䞍䞀臎がある堎合、コピヌが最初に䞀臎したす* copy_player = * current_player;そしお、タむプMESSAGE_TYPE_PLAYER_INFOのメッセヌゞをセッションのすべおの参加者に送信したす。この参加者では、プレヌダヌの新しい蚭定が送信されたす。



このアプロヌチの倧きな利点は䜕ですか事実は、ゲヌム自䜓が他のプレむダヌに送信される蚭定に埓うべきではないずいうこずです。GNetStadyJoinSession :: OnUpdateはすぐにこの倉曎を認識し、セッションのすべおの参加者に新しいデヌタを自動的に送信するため、構成で少なくずも1バむトを倉曎する䟡倀がありたす。同時に、GNetStadyJoinSession :: OnUpdateは、構成可胜な実際のデヌタに぀いお䜕も知りたせん。これは、比范挔算子がシリアラむザヌを介しお機胜し、長さが等しい堎合にバむトストリヌムずストリヌムの長さが比范されるためです。



蚘事の䟋では、プレヌダヌの構成の構造は次のようになりたす。



 struct PlayerCfg { int type; CMagicString name; unsigned int player_id; unsigned int color; bool ready; void Serialize(CMagicStream& stream) { if (stream.IsStoring()) { stream<<type; stream<<name; stream<<player_id; stream<<color; stream<<ready; } else { stream>>type; stream>>name; stream>>player_id; stream>>color; stream>>ready; } } };
      
      





しかし、ゲヌムではフィヌルドが完党に異なり、さらに倚くのフィヌルドがありたす。ただし、このアプロヌチは汎甚性の点で優れおいたす。



セッション䜜成



ただし、セッションに参加する前に、誰かがこのセッションを䜜成する必芁がありたす。これにはNET_STADY_CREATE_SESSIONステヌゞがありたす。このステヌゞのクラスは、NET_STADY_JOIN_SESSIONステヌゞから継承されたす。



 class GNetStadyCreateSession: public GNetStadyJoinSession
      
      





これは、セッションの䜜成たたはセッションぞの参加のいずれかを実行するOnStart関数を陀き、これらのステヌゞが非垞に類䌌しおおり、倧幅に異なるためです。NET_STADY_CREATE_SESSIONステヌゞでは、構成倉曎のチェックも機胜したすが、セッション内の各プレヌダヌのチェックもありたす。これは、ホストがセッション党䜓を制埡し、たずえば他のプレヌダヌを削陀できるためです。



ずころで、ネットワヌククラむアントは、プレヌダヌの構成デヌタの分析にただ少し関䞎しおいたす。メッセヌゞMESSAGE_TYPE_PLAYER_INFOは、クラむアントが次のように分析する構成を担圓したす。メッセヌゞを受け取った時点で、圌は新しい構成が来たプレむダヌが生きおいる人であったかどうかを芚えおいたすタむプ= PLAYER_MAN。メッセヌゞを受信するず、珟圚の構成が新しい構成に眮き換えられたす。ただし、クラむアントはtypeフィヌルドでPLAYER_MANであるかどうかのみをチェックしたす。たた、フィヌルドが突然PLAYER_OPENEDに倉曎された堎合、これは、たずえば、ホストがセッションからプレヌダヌを削陀し、スロットが開いおいるこずを意味する堎合がありたす。クラむアントはこの状況の凊理に関䞎しおおり、その結果ずしおbool GNetClient :: LostPlayerunsigned int player_idが呌び出されたす。぀たり、プレヌダヌずの通信が倱われたす。さらに、これらはすべお、オプションのいずれかの圢匏でゲヌムに提䟛されたす。



 //    (    ) virtual void GGame::OnCancelSession(); //        virtual void GGame::OnDeletingFromSession();
      
      





bool関数GNetStadyCreateSession :: OnStart以前のNET_STADY、void * initは、プレヌダヌが[ゲヌムの䜜成]ボタンをクリックするず呌び出されたす。これがロヌカルネットワヌク䞊のゲヌムである堎合、たず独自のサヌバヌを起動しお参加する必芁がありたす。



 //    bool GNetClient::CreateSession() { bool is=false; if (!internet) { //    if (StartLocalServer()) { is=ConnectToServer("127.0.0.1"); } else is=false; } else { is=true; } return is; }
      
      





ConnectToServer 関数「127.0.0.1」では、UDP゜ケットが砎棄され、T CP゜ケットが䜜成されたす。次に、サヌバヌずの接続が確立されたす。サヌバヌは、StartLocalServer関数によっお同じコンピュヌタヌで起動されたす。サヌバヌのIPアドレスは「127.0.0.1」で、「同じコンピュヌタヌ」を意味したす。



次に、ホストは仮想void GGame :: OnCreateSessionint index_player関数を呌び出したす。この堎合、ホストのスロットのみが垞に0に蚭定されたす。



次に、void関数GNetStadyCreateSession :: OnPeriodを䜿甚するホストセッション状態に぀いおサヌバヌに定期的に通知し始めたす。この関数は、0.5秒ごずに自動的に呌び出されたす。タむプMESSAGE_TYPE_SESSION_INFOのメッセヌゞをサヌバヌに送信したす。このメッセヌゞは垞に送信されるわけではなく、セッション蚭定が倉曎された堎合にのみ送信されたす。ここでは、プレヌダヌ構成の倉曎ず同じ原則が䜿甚されたす。



メッセヌゞMESSAGE_TYPE_SESSION_INFOを受信するず、サヌバヌはたず、そのようなセッションがすでにセッションのリストにあるかどうかを確認し、ない堎合は新しいセッションを远加したす。メッセヌゞの送信者は、最初の参加者およびホストずしお新しいセッションに远加されたす。

さらに、サヌバヌは、クラむアントMESSAGE_TYPE_ENUM_SESSIONからの芁求に応じお、既存のセッションに関する情報を送信したす。



ステヌゞNET_STADY_CREATE_SESSIONは、プレヌダヌが「開始」ボタンを抌しおゲヌムの起動を開始するたで続きたす。実際には、珟時点では、NET_STADY_START_GAMEステヌゞは、net-> SetStadyNET_STADY_START_GAME、NULLぞの呌び出しを介しお蚭定されたす。 。

メッセヌゞMESSAGE_TYPE_START_GAMEは

void関数GNetStadyCreateSession :: OnFinishNET_STADY nextから自動的に送信されたす。これはNET_STADY_CREATE_SESSIONが完了するず呌び出されたす。ここで、ホストはサヌバヌに、セッションが参加のために閉じられたこずを通知し、他のプレヌダヌに通知する必芁はありたせん。



メッセヌゞMESSAGE_TYPE_START_GAMEは、サヌバヌによっおセッションのすべおの参加者に送信されたす。それを受信するず、それらすべおもNET_STADY_START_GAMEステヌゞに切り替えたす。



さらに、ゲヌムの起動段階の䞻な䜜業は、関数

void GNetStadyStartGame :: OnPeriodで実行されたす。これは、関数を介しお5から1ぞのカりントダりンを実行したす仮想ボむドGGame :: OnStartNetCounterint counter;実際には、5、4、3、2、1ずいう数字がチャット゚リアに出力されたす。次に、仮想ボむドGGame :: OnStartNetGameが呌び出され、ゲヌムセッションの起動準備プロセスが開始されたす。この時点で、ゲヌムカヌドが読み蟌たれ、プレヌダヌがその䞊に配眮されたす。このプロセス党䜓が各コンピュヌタヌで独立しお実行されるこずに泚意しおください。すべおのデヌタが初期化されるず、仮想ブヌルGGame :: IsNetGameLoaded関数はtrueを返すはずです。この関数はGNetStadyStartGame :: OnPeriodから継続的に呌び出され、falseが返される間、ネットワヌククラむアントはゲヌムの初期化が継続するず想定したす。垰っおすぐにtrueの堎合、メッセヌゞMESSAGE_TYPE_PLAYER_STARTEDがすぐに他のすべおのプレヌダヌに送信されたす。その時点で、ネットワヌククラむアントは、セッションのすべおの参加者からメッセヌゞMESSAGE_TYPE_PLAYER_STARTEDを既に受信しおいるこずを怜出するず、NET_STADY_GAMEステヌゞに進み、これはゲヌムの開始を意味したす。bool



関数GNetStadyGame :: OnStartNET_STADY以前、void * initはすぐに仮想void GGame :: OnLaunchNetGameを呌び出し、これが起動です。それだけです その埌、ゲヌムが始たりたす。



ネットワヌクゲヌム



ステヌゞNET_STADY_GAMEは、void関数GNetStadyGame :: OnUpdateを通じおゲヌムプレむ党䜓を制埡したす。実際には、このステヌゞでは、プレヌダヌが䞀定期間マりスずキヌボヌドを䜿甚しお入力したチヌムを送信したす。たた、この段階では、他のプレむダヌからのたったく同じデヌタが期埅されたす。



ナヌザヌコマンドは、メッセヌゞMESSAGE_TYPE_PLAYER_GAMEを介しお送信されたす。 GNetStadyGameステヌゞには次のフィヌルドがありたす。



 int k_player; PLAYER_MESSAGE* m_player;
      
      





他のプレむダヌからチヌムを受け取るために䜿甚されたす。PLAYER_MESSAGE構造には、NET_BUFFER next_messageメッセヌゞを受信するためのバッファがありたす。ただし、その目的は、䞀芋するず思われるかもしれたせん。実際には、ネットワヌクタクト番号ずいう抂念がありたす。これは、ゲヌムが終了するたで0から無限に増加するクロックをカりントするような倉数です。ネットワヌククロックは、ネットワヌククラむアントが珟圚のネットワヌククロックのすべおのプレヌダヌからコマンドを受信した堎合にのみ増加したす。それ以倖の堎合、埅機があり、ゲヌムがフリヌズしたす。ただし、通垞は通信の品質によりメッセヌゞを非垞に安定しお配信できるため、これらの遅延はプレヌダヌに気付かれずに発生したす。



それでもクラむアントが長時間埅機し始める堎合、仮想ボむド関数GGame :: OnWaitingPlayersunsigned int dtime、int k_player_id、unsigned int * m_player_idを介しおこのゲヌムを報告し、ゲヌムのタスクはこのプレむダヌのリストを画面に衚瀺するこずです。埅機時間は制限されおおり、クラむアントは埅機時間が無限にならないようにしたす。制限時間に達するず、クラむアントは問題のあるプレヌダヌを切断し始め、敗者であるず宣蚀したす。これにより、すぐにゲヌムが続行されるか、勝利によりゲヌムが完了したす。



クラむアントが珟圚のメゞャヌに察する別のプレヌダヌからコマンドを受け取るず、それらは仮想ボむド関数GGame :: SetPlayerNetMessageunsigned int sender、MEM_DATAmessageを䜿甚しおすぐにゲヌムに転送されたす。ただし、受信したメッセヌゞがすぐにゲヌムに転送される堎合、なぜもう1぀のNET_BUFFER next_messageバッファヌが必芁なのかはあたり明確ではないでしょうかしかし、なぜ。



すでに述べたように、ネットワヌクの盞互䜜甚はマルチスレッドず非垞によく䌌おおり、スレッドの同期により䞀郚のアクションが無期限に遅延する可胜性がありたす。ネットワヌクゲヌムでは、あるコンピュヌタヌが1 ネットワヌククロックサむクルだけ別のコンピュヌタヌを远い越し始めるず、状況が簡単に発生したす。この堎合、先の次のサむクルのためのメッセヌゞずしお衚瀺されるコンピュヌタメッセヌゞを远い越しから来た私たちのコンピュヌタはただ達しおいたせん。そしお、コンピュヌタは次のこずを行う必芁がありたす...このメッセヌゞを独自のバッファに保存するだけですNET_BUFFER next_message、珟圚のずころ䞀時的にこのサブゞェクトに察しおアクションを実行しなくなりたすが、次のネットワヌククロックのためにそのようなプレヌダヌからメッセヌゞが既に受信されおいるこずがわかりたす。い぀になる次のサむクルのネットワヌクは、たず最初に私たちのコンピュヌタvozmotその埌、これらの独自のバッファからのコマンドずは、を介しおゲヌムに枡す仮想空GGame :: SetPlayerNetMessageunsigned int型の送信者、MEM_DATAメッセヌゞ。これは、同様のゲヌムでネットワヌクの盞互䜜甚を構築するために理解するこずが望たしいずいう非垞に重芁なポむントです。



たた、「他のプレむダヌを埅぀」プロセスが機胜するため、2メゞャヌの远い越しができなくなるこずも理解する必芁がありたす。"したがっお、最倧の前進は1 ネットワヌククロックのみです。



ただし、コマンドを受信するには、最初にそれらを送信する必芁がありたす。ネットワヌククラむアントは、ゲヌムで䜿甚されるコマンドの詳现に぀いお䜕も知る必芁がないため、仮想MEM_DATA関数を呌び出すだけです:: GetPlayerNetMessage GGameなどのチヌムに察する緩衝準備するために圌を返し、MEM_DATAが埗られたバッファは、他のすべおのプレむダヌ自身のプレむダヌに同時に送信されたす。



 MEM_DATA message=owner->game->GetPlayerNetMessage(); GNetSocket* socket=owner->GetMainSocket(); owner->game->SetPlayerNetMessage(socket->player_id, message); //     ,       int k_receiver=owner->RefreshReceiverList(); owner->m_receiver[k_receiver]=takt; //      ,     NET_BUFFER_INDEX* result=owner->PrepareMessageForPlayers(MESSAGE_TYPE_PLAYER_GAME, message.length, message.data, k_receiver, owner->m_receiver, 1); socket->SendMessage(result); //       
      
      





ゲヌムはネットワヌククロックを制埡し、仮想int関数GGame :: GetNetTaktを介しおクラむアントに返したす。ただし、クラむアントはネットワヌクタクトの完了を制埡したす。これは、すべおのプレヌダヌからすべおのチヌムが受信される瞬間です。クラむアントは、仮想ゲヌムGGame :: OnNextNetTaktを呌び出しお、このゲヌムをすぐに報告したす。私の堎合、この関数はネットワヌクの同期解陀をチェックし、すべおが正垞な堎合にtrueを返したす。falseが返される堎合、ネットワヌククラむアントは非同期の修正プロセスを自動的に開始したす。ホストはすべおのデヌタをファむルに曞き蟌み、このファむルを他のすべおのプレヌダヌに転送したす。他のプレヌダヌはこのファむルを読み取り、このデヌタでゲヌムを続行したす。実際には、ホストは保存を行いたすそしお、残りのプレヌダヌはLoadです。1぀のネットワヌククロックサむクルで生成された乱数の合蚈をカりントするこずにより、ネットワヌクの同期を制埡したす。乱数は連続的に生成され、すべおのコンピュヌタヌで同じである必芁がありたす。そうでない堎合、これはネットワヌクが同期しおいないこずを瀺しおいたす。もし仮想BOOL GGame :: OnNextNetTaktを返すtrueに、クラむアント自身が倉数にこの事実を指摘しon_next_net_takt -圌のためにそれは電源サむクルが完了したこずを意味したす。ゲヌムは、メむンルヌプでクラむアント関数bool GNetClient :: IsNextNetTakt{return on_next_net_takt;}を定期的に呌び出す必芁がありたす



trueが返されるず、ゲヌムはネットワヌククロックを1 増やし、各プレヌダヌの最埌のネットワヌククロックでネットワヌク経由で受信したすべおのコマンドを実行したす。その埌、コマンド配列がクリアされ、すべおが新しい方法で開始されたすが、ネットワヌククロックの倀が増加したす。



プレヌダヌがマりスずキヌボヌドから入力したコマンドは、すぐにはネットワヌクに到達したせん。実際、珟圚のネットワヌククロックサむクルでは、最埌のネットワヌククロックサむクルで収集されたコマンドがネットワヌクに送信され、この時点でマりスずキヌボヌドからの新しいコマンドが収集されたす。぀たり1ネットワヌククロックの反応に遅れがありたす。この遅延はネットワヌク遅延ず呌ばれたす。ただし、ネットワヌククロックをゲヌムクロックず同じにするこずには意味がありたせん。たずえば、ゲヌムが1秒間に60回曎新される堎合、1぀のネットワヌククロックに察応するゲヌムの詊合を10 回行うこずは完党に可胜です。ナヌザヌが1/6秒の反応遅延に非垞に悩たされるこずはほずんどありたせん。



トラフィック暗号化



私は、デヌタ保護のトピックに匷くないこずを認めなければなりたせん。そしお、結局、私はそれに泚意を向けたいだけです。開発者がこれを行うこずを期埅しおいるため、ゲヌムからサヌバヌに接続する必芁はありたせん。実際には、任意のIPアドレスずポヌトずの接続を確立する機胜を持぀任意のプログラムに接続できたす。その埌、サヌバヌぞの送信を開始できたす。サヌバヌは、少なくずもメッセヌゞが正しくないこずを確認する必芁がありたす。そうでない堎合、サヌバヌは単に「せん劄」の䞀郚を受け取り、その䞭に巻き蟌たれたす。



たた、䞡偎のメッセヌゞは通垞暗号化されたす。



このトピックに぀いおは特に説明したせんが、自宅でのトラフィックの最小暗号化も䜿甚したずのみ蚀いたす。防埡を砎るこずは難しいずは思わない。なぜなら、この段階では、それを行う意欲も匷さもないからだ。圓分の間、私のプロゞェクトの小さな名声に守られ、そこにそれが芋られるこずを願っおいたす...



Windowsに非垞に長い間存圚する1぀の゚ラヌに぀いお



蚘事の最初の郚分では、RTSのネットワヌクゲヌムを䞀般的なアむデアずしおのみ説明したした。しかし、そこで私はRTSのネットワヌクがあふれおいる最倧の問題を指摘したした-すべおのコンピュヌタヌで完党に同䞀の蚈算が必芁です。コンピュヌタヌが少なくずも少し間違え始めたら、数分ですべおがコンピュヌタヌによっお倧きく異なりたす。そしお、プレヌダヌが他のコンピュヌタヌの「銃撃戊で殺された」コンピュヌタヌのナニットをコントロヌルしようずするず、ゲヌムは単玔に混乱に陥りたす。このような゚ラヌは、私が芋たものの䞭で最もひどいものだず思いたす。なぜなら、そのようなバグを論理で芋぀けるこずは実際には䞍可胜だからです。通垞、このような゚ラヌの原因は、「ネットワヌクゲヌムの再起動時に倉数を再初期化するのを忘れた」などの些现な些现なこずです。その結果、この倉数はネットワヌクの同期を砎るこずが保蚌されおおり、ゲヌム自䜓が最終的に厩壊するずっず前からです。



このトピックに関する私の掚論に興味がある人は、蚘事の最初の郚分を参照しおください。私の意芋では、昔からWindowsに存圚しおいた非垞に汚いグリッチに぀いおお話したいず思いたす。浮動小数点の蚈算で゚ラヌが発生するこずが保蚌されおおり、これによりネットワヌクが匷制終了されたす。



この問題はおそらくWindows 98で2003-2004幎に発芋され、そこからWindows XPに正垞に移行し、最近Windows 8でも䜕も倉わっおいないこずがわかりたした。



゚ラヌの䞻なポむントは、察応するWindows関数がFPU制埡ワヌドを倉曎するそしお戻らないこずです。。そしお、もちろん、そのような動䜜に぀いおはドキュメントのどこにも蚀及されおいたせん。



ここに、Windows XPの問題の存圚を蚌明する叀いコヌドがありたす。䞊のWindows 8、私はそれを詊しおいないが、䞊のWindows 8私のよく確立されたネットワヌクゲヌムは、明癜な理由もなく突然醜い䜜業を開始するずき、私はたた、「状況に飛びたした」。この問題を補うコヌドを誀っお削陀したこずが刀明したした。



したがっお、関数の䟋



 int Error1() { double step=66.666664123535156; double start_position_interpolation=0; double position_interpolation=199.99998474121094; double vdiscret=(position_interpolation-start_position_interpolation)/step; int discret=(int)vdiscret; return discret; }
      
      





もちろん、数字は奇劙ですが、゚ラヌが衚瀺されたす。Error1



関数を呌び出しおデバッガヌで凊理するず、結果ずしお数倀2がdiscret倉数になりたす。



 int result=Error1(); // result=2 ok=direct_3d->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3d9pp, device_3d ); result=Error1(); //    result=3
      
      





぀たり関数間呌び出す堎合ERROR1りェッゞ機胜のDirectX䜜成-aデバむスのDirectXを、第2の機胜呌び出しであろう予期しない結果discret = 3。これは、vdiscretが初めお2.99999 .......になり、2回目にはすでに3になるずいう事実によるものです。実際には違いはわずかですが、ゲヌムでdouble型の倉数を䜿甚しおいるため、ネットワヌクゲヌム党䜓を殺すのに十分です。さらに、理由を知らずに、そこに䜕かを修正する方法はありたせん。圢匏自䜓はコヌド自䜓が正しいため、どこかで䜕らかのプロセッサ状態フラグが目的の倀に戻らないためです。Windows 98



この問題は、Windows XPよりもさらに激しく珟れたした。そこで、この䞍具合は、WinAPIを䜿甚しお、利甚可胜なモニタヌ解像床を䞀芧衚瀺しようずしたずきに発生したした。DirectXがなくおも。Windows 8、私はすでに䜕をするかを事前に知っおいたこずから、この質問を調査しおいたせん。



私はこの惚劇を「治す」2぀の解決策を知っおいたす。別のストリヌムを䜜成したら、画面解像床を切り替えお、゚ラヌずずもにストリヌムを匷制終了したした。この堎合、この問題はメむンスレッドに圱響したせんでした。



2番目の方法は簡単です。



 unsigned int status=_controlfp(0,0); //    // ... // ... // ... _controlfp(status,_MCW_DN | _MCW_IC | _MCW_RC | _MCW_PC);
      
      





これにより、画面の解像床を切り替えた埌に衚瀺されるグリッチがなくなり、数孊は正しく動䜜し続けたす。



ゲヌムたたはリリヌスの珟圚の状態



ゲヌムのリリヌス準備がすでに敎っおいるず刀断したした。はい、バランスを調敎するか、いく぀かの小さな間違いを修正しなければならない可胜性がありたすが、実際にはゲヌムをリリヌスする時です。いずれにせよ、さらなる改善に取り組むために、私はポむントが通垞のプレヌダヌがいる堎合にのみ芋たす。



最近、私は、com、org、netなどのよく知られたドメむンに加えお、䞖界に陞地ドメむンがあるこずを知っお驚いた。私の英語のゲヌムはOnimod landず呌ばれおいるので、すぐにゲヌムのonimod.landドメむンを取埗したした。そのため、ゲヌムは以前のように独自の個人サむトonimod.landを持ちたす。



Steamでのリリヌスの前に、それは少し埌で来るず思いたすが、今のずころ、私は自分のサむトを通しおゲヌムをリリヌスしたす。私のプロゞェクトを財政的に支揎したい人は、ゲヌムのあるサむトでこれを行うこずができたす。しかし、私自身はロシアに䜏んでおり、ここの人々は゜フトりェアを賌入するよりもはるかに緊急の費甚項目があるこずを理解しおいたす。したがっお、このゲヌムが気に入っおいお、経枈的に私をサポヌトする経枈力がない堎合は、サむトのフィヌドバックフォヌムを䜿甚しお無料でキヌを芁求できたす。自己玹介を怠らないでください。さもないず、wertwq @ mail.ruからの「キヌをくれ」のような文字が私の䞭にネガティブな感情を匕き起こしたす。



ゲヌムは商業化され、おそらくすぐに私以倖の誰かがそれを必芁ずしおいるかどうかを知るでしょう。



おそらく、この哲孊的なメモで、私は私の話を終えるでしょう。この蚘事を読んで瀺された驚くべき意志力ず、私の「文孊的な才胜」に察する芋䞋した態床に皆に感謝したす。



敬具、アレクセむ・セドフ別名Odin_KG



PS

ゲヌムに関する蚘事を英語に翻蚳したい、少なくずも最初の郚分ずパス怜玢アルゎリズム。



誰かが英語に぀いお十分な知識があり、少なくずも私が話そうずしおいるこずに぀いおある皋床理解し、この問題で私を助けたいずいう願望を持っおいるなら、私はずおも幞せです。どういうわけか翻蚳者を雇おうずしたしたが、圌は文法的に正しい意味的なナンセンスを私に䞎えおくれたので、別の同様の詊みをする特別な欲求はありたせん。



誰かが応答し、実際に仕事に取り掛かる堎合、このリク゚ストを蚘事から削陀したす。



→ 蚘事の冒頭ゲヌムの埩掻

→ 蚘事の続きGUI



All Articles