Frederic G. MARAND 4 years ago
commit
2da231b7e5

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+.idea/workspace.xml

+ 6 - 0
.idea/$CACHE_FILE$

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="NodePackageJsonFileManager">
+    <packageJsonPaths />
+  </component>
+</project>

+ 18 - 0
.idea/$PRODUCT_WORKSPACE_FILE$

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="masterDetails">
+    <states>
+      <state key="ScopeChooserConfigurable.UI">
+        <settings>
+          <splitter-proportions>
+            <option name="proportions">
+              <list>
+                <option value="0.2" />
+              </list>
+            </option>
+          </splitter-proportions>
+        </settings>
+      </state>
+    </states>
+  </component>
+</project>

+ 18 - 0
.idea/codeStyles/Project.xml

@@ -0,0 +1,18 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <option name="SOFT_MARGINS" value="80" />
+    <HTMLCodeStyleSettings>
+      <option name="HTML_KEEP_BLANK_LINES" value="1" />
+      <option name="HTML_DO_NOT_INDENT_CHILDREN_OF" value="html" />
+      <option name="HTML_ENFORCE_QUOTES" value="true" />
+    </HTMLCodeStyleSettings>
+    <codeStyleSettings language="HTML">
+      <option name="WRAP_ON_TYPING" value="1" />
+      <indentOptions>
+        <option name="INDENT_SIZE" value="2" />
+        <option name="CONTINUATION_INDENT_SIZE" value="2" />
+        <option name="TAB_SIZE" value="2" />
+      </indentOptions>
+    </codeStyleSettings>
+  </code_scheme>
+</component>

+ 5 - 0
.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
+  </state>
+</component>

+ 6 - 0
.idea/misc.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="JavaScriptSettings">
+    <option name="languageLevel" value="ES6" />
+  </component>
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/whispering_gophers.iml" filepath="$PROJECT_DIR$/.idea/whispering_gophers.iml" />
+    </modules>
+  </component>
+</project>

+ 14 - 0
.idea/runConfigurations/WG_Final.xml

@@ -0,0 +1,14 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="WG Final" type="GoApplicationRunConfiguration" factoryName="Go Application" editBeforeRun="true">
+    <module name="whispering_gophers" />
+    <working_directory value="$PROJECT_DIR$/final" />
+    <go_parameters value="-o wg" />
+    <parameters value="-self 192.16.8.19.127" />
+    <kind value="DIRECTORY" />
+    <filePath value="$PROJECT_DIR$/" />
+    <package value="code.osinet.fr/fgm/whispering_gophers" />
+    <directory value="$PROJECT_DIR$/final" />
+    <output_directory value="$PROJECT_DIR$/final" />
+    <method v="2" />
+  </configuration>
+</component>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 8 - 0
.idea/whispering_gophers.iml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 3 - 0
final/go.mod

@@ -0,0 +1,3 @@
+module code.osinet.fr/fgm/whispering_gophers/final
+
+go 1.12

+ 191 - 0
final/wg.go

