Jump to content
Sign in to follow this  
  • entries
    25
  • comments
    26
  • views
    26353

Server and client for SnailLife Go

Sign in to follow this  
Liza Shulyayeva

787 views

Over the last couple of days I restructured SnailLife Go into a server and client. I’m still in the “rough draft” stage, but the top level project structure now looks like this:

gosnaillife
├── client
├── cmd
├── common
├── LICENSE.md
├── README.md
├── server
└── setup

Intent

  • Split application into server and client CLI apps.
  • Create REST API for client-server communication
  • Have a “common” package for structures which will be reused by both server and client
  • Create some rudimentary deployment scripts for both server and client

main.go

I started by creating snaillifesrv/main.go alongside snaillifecli/main.go The cmd directory now looks like this:

cmd
├── snaillifecli
│   └── main.go
└── snaillifesrv
    └── main.go

snaillifecli/main.go

The client main.go runs some simple configuration with viper (right now there is just a server.json config file with the server url to connect to depending on which environment you are running). After running the configuration it waits for user input. Once input is received, it tries to find and run a cobra command by that name.

package main

import "fmt"
import (
	"os"
	"bufio"
	"gitlab.com/drakonka/gosnaillife/client/lib/interfaces/cli"
	"gitlab.com/drakonka/gosnaillife/client/lib/interfaces/cli/commands"
	"runtime"
	"strings"
	"path/filepath"
	"io/ioutil"
	"github.com/spf13/viper"
	"gitlab.com/drakonka/gosnaillife/common/util"
)

func main() {
	fmt.Println("Welcome to SnailLife! The world is your oyster.")
	configureClient()
	if err := commands.RootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	waitForInput()
}

func configureClient() {
	projectRoot := getProjectRootPath()
	confPath := projectRoot + "/config"
	envname, err := ioutil.ReadFile(confPath + "/env.conf")
	if err != nil {
		util.HandleErr(err, "")
	}
	envFile := string(envname)
	configPath := confPath + "/" + envFile

	viper.AddConfigPath(configPath)

	// Config client
	viper.SetConfigName("server")
	err = viper.ReadInConfig()
	if err != nil {
		util.HandleErr(err, "")
	}
}

func waitForInput() {
	buf := bufio.NewReader(os.Stdin)
	fmt.Print("> ")
	input, err := buf.ReadBytes('\n')
	if err != nil {
		fmt.Println(err)
	} else {
		cmd, err := cli.TryGetCmd(string(input))
		if err != nil {
			fmt.Println(err)
		} else {
			err := cmd.Execute()
			if err != nil {
				fmt.Println("ERROR: " + err.Error())
			}
		}
	}
	waitForInput()
}

func getProjectRootPath() string {
	_, b, _, _ := runtime.Caller(0)
	folders := strings.Split(b, "/")
	folders = folders[:len(folders)-2]
	path := strings.Join(folders, "/")
	basepath := filepath.Dir(path) + "/client"
	return basepath
}

snaillifesrv/main.go

When launching snaillifesrv, a subcommand is expected immediately. Right now the only supported subcommand is serve, which will start the server.

package main

import "fmt"
import (
	"gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/env"
	"os"
	"runtime"
	"path/filepath"
	"strings"
	"gitlab.com/drakonka/gosnaillife/server/lib/infrastructure"
	"gitlab.com/drakonka/gosnaillife/common"
	"gitlab.com/drakonka/gosnaillife/server/lib/interfaces/cli/commands"
)

var App env.Application

func main() {
	setProjectRootPath()
	confPath := env.ProjectRoot + "/config"
	App = infrastructure.Init(confPath, common.CLI)
	if err := commands.RootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func setProjectRootPath() {
	_, b, _, _ := runtime.Caller(0)
	folders := strings.Split(b, "/")
	folders = folders[:len(folders)-2]
	path := strings.Join(folders, "/")
	basepath := filepath.Dir(path) + "/server"
	env.ProjectRoot = basepath
}

Client

So far an extremely barebones implementation, it looks like this:

client
├── config
│   ├── config.go
│   ├── dev
│   │   └── server.json
│   └── env.conf
└── lib
    └── interfaces
        └── cli
            ├── cli.go
            ├── cmd.go
            └── commands
                ├── register.go
                ├── root.go
                └── test.go

Right now only the register command is implemented. 

Server

The server is where the bulk of the existing packages ended up going:

server
├── config
│   ├── config.go
│   ├── dev
│   │   ├── auth.json
│   │   └── database.json
│   └── env.conf
└── lib
    ├── domain
    │   ├── item
    │   └── snail
    │       ├── snail.go
    │       └── snailrepo.go
    ├── infrastructure
    │   ├── auth
    │   │   ├── authenticator.go
    │   │   ├── auth.go
    │   │   ├── cli
    │   │   │   ├── auth0
    │   │   │   │   ├── auth0.go
    │   │   │   │   └── tests
    │   │   │   │       ├── auth0_test.go
    │   │   │   │       └── config_test.go
    │   │   │   ├── cli.go
    │   │   │   └── cli.so
    │   │   ├── provider.go
    │   │   └── web
    │   ├── databases
    │   │   ├── database.go
    │   │   ├── mysql
    │   │   │   ├── delete.go
    │   │   │   ├── insert.go
    │   │   │   ├── mysql.go
    │   │   │   ├── retrieve.go
    │   │   │   ├── tests
    │   │   │   │   └── mysql_test.go
    │   │   │   └── update.go
    │   │   ├── repo
    │   │   │   ├── repo.go
    │   │   │   ├── tests
    │   │   │   │   ├── repo_test.go
    │   │   │   │   ├── testmodel_test.go
    │   │   │   │   └── testrepo_test.go
    │   │   │   └── util.go
    │   │   └── tests
    │   │       └── testutil.go
    │   ├── env
    │   │   └── env.go
    │   ├── init.go
    │   └── init_test.go
    └── interfaces
        ├── cli
        │   └── commands
        │       ├── root.go
        │       └── serve.go
        └── restapi
            ├── err.go
            ├── handlers
            │   └── user.go
            ├── handlers.go
            ├── logger.go
            ├── restapi.go
            ├── router.go
            └── routes.go

I followed a lot of the advice from this useful post about creating REST APIs in Go. When the user runs the register command on the client, here is what happens on the server. I have added comments to the copy below to help explain:

package handlers


import (
	"encoding/json"
	"fmt"
	"errors"
	"io/ioutil"
	"io"
	"net/http"
	"gitlab.com/drakonka/gosnaillife/common/restapi"
	"gitlab.com/drakonka/gosnaillife/common/util"
	"strings"
	"gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/auth"
	"gitlab.com/drakonka/gosnaillife/server/lib/infrastructure/env"
	http2 "gitlab.com/drakonka/gosnaillife/common/util/http"
)

func CreateUser(w http.ResponseWriter, r *http.Request) {
	fmt.Println("Creating user")
	var user restapi.UserReq
	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
    // The infamous Go error handling - I need a better way.
	if err != nil {
		util.HandleErr(err, "CreateUserErr")
		return
	}
	if err := r.Body.Close(); err != nil {
		util.HandleErr(err, "CreateUserErr")
		return
	}

    // Unmarshal the data we get from the client into UserReq
	if err := json.Unmarshal(body, &user); err != nil {
        // If we were unable to unmarshal, send an error response back to the client
		util.HandleErr(err, "CreateUserErr")
		w.Header().Set("Content-Type", "application/json; charset=UTF-8")
		w.WriteHeader(422) // unprocessable entity
		if err := json.NewEncoder(w).Encode(err); err != nil {
			util.HandleErr(err, "CreateUser")
			return
		}
		return
	}

	fmt.Println("Running registration")
	resBody, err := registerUser(user)
	if err != nil {
		util.HandleErr(err, "CreateUserErr")
	}

    // Start creating a userRes to send back to the client.
	userRes := buildUserResponse(resBody)
	status := http.StatusOK
	if err != nil {
		status = http.StatusInternalServerError
	}
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(userRes); err != nil {
		util.HandleErr(err, "CreateUserErr")
		return
	}
}

func registerUser(user restapi.UserReq) (resBody []byte, err error) {
    // Find an Auth0 provider (that is all we'll support for now)
	var auth0 auth.Provider
	auth0 = env.App.Authenticator.FindProvider("Auth0")
	if auth0 != nil {
		resBody, err = auth0.Register(user.Username, user.Password)
	} else {
		err = errors.New("Auth0 provider not found")
	}
	return resBody, err
}

func buildUserResponse(resBody []byte) (*restapi.UserRes) {
	res := restapi.UserRes{}
    // Find any keys we may find relevant from the Auth0 response body
	m, _ := util.FindInJson(resBody, []string {"_id", "statusCode", "name", "description", "error"})
	httpErr := buildHttpErr(m)
	if id, ok := m["_id"]; ok {
		res.Id = fmt.Sprintf("%v", id)
	}
	res.HttpErr = httpErr
	return &res
}

func buildHttpErr(m map[string]interface{}) (httpErr http2.HttpErr) {
    // The Auth0 response body *sometimes* contains errors in statusCode/name/description format and *sometimes* just contains a single "error" json key
	if sc, ok := m["statusCode"]; ok {
		codeStr := fmt.Sprintf("%v", sc)
		if strings.HasPrefix(codeStr,"4") || strings.HasPrefix(codeStr, "5") {
			scf := sc.(float64)
			httpErr.StatusCode = int(scf)
			httpErr.Name = fmt.Sprintf("%v", m["name"])
			httpErr.Desc = fmt.Sprintf("%v", m["description"])
		}
	} else if error, ok := m["error"]; ok {
		httpErr.StatusCode = 500
		httpErr.Name = "Error"
		httpErr.Desc = fmt.Sprintf("%v", error)
	}
	return httpErr
}

In the end the server sends a UserRes back to the client

package restapi

import (
	"gitlab.com/drakonka/gosnaillife/common/util/http"
)


type UserRes struct {
	HttpErr http.HttpErr `json:"httpErr"`
	Id string `json:"id"`
	Username string `json:"username"`
}

type UserReq struct {
	Username string `json:"username"`
	Password string `json:"password"`
	Connection string `json:"connection"`
}

Deployment

I made a couple of quick scripts to deploy client and server. Note that go-bindata lets you compile your config files into the binary, making for easier distribution (and maybe slightlyimproved security for the secret keys stored in the server config since you don’t have loose configs with credentials sitting around)

Client

#!/bin/sh

echo "Building and installing SnailLife"
go-bindata -o ../../client/config/config.go ../../client/config/...
cd ../../cmd/snaillifecli;
go build
GOBIN=$GOPATH/bin go install

Server

#!/bin/sh

echo "Building and installing SnailLife server"
go-bindata -o ../../server/config/config.go ../../server/config/...
cd ../../server/lib/infrastructure/auth/cli

echo "Building cli.so auth plugin"
go build -buildmode=plugin -o cli.so

echo "Building SnailLifeSrv"
cd ../../../../../cmd/snaillifesrv;
go build
GOBIN=$GOPATH/bin go install

Anyway, as you can see there is a long way to go. Up next I am going to write some tests for the REST API and the cobra commands (which I should really have been doing already).

snaillifecli-register.png

Sign in to follow this  


0 Comments


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement
  • Advertisement
  • Blog Entries

  • Similar Content

    • By MakeIndieGreatAgain
      Game developers will be able to become pioneers in the development of decentralized games for the gambling industry using DAO.Casino protocol.
      On September 17, 2018, DAO.Casino is opening Sandbox for developers, independent teams and game development studios that choose to harness the power of the rapidly developing DApp industry.
      Starting today everyone may submit their application for Sandbox on the official Sandbox page.
      The Sandbox project is designed by DAO.Casino developers. Participants of Sandbox will learn the basics of decentralized applications development on DAO.Casino protocol. Developers participating in Sandbox will learn to create, design and deploy decentralized games and applications on Ethereum blockchain.
      DAO.Casino is planning to reward most active developers for their constructive feedback on the improvement and optimization of the SDK and related documentation. The company will separately announce the details of the rewards program later this fall.
      “We are confident that the Sandbox project will play an important role in our collaboration with studios and independent game developers. We cannot wait to see our product helping developers unleash their creative and entrepreneurial talents and apply those to one of the most groundbreaking technologies of the XXI century. — states Ilya Tarutov, CEO, DAO.Casino. – I am sure that the products we’re developing will transform the online gambling into a fair and transparent industry for all of the involved parties: casino operators, developers, and affiliate marketers. “
      “We are launching the Sandbox with the goal of enabling as many developers as possible to learn to create decentralized games. We have achieved an important milestone by starting to accept applications from developers all around the world who share our idea to make online gambling fair and transparent. With our technology, developers can take the whole gambling industry to the next level” – says Alexandra Fetisova from DAO.Casino.
      DAO.Casino is disrupting the online gambling industry by developing the protocol based on Ethereum blockchain technology. The protocol ensures the automation of transactions and facilitates interactions between all the industry participants: casino operators, game developers, and affiliate marketers. DAO.Casino team is fully dedicated to developing the best products and making the gambling industry a better place.

      View full story
    • By MakeIndieGreatAgain
      Game developers will be able to become pioneers in the development of decentralized games for the gambling industry using DAO.Casino protocol.
      On September 17, 2018, DAO.Casino is opening Sandbox for developers, independent teams and game development studios that choose to harness the power of the rapidly developing DApp industry.
      Starting today everyone may submit their application for Sandbox on the official Sandbox page.
      The Sandbox project is designed by DAO.Casino developers. Participants of Sandbox will learn the basics of decentralized applications development on DAO.Casino protocol. Developers participating in Sandbox will learn to create, design and deploy decentralized games and applications on Ethereum blockchain.
      DAO.Casino is planning to reward most active developers for their constructive feedback on the improvement and optimization of the SDK and related documentation. The company will separately announce the details of the rewards program later this fall.
      “We are confident that the Sandbox project will play an important role in our collaboration with studios and independent game developers. We cannot wait to see our product helping developers unleash their creative and entrepreneurial talents and apply those to one of the most groundbreaking technologies of the XXI century. — states Ilya Tarutov, CEO, DAO.Casino. – I am sure that the products we’re developing will transform the online gambling into a fair and transparent industry for all of the involved parties: casino operators, developers, and affiliate marketers. “
      “We are launching the Sandbox with the goal of enabling as many developers as possible to learn to create decentralized games. We have achieved an important milestone by starting to accept applications from developers all around the world who share our idea to make online gambling fair and transparent. With our technology, developers can take the whole gambling industry to the next level” – says Alexandra Fetisova from DAO.Casino.
      DAO.Casino is disrupting the online gambling industry by developing the protocol based on Ethereum blockchain technology. The protocol ensures the automation of transactions and facilitates interactions between all the industry participants: casino operators, game developers, and affiliate marketers. DAO.Casino team is fully dedicated to developing the best products and making the gambling industry a better place.
    • By trapazza
      Do heavily-loaded compute shaders affect the performance of the other "normal/render" shaders? or do they use a dedicated core?
       
       
    • By Simon Crawford
      Hello, and thanks for taking the time to read my post.
       
      I am not here asking for someone to do my work for me. I am just looking for a mentor who would not mind answering a few of my questions, and give me a little guidance.
       
      I prefer chatting on discord, so if you are interested in helping me get started, please add me. My username is wize1 @8135
    • By JoAndRoPo
      Hi!
      I'm creating a spider solitaire game in my free time and will be adding daily challenges. There will be a challenge each day until the end of the month. After which, the challenges will reset for the next month. 
      I do have some in mind but for a card game, creating unique challenges for each day is kind of tough.
      I played Microsoft's Spider Solitaire's daily challenges and found them to be the same/boring after a while. 
      I would love to hear your ideas (unique) if any. Something different from the daily challenges created by Microsoft Spider Solitaire. 
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!