Minecraft Bedrock сервер на Go. Часть #2

В прошлой части мы написали основу для нашего сервера и разобрались с принципом, который используется для написания кастомных серверов Minecraft. В этой части мы разберем, почему наш сервер все таки сервер, подумаем о том как лучше расширять его функциональность, научимся банить пользователей.

Почему это все таки сервер(или все-таки прокси)

Читая первую часть статьи, могло сложиться мнение, что мы пишем ничем не примечательный прокси сервер и он не сможет сравниться с написанием полностью самописного. Это в корне неверное мнение и далее мы рассмотрим почему.

Как устроенна локальная сессия игры?

Когда мы запускаем локальную сессию игры, клиент Minecraft запускает встроенный сервер:

Интересно что сервер доступен как по 19132 порту, так и по 50968
Интересно что сервер доступен как по 19132 порту, так и по 50968

Это создает возможность подключаться к нашему миру как через локальную сеть, так и через xbox live(который тут выступает в виде сервиса публичного туннеля). Вероятнее всего, основной клиент общается с миром точно так же, как с ним общаются все остальные клиенты, через UDP протокол RakNet. На основе этого предположения, мы можем сделать вывод, что клиентская часть игры существует обособлено от управляющей и основное ядро Minecraft заложенно в серверной части.Написать свой сервер с нуля, это написать свой Minecraft. Возможно ли это? Возможно, протоколы полностью описаны, существуют реализации протокола на разных языках. Нужно ли нам это для модификации игрового процесса? Давайте разбираться.

Можно ли называть наш сервер прокси?

Является ли сервер, прокси между клиентом и базой данных? Является ли база данных прокси между сервером и файловой системой? Наша прослойка общается с оригинальным сервером и клиентом по тому-же протоколу, по которому оригинальный сервер и клиенты могли бы общаться сами со себе. Но делает ли это нашу прослойку просто прокси? Оригинальный сервер в нашем случае, выступает в роли готового движка, который предоставляет api интерфейс для нас, через udp, когда как наш сервер содержит всю дополнительную логику:

Зачем нам реализовывать ожидаемую от Minecraft логику с нуля, когда мы можем подмешивать к уже существующей свою? Нужна ли нам та гибкость, которую мы получим при нативном обращении к коду движка?

Так все выглядит на самом деле
Так все выглядит на самом деле
Так это должно быть у нас в голове
Так это должно быть у нас в голове

Вероятнее всего цепочка действий, имей бы мы возможность обращаться к движку нативно, выглядела бы точно также.

Как расширять функциональность нашего сервера?

Думая о том, как можно расширять функциональность нашего сервера, так чтобы различные части функциональности могли работать как синхронно, так и асинхронно, я не смог придумать варианта лучше, чем разделение функциональности на модули, последовательность выполнения которых будет настраиваться в callback структуры сервера:

server/server.go

package server  import ( "github.com/sandertv/gophertunnel/minecraft" "log" )  type Server struct { listener          *minecraft.Listener ConnectionHandler func(*minecraft.Conn, *minecraft.Listener) }  func (server *Server) Start() { p, _ := minecraft.NewForeignStatusProvider(":19131")  listener, err := minecraft.ListenConfig{ StatusProvider:         p, AuthenticationDisabled: true, }.Listen("raknet", ":19130") if err != nil { panic(err) }  server.listener = listener server.acceptConnections() }  func (server *Server) acceptConnections() { for { c, err := server.listener.Accept() if err != nil { log.Println(err) }  go server.ConnectionHandler(c.(*minecraft.Conn), server.listener) } }

modules/init_dialer.go

package modules  import "github.com/sandertv/gophertunnel/minecraft"  type InitDialer struct{}  func (InitDialer) Run(conn *minecraft.Conn) (*minecraft.Conn, error) { return minecraft.Dialer{ ClientData: conn.ClientData(), }.Dial("raknet", ":19131") } 

modules/spawner.go

package modules  import ( "github.com/sandertv/gophertunnel/minecraft" "sync" )  type Spawner struct{}  func (Spawner) Run(conn *minecraft.Conn, dialer *minecraft.Conn) error { var err error  g := sync.WaitGroup{}  g.Add(2)  go func() { err = conn.StartGame(dialer.GameData())  if err != nil { return }  g.Done() }()  go func() { err := dialer.DoSpawn()  if err != nil { return }  g.Done() }()  g.Wait()  return err }

modules/proxy.go

package modules  import "github.com/sandertv/gophertunnel/minecraft"  type Proxy struct{}  func (Proxy) Run(conn *minecraft.Conn, dialer *minecraft.Conn, listener *minecraft.Listener) { go func() { defer dialer.Close() defer listener.Disconnect(conn, "connection lost")  for { pk, err := conn.ReadPacket() if err != nil { return }  err = dialer.WritePacket(pk) if err != nil { return } } }()  go func() { defer dialer.Close() defer listener.Disconnect(conn, "connection lost")  for { pk, err := dialer.ReadPacket() if err != nil { return }  err = conn.WritePacket(pk) if err != nil { return } } }() }

main.go

package main  import ( "github.com/sandertv/gophertunnel/minecraft" "minecraft-server/modules" "minecraft-server/server" )  func main() { s := server.Server{ ConnectionHandler: handleConnection, } s.Start() }  func handleConnection(conn *minecraft.Conn, listener *minecraft.Listener) { dialer, err := modules.InitDialer{}.Run(conn)  if err != nil { listener.Disconnect(conn, "Connect error")  return }  err = modules.Spawner{}.Run(conn, dialer)  if err != nil { listener.Disconnect(conn, "Spawn error")  return }  modules.Proxy{}.Run(conn, dialer, listener) }

В нашем случае, мы обрабатываем ошибки синхронных модулей, чтобы иметь возможность завершить выполнение функции, когда как асинхронные модули берут эту ответственность на себя. Недостатком такой организации очевидны. Если какой-то асинхронный модуль сделает что с conn, dialer, listener, остальные асинхронные модули использующие их, посыпятся. Модуль в нашем случае понятие не имеющее никакого четкого определения и не подчиняющиеся никаким конкретным правилам. Мы можем позволить синхронным модулям обрабатывать ошибки, вызывать модули из других модулей, позволять им быть ассинхронными и синхронными одновременно.

Баним пользователя

modules/ban.go

package modules  import ( "errors" "fmt" "github.com/sandertv/gophertunnel/minecraft" )  type Ban struct{}  func (Ban) Run(conn *minecraft.Conn) error { if conn.IdentityData().DisplayName == "Steve" { return errors.New("player is banned") }  return nil } 
func handleConnection(conn *minecraft.Conn, listener *minecraft.Listener) { err := modules.Ban{}.Run(conn)  if err != nil { listener.Disconnect(conn, err.Error())  return } }

Заключение

Исходный код приведенный в публикации доступен здесь. Буду очень рад, если вы предложите свои варианты организации добавления функциональности и/или приведете аргументы против той, что используется в статье. В последующих частях мы займемся добавлением более функциональных модулей и углубимся в возможности общения как с клиентом, так и с оригинальным сервером Minecraft.


ссылка на оригинал статьи https://habr.com/ru/post/645769/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *