客戶端與伺服器端 === 回想看看,你過去在玩模組的時後,你可曾經需要下載客戶端和伺服器端兩個版本的模組jar檔嗎? Ex. `EnderIO-1.12.2-5.1.55-server.jar`、`EnderIO-1.12.2-5.1.55-client.jar` 沒有對吧?這代表模組中客戶端和伺服器端的程式碼都包在同一個jar檔之中 (๑´ㅂ\`๑) 但這樣一來就會有明明是伺服器,運行環境卻包含了物品顯示模型部分的程式碼這種對伺服器端毫無意義的內容 執行端處理得好,模組穩定沒煩惱;執行端處理不好,模組每天Crash到老 (⁰▿⁰) 究竟我們該怎麼處理這件事呢?就讓我們繼續看下去吧 在本章節你會了解 --- - 執行端的定義 - 執行端的分工 - 實現 "只在某執行端XXX" 的幾個方式 - 如何選擇使用哪個方法來達成目的 執行端的定義 --- 你會說:欸M鼠你是想湊字數喔,執行端不就客戶端和伺服器端這兩個嗎? (゚皿゚メ) 恩對,你可以說執行端就兩個,但是我們需要更細分,否則在往後很容易犯錯 執行端可以分為四個 - 物理伺服器端 (Physical server) - 物理客戶端 (Physical client) - 邏輯伺服器端 (Logical server) - 邏輯客戶端 (Logical client) 為什麼要搞成這樣?原因只有一個 早在1.3開始,Minecraft的單人世界,Minecraft其實背後架了一個伺服器供玩家遊玩 (´ー\`) 好處是開發時不需要考慮當前是不是單人世界這件事 你只要讓 **邏輯客戶端** 和 **邏輯伺服器端** 分工好就可以了 壞處顯而易見,對於初次開發Minecraft模組的作者很容易搞混這四個東西 ╮(╯_╰)╭ 來說說這四個定義吧 - 物理伺服器端 (Physical server) 就是你執行伺服器版本jar的那個執行環境,這邊指的是那個(如果有開GUI)開起來有白色視窗,跑一大堆文字,還有顯示目前記憶體用量和Tick運算時間的那個程式  - 物理客戶端 (Physical client) 就是你執行客戶端,用那個launcher開起來的執行環境,這邊指的是可以選版本、設定檔,然後把遊戲打開後的那個主程式  - 邏輯伺服器端 (Logical server) Minecraft程式執行過程中,創建出來的伺服器端,用來為邏輯客戶端服務 這邊當然沒有圖,它是 "邏輯",是虛擬的 - 邏輯客戶端 (Logical client) Minecraft程式執行過程中,負責讓玩家遊玩,處理鍵盤、滑鼠輸入、顯示以及和伺服器通訊。 當然,沒圖 好,聰明的你可能發現了 ( `∀`)つt\[ ] **在單人遊玩世界的時候,物理伺服器端在任何地方都不存在** **在單人遊玩世界的時候,物理伺服器端在任何地方都不存在** **在單人遊玩世界的時候,物理伺服器端在任何地方都不存在** 很重要所以要說三次 如果你還有疑問,回頭看剛才我在 物理伺服器端 和 物理客戶端 中附的兩張圖吧 當然,你現在應該可以理解另外幾件同樣道理的事 因為 **物理客戶端** 可能同時包含了 **邏輯伺服器端** 和 **邏輯客戶端**,所以 - **邏輯客戶端** 一定會是 **物理客戶端** - **物理客戶端** 卻不一定是 **邏輯客戶端** - **物理伺服器端** 如果存在,永遠只可能是 **邏輯伺服器端** - **邏輯伺服器端** 卻不一定是 **物理伺服器端** 好的,定義就先到這,希望你腦袋沒有打結 (\*゚ー゚) 我們先來休息一下講另一件事吧 執行端的分工 --- 分工,我們先撇開物理執行端的部分不聊 因為物理執行端其實對我們來說沒有什麼太大的意義,重要的會是邏輯執行端 (=´ω`=) 我簡單舉例一些邏輯執行端負責的工作 1. 邏輯伺服器端 - 掌管地圖的創建 - 掌管世界每個Tick會發生的事,更新資料 - 接收邏輯客戶端傳來的資料,並依規則改變世界內容 - 把資料傳送給邏輯客戶端 - 其他... 2. 邏輯客戶端 - 處理玩家的輸入(鍵盤、滑鼠) - 將玩家的動作用網路傳給邏輯伺服器端 - 接收邏輯伺服器端傳來的資料 - 運算及顯示畫面供玩家查看 - 其他... 聽起來很簡單對吧,但其實很多人會在分工上出差錯 (╥﹏╥) 試想一個情境,玩家吃下一個特別的模組食物,會給予玩家經驗值十點 你來比較下面這兩個解決方式和分工 1. 客戶端發現玩家吃下了食物,傳封包給伺服器端,通知伺服器把玩家經驗值加十點 伺服器端收到封包,依據封包內容給予玩家經驗值加十點,更新資料 2. 客戶端發現玩家吃下了食物,傳封包給伺服器端,通知伺服器玩家吃下了手中的食物 伺服器端收到封包,根據遊戲規則給予玩家十點經驗值,更新資料 你覺得哪一個方式會出問題? ... 答案是第一個解決方案可能會出事 :;(∩´﹏\`∩);: 不管是遊戲界、網頁前端、後端,都有一句該銘記在心中的話 **永遠不要相信客戶端傳來的資料** **永遠不要相信客戶端傳來的資料** **永遠不要相信客戶端傳來的資料** 很重要所以我說三次 (#\`皿´) 你可能會想,欸客戶端的程式也是我寫的阿,不會有規格外的資料傳過來吧? (\*´・д・)? 你要知道,封包這種東西是很容易偽造的,有一點資訊底子的人都能做到 >*你如果真的不知道怎麼偽造,你可以看[Wireshark](https://www.wireshark.org/)這個軟體,但請不要把它拿來做壞事 >甚至攻擊者可以下載你開源的模組,竄改後重新編譯,在他的客戶端執行* 剛才的例子一,如果客戶端刻意製作一個假的封包,通知伺服器把玩家經驗值加一百點呢? 伺服器很可能就傻傻的幫玩家加一百點經驗了 (’へ’) 如果你怕出漏洞,分工有一個很簡單,絕對不會弄錯的思考方式 **永遠都只把玩家的動作傳給伺服器** **只有動作,動!作!** 其他的一切都給伺服器處理,同時伺服器也要進行檢查 Ex. 玩家是否有權限這樣做、玩家在伺服器的資料中,手中的東西是否符合條件、伺服器其他相關的資料是不是也支持這樣做 這樣子,就可以抵擋 **大部分** 的不肖玩家的破壞 對,大部分,攻擊伺服器、利用伺服器漏洞這種事情要追究是沒完沒了的 (´・ω・\`) 實現 "只在某執行端XXX" 的幾個方式 --- 我先列出來,後面再一一解釋 - `net.minecraft.world.World.isRemote()` - `net.minecraftforge.api.distmarker.Dist` - `net.minecraftforge.fml.DistExecutor` - `java.lang.Thread.currentThread().getThreadGroup()` - `net.minecraftforge.api.distmarker.OnlyIn` 喔對了,因為是共用jar檔的關係,如果你某段程式碼什麼都沒做 **這段程式碼會在客戶端和伺服器端同時執行** 問題就會出現了,熔爐在燒東西 結果因為時間不是完美對上的關係,客戶端和伺服器端燒好的時間不一樣 這兩邊的資料就會不一樣 儘管基本上,衝突的部分伺服器的資料會蓋過客戶端的資料 (有例外),但是這樣還是會有些問題 比如說玩家在客戶端看到東西燒好了,然後拿出來,但伺服器端還沒燒好 就會發生玩家拿出物品,但過一陣子,玩家手上的東西就莫名消失 不過根據伺服器資料,東西不是完全消失,而是留在熔爐裡 *(這個例子,應該要是只有伺服器在燒東西,客戶端負責不斷問伺服器燒的進度,然後顯示給玩家)* 所以,我們必定會遇到需要在不同執行端有不同程式行為的狀況 OK,我們開始解釋剛剛列表中的方法吧 (ง๑ •̀_•́)ง - `net.minecraft.world.World.isRemote()` **執行端種類:邏輯** 當你手中可以拿到一個`World`的實例(Instance)的時候,你可以透過`isRemote()`知道這個世界是否是被 "遙控" 的 簡單來說,客戶端的世界是被伺服器遙控著的 (\*´д\`) 所以這方法回傳`true`代表你當前正在客戶端,回傳`false`則代表伺服器端 - `net.minecraftforge.api.distmarker.Dist` **執行端種類:物理** `Dist`是一個列舉 (Enum),值有兩個 - `Dist.CLIENT` - `Dist.DEDICATED_SERVER` 同時,你可以從`net.minecraftforge.fml.loading.FMLEnvironment.dist`拿到一個`Dist`,代表當前的執行環境 `Dist`還提供兩個方法(Method)使用,分別是`isClient()`和`isDedicatedServer()` 最後,還記得我前面說過在單人世界不存在物理客戶端嗎? 是的,如果是 **單人世界** ,不管是邏輯客戶端或邏輯伺服器端,這邊的值 **永遠** 都會是`Dist.CLIENT` **\~永\~遠~** ╮(╯∀╰)╭ **都是** `Dist.CLIENT` - `net.minecraftforge.fml.DistExecutor` **執行端種類:物理** `DistExecutor`提供了三個靜態方法 - `callWhenOn()`,你可以透過傳入`Dist`和`Supplier<Callable<T>>`達到在某執行端時執行這個`Callable<T>`並回傳結果 - `runWhenOn()`和上面相似,但你傳入的型態改成了`Supplier<Runnable>`,也就是說不會有結果回傳 - `runForDist()`,你可以一次指定兩個執行端的工作,傳入型態是`Supplier<Supplier<T>>`,第一個參數代表客戶端要執行的內容,第二個參數代表伺服器端 一個簡單的例子 (\*゚ー゚) ```java= int result = DistExecutor.callWhenOn(Dist.CLIENT, () -> { return () -> { int returnValue = 0; return returnValue; }; }); ``` 請注意,以上的程式碼如果是在`Dist.DEDICATED_SERVER`的環境執行,`result`的值會是`null` 後續處理請小心,或是直接一個簡單的`if(result != null)`就可以了 (\^u\^) - `java.lang.Thread.currentThread().getThreadGroup()` **執行端種類:邏輯** 這邊用的方法是檢查當前執行續所屬的群組`ThreadGroup`,然後 **猜測** 是客戶端還是伺服器端 Forge在`net.minecraftforge.fml.common.thread.SidedThreadGroups`宣告了兩個靜態的`SidedThreadGroup`(繼承了`java.lang.ThreadGroup`) - `SidedThreadGroups.CLIENT` - `SidedThreadGroups.SERVER` 所以你可以這樣使用 (。A。) ```java= if(Thread.currentThread().getThreadGroup() == SidedThreadGroups.SERVER){ //Do something on "LOGICAL SERVER" } ``` - `net.minecraftforge.api.distmarker.OnlyIn` **執行端種類:物理** 這是一個註解(Annotation),他可以用在類別、方法和欄位上 用法大概會像這樣 (\*゚∀゚\*) ```java= @OnlyIn(Dist.CLIENT) public class YourClass{ } ``` 當使用了這個註解,註解後 **緊鄰** 的那個東西會在另一個執行端 **徹底消失** 比如剛剛舉例的`YourClass`,他在伺服器端載入時被Forge從程式碼中抹除 如果你在伺服器中嘗試引用這個類別時,會丟出`java.lang.ClassNotFoundException` 另外要注意的,就是我剛剛特別強調的 **緊鄰** ,這問題通常會出現在把`@OnlyIn`使用在欄位的時候 比如說以下程式碼會導致在伺服器端Crash,即使這欄位從頭到尾都沒被人引用過 ```java= @OnlyIn(Dist.CLIENT) public SomeClass field = new SomeClass(); ``` 因為這段程式碼`@OnlyIn`抹除的只有`field`的宣告,等號後面的程式碼卻還會在 因此他會`new`了一個`SomeClass`,但是找不到欄位放進去,然後默默的Crash > 嘿,我想現在的你已經知道要怎麼用程式碼處理兩個執行端 > 然後打算關掉這一頁教學回頭去寫你的程式碼了,我可以理解 > 但下面那一節很重要,這不是工商,只是因為這東西真的很重要 > 希望你可以忍耐一下,把下一節的內容看完吧 (・ε・) 如何選擇使用哪個方法來達成目的 --- 可以分辨執行端的方法這麼多種,感覺好像很棒 \(●´ϖ\`●)/ 呃...事情沒你想像的這麼簡單... 首先第一點,你通常在需要辨認執行端的時候,你需要的是邏輯上的執行端,而不是物理 這邊幫大家重新整理剛才提到的方法,並依照邏輯和物理分類 - 邏輯 - `net.minecraft.world.World.isRemote()` - `java.lang.Thread.currentThread().getThreadGroup()` - 物理 - `net.minecraftforge.api.distmarker.Dist` - `net.minecraftforge.fml.DistExecutor` - `net.minecraftforge.api.distmarker.OnlyIn` 是的,如果你需要的是邏輯上的執行端,選擇瞬間少一半 ( ´Д\`) 首先我們先來談談屬於處理 **邏輯執行端** 的這兩個方法 基本上Forge推薦優先使用`World.isRemote()` 但這方法有一個致命的缺點,那就是首先你必須要拿到一個`World`實例 沒有`World`實例,你就沒辦法用`isRemote()`這方法 (´-ω-`) 喔對了,不要妄想用`net.minecraft.client.Minecraft.world`這靜態欄位拿到實例 因為`Minecraft`這類別只在 **客戶端** 有,伺服器端是不會有`Minecraft`這類別的 你可能會問說,`World`這麼重要的東西難道沒有任何靜態方法可以拿到嗎? (。ŏ_ŏ) 因為每一個維度(Dimension),就有一個對應的`World`實例 對於客戶端的玩家來說,`World`實例只會有一個,因為玩家不可能同時身處終界和地獄 對於伺服器端來說,`World`實例就會有很多個了,可能這玩家在地獄,那個玩家在主世界 所以這是完全不一樣的,沒有任何方法可以給雙端共用,來取得`World`實例 > 順帶一提,伺服器端如果要獲得`World` > 要使用`net.minecraft.server.MinecraftServer.getWorld(DimensionType)` > 這個`MinecraftServer`理所當然的是只有伺服器端才有的類別 為了要從任何地方都能拿到`World`實例,你首先要知道你在客戶端還是伺服器端 呃,我們不就是為了要用`World.isRemote()`來知道執行端,所以才需要一個`World`實例嗎 ( •́ _ •̀)? 所以如果你真的沒辦法在雙端都不會Crash前提下拿到`World`實例的話 你的選擇就只剩第二個`Thread.currentThread().getThreadGroup()` 但這麼好的方法,在哪都能用,Forge卻推薦使用`World.isRemote()`呢? 這是因為,這個方法**無法保證正確**,他只是透過執行緒的群組來 **猜測** 當前的執行環境而已 如果今天有一個人用在客戶端用`SidedThreadGroups.SERVER`開啟了一個新的執行緒呢? ( ˘•ω•˘ ) 雖然應該不會有人會這樣做,但這已經充分說明這方法並不是完全可靠的 *(這個例子是M鼠想的,[Forge官方](https://mcforge.readthedocs.io/en/1.15.x/concepts/sides/)用 "guess" 來描述這方法的不可靠性,但是沒有給出一個例子,所以可能還會存在著其他讓這方法失效的狀況 )* 所以邏輯執行端的總結是,優先用 `World.isRemote()`,然後才用`Thread.currentThread().getThreadGroup()` 換說**物理執行端**的部分吧 其實物理執行端的這三個方法沒什麼好比較誰優先使用的 因為他們的核心都圍繞在`Dist`這個列舉 (Enum) 上 (・∀・) 不過有一個東西一定要拿出來特別講,這個傢伙就是`@OnlyIn` 來看一下Forge為這東西寫下的其中一段註解吧 ```java= /* * .... * * This is generally meant for internal Forge and FML use only * and modders should avoid its use whenever possible * * .... */ ``` 啥鬼?這東西是Forge和FML專用的?然後我們模組作者最好別用?這麼小氣的嗎? (╯\`□′)╯︵┴─┴ Forge給的[理由](https://www.minecraftforge.net/forum/topic/18410-172-worldisremote-and-sideonlysideserver/)其實很簡單 *(這邊提到的`@SideOnly`,就是過往的`@OnlyIn`,功能上是相等的)* > That combined with the fact that unexperienced modders usually understand @SideOnly as a way to differentiate between Client and Server Thread ("logical side") leads to me not recommending it 也就是說,初學的模組作者很容易誤會,並把邏輯執行端和物理執行端搞混 ┬─┬ ノ( ' - 'ノ) *(M鼠會誠實地說,我也曾經在這犯錯過,但希望看了這篇教學的你不會犯錯 )* 搞混的後果是什麼呢? 通常就是一個`ClassNotFoundException`往你臉上砸,然後遊戲就華麗的Crash了 (╯°▽°)╯ ┻-┻ 開發時Crash是好事,它會讓我們注意到有問題存在,然後修改程式碼 但玩家在玩你的模組時Crash就不是件好事了,嚴重的話玩家會連自己辛苦建造的地圖都打不開來 不過,當然不代表你完全不該用這東西 還是有幾個情況下可以用,甚至是建議用的 ┳-┳ノ( OωOノ) 比如說你正在覆寫一個原本就已經帶有這個標記的方法(Method)啦 或者說你很清楚自己在做甚麼,打算把它用在你處理Rendering的Class啦 ... ... ...我是很想這樣說啦... 各位,這邊原本有一百多字,但被我刪了,現在要來重寫...(〒︿〒) 根據LexManos *(Forge的核心人物之一)* 本人在2020/06/01對[這篇帖子]()的回覆 打翻了所有人以前的觀念 *(其實這整個過程有點像在演八點檔,~~有興趣可以去2020/06/01的碎碎念看~~)* 這邊節錄一下 > No, You should NEVER be using these annotations. Not even for your rendering code. Not even if the overridden method has it. NEVER. If you run into sided issues, bypass it using proper side checks. If you never call the client side code, on the dedicated server, it doesn't matter if the classes are in your jar. 恩,LexManos的想法是無論如何你都不該用到這東西 因此,在物理執行端的部分,你可以放心地用前面提到的`Dist`和`DistExecutor` **而`@OnlyIn`請永遠不要使用它** (☍﹏⁰) 我能理解,對於模組開發者來說,`@OnlyIn`不是必要的東西 同時,`@OnlyIn`又帶有很嚴重的副作用 ~~但我還沒能理解LexManos對待這件事這麼嚴厲的原因~~ M鼠對ASM並不是非常的理解,如果哪天開竅了,再來跟大家好好聊聊吧 (つд⊂) --- *本頁面撰寫於2020/05/23,目前最後更新日期為2020/06/02* *若上述時間與你閱讀的時間相距過遠,請自行斟酌是否採用本頁面的資訊* *完整的程式碼可以到本教學文的[Github Repo](https://github.com/immortalmice/MinecraftForge1.15.2-ModdingTutorial)中查看*
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up