Socket Client and Server in Golang: A Chat Example
This is the fourth version of our socket client and server. In this version, I made some interesting improvements. We now have a complete chat system with useful commands and a few optimizations.
The goal of this version is to refine the system. I removed one of the write channels to send data directly, increasing efficiency. I also dropped the automatic reconnection to keep the code cleaner, since I added several new features.
Server
Let’s go through the changes on the server.
The handler that reads data from the client connection now uses a fixed-size buffer. This avoids waiting for an enter key, making reads more efficient.
func handleReadConn(conn net.Conn, msgReadCh chan string, errCh chan error) {
for {
if msgReadCh == nil || conn == nil {
return
}
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
errCh <- err
return
}
m := string(buf[:n])
msgReadCh <- m
}
}
To send messages from the server to the clients, we replaced the use of channels in a goroutine with a single function. This simplifies sending, since reading data is kept separate, allowing concurrent reads and writes.
To avoid code duplication, the send-error handling lives in the send function, which returns a boolean indicating whether the send succeeded.
func send(conn net.Conn, msg string) bool {
_, err := conn.Write([]byte(msg))
if err != nil {
if err == io.EOF {
fmt.Printf("%v Connection closed\n", conn.RemoteAddr())
return false
}
fmt.Printf("%v Error: %v\n", conn.RemoteAddr(), err)
return false
}
return true
}
I also made several changes to the connection handler. It now interprets basic commands inspired by the IRC protocol. For example, /nick changes the user’s nickname and /who lists the connected users.
Client
On the client, the function that reads data from the connection also uses a fixed-size buffer. This makes reads faster, just like on the server.
func readConn(conn net.Conn) {
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
errorChan <- err
return
}
m := string(buf[:n])
output <- m
}
}
As on the server, the client uses a send function to send data. If an error occurs, we log it and close the connection.
func send(conn net.Conn, m string) {
if conn == nil {
return
}
_, err := conn.Write([]byte(m))
if err != nil {
fmt.Println(err)
conn.Close()
conn = nil
}
}
On the client, we have important commands such as /connect, which establishes a connection to the server, and /help, which shows the available commands. If the client is connected, the /help command also calls the server’s /help, producing a single help list.
Source Code
Here is the source code for our server and client.
Videos with Explanation
Conclusion
With these changes, we added a lot of functionality. We now have a usable chat system that is faster and more reliable. The commands between client and server illustrate good communication and a simple protocol. The same features could be implemented with more sophisticated protocols, such as gRPC or JSON.