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.
(Undo revision 187383 by Wswiss (talk) Does not ocompile)
(→‎{{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 (
"osbufio"
"flag"
"fmt"
"log"
"net"
"flagstrings"
"bufiotime"
"bytes"
)
 
func main() {
// Quick and dirty error handling.
log.SetPrefix("chat: ")
func error_(err error, r int) {
addr := flag.String("addr", "localhost:4000", "listen address")
fmt.Printf("Error: %v\n", err)
flag.Parse()
log.Fatal(ListenAndServe(*addr))
}
 
// A Server represents a chat server that accepts incoming connections.
if r >= 0 {
type Server struct {
os.Exit(r)
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
// A type for storing the connections.
// new chat client connections.
type clientMap map[string]net.Conn
func ListenAndServe(addr string) error {
 
ln, err := net.Listen("tcp", addr)
// A method that makes clientMap compatible with io.Writer, allowing it to be
if err != nil {
// used with fmt.Fprintf().
return err
func (cm clientMap) Write(buf []byte) (n int, err error) {
for _, c := range cm {
// Write to each client in a seperate goroutine.
go c.Write(buf)
}
log.Println("Listening for connections on", addr)
 
defer ln.Close()
n = len(buf)
s := &Server{stop: make(chan bool)}
 
go s.handleConns()
return
for {
}
// TODO use AcceptTCP() so that we can get a TCPConn on which
 
// we can call SetKeepAlive() and SetKeepAlivePeriod()
// Check if a name exists; if it doesn't, add it.
rwc, err := ln.Accept()
func (cm clientMap) Add(name string, c net.Conn) bool {
for if kerr :!= range cmnil {
// TODO Could handle err.(net.Error).Temporary()
if name == k {
// here by adding a backoff delay.
return false
close(s.stop)
return err
}
log.Println("New connection from", rwc.RemoteAddr())
go newConn(s, rwc).welcome()
}
 
cm[name] = c
 
return true
}
 
// handleConns is run as a go routine to handle adding and removal of
// A clientMap variable.
// chat client connections as well as broadcasting messages to them.
var clients clientMap
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,
func init() {
// and we use local function literals rather than methods to be
// Initialize the map.
// extra sure that the only place that touches this map is this
clients = make(clientMap)
// 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)
func client(c net.Conn) {
writeAll := func(str string) {
// Close the connection when this function returns.
log.Printf("Broadcast: %q", str)
defer c.Close()
// TODO handle blocked connections
 
for name, c := range conns {
br := bufio.NewReader(c)
c.SetWriteDeadline(time.Now().Add(500 * time.Millisecond))
 
_, err := c.Write([]byte(str))
fmt.Fprintf(c, "Please enter your name: ")
if err != nil {
 
log.Printf("Error writing to %q: %v", name, err)
buf, err := br.ReadBytes('\n')
c.Close()
if err != nil {
error_ delete(errconns, -1name)
// Defer all the disconnect messages until after
return
// we've closed all currently problematic conns.
defer dropConn(name)
}
}
}
name := string(bytes.Trim(buf, " \t\n\r\x00"))
 
ifdropConn = func(name == ""string) {
if c, ok := conns[name]; ok {
fmt.Fprintf(c, "!!! %v is invalid !!!\n", name)
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() {
// Try to add the connection to the map.
writeAll("Server stopping!\n")
if !clients.Add(name, c) {
for _, c := range conns {
fmt.Fprintf(c, "!!! %v is not available !!!\n", name)
c.Close()
return
}
}()
 
// Send a message telling the clients who connected.
fmt.Fprintf(clients, "+++ %v connected +++\n", name)
// Send a disconnected message when the function returns.
defer fmt.Fprintf(clients, "--- %v disconnected ---\n", name)
// Remove the client from the list.
defer delete(clients, name)
 
for {
select {
buf, err = br.ReadBytes('\n')
ifcase errc !:= nil {<-s.add:
if _, exists := conns[c.name]; exists {
break
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
}
buf = bytes.Trim(buf, " \t\n\r\x00")
 
// Ignore empty messages.
if len(buf) == 0 {
continue
}
 
switch {
// Support for '/me' type messages.
case string(buf[0:3]) == "/me":
buf = append([]byte(name), buf[3:]...)
default:
// Prepend the user-name and '> '.
buf = append([]byte(name+"> "), buf...)
}
 
// Send the message to all the clients.
fmt.Fprintf(clients, "%v\n", string(buf))
}
}
 
// A conn represents the server side of a single chat connection.
func main() {
// Note we embed the bufio.Reader and net.Conn (and specifically in
// Flags. Use -help for usage info.
// that order) so that a conn gets the appropriate methods from each
var (
// to be a full io.ReadWriteCloser.
port int
type conn struct {
help bool
*bufio.Reader // buffered input
)
net.Conn // raw connection
flag.IntVar(&port, "port", 23, "Port to listen on")
server *Server // the Server on which the connection arrived
flag.BoolVar(&help, "help", false, "Display this")
name string
flag.Parse()
}
 
func newConn(s *Server, rwc net.Conn) *conn {
if help {
return &conn{
flag.Usage()
Reader: bufio.NewReader(rwc),
return
Conn: rwc,
server: s,
}
}
 
// welcome requests a name from the client before attempting to add the
// Initialize a new listener.
// named connect to the set handled by the server.
lis, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
func (c *conn) welcome() {
if err != nil {
error_( var err, 1)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
// Begin the main loop.
// 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 {
cmsg, err := lisc.AcceptReadString('\n')
if err != nil {
break
error_(err, -1)
continue
}
//msg = strings.TrimSpace(msg)
 
c.server.msg <- c.name + "> " + msg
// Launch a new goroutine to handle the connection.
go client(c)
}
c.server.rem <- c.name
}</lang>
 
Anonymous user