Skip to content

Commit

Permalink
feat: connection engine
Browse files Browse the repository at this point in the history
feat: connection engine
  • Loading branch information
burntcarrot authored Oct 22, 2022
2 parents bb0d23c + 5f26bc3 commit 8a1c883
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 1 deletion.
15 changes: 15 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Lint
on: [push, pull_request]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.18
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.48
14 changes: 14 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
on: [push, pull_request]
name: Tests
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install Go.
uses: actions/setup-go@v2
with:
go-version: 1.18
- name: Checkout code.
uses: actions/checkout@v2
- name: Run tests.
run: go test -v ./...
124 changes: 124 additions & 0 deletions client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package main

import (
"bufio"
"flag"
"fmt"
"log"
"net/url"
"os"

"github.com/fatih/color"
"github.com/gorilla/websocket"
)

type ConnReader interface {
ReadJSON(v interface{}) error
}

type ConnWriter interface {
WriteJSON(v interface{}) error
Close() error
}

type Scanner interface {
Scan() bool
Text() string
}

type message struct {
Username string `json:"username"`
Text string `json:"text"`
Type string `json:"type"`
}

func main() {
var name string
var s *bufio.Scanner

// Parse flags.
server := flag.String("server", "localhost:9000", "Server network address")
path := flag.String("path", "/", "Server path")
flag.Parse()

// Construct WebSocket URL.
u := url.URL{Scheme: "ws", Host: *server, Path: *path}

// Read username.
fmt.Printf("%s", color.YellowString("Enter your Name: "))
s = bufio.NewScanner(os.Stdin)
s.Scan()
name = s.Text()

// Display welcome message.
color.Green("\nWelcome %s!\n", name)
color.Green("Connecting to server @ %s\n", *server)
color.Yellow("Send a message, or type !q to exit.\n")

// Get WebSocket connection.
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
color.Red("Connection error, exiting: %s", err)
os.Exit(0)
}
defer conn.Close()

// Send joining message.
msg := message{Username: name, Text: "has joined the chat.", Type: "info"}
_ = conn.WriteJSON(msg)

go readMessages(conn) // Handle incoming messages concurrently.
writeMessages(conn, s, name) // Handle outgoing messages concurrently.
}

// readMessages handles incoming messages on the WebSocket connection.
func readMessages(conn ConnReader) {
for {
var msg message

// Read message.
err := conn.ReadJSON(&msg)
if err != nil {
color.Red("Server closed. Exiting...")
os.Exit(0)
}

// Display message.
color.Magenta("%s: %s\n", msg.Username, msg.Text)
}
}

// writeMessages scans stdin and sends each scanned line to the server as JSON.
func writeMessages(conn ConnWriter, s Scanner, name string) {
var msg message
msg.Username = name

for {
fmt.Print("> ")
if s.Scan() {
fmt.Printf("\033[A")
msg.Text = s.Text()

// Handle quit event.
if msg.Text == "!q" {
fmt.Println("Goodbye!")
_ = conn.WriteJSON(message{Username: name, Text: "has disconnected.", Type: "info"})
conn.Close()
os.Exit(0)
}

// Display message.
if msg.Type != "" {
color.Cyan("%s %s\n", msg.Username, msg.Text)
} else {
color.Cyan("%s: %s\n", msg.Username, msg.Text)
}

// Write message to connection.
err := conn.WriteJSON(msg)
if err != nil {
log.Fatal("Error sending message, exiting")
}
}
}
}
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ go 1.18
require (
github.com/charmbracelet/bubbles v0.14.0
github.com/charmbracelet/bubbletea v0.22.1
github.com/fatih/color v1.13.0
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/charmbracelet/lipgloss v0.5.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,18 @@ github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DA
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
Expand All @@ -37,6 +46,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
108 changes: 108 additions & 0 deletions server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package main

import (
"flag"
"log"
"net/http"
"time"

"github.com/fatih/color"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)

type message struct {
Username string `json:"username"`
Text string `json:"text"`
Type string `json:"type"`
ID uuid.UUID
}

// Upgrader instance to upgrade all HTTP connections to a WebSocket.
var upgrader = websocket.Upgrader{}

// Map to store currently active client connections.
var activeClients = make(map[*websocket.Conn]uuid.UUID)

// Channel for client messages.
var messageChan = make(chan message)

func main() {
// Parse flags.
addr := flag.String("addr", ":9000", "Server's network address")
flag.Parse()

mux := http.NewServeMux()
mux.HandleFunc("/", handleConn)

// Handle incoming messages.
go handleMsg()

// Start the server.
log.Printf("Starting server on %s", *addr)
err := http.ListenAndServe(*addr, mux)
if err != nil {
log.Fatal("Error starting server, exiting.", err)
}
}

// handleConn handles incoming HTTP connections by adding the connection to activeClients and reads messages from the connection.
func handleConn(w http.ResponseWriter, r *http.Request) {
// Upgrade incoming HTTP connections to WebSocket connections
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Error upgrading connection to websocket: %v", err)
}
defer conn.Close()

// Generate a UUID for the client.
activeClients[conn] = uuid.New()

for {
var msg message

// Read message from the connection.
err := conn.ReadJSON(&msg)
if err != nil {
log.Printf("Closing connection with ID: %v", activeClients[conn])
delete(activeClients, conn)
break
}

// Set message ID
msg.ID = activeClients[conn]

// Send message to messageChan.
messageChan <- msg
}
}

// handleMsg listens to the messageChan channel and broadcasts messages to other clients.
func handleMsg() {
for {
// Get message from messageChan.
msg := <-messageChan

// Log each message to stdout.
t := time.Now().Format(time.ANSIC)
if msg.Type != "" {
color.Green("%s >> %s %s\n", t, msg.Username, msg.Text)
} else {
color.Green("%s >> %s: %s\n", t, msg.Username, msg.Text)
}

// Broadcast to all active clients.
for client, UUID := range activeClients {
// Check the UUID to prevent sending messages to their origin.
if msg.ID != UUID {
// Write JSON message.
err := client.WriteJSON(msg)
if err != nil {
log.Printf("Error sending message to client: %v", err)
client.Close()
delete(activeClients, client)
}
}
}
}
}
2 changes: 1 addition & 1 deletion tui/main.go → tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
tea "github.com/charmbracelet/bubbletea"
)

func main() {
func UI() { //nolint:deadcode
p := tea.NewProgram(initialModel())
if err := p.Start(); err != nil {
log.Fatal(err)
Expand Down

0 comments on commit 8a1c883

Please sign in to comment.