March 30, 2020 Tcp Server Chat
An example of a simple TCP chat in Go with logic explanation.
Let’s start the server:
l, err := net.Listen("tcp", "localhost:9090")
if err != nil {
return
}
defer l.Close()
The server listens on TCP port 9090 on localhost. It is normal practice to launch services bound to localhost.
Next, let’s accept connections:
conn, err := l.Accept()
if err != nil {
return
}
No error handling yet. I’ll focus on it later.
In a chat, we need to read messages sent by clients.
The number of clients is generally more than one, and reading is a blocking operation. Having a single program thread doesn’t allow us to read from all connections. Let’s fix this issue by adding a goroutine for each connected client:
l, err := net.Listen("tcp", "localhost:9090")
if err != nil {
return
}
defer l.Close()
for {
conn, err := l.Accept()
if err != nil {
return
}
go handleUserConnection(conn)
}
func handleUserConnection(c net.Conn) {
defer c.Close()
for {
userInput, err := bufio.NewReader(c).ReadString('\n')
if err != nil {
return
}
}
}
Here we return from the handling function on read error, which will happen when a client disconnects. After returning from the function, we also close the connection. The defer helps here — no matter how the function finishes — defer is called after return.
We don’t need to handle connection closing errors, because errors here don’t change the program flow.
Now we have a working server, but there is no logic besides reading data from clients. Let’s add chat logic. The simplest chat will send any message to all clients. We may call this the “fan out” pattern and it is illustrated below:
To implement “fan out”, we have to iterate over all client connections and write messages to them.
In order to do that, we need to store connections in some way so we can iterate them. I chose sync.Map here because it solves all concurrent access issues.
For our task, a slice would be enough. But in our concurrent program, we would have to add/remove data with that slice, and it would be impossible without sync.Lock. Since we’re coding a simple TCP chat, let’s just use a predefined type that solves concurrency issues.
// Using sync.Map to not deal with concurrency slice/map issues
var connMap = &sync.Map{}
It is important to work with a pointer to sync.Map, not with a value.
sync.Map is a struct that contains a sync.Lock. There is a known issue called “locks passed by value”.
For our map, we need keys. I use UUID here because it guarantees that the keys will be different. (Extremely low probability that UUID generates two equal values).
When a client disconnects, we have to remove that client from the map. Let’s pass the client’s ID to the handling function:
id := uuid.New().String()
connMap.Store(id, conn)
Finally, let’s implement the message fan out pattern:
for {
userInput, err := bufio.NewReader(c).ReadString('\n')
if err != nil {
return
}
connMap.Range(func(key, value interface{}) bool {
if conn, ok := value.(net.Conn); ok {
conn.Write([]byte(userInput))
}
return true
})
}
We can stop iterating the map if we return false from the range function.
Finally, I added error handling and use of the zap logger. The following code is the complete solution. Full source code is also available on GitHub.
package main
import (
"bufio"
"net"
"sync"
"github.com/google/uuid"
"go.uber.org/zap"
)
func main() {
var loggerConfig = zap.NewProductionConfig()
loggerConfig.Level.SetLevel(zap.DebugLevel)
logger, err := loggerConfig.Build()
if err != nil {
panic(err)
}
l, err := net.Listen("tcp", "localhost:9090")
if err != nil {
return
}
defer l.Close()
// Using sync.Map to not deal with concurrency slice/map issues
var connMap = &sync.Map{}
for {
conn, err := l.Accept()
if err != nil {
logger.Error("error accepting connection", zap.Error(err))
return
}
id := uuid.New().String()
connMap.Store(id, conn)
go handleUserConnection(id, conn, connMap, logger)
}
}
func handleUserConnection(id string, c net.Conn, connMap *sync.Map, logger *zap.Logger) {
defer func() {
c.Close()
connMap.Delete(id)
}()
for {
userInput, err := bufio.NewReader(c).ReadString('\n')
if err != nil {
logger.Error("error reading from client", zap.Error(err))
return
}
connMap.Range(func(key, value interface{}) bool {
if conn, ok := value.(net.Conn); ok {
if _, err := conn.Write([]byte(userInput)); err != nil {
logger.Error("error on writing to connection", zap.Error(err))
}
}
return true
})
}
}
Both utility and Go package to wait for ports to open (TCP, UDP).
Read More → Tcp Udp DockerAll examples from the article are located in the github repository.
Let’s take a look at HTTP tools in GO.
Read More → Http Client Server Middleware