Building an HTML and JavaScript chat client
In order for the users of our chat application to interact with the server and therefore other users, we need to write some client-side code that makes use of the web sockets found in modern browsers. We are already delivering HTML content via the template when users hit the root of our application, so we can enhance that.
Update the chat.html
file in the templates
folder with the following markup:
<html> <head> <title>Chat</title> <style> input { display: block; } ul { list-style: none; } </style> </head> <body> <ul id="messages"></ul> <form id="chatbox"> <textarea></textarea> <input type="submit" value="Send" /> </form> </body> </html>
The preceding HTML will render a simple web form on the page containing a text area and a Send button this is how our users will submit messages to the server. The messages
element in the preceding code will contain the text of the chat messages so that all the users can see what is being said. Next, we need to add some JavaScript to add some functionality to our page. Underneath the form
tag, above the closing </body>
tag, insert the following code:
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"> </script> <script> $(function(){ var socket = null; var msgBox = $("#chatbox textarea"); var messages = $("#messages"); $("#chatbox").submit(function(){ if (!msgBox.val()) return false; if (!socket) { alert("Error: There is no socket connection."); return false; } socket.send(msgBox.val()); msgBox.val(""); return false; }); if (!window["WebSocket"]) { alert("Error: Your browser does not support web sockets.") } else { socket = new WebSocket("ws://localhost:8080/room"); socket.onclose = function() { alert("Connection has been closed."); } socket.onmessage = function(e) { messages.append($("<li>").text(e.data)); } } }); </script>
The socket = new WebSocket("ws://localhost:8080/room")
line is where we open the socket and add event handlers for two key events: onclose
and onmessage
. When the socket receives a message, we use jQuery to append the message to the list element and thus present it to the user.
Submitting the HTML form triggers a call to socket.send
, which is how we send messages to the server.
Build and run the program again to ensure the templates recompile so these changes are represented.
Navigate to http://localhost:8080/
in two separate browsers (or two tabs of the same browser) and play with the application. You will notice that messages sent from one client appear instantly in the other clients:
Getting more out of templates
Currently, we are using templates to deliver static HTML, which is nice because it gives us a clean and simple way to separate the client code from the server code. However, templates are actually much more powerful, and we are going to tweak our application to make some more realistic use of them.
The host address of our application (:8080
) is hardcoded at two places at the moment. The first instance is in main.go
where we start the web server:
if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal("ListenAndServe:", err) }
The second time it is hardcoded in the JavaScript when we open the socket:
socket = new WebSocket("ws://localhost:8080/room");
Our chat application is pretty stubborn if it insists on only running locally on port 8080
, so we are going to use command-line flags to make it configurable and then use the injection capabilities of templates to make sure our JavaScript knows the right host.
Update your main
function in main.go
:
func main() { var addr = flag.String("addr", ":8080", "The addr of the application.") flag.Parse() // parse the flags r := newRoom() http.Handle("/", &templateHandler{filename: "chat.html"}) http.Handle("/room", r) // get the room going go r.run() // start the web server log.Println("Starting web server on", *addr) if err := http.ListenAndServe(*addr, nil); err != nil { log.Fatal("ListenAndServe:", err) } }
You will need to import the flag
package in order for this code to build. The definition for the addr
variable sets up our flag as a string that defaults to :8080
(with a short description of what the value is intended for). We must call flag.Parse()
that parses the arguments and extracts the appropriate information. Then, we can reference the value of the host flag by using *addr
.
Note
The call to flag.String
returns a type of *string
, which is to say it returns the address of a string variable where the value of the flag is stored. To get the value itself (and not the address of the value), we must use the pointer indirection operator, *
.
We also added a log.Println
call to output the address in the terminal so we can be sure that our changes have taken effect.
We are going to modify the templateHandler
type we wrote so that it passes the details of the request as data into the template's Execute
method. In main.go
, update the ServeHTTP
function to pass the request r
as the data
argument to the Execute
method:
func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t.once.Do(func() { t.templ = template.Must(template.ParseFiles(filepath.Join("templates", t.filename))) }) t.templ.Execute(w, r) }
This tells the template to render itself using data that can be extracted from http.Request
, which happens to include the host address that we need.
To use the Host
value of http.Request
, we can then make use of the special template syntax that allows us to inject data. Update the line where we create our socket in the chat.html
file:
socket = new WebSocket("ws://{{.Host}}/room");
The double curly braces represent an annotation and the way we tell our template source to inject data. The {{.Host}}
is essentially equivalent of telling it to replace the annotation with the value from request.Host
(since we passed the request r
object in as data).
Tip
We have only scratched the surface of the power of the templates built into Go's standard library. The text/template
package documentation is a great place to learn more about what you can achieve. You can find more about it at http://golang.org/pkg/text/template.
Rebuild and run the chat program again, but this time notice that the chatting operations no longer produce an error, whichever host we specify:
go build -o chat ./chat -addr=":3000"
View the source of the page in the browser and notice that {{.Host}}
has been replaced with the actual host of the application. Valid hosts aren't just port numbers; you can also specify the IP addresses or other hostnames provided they are allowed in your environment, for example, -addr="192.168.0.1:3000"
.