better networking

This commit is contained in:
Beric Bearnson 2024-08-15 20:08:23 -06:00
parent 35d8ebc459
commit e90b9c26b1
6 changed files with 265 additions and 307 deletions

View File

@ -1,95 +1,107 @@
package main package main
import ( import (
"bufio"
"fmt" "fmt"
"io"
"log"
"os" "os"
"sshpong/internal/client"
"sshpong/internal/netwrk" "sshpong/internal/netwrk"
"strings"
"google.golang.org/protobuf/proto"
) )
var exit chan bool
func main() { func main() {
lobbyChan := make(chan netwrk.LobbyPlayerStatus)
interrupter := make(chan netwrk.Interrupter)
messageOutput := make(chan *netwrk.LobbyMessage)
inputChan := make(chan string)
fmt.Println("Welcome to sshpong!") fmt.Println("Welcome to sshpong!")
fmt.Println("Please enter your username") fmt.Println("Please enter your username")
go func() { egress := make(chan *netwrk.LobbyMessage)
scanner := bufio.NewScanner(os.Stdin) ingress := make(chan *netwrk.LobbyMessage)
for scanner.Scan() {
text := scanner.Text()
inputChan <- text
}
}()
reader := bufio.NewReader(os.Stdin)
username, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Error reading from your shit bro...")
}
go netwrk.ConnectToLobby(username, messageOutput, lobbyChan, interrupter)
buf := make([]byte, 1024) buf := make([]byte, 1024)
n, err := os.Stdin.Read(buf)
if err != nil {
log.Panic("Bro your input is no good...")
}
username := string(buf[:n])
for { conn, err := netwrk.ConnectToLobby(username)
select { if err != nil {
case msg := <-interrupter: log.Panic(err)
fmt.Println(msg.Message)
default:
n, err := os.Stdin.Read(buf)
if err != nil {
fmt.Println("Error reading from stdin")
return
}
input := string(buf[:n])
args := strings.Fields(input)
switch args[0] {
case "invite":
if args[1] != "" {
messageOutput <- &netwrk.LobbyMessage{
PlayerId: username,
Type: "invite",
Content: args[1],
}
} else {
fmt.Println("Please provide a player to invite ")
}
case "chat":
if args[1] != "" {
messageOutput <- &netwrk.LobbyMessage{
PlayerId: username,
Type: "chat",
Content: strings.Join(args[1:], " "),
}
}
case "/":
if args[1] != "" {
messageOutput <- &netwrk.LobbyMessage{
PlayerId: username,
Type: "chat",
Content: strings.Join(args[1:], " "),
}
}
case "quit":
return
case "q":
return
case "help":
fmt.Println("use invite <player name> to invite a player\nchat or / to send a message to the lobby\nq or quit to leave the game")
case "h":
fmt.Println("use invite <player name> to invite a player\nchat or / to send a message to the lobby\nq or quit to leave the game")
default:
fmt.Println("use invite <player name> to invite a player\nchat or / to send a message to the lobby\nq or quit to leave the game")
}
}
} }
// User input handler
go func(egress chan *netwrk.LobbyMessage) {
buf := make([]byte, 1024)
for {
n, err := os.Stdin.Read(buf)
if err != nil {
log.Panic("Bro your input wack as fuck")
}
userMessage, err := client.HandleUserInput(buf[:n])
if err != nil {
fmt.Println(err)
continue
}
userMessage.PlayerId = username
egress <- userMessage
}
}(egress)
// Ingress Handler
go func(oc chan *netwrk.LobbyMessage) {
for {
msg := <-ingress
client.HandleServerMessage(msg)
}
}(ingress)
// Network writer
go func(userMessages chan *netwrk.LobbyMessage) {
for {
msg := <-userMessages
bytes, err := proto.Marshal(msg)
if err != nil {
log.Panic("Malformed proto message", err)
}
_, err = conn.Write(bytes)
if err == io.EOF {
log.Panic("Server disconnected sorry...")
} else if err != nil {
log.Panic("Error reading from server connection...")
}
}
}(egress)
// Network reader
go func(serverMessages chan *netwrk.LobbyMessage) {
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err == io.EOF {
log.Panic("Server disconnected sorry...")
} else if err != nil {
log.Panic("Error reading from server connection...")
}
message := &netwrk.LobbyMessage{}
err = proto.Unmarshal(buf[:n], message)
if err != nil {
log.Panic("Error reading message from server")
}
serverMessages <- message
}
}(ingress)
_ = <-exit
} }

