+9

Hướng dẫn xây dựng game server đơn giản bằng Golang - Part 1

Như tiêu đề, trong bài viết này mình sẽ hướng dẫn các bạn xây dựng một game server bằng Golang, tuy nhiên mình sẽ không đi sâu vào từng dòng code, mà thay vào đó, mình sẽ mô hình hoá và đưa ra các module cần thiết để các bạn có thể code được một server đơn giản cho các game chiến thuật hay multiplayer sử dụng SOCKET

1. Các công cụ cần chuẩn bị Điều đầu tiên và không thể thiếu đó chính là Go. Các bạn vào trang chủ tại https://golang.org/ và download binary distribution tương ứng với hệ điều hành mà bạn đang sử dụng Và để code thì chúng ta cần phải có 1 IDE hay Text Editor bất kỳ. Hiện nay có rất nhiều Text Editor khác nhau có thể cài thêm plugin golang giúp tiện hơn cho quá trình code, bài này mình hướng dẫn các bạn sử dụng Visual Studio Code, theo mình đánh giá là khá hay và thuận tiện. Nếu các bạn đã từng có nghiên cứu về golang rồi thì sẽ biết, việc debug golang step by step là điều không hề dễ dàng, theo kinh nghiệm cá nhân mình thì chủ yếu là mình dùng console log. Còn nếu các bạn vẫn muốn debug step by step thì có thể sử dụng delve hoặc sử dụng 1 IDE đang trong giai đoạn thử nghiệm của Intellij, download tại https://www.jetbrains.com/go/. Mình cũng đã từng thử qua Intellij Gogland, nhưng chạy khá nặng và hiện tại thì việc jump tới file đang chưa thực sự tốt Quay trở lại với visual studio code, các bạn có thể download tại đây https://code.visualstudio.com/

Sau khi cài xong visual studio code, các bạn vào mục plugin và tìm với từ khoá golang và install plugin của lukehoban

Và cuối cùng là các bạn install một số package cơ bản để sử dụng golang với visual code theo link sau https://github.com/Microsoft/vscode-go

2. Mô hình hoá server Với các bạn từng lập trình với socket thì có thể dễ dàng hình dung ra các module cần thiết cho một socket server. Ở đây mình đưa ra mô hình đơn giản mà mình đang sử dụng Như hình vẽ trên, các module bao gồm:

  • Agent
  • Core net
  • Database
  • Matcher
  • Config
  • Logging

Với mô hình này, agent là nơi khởi tạo socket server, khởi tạo kết nối tới database, ..., nó được coi là trung tâm của mọi xử lý. Core net chính là module socket. Bài viết này mình chỉ đề cập tới tcp socket và không sử dụng websocket. Database mình sử dụng là postgresql, và mình sử dụng package pgx của jackc https://github.com/jackc/pgx Matcher là module ghép cặp các connection, ở đây có thể hiểu là player. Ví dụ trong game cờ caro, 2 đối thủ đấu với nhau sẽ là một cặp, matcher có nhiệm vụ tìm các connection và kết hợp lại theo từng cặp để đấu với nhau Config, như tên gọi, sẽ là nơi chứa các thông tin cấu hình server, ví dụ game cờ caro, config bao gồm host address, host port, database host, database port, kích thước bàn chơi (số ô vuông có trong bàn chơi theo mỗi chiều) Và cuối cùng là logging, đương nhiên khi lập trình, các bạn sẽ cần log các thông tin quá trình chạy server để theo dõi, tìm sửa lỗi server

Với các server phức tạp sẽ có thể có thêm nhiều module khác nữa, ví dụ như module về http, để giải quyết các vấn đề về billing, cms, hay các module về bảo mật dữ liệu... Mục tiêu của bài viết chỉ dừng lại ở mức xây dựng một mô hình game server đơn giản, giúp các bạn dễ tiếp cận hơn với MMO

