How to resolve the algorithm Chat server step by step in the Go programming language

Published on 12 May 2024 09:40 PM
#Go

How to resolve the algorithm Chat server step by step in the Go programming language

Table of Contents

Problem Statement

Write a server for a minimal text based chat. People should be able to connect via ‘telnet’, sign on with a nickname, and type messages which will then be seen by all other connected users. Arrivals and departures of chat members should generate appropriate notification messages.

Let's start with the solution:

Step by Step solution about How to resolve the algorithm Chat server step by step in the Go programming language

Overview

This Go program implements a simple chat server using the TCP protocol. It allows multiple clients to connect and send messages to each other.

Code Structure

  • main.go: Main entry point of the program.
  • chatserver.go: Defines the Server and conn types and the ListenAndServe function.
  • chatconn.go: Defines the conn type and its methods.

Detailed Explanation

Server Type

type Server struct {
   add  chan *conn  // To add a connection
   rem  chan string // To remove a connection by name
   msg  chan string // To send a message to all connections
   stop chan bool   // To stop early
}

The Server type manages the connections and communication between chat clients. It has four channels:

  • add: To add a new connection.
  • rem: To remove a connection by its name.
  • msg: To broadcast messages to all connections.
  • stop: To signal that the server should stop running.

ListenAndServe Function

func ListenAndServe(addr string) error {
   // ...
}

The ListenAndServe function listens for incoming TCP connections on the specified address (addr). It creates a new Server and delegates the handling of connections to the handleConns goroutine.

handleConns Method

func (s *Server) handleConns() {
   // ...
}

The handleConns method is a goroutine that manages the addition, removal, and messaging of connections. It maintains a conns map to track all active connections.

When a new connection is added (case c := <-s.add), the server checks if the name is already taken. If not, it adds the connection to the conns map and broadcasts a message to all clients announcing the new connection.

When a message is received (case str := <-s.msg), the server broadcasts it to all clients.

When a connection is removed (case name := <-s.rem), the server closes the connection and removes it from the conns map. It also broadcasts a message announcing the disconnection.

When the server is stopped (case <-s.stop), it closes all connections and broadcasts a stop message.

conn Type

type conn struct {
   *bufio.Reader         // buffered input
   net.Conn              // raw connection
   server        *Server // the Server on which the connection arrived
   name          string
}

The conn type represents a single client connection. It embeds bufio.Reader and net.Conn so that it implements the io.ReadWriteCloser interface.

newConn Function

func newConn(s *Server, rwc net.Conn) *conn {
   // ...
}

The newConn function creates a new conn using the provided server and network connection.

welcome Method

func (c *conn) welcome() {
   // ...
}

The welcome method asks the client to enter their name. It loops until the client provides a non-empty name. If the name is already taken, it informs the client and prompts them to enter a new name.

readloop Method

func (c *conn) readloop() {
   // ...
}

The readloop method is a goroutine that reads messages from the client and broadcasts them to all other clients. It continues reading until the client disconnects. When the client disconnects, it informs the server to remove the connection.

Source code in the go programming language

package main

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

func main() {
	log.SetPrefix("chat: ")
	addr := flag.String("addr", "localhost:4000", "listen address")
	flag.Parse()
	log.Fatal(ListenAndServe(*addr))
}

// A Server represents a chat server that accepts incoming connections.
type Server struct {
	add  chan *conn  // To add a connection
	rem  chan string // To remove a connection by name
	msg  chan string // To send a message to all connections
	stop chan bool   // To stop early
}

// ListenAndServe listens on the TCP network address addr for
// new chat client connections.
func ListenAndServe(addr string) error {
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	log.Println("Listening for connections on", addr)
	defer ln.Close()
	s := &Server{
		add:  make(chan *conn),
		rem:  make(chan string),
		msg:  make(chan string),
		stop: make(chan bool),
	}
	go s.handleConns()
	for {
		// TODO use AcceptTCP() so that we can get a TCPConn on which
		// we can call SetKeepAlive() and SetKeepAlivePeriod()
		rwc, err := ln.Accept()
		if err != nil {
			// TODO Could handle err.(net.Error).Temporary()
			// here by adding a backoff delay.
			close(s.stop)
			return err
		}
		log.Println("New connection from", rwc.RemoteAddr())
		go newConn(s, rwc).welcome()
	}
}

