I strongly believe that the best way to learn a new programming language is by doing side projects.
In this article we will write the simplest TCP Chat only using STD in Go, after that you can improve and add features as you want.
We can start talking about the imports needed for this project
imports, types and structs
import (
"log"
"net"
"time"
)
type MessageType int
type Message struct {
Conn net.Conn
Type MessageType
Text []byte
}
const (
NewClient MessageType = iota
DisconnectedClient
NewTextClient
)
log: add time information for each printed log and writes to os.Stderr by default.
net: will help us to listen to TCP protocol and to stablish a connnection.
time: for throttling implementation, don't need to worry right now.
main()
func main() {
ln := startServer()
channel := make(chan Message)
go Chat(channel)
for {
conn, err := ln.Accept()
if err != nil {
log.Println("Could not accept connection. ", err)
}
channel <- NewMessage(conn, NewClient, nil)
log.Println("connection accepted. ", conn.RemoteAddr())
go HandleConnection(conn, channel)
}
}
Now we can talk about each line to better understand and adding code as needed.
The line ln := startServer()
calls a method that returns a TCP listener.
startServer()
func startServer() net.Listener {
ln, err := net.Listen("tcp", ":9595")
if err != nil {
log.Fatalf("Could not listen on port %s. Shutting down ...\n", Port)
}
log.Printf("Listening on port %s\n", Port)
return ln
}
We call net.Listen("tcp", ":9595")
to create a TCP listener on port 9595. Then, if something goes wrong there isn't much we can do, so we log and exit the app.
log.Fatalf()
writes to stderr and exit the app.
If the listener worked, we return to main()
.
go Chat(channel)
Our application will have 1 go routine for each connected user, so we need a channel to communicate between go routines. When a user sends a message, we need to send that message to all users.
func Chat(broadcastChan chan Message) {
clients := make(map[string]net.Conn)
lastNewTextClient := make(map[string]time.Time)
for {
msg := <-broadcastChan
if msg.Type == NewClient {
clients[msg.Conn.RemoteAddr().String()] = msg.Conn
log.Println("New client = ", msg.Conn.RemoteAddr().String())
} else if msg.Type == DisconnectedClient {
delete(clients, msg.Conn.RemoteAddr().String())
msg.Conn.Close()
log.Println("Client disconnected. Connection closed.")
} else if msg.Type == NewTextClient {
lastTime := lastNewTextClient[msg.Conn.RemoteAddr().String()]
if !lastTime.IsZero() && lastTime.After(time.Now().Add(-time.Second*5)) {
msg.Conn.Write([]byte("The time elapse between messages is 5 seconds."))
} else {
lastNewTextClient[msg.Conn.RemoteAddr().String()] = time.Now()
for _, conn := range clients {
if conn.RemoteAddr().String() == msg.Conn.RemoteAddr().String() {
continue
}
conn.Write(msg.Text)
}
}
} else {
log.Println("Unknown message type received = ", msg.Type)
}
}
This function has another infinite for-loop, so we can keep the connection alive with the user.
We create a map of users to add and remove users from the app as needed.
We also create a map to keep track of the last message from a user, so each user can only send a new message after 5 seconds.
The line msg := <-broadcastChan
await for the next message from the channel.
If it is a NewClient
, then add this client to the map of users.
If it is a DisconnectedClient
, then remove this client from the map of users and close the connection.
If it is a NewTextClient
, then we iterate over the users and send the message to all other users except the one who sent it.
infinite for-loop
We open a infinite for-loop so the server stay alive indefinitely. Inside the for-loop we call ln.Accept()
, this function blocks the routine until a new connection arrives and return this connection to us i.e. the conn
variable
channel <- NewMessage(conn, NewClient, nil)
If the ln.Accept()
worked, we send a message to the channel to inform that a new user has arrived.
Now, the NewMessage function is defined as
func NewMessage(conn net.Conn, msgType MessageType, buffer []byte) Message {
if msgType == NewClient {
return Message{Conn: conn, Type: NewClient}
} else if msgType == DisconnectedClient {
return Message{Conn: conn, Type: DisconnectedClient}
} else if msgType == NewTextClient {
return Message{Conn: conn, Type: NewTextClient, Text: buffer}
} else {
return Message{Conn: conn}
}
}
go HandleConnection(conn, channel)
Finally, we have the implementation of the last function from main()
func HandleConnection(conn net.Conn, channel chan Message) {
for {
buffer := make([]byte, 512)
_, err := conn.Read(buffer)
if err != nil {
channel <- NewMessage(conn, DisconnectedClient, nil)
break
}
channel <- NewMessage(conn, NewTextClient, buffer)
}
}
If there is any errors to read the user message, we disconnect the client and break the connection after close it.
If we successfully read the message, we send the message to the channel.
Don'f forget, all messages sent to the channel will be handled by the Chat(channel)
function, as is the only moment in the app that read from the channel.
Now, you can improve this code and add new features. This app has only one chat for all users, so one idea can be to add users to groups.
Hope this article helps to better understand the usage of channels in practice!
Top comments (0)