Anonymous user
Chat server: Difference between revisions
→{{header|Go}}: Replace the Go solution with one that handles concurrent I/O much more correctly and is more idiomatic.
(→{{header|Go}}: Replace the Go solution with one that handles concurrent I/O much more correctly and is more idiomatic.) |
|||
Line 532:
=={{header|Go}}==
This example uses the Go idiom of [http://blog.golang.org/share-memory-by-communicating ''Do not communicate by sharing memory; instead, share memory by communicating'']; there are no explicit locks used, instead Go channels are used to safely synchronize where required.
A similar exercise of a chat roulette (different in that messages only have to be written to a single partner rather than broadcast) was the topic of a [http://talks.golang.org/2012/chat.slide#1 2012 Go Talk].
This example handles the case of one specific client "falling behind" by relying on the underlying TCP stack to do a reasonable job of buffering. Once that buffer fills, a write to the that client's connection will time out and the connection will dropped. Other minor improvements would include enabling TCP keep alives, handling temporary errors from accept, and better logging. Not ideal, but it should be good enough for this example.
<lang go>package main
import (
"
"flag"
"fmt"
"log"
"net"
"
"
)
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{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()
// 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() {
s.add = make(chan *conn)
s.rem = make(chan string)
s.msg = make(chan string)
// We define the `conns` map here and 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()
// Defer all the disconnect messages until after
// we've closed all currently problematic conns.
defer dropConn(name)
}
}
}
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 {
if _, exists := conns[c.name]; exists {
fmt.Fprintf(c, "Name %q is not available\n", c.name)
go c.welcome()
}
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() {
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 our (and close) our connection.
func (c *conn) readloop() {
for {
if err != nil {
break
}
//msg = strings.TrimSpace(msg)
c.server.msg <- c.name + "> " + msg
}
c.server.rem <- c.name
}</lang>
|