@@ -0,0 +1,191 @@
+// Solution to part 9 of the Whispering Gophers code lab.
+//
+// This program extends part 9.
+//
+// It connects to the peer specified by -peer.
+// It accepts connections from peers and receives messages from them.
+// When it sees a peer with an address it hasn't seen before, it makes a
+// connection to that peer.
+// It adds an ID field containing a random string to each outgoing message.
+// When it recevies a message with an ID it hasn't seen before, it broadcasts
+// that message to all connected peers.
+//
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"os"
+	"sync"
+
+	"code.osinet.fr/fgm/whispering_gophers/util"
+)
+
+var (
+	selfAddr = flag.String("self", "", "self host")
+	peerAddr = flag.String("peer", "", "peer host:port")
+	self     string
+)
+
+type Message struct {
+	ID   string
+	Addr string
+	Body string
+}
+
+func main() {
+	flag.Parse()
+	var l net.Listener
+	var err error
+	if *selfAddr != "" {
+		l, err = net.Listen("tcp4", *selfAddr + ":0")
+	} else {
+		l, err = util.ListenOnFirstUsableInterface()
+	}
+	if err != nil {
+		log.Fatal(err)
+	}
+	self = l.Addr().String()
+	log.Println("Listening on", self)
+
+	go dial(*peerAddr)
+	go readInput()
+
+	for {
+		c, err := l.Accept()
+		if err != nil {
+			log.Fatal(err)
+		}
+		go serve(c)
+	}
+}
+
+var peers = &Peers{m: make(map[string]chan<- Message)}
+
+type Peers struct {
+	m  map[string]chan<- Message
+	mu sync.RWMutex
+}
+
+// Add creates and returns a new channel for the given peer address.
+// If an address already exists in the registry, it returns nil.
+func (p *Peers) Add(addr string) <-chan Message {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	if _, ok := p.m[addr]; ok {
+		return nil
+	}
+	ch := make(chan Message)
+	p.m[addr] = ch
+	return ch
+}
+
+// Remove deletes the specified peer from the registry.
+func (p *Peers) Remove(addr string) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	delete(p.m, addr)
+}
+
+// List returns a slice of all active peer channels.
+func (p *Peers) List() []chan<- Message {
+	p.mu.RLock()
+	defer p.mu.RUnlock()
+	l := make([]chan<- Message, 0, len(p.m))
+	for _, ch := range p.m {
+		l = append(l, ch)
+	}
+	return l
+}
+
+func broadcast(m Message) {
+	for _, ch := range peers.List() {
+		select {
+		case ch <- m:
+		default:
+			// Okay to drop messages sometimes.
+		}
+	}
+}
+
+func serve(c net.Conn) {
+	defer c.Close()
+	d := json.NewDecoder(c)
+	for {
+		var m Message
+		err := d.Decode(&m)
+		if err != nil {
+			log.Println(err)
+			return
+		}
+		if Seen(m.ID) {
+			continue
+		}
+		fmt.Printf("%#v\n", m)
+		broadcast(m)
+		go dial(m.Addr)
+	}
+}
+
+func readInput() {
+	s := bufio.NewScanner(os.Stdin)
+	for s.Scan() {
+		m := Message{
+			ID:   util.RandomID(),
+			Addr: self,
+			Body: s.Text(),
+		}
+		Seen(m.ID)
+		broadcast(m)
+	}
+	if err := s.Err(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func dial(addr string) {
+	if addr == self {
+		return // Don't try to dial self.
+	}
+
+	ch := peers.Add(addr)
+	if ch == nil {
+		return // Peer already connected.
+	}
+	defer peers.Remove(addr)
+
+	c, err := net.Dial("tcp", addr)
+	if err != nil {
+		log.Println(addr, err)
+		return
+	}
+	defer c.Close()
+
+	e := json.NewEncoder(c)
+	for m := range ch {
+		err := e.Encode(m)
+		if err != nil {
+			log.Println(addr, err)
+			return
+		}
+	}
+}
+
+var seenIDs = struct {
+	m map[string]bool
+	sync.Mutex
+}{m: make(map[string]bool)}
+
+// Seen returns true if the specified id has been seen before.
+// If not, it returns false and marks the given id as "seen".
+func Seen(id string) bool {
+	seenIDs.Lock()
+	ok := seenIDs.m[id]
+	seenIDs.m[id] = true
+	seenIDs.Unlock()
+	return ok
+}

+ 29 - 0
part1/main.go

@@ -0,0 +1,29 @@
+// Solution to part 1 of the Whispering Gophers code lab.
+// This program reads from standard input and writes JSON-encoded messages to
+// standard output. For example, this input line:
+//	Hello!
+// Produces this output:
+//	{"Body":"Hello!"}
+//
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"log"
+	"os"
+)
+
+type Message struct {
+	Body string
+}
+
+func main() {
+	// TODO: Create a new bufio.Scanner reading from the standard input.
+	// TODO: Create a new json.Encoder writing into the standard output.
+	for /* TODO: Iterate over every line in the scanner */ {
+		// TODO: Create a new message with the read text.
+		// TODO: Encode the message, and check for errors!
+	}
+	// TODO: Check for a scan error.
+}

+ 52 - 0
part2/main.go

@@ -0,0 +1,52 @@
+// Solution to part 2 of the Whispering Gophers code lab.
+//
+// This program extends part 1.
+//
+// It makes a connection the host and port specified by the -dial flag, reads
+// lines from standard input and writes JSON-encoded messages to the network
+// connection.
+//
+// You can test this program by installing and running the dump program:
+// 	$ go get github.com/campoy/whispering-gophers/util/dump
+// 	$ dump -listen=localhost:8000
+// And in another terminal session, run this program:
+// 	$ part2 -dial=localhost:8000
+// Lines typed in the second terminal should appear as JSON objects in the
+// first terminal.
+//
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"flag"
+	"log"
+	"net"
+	"os"
+)
+
+var dialAddr = flag.String("dial", "localhost:8000", "host:port to dial")
+
+type Message struct {
+	Body string
+}
+
+func main() {
+	// TODO: Parse the flags.
+
+	// TODO: Open a new connection using the value of the "dial" flag.
+	// TODO: Don't forget to check the error.
+
+	s := bufio.NewScanner(os.Stdin)
+	// TODO: Create a json.Encoder writing into the connection you created before.
+	for s.Scan() {
+		m := Message{Body: s.Text()}
+		err := e.Encode(m)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+	if err := s.Err(); err != nil {
+		log.Fatal(err)
+	}
+}

+ 51 - 0
part3/main.go

@@ -0,0 +1,51 @@
+// Solution to part 3 of the Whispering Gophers code lab.
+//
+// This program listens on the host and port specified by the -listen flag.
+// For each incoming connection, it launches a goroutine that reads and decodes
+// JSON-encoded messages from the connection and prints them to standard
+// output.
+//
+// You can test this program by running it in one terminal:
+// 	$ part3 -listen=localhost:8000
+// And running part2 in another terminal:
+// 	$ part2 -dial=localhost:8000
+// Lines typed in the second terminal should appear as JSON objects in the
+// first terminal.
+//
+package main
+
+import (
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+)
+
+var listenAddr = flag.String("listen", "localhost:8000", "host:port to listen on")
+
+type Message struct {
+	Body string
+}
+
+func main() {
+	flag.Parse()
+
+	// TODO: Create a net.Listener listening from the address in the "listen" flag.
+
+	for {
+		// TODO: Accept a new connection from the listener.
+		go serve(c)
+	}
+}
+
+func serve(c net.Conn) {
+	// TODO: Use defer to Close the connection when this function returns.
+
+	// TODO: Create a new json.Decoder reading from the connection.
+	for {
+		// TODO: Create an empty message.
+		// TODO: Decode a new message into the variable you just created.
+		// TODO: Print the message to the standard output.
+	}
+}

+ 78 - 0
part4/main.go

@@ -0,0 +1,78 @@
+// Solution to part 4 of the Whispering Gophers code lab.
+//
+// This program is a combination of parts 2 and 3.
+//
+// It listens on the host and port specified by the -listen flag.
+// For each incoming connection, it launches a goroutine that reads and decodes
+// JSON-encoded messages from the connection and prints them to standard
+// output.
+// It concurrently makes a connection the host and port specified by the -dial
+// flag, reads lines from standard input, and writes JSON-encoded messages to
+// the network connection.
+//
+// You can test it by running part3 in one terminal:
+// 	$ part3 -listen=localhost:8000
+// Running this program in another terminal:
+// 	$ part4 -listen=localhost:8001 -dial=localhost:8000
+// And running part2 in another terminal:
+// 	$ part2 -dial=localhost:8001
+// Lines typed in the second terminal should appear as JSON objects in the
+// first terminal, and those typed at the third terminal should appear in the
+// second.
+//
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"os"
+)
+
+var (
+	listenAddr = flag.String("listen", "", "host:port to listen on")
+	dialAddr   = flag.String("dial", "", "host:port to dial")
+)
+
+type Message struct {
+	Body string
+}
+
+func main() {
+	flag.Parse()
+
+	// TODO: Launch dial in a new goroutine, passing in *dialAddr.
+
+	l, err := net.Listen("tcp", *listenAddr)
+	if err != nil {
+		log.Fatal(err)
+	}
+	for {
+		c, err := l.Accept()
+		if err != nil {
+			log.Fatal(err)
+		}
+		go serve(c)
+	}
+}
+
+func serve(c net.Conn) {
+	defer c.Close()
+	d := json.NewDecoder(c)
+	for {
+		var m Message
+		err := d.Decode(&m)
+		if err != nil {
+			log.Println(err)
+			return
+		}
+		fmt.Printf("%#v\n", m)
+	}
+}
+
+func dial(addr string) {
+	// TODO: put the contents of the main function from part 2 here.
+}