3. Xây dựng các module trong server 3.1. Config Module đầu tiên và đơn giản nhất trong hệ thống, đó chính là config. Để load/save config cho hệ thống, mình giới thiệu các bạn sử dụng package viper, download tại đây https://github.com/spf13/viper Đầu tiên, các bạn tạo một file tên là config.json đặt vào thư mục ~/.[tên_project], ở đây ví dụ server của mình đặt tên là sample, thì file config.json sẽ nằm ở đường dẫn ~/.sample/config.json Nội dung file config.json như sau:

{
    "host": {
        "address": "localhost",
        "backend_port": "7201",
        "frontend_port": "7101"
    }
}

Để load config, trong module config, các bạn tạo file config.go với init như sau

func init() {
	viper.SetConfigName("config")      // name of config file (without extension)
	viper.SetConfigType("json")        // config file extension
	viper.AddConfigPath("$HOME/.sample") // optionally look for config in the working directory
	err := viper.ReadInConfig()        // Find and read the config file
	if err != nil {                    // Handle errors reading the config file
		panic(fmt.Errorf("Fatal error config file: %s \n", err))
	}

	Host = viper.GetString("host.address")
	Port = viper.GetString("host.backend_port")

	MatchingTimeout = 5 * time.Second
	FinishToRestartTimeout = 5 * time.Second
	MovePlacemarkTimeout = 7 * time.Second

	BoardWidth = 16
	BoardHeight = 16
}

Và để config cho postgresql, các bạn tạo code như sau

// Config returns the postgres config object
func Config(cmd *cobra.Command) (config pgx.ConnPoolConfig) {
	var err error
	var vHost, vDatabase, vUser, vPassword = host, database, user, password
	if cmd != nil {
		vHost = cmd.Name() + "_" + host
		vDatabase = cmd.Name() + "_" + database
		vUser = cmd.Name() + "_" + user
		vPassword = cmd.Name() + "_" + password
	}
	config.ConnConfig, err = pgx.ParseEnvLibpq()
	if err != nil {
		return config
	}
	config.Host = viper.GetString(vHost)
	config.Database = viper.GetString(vDatabase)
	config.User = viper.GetString(vUser)
	config.Password = viper.GetString(vPassword)

	return
}

3.2. Logging Để thực hiện log, mình sử dụng package zap của Uber, vâng, Uber. Để sử dụng package zap, các bạn sử dụng go get https://github.com/uber-go/zap. Tại thời điểm mình sử dụng để viết sample này thì zap còn đang ở trạng thái alpha, và mình phải fork về repo của mình và chỉnh sửa lại một chút, có lẽ ở thời điểm hiện tại thì không cần phải làm vậy nữa. Trong module log, các bạn tạo một file log.go với nội dung như sau

var logger zap.Logger

func init() {
	logger = zap.New(
		zap.NewJSONEncoder(
			zap.RFC3339Formatter("timestamp"), // human-readable timestamps
		),
		zap.DebugLevel,
	)
}

// Debug logs a message at level Debug on the standard logger.
func Debug(msg string, fields ...zap.Field) {
	logger.Debug(msg, fields...)
}

// Info logs a message at level Info on the standard logger.
func Info(msg string, fields ...zap.Field) {
	logger.Info(msg, fields...)
}

// Warn logs a message at level Warn on the standard logger.
func Warn(msg string, fields ...zap.Field) {
	logger.Warn(msg, fields...)
}

// Error logs a message at level Error on the standard logger.
func Error(msg string, fields ...zap.Field) {
	logger.Error(msg, fields...)
}

// Panic logs a message at level Panic on the standard logger.
// then call panic()
func Panic(msg string, fields ...zap.Field) {
	logger.Panic(msg, fields...)
}

// Fatal logs a message at level Fatal on the standard logger
// then call os.Exit(1)
func Fatal(msg string, fields ...zap.Field) {
	logger.Fatal(msg, fields...)
}

/// Golang không cho phép bạn import chéo package, vì vậy, khi các bạn code cần cố gắng chia module rõ ràng.

Ở phần này, mình đã giới thiệu các component đơn giản cần có trong game server, và đã giới thiệu các bạn 2 modules đơn giản nhất của hệ thống: Config, Logging. Trong phần tiếp theo, mình sẽ hướng dẫn các bạn xây dựng Agent và Core Net

Happy coding


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.