there are multiple ways to create a real-time chat, where user A can communicate with user B and user C. On this occasion, we’ll create a server chat where multiple users can enter a user name and send/receive messages in real-time.
For this article you will need to have previous experience with:
- channels
- goroutines
today we’ll learn how to create a TCP chat in Golang using channels and goroutines
index:
- accept connections (accept incoming connections)
- client
- give it structure
- chat
- client
- listen for connections
- start new client and book new room
- setting up user
- receive messages
- send messages
- test #1
- exit queue channel
- exit guide
- update handleConnection
- test #2
- wrap-up
let’s get started!
first, we need a way to accept incoming connections.
accept connections
package huevosrevueltos.com/simplechat
func buildServer() {
server, err := net.Listen("tcp", ":8080") // localhost:8080
if err != nil {
log.Fatalf("could not start chat: %v", err)
}
for {
conn, err := server.Accept()
if err != nil {
log.Fatal("connection err: %v", err)
continue
}
go handleConnection(conn) // start new goroutine per connection
}
}
with this function we start listening for connections in port 8080. In an infinite loop, we keep accepting new connections until the program stops.
client
func handleConnection(conn net.Conn) {
scanner := bufio.NewScanner(conn)
for scanner.Scan(){
if scanner.Err() != nil{
break
}
fmt.Println(scanner.Text())
}
defer conn.Close()
}
this simple connection handler will print a message every time a client writes to its console
give it structure
the next step it’s to give more structure to the chat and start sending and start processing the messages.
chat
the chat struct will be in charge of receiving, sending, and booking rooms for clients
type chat struct {
clients []*client
messageQueue chan []byte
}
message queue is a channel that waits for incoming client messages, and then propagates them
client
client holds more logic, so let’s go by steps
type client struct {
name string
conn net.Conn
in chan []byte
out chan []byte
token chan struct{}
}
- name: is the username of the client, after the connection is established the server asks the user to put a user name to
- conn: is the connection we start with the server
- in: incoming messages channel
- out: outgoing messages channel
- token: room token that it’s held until the connection is finished
listen for connections
func (c *chat) buildServer() {
server, err := net.Listen("tcp", ":8080") // localhost:8080
if err != nil {
log.Fatalf("could not start chat: %v", err)
}
go c.serve()
for {
conn, err := server.Accept()
if err != nil {
log.Fatalf("connection err: %v", err)
continue
}
go c.handleConnection(conn) // start new goroutine per connection
}
}
the structure here didn’t change that much, go c.serve
creates a new goroutine that will handle the message queue and send the message to all the connected clients.
start the new client and book a new room
next, we need to create a new client and the channels where she can send/receive messages
func (c *chat) handleConnection(conn net.Conn) {
in, out := make(chan []byte), make(chan []byte)
client := newClient(conn, in, out)
c.clients = append(c.clients, client)
client.start()
go c.bookRoom(client)
<-client.token
defer conn.Close()
}
the steps that this function follows are
- start the user
- add the client to the current chat
- create a new “room” to listen for messages from the client
setting up user
func newClient(conn net.Conn, in chan []byte, out chan []byte) *client {
return &client{in: in, out: out, conn: conn}
}
here we ask the user for a username
and start goroutines for incoming and outgoing messages
func (cl *client) start() {
cl.setUpUsername()
go cl.receiveMessages()
go cl.sendMessages()
}
receive messages
here we create a new scanner and go until the connection ends or an error happens
func (cl *client) receiveMessages() {
scanner := bufio.NewScanner(cl.conn)
for scanner.Scan() {
if scanner.Err() != nil {
log.Fatalf("receive messages %v: ", scanner.Err())
break
}
cl.in <- scanner.Bytes()
}
cl.token <- struct{}{} // end connection
}
the content of the scanners is redirected to the in-client channel and then it’s added to the chat message queue
send messages
to send messages to a specific client, we listen to the out channel and write to the connection
func (cl *client) sendMessages() {
for msg := range cl.out {
cl.conn.Write(msg)
}
}
test #1
let’s give it a go and run the server and a couple of clients server
go run main. go
this will start listening to connections on port 8080
for the clients we’ll use nc
client A
nc localhost 8080
Enter your username: megumin
welcome megumin
client B
nc localhost 8080
Enter your username: gopher
welcome gopher
if we write a message on either client we should see the message in both places
client A
nc localhost 8080
Enter your username: megumin
welcome megumin
gopher says: hello megumin
client B
nc localhost 8080
Enter your username: gopher
welcome gopher
hello megumin
gopher says: hello megumin
Cool, we have a working chat in place, the next and final step for this time it’s to let the other users know when someone leaves the chat
exit queue channel
this will help us close the channels, and the connection and let the other users in the room who left the chat
type chat struct {
clients map[*client]bool //<- notice the update to clients
messageQueue chan []byte
exitQueue chan *client
}
the exit queue is a channel that holds clients
exit guide
func (c *chat) exitGuide(client *client) {
delete(c.clients, client)
close(client.in)
close(client.out)
c.messageQueue <- []byte(fmt.Sprintf("%s left the chat\n", client.name))
defer client.conn.Close()
}
update handleConnection
func (c *chat) handleConnection(conn net.Conn) {
in, out := make(chan []byte), make(chan []byte)
client := newClient(conn, in, out)
c.clients[client] = true
client.start()
go c.bookRoom(client)
<-client.token // wait until the client ends writting
c.exitQueue <- client // send client to exit queue
}
test #2
last but not least the handle connection waits until the client ends writing to the connection and writes to the exit queue passing the client pointer to the channel
client A
nc localhost 8080
Enter your username: megumin
welcome megumin
trump says: hello megumin
hi trum
megumin says: hi trum
bye
megumin says: bye
^C
client B
nc localhost 8080
Enter your username: trump
welcome trump
hello megumin
trump says: hello megumin
megumin says: hi trum
megumin says: bye
megumin left the chat
TLDR;
the overall parts of the chat are
- handleConnection // creates new goroutines for incomming connection
- messagesManager // sends and receives messages from clients
- server // single serve function that waits for channel changes, newuser, disconnect, sendmessages
- bookRoom: to format and send messages
- setUpUsername: at the beginning of the connection we ask for a user name
- exitGuide: closes the in / out channels of the client, deletes the client to the chat and closes the connection