// handleConns is run as a go routine to handle adding and removal of
// chat client connections as well as broadcasting messages to them.
func (s *Server) handleConns() {
	// We define the `conns` map here rather than within Server,
	// and we use local function literals rather than methods to be
	// extra sure that the only place that touches this map is this
	// method. In this way we forgo any explicit locking needed as
	// we're the only go routine that can see or modify this.
	conns := make(map[string]*conn)

	var dropConn func(string)
	writeAll := func(str string) {
		log.Printf("Broadcast: %q", str)
		// TODO handle blocked connections
		for name, c := range conns {
			c.SetWriteDeadline(time.Now().Add(500 * time.Millisecond))
			_, err := c.Write([]byte(str))
			if err != nil {
				log.Printf("Error writing to %q: %v", name, err)
				c.Close()
				delete(conns, name)
				// Defer all the disconnect messages until after
				// we've closed all currently problematic conns.
				defer dropConn(name)
			}
		}
	}

	dropConn = func(name string) {
		if c, ok := conns[name]; ok {
			log.Printf("Closing connection with %q from %v",
				name, c.RemoteAddr())
			c.Close()
			delete(conns, name)
		} else {
			log.Printf("Dropped connection with %q", name)
		}
		str := fmt.Sprintf("--- %q disconnected ---\n", name)
		writeAll(str)
	}

	defer func() {
		writeAll("Server stopping!\n")
		for _, c := range conns {
			c.Close()
		}
	}()

	for {
		select {
		case c := <-s.add:
			if _, exists := conns[c.name]; exists {
				fmt.Fprintf(c, "Name %q is not available\n", c.name)
				go c.welcome()
				continue
			}
			str := fmt.Sprintf("+++ %q connected +++\n", c.name)
			writeAll(str)
			conns[c.name] = c
			go c.readloop()
		case str := <-s.msg:
			writeAll(str)
		case name := <-s.rem:
			dropConn(name)
		case <-s.stop:
			return
		}
	}
}

// A conn represents the server side of a single chat connection.
// Note we embed the bufio.Reader and net.Conn (and specifically in
// that order) so that a conn gets the appropriate methods from each
// to be a full io.ReadWriteCloser.
type conn struct {
	*bufio.Reader         // buffered input
	net.Conn              // raw connection
	server        *Server // the Server on which the connection arrived
	name          string
}

func newConn(s *Server, rwc net.Conn) *conn {
	return &conn{
		Reader: bufio.NewReader(rwc),
		Conn:   rwc,
		server: s,
	}
}

// welcome requests a name from the client before attempting to add the
// named connect to the set handled by the server.
func (c *conn) welcome() {
	var err error
	for c.name = ""; c.name == ""; {
		fmt.Fprint(c, "Enter your name: ")
		c.name, err = c.ReadString('\n')
		if err != nil {
			log.Printf("Reading name from %v: %v", c.RemoteAddr(), err)
			c.Close()
			return
		}
		c.name = strings.TrimSpace(c.name)
	}
	// The server will take this *conn and do a final check
	// on the name, possibly starting c.welcome() again.
	c.server.add <- c
}

// readloop is started as a go routine by the server once the initial
// welcome phase has completed successfully. It reads single lines from
// the client and passes them to the server for broadcast to all chat
// clients (including us).
// Once done, we ask the server to remove (and close) our connection.
func (c *conn) readloop() {
	for {
		msg, err := c.ReadString('\n')
		if err != nil {
			break
		}
		//msg = strings.TrimSpace(msg)
		c.server.msg <- c.name + "> " + msg
	}
	c.server.rem <- c.name
}


  

You may also check:How to resolve the algorithm Singly-linked list/Element insertion step by step in the ALGOL 68 programming language
You may also check:How to resolve the algorithm Execute a system command step by step in the Bracmat programming language
You may also check:How to resolve the algorithm Jordan-Pólya numbers step by step in the C programming language
You may also check:How to resolve the algorithm ISBN13 check digit step by step in the C# programming language
You may also check:How to resolve the algorithm Peripheral drift illusion step by step in the Delphi programming language