+ 100 - 0
part5/main.go

@@ -0,0 +1,100 @@
+// Solution to part 5 of the Whispering Gophers code lab.
+//
+// This program extends part 4.
+//
+// It listens on an available public IP and port, and for each incoming
+// connection it decodes JSON-encoded messages and writes them to standard
+// output.
+// It simultaneously makes a connection to the host and port specified by -peer,
+// reads lines from standard input, and writes JSON-encoded messages to the
+// network connection.
+// The messages include the listen address. For example:
+// 	{"Addr": "127.0.0.1:41232", "Body": "Hello"!}
+//
+// You can test this program by listening with the dump program:
+// 	$ dump -listen=localhost:8000
+// In another terminal, running this program:
+// 	$ part5 -peer=localhost:8000
+// And in a third terminal, running part2 with the address printed by part5:
+// 	$ part2 -dial=192.168.1.200:54312
+// Lines typed in the third terminal should appear in the second, and those
+// typed in the second window should appear in the first.
+//
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"os"
+
+	"github.com/campoy/whispering-gophers/util"
+)
+
+var (
+	peerAddr = flag.String("peer", "", "peer host:port")
+	self     string
+)
+
+type Message struct {
+	Addr string
+	Body string
+}
+
+func main() {
+	flag.Parse()
+
+	// TODO: Create a new listener using util.Listen and put it in a variable named l.
+	// TODO: Set the global variable self with the address of the listener.
+	// TODO: Print the address to the standard output
+
+	go dial(*peerAddr)
+
+	for {
+		c, err := l.Accept()
+		if err != nil {
+			log.Fatal(err)
+		}
+		go serve(c)
+	}
+}
+
+func serve(c net.Conn) {
+	defer c.Close()
+	d := json.NewDecoder(c)
+	for {
+		var m Message
+		err := d.Decode(&m)
+		if err != nil {
+			log.Println(err)
+			return
+		}
+		fmt.Printf("%#v\n", m)
+	}
+}
+
+func dial(addr string) {
+	c, err := net.Dial("tcp", addr)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	s := bufio.NewScanner(os.Stdin)
+	e := json.NewEncoder(c)
+	for s.Scan() {
+		m := Message{
+			// TODO: Put the self variable in the new Addr field.
+			Body: s.Text(),
+		}
+		err := e.Encode(m)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+	if err := s.Err(); err != nil {
+		log.Fatal(err)
+	}
+}

+ 100 - 0
part6/main.go

@@ -0,0 +1,100 @@
+// Solution to part 6 of the Whispering Gophers code lab.
+//
+// This program is functionally equivalent to part 5,
+// but the reading from standard input and writing to the
+// network connection are done by separate goroutines.
+//
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"os"
+
+	"github.com/campoy/whispering-gophers/util"
+)
+
+var (
+	peerAddr = flag.String("peer", "", "peer host:port")
+	self     string
+)
+
+type Message struct {
+	Addr string
+	Body string
+}
+
+func main() {
+	flag.Parse()
+
+	l, err := util.Listen()
+	if err != nil {
+		log.Fatal(err)
+	}
+	self = l.Addr().String()
+	log.Println("Listening on", self)
+
+	go dial(*peerAddr)
+	go readInput()
+
+	for {
+		c, err := l.Accept()
+		if err != nil {
+			log.Fatal(err)
+		}
+		go serve(c)
+	}
+}
+
+func serve(c net.Conn) {
+	defer c.Close()
+	d := json.NewDecoder(c)
+	for {
+		var m Message
+		err := d.Decode(&m)
+		if err != nil {
+			log.Println(err)
+			return
+		}
+		fmt.Printf("%#v\n", m)
+	}
+}
+
+// TODO: Make a new channel of Messages.
+
+func readInput() {
+	s := bufio.NewScanner(os.Stdin)
+	for s.Scan() {
+		m := Message{
+			Addr: self,
+			Body: s.Text(),
+		}
+		// TODO: Send the message to the channel of messages.
+	}
+	if err := s.Err(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func dial(addr string) {
+	c, err := net.Dial("tcp", addr)
+	if err != nil {
+		log.Println(addr, err)
+		return
+	}
+	defer c.Close()
+
+	e := json.NewEncoder(c)
+
+	for /* TODO: Receive messages from the channel using range, storing them in the variable m. */ {
+		err := e.Encode(m)
+		if err != nil {
+			log.Println(addr, err)
+			return
+		}
+	}
+}

+ 139 - 0
part7/main.go

@@ -0,0 +1,139 @@
+// Skeleton to part 7 of the Whispering Gophers code lab.
+//
+// This program extends part 6 by adding a Peers type.
+// The rest of the code is left as-is, so functionally there is no change.
+//
+// However we have added a peers_test.go file, so that running
+//   go test
+// from the package directory will test your implementation of the Peers type.
+//
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"os"
+	"sync"
+
+	"github.com/campoy/whispering-gophers/util"
+)
+
+var (
+	peerAddr = flag.String("peer", "", "peer host:port")
+	self     string
+)
+
+type Message struct {
+	Addr string
+	Body string
+}
+
+func main() {
+	flag.Parse()
+
+	l, err := util.Listen()
+	if err != nil {
+		log.Fatal(err)
+	}
+	self = l.Addr().String()
+	log.Println("Listening on", self)
+
+	go dial(*peerAddr)
+	go readInput()
+
+	for {
+		c, err := l.Accept()
+		if err != nil {
+			log.Fatal(err)
+		}
+		go serve(c)
+	}
+}
+
+type Peers struct {
+	m  map[string]chan<- Message
+	mu sync.RWMutex
+}
+
+// Add creates and returns a new channel for the given peer address.
+// If an address already exists in the registry, it returns nil.
+func (p *Peers) Add(addr string) <-chan Message {
+	// TODO: Take the write lock on p.mu. Unlock it before returning (using defer).
+
+	// TODO: Check if the address is already in the peers map under the key addr.
+	// TODO: If it is, return nil.
+
+	// TODO: Make a new channel of messages
+	// TODO: Add it to the peers map
+	// TODO: Return the newly created channel.
+}
+
+// Remove deletes the specified peer from the registry.
+func (p *Peers) Remove(addr string) {
+	// TODO: Take the write lock on p.mu. Unlock it before returning (using defer).
+	// TODO: Delete the peer from the peers map.
+}
+
+// List returns a slice of all active peer channels.
+func (p *Peers) List() []chan<- Message {
+	// TODO: Take the read lock on p.mu. Unlock it before returning (using defer).
+	// TODO: Declare a slice of chan<- Message.
+
+	for /* TODO: Iterate over the map using range */ {
+		// TODO: Append each channel into the slice.
+	}
+	// TODO: Return the slice.
+}
+
+func serve(c net.Conn) {
+	defer c.Close()
+	d := json.NewDecoder(c)
+	for {
+		var m Message
+		err := d.Decode(&m)
+		if err != nil {
+			log.Println(err)
+			return
+		}
+		fmt.Printf("%#v\n", m)
+	}
+}
+
+var peer = make(chan Message)
+
+func readInput() {
+	s := bufio.NewScanner(os.Stdin)
+	for s.Scan() {
+		m := Message{
+			Addr: self,
+			Body: s.Text(),
+		}
+		peer <- m
+	}
+	if err := s.Err(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func dial(addr string) {
+	c, err := net.Dial("tcp", addr)
+	if err != nil {
+		log.Println(addr, err)
+		return
+	}
+	defer c.Close()
+
+	e := json.NewEncoder(c)
+
+	for m := range peer {
+		err := e.Encode(m)
+		if err != nil {
+			log.Println(addr, err)
+			return
+		}
+	}
+}

+ 83 - 0
part7/peers_test.go

@@ -0,0 +1,83 @@
+package main
+
+import (
+	"testing"
+	"time"
+)
+
+func TestPeers(t *testing.T) {
+	peers := &Peers{m: make(map[string]chan<- Message)}
+	done := make(chan bool, 1)
+
+	var chA, chB <-chan Message
+	go func() {
+		defer func() { done <- true }()
+		if chA = peers.Add("a"); chA == nil {
+			t.Fatal(`peers.Add("a") returned nil, want channel`)
+		}
+	}()
+	go func() {
+		defer func() { done <- true }()
+		if chB = peers.Add("b"); chB == nil {
+			t.Fatal(`peers.Add("b") returned nil, want channel`)
+		}
+	}()
+	<-done
+	<-done
+	if chA == chB {
+		t.Fatal(`peers.Add("b") returned same channel as "a"!`)
+	}
+	if ch := peers.Add("a"); ch != nil {
+		t.Fatal(`second peers.Add("a") returned non-nil channel, want nil`)
+	}
+	if ch := peers.Add("b"); ch != nil {
+		t.Fatal(`second peers.Add("b") returned non-nil channel, want nil`)
+	}
+
+	list := peers.List()
+	if len(list) != 2 {
+		t.Fatalf("peers.List() returned a list of length %d, want 2", len(list))
+	}
+
+	go func() {
+		for _, ch := range list {
+			select {
+			case ch <- Message{Body: "foo"}:
+			case <-time.After(10 * time.Millisecond):
+			}
+		}
+		done <- true
+	}()
+	select {
+	case m := <-chA:
+		if m.Body != "foo" {
+			t.Fatal("received message %q, want %q", m.Body, "foo")
+		}
+	case <-done:
+		t.Fatal(`didn't receive message on "a" channel`)
+	}
+	<-done
+
+	peers.Remove("a")
+
+	list = peers.List()
+	if len(list) != 1 {
+		t.Fatalf("peers.List() returned a list of length %d, want 1", len(list))
+	}
+
+	go func() {
+		select {
+		case list[0] <- Message{Body: "bar"}:
+		case <-time.After(10 * time.Millisecond):
+		}
+		done <- true
+	}()
+	select {
+	case m := <-chB:
+		if m.Body != "bar" {
+			t.Fatalf("received message %q, want %q", m.Body, "bar")
+		}
+	case <-done:
+		t.Fatal(`didn't receive message on "b" channel`)
+	}
+}

+ 156 - 0
part8/main.go

@@ -0,0 +1,156 @@
+// Skeleton to part 8 of the Whispering Gophers code lab.
+//
+// This program extends part 7.
+//
+// It connects to the peer specified by -peer.
+// It accepts connections from peers and receives messages from them.
+// When it sees a peer with an address it hasn't seen before, it opens a
+// connection to that peer.
+//
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"os"
+	"sync"
+
+	"github.com/campoy/whispering-gophers/util"
+)
+
+var (
+	peerAddr = flag.String("peer", "", "peer host:port")
+	self     string
+)
+
+type Message struct {
+	Addr string
+	Body string
+}
+
+func main() {
+	flag.Parse()
+
+	l, err := util.Listen()
+	if err != nil {
+		log.Fatal(err)
+	}
+	self = l.Addr().String()
+	log.Println("Listening on", self)
+
+	go dial(*peerAddr)
+	go readInput()
+
+	for {
+		c, err := l.Accept()
+		if err != nil {
+			log.Fatal(err)
+		}
+		go serve(c)
+	}
+}
+
+// TODO: create a global shared Peers instance
+
+type Peers struct {
+	m  map[string]chan<- Message
+	mu sync.RWMutex
+}
+
+// Add creates and returns a new channel for the given peer address.
+// If an address already exists in the registry, it returns nil.
+func (p *Peers) Add(addr string) <-chan Message {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	if _, ok := p.m[addr]; ok {
+		return nil
+	}
+	ch := make(chan Message)
+	p.m[addr] = ch
+	return ch
+}
+
+// Remove deletes the specified peer from the registry.
+func (p *Peers) Remove(addr string) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	delete(p.m, addr)
+}
+
+// List returns a slice of all active peer channels.
+func (p *Peers) List() []chan<- Message {
+	p.mu.RLock()
+	defer p.mu.RUnlock()
+	l := make([]chan<- Message, 0, len(p.m))
+	for _, ch := range p.m {
+		l = append(l, ch)
+	}
+	return l
+}
+
+func broadcast(m Message) {
+	for /* TODO: Range over the list of peers */ {
+		// TODO: Send a message to the channel, but don't block.
+		// Hint: Select is your friend.
+	}
+}
+
+func serve(c net.Conn) {
+	defer c.Close()
+	d := json.NewDecoder(c)
+	for {
+		var m Message
+		err := d.Decode(&m)
+		if err != nil {
+			log.Println(err)
+			return
+		}
+
+		// TODO: Launch dial in a new goroutine, to connect to the address in the message's Addr field.
+
+		fmt.Printf("%#v\n", m)
+	}
+}
+
+func readInput() {
+	s := bufio.NewScanner(os.Stdin)
+	for s.Scan() {
+		m := Message{
+			Addr: self,
+			Body: s.Text(),
+		}
+		broadcast(m)
+	}
+	if err := s.Err(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func dial(addr string) {
+	// TODO: If dialing self, return.
+
+	// TODO: Add the address to the peers map.
+	// TODO: If you get a nil channel the peer is already connected, return.
+	// TODO: Remove the address from peers map when this function returns
+	//       (use defer).
+
+	c, err := net.Dial("tcp", addr)
+	if err != nil {
+		log.Println(addr, err)
+		return
+	}
+	defer c.Close()
+
+	e := json.NewEncoder(c)
+	for m := range ch {
+		err := e.Encode(m)
+		if err != nil {
+			log.Println(addr, err)
+			return
+		}
+	}
+}

+ 180 - 0
part9/main.go

@@ -0,0 +1,180 @@
+// Skeleton to part 9 of the Whispering Gophers code lab.
+//
+// This program extends part 8.
+//
+// It connects to the peer specified by -peer.
+// It accepts connections from peers and receives messages from them.
+// When it sees a peer with an address it hasn't seen before, it makes a
+// connection to that peer.
+// It adds an ID field containing a random string to each outgoing message.
+// When it recevies a message with an ID it hasn't seen before, it broadcasts
+// that message to all connected peers.
+//
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"net"
+	"os"
+	"sync"
+
+	"github.com/campoy/whispering-gophers/util"
+)
+
+var (
+	peerAddr = flag.String("peer", "", "peer host:port")
+	self     string
+)
+
+type Message struct {
+	// TODO: add ID field
+	Addr string
+	Body string
+}
+
+func main() {
+	flag.Parse()
+
+	l, err := util.Listen()
+	if err != nil {
+		log.Fatal(err)
+	}
+	self = l.Addr().String()
+	log.Println("Listening on", self)
+
+	go dial(*peerAddr)
+	go readInput()
+
+	for {
+		c, err := l.Accept()
+		if err != nil {
+			log.Fatal(err)
+		}
+		go serve(c)
+	}
+}
+
+var peers = &Peers{m: make(map[string]chan<- Message)}
+
+type Peers struct {
+	m  map[string]chan<- Message
+	mu sync.RWMutex
+}
+
+// Add creates and returns a new channel for the given peer address.
+// If an address already exists in the registry, it returns nil.
+func (p *Peers) Add(addr string) <-chan Message {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	if _, ok := p.m[addr]; ok {
+		return nil
+	}
+	ch := make(chan Message)
+	p.m[addr] = ch
+	return ch
+}
+
+// Remove deletes the specified peer from the registry.
+func (p *Peers) Remove(addr string) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+	delete(p.m, addr)
+}
+
+// List returns a slice of all active peer channels.
+func (p *Peers) List() []chan<- Message {
+	p.mu.RLock()
+	defer p.mu.RUnlock()
+	l := make([]chan<- Message, 0, len(p.m))
+	for _, ch := range p.m {
+		l = append(l, ch)
+	}
+	return l
+}
+
+func broadcast(m Message) {
+	for _, ch := range peers.List() {
+		select {
+		case ch <- m:
+		default:
+			// Okay to drop messages sometimes.
+		}
+	}
+}
+
+func serve(c net.Conn) {
+	defer c.Close()
+	d := json.NewDecoder(c)
+	for {
+		var m Message
+		err := d.Decode(&m)
+		if err != nil {
+			log.Println(err)
+			return
+		}
+
+		// TODO: If this message has seen before, ignore it.
+
+		fmt.Printf("%#v\n", m)
+		broadcast(m)
+		go dial(m.Addr)
+	}
+}
+
+func readInput() {
+	s := bufio.NewScanner(os.Stdin)
+	for s.Scan() {
+		m := Message{
+			// TODO: use util.RandomID to populate the ID field.
+			Addr: self,
+			Body: s.Text(),
+		}
+		// TODO: Mark the message ID as seen.
+		broadcast(m)
+	}
+	if err := s.Err(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func dial(addr string) {
+	if addr == self {
+		return // Don't try to dial self.
+	}
+
+	ch := peers.Add(addr)
+	if ch == nil {
+		return // Peer already connected.
+	}
+	defer peers.Remove(addr)
+
+	c, err := net.Dial("tcp", addr)
+	if err != nil {
+		log.Println(addr, err)
+		return
+	}
+	defer c.Close()
+
+	e := json.NewEncoder(c)
+	for m := range ch {
+		err := e.Encode(m)
+		if err != nil {
+			log.Println(addr, err)
+			return
+		}
+	}
+}
+
+// TODO: Create a new map of seen message IDs and a mutex to protect it.
+
+// Seen returns true if the specified id has been seen before.
+// If not, it returns false and marks the given id as "seen".
+func Seen(id string) bool {
+	// TODO: Get a write lock on the seen message IDs map and unlock it at before returning.
+	// TODO: Check if the id has been seen before and return that later.
+	// TODO: Mark the ID as seen in the map.
+}

+ 41 - 0
util/dump/dump.go

@@ -0,0 +1,41 @@
+// This program listens to the host and port specified by the -listen flag and
+// dumps any incoming data to standard output.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"os"
+)
+
+var addr = flag.String("listen", "localhost:8000", "server listen address")
+
+type dumpWriter struct {
+	c net.Conn
+	w io.Writer
+}
+
+func (w dumpWriter) Write(v []byte) (int, error) {
+	fmt.Fprintf(w.w, "[%v->%v] ", w.c.RemoteAddr(), w.c.LocalAddr())
+	return w.w.Write(v)
+}
+
+func main() {
+	flag.Parse()
+	l, err := net.Listen("tcp", *addr)
+	if err != nil {
+		log.Fatal(err)
+	}
+	log.Println("Listening on", l.Addr())
+	for {
+		c, err := l.Accept()
+		if err != nil {
+			log.Println(err)
+			continue
+		}
+		go io.Copy(dumpWriter{c, os.Stdout}, c)
+	}
+}

+ 66 - 0
util/helper.go

@@ -0,0 +1,66 @@
+// Package util provides various useful functions for completing the
+// "Whispering Gophers" code lab.
+package util
+
+import (
+	"crypto/rand"
+	"errors"
+	"fmt"
+	"net"
+)
+
+// ListenOnFirstUsableInterface returns a Listener that listens on the first
+// available port on the first available non-loopback IPv4 network interface.
+// It may not be the one you want (192.168.nn.128 won't usually be reachable from
+// outside your machine).
+func ListenOnFirstUsableInterface() (net.Listener, error) {
+	ip, err := firstExternalIP()
+	if err != nil {
+		return nil, fmt.Errorf("could not find active non-loopback address: %v", err)
+	}
+	return net.Listen("tcp4", ip+":0")
+}
+
+func firstExternalIP() (string, error) {
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		return "", err
+	}
+	for _, iface := range ifaces {
+		if iface.Flags&net.FlagUp == 0 {
+			continue // interface down
+		}
+		if iface.Flags&net.FlagLoopback != 0 {
+			continue // loopback interface
+		}
+		addrs, err := iface.Addrs()
+		if err != nil {
+			return "", err
+		}
+		for _, addr := range addrs {
+			var ip net.IP
+			switch v := addr.(type) {
+			case *net.IPNet:
+				ip = v.IP
+			case *net.IPAddr:
+				ip = v.IP
+			}
+			if ip == nil || ip.IsLoopback() {
+				continue
+			}
+			ip = ip.To4()
+			if ip == nil {
+				continue // not an ipv4 address
+			}
+			return ip.String(), nil
+		}
+	}
+	return "", errors.New("are you connected to the network?")
+}
+
+// RandomID returns an 8 byte random string in hexadecimal.
+func RandomID() string {
+	b := make([]byte, 8)
+	n, _ := rand.Read(b)
+	return fmt.Sprintf("%x", b[:n])
+}