Please purchase the course to watch this video.

Full Course
Creating a TCP client using the net package in Go allows developers to connect to servers seamlessly, mirroring the functionality of the telnet command. By initializing a Go project and employing the net.Dial
method, users can establish a connection to a specified address, handling potential errors effectively. This lesson explores how to send input from standard input to a server using io.Copy
and addresses common challenges, such as blocking calls, by leveraging goroutines for concurrent execution. The final outcome is a simple yet operational telnet client that communicates with an echo server, demonstrating the practical application of Go’s networking capabilities.
No links available for this lesson.
In the previous lesson, we created a TCP server using Go's net
package. In this lesson, we'll look at the client side of TCP connections and create a simple telnet-like client that can connect to our echo server.
Creating a Basic TCP Client
Let's start by initializing a new project and creating a simple TCP client:
package main
import (
"io"
"log"
"net"
"os"
)
func main() {
// Connect to the TCP server
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal("Failed to connect to TCP server:", err)
}
defer conn.Close()
// Copy standard input to the connection
io.Copy(conn, os.Stdin)
}
This code:
- Uses
net.Dial
to establish a connection to a TCP server - Copies data from standard input (keyboard) to the connection
Understanding net.Dial
The net.Dial
function is the client-side counterpart to net.Listen
. It allows a Go application to connect to a listening server. The function signature is:
func Dial(network, address string) (Conn, error)
Where:
network
is the protocol ("tcp", "udp", "unix", etc.)address
is the destination address (e.g., "127.0.0.1:8080")
Like net.Listen
, there are specialized versions for specific protocols:
net.DialTCP
net.DialUDP
net.DialIP
net.DialUnix
Two-Way Communication
Our current client only sends data to the server but doesn't display any responses. Let's modify it to handle both directions:
package main
import (
"io"
"log"
"net"
"os"
)
func main() {
// Connect to the TCP server
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal("Failed to connect to TCP server:", err)
}
defer conn.Close()
// Copy standard input to the connection
io.Copy(conn, os.Stdin)
// Copy responses from the connection to standard output
io.Copy(os.Stdout, conn)
}
However, there's a problem with this approach. The first io.Copy
call is blocking, meaning it won't return until the connection is closed, so the second io.Copy
never gets executed.
Using Goroutines for Concurrent I/O
To handle bidirectional communication, we need to perform the read and write operations concurrently using goroutines:
package main
import (
"io"
"log"
"net"
"os"
)
func main() {
// Connect to the TCP server
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatal("Failed to connect to TCP server:", err)
}
defer conn.Close()
// Start a goroutine to copy stdin to the connection
go func() {
io.Copy(conn, os.Stdin)
// Optional: close connection when stdin is closed
// conn.Close()
}()
// Copy responses from the connection to stdout
io.Copy(os.Stdout, conn)
}
Now our client can both send data to the server and display responses, creating a full-duplex communication channel.
Testing with Our Echo Server
To test this client:
-
Start the echo server from the previous lesson:
$ cd echo $ go run main.go Server listening on port 8080
-
In another terminal, run our telnet client:
$ cd telnet $ go run main.go
-
Type messages in the telnet client, and you should see:
- The messages appear on the server console
- The server echo responses appear in the client
Enhanced Client with Command-Line Arguments
Let's enhance our client to accept the server address as a command-line argument:
package main
import (
"flag"
"fmt"
"io"
"log"
"net"
"os"
)
func main() {
// Parse command-line arguments
var addr string
flag.StringVar(&addr, "addr", "127.0.0.1:8080", "Server address (host:port)")
flag.Parse()
fmt.Printf("Connecting to %s...\n", addr)
// Connect to the TCP server
conn, err := net.Dial("tcp", addr)
if err != nil {
log.Fatal("Failed to connect to TCP server:", err)
}
defer conn.Close()
fmt.Println("Connected! Type messages to send.")
// Start a goroutine to copy stdin to the connection
go func() {
io.Copy(conn, os.Stdin)
// Signal EOF to server by closing write half of connection
conn.(*net.TCPConn).CloseWrite()
}()
// Copy responses from the connection to stdout
io.Copy(os.Stdout, conn)
fmt.Println("\nConnection closed.")
}
This version:
- Accepts a custom server address via the
-addr
flag - Gives user feedback about connection status
- Properly closes the write side of the connection when input ends
Specialized TCP Client
As with the server, we can use the TCP-specific functions for more control:
package main
import (
"flag"
"fmt"
"io"
"log"
"net"
"os"
"time"
)
func main() {
// Parse command-line arguments
var addr string
flag.StringVar(&addr, "addr", "127.0.0.1:8080", "Server address (host:port)")
flag.Parse()
fmt.Printf("Connecting to %s...\n", addr)
// Resolve the TCP address
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
log.Fatal("Failed to resolve address:", err)
}
// Connect to the TCP server
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
log.Fatal("Failed to connect to TCP server:", err)
}
defer conn.Close()
// Configure the connection
conn.SetKeepAlive(true)
conn.SetKeepAlivePeriod(30 * time.Second)
fmt.Println("Connected! Type messages to send.")
// Start a goroutine to copy stdin to the connection
go func() {
io.Copy(conn, os.Stdin)
conn.CloseWrite()
}()
// Copy responses from the connection to stdout
io.Copy(os.Stdout, conn)
fmt.Println("\nConnection closed.")
}
Summary
In this lesson, we've learned:
- How to create a TCP client using
net.Dial
- Using goroutines to handle bidirectional communication
- Properly closing connections
- Using command-line flags for configuration
- Using specialized TCP functions for additional control
This simple telnet client demonstrates the basics of TCP client programming in Go. The same principles apply to other network protocols and more complex client applications.
By understanding both the server and client sides of TCP networking, you can now build complete networking applications in Go, from simple utilities like our echo server and telnet client to more complex distributed systems.