author
Kevin Kelche

A Complete Guide to Socket Programming in Go


Introduction

Socket programming is a way of connecting two nodes on a network to communicate with each other. One socket(node) listens on a particular port at an IP, while the other socket reaches out to the other to form a connection. The connection is bidirectional, meaning that both nodes can send and receive data. Sockets implement a client-server model, where the server is the listener and the client reaches out to the server. Socket programming forms the basis of many applications, such as web servers, email clients, etc.

Go’s rich standard library and architecture make it a great choice for writing network applications. With primitives like the net package and its concurrency features, Go makes it easy to write performant and scalable network applications.

In this article, we’ll explore socket programming in Go in depth. We’ll start with understanding TCP and UDP protocols. Then, we’ll dive into creating UDP and TCP clients and servers and finally, we’ll explore advanced topics like handling multiple client connections and concurrency.

Understanding TCP and UDP

TCP and UDP are the two most popular networking protocols. They occur in the transport layer of the OSI model and are used to transfer data over the network. However, they differ in their approach to data transfer.

What are TCP and UDP?

TCP (Transmission Control Protocol) is a connection-oriented protocol, meaning a connection must be established between two given nodes before data is transferred. On the other hand, UDP(User Datagram Protocol) is a connectionless protocol that does not establish a connection between the nodes before data transfer.

Difference between TCP and UDP

The main difference between TCP and UDP are:

When to use TCP and UDP?

TCP is suitable for applications that require reliable data transfer, such as email clients, file transfers, etc. UDP on the other hand is ideal for applications that require high speed and low overhead, such as video streaming, VoIP, etc.

In the next section, we’ll explore how to create TCP and UDP clients and servers in Go.

The net package

The net package provides a rich set of primitives for creating network applications. It provides the basic building blocks for creating clients and servers. The net package is divided into two parts:

Creating a TCP server

TCP Server

A TCP server is a process that listens on a particular port at an IP. Other sockets can reach out to this server and form a TCP connection. The server can then read and write data to the connection. Let’s look at a simple TCP server that listens on port 8000 at localhost.

server.go
package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "strings"
)

func main() {
    ln, err := net.Listen("tcp", ":8000")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Listening on port 8000")
    conn, err := ln.Accept()
    if err != nil {
        log.Fatal(err)
    }
    for {
        message, err :=  bufio.NewReader(conn).ReadString('\n')
        if err != nil {
            log.Fatal(err)
        }
        fmt.Print("Message Received:", string(message))
        newmessage := strings.ToUpper(message)
        conn.Write([]byte(newmessage + "\n"))
    }
}

Copied!

Let’s dissect the code above:

ln, err := net.Listen("tcp", ":8000")

Copied!

conn, err := ln.Accept()

Copied!

To run the server, execute the following command:

terminal
go run server.go

Copied!

TCP Client

Let’s look at a simple TCP client that connects to the server we created above.

client.go
package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "os"
)

func main() {
    conn, err := net.Dial("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    for {
        reader := bufio.NewReader(os.Stdin)
        fmt.Print("Text to send: ")
        text, _ := reader.ReadString('\n')
        fmt.Fprintf(conn, text + "\n")
        message, _ := bufio.NewReader(conn).ReadString('\n')
        fmt.Print("Message from server: "+message)
    }
}

Copied!

Let’s do some code review.

conn, err := net.Dial("tcp", "localhost:8000")

Copied!

To run the client, execute the following command:

terminal
go run client.go

Copied!

Output
Text to send: I am a client
Message from server: I AM A CLIENT

Copied!

Creating a UDP server

UDP Server

Let’s look at a simple UDP server that streams data to the client.

UDPServer.go
package main

import (
    "fmt"
    "log"
    "net"
    "time"
)

func main() {
    addr, err := net.ResolveUDPAddr("udp", ":8000")
    if err != nil {
        log.Fatal(err)
    }
    ln, err := net.ListenUDP("udp", addr)
    if err != nil {
        log.Fatal(err)
    }
    defer ln.Close()
    fmt.Println("Listening on port 8000")
    buf := make([]byte, 1024)
    for {
        n, addr, err := ln.ReadFromUDP(buf)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("Received %s from %s \n", string(buf[:n]), addr)
        _, err = ln.WriteToUDP([]byte(time.Now().String()), addr)
        if err != nil {
            log.Fatal(err)
        }
    }
}

