From 17056eb77eb232f0cb108c7effe747669c65deba Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Thu, 3 Aug 2023 22:29:54 -0700 Subject: [PATCH] Add basic command parsing and handling --- client.go | 4 +- cmd/bgammon-server/account.go | 6 + cmd/bgammon-server/client.go | 14 +++ cmd/bgammon-server/client_tcp.go | 55 +++++++++ cmd/bgammon-server/server.go | 184 +++++++++++++++++++++++++++++-- command.go | 22 ++-- event.go | 12 +- 7 files changed, 260 insertions(+), 37 deletions(-) create mode 100644 cmd/bgammon-server/account.go create mode 100644 cmd/bgammon-server/client.go create mode 100644 cmd/bgammon-server/client_tcp.go diff --git a/client.go b/client.go index e859eab..2a42993 100644 --- a/client.go +++ b/client.go @@ -1,7 +1,5 @@ package bgammon type Client interface { - ReadCommand() (Command, error) - WriteEvent() (Event, error) - Terminate() error + Terminate(reason string) error } diff --git a/cmd/bgammon-server/account.go b/cmd/bgammon-server/account.go new file mode 100644 index 0000000..8247d0b --- /dev/null +++ b/cmd/bgammon-server/account.go @@ -0,0 +1,6 @@ +package main + +type account struct { + id int + username string +} diff --git a/cmd/bgammon-server/client.go b/cmd/bgammon-server/client.go new file mode 100644 index 0000000..3749409 --- /dev/null +++ b/cmd/bgammon-server/client.go @@ -0,0 +1,14 @@ +package main + +import "code.rocket9labs.com/tslocum/bgammon" + +type serverClient struct { + id int + name []byte + account int + connected int64 + lastActive int64 + commands <-chan []byte + events chan<- []byte + bgammon.Client +} diff --git a/cmd/bgammon-server/client_tcp.go b/cmd/bgammon-server/client_tcp.go new file mode 100644 index 0000000..df4b2b6 --- /dev/null +++ b/cmd/bgammon-server/client_tcp.go @@ -0,0 +1,55 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "net" + + "code.rocket9labs.com/tslocum/bgammon" +) + +var _ bgammon.Client = &socketClient{} + +type socketClient struct { + conn net.Conn + events <-chan []byte + commands chan<- []byte +} + +func newSocketClient(conn net.Conn, commands chan<- []byte, events <-chan []byte) *socketClient { + c := &socketClient{ + conn: conn, + events: events, + commands: commands, + } + go c.readCommands() + go c.writeEvents() + return c +} + +func (c *socketClient) readCommands() { + var scanner = bufio.NewScanner(c.conn) + for scanner.Scan() { + log.Printf("READ COMMAND %s", scanner.Text()) + + buf := make([]byte, len(scanner.Bytes())) + copy(buf, scanner.Bytes()) + c.commands <- buf + } +} + +func (c *socketClient) writeEvents() { + var event []byte + for event = range c.events { + c.conn.Write(event) + c.conn.Write([]byte("\n")) + log.Printf("write event %s", event) + } +} + +func (c *socketClient) Terminate(reason string) error { + c.conn.Write([]byte(fmt.Sprintf("Connection closed: %s\n", reason))) + c.conn.Close() + return nil +} diff --git a/cmd/bgammon-server/server.go b/cmd/bgammon-server/server.go index 2ea34d2..ec7153a 100644 --- a/cmd/bgammon-server/server.go +++ b/cmd/bgammon-server/server.go @@ -1,19 +1,17 @@ package main import ( + "bytes" + "fmt" "log" + "math/rand" "net" + "strings" + "time" "code.rocket9labs.com/tslocum/bgammon" ) -type serverClient struct { - id int - connected int64 - lastActive int64 - bgammon.Client -} - type serverGame struct { id int created int64 @@ -23,14 +21,28 @@ type serverGame struct { bgammon.Game } +type serverCommand struct { + client *serverClient + command []byte +} + type server struct { - clients []*serverClient - games []*serverGame - listeners []net.Listener + clients []*serverClient + games []*serverGame + listeners []net.Listener + newClientIDs chan int + commands chan serverCommand } func newServer() *server { - return &server{} + const bufferSize = 10 + s := &server{ + newClientIDs: make(chan int), + commands: make(chan serverCommand, bufferSize), + } + go s.handleNewClientIDs() + go s.handleCommands() + return s } func (s *server) listen(network string, address string) { @@ -55,4 +67,154 @@ func (s *server) handleListener(listener net.Listener) { func (s *server) handleConnection(conn net.Conn) { log.Printf("new conn %+v", conn) + + const bufferSize = 8 + commands := make(chan []byte, bufferSize) + events := make(chan []byte, bufferSize) + + now := time.Now().Unix() + + c := &serverClient{ + id: <-s.newClientIDs, + account: -1, + connected: now, + lastActive: now, + commands: commands, + events: events, + Client: newSocketClient(conn, commands, events), + } + log.Println("socket client", c) + + s.handleClientCommands(c) +} + +func (s *server) handleClientCommands(c *serverClient) { + var command []byte + for command = range c.commands { + s.commands <- serverCommand{ + client: c, + command: command, + } + } +} + +func (s *server) handleNewClientIDs() { + clientID := 1 + for { + s.newClientIDs <- clientID + clientID++ + } +} + +func (s *server) randomUsername() []byte { + // TODO check if username is available + i := 100 + rand.Intn(900) + return []byte(fmt.Sprintf("Guest%d", i)) +} + +func (s *server) sendWelcome(c *serverClient) { + c.events <- []byte(fmt.Sprintf("welcome %s there are %d clients playing %d games", c.name, len(s.clients), len(s.games))) +} + +func (s *server) handleCommands() { + var cmd serverCommand + for cmd = range s.commands { + if cmd.client == nil { + log.Panicf("nil client with command %s", cmd.command) + } + + cmd.command = bytes.TrimSpace(cmd.command) + + firstSpace := bytes.IndexByte(cmd.command, ' ') + var keyword string + var startParameters int + if firstSpace == -1 { + keyword = string(cmd.command) + startParameters = len(cmd.command) + } else { + keyword = string(cmd.command[:firstSpace]) + startParameters = firstSpace + 1 + } + if keyword == "" { + continue + } + keyword = strings.ToLower(keyword) + + log.Printf("server client %+v command %s with keyword %s", cmd.client, cmd.command, keyword) + + params := bytes.Fields(cmd.command[startParameters:]) + log.Printf("params %+v", params) + + // Require users to send login command first. + if cmd.client.account == -1 { + if keyword == bgammon.CommandLogin { + var username []byte + var password []byte + switch len(params) { + case 0: + username = s.randomUsername() + case 1: + username = params[0] + default: + username = params[0] + password = bytes.Join(params[1:], []byte(" ")) + } + + if len(password) > 0 { + cmd.client.account = 1 + } else { + cmd.client.account = 0 + } + cmd.client.name = username + + s.sendWelcome(cmd.client) + + log.Printf("login as %s - %s", username, password) + continue + } else { + cmd.client.Terminate("You must login before using other commands.") + continue + } + } + + switch keyword { + case bgammon.CommandSay: + if len(params) == 0 { + continue + } + case bgammon.CommandList: + case bgammon.CommandCreate: + sendUsage := func() { + cmd.client.events <- []byte("notice To create a public game specify whether it is public or private.") + cmd.client.events <- []byte("notice When creating a private game, a password must be set.") + } + if len(params) == 0 { + sendUsage() + continue + } + var gamePassword []byte + gameType := bytes.ToLower(params[0]) + var gameName []byte + switch { + case bytes.Equal(gameType, []byte("public")): + gameName = bytes.Join(params[1:], []byte(" ")) + case bytes.Equal(gameType, []byte("private")): + if len(params) < 2 { + sendUsage() + continue + } + gamePassword = params[1] + gameName = bytes.Join(params[2:], []byte(" ")) + default: + sendUsage() + continue + } + log.Printf("create game (password %s) name: %s", gamePassword, gameName) + case bgammon.CommandJoin: + case bgammon.CommandRoll: + case bgammon.CommandMove: + default: + log.Printf("unknown command %s", keyword) + } + } } diff --git a/command.go b/command.go index 7de9be9..f4e6da7 100644 --- a/command.go +++ b/command.go @@ -2,16 +2,14 @@ package bgammon // commands are always sent TO the server -type Command struct { - Type int -} +type Command string -type CommandChat struct { - Command - Message string -} - -type CommandMove struct { - Event - Spaces []int // One or more sets of moves from A->B as A,B,A,B,A,B... -} +const ( + CommandLogin = "login" // Log in with username and password, or as a guest. + CommandSay = "say" // Send chat message. + CommandList = "list" // List available games. + CommandCreate = "create" // Create game. + CommandJoin = "join" // Join game. + CommandRoll = "roll" // Roll dice. + CommandMove = "move" // Move checkers. +) diff --git a/event.go b/event.go index 8a2dcf9..fe141df 100644 --- a/event.go +++ b/event.go @@ -3,16 +3,6 @@ package bgammon // events are always received FROM the server type Event struct { - Type int Player int -} - -type EventChat struct { - Event - Message string -} - -type EventMove struct { - Event - Spaces []int // One or more sets of moves from A->B as A,B,A,B,A,B... + Command }