Had a brief look at this last night, and hacked together a simple telnet talker. Might be of interest to someone.
Go is interesting. I like types, how you can create funcs specific to those types, interfaces, channels are especially cool as are goroutines. I think it has a lot of promise, and I’ll be keeping an eye on this one.
/* gotalk
simple telnet chat server that lets you talk to your friends using telnet.
would be better, if we had some kind of ncurses style terminal control but
its too much work.
*/
package main
import (
"os";
"strings";
"bytes";
"fmt";
"net";
"log";
"container/vector";
)
// Encapsulates a Client connection
type Client struct {
nickname string;
connection *net.Conn;
}
// Encapsulates a message from a client.
type Message struct {
client *Client;
message string;
}
// Woah, you can define main right here without prototypes for
// other funcs!
func main() {
// Go provides some handy functions for doing network stuff
listener, err := net.Listen("tcp4", "0.0.0.0:5555");
// Set up a couple of "channels" to provide messaging
// from the goroutines that handle the input from clients
// to the goroutine that handles output to the clients
message_channel := make(chan Message, 100);
client_channel := make(chan Client, 100);
if err != nil {
log.Exitf("net.Listen tcp :5555: %v", err)
}
// This starts a goroutine. Or a thread. Or both!
// who knows?
go broadcaster(message_channel, client_channel);
// No while loops in go. Just "for". With no expression
// it is always true; an infinite loop.
for {
conn, err := listener.Accept(); // Accept the next connection
if err != nil {
log.Exitf("net.Listener.Accept: %v", err)
}
go reader(conn, message_channel, client_channel); // start a goroutine to handle it
}
}
/* Writes a string to the network connection. Terminates with \r\n */
func writeline(conn net.Conn, message string) (err os.Error) {
_, err = conn.Write(strings.Bytes(fmt.Sprintf("%v\r\n", message)));
return err;
}
/* each of the connections need to see what every other connection says, so we
have a goroutine that receives the messages and handles sending them
to each of the connections. */
func broadcaster(message_channel chan Message, client_channel chan Client) {
// Vectors store "stuff" like vectors in other languages. In this
// case, we want to store a Client. Note the := syntax; declare and define.
clients := vector.New(0);
for {
// If you have multiple channels that you want to
// read from the same goroutine, you need to use select.
// Note that with select, each "case" creates and assigns the
// item being received from the channel, and is followed by
// a colon; there is no break or other construct to signify
// the end of a case, just the next case beginning.
select {
case new_message := <-message_channel: // reads from the message_channel
fmt.Printf("%v: %v\n", new_message.client.nickname, new_message.message);
// Pay attention: This creates an anonymous function that takes
// an argument of type vector.Element, which is what the docs
// say that vector.Do needs. The anon func gets access to stuff
// in the parent context, so you can do some neat stuff. In this
// case, we are sending the same message to each of the Clients.
clients.Do(func(e vector.Element) {
client := e.(Client);
message := "<" + new_message.client.nickname + "> " + new_message.message;
err := writeline(*client.connection, message);
if err != nil {
fmt.Printf("Problem writing: %v \n", err)
}
});
case new_client := <-client_channel: // manage our vector when a Client connects.
clients.Push(new_client);
fmt.Printf("*** %v joined (%v).\n", new_client.nickname, clients.Len());
// TODO: we need a channel to handle removal of Clients from the vector
// when they disconnect.
}
}
}
/* reads a line from a given client and removes the trailing \r\n.
it returns a string and an os.Error as two separate return values
*/
func readline(client Client) (line string, err os.Error) {
// If you send more than bytebuffer has bytes, then Read will fail.
// I think this is a bug.
bytebuffer := make([]byte, 1024);
bytesread, err := client.connection.Read(bytebuffer);
buffer := bytes.NewBuffer(bytebuffer);
if err != nil {
fmt.Printf("*** %v (%v) has closed the connection: %v\n", client.nickname, client.connection.RemoteAddr(), err);
return "", err;
}
buffer.Truncate(bytesread - 2); // for the \r\n, there is probably a better way
line = strings.TrimSpace(buffer.String());
return line, err;
}
/* reader is a goroutine that reads messages from a client. */
func reader(conn net.Conn, message_channel chan Message, client_channel chan Client) {
var client Client;
client.connection = &conn;
fmt.Printf("*** Connection from %v\n", client.connection.RemoteAddr());
banner := string("**** WELCOME ****\r\nChoose a nickname: ");
_, err := client.connection.Write(strings.Bytes(banner));
message, err := readline(client); // note that readline returns two items, not just one.
if err != nil {
return
}
client.nickname = message;
client_channel <- client; // informs the broadcaster goroutine of the new Client
for {
var message Message;
line, err := readline(client);
if err != nil {
return
}
message.message = line;
message.client = &client;
// Send it onward to the other clients.
message_channel <- message; // Every time we see some data, we send it to the broadcaster
}
}
simple telnet chat server that lets you talk to your friends using telnet.
would be better, if we had some kind of ncurses style terminal control but
its too much work.
*/
package main
import (
"os";
"strings";
"bytes";
"fmt";
"net";
"log";
"container/vector";
)
// Encapsulates a Client connection
type Client struct {
nickname string;
connection *net.Conn;
}
// Encapsulates a message from a client.
type Message struct {
client *Client;
message string;
}
// Woah, you can define main right here without prototypes for
// other funcs!
func main() {
// Go provides some handy functions for doing network stuff
listener, err := net.Listen("tcp4", "0.0.0.0:5555");
// Set up a couple of "channels" to provide messaging
// from the goroutines that handle the input from clients
// to the goroutine that handles output to the clients
message_channel := make(chan Message, 100);
client_channel := make(chan Client, 100);
if err != nil {
log.Exitf("net.Listen tcp :5555: %v", err)
}
// This starts a goroutine. Or a thread. Or both!
// who knows?
go broadcaster(message_channel, client_channel);
// No while loops in go. Just "for". With no expression
// it is always true; an infinite loop.
for {
conn, err := listener.Accept(); // Accept the next connection
if err != nil {
log.Exitf("net.Listener.Accept: %v", err)
}
go reader(conn, message_channel, client_channel); // start a goroutine to handle it
}
}
/* Writes a string to the network connection. Terminates with \r\n */
func writeline(conn net.Conn, message string) (err os.Error) {
_, err = conn.Write(strings.Bytes(fmt.Sprintf("%v\r\n", message)));
return err;
}
/* each of the connections need to see what every other connection says, so we
have a goroutine that receives the messages and handles sending them
to each of the connections. */
func broadcaster(message_channel chan Message, client_channel chan Client) {
// Vectors store "stuff" like vectors in other languages. In this
// case, we want to store a Client. Note the := syntax; declare and define.
clients := vector.New(0);
for {
// If you have multiple channels that you want to
// read from the same goroutine, you need to use select.
// Note that with select, each "case" creates and assigns the
// item being received from the channel, and is followed by
// a colon; there is no break or other construct to signify
// the end of a case, just the next case beginning.
select {
case new_message := <-message_channel: // reads from the message_channel
fmt.Printf("%v: %v\n", new_message.client.nickname, new_message.message);
// Pay attention: This creates an anonymous function that takes
// an argument of type vector.Element, which is what the docs
// say that vector.Do needs. The anon func gets access to stuff
// in the parent context, so you can do some neat stuff. In this
// case, we are sending the same message to each of the Clients.
clients.Do(func(e vector.Element) {
client := e.(Client);
message := "<" + new_message.client.nickname + "> " + new_message.message;
err := writeline(*client.connection, message);
if err != nil {
fmt.Printf("Problem writing: %v \n", err)
}
});
case new_client := <-client_channel: // manage our vector when a Client connects.
clients.Push(new_client);
fmt.Printf("*** %v joined (%v).\n", new_client.nickname, clients.Len());
// TODO: we need a channel to handle removal of Clients from the vector
// when they disconnect.
}
}
}
/* reads a line from a given client and removes the trailing \r\n.
it returns a string and an os.Error as two separate return values
*/
func readline(client Client) (line string, err os.Error) {
// If you send more than bytebuffer has bytes, then Read will fail.
// I think this is a bug.
bytebuffer := make([]byte, 1024);
bytesread, err := client.connection.Read(bytebuffer);
buffer := bytes.NewBuffer(bytebuffer);
if err != nil {
fmt.Printf("*** %v (%v) has closed the connection: %v\n", client.nickname, client.connection.RemoteAddr(), err);
return "", err;
}
buffer.Truncate(bytesread - 2); // for the \r\n, there is probably a better way
line = strings.TrimSpace(buffer.String());
return line, err;
}
/* reader is a goroutine that reads messages from a client. */
func reader(conn net.Conn, message_channel chan Message, client_channel chan Client) {
var client Client;
client.connection = &conn;
fmt.Printf("*** Connection from %v\n", client.connection.RemoteAddr());
banner := string("**** WELCOME ****\r\nChoose a nickname: ");
_, err := client.connection.Write(strings.Bytes(banner));
message, err := readline(client); // note that readline returns two items, not just one.
if err != nil {
return
}
client.nickname = message;
client_channel <- client; // informs the broadcaster goroutine of the new Client
for {
var message Message;
line, err := readline(client);
if err != nil {
return
}
message.message = line;
message.client = &client;
// Send it onward to the other clients.
message_channel <- message; // Every time we see some data, we send it to the broadcaster
}
}
That’s a great little script. Thanks. It’s pretty sweet how quickly something like this can be thrown together.
Out of curiosity, it seems that the program crashes when all the participants exit. Is there a clean way to exit the chat? (not that I’m using this for anything)
Yeah, I know. Note the TODO. I need to clean up stuff when they exit. I will take a look tonight when I get home :)
Very clean code! I like the whole script.