networking protocol refactor

This commit is contained in:
2024-09-28 23:26:06 -06:00
parent a97bd301d5
commit 15e7f20f1a
5 changed files with 397 additions and 480 deletions
+211 -144
View File
@@ -2,7 +2,6 @@ package lobby
import (
"fmt"
"io"
"log"
"log/slog"
"net"
@@ -24,7 +23,7 @@ type Client struct {
type ExternalMessage struct {
From string
Target string
Message LobbyMessage
Message []byte
}
func CreateLobby() *Lobby {
@@ -52,13 +51,7 @@ func CreateLobby() *Lobby {
slog.Debug("Item that was not a client found in the lobby map...", slog.Any("key", msg.From))
}
go func() {
em := LobbyMessage{
MessageType: "error",
Message: Error{
Message: fmt.Sprintf("Sorry, player %s is not available...", msg.Target),
},
}
b, err := Marshal(em)
b, err := Marshal(ErrorData{Message: fmt.Sprintf("Sorry player %s is not available...", msg.Target)}, Error)
if err != nil {
slog.Debug("Could not marshall error message for missing player", slog.Any("error", err))
}
@@ -68,16 +61,12 @@ func CreateLobby() *Lobby {
}
c, ok := tc.(Client)
if !ok {
slog.Debug("Item that was not a client found in the lobby map...", slog.Any("key", msg.From))
lm.Delete(msg.Target)
continue
}
go func() {
b, err := Marshal(msg.Message)
if err != nil {
slog.Debug("Could not marshal external message...", slog.Any("error", err))
}
c.Conn.Write(b)
c.Conn.Write(msg.Message)
}()
}
@@ -86,33 +75,33 @@ func CreateLobby() *Lobby {
return &l
}
func (l *Lobby) HandleLobbyConnection(conn net.Conn) {
func (l *Lobby) HandleLobbyConnection(client Client) {
messageBytes := make([]byte, 4096)
ingress := make(chan LobbyMessage)
egress := make(chan LobbyMessage)
ingress := make(chan []byte)
egress := make(chan []byte)
// Network Reader
go func() {
for {
n, err := conn.Read(messageBytes)
if err == io.EOF {
conn.Close()
return
}
n, err := client.Conn.Read(messageBytes)
if err != nil {
conn.Close()
client.Conn.Close()
log.Printf("Error reading message %v", err)
l.lobbyMembers.Delete(client.Username)
// Server receives a disconnect message of the user
msg, err := Marshal(DisconnectData{
From: client.Username,
}, Disconnect)
if err != nil {
slog.Error("error marshalling responsive disconnect of EOF error", "error", err)
} else {
ingress <- msg
}
return
}
message := LobbyMessage{}
message, err = Unmarshal(messageBytes[:n])
if err != nil {
log.Println("Invalid message received from client", err)
}
ingress <- message
ingress <- messageBytes[:n]
}
}()
@@ -120,21 +109,21 @@ func (l *Lobby) HandleLobbyConnection(conn net.Conn) {
go func() {
for {
msg := <-egress
bytes, err := Marshal(msg)
_, err := client.Conn.Write(msg)
if err != nil {
log.Println("Error marshalling message to send to user...", err)
}
_, err = conn.Write(bytes)
if err == io.EOF {
conn.Close()
log.Println("User has disconnected", err)
client.Conn.Close()
// TODO: write message for disconnect to everyone?
slog.Debug("Sending bad disconnect message")
ingress <- LobbyMessage{MessageType: "disconnect", Message: Disconnect{}}
}
if err != nil {
log.Println("Error writing to user...", err)
l.lobbyMembers.Delete(client.Username)
// Server receives a disconnect message of the user
msg, err := Marshal(DisconnectData{
From: client.Username,
}, Disconnect)
if err != nil {
slog.Error("error marshalling responsive disconnect of EOF error", "error", err)
} else {
ingress <- msg
}
}
}
}()
@@ -143,139 +132,158 @@ func (l *Lobby) HandleLobbyConnection(conn net.Conn) {
go func() {
for {
msg := <-ingress
serverMsg, err := l.handleClientLobbyMessage(&msg, conn)
slog.Debug("Received an ingress message", "message", msg)
resMsg, err := l.handleClientLobbyMessage(msg)
if err != nil {
log.Println("Error handling client lobby message...", err)
resMsg, err = Marshal(ErrorData{
Message: err.Error(),
}, Error)
}
if serverMsg.MessageType != "" {
egress <- serverMsg
if len(resMsg) > 0 {
egress <- resMsg
}
}
}()
}
// Returns a bool of whether the player has disconnected from the lobby and an error
func (l *Lobby) handleClientLobbyMessage(message *LobbyMessage, conn net.Conn) (LobbyMessage, error) {
switch message.MessageType {
// Handle an name/login message from a player
// Store the new player in the l.lobbyMembers
// Send a connection message for each of the l.lobbyMembers to the new player
// Send a connection message to all members in the lobby
case "name":
_, ok := l.lobbyMembers.Load(message.Message)
if ok {
return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry, that name is already taken, please try a different name"}}, nil
func (l *Lobby) handleClientLobbyMessage(msg []byte) ([]byte, error) {
header := msg[0]
switch header {
case Chat:
l.BroadcastToLobby(msg)
return []byte{}, nil
case Invite:
i, err := Unmarshal[InviteData](msg)
if err != nil {
slog.Debug("error unmarshalling invite message", "error", err)
return []byte{}, err
}
nm, ok := message.Message.(Name)
if !ok {
return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry the message value and type were not matching for name"}}, nil
msg, err := Marshal(InviteData{
From: i.From,
To: i.To,
}, Invite)
if err != nil {
slog.Error("error marshalling invite data...", "error", err)
return []byte{}, err
}
l.lobbyMembers.Store(nm.Name, Client{Username: nm.Name, Conn: conn})
// Build current lobby list
var lobby []string
l.lobbyMembers.Range(func(lobbyUsername any, client any) bool {
usernameString, _ := lobbyUsername.(string)
lobby = append(lobby, usernameString)
return true
})
l.broadcastToLobby(LobbyMessage{MessageType: "connect", Message: Name{Name: nm.Name}})
return LobbyMessage{MessageType: "name", Message: Name{
Name: nm.Name,
},
}, nil
// Handle an invite message by sending a message to the target player
// Send an invite message to the invitee: message.Content
// Send an ack message to the inviter: message.PlayerId
case "invite":
i, ok := message.Message.(Invite)
if !ok {
return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry the message value and type were not matching for invite"}}, nil
}
// TODO: figure out this shit
l.ExternalMessageChannel <- ExternalMessage{
From: i.From,
Target: i.To,
Message: LobbyMessage{},
Message: msg,
}
return LobbyMessage{MessageType: "pending_invite", Message: PendingInvite{
return Marshal(PendingInviteData{
Recipient: i.To,
}}, nil
}, PendingInvite)
// Handle a accept message from a player that was invited
// Send a game_start message back to the player: message.Content
// Send an accepted message back to the inviter: message.PlayerId
case "accept":
gameID := uuid.NewString()
// TODO: is pending invite really something that we need?
// case PendingInvite:
// pi, err := Unmarshal[PendingInviteData](msg)
// if err != nil {
// slog.Debug("error unmarshalling pending invite message", err)
// return
// }
am, ok := message.Message.(Accept)
if !ok {
return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry the message value and type were not matching for accept"}}, nil
case Accept:
a, err := Unmarshal[AcceptData](msg)
if err != nil {
slog.Debug("error unmarshalling accept message", "error", err)
return []byte{}, err
}
slog.Debug("incoming accept message", slog.Any("From", am.From), slog.Any("To", am.To))
gID := uuid.NewString()
msg, err := Marshal(AcceptedData{
Accepter: a.From,
GameID: gID,
}, Accepted)
l.ExternalMessageChannel <- ExternalMessage{
Target: am.To,
Message: LobbyMessage{MessageType: "accepted", Message: Accepted{
Accepter: am.From,
GameID: gameID,
},
}}
return LobbyMessage{MessageType: "start_game", Message: StartGame{To: am.From, GameID: gameID}}, nil
// Handle a chat message from a player with PlayerId
case "chat":
c, ok := message.Message.(Chat)
if !ok {
return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry the message value and type were not matching for chat"}}, nil
From: a.From,
Target: a.To,
Message: msg,
}
l.broadcastToLobby(LobbyMessage{MessageType: "text", Message: Chat{
From: c.From,
Message: c.Message,
}})
return LobbyMessage{}, nil
// Handle a quit message from a player that was connected
// broadcast the player quit to the lobby
case "quit":
q, ok := message.Message.(Disconnect)
if !ok {
return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry the message value and type were not matching for quit"}}, nil
return Marshal(StartGameData{
To: a.From,
GameID: gID,
}, StartGame)
case Accepted:
a, err := Unmarshal[AcceptedData](msg)
if err != nil {
slog.Debug("error unmarshalling accpeted message", "error", err)
return []byte{}, err
}
l.lobbyMembers.Delete(q.From)
l.broadcastToLobby(LobbyMessage{MessageType: "disconnect", Message: Disconnect{
From: q.From,
}})
return LobbyMessage{}, nil
// Ping and pong
case "ping":
return LobbyMessage{MessageType: "pong", Message: "pong"}, nil
// TODO: figure out the accepted and start game data situation... To field is a little hard to fill.
return Marshal(StartGameData{
To: "",
GameID: a.GameID,
}, StartGame)
// Ping and pong
default:
return LobbyMessage{MessageType: "pong", Message: "pong"}, nil
// TODO: Like pending invite, I think start game is only a client message
// case StartGame:
// sg, err := Unmarshal[StartGameData](msg)
// if err != nil {
// slog.Debug("error unmarshalling start game message", err)
// return []byte{}, err
// }
// TODO: Do we even want to support decline responses?
// case Decline:
// d, err := Unmarshal[DeclineData](msg)
// if err != nil {
// slog.Debug("error unmarshalling decline message", err)
// return []byte{}, err
// }
case Disconnect:
d, err := Unmarshal[DisconnectData](msg)
if err != nil {
slog.Debug("error unmarshalling disconnect message", "error", err)
return []byte{}, err
}
l.lobbyMembers.Delete(d.From)
msg, err := Marshal(DisconnectData{
From: d.From,
}, Disconnect)
l.BroadcastToLobby(msg)
// TODO: how do we handle a disconnect for the client's side
return []byte{}, nil
// TODO: This is just a client side message right...?
// case Connect:
// c, err := Unmarshal[ConnectData](msg)
// if err != nil {
// slog.Debug("error unmarshalling connect message", err)
// return
// }
// TODO: This is just a client side message right...?
// case Error:
// e, err := Unmarshal[ErrorData](msg)
// if err != nil {
// slog.Debug("error unmarshalling error message", err)
// return []byte{}, err
// }
}
return []byte{}, nil
}
func (l *Lobby) broadcastToLobby(message LobbyMessage) {
func (l *Lobby) BroadcastToLobby(bytes []byte) {
var disconnectedUsers []string
l.lobbyMembers.Range(func(playerId, player interface{}) bool {
bytes, err := Marshal(message)
if err != nil {
log.Println("Error marshalling broadcast message", err)
}
client := player.(Client)
_, err = client.Conn.Write(bytes)
_, err := client.Conn.Write(bytes)
if err != nil {
log.Println("Error broadcasting to clients...", err)
disconnectedUsers = append(disconnectedUsers, playerId.(string))
@@ -288,3 +296,62 @@ func (l *Lobby) broadcastToLobby(message LobbyMessage) {
l.lobbyMembers.Delete(player)
}
}
func (l *Lobby) InitialConnectionHandler(conn net.Conn) (Client, []byte) {
msg := make([]byte, 256)
nb, err := conn.Read(msg)
if err != nil {
slog.Debug("error reading from initial connection")
}
msg = msg[:nb]
n, err := Unmarshal[NameData](msg)
if err != nil {
slog.Debug("error unmarshalling name message:", "error", err.Error(), "message", msg[1:nb])
msgOut, err := Marshal(ErrorData{
Message: "incorrectly formatted username in name message",
}, Error)
if err != nil {
slog.Error("error marshalling error message for incorrectly formatted username")
}
return Client{}, msgOut
}
_, ok := l.lobbyMembers.Load(n.Name)
if ok {
msg, err := Marshal(ErrorData{
Message: "Sorry that name is already taken, please try a different name",
}, Error)
if err != nil {
slog.Error("error marshalling error on name already taken msg")
}
return Client{}, msg
}
h, err := Marshal(ConnectData{
From: n.Name,
}, Connect)
if err != nil {
slog.Debug("error marshalling broadcast connect message on player connect", "error", err)
return Client{Username: n.Name, Conn: conn}, h
}
l.BroadcastToLobby(h)
// Build current lobby list
var lobby []string
l.lobbyMembers.Range(func(lobbyUsername any, client any) bool {
usernameString, _ := lobbyUsername.(string)
lobby = append(lobby, usernameString)
return true
})
msgOut, err := Marshal(CurrentlyConnectedData{Players: lobby}, CurrentlyConnected)
if err != nil {
slog.Debug("Error marshalling currectly connected data on player connect")
}
client := Client{
Username: n.Name,
Conn: conn,
}
l.lobbyMembers.Store(n.Name, client)
return client, msgOut
}
+41 -191
View File
@@ -1,244 +1,94 @@
package lobby
import (
"encoding/base64"
"encoding/json"
"errors"
"log/slog"
"reflect"
"strings"
import "encoding/json"
const (
Name = iota
Chat
Invite
PendingInvite
Accept
Accepted
StartGame
Decline
Disconnect
Connect
CurrentlyConnected
Error
)
type LobbyMessage struct {
MessageType string `json:"message_type"`
Message any `json:"message"`
}
type Name struct {
type NameData struct {
Name string `json:"name"`
}
type Chat struct {
type ChatData struct {
From string `json:"from"`
Message string `json:"message"`
}
type Invite struct {
type InviteData struct {
From string `json:"from"`
To string `json:"to"`
}
type PendingInvite struct {
type PendingInviteData struct {
Recipient string `json:"recipient"`
}
type Accept struct {
type AcceptData struct {
From string `json:"from"`
To string `json:"to"`
}
type Accepted struct {
type AcceptedData struct {
Accepter string `json:"accepter"`
GameID string `json:"game_id"`
}
type StartGame struct {
type StartGameData struct {
To string `json:"to"`
GameID string `json:"game_id"`
}
type Decline struct {
type DeclineData struct {
From string `json:"from"`
To string `json:"to"`
}
type Disconnect struct {
type DisconnectData struct {
From string `json:"from"`
}
type Connect struct {
type ConnectData struct {
From string `json:"from"`
}
type Error struct {
type CurrentlyConnectedData struct {
Players []string `json:"players"`
}
type ErrorData struct {
Message string `json:"message"`
}
func Marshal(a LobbyMessage) ([]byte, error) {
slog.Debug("Marshalling message", slog.Any("message type", a.MessageType))
bm, err := json.Marshal(a.Message)
func Unmarshal[T NameData | ChatData | InviteData | PendingInviteData | AcceptData | AcceptedData | StartGameData | DeclineData | DisconnectData | ConnectData | CurrentlyConnectedData | ErrorData](msg []byte) (T, error) {
var d T
err := json.Unmarshal(msg[1:], &d)
if err != nil {
return nil, err
return d, err
}
a.Message = bm
return json.Marshal(a)
return d, nil
}
// Use this to get the appropriate message type into the message field then assert the
// right struct accordingly to safely access the fields you need.
func Unmarshal(b []byte) (LobbyMessage, error) {
lm := LobbyMessage{}
err := json.Unmarshal(b, &lm)
func Marshal[T NameData | ChatData | InviteData | PendingInviteData | AcceptData | AcceptedData | StartGameData | DeclineData | DisconnectData | ConnectData | CurrentlyConnectedData | ErrorData](msg T, header int) ([]byte, error) {
mb, err := json.Marshal(msg)
if err != nil {
return lm, err
return mb, err
}
smsg, ok := lm.Message.(string)
if !ok {
slog.Debug("error asserting message to string")
}
slog.Debug("type of message", slog.Any("type of message", reflect.TypeOf(smsg)), slog.String("message", smsg))
b := []byte{byte(header)}
jsonBytes, err := base64.StdEncoding.DecodeString(smsg)
lm.Message = jsonBytes
b = append(b, mb...)
switch strings.ToLower(lm.MessageType) {
case "name":
n := Name{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &n)
if err != nil {
slog.Debug("Error", slog.Any("error", err))
return lm, err
}
lm.Message = n
return lm, nil
case "chat":
c := Chat{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &c)
if err != nil {
slog.Debug("chat", slog.Any("error", err))
return lm, err
}
lm.Message = c
return lm, nil
case "invite":
i := Invite{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &i)
if err != nil {
slog.Debug("invite", slog.Any("error", err))
return lm, err
}
lm.Message = i
return lm, nil
case "pending_invite":
pi := PendingInvite{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &pi)
if err != nil {
slog.Debug("pending_invite", slog.Any("error", err))
return lm, err
}
lm.Message = pi
case "accept":
a := Accept{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &a)
if err != nil {
slog.Debug("accept", slog.Any("error", err))
return lm, err
}
lm.Message = a
return lm, nil
case "accepted":
a := Accepted{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &a)
if err != nil {
slog.Debug("accepted", slog.Any("error", err))
return lm, err
}
lm.Message = a
return lm, nil
case "start_game":
sg := StartGame{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &sg)
if err != nil {
slog.Debug("start_game", slog.Any("error", err))
return lm, err
}
lm.Message = sg
return lm, nil
case "decline":
d := Decline{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &d)
if err != nil {
slog.Debug("decline", slog.Any("error", err))
return lm, err
}
lm.Message = d
return lm, nil
case "disconnect":
di := Disconnect{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &di)
if err != nil {
slog.Debug("disconnect", slog.Any("error", err))
return lm, err
}
lm.Message = di
return lm, nil
case "connect":
co := Connect{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &co)
if err != nil {
slog.Debug("connect", slog.Any("error", err))
return lm, err
}
lm.Message = co
return lm, nil
case "error":
e := Error{}
bs, ok := lm.Message.([]byte)
if !ok {
return lm, err
}
err := json.Unmarshal(bs, &e)
if err != nil {
slog.Debug("error", slog.Any("error", err))
return lm, err
}
lm.Message = e
return lm, nil
default:
return lm, errors.New("unknown message type")
}
return lm, errors.New("unknown message type")
return b, nil
}