TCP Client and Server Implementation
Starts a TCP server that accepts clients in goroutines, responds to messages, and can be stopped via context or client commands.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
package main
import (
"bufio"
"context"
"errors"
"fmt"
"log"
"net"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// TCPServer represents a TCP server with graceful shutdown
type TCPServer struct {
address string
listener net.Listener
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
// NewTCPServer creates a new TCP server instance
func NewTCPServer(address string) *TCPServer {
ctx, cancel := context.WithCancel(context.Background())
return &TCPServer{
address: address,
ctx: ctx,
cancel: cancel,
}
}
// Start starts the TCP server and listens for connections
func (s *TCPServer) Start() error {
// Create TCP listener
listener, err := net.Listen("tcp", s.address)
if err != nil {
return fmt.Errorf("failed to start listener: %w", err)
}
s.listener = listener
log.Printf("TCP server started on %s", s.address)
// Handle OS signals for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("Received shutdown signal, stopping server...")
s.Stop()
}()
// Accept connections in a loop
for {
// Check if server is shutting down
select {
case <-s.ctx.Done():
return nil
default:
// Accept new connection with timeout
s.listener.(*net.TCPListener).SetDeadline(time.Now().Add(1 * time.Second))
conn, err := s.listener.Accept()
if err != nil {
// Check if it's a timeout error (normal during shutdown)
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
continue
}
// Check if listener is closed (normal during shutdown)
if errors.Is(err, net.ErrClosed) {
return nil
}
log.Printf("Failed to accept connection: %v", err)
continue
}
// Handle connection in a separate goroutine
s.wg.Add(1)
go func() {
defer s.wg.Done()
s.handleConnection(conn)
}()
}
}
}
// Stop gracefully stops the TCP server
func (s *TCPServer) Stop() {
// Cancel context to signal shutdown
s.cancel()
// Close listener to stop accepting new connections
if s.listener != nil {
s.listener.Close()
}
// Wait for all connections to be handled
s.wg.Wait()
log.Println("TCP server stopped gracefully")
}
// handleConnection processes a single client connection
func (s *TCPServer) handleConnection(conn net.Conn) {
// Ensure connection is closed when done
defer func() {
conn.Close()
log.Printf("Connection closed: %s", conn.RemoteAddr())
}()
log.Printf("New connection from: %s", conn.RemoteAddr())
// Set read/write deadlines to prevent hanging connections
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
// Create buffered reader for line-based communication
reader := bufio.NewReader(conn)
for {
// Check if server is shutting down
select {
case <-s.ctx.Done():
return
default:
// Read line from client
message, err := reader.ReadString('\n')
if err != nil {
// Check for normal disconnection
if errors.Is(err, net.ErrClosed) || err.Error() == "EOF" {
return
}
log.Printf("Error reading from %s: %v", conn.RemoteAddr(), err)
return
}
// Trim whitespace and process message
message = message[:len(message)-1] // Remove newline
log.Printf("Received from %s: %s", conn.RemoteAddr(), message)
// Handle special commands
switch message {
case "quit":
_, err := conn.Write([]byte("Goodbye!\n"))
if err != nil {
log.Printf("Error writing to %s: %v", conn.RemoteAddr(), err)
}
return
case "ping":
_, err := conn.Write([]byte("pong\n"))
if err != nil {
log.Printf("Error writing to %s: %v", conn.RemoteAddr(), err)
return
}
default:
// Echo message back to client
response := fmt.Sprintf("Echo: %s\n", message)
_, err := conn.Write([]byte(response))
if err != nil {
log.Printf("Error writing to %s: %v", conn.RemoteAddr(), err)
return
}
}
// Reset read deadline after successful read
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
}
}
}
// TCPClient represents a TCP client
type TCPClient struct {
address string
conn net.Conn
}
// NewTCPClient creates a new TCP client instance
func NewTCPClient(address string) *TCPClient {
return &TCPClient{address: address}
}
// Connect establishes a connection to the TCP server
func (c *TCPClient) Connect() error {
conn, err := net.DialTimeout("tcp", c.address, 5*time.Second)
if err != nil {
return fmt.Errorf("failed to connect to server: %w", err)
}
c.conn = conn
log.Printf("Connected to server at %s", c.address)
return nil
}
// Send sends a message to the server and returns the response
func (c *TCPClient) Send(message string) (string, error) {
if c.conn == nil {
return "", errors.New("not connected to server")
}
// Set write deadline
c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
// Send message with newline (server expects line-based messages)
_, err := c.conn.Write([]byte(message + "\n"))
if err != nil {
return "", fmt.Errorf("failed to send message: %w", err)
}
// Set read deadline
c.conn.SetReadDeadline(time.Now().Add(5 * time.Second))
// Read response
reader := bufio.NewReader(c.conn)
response, err := reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
// Remove newline and return
return response[:len(response)-1], nil
}
// Close closes the client connection
func (c *TCPClient) Close() error {
if c.conn != nil {
return c.conn.Close()
}
return nil
}
func main() {
// Start server in a goroutine
server := NewTCPServer(":8080")
go func() {
if err := server.Start(); err != nil {
log.Fatalf("Server error: %v", err)
}
}()
// Give server time to start
time.Sleep(100 * time.Millisecond)
// Create and connect client
client := NewTCPClient(":8080")
if err := client.Connect(); err != nil {
log.Fatalf("Client connection error: %v", err)
}
defer client.Close()
// Send test messages
messages := []string{"Hello Server!", "ping", "How are you?", "quit"}
for _, msg := range messages {
response, err := client.Send(msg)
if err != nil {
log.Printf("Error sending '%s': %v", msg, err)
continue
}
fmt.Printf("Sent: %-15s Received: %s\n", msg, response)
}
// Give server time to process quit command
time.Sleep(100 * time.Millisecond)
// Stop server
server.Stop()
}How It Works
Starts a TCP server that accepts clients in goroutines, responds to messages, and can be stopped via context or client commands.
Server listens and handles OS signals, sets deadlines on sockets, reads newline-delimited messages, responds to commands like ping or quit, and tracks goroutines with a WaitGroup; the client dials with timeouts, sends sample messages, and prints responses before shutdown.
Key Concepts
- 1Context-driven shutdown plus signal handling stops accepts and handlers.
- 2Per-connection deadlines prevent hanging reads and writes.
- 3WaitGroup ensures all handler goroutines finish before exit.
- 4Command handling shows how to implement protocol verbs cleanly.
When to Use This Pattern
- Starting point for custom TCP protocols or chat servers.
- Teaching graceful shutdown patterns for long-lived sockets.
- Local testing harness for networking features.
- Building health-checkable services that respond to ping or quit semantics.
Best Practices
- Set deadlines on every connection to avoid stuck goroutines.
- Validate and trim incoming data before acting on it.
- Close listeners on shutdown and wait for handlers to finish.
- Use buffered I/O for line-based protocols to minimize system calls.
Go Version1.18+
Difficultyadvanced
Production ReadyYes
Lines of Code266