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 }