Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanfetch committed May 29, 2023
0 parents commit ebc05a3
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 0 deletions.
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/ivanfetch/chat-server

go 1.20

require (
github.com/sirupsen/logrus v1.9.2 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
)
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
150 changes: 150 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package main

import (
"bufio"
"fmt"
"net"
"strings"

log "github.com/sirupsen/logrus"
)

type message struct {
sender, text string
}

// String formats the message sender and text.
func (m message) String() string {
return fmt.Sprintf("%s: %s\n", m.sender, m.text)
}

var debugLog *log.Logger = log.New()

// startConnectionAndMessageManager runs a goroutine that tracks connections to the chat
// server, and processes messages sent by clients.
func startConnectionAndMessageManager(addConnCh, removeConnCh chan net.Conn, addMessageCh chan message) {
var connections []net.Conn
// TODO: Add context to respond to signals and cleanup connections?
go func() {
debugLog.Println("starting connection manager")
for {
select {
case newConn := <-addConnCh:
debugLog.Printf("adding connection from %s", newConn.RemoteAddr())
connections = append(connections, newConn)
case removeConn := <-removeConnCh:
debugLog.Printf("removing connection %s", removeConn.RemoteAddr())
newConnections := make([]net.Conn, len(connections)-1)
newI := 0
for _, conn := range connections {
if conn != removeConn {
newConnections[newI] = conn
newI++
}
}
connections = newConnections
case newMessage := <-addMessageCh:
debugLog.Printf("processing new message from %s: %s", newMessage.sender, newMessage.text)
go broadcast(newMessage, connections, removeConnCh)
default:
// do nothing
}
}
}()
}

// processInput scans a chat connection for text and hands chat commands or
// messages.
func processInput(con net.Conn, addMessageCh chan message, removeConnCh chan net.Conn) {
log.Println("saying hello to our connection")
fmt.Fprintln(con, `Well hello there!
Anything you type to me will be displayed in the output of this chat-server.
`)
scanner := bufio.NewScanner(con)
for scanner.Scan() {
line := scanner.Text()
debugLog.Printf("received from %s: %s", con.RemoteAddr(), line)
if strings.HasPrefix(line, "/") {
exiting := processCommands(line, con)
if exiting {
con.Close()
removeConnCh <- con
return
}
continue
}
addMessageCh <- message{
text: line,
sender: con.RemoteAddr().String(),
}
}
err := scanner.Err()
if err != nil {
debugLog.Println("while reading from %s: %v", con.RemoteAddr(), err)
}
}

func broadcast(msg message, allConnections []net.Conn, removeConnCh chan net.Conn) {
debugLog.Printf("broadcasting to %d connections: %s", len(allConnections), msg)
for _, con := range allConnections {
if con.RemoteAddr().String() == msg.sender {
_, err := fmt.Fprintf(con, "> %s\n", msg.text)
if err != nil {
debugLog.Printf("error writing to %v: %v", con, err)
removeConnCh <- con
}
continue
}
_, err := fmt.Fprintf(con, msg.String())
if err != nil {
debugLog.Printf("error writing to %v: %v", con, err)
removeConnCh <- con
}
}
}

func processCommands(input string, con net.Conn) (clientIsLeaving bool) {
fields := strings.Fields(input)
switch strings.ToLower(fields[0][1:]) { // first word minus its first character(/)
case "quit", "exit", "leave":
fmt.Fprintf(con, "You're leaving? Ok - have a nice day. :)\n")
debugLog.Printf("client %s has signed off", con.RemoteAddr())
return true
case "help":
fmt.Fprintf(con, `Available commands are:
/quit|leave|exit - Sign off and disconnect from chat.
/nick|nickname - Set your nickname, to be displayed with your messages.
`)
default:
fmt.Fprintf(con, "%q is an invalid command, please use /help for a list of commands.\n", fields[0])
}
return false
}

func main() {
debugLog.SetFormatter(&log.TextFormatter{
PadLevelText: true,
})

const listenAddress = ":8080"
l, err := net.Listen("tcp", listenAddress)
if err != nil {
debugLog.Fatalf("cannot listen: %v", err)
}
debugLog.Printf("listening for connections on %s", listenAddress)
addConnCh := make(chan net.Conn)
removeConnCh := make(chan net.Conn)
addMessageCh := make(chan message)
startConnectionAndMessageManager(addConnCh, removeConnCh, addMessageCh)
debugLog.Println("waiting for new connections")
for {
con, err := l.Accept()
if err != nil {
debugLog.Printf("while accepting a connection: %v", err)
continue
}
addConnCh <- con
go processInput(con, addMessageCh, removeConnCh)
}
}

0 comments on commit ebc05a3

Please sign in to comment.