View File

@ -5,9 +5,12 @@ import (
"sshpong/internal/netwrk" "sshpong/internal/netwrk"
) )
var exit chan bool
func main() { func main() {
fmt.Println("Starting sshpong server!") fmt.Println("Starting sshpong server!")
netwrk.Listen() netwrk.Listen()
_ = <-exit
} }

View File

@ -0,0 +1,71 @@
package client
import (
"fmt"
"io"
"log"
"sshpong/internal/netwrk"
"strings"
)
func HandleUserInput(buf []byte) (*netwrk.LobbyMessage, error) {
input := string(buf)
args := strings.Fields(input)
switch args[0] {
case "invite":
if args[1] != "" {
return &netwrk.LobbyMessage{
Type: "invite",
Content: args[1],
}, nil
} else {
fmt.Println("Please provide a player to invite ")
}
case "chat":
if args[1] != "" {
return &netwrk.LobbyMessage{
Type: "chat",
Content: strings.Join(args[1:], " "),
}, nil
}
case "/":
if args[1] != "" {
return &netwrk.LobbyMessage{
Type: "chat",
Content: strings.Join(args[1:], " "),
}, nil
}
case "quit":
return nil, io.EOF
case "q":
return nil, io.EOF
case "help":
return nil, fmt.Errorf("use invite <player name> to invite a player\nchat or / to send a message to the lobby\nq or quit to leave the game")
case "h":
return nil, fmt.Errorf("use invite <player name> to invite a player\nchat or / to send a message to the lobby\nq or quit to leave the game")
default:
return nil, fmt.Errorf("use invite <player name> to invite a player\nchat or / to send a message to the lobby\nq or quit to leave the game")
}
return nil, nil
}
func HandleServerMessage(message *netwrk.LobbyMessage) {
switch message.Type {
case "invite":
log.Println(message.PlayerId, "is inviting you to a game.", message.Content)
case "accepted":
log.Println(message.PlayerId, "accepted your invite.", message.Content)
case "text":
log.Println(message.PlayerId, ":", message.Content)
case "decline_game":
log.Println("Invite was declined:", message.Content)
case "disconnect":
log.Println("Got disconnect for player:", message.Content)
case "connect":
log.Println("Got connect for player:", message.Content)
case "pong":
log.Println("Received", message.Content)
default:
log.Println("Received", message.Content)
}
}

View File

@ -2,152 +2,26 @@ package netwrk
import ( import (
"fmt" "fmt"
"log"
"net" "net"
"strings"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
type LobbyPlayerStatus struct { func ConnectToLobby(username string) (net.Conn, error) {
Username string
Status string
}
type Interrupter struct {
MessageType string
Message string
ReplyChan chan string
}
var username string
var lobby chan LobbyPlayerStatus
var interruptChan chan Interrupter
func ConnectToLobby(playerUsername string, messageOutputChan chan *LobbyMessage, lobbyMessageChan chan LobbyPlayerStatus, interruptChannel chan Interrupter) {
username = playerUsername
lobby = lobbyMessageChan
interruptChan = interruptChannel
conn, err := net.Dial("tcp", "127.0.0.1:12345") conn, err := net.Dial("tcp", "127.0.0.1:12345")
if err != nil { if err != nil {
fmt.Println("Sorry, failed to connect to server...") return nil, fmt.Errorf("Sorry, failed to connect to server...")
return
} }
loginMsg, err := proto.Marshal(&LobbyMessage{Type: "name", Content: username}) loginMsg, err := proto.Marshal(&LobbyMessage{Type: "name", Content: username})
if err != nil { if err != nil {
fmt.Println("Sorry bro but your username is wack AF...") return nil, fmt.Errorf("Sorry bro but your username is wack AF...")
} }
_, err = conn.Write(loginMsg) _, err = conn.Write(loginMsg)
if err != nil { if err != nil {
fmt.Println("Sorry, could not communicate with server...") return nil, fmt.Errorf("Sorry, could not communicate with server...")
} }
fmt.Println("Starting client loop") return conn, nil
messageInputChan := make(chan *LobbyMessage)
go func() {
for {
messageBytes := make([]byte, 1024)
n, err := conn.Read(messageBytes)
if err != nil {
fmt.Println("Sorry, failed to read message from server...", err)
}
message := &LobbyMessage{}
err = proto.Unmarshal(messageBytes[:n], message)
if err != nil {
fmt.Println("Sorry, the server sent something weird back...")
}
messageInputChan <- message
}
}()
for {
select {
case msg := <-messageInputChan:
if isDone, gameID := handleLobbyMessage(conn, msg); isDone {
if gameID != "" {
interruptChan <- Interrupter{
MessageType: "game",
Message: gameID,
ReplyChan: make(chan string),
}
return
} else {
return
}
}
case msg := <-messageOutputChan:
fmt.Println("Sending message out", msg)
err := SendMessageToServer(conn, msg)
if err != nil {
fmt.Println("Error!", err)
}
}
}
}
func handleLobbyMessage(serverConn net.Conn, message *LobbyMessage) (bool, string) {
switch message.Type {
case "text":
fmt.Println(message.Content)
return false, ""
case "error":
fmt.Println("Error:", message.Content)
return false, ""
case "invite":
fmt.Println("GOT INVITE!")
replyChan := make(chan string)
interruptChan <- Interrupter{
MessageType: "invite",
Message: fmt.Sprintf("Invite from player %s\nAccept: Y Decline: N", message.Content),
ReplyChan: replyChan,
}
input := <-replyChan
if strings.ToLower(input) == "yes" || strings.ToLower(input) == "y" {
SendMessageToServer(serverConn, &LobbyMessage{Type: "accept_game", Content: username})
SendMessageToServer(serverConn, &LobbyMessage{Type: "quit", Content: username})
return true, message.Content
} else {
SendMessageToServer(serverConn, &LobbyMessage{Type: "decline_game", Content: username})
}
return false, ""
case "accept":
fmt.Println(message.Content, "accepted your invite. Game starting...")
SendMessageToServer(serverConn, &LobbyMessage{Type: "quit", Content: username})
return true, message.Content
case "decline_game":
fmt.Println("Sorry,", message.Content, "declined your game invite...")
return false, ""
case "connect":
lobby <- LobbyPlayerStatus{Username: message.Content, Status: "connected"}
fmt.Println(message.Content, "connected!")
return false, ""
case "disconnect":
lobby <- LobbyPlayerStatus{Username: message.Content, Status: "disconnected"}
fmt.Println(message.Content, "disconnected!")
return false, ""
case "pong":
log.Println("PoNg!")
return false, ""
default:
log.Println("Got message", message)
}
return false, ""
}
func SendMessageToServer(connection net.Conn, message *LobbyMessage) error {
bytes, err := proto.Marshal(message)
if err != nil {
return fmt.Errorf("Error marshalling message. Your protobuf is wack yo.")
}
_, err = connection.Write(bytes)
if err != nil {
return fmt.Errorf("Error writing to client connection")
}
return nil
} }

View File

@ -1,7 +1,6 @@
package netwrk package netwrk
import ( import (
"fmt"
"io" "io"
"log" "log"
"net" "net"
@ -14,15 +13,16 @@ func handleLobbyConnection(conn net.Conn) {
messageBytes := make([]byte, 4096) messageBytes := make([]byte, 4096)
recvMessageChan := make(chan *LobbyMessage) ingress := make(chan *LobbyMessage)
egress := make(chan *LobbyMessage)
// Network Reader
go func() { go func() {
for { for {
fmt.Println("READING!")
n, err := conn.Read(messageBytes) n, err := conn.Read(messageBytes)
if err == io.EOF { if err == io.EOF {
return return
} }
fmt.Println("READ something!")
if err != nil { if err != nil {
log.Printf("Error reading message %v", err) log.Printf("Error reading message %v", err)
return return
@ -32,106 +32,103 @@ func handleLobbyConnection(conn net.Conn) {
err = proto.Unmarshal(messageBytes[:n], &message) err = proto.Unmarshal(messageBytes[:n], &message)
if err != nil { if err != nil {
log.Println("Invalid message received from client") log.Println("Invalid message received from client", err)
} }
recvMessageChan <- &message ingress <- &message
} }
}() }()
for { // Network Writer
go func() {
select { for {
case msg := <-recvMessageChan: msg := <-egress
if isDone, err := handleClientLobbyMessage(conn, msg); err != nil || isDone { bytes, err := proto.Marshal(msg)
log.Println(err) if err != nil {
return log.Println("Error marshalling message to send to user...", err)
}
_, err = conn.Write(bytes)
if err == io.EOF {
log.Println("User has disconnected", err)
ingress <- &LobbyMessage{Type: "disconnect"}
}
if err != nil {
log.Println("Error writing to user...", err)
} }
fmt.Println("Handled message")
} }
} }()
// Client message handler
go func() {
for {
msg := <-ingress
serverMsg, err := handleClientLobbyMessage(msg)
if err != nil {
log.Println("Error handling client lobby message...", err)
}
if serverMsg != nil {
egress <- serverMsg
}
}
}()
} }
// Returns a bool of whether the player has disconnected from the lobby and an error // Returns a bool of whether the player has disconnected from the lobby and an error
func handleClientLobbyMessage(playerConnection net.Conn, message *LobbyMessage) (bool, error) { func handleClientLobbyMessage(message *LobbyMessage) (*LobbyMessage, error) {
switch message.Type { switch message.Type {
case "name": case "name":
_, ok := clientPool.clients[message.Content] _, ok := lobbyMembers.Load(message.Content)
if ok { if ok {
SendMessageToClient(playerConnection, &LobbyMessage{Type: "error", Content: "Sorry, that name is already taken"}) return &LobbyMessage{Type: "name_error", Content: "Sorry, that name is already taken, please try a different name"}, nil
return false, nil
}
playerID := message.Content
clientPool.clients[playerID] = Client{
name: playerID,
conn: playerConnection,
ready: false,
}
for _, player := range clientPool.clients {
err := SendMessageToClient(playerConnection, &LobbyMessage{PlayerId: player.name, Type: "connect", Content: player.name})
if err != nil {
log.Println("There was an error sending the list of lobby players to client", message.Content)
}
} }
username := message.Content
// Send all client messages
lobbyMembers.Range(func(lobbyUsername string, client Client) bool {
externalMessageChan <- ExternalMessage{Target: username, Message: &LobbyMessage{Type: "connect", Content: lobbyUsername}}
return true
})
log.Println("Broadcasting new player", message.Content) log.Println("Broadcasting new player", message.Content)
broadcastToLobby(&LobbyMessage{PlayerId: "", Type: "connect", Content: playerID}) broadcastToLobby(&LobbyMessage{PlayerId: "", Type: "connect", Content: username})
return false, SendMessageToClient(playerConnection, &LobbyMessage{PlayerId: playerID, Type: "name", Content: playerID}) return &LobbyMessage{PlayerId: username, Type: "name", Content: username}, nil
case "invite": case "invite":
log.Println("Got invite for player:", message.Content) log.Println("Got invite for player:", message.Content)
invitee, ok := clientPool.clients[message.Content] invitee, ok := lobbyMembers[message.Content]
if !ok { if !ok {
SendMessageToClient(playerConnection, &LobbyMessage{Type: "text", Content: "Sorry, that player is not available..."}) return &LobbyMessage{Type: "text", Content: "Sorry, that player is not available..."}, nil
return false, nil
} }
SendMessageToClient(invitee.conn, &LobbyMessage{Type: "invite", Content: message.PlayerId}) return &LobbyMessage{Type: "invite", Content: message.PlayerId}, nil
return false, nil
case "accept_game": case "accept_game":
player := clientPool.clients[message.Content] player := lobbyMembers[message.Content]
if err := SendMessageToClient(player.conn, &LobbyMessage{Type: "accept", Content: ""}); err != nil { return &LobbyMessage{Type: "accept", Content: ""}, nil
SendMessageToClient(playerConnection, &LobbyMessage{Type: "error", Content: "Sorry that game is no longer available..."})
return false, nil
}
return true, nil
case "chat": case "chat":
broadcastToLobby(&LobbyMessage{PlayerId: message.PlayerId, Type: "text", Content: message.Content}) broadcastToLobby(&LobbyMessage{PlayerId: message.PlayerId, Type: "text", Content: message.Content})
return false, nil return nil, nil
case "decline_game": case "decline_game":
inviter := clientPool.clients[message.Content] inviter := lobbyMembers[message.Content]
SendMessageToClient(inviter.conn, &LobbyMessage{Type: "decline_game", Content: message.PlayerId}) return &LobbyMessage{Type: "decline_game", Content: message.PlayerId}, nil
return false, nil
case "quit": case "quit":
delete(clientPool.clients, message.PlayerId) delete(lobbyMembers, message.PlayerId)
broadcastToLobby(&LobbyMessage{Type: "disconnect", Content: message.PlayerId}) broadcastToLobby(&LobbyMessage{Type: "disconnect", Content: message.PlayerId})
return true, nil return nil, nil
case "ping": case "ping":
SendMessageToClient(playerConnection, &LobbyMessage{Type: "pong", Content: "pong"}) return &LobbyMessage{Type: "pong", Content: "pong"}, nil
return false, nil
default: default:
SendMessageToClient(playerConnection, &LobbyMessage{Type: "pong", Content: "pong"}) return &LobbyMessage{Type: "pong", Content: "pong"}, nil
return false, nil
} }
} }
func SendMessageToClient(connection net.Conn, message *LobbyMessage) error {
bytes, err := proto.Marshal(message)
if err != nil {
return fmt.Errorf("Error marshalling message. Your protobuf is wack yo.")
}
_, err = connection.Write(bytes)
if err != nil {
return fmt.Errorf("Error writing to client connection")
}
fmt.Println("Sent message to client")
return nil
}
func broadcastToLobby(message *LobbyMessage) { func broadcastToLobby(message *LobbyMessage) {
for _, player := range clientPool.clients { for _, player := range lobbyMembers {
err := SendMessageToClient(player.conn, message) bytes, err := proto.Marshal(message)
if err != nil {
log.Println("Error marshalling broadcast message", err)
}
_, err = player.Conn.Write(bytes)
if err != nil { if err != nil {
log.Println("Error broadcasting to clients...", err) log.Println("Error broadcasting to clients...", err)
} }

View File

@ -3,36 +3,40 @@ package netwrk
import ( import (
"log" "log"
"net" "net"
sync "sync"
) )
type Client struct { type Client struct {
name string Username string
conn net.Conn Conn net.Conn
ready bool
} }
type ClientPool struct { type LobbyPlayersMessage struct {
clients map[string]Client Type string
Username string
IsAvailable chan bool
} }
type GameClients struct { type ExternalMessage struct {
client1 chan GameMessage Target string
client2 chan GameMessage Message *LobbyMessage
} }
type GameChans struct { var lobbyListener chan LobbyPlayersMessage
games map[string]GameClients var externalMessageChan chan ExternalMessage
}
var clientPool *ClientPool var lobbyMembers sync.Map
var gameChans *GameChans
func init() {
lobbyListener = make(chan LobbyPlayersMessage)
externalMessageChan = make(chan ExternalMessage)
lobbyMembers = sync.Map{}
}
// Starts listening on port 12345 for TCP connections // Starts listening on port 12345 for TCP connections
// Also creates client pool and game connection singletons // Also creates client pool and game connection singletons
func Listen() { func LobbyListen() {
clientPool = &ClientPool{
clients: map[string]Client{},
}
listener, err := net.Listen("tcp", "127.0.0.1:12345") listener, err := net.Listen("tcp", "127.0.0.1:12345")
if err != nil { if err != nil {
@ -41,21 +45,18 @@ func Listen() {
defer listener.Close() defer listener.Close()
go func() { for {
for { conn, err := listener.Accept()
conn, err := listener.Accept() log.Println("got a connection!")
log.Println("got a connection!") if err != nil {
if err != nil { log.Println(err)
log.Println(err) continue
continue
}
go handleLobbyConnection(conn)
} }
}() go handleLobbyConnection(conn)
gameChans = &GameChans{
games: map[string]GameClients{},
} }
}
func GamesListen() {
gameListener, err := net.Listen("tcp", "127.0.0.1:42069") gameListener, err := net.Listen("tcp", "127.0.0.1:42069")
if err != nil { if err != nil {