Copied!

In this code we:

addr, err := net.ResolveUDPAddr("udp", ":8000")

Copied!

Run the server:

terminal
go run server.go

Copied!

UDP Client

UDPclient.go
package main

import (
    "fmt"
    "log"
    "net"
    "time"
)

func main() {
    addr, err := net.ResolveUDPAddr("udp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    conn, err := net.DialUDP("udp", nil, addr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    buf := make([]byte, 1024)
    for {
        _, err = conn.Write([]byte("Hello UDP Server"))
        if err != nil {
            log.Fatal(err)
        }
        n, err := conn.Read(buf)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(string(buf[:n]))
        time.Sleep(time.Second)
    }
}

Copied!

Let’s review the code above:

conn, err := net.DialUDP("udp", nil, addr)

Copied!

In this code, we used net.DialUDP to create a UDP connection. This function will establish a connection to the server at the address specified in the second argument.

terminal
go run client.go

Copied!

Upon running this code, it will repeatedly send the message “Hello UDP server” to the server and print the timestamp received from the server. This differs from the TCP client we previously created. In the TCP client, we had to send data and then wait for a response from the server. However, with the UDP client, we can send data and immediately receive the response without waiting for the server to respond.

Implementing Advanced Socket Programming

Handling multiple client connections

Almost all servers I have worked with have to handle multiple client connections. In this section, we will look at how to handle multiple client connections in a TCP server.

server.go
package main

import (
    "fmt"
    "log"
    "net"
    "strings"
)

func handleConnection(conn net.Conn) {
    defer conn.Close()
    buf := make([]byte, 1024)
    for {
        n, err := conn.Read(buf)
        if err != nil {
            log.Println(err)
            return
        }
        fmt.Printf("Received: %s", string(buf[:n]))
        data := strings.ToUpper(string(buf[:n]))
        _, err = conn.Write([]byte(data))
        if err != nil {
            log.Println(err)
            return
        }
    }
}

func main() {
    addr, err := net.ResolveTCPAddr("tcp", ":8000")
    if err != nil {
        log.Fatal(err)
    }
    ln, err := net.ListenTCP("tcp", addr)
    if err != nil {
        log.Fatal(err)
    }
    defer ln.Close()
    fmt.Println("Listening on port 8000")
    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Fatal(err)
        }
        go handleConnection(conn)
    }
}

Copied!

Compared to the previous server, we have made the following changes:

terminal
go run server.go

Copied!

Run multiple clients to test the server.

By running multiple clients, we can observe that the server is capable of handling multiple connections simultaneously. This is an improvement from the earlier setup where only a single client was able to connect to the server at a time.

With the use of goroutines, we can handle a vast number of client connections simultaneously. This is why Golang is a popular choice for large Backend as a Service (BaaS) providers such as Pocketbase, as it can handle tens of thousands of connections while still maintaining high performance.

Dealing with Timeouts

Having a timeout in a server is crucial. Consider a scenario where a client connects to the server but fails to send any data. This would result in an open connection that could potentially cause a resource leak. To prevent this, we can set a timeout on the connection.

Timeouts can be implemented using the SetDeadline method.

conn.SetDeadline(time.Now().Add(5 * time.Second))

Copied!

This code will set a timeout of 5 seconds for the connection. If the client does not send any data within 5 seconds, the connection will be closed.

Conclusion

In this article, we explored the implementation of socket programming in Go. We began by discussing the fundamentals of socket programming and proceeded to demonstrate how to create a TCP and UDP server and client. Furthermore, we explored how to manage multiple client connections and implement timeouts.

Overall, socket programming is a powerful tool for building distributed systems, and Go’s built-in support for network programming makes it an excellent language for creating robust and scalable network applications. By following the examples in this article, you should have a solid understanding of how to use socket programming in Go and be well-equipped to tackle your network programming challenges.

Subscribe to my newsletter

Get the latest posts delivered right to your inbox.