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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

В прошлой части мы написали основу для нашего сервера и разобрались с принципом, который используется для написания кастомных серверов 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/


Интересные статьи

Интересные статьи

Эта статья — перевод оригинальной статьи Andy Li из Vue Mastery "Vue 3 Migration Build: safely upgrade your app to Vue 3 (Pt. 1)".Команда Vue недавно выпустила долгожданный билд миграции для Vue ...
Вы, наверное, в последнее время часто слышите о новых продуктах Сбера, со многими из них сталкиваетесь как клиенты.А есть в Сбере крупные и сложные технологические проект...
По стране полным ходом шагает импортозамещение, и всё чаще мы видим тестирование различных устройств, произведенных в России. При этом, производители обычно стыдливо обхо...
Все больше инженеров выбирают Kotlin для разработки серверных приложений. Полная совместимость с Java, корутины и высокая безопасность делают Kotlin отличным инструментом для подобных зад...
Пару лет назад мы опубликовали RESTinio — свой небольшой OpenSource C++фреймворк для встраивания HTTP-сервера в C++ приложения. Мегапопулярным за это время RESTinio не стал, но и не потерялся. Кт...