├── .gitignore ├── README.md ├── Vagrantfile ├── lessons ├── 01-setup.md ├── 02-socket.md ├── 03-data-structures.md ├── 04-login.md ├── 05-handle-joins.md ├── 06-broadcast-msgs.md ├── 07-logouts.md ├── 08-extra-credit.md └── code │ ├── 01-setup │ └── chat.go │ ├── 02-socket │ └── chat.go │ ├── 03-data-structures │ └── chat.go │ ├── 04-login │ └── chat.go │ ├── 05-handle-joins │ └── chat.go │ ├── 06-broadcast-msgs │ └── chat.go │ ├── 07-logouts │ └── chat.go │ └── final │ └── chat.go └── vagrant ├── install-go.sh └── provision.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | .vagrant 3 | chat-server 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Boston Lab #1 - Building a Chat Server 2 | 3 | Welcome to the first Boston Go language lab! We are going to build a chat server in Go, then chat with each other! 4 | 5 | The goal is to practice implementing some of the concurrency primitives we learned in the previous meetup, and 6 | get your hands dirty by writing some real Go code. 7 | 8 | ## Prerequisites 9 | 10 | 1. PLEASE BRING: A laptop with a Go language environment with Go 1.4 already set up. Please see 'Vagrant setup' below to get started with one easily. 11 | * If you are having trouble setting up your Go programming environment, please join the #lab-help channel in the Boston Golang Slack group. You can signup for the Slack group [here](http://bostongolang-slack-invite.herokuapp.com/). 12 | 13 | 2. PLEASE BRING: A text editor or IDE suitable for writing Go code. 14 | * For beginners, [Sublime](http://www.sublimetext.com) is a good option. Make sure you install the Go plugin [here](https://github.com/DisposaBoy/GoSublime). 15 | * For VIM users, there is a pretty nice VIM setup [here](https://github.com/fatih/vim-go). 16 | 17 | 3. PLEASE HAVE: Some basic Go language exposure. You should be familiar with the Go basics: e.g., Go's types, structs, control flow structures, goroutines, and channels. 18 | Other programming language experience and concepts (such as networking, etc) will be helpful. A good introduction to the basics of Go for people familiar with 19 | other programming language is available at: [https://tour.golang.org](https://tour.golang.org). If you can get through this tour, you will be well-prepared for this meetup! 20 | 21 | ### General setup 22 | 23 | 1. Fork this repository in Github. 24 | 25 | 1. Clone the repository into a directory 26 | 27 | ```bash 28 | # open a terminal window and type: 29 | $ git clone https://github.com/bostongolang/golang-lab-chat.git 30 | ``` 31 | 32 | ### Vagrant setup 33 | 34 | 1. Install [Vagrant](http://www.vagrantup.com/downloads) for your platform. 35 | 1. Open a terminal window, and cd /path/to/this/repository. 36 | 37 | ```bash 38 | # open a terminal window and type: 39 | $ cd golang-lab-chat 40 | ``` 41 | 42 | 1. From within the `golang-lab-chat` directory, type `vagrant up`. This will create a virtual machine with Ubuntu linux and Go 1.4 installed for the Vagrant user. 43 | 1. Type `vagrant ssh` to ssh into the virtual machine. 44 | 45 | Within the virtual machine, `golang-lab-chat` on the host computer 46 | will be mapped to /opt/golang-lab-chat in the guest. So any changes 47 | you make in the normal filesystem should be reflected in the VM! 48 | 49 | Need help? Arrive a little early and we can help you get up-and-running, or join 50 | the Boston Golang Slack group #lab-help channel group by signing up [here](http://bostongolang-slack-invite.herokuapp.com/). 51 | 52 | You can also email me directly: [jandre+bostongolang@gmail.com](mailto:jandre+bostongolang@gmail.com). 53 | 54 | 55 | ## Lessons - Table of contents 56 | 57 | This lab is organized into the following steps -- if you ever need to 'cheat' and see an example of how the code is 58 | implemented, we have provided working code examples for each section. 59 | 60 | 1. [Setting up your chat project](lessons/01-setup.md) 61 | 1. [Creating the TCP socket](lessons/02-socket.md) 62 | 1. [Populating our data structures](lessons/03-data-structures.md) 63 | 1. [Handling user logins](lessons/04-login.md) 64 | 1. [Notifying when users join](lessons/05-handle-joins.md) 65 | 1. [Broadcasting chat messages](lessons/06-broadcast-msgs.md) 66 | 1. [Notifying when users logout](lessons/07-logouts.md) 67 | 1. [Extra credit!!](lessons/08-extra-credit.md) 68 | 69 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # ssh-agent must be aware of your identity or vagrant ssh forwarding will fail 5 | def sshCheck 6 | output=`ssh-add -L 2>&1` 7 | if 0 != $? 8 | if output =~ /Could not open a connection/ 9 | puts 'error: start your ssh-agent' 10 | puts 'bash: eval `ssh-agent -s`' 11 | puts 'rc: eval `{ssh-agent -s}' 12 | elsif output =~ /The agent has no identities/ 13 | puts 'error: add your ssh identity to the agent' 14 | puts 'ssh-add [key-path]' 15 | else 16 | puts 'error: something is wrong with your ssh-agent' 17 | end 18 | 19 | return false 20 | end 21 | 22 | return true 23 | end 24 | 25 | exit 1 if false == sshCheck 26 | 27 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 28 | VAGRANTFILE_API_VERSION = "2" 29 | 30 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 31 | config.vm.box = "golang-lab-chat" 32 | config.vm.network "forwarded_port", guest: 6677, host: 6677, auto_correct: true 33 | config.vm.box_url = "http://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-amd64-vagrant-disk1.box" 34 | config.vm.network "private_network", ip: "192.168.13.50" 35 | config.ssh.forward_agent = true 36 | config.vm.synced_folder ".", "/opt/golang-lab-chat" 37 | config.vm.provider "virtualbox" do |v| 38 | v.memory = 2048 39 | v.cpus = 2 40 | end 41 | config.vm.provision "shell", path: "./vagrant/provision.sh" 42 | end 43 | -------------------------------------------------------------------------------- /lessons/01-setup.md: -------------------------------------------------------------------------------- 1 | # Setup the chat project (5-10 minutes) 2 | 3 | ## Goals 4 | 5 | The goal of the portion is to setup project `chat-server` which will 6 | hold the code for your chat project. 7 | 8 | In the interest of time, we will all organize our chat server around the following 9 | data structures which we have scaffolded for you in [chat.go](code/01-setup/chat.go). 10 | 11 | * a `ChatUser` struct to handle the individual chat connection for the user. 12 | * a `ChatRoom` struct to handle all of the connections, chatroom messages, and disconnects. 13 | * a `main` function that will be the entry point of your code. 14 | 15 | We will walk through populating all of these to create the chat server in later lessons. 16 | 17 | ## Steps 18 | 19 | 1. Create a directory `chat-server` for your code underneath the main folder for 20 | this repository, and a file `chat.go` that will contain the code. 21 | 22 | E.g. under `golang-lab-chat/chat-server` 23 | 24 | ```bash 25 | mkdir -p chat-server 26 | cd chat-server 27 | ``` 28 | 29 | tip: If you are using Vagrant, this will 'appear' in `/opt/golang-lab-chat/chat-server`. 30 | The /opt/golang-lab-chat folder is shared from the repository folder in your host operating 31 | system, so any changes you make will be reflected. 32 | 33 | 34 | 1. :star2: Download the basic scaffold code [chat.go](code/01-setup/chat.go), and put it in the `chat-server` folder. Open `chat.go` with your favorite code editor. 35 | 36 | 1. Review the struct `ChatRoom`. This struct will handle 37 | 38 | * users joining; 39 | * users disconnecting; 40 | * receiving individual messages from users and broadcasting (relaying) them to other users. 41 | 42 | 1. Review the struct `ChatUser`. This struct will handle 43 | * reading lines of data from the user socket and notifying 44 | the chatroom there is a new messages 45 | * writing data back to the socket (e.g messages from other users) 46 | 47 | 1. Ensure the code runs by typing `go run chat.go`. You should see a message about the chat server starting. 48 | 49 | ```bash 50 | $ cd /opt/golang-lab-chat # if using vagrant 51 | $ go run chat.go 52 | 2015/05/03 20:13:42 Chat server starting! 53 | ``` 54 | 55 | [Get the solution](code/01-setup/chat.go) 56 | 57 | [Proceed to Lesson 2](02-socket.md) 58 | 59 | -------------------------------------------------------------------------------- /lessons/02-socket.md: -------------------------------------------------------------------------------- 1 | # Create the TCP socket (5-10 minutes) 2 | 3 | ## Goals 4 | 5 | The goal of this lesson is to create a TCP server on port 6677 that will 6 | listen for connections. 7 | 8 | ## Steps 9 | 10 | 11 | 1. Open your `chat.go` file and find the `main` function. Add the following code to `main`: 12 | 13 | 1. :star2: Create a TCP listener on port `6677`. 14 | hint: Use the `net.Listen` function [docs here](http://golang.org/pkg/net/#Listen) 15 | to create a listener that binds to TCP port `6677`. 16 | 17 | 1. :star2: Create a new instance of the chatroom using `NewChatRoom()` and call 18 | `chatroom.ListenForMessages()` 19 | 20 | 1. :star2: Write some code that will loop listen for accepted connections on port 21 | 6677, and print out the remote address of the connection using `log`. 22 | 23 | hint: Use the `listener.Accept()` function to accept connections and print 24 | the remote address when the connection is joined. You 25 | can use the `RemoteAddr()` function on `net.Conn` to do this. [Docs for the net package and example are here](http://golang.org/pkg/net/) 26 | 27 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/02-socket/chat.go##L70-L94) 28 | 29 | 1. Run the server using `go run chat.go`. It should wait after the "Chat server starting!" message. 30 | You can stop the server using CTRL-C. 31 | 32 | 1. To test your server connection, you can use `nc localhost 6677` or `telnet localhost 6677` (the server will accept the connection, but it won't respond to messages - we still need to write the code for that!). 33 | 34 | [Proceed to Lesson 3](03-data-structures.md) 35 | -------------------------------------------------------------------------------- /lessons/03-data-structures.md: -------------------------------------------------------------------------------- 1 | # Populate `ChatRoom` and `ChatUser` (5-10 minutes) 2 | 3 | ## Goals 4 | 5 | In this lesson, we'll setup the ChatRoom and ChatUser structs. 6 | 7 | Let's review the design of the data structures and some of the requirements: 8 | 9 | 1. There is one `ChatRoom` in the app. 10 | 1. The `ChatRoom` must know about all of the active connections. 11 | 1. Each connection is tracked in a `ChatUser` object. 12 | 1. The `ChatRoom` must be able to receive messages from a single connection, 13 | and replay it back to the other connections. Otherwise, it will not be a very 14 | good chatroom! 15 | 1. When a new connection is established, the `ChatRoom` must be notified 16 | of these new connections. 17 | 18 | ## Steps 19 | 20 | 1. Find the `ChatRoom` struct in `chat.go`. 21 | 22 | ```go 23 | type ChatRoom struct { 24 | // TODO: populate this 25 | } 26 | ``` 27 | 28 | 1. :star2: Add to the `ChatRoom` struct: 29 | * a private member `users` of type `map[string]*ChatUser` 30 | * a private member channel `incoming` of type `chan string` 31 | * a private member channel `joins` of type `chan *ChatUser` 32 | * a private member channel `disconnects` of type `chan string` 33 | 1. :star2: Initialize all of these data structures in the `NewChatRoom()` constructor. 34 | 35 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/03-data-structures/chat.go#L17-L34) 36 | 37 | 1. Find the `ChatUser` struct in `chat.go`. 38 | 39 | ```go 40 | type ChatUser struct { 41 | // TODO: populate this 42 | } 43 | ``` 44 | 45 | 1. :star2: Add to the `ChatUser` struct 46 | * a private member `conn` of type `net.Conn` 47 | * a private member `disconnect` of type `bool` 48 | * a private member `username` of type `string` 49 | * a private member channel `outgoing` of type `chan string` 50 | * a private member `reader` of type `*bufio.Reader` 51 | * a private member `writer` of type `*bufio.Writer` 52 | 1. :star2: Initialize all of these data structures in the `NewChatUser()` constructor. 53 | * `bufio.NewReader/bufio.NewWriter` should accept the `conn` to create the `reader` 54 | and `writer` variables. 55 | * `disconnect` should be initially set to false 56 | 57 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/03-data-structures/chat.go#L52-L74) 58 | 59 | 1. Don't worry too much about what all of these variables are for right now. 60 | As we fill out the rest of the code, you will gain a better understanding of what they will be used for, or you can ask your TA for more information. 61 | 62 | [You can also view the comments on the structs here](code/03-data-structures/chat.go). 63 | 64 | 1. Verify the code still runs when you type `go run chat.go`. 65 | 66 | [Next, proceed to Lesson 4 - where we actually read something from the user socket!](04-login.md) 67 | -------------------------------------------------------------------------------- /lessons/04-login.md: -------------------------------------------------------------------------------- 1 | # Lesson 4 - Login to the chat server! (15-20 minutes) 2 | 3 | ## Goals 4 | 5 | When a user logs in to our chat server, we need to know who they are. 6 | And they will want to know who we are. 7 | 8 | In this exercise, we will: 9 | 10 | * Print a banner when the user connects. 11 | * Implement `ChatUser.Login` (and some related functions) to be able to read 12 | a username from the user. 13 | 14 | Reminder: To test your server, you can use `nc localhost 6677` or `telnet localhost 6677`. 15 | 16 | ## Steps 17 | 18 | 19 | 1. In `chat.go`, view the `main` function and notice how `ChatRoom.Join` is called 20 | on each connection. Find the section of the code `ChatRoom.Join` 21 | 22 | ```go 23 | // This is what we want to modify 24 | func (cr *ChatRoom) Join(conn net.Conn) {} 25 | ``` 26 | 27 | 1. In `ChatRoom.Join`, write the code that does the following: 28 | * :star2: Creates a new `ChatUser` object using `NewChatUser` 29 | * :star2: Calls `ChatUser.Login` on this object (and verify there is no error) 30 | * :star2: Notifies of a new user by putting the newly created `ChatUser` object on the `ChatRoom.joins` channel. 31 | (Don't worry about how this is used for now, I'll show you how we consume it later.) 32 | 33 | [Stuck on any of the steps above? See the solution!](code/04-login/chat.go#L39-L44) 34 | 35 | 1. Great! Now let's start implementing `ChatUser.Login`. First, let's create a 36 | helpful banner that says "Welcome to [foo's] server", where `foo` is your name. 37 | 38 | 1. :star2: Find `ChatUser.Login` and edit it to call `cu.WriteString` with your banner message. 39 | Make sure you also write the newline. 40 | 41 | Here's what it should look like: 42 | ```go 43 | func (cu *ChatUser) Login(chatroom *ChatRoom) error { 44 | // TODO: login the user 45 | cu.WriteString("Welcome to Jen's chat server!\n") 46 | return nil 47 | } 48 | ``` 49 | 50 | 1. Find the function `ChatUser.WriteString` 51 | 52 | ```go 53 | func (cu *ChatUser) WriteString(msg string) error { 54 | // TODO: write a line to the socket 55 | return nil 56 | } 57 | ``` 58 | 59 | 1. :star2: Implement the code in `WriteString` that will write the `msg` to the `writer`. 60 | *Make sure you call `writer.Flush`*. 61 | 62 | [Stuck on any of the steps above? See the solution!](code/04-login/chat.go##L115-L121) 63 | 64 | 1. Start the server using `go run chat.go`. Test this using the `telnet` or `nc` tool 65 | to connect to port `6677`. 66 | 67 | ```bash 68 | $ telnet localhost 6677 ~ 1 ↵ 69 | Trying ::1... 70 | telnet: connect to address ::1: Connection refused 71 | Trying 127.0.0.1... 72 | Connected to localhost. 73 | Escape character is '^]'. 74 | Welcome to Jen's chat server! 75 | ``` 76 | 1. Now we are going to read from the socket. We want to ask for the person's 77 | username and store it on the `ChatUser.username` field. 78 | 79 | 1. First, let's implement the `ChatUser.ReadLine` function. 80 | 81 | Find this code: 82 | 83 | ```go 84 | func (cu *ChatUser) ReadLine() (string, error) { 85 | // TODO: read a line from the socket 86 | return "", nil 87 | } 88 | ``` 89 | 90 | 1. :star2: Implement the code that calls `cu.reader.ReadLine` and returns the results as a string. 91 | 92 | [Stuck on any of the steps above? See the solution!](code/04-login/chat.go#L109-L113) 93 | 94 | 1. Go back to the `ChatUser.Login` function. After you print the banner, write some code that: 95 | 96 | 97 | 1. :star2: Will print to the socket "Please enter your username:"; 98 | 1. :star2: Read the username from the socket using `cu.ReadLine`; 99 | 1. :star2: Store the read username on the `cu.username` field; 100 | 1. :star2: And write back to the socket "Welcome, [cu.username]". 101 | 102 | [Stuck on any of the steps above? See the solution!](code/04-login/chat.go#L91-L107) 103 | 104 | Here is what is should look like when you connect via `telnet localhost 6677`: 105 | 106 | ```bash 107 | $ telnet localhost 6677 ~ 1 ↵ 108 | Trying ::1... 109 | telnet: connect to address ::1: Connection refused 110 | Trying 127.0.0.1... 111 | Connected to localhost. 112 | Escape character is '^]'. 113 | Welcome to Jen's chat server! 114 | Please enter your username: funcuddles 115 | Welcome, funcuddles 116 | ``` 117 | 118 | 1. One more thing! when you call `chatroom.Join` in `main`, what 119 | happens if more than one client tries to connect? 120 | 121 | ```go 122 | for { 123 | conn, _ := listener.Accept() 124 | log.Println("Connection joined.", conn.RemoteAddr()) 125 | chatroom.Join(conn) 126 | } 127 | ``` 128 | 129 | Hint: only one thing can be connecting at a time! 130 | 131 | :star2: How can you fix this? Update `main` accordingly [(View solution)](code/04-login/chat.go##L160). 132 | 133 | [Proceed to Lesson 5](05-handle-joins.md) 134 | -------------------------------------------------------------------------------- /lessons/05-handle-joins.md: -------------------------------------------------------------------------------- 1 | # Lesson 5 - Handling Chatroom Joins (15-20 minutes) 2 | 3 | ## Goals 4 | 5 | Let's start playing some more with concurrency! 6 | 7 | In the previous lesson, we implemented `ChatRoom.Join` which placed a newly created 8 | `ChatUser` object on the `joins` channel. 9 | 10 | What we *want* to do is to be able to track all of the users in the `ChatRoom.users` 11 | map, so we can tell the other users when new users have joined, and also make sure 12 | we can broadcast messages correctly. 13 | 14 | We will do this by implementing the dispatcher in `chatroom.ListenForMessages` 15 | which is designed to handle the messages on the various chatroom channels, 16 | and act accordingly (e.g. add or remove users from internal data structures, broadcast 17 | connection, disconnection, and other messages to all users). This dispatcher 18 | will run in its own goroutine constantly looking for message on its queues. 19 | 20 | So, let's go! 21 | 22 | ## Steps 23 | 24 | 25 | 1. In `chat.go`, remember how the `main` function has some code that calls 26 | `chatroom.ListenForMessages`? 27 | 28 | ```go 29 | // This binds the socket to TCP port 6677 30 | // on all interfaces. 31 | listener, err := net.Listen("tcp", ":6677") 32 | 33 | chatroom.ListenForMessages() 34 | ``` 35 | 36 | Well, the job of `chatroom.ListenForMessages` is to _listen in a loop_ for any messages on the channels in the chatroom 37 | object, and then handle those messages accordingly. 38 | 39 | Let's write some code in this function that will handle a new `ChatUser` object on the 40 | `joins` channel! 41 | 42 | 1. In `ChatRoom.ListenForMessages`, implement the following 43 | * :star2: Create a `for/select` loop, and ensure this loop runs in a goroutine. 44 | * :star2: In the loop, receive a `ChatUser` object on the `cr.joins` channel. Store it in the `users` map by its username. 45 | 46 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/05-handle-joins/chat.go) 47 | 48 | 1. Great! now the object should be stored in the users map. However: it would 49 | be nice if the other people in the chatroom knew when new users joined the chatroom server. 50 | 51 | To do this, we need to implement `ChatRoom.Broadcast`. This function will 52 | pass a message on each of the `ChatUser.outgoing` channels (using `ChatUser.Send`). 53 | 54 | The idea is that each `ChatUser` will be listening on this channel for any outgoing messages via the `ChatUser.WriteOutgoingMessages` 55 | function. This function runs in a loop in a goroutine, and calls `ChatUser.WriteString()` to write that message to the socket when it sees a new message on the `outgoing channel`. 56 | 57 | 1. :star2: Implement `ChatUser.Send` to place a message on the `chatuser.outgoing` channel. 58 | 59 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/05-handle-joins/chat.go) 60 | 61 | 1. :star2: Implement `ChatUser.WriteOutgoingMessages` by: 62 | 63 | 1. :star2: Creating a loop that constantly reads a msg from the `chatuser.outgoing` channel; 64 | 1. :star2: Adding a newline to this msg; 65 | 1. :star2: Write the msg to the socket by calling `chatuser.WriteString`. 66 | 67 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/05-handle-joins/chat.go) 68 | 69 | 4. :star2: Let's start the outgoing message listener! At the end of `ChatUser.Login`, call `cu.WriteOutgoingMessages` loop and make 70 | sure it runs in a goroutine. 71 | 72 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/05-handle-joins/chat.go) 73 | 74 | 1. Now that this is all setup, let's broadcast a message whenever the chatroom 75 | sees a new user login. 76 | 77 | 1. :star2: Modify `ChatRoom.ListenForMessages` to broadcast a message whenever 78 | a new user logs in that says `*** [name] joined the chatroom`: 79 | 80 | ```go 81 | func (cr *ChatRoom) ListenForMessages() { 82 | go func() { 83 | for { 84 | select { 85 | case user := <-cr.joins: 86 | cr.users[user.username] = user 87 | cr.Broadcast("*** " + user.username + " just joined the chatroom") 88 | } 89 | } 90 | }() 91 | } 92 | ``` 93 | 94 | 1. Verify everything works as designed when you connect! You can 95 | try connecting with multiple simultaneous clients to make sure you see the join messages everywhere. 96 | 97 | 98 | ```bash 99 | $ telnet localhost 6677 ~ 1 ↵ 100 | Trying ::1... 101 | telnet: connect to address ::1: Connection refused 102 | Trying 127.0.0.1... 103 | Connected to localhost. 104 | Escape character is '^]'. 105 | Welcome to Jen's chat server! 106 | Please enter your username: funcuddles 107 | Welcome, funcuddles 108 | *** funcuddles just joined the chatroom 109 | ``` 110 | 111 | [Finally! Let's chat! Proceed to Lesson 6](06-broadcast-msgs.md) 112 | -------------------------------------------------------------------------------- /lessons/06-broadcast-msgs.md: -------------------------------------------------------------------------------- 1 | # Lesson 6 - Broadcasting Messages (15-20 minutes) 2 | 3 | ## Goals 4 | 5 | Finally, to the heart of the matter! Chatting! 6 | 7 | To chat with each other, we need to be able to read messages from each 8 | individual `ChatUser`'s socket, then broadcast them back to all of the other user 9 | sockets. 10 | 11 | Here's how it will work: 12 | 13 | * We have a channel `ChatRoom.incoming` where we will put all of the messages 14 | that come in from all of the user sockets. 15 | * Each `ChatUser` object will be modified to have a loop that looks for any new lines 16 | read in from the client, and sends it to `ChatRoom.incoming` channel. 17 | * In our `ChatRoom.ListenForMessages`, we will look for any new messages 18 | on `ChatRoom.incoming` and call `Broadcast` to broadcast it out to all of 19 | the other connected sockets. 20 | 21 | Got it?? Not sure? No worries, Let's go through the steps. 22 | 23 | ## Steps 24 | 25 | 26 | 1. Look for the empty `ChatUser.ReadIncomingMessages` function. Let's implement it so it constantly reads incoming messages from the socket buffer. 27 | 28 | ```go 29 | func (cu *ChatUser) ReadIncomingMessages(chatroom *ChatRoom) { 30 | // TODO: read incoming messages in a loop 31 | } 32 | ``` 33 | 1. In `ChatUser.ReadIncomingMessages`, implement the following: 34 | 35 | 1. :star2: Create a `for` loop, and ensure this loop runs in a goroutine. 36 | 1. :star2: In the loop, call `cu.ReadLine` to read an incoming msg. 37 | 1. :star2: Prepend the incoming msg with the username surrounded by brackets, e.g.: `msg = "[" + cu.username "]" + msg` 38 | 1. :star2: Place this modified message on the `chatroom.incoming` queue. 39 | 40 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/06-broadcast-msgs/chat.go#L100-L109) 41 | 42 | 1. Now, you need to make sure that the `ReadIncomingMessages` goroutine is started 43 | in `ChatUser.Login`. 44 | 45 | 1. :star2: Modify the `Login` function to start the goroutine for `ReadIncomingMessages` after `WriteOutgoingMessages`. 46 | 47 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/06-broadcast-msgs/chat.go#L138) 48 | 49 | 1. Cool. Remember `ChatRoom.ListenForMessages()`? Let's modify it so it sees 50 | the new messages on the `chatroom.incoming` channel. 51 | 52 | 1. :star2: Add a 'case' within the `for/select` loop that reads a msg string from the `incoming` channel. 53 | 1. :star2: Call `ChatRoom.Broadcast` on the read msg. 54 | 55 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/06-broadcast-msgs/chat.go#L40-L41) 56 | 57 | At the end it will look like: 58 | 59 | ```go 60 | func (cr *ChatRoom) ListenForMessages() { 61 | go func() { 62 | for { 63 | select { 64 | case msg := <-cr.incoming: 65 | cr.Broadcast(msg) 66 | case user := <-cr.joins: 67 | cr.users[user.username] = user 68 | cr.Broadcast("*** " + user.username + " just joined the chatroom") 69 | } 70 | } 71 | }() 72 | } 73 | ``` 74 | 75 | 1. Verify everything works as designed. Connect to port 6677, then type a message and 76 | hit enter to send a chat msg to everyone. You can try connecting with 77 | multiple simultaneous clients to test how messages look to other users. 78 | 79 | ```bash 80 | $ nc localhost 6677 81 | Welcome to Jen's chat server! 82 | Please enter your username: funcuddles 83 | Welcome, funcuddles 84 | *** funcuddles just joined the chatroom 85 | hello this is dog 86 | [funcuddles] hello this is dog 87 | ``` 88 | 89 | [Next lesson - Handle user disconnects](07-logouts.md) 90 | -------------------------------------------------------------------------------- /lessons/07-logouts.md: -------------------------------------------------------------------------------- 1 | # Lesson 7 - Notify When Users Disconnect 2 | 3 | ## Goals 4 | 5 | We know when users login, we send and receive chatroom messages.. but how do we know when users disconnect? 6 | 7 | Let's write some code to handle disconnects and display a message 8 | `*** [username] disconnected` when a socket disconnects. 9 | 10 | How will we accomplish this? With more channels, of course! 11 | 12 | 1. Whenever a ChatUser fails to read/write to its connection socket, we will send a message on the `ChatRoom.disconnect` channel to notify the `ListenForMessages` loop 13 | that the socket has been disconnected. 14 | 1. When such a message is received by the `ChatRoom`, remove it from the `users` map and broadcast a message to the rest of the clients saying it's disconnected. 15 | 16 | ## Steps 17 | 18 | 1. Find the `ChatRoom.Logout` function. Add some code that does the following: 19 | 1. :star2: Send the supplied `username` to the `cr.disconnects` channel. 20 | 21 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/07-logouts/chat.go#L59-L61) 22 | 23 | 1. Update the `ListenForMessages` loop to handle messages on `cr.disconnects`: 24 | 1. :star2: Add a `case` statement that reads the `username` from `cr.disconnects` 25 | 1. :star2: if the `username` is in the `cr.users` map: 26 | 1. :star2: call `Close()` on the `ChatUser` object and remove it from the map. 27 | 1. :star2: call `Broadcast` to send a message: _`*** [username] has disconnected`_ 28 | 29 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/07-logouts/chat.go#L45-L54) 30 | 31 | 1. Now that we have `Logout` implemented, let's call `Logout` whenever the ChatUser read and write loops 32 | have errors reading or writing from the socket. 33 | 34 | 1. Update `ChatUser.ReadIncomingMessages` so that it does the following: 35 | 36 | 1. :star2: If cu.ReadLine() returns an error, do not write a message to the `chatroom.incoming` queue. 37 | Instead, call `chatroom.Logout` with the current username. 38 | 1. :star2: Add some logic that checks if `cu.disconnect` is set, then exit the loop. 39 | 40 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/07-logouts/chat.go#L115-L121) 41 | 42 | 1. Update `ChatUser.WriteOutgoingMessages` so that: 43 | 44 | 1. :star2: If cu.ReadLine() returns an error, do not write a message to the queue. Instead, 45 | call `chatroom.Logout` with the current username. 46 | 1. :star2: Add some logic that checks if `cu.disconnect` is set, then exit the loop. 47 | 48 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/07-logouts/chat.go#L133-L141) 49 | 50 | 1. Finally, let's implement `ChatUser.Close`. This will set the `disconnect` variable and close the socket. 51 | 52 | ```go 53 | func (cu *ChatUser) Close() { 54 | // TODO: close the socket 55 | } 56 | ``` 57 | 1. :star2: In `Close()`, set `ChatUser.disconnect = true` 58 | 1. :star2: Close the `ChatUser.conn` 59 | 60 | [Stuck on any of the steps above? Ask your TA, or see the solution!](code/07-logouts/chat.go#L184-L187) 61 | 62 | 1. Now when a user disconnects, you'll see a disconnect message! 63 | ```bash 64 | $ nc localhost 6677 65 | Welcome to Jen's chat server! 66 | Please enter your username: funcuddles 67 | Welcome, funcuddles 68 | *** funcuddles just joined the chatroom 69 | *** bob just joined the chatroom 70 | hello this is dog 71 | [funcuddles] hello this is dog 72 | [bob] bye funcuddles, I gotta go! 73 | *** bob has disconnected 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /lessons/08-extra-credit.md: -------------------------------------------------------------------------------- 1 | 2 | ## Some more ideas! 3 | 4 | 1. Rather than just chat messages, add logic to accept commands from user socket and send back results e.g: typing `/names` to list all of the users in the chatroom 5 | 6 | 2. Add the ability to send and receive private messages to users e.g. `/msg [username] [msg]` to send a private message to a user 7 | 8 | 3. Create a web client to connect to your chatroom 9 | 10 | 4. Support more than 1 chatroom 11 | 12 | -------------------------------------------------------------------------------- /lessons/code/01-setup/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | ) 7 | 8 | type ChatRoom struct { 9 | // TODO: populate this 10 | } 11 | 12 | // NewChatRoom will create a chatroom 13 | func NewChatRoom() *ChatRoom { 14 | // TODO: initialize struct members 15 | return &ChatRoom{} 16 | } 17 | 18 | func (cr *ChatRoom) ListenForMessages() {} 19 | func (cr *ChatRoom) Logout(username string) {} 20 | func (cr *ChatRoom) Join(conn net.Conn) {} 21 | func (cr *ChatRoom) Broadcast(msg string) {} 22 | 23 | type ChatUser struct { 24 | // TODO: populate this 25 | } 26 | 27 | func NewChatUser(conn net.Conn) *ChatUser { 28 | // TODO: initialize chat user 29 | return &ChatUser{} 30 | } 31 | 32 | func (cu *ChatUser) ReadIncomingMessages(chatroom *ChatRoom) { 33 | // TODO: read incoming messages in a loop 34 | } 35 | 36 | func (cu *ChatUser) WriteOutgoingMessages(chatroom *ChatRoom) { 37 | // TODO: wait for outgoing messages in a loop, and write them 38 | } 39 | 40 | func (cu *ChatUser) Login(chatroom *ChatRoom) error { 41 | // TODO: login the user 42 | return nil 43 | } 44 | 45 | func (cu *ChatUser) ReadLine() (string, error) { 46 | // TODO: read a line from the socket 47 | return "", nil 48 | } 49 | 50 | func (cu *ChatUser) WriteString(msg string) error { 51 | // TODO: write a line from the socket 52 | return nil 53 | } 54 | 55 | func (cu *ChatUser) Send(msg string) { 56 | // TODO: put a message on the outgoing messages queue 57 | } 58 | 59 | func (cu *ChatUser) Close() { 60 | // TODO: close the socket 61 | } 62 | 63 | // 64 | // main will create a socket, bind to port 6677, 65 | // and loop while waiting for connections. 66 | // 67 | // When it receives a connection it will pass it to 68 | // `chatroom.Join()`. 69 | // 70 | func main() { 71 | log.Println("Chat server starting!") 72 | // TODO add other logic 73 | } 74 | -------------------------------------------------------------------------------- /lessons/code/02-socket/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | ) 7 | 8 | type ChatRoom struct { 9 | // TODO: populate this 10 | } 11 | 12 | // NewChatRoom will create a chatroom 13 | func NewChatRoom() *ChatRoom { 14 | // TODO: initialize struct members 15 | return &ChatRoom{} 16 | } 17 | 18 | func (cr *ChatRoom) ListenForMessages() {} 19 | func (cr *ChatRoom) Logout(username string) {} 20 | func (cr *ChatRoom) Join(conn net.Conn) {} 21 | func (cr *ChatRoom) Broadcast(msg string) {} 22 | 23 | type ChatUser struct { 24 | // TODO: populate this 25 | } 26 | 27 | func NewChatUser(conn net.Conn) *ChatUser { 28 | // TODO: initialize chat user 29 | return &ChatUser{} 30 | } 31 | 32 | func (cu *ChatUser) ReadIncomingMessages(chatroom *ChatRoom) { 33 | // TODO: read incoming messages in a loop 34 | } 35 | 36 | func (cu *ChatUser) WriteOutgoingMessages(chatroom *ChatRoom) { 37 | // TODO: wait for outgoing messages in a loop, and write them 38 | } 39 | 40 | func (cu *ChatUser) Login(chatroom *ChatRoom) error { 41 | // TODO: login the user 42 | return nil 43 | } 44 | 45 | func (cu *ChatUser) ReadLine() (string, error) { 46 | // TODO: read a line from the socket 47 | return "", nil 48 | } 49 | 50 | func (cu *ChatUser) WriteString(msg string) error { 51 | // TODO: write a line from the socket 52 | return nil 53 | } 54 | 55 | func (cu *ChatUser) Send(msg string) { 56 | // TODO: put a message on the outgoing messages queue 57 | } 58 | 59 | func (cu *ChatUser) Close() { 60 | // TODO: close the socket 61 | } 62 | 63 | // 64 | // main will create a socket, bind to port 6677, 65 | // and loop while waiting for connections. 66 | // 67 | // When it receives a connection it will pass it to 68 | // `chatroom.Join()`. 69 | // 70 | func main() { 71 | log.Println("Chat server starting!") 72 | chatroom := NewChatRoom() 73 | 74 | // This binds the socket to TCP port 6677 75 | // on all interfaces. 76 | listener, err := net.Listen("tcp", ":6677") 77 | 78 | chatroom.ListenForMessages() 79 | 80 | if err != nil { 81 | log.Fatal("Unable to bind to 6677", err) 82 | } 83 | 84 | // Accept a connection, and print out the remote 85 | // address so we know who has connected. 86 | for { 87 | conn, _ := listener.Accept() 88 | log.Println("Connection joined.", conn.RemoteAddr()) 89 | 90 | // run this in a goroutine so more than one thing 91 | // can connect 92 | chatroom.Join(conn) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lessons/code/03-data-structures/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "net" 7 | ) 8 | 9 | // 10 | // ChatRoom is the main chatroom data structure. 11 | // 12 | // `users` contains connected ChatUser connections. 13 | // `incoming` receives incoming messages from ChatUser connections. 14 | // `joins` receives incoming new ChatUser connections. 15 | // `disconnects` receives disconnect notifications. 16 | // 17 | type ChatRoom struct { 18 | users map[string]*ChatUser 19 | incoming chan string 20 | joins chan *ChatUser 21 | disconnects chan string 22 | } 23 | 24 | // 25 | // NewChatRoom() will create a new chatroom. 26 | // 27 | func NewChatRoom() *ChatRoom { 28 | return &ChatRoom{ 29 | users: make(map[string]*ChatUser), 30 | incoming: make(chan string), 31 | joins: make(chan *ChatUser), 32 | disconnects: make(chan string), 33 | } 34 | } 35 | 36 | func (cr *ChatRoom) ListenForMessages() {} 37 | func (cr *ChatRoom) Logout(username string) {} 38 | func (cr *ChatRoom) Join(conn net.Conn) {} 39 | func (cr *ChatRoom) Broadcast(msg string) {} 40 | 41 | // 42 | // ChatUser contains information for the connected user. 43 | // 44 | // `conn` is the socket. 45 | // `disconnect` indicates whether or not the socket is disconnected. 46 | // `username` is the chat username. 47 | // `outgoing` is a channel with all pending outgoing messages 48 | // to be written to the socket. 49 | // `reader` is the buffered socket read stream. 50 | // `writer` is the buffered socket write stream. 51 | // 52 | type ChatUser struct { 53 | conn net.Conn 54 | disconnect bool 55 | username string 56 | outgoing chan string 57 | reader *bufio.Reader 58 | writer *bufio.Writer 59 | } 60 | 61 | func NewChatUser(conn net.Conn) *ChatUser { 62 | writer := bufio.NewWriter(conn) 63 | reader := bufio.NewReader(conn) 64 | 65 | cu := &ChatUser{ 66 | conn: conn, 67 | disconnect: false, 68 | reader: reader, 69 | writer: writer, 70 | outgoing: make(chan string), 71 | } 72 | 73 | return cu 74 | } 75 | 76 | func (cu *ChatUser) ReadIncomingMessages(chatroom *ChatRoom) { 77 | // TODO: read incoming messages in a loop 78 | } 79 | 80 | func (cu *ChatUser) WriteOutgoingMessages(chatroom *ChatRoom) { 81 | // TODO: wait for outgoing messages in a loop, and write them 82 | } 83 | 84 | func (cu *ChatUser) Login(chatroom *ChatRoom) error { 85 | // TODO: login the user 86 | return nil 87 | } 88 | 89 | func (cu *ChatUser) ReadLine() (string, error) { 90 | // TODO: read a line from the socket 91 | return "", nil 92 | } 93 | 94 | func (cu *ChatUser) WriteString(msg string) error { 95 | // TODO: write a line from the socket 96 | return nil 97 | } 98 | 99 | func (cu *ChatUser) Send(msg string) { 100 | // TODO: put a message on the outgoing messages queue 101 | } 102 | 103 | func (cu *ChatUser) Close() { 104 | // TODO: close the socket 105 | } 106 | 107 | // 108 | // main will create a socket, bind to port 6677, 109 | // and loop while waiting for connections. 110 | // 111 | // When it receives a connection it will pass it to 112 | // `chatroom.Join()`. 113 | // 114 | func main() { 115 | log.Println("Chat server starting!") 116 | chatroom := NewChatRoom() 117 | 118 | // This binds the socket to TCP port 6677 119 | // on all interfaces. 120 | listener, err := net.Listen("tcp", ":6677") 121 | 122 | chatroom.ListenForMessages() 123 | 124 | if err != nil { 125 | log.Fatal("Unable to bind to 6677", err) 126 | } 127 | 128 | // Accept a connection, and print out the remote 129 | // address so we know who has connected. 130 | for { 131 | conn, _ := listener.Accept() 132 | log.Println("Connection joined.", conn.RemoteAddr()) 133 | 134 | // run this in a goroutine so more than one thing 135 | // can connect 136 | chatroom.Join(conn) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lessons/code/04-login/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "net" 7 | ) 8 | 9 | // 10 | // ChatRoom is the main chatroom data structure. 11 | // 12 | // `users` contains connected ChatUser connections. 13 | // `incoming` receives incoming messages from ChatUser connections. 14 | // `joins` receives incoming new ChatUser connections. 15 | // `disconnects` receives disconnect notifications. 16 | // 17 | type ChatRoom struct { 18 | users map[string]*ChatUser 19 | incoming chan string 20 | joins chan *ChatUser 21 | disconnects chan string 22 | } 23 | 24 | // 25 | // NewChatRoom() will create a new chatroom. 26 | // 27 | func NewChatRoom() *ChatRoom { 28 | return &ChatRoom{ 29 | users: make(map[string]*ChatUser), 30 | incoming: make(chan string), 31 | joins: make(chan *ChatUser), 32 | disconnects: make(chan string), 33 | } 34 | } 35 | 36 | func (cr *ChatRoom) ListenForMessages() {} 37 | func (cr *ChatRoom) Logout(username string) {} 38 | 39 | func (cr *ChatRoom) Join(conn net.Conn) { 40 | user := NewChatUser(conn) 41 | if user.Login(cr) == nil { 42 | cr.joins <- user 43 | } 44 | } 45 | 46 | func (cr *ChatRoom) Broadcast(msg string) {} 47 | 48 | // 49 | // ChatUser contains information for the connected user. 50 | // 51 | // `conn` is the socket. 52 | // `disconnect` indicates whether or not the socket is disconnected. 53 | // `username` is the chat username. 54 | // `outgoing` is a channel with all pending outgoing messages 55 | // to be written to the socket. 56 | // `reader` is the buffered socket read stream. 57 | // `writer` is the buffered socket write stream. 58 | // 59 | type ChatUser struct { 60 | conn net.Conn 61 | disconnect bool 62 | username string 63 | outgoing chan string 64 | reader *bufio.Reader 65 | writer *bufio.Writer 66 | } 67 | 68 | func NewChatUser(conn net.Conn) *ChatUser { 69 | writer := bufio.NewWriter(conn) 70 | reader := bufio.NewReader(conn) 71 | 72 | cu := &ChatUser{ 73 | conn: conn, 74 | disconnect: false, 75 | reader: reader, 76 | writer: writer, 77 | outgoing: make(chan string), 78 | } 79 | 80 | return cu 81 | } 82 | 83 | func (cu *ChatUser) ReadIncomingMessages(chatroom *ChatRoom) { 84 | // TODO: read incoming messages in a loop 85 | } 86 | 87 | func (cu *ChatUser) WriteOutgoingMessages(chatroom *ChatRoom) { 88 | // TODO: wait for outgoing messages in a loop, and write them 89 | } 90 | 91 | func (cu *ChatUser) Login(chatroom *ChatRoom) error { 92 | // TODO: login the user 93 | var err error 94 | cu.WriteString("Welcome to Jen's chat server!\n") 95 | cu.WriteString("Please enter your username: ") 96 | 97 | cu.username, err = cu.ReadLine() 98 | 99 | if err != nil { 100 | return err 101 | } 102 | 103 | log.Println("User logged in:", cu.username) 104 | 105 | cu.WriteString("Welcome, " + cu.username + "\n") 106 | return nil 107 | } 108 | 109 | func (cu *ChatUser) ReadLine() (string, error) { 110 | bytes, _, err := cu.reader.ReadLine() 111 | str := string(bytes) 112 | return str, err 113 | } 114 | 115 | func (cu *ChatUser) WriteString(msg string) error { 116 | _, err := cu.writer.WriteString(msg) 117 | if err != nil { 118 | return err 119 | } 120 | return cu.writer.Flush() 121 | } 122 | 123 | func (cu *ChatUser) Send(msg string) { 124 | // TODO: put a message on the outgoing messages queue 125 | } 126 | 127 | func (cu *ChatUser) Close() { 128 | // TODO: close the socket 129 | } 130 | 131 | // 132 | // main will create a socket, bind to port 6677, 133 | // and loop while waiting for connections. 134 | // 135 | // When it receives a connection it will pass it to 136 | // `chatroom.Join()`. 137 | // 138 | func main() { 139 | log.Println("Chat server starting!") 140 | chatroom := NewChatRoom() 141 | 142 | // This binds the socket to TCP port 6677 143 | // on all interfaces. 144 | listener, err := net.Listen("tcp", ":6677") 145 | 146 | chatroom.ListenForMessages() 147 | 148 | if err != nil { 149 | log.Fatal("Unable to bind to 6677", err) 150 | } 151 | 152 | // Accept a connection, and print out the remote 153 | // address so we know who has connected. 154 | for { 155 | conn, _ := listener.Accept() 156 | log.Println("Connection joined.", conn.RemoteAddr()) 157 | 158 | // run this in a goroutine so more than one thing 159 | // can connect 160 | go chatroom.Join(conn) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /lessons/code/05-handle-joins/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "net" 7 | ) 8 | 9 | // 10 | // ChatRoom is the main chatroom data structure. 11 | // 12 | // `users` contains connected ChatUser connections. 13 | // `incoming` receives incoming messages from ChatUser connections. 14 | // `joins` receives incoming new ChatUser connections. 15 | // `disconnects` receives disconnect notifications. 16 | // 17 | type ChatRoom struct { 18 | users map[string]*ChatUser 19 | incoming chan string 20 | joins chan *ChatUser 21 | disconnects chan string 22 | } 23 | 24 | // 25 | // NewChatRoom() will create a new chatroom. 26 | // 27 | func NewChatRoom() *ChatRoom { 28 | return &ChatRoom{ 29 | users: make(map[string]*ChatUser), 30 | incoming: make(chan string), 31 | joins: make(chan *ChatUser), 32 | disconnects: make(chan string), 33 | } 34 | } 35 | 36 | func (cr *ChatRoom) ListenForMessages() { 37 | go func() { 38 | for { 39 | select { 40 | case user := <-cr.joins: 41 | cr.users[user.username] = user 42 | cr.Broadcast("*** " + user.username + " just joined the chatroom") 43 | } 44 | } 45 | }() 46 | } 47 | 48 | func (cr *ChatRoom) Logout(username string) {} 49 | 50 | func (cr *ChatRoom) Join(conn net.Conn) { 51 | user := NewChatUser(conn) 52 | if user.Login(cr) == nil { 53 | cr.joins <- user 54 | } 55 | } 56 | 57 | func (cr *ChatRoom) Broadcast(msg string) { 58 | for _, user := range cr.users { 59 | user.Send(msg) 60 | } 61 | } 62 | 63 | // 64 | // ChatUser contains information for the connected user. 65 | // 66 | // `conn` is the socket. 67 | // `disconnect` indicates whether or not the socket is disconnected. 68 | // `username` is the chat username. 69 | // `outgoing` is a channel with all pending outgoing messages 70 | // to be written to the socket. 71 | // `reader` is the buffered socket read stream. 72 | // `writer` is the buffered socket write stream. 73 | // 74 | type ChatUser struct { 75 | conn net.Conn 76 | disconnect bool 77 | username string 78 | outgoing chan string 79 | reader *bufio.Reader 80 | writer *bufio.Writer 81 | } 82 | 83 | func NewChatUser(conn net.Conn) *ChatUser { 84 | writer := bufio.NewWriter(conn) 85 | reader := bufio.NewReader(conn) 86 | 87 | cu := &ChatUser{ 88 | conn: conn, 89 | disconnect: false, 90 | reader: reader, 91 | writer: writer, 92 | outgoing: make(chan string), 93 | } 94 | 95 | return cu 96 | } 97 | 98 | func (cu *ChatUser) ReadIncomingMessages(chatroom *ChatRoom) { 99 | // TODO: read incoming messages in a loop 100 | } 101 | 102 | func (cu *ChatUser) WriteOutgoingMessages(chatroom *ChatRoom) { 103 | go func() { 104 | for { 105 | data := <-cu.outgoing 106 | data = data + "\n" 107 | cu.WriteString(data) 108 | } 109 | }() 110 | } 111 | 112 | func (cu *ChatUser) Login(chatroom *ChatRoom) error { 113 | // TODO: login the user 114 | var err error 115 | cu.WriteString("Welcome to Jen's chat server!\n") 116 | cu.WriteString("Please enter your username: ") 117 | 118 | cu.username, err = cu.ReadLine() 119 | 120 | if err != nil { 121 | return err 122 | } 123 | 124 | log.Println("User logged in:", cu.username) 125 | 126 | cu.WriteString("Welcome, " + cu.username + "\n") 127 | 128 | cu.WriteOutgoingMessages(chatroom) 129 | return nil 130 | } 131 | 132 | func (cu *ChatUser) ReadLine() (string, error) { 133 | bytes, _, err := cu.reader.ReadLine() 134 | str := string(bytes) 135 | return str, err 136 | } 137 | 138 | func (cu *ChatUser) WriteString(msg string) error { 139 | _, err := cu.writer.WriteString(msg) 140 | if err != nil { 141 | return err 142 | } 143 | return cu.writer.Flush() 144 | } 145 | 146 | func (cu *ChatUser) Send(msg string) { 147 | cu.outgoing <- msg 148 | } 149 | 150 | func (cu *ChatUser) Close() { 151 | // TODO: close the socket 152 | } 153 | 154 | // 155 | // main will create a socket, bind to port 6677, 156 | // and loop while waiting for connections. 157 | // 158 | // When it receives a connection it will pass it to 159 | // `chatroom.Join()`. 160 | // 161 | func main() { 162 | log.Println("Chat server starting!") 163 | chatroom := NewChatRoom() 164 | 165 | // This binds the socket to TCP port 6677 166 | // on all interfaces. 167 | listener, err := net.Listen("tcp", ":6677") 168 | 169 | chatroom.ListenForMessages() 170 | 171 | if err != nil { 172 | log.Fatal("Unable to bind to 6677", err) 173 | } 174 | 175 | // Accept a connection, and print out the remote 176 | // address so we know who has connected. 177 | for { 178 | conn, _ := listener.Accept() 179 | log.Println("Connection joined.", conn.RemoteAddr()) 180 | 181 | // run this in a goroutine so more than one thing 182 | // can connect 183 | go chatroom.Join(conn) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lessons/code/06-broadcast-msgs/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "net" 7 | ) 8 | 9 | // 10 | // ChatRoom is the main chatroom data structure. 11 | // 12 | // `users` contains connected ChatUser connections. 13 | // `incoming` receives incoming messages from ChatUser connections. 14 | // `joins` receives incoming new ChatUser connections. 15 | // `disconnects` receives disconnect notifications. 16 | // 17 | type ChatRoom struct { 18 | users map[string]*ChatUser 19 | incoming chan string 20 | joins chan *ChatUser 21 | disconnects chan string 22 | } 23 | 24 | // 25 | // NewChatRoom() will create a new chatroom. 26 | // 27 | func NewChatRoom() *ChatRoom { 28 | return &ChatRoom{ 29 | users: make(map[string]*ChatUser), 30 | incoming: make(chan string), 31 | joins: make(chan *ChatUser), 32 | disconnects: make(chan string), 33 | } 34 | } 35 | 36 | func (cr *ChatRoom) ListenForMessages() { 37 | go func() { 38 | for { 39 | select { 40 | case msg := <-cr.incoming: 41 | cr.Broadcast(msg) 42 | case user := <-cr.joins: 43 | cr.users[user.username] = user 44 | cr.Broadcast("*** " + user.username + " just joined the chatroom") 45 | } 46 | } 47 | }() 48 | } 49 | 50 | func (cr *ChatRoom) Logout(username string) {} 51 | 52 | func (cr *ChatRoom) Join(conn net.Conn) { 53 | user := NewChatUser(conn) 54 | if user.Login(cr) == nil { 55 | cr.joins <- user 56 | } 57 | } 58 | 59 | func (cr *ChatRoom) Broadcast(msg string) { 60 | for _, user := range cr.users { 61 | user.Send(msg) 62 | } 63 | } 64 | 65 | // 66 | // ChatUser contains information for the connected user. 67 | // 68 | // `conn` is the socket. 69 | // `disconnect` indicates whether or not the socket is disconnected. 70 | // `username` is the chat username. 71 | // `outgoing` is a channel with all pending outgoing messages 72 | // to be written to the socket. 73 | // `reader` is the buffered socket read stream. 74 | // `writer` is the buffered socket write stream. 75 | // 76 | type ChatUser struct { 77 | conn net.Conn 78 | disconnect bool 79 | username string 80 | outgoing chan string 81 | reader *bufio.Reader 82 | writer *bufio.Writer 83 | } 84 | 85 | func NewChatUser(conn net.Conn) *ChatUser { 86 | writer := bufio.NewWriter(conn) 87 | reader := bufio.NewReader(conn) 88 | 89 | cu := &ChatUser{ 90 | conn: conn, 91 | disconnect: false, 92 | reader: reader, 93 | writer: writer, 94 | outgoing: make(chan string), 95 | } 96 | 97 | return cu 98 | } 99 | 100 | func (cu *ChatUser) ReadIncomingMessages(chatroom *ChatRoom) { 101 | go func() { 102 | for { 103 | line, _ := cu.ReadLine() 104 | if line != "" { 105 | chatroom.incoming <- ("[" + cu.username + "] " + line) 106 | } 107 | } 108 | }() 109 | } 110 | 111 | func (cu *ChatUser) WriteOutgoingMessages(chatroom *ChatRoom) { 112 | go func() { 113 | for { 114 | data := <-cu.outgoing 115 | data = data + "\n" 116 | cu.WriteString(data) 117 | } 118 | }() 119 | } 120 | 121 | func (cu *ChatUser) Login(chatroom *ChatRoom) error { 122 | // TODO: login the user 123 | var err error 124 | cu.WriteString("Welcome to Jen's chat server!\n") 125 | cu.WriteString("Please enter your username: ") 126 | 127 | cu.username, err = cu.ReadLine() 128 | 129 | if err != nil { 130 | return err 131 | } 132 | 133 | log.Println("User logged in:", cu.username) 134 | 135 | cu.WriteString("Welcome, " + cu.username + "\n") 136 | 137 | cu.WriteOutgoingMessages(chatroom) 138 | cu.ReadIncomingMessages(chatroom) 139 | return nil 140 | } 141 | 142 | func (cu *ChatUser) ReadLine() (string, error) { 143 | bytes, _, err := cu.reader.ReadLine() 144 | str := string(bytes) 145 | return str, err 146 | } 147 | 148 | func (cu *ChatUser) WriteString(msg string) error { 149 | _, err := cu.writer.WriteString(msg) 150 | if err != nil { 151 | return err 152 | } 153 | return cu.writer.Flush() 154 | } 155 | 156 | func (cu *ChatUser) Send(msg string) { 157 | cu.outgoing <- msg 158 | } 159 | 160 | func (cu *ChatUser) Close() { 161 | // TODO: close the socket 162 | } 163 | 164 | // 165 | // main will create a socket, bind to port 6677, 166 | // and loop while waiting for connections. 167 | // 168 | // When it receives a connection it will pass it to 169 | // `chatroom.Join()`. 170 | // 171 | func main() { 172 | log.Println("Chat server starting!") 173 | chatroom := NewChatRoom() 174 | 175 | // This binds the socket to TCP port 6677 176 | // on all interfaces. 177 | listener, err := net.Listen("tcp", ":6677") 178 | 179 | chatroom.ListenForMessages() 180 | 181 | if err != nil { 182 | log.Fatal("Unable to bind to 6677", err) 183 | } 184 | 185 | // Accept a connection, and print out the remote 186 | // address so we know who has connected. 187 | for { 188 | conn, _ := listener.Accept() 189 | log.Println("Connection joined.", conn.RemoteAddr()) 190 | 191 | // run this in a goroutine so more than one thing 192 | // can connect 193 | go chatroom.Join(conn) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lessons/code/07-logouts/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "net" 7 | ) 8 | 9 | // 10 | // ChatRoom is the main chatroom data structure. 11 | // 12 | // `users` contains connected ChatUser connections. 13 | // `incoming` receives incoming messages from ChatUser connections. 14 | // `joins` receives incoming new ChatUser connections. 15 | // `disconnects` receives disconnect notifications. 16 | // 17 | type ChatRoom struct { 18 | users map[string]*ChatUser 19 | incoming chan string 20 | joins chan *ChatUser 21 | disconnects chan string 22 | } 23 | 24 | // 25 | // NewChatRoom() will create a new chatroom. 26 | // 27 | func NewChatRoom() *ChatRoom { 28 | return &ChatRoom{ 29 | users: make(map[string]*ChatUser), 30 | incoming: make(chan string), 31 | joins: make(chan *ChatUser), 32 | disconnects: make(chan string), 33 | } 34 | } 35 | 36 | func (cr *ChatRoom) ListenForMessages() { 37 | go func() { 38 | for { 39 | select { 40 | case msg := <-cr.incoming: 41 | cr.Broadcast(msg) 42 | case user := <-cr.joins: 43 | cr.users[user.username] = user 44 | cr.Broadcast("*** " + user.username + " just joined the chatroom") 45 | case username := <-cr.disconnects: 46 | // perform the logout by removing 47 | // the username from the `users` map if it exists. 48 | // Also call Close() on the user object. 49 | if cr.users[username] != nil { 50 | cr.users[username].Close() 51 | delete(cr.users, username) 52 | cr.Broadcast("*** " + username + " has disconnected") 53 | } 54 | } 55 | } 56 | }() 57 | } 58 | 59 | func (cr *ChatRoom) Logout(username string) { 60 | cr.disconnects <- username 61 | } 62 | 63 | func (cr *ChatRoom) Join(conn net.Conn) { 64 | user := NewChatUser(conn) 65 | if user.Login(cr) == nil { 66 | cr.joins <- user 67 | } 68 | } 69 | 70 | func (cr *ChatRoom) Broadcast(msg string) { 71 | for _, user := range cr.users { 72 | user.Send(msg) 73 | } 74 | } 75 | 76 | // 77 | // ChatUser contains information for the connected user. 78 | // 79 | // `conn` is the socket. 80 | // `disconnect` indicates whether or not the socket is disconnected. 81 | // `username` is the chat username. 82 | // `outgoing` is a channel with all pending outgoing messages 83 | // to be written to the socket. 84 | // `reader` is the buffered socket read stream. 85 | // `writer` is the buffered socket write stream. 86 | // 87 | type ChatUser struct { 88 | conn net.Conn 89 | disconnect bool 90 | username string 91 | outgoing chan string 92 | reader *bufio.Reader 93 | writer *bufio.Writer 94 | } 95 | 96 | func NewChatUser(conn net.Conn) *ChatUser { 97 | writer := bufio.NewWriter(conn) 98 | reader := bufio.NewReader(conn) 99 | 100 | cu := &ChatUser{ 101 | conn: conn, 102 | disconnect: false, 103 | reader: reader, 104 | writer: writer, 105 | outgoing: make(chan string), 106 | } 107 | 108 | return cu 109 | } 110 | 111 | func (cu *ChatUser) ReadIncomingMessages(chatroom *ChatRoom) { 112 | go func() { 113 | for { 114 | line, err := cu.ReadLine() 115 | if cu.disconnect { 116 | break 117 | } 118 | if err != nil { 119 | chatroom.Logout(cu.username) 120 | break 121 | } 122 | if line != "" { 123 | chatroom.incoming <- ("[" + cu.username + "] " + line) 124 | } 125 | } 126 | }() 127 | } 128 | 129 | func (cu *ChatUser) WriteOutgoingMessages(chatroom *ChatRoom) { 130 | go func() { 131 | for { 132 | data := <-cu.outgoing 133 | if cu.disconnect { 134 | break 135 | } 136 | data = data + "\n" 137 | err := cu.WriteString(data) 138 | if err != nil { 139 | chatroom.Logout(cu.username) 140 | break 141 | } 142 | } 143 | }() 144 | } 145 | 146 | func (cu *ChatUser) Login(chatroom *ChatRoom) error { 147 | var err error 148 | cu.WriteString("Welcome to Jen's chat server!\n") 149 | cu.WriteString("Please enter your username: ") 150 | 151 | cu.username, err = cu.ReadLine() 152 | 153 | if err != nil { 154 | return err 155 | } 156 | 157 | log.Println("User logged in:", cu.username) 158 | 159 | cu.WriteString("Welcome, " + cu.username + "\n") 160 | 161 | cu.WriteOutgoingMessages(chatroom) 162 | cu.ReadIncomingMessages(chatroom) 163 | return nil 164 | } 165 | 166 | func (cu *ChatUser) ReadLine() (string, error) { 167 | bytes, _, err := cu.reader.ReadLine() 168 | str := string(bytes) 169 | return str, err 170 | } 171 | 172 | func (cu *ChatUser) WriteString(msg string) error { 173 | _, err := cu.writer.WriteString(msg) 174 | if err != nil { 175 | return err 176 | } 177 | return cu.writer.Flush() 178 | } 179 | 180 | func (cu *ChatUser) Send(msg string) { 181 | cu.outgoing <- msg 182 | } 183 | 184 | func (cu *ChatUser) Close() { 185 | cu.disconnect = true 186 | cu.conn.Close() 187 | } 188 | 189 | // 190 | // main will create a socket, bind to port 6677, 191 | // and loop while waiting for connections. 192 | // 193 | // When it receives a connection it will pass it to 194 | // `chatroom.Join()`. 195 | // 196 | func main() { 197 | log.Println("Chat server starting!") 198 | chatroom := NewChatRoom() 199 | 200 | // This binds the socket to TCP port 6677 201 | // on all interfaces. 202 | listener, err := net.Listen("tcp", ":6677") 203 | 204 | chatroom.ListenForMessages() 205 | 206 | if err != nil { 207 | log.Fatal("Unable to bind to 6677", err) 208 | } 209 | 210 | // Accept a connection, and print out the remote 211 | // address so we know who has connected. 212 | for { 213 | conn, _ := listener.Accept() 214 | log.Println("Connection joined.", conn.RemoteAddr()) 215 | 216 | // run this in a goroutine so more than one thing 217 | // can connect 218 | go chatroom.Join(conn) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /lessons/code/final/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "net" 7 | ) 8 | 9 | // 10 | // This is the main chatroom data structure. 11 | // 12 | // `users` contains connected ChatUser connections. 13 | // `incoming` receives incoming messages from ChatUser connections. 14 | // `joins` receives incoming new ChatUser connections. 15 | // `disconnects` receives disconnect notifications. 16 | // 17 | type ChatRoom struct { 18 | users map[string]*ChatUser 19 | incoming chan string 20 | joins chan *ChatUser 21 | disconnects chan string 22 | } 23 | 24 | // 25 | // NewChatRoom() will create a new chatroom. 26 | // 27 | func NewChatRoom() *ChatRoom { 28 | return &ChatRoom{ 29 | users: make(map[string]*ChatUser), 30 | incoming: make(chan string), 31 | joins: make(chan *ChatUser), 32 | disconnects: make(chan string), 33 | } 34 | } 35 | 36 | // 37 | // ListenForMessages will listen for chatroom messages. 38 | // 39 | // It runs in a goroutine and listens in a loop on the following channels 40 | // 41 | // - cr.joins <- join requests from incoming clients 42 | // - cr.incoming <- messages from incoming clients 43 | // - cr.disconnects <- disconnect requests from incoming clients 44 | // 45 | func (cr *ChatRoom) ListenForMessages() { 46 | go func() { 47 | for { 48 | select { 49 | case data := <-cr.incoming: 50 | cr.Broadcast(data) 51 | case user := <-cr.joins: 52 | cr.users[user.username] = user 53 | cr.Broadcast("*** " + user.username + " just joined the chatroom") 54 | case username := <-cr.disconnects: 55 | // perform the logout by removing 56 | // the username from the `users` map if it exists. 57 | // Also call Close() on the user object. 58 | if cr.users[username] != nil { 59 | cr.users[username].Close() 60 | delete(cr.users, username) 61 | cr.Broadcast("*** " + username + " has disconnected") 62 | } 63 | } 64 | } 65 | }() 66 | } 67 | 68 | func (cr *ChatRoom) Logout(username string) { 69 | cr.disconnects <- username 70 | } 71 | 72 | func (cr *ChatRoom) Join(conn net.Conn) { 73 | user := NewChatUser(conn) 74 | if user.Login(cr) == nil { 75 | cr.joins <- user 76 | } 77 | } 78 | 79 | func (cr *ChatRoom) Broadcast(msg string) { 80 | for _, user := range cr.users { 81 | user.Send(msg) 82 | } 83 | } 84 | 85 | // 86 | // ChatUser contains information for the connected user. 87 | // 88 | // `conn` is the socket. 89 | // `disconnect` indicates whether or not the socket is disconnected. 90 | // `username` is the chat username. 91 | // `outgoing` is a channel with all pending outgoing messages 92 | // to be written to the socket. 93 | // `reader` is the buffered socket read stream. 94 | // `writer` is the buffered socket write stream. 95 | // 96 | type ChatUser struct { 97 | conn net.Conn 98 | disconnect bool 99 | username string 100 | outgoing chan string 101 | reader *bufio.Reader 102 | writer *bufio.Writer 103 | } 104 | 105 | func NewChatUser(conn net.Conn) *ChatUser { 106 | writer := bufio.NewWriter(conn) 107 | reader := bufio.NewReader(conn) 108 | 109 | cu := &ChatUser{ 110 | conn: conn, 111 | disconnect: false, 112 | reader: reader, 113 | writer: writer, 114 | outgoing: make(chan string), 115 | } 116 | 117 | return cu 118 | } 119 | 120 | func (cu *ChatUser) ReadIncomingMessages(chatroom *ChatRoom) { 121 | go func() { 122 | for { 123 | line, err := cu.ReadLine() 124 | if cu.disconnect { 125 | break 126 | } 127 | if err != nil { 128 | chatroom.Logout(cu.username) 129 | break 130 | } 131 | if line != "" { 132 | chatroom.incoming <- ("[" + cu.username + "] " + line) 133 | } 134 | } 135 | }() 136 | } 137 | 138 | func (cu *ChatUser) WriteOutgoingMessages(chatroom *ChatRoom) { 139 | go func() { 140 | for { 141 | data := <-cu.outgoing 142 | if cu.disconnect { 143 | break 144 | } 145 | data = data + "\n" 146 | err := cu.WriteString(data) 147 | if err != nil { 148 | chatroom.Logout(cu.username) 149 | break 150 | } 151 | } 152 | }() 153 | } 154 | 155 | func (cu *ChatUser) Login(chatroom *ChatRoom) error { 156 | var err error 157 | cu.WriteString("Welcome to Jen's chat server!\n") 158 | cu.WriteString("Please enter your username: ") 159 | 160 | cu.username, err = cu.ReadLine() 161 | 162 | if err != nil { 163 | return err 164 | } 165 | 166 | log.Println("User logged in:", cu.username) 167 | 168 | cu.WriteString("Welcome, " + cu.username + "\n") 169 | 170 | cu.ReadIncomingMessages(chatroom) 171 | cu.WriteOutgoingMessages(chatroom) 172 | return nil 173 | } 174 | 175 | func (cu *ChatUser) ReadLine() (string, error) { 176 | bytes, _, err := cu.reader.ReadLine() 177 | str := string(bytes) 178 | return str, err 179 | } 180 | 181 | func (cu *ChatUser) WriteString(msg string) error { 182 | _, err := cu.writer.WriteString(msg) 183 | if err != nil { 184 | return err 185 | } 186 | return cu.writer.Flush() 187 | } 188 | 189 | func (cu *ChatUser) Send(msg string) { 190 | cu.outgoing <- msg 191 | } 192 | 193 | func (cu *ChatUser) Close() { 194 | cu.disconnect = true 195 | cu.conn.Close() 196 | } 197 | 198 | func main() { 199 | log.Println("Chat server starting!") 200 | chatroom := NewChatRoom() 201 | 202 | // This binds the socket to TCP port 6677 203 | // on all interfaces. 204 | listener, err := net.Listen("tcp", ":6677") 205 | 206 | chatroom.ListenForMessages() 207 | 208 | if err != nil { 209 | log.Fatal("Unable to bind to 6677", err) 210 | } 211 | 212 | // Accept a connection, and print out the remote 213 | // address so we know who has connected. 214 | for { 215 | conn, _ := listener.Accept() 216 | log.Println("Connection joined.", conn.RemoteAddr()) 217 | go chatroom.Join(conn) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /vagrant/install-go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install golang 4 | 5 | bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer) 6 | 7 | [[ -s "$HOME/.gvm/scripts/gvm" ]] && source "$HOME/.gvm/scripts/gvm" 8 | 9 | gvm install go1.4 10 | 11 | echo "gvm use go1.4" >> ~/.bashrc 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /vagrant/provision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo apt-get install -y git bison 4 | 5 | # install golang 6 | sudo -i -u vagrant /opt/golang-lab-chat/vagrant/install-go.sh 7 | --------------------------------------------------------------------------------