WebSockets
In this chapter we'll learn how to use WebSockets to improve our application.
We have two applications in our poker codebase
- Command line app. Prompts the user to enter the number of players in a game. From then on informs the players of what the "blind bet" value is, which increases over time. At any point a user can enter
"{Playername} wins"
to finish the game and record the victor in a store. - Web app. Allows users to record winners of games and displays a league table. Shares the same store as the command line app.
The product owner is thrilled with the command line application but would prefer it if we could bring that functionality to the browser. She imagines a web page with a text box that allows the user to enter the number of players and when they submit the form the page displays the blind value and automatically updates it when appropriate. Like the command line application the user can declare the winner and it'll get saved in the database.
On the face of it, it sounds quite simple but as always we must emphasise taking an iterative approach to writing software.
First we will need to serve HTML. So far all of our HTTP endpoints have returned either plaintext or JSON. We could use the same techniques we know (as they're all ultimately strings) but we can also use the html/template package for a cleaner solution.
We also need to be able to asynchronously send messages to the user saying
The blind is now *y*
without having to refresh the browser. We can use WebSockets to facilitate this.WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection
Given we are taking on a number of techniques it's even more important we do the smallest amount of useful work possible first and then iterate.
For that reason the first thing we'll do is create a web page with a form for the user to record a winner. Rather than using a plain form, we will use WebSockets to send that data to our server for it to record.
After that we'll work on the blind alerts by which point we will have a bit of infrastructure code set up.
There will be some JavaScript written to do this but I won't go in to writing tests.
It is of course possible but for the sake of brevity I won't be including any explanations for it.
Sorry folks. Lobby O'Reilly to pay me to make a "Learn JavaScript with tests".
First thing we need to do is serve up some HTML to users when they hit
/game
.Here's a reminder of the pertinent code in our web server
type PlayerServer struct {
store PlayerStore
http.Handler
}
const jsonContentType = "application/json"
func NewPlayerServer(store PlayerStore) *PlayerServer {
p := new(PlayerServer)
p.store = store
router := http.NewServeMux()
router.Handle("/league", http.HandlerFunc(p.leagueHandler))
router.Handle("/players/", http.HandlerFunc(p.playersHandler))
p.Handler = router
return p
}
The easiest thing we can do for now is check when we
GET /game
that we get a 200
.func TestGame(t *testing.T) {
t.Run("GET /game returns 200", func(t *testing.T) {
server := NewPlayerServer(&StubPlayerStore{})
request, _ := http.NewRequest(http.MethodGet, "/game", nil)
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertStatus(t, response.Code, http.StatusOK)
})
}
--- FAIL: TestGame (0.00s)
=== RUN TestGame/GET_/game_returns_200
--- FAIL: TestGame/GET_/game_returns_200 (0.00s)
server_test.go:109: did not get correct status, got 404, want 200
Our server has a router setup so it's relatively easy to fix.
To our router add
router.Handle("/game", http.HandlerFunc(p.game))
And then write the
game
methodfunc (p *PlayerServer) game(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
The server code is already fine due to us slotting in more code into the existing well-factored code very easily.
We can tidy up the test a little by adding a test helper function
newGameRequest
to make the request to /game
. Try writing this yourself.func TestGame(t *testing.T) {
t.Run("GET /game returns 200", func(t *testing.T) {
server := NewPlayerServer(&StubPlayerStore{})
request := newGameRequest()
response := httptest.NewRecorder()
server.ServeHTTP(response, request)
assertStatus(t, response, http.StatusOK)
})
}
You'll also notice I changed
assertStatus
to accept response
rather than response.Code
as I feel it reads better.Now we need to make the endpoint return some HTML, here it is
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Let's play poker</title>
</head>
<body>
<section id="game">
<div id="declare-winner">
<label for="winner">Winner</label>
<input type="text" id="winner"/>
<button id="winner-button">Declare winner</button>
</div>
</section>
</body>
<script type="application/javascript">
const submitWinnerButton = document.getElementById('winner-button')
const winnerInput = document.getElementById('winner')
if (window['WebSocket']) {
const conn = new WebSocket('ws://' + document.location.host + '/ws')
submitWinnerButton.onclick = event => {
conn.send(winnerInput.value)
}
}
</script>
</html>
We have a very simple web page
- A text input for the user to enter the winner into
- A button they can click to declare the winner.
- Some JavaScript to open a WebSocket connection to our server and handle the submit button being pressed
WebSocket
is built into most modern browsers so we don't need to worry about bringing in any libraries. The web page won't work for older browsers, but we're ok with that for this scenario.There are a few ways. As has been emphasised throughout the book, it is important that the tests you write have sufficient value to justify the cost.
- 1.Write a browser based test, using something like Selenium. These tests are the most "realistic" of all approaches because they start an actual web browser of some kind and simulates a user interacting with it. These tests can give you a lot of confidence your system works but are more difficult to write than unit tests and much slower to run. For the purposes of our product this is overkill.
- 2.Do an exact string match. This can be ok but these kind of tests end up being very brittle. The moment someone changes the markup you will have a test failing when in practice nothing has actually broken.
- 3.Check we call the correct template. We will be using a templating library from the standard lib to serve the HTML (discussed shortly) and we could inject in the thing to generate the HTML and spy on its call to check we're doing it right. This would have an impact on our code's design but doesn't actually test a great deal; other than we're calling it with the correct template file. Given we will only have the one template in our project the chance of failure here seems low.
So in the book "Learn Go with Tests" for the first time, we're not going to write a test.
Put the markup in a file called
game.html
Next change the endpoint we just wrote to the following
func (p *PlayerServer) game(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("game.html")
if err != nil {
http.Error(w, fmt.Sprintf("problem loading template %s", err.Error()), http.StatusInternalServerError)
return
}
tmpl.Execute(w, nil)
}
html/template
is a Go package for creating HTML. In our case we call template.ParseFiles
, giving the path of our html file. Assuming there is no error you can then Execute
the template, which writes it to an io.Writer
. In our case we want it to Write
to the internet, so we give it our http.ResponseWriter
.As we have not written a test, it would be prudent to manually test our web server just to make sure things are working as we'd hope. Go to
cmd/webserver
and run the main.go
file. Visit http://localhost:5000/game
.You should have got an error about not being able to find the template. You can either change the path to be relative to your folder, or you can have a copy of the
game.html
in the cmd/webserver
directory. I chose to create a symlink (ln -s ../../game.html game.html
) to the file inside the root of the project so if I make changes they are reflected when running the server.If you make this change and run again you should see our UI.
Now we need to test that when we get a string over a WebSocket connection to our server that we declare it as a winner of a game.
For the first time we are going to use an external library so that we can work with WebSockets.
Run
go get github.com/gorilla/websocket
This will fetch the code for the excellent Gorilla WebSocket library. Now we can update our tests for our new requirement.
t.Run("when we get a message over a websocket it is a winner of a game", func(t *testing.T) {
store := &StubPlayerStore{}
winner := "Ruth"
server := httptest.NewServer(NewPlayerServer(store))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws"
ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("could not open a ws connection on %s %v", wsURL, err)
}
defer ws.Close()
if err := ws.WriteMessage(websocket.TextMessage, []byte(winner)); err != nil {
t.Fatalf("could not send message over ws connection %v", err)
}
AssertPlayerWin(t, store, winner)
})
Make sure that you have an import for the
websocket
library. My IDE automatically did it for me, so should yours.To test what happens from the browser we have to open up our own WebSocket connection and write to it.
Our previous tests around our server just called methods on our server but now we need to have a persistent connection to our server. To do that we use
httptest.NewServer
which takes a http.Handler
and will spin it up and listen for connections.Using
websocket.DefaultDialer.Dial
we try to dial in to our server and then we'll try and send a message with our winner
.Finally, we assert on the player store to check the winner was recorded.
=== RUN TestGame/when_we_get_a_message_over_a_websocket_it_is_a_winner_of_a_game
--- FAIL: TestGame/when_we_get_a_message_over_a_websocket_it_is_a_winner_of_a_game (0.00s)
server_test.go:124: could not open a ws connection on ws://127.0.0.1:55838/ws websocket: bad handshake
We have not changed our server to accept WebSocket connections on
/ws
so we're not shaking hands yet.Add another listing to our router
router.Handle("/ws", http.HandlerFunc(p.webSocket))
Then add our new
webSocket
handlerfunc (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
upgrader.Upgrade(w, r, nil)
}
To accept a WebSocket connection we
Upgrade
the request. If you now re-run the test you should move on to the next error.=== RUN TestGame/when_we_get_a_message_over_a_websocket_it_is_a_winner_of_a_game
--- FAIL: TestGame/when_we_get_a_message_over_a_websocket_it_is_a_winner_of_a_game (0.00s)
server_test.go:132: got 0 calls to RecordWin want 1
Now that we have a connection opened, we'll want to listen for a message and then record it as the winner.
func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
conn, _ := upgrader.Upgrade(w, r, nil)
_, winnerMsg, _ := conn.ReadMessage()
p.store.RecordWin(string(winnerMsg))
}
(Yes, we're ignoring a lot of errors right now!)
conn.ReadMessage()
blocks on waiting for a message on the connection. Once we get one we use it to RecordWin
. This would finally close the WebSocket connection.If you try and run the test, it's still failing.
The issue is timing. There is a delay between our WebSocket connection reading the message and recording the win and our test finishes before it happens. You can test this by putting a short
time.Sleep
before the final assertion.Let's go with that for now but acknowledge that putting in arbitrary sleeps into tests is very bad practice.
time.Sleep(10 * time.Millisecond)
AssertPlayerWin(t, store, winner)
We committed many sins to make this test work both in the server code and the test code but remember this is the easiest way for us to work.
We have nasty, horrible, working software backed by a test, so now we are free to make it nice and know we won't break anything accidentally.
Let's start with the server code.
We can move the
upgrader
to a private value inside our package because we don't need to redeclare it on every WebSocket connection requestvar wsUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) {
conn, _ := wsUpgrader.Upgrade(w, r, nil)
_, winnerMsg, _ := conn.ReadMessage()
p.store.RecordWin(string(winnerMsg))
}
Our call to
template.ParseFiles("game.html")
will run on every GET /game
which means we'll go to the file system on every request even though we have no need to re-parse the template. Let's refactor our code so that we parse the template once in NewPlayerServer
instead. We'll have to make it so this function can now return an error in case we have problems fetching the template from disk or parsing it.Here's the relevant changes to
PlayerServer
type PlayerServer struct {
store PlayerStore
http.Handler
template *template.Template
}
const htmlTemplatePath = "game.html"
func NewPlayerServer(store PlayerStore) (*PlayerServer, error) {
p := new(PlayerServer)
tmpl, err := template.ParseFiles(htmlTemplatePath)
if err != nil {
return nil, fmt.Errorf("problem opening %s %v", htmlTemplatePath, err)
}
p.template = tmpl
p.store = store
router := http.NewServeMux()
router.Handle("/league", http.HandlerFunc(p.leagueHandler))
router.Handle("/players/", http.HandlerFunc(p.playersHandler))
router.Handle("/game", http.HandlerFunc(p.game))
router.Handle("/ws", http.HandlerFunc(p.webSocket))
p.Handler = router
return p, nil
}
func (p *PlayerServer) game(w http.ResponseWriter, r *http.Request) {
p.template.Execute(w, nil)
}
By changing the signature of
NewPlayerServer
we now have compilation problems. Try and fix them yourself or refer to the source code if you struggle.For the test code I made a helper called
mustMakePlayerServer(t *testing.T, store PlayerStore) *PlayerServer
so that I could hide the error noise away from the tests.func mustMakePlayerServer(t *testing.T, store PlayerStore) *PlayerServer {
server, err := NewPlayerServer(store)
if err != nil {
t.Fatal("problem creating player server", err)
}
return server
}
Similarly, I created another helper
mustDialWS
so that I could hide nasty error noise when creating the WebSocket connection.func mustDialWS(t *testing.T, url string) *websocket.Conn {
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
t.Fatalf("could not open a ws connection on %s %v", url, err)
}
return ws
}
Finally, in our test code we can create a helper to tidy up sending messages
func writeWSMessage(t testing.TB, conn *websocket.Conn, message string) {
t.Helper()
if err := conn.WriteMessage(websocket.TextMessage, []byte(message)); err != nil {
t.Fatalf("could not send message over ws connection %v", err)
}
}
Now the tests are passing try running the server and declare some winners in
/game
. You should see them recorded in /league
. Remember that every time we get a winner we close the connection, you will need to refresh the page to open the connection again.We've made a trivial web form that lets users record the winner of a game. Let's iterate on it to make it so the user can start a game by providing a number of players and the server will push messages to the client informing them of what the blind value is as time passes.
First update
game.html
to update our client side code for the new requirements<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lets play poker</title>
</head>
<body>
<section id="game">
<div id="game-start">
<label for="player-count">Number of players</label>
<input type="number" id="player-count"/>
<button id="start-game">Start</button>
</div>
<div id="declare-winner">
<label for="winner">Winner</label>
<input type="text" id="winner"/>
<button id="winner-button">Declare winner</button>
</div>
<div id="blind-value"/>
</section>
<section id="game-end">
<h1>Another great game of poker everyone!</h1>
<p><a href="/league">Go check the league table</a></p>
</section>
</body>
<script type="application/javascript">
const startGame = document.getElementById('game-start')
const declareWinner = document.getElementById('declare-winner')
const submitWinnerButton = document.getElementById('winner-button')
const winnerInput = document.getElementById('winner')
const blindContainer = document.getElementById('blind-value')
const gameContainer = document.getElementById('game')
const gameEndContainer = document.getElementById('game-end')
declareWinner.hidden = true
gameEndContainer.hidden = true
document.getElementById('start-game').addEventListener('click', event => {
startGame.hidden = true
declareWinner.hidden = false
const numberOfPlayers = document.getElementById('player-count').value
if (window['WebSocket']) {
const conn = new WebSocket('ws://' + document.location.host + '/ws')
submitWinnerButton.onclick = event => {
conn.send(winnerInput.value)
gameEndContainer.hidden = false
gameContainer.hidden = true
}
conn.onclose = evt => {
blindContainer.innerText = 'Connection closed'
}
conn.onmessage = evt => {
blindContainer.innerText = evt.data
}
conn.onopen = function () {
conn.send(numberOfPlayers)
}
}
})
</script>
</html>
The main changes is bringing in a section to enter the number of players and a section to display the blind value. We have a little logic to show/hide the user interface depending on the stage of the game.
Any message we receive via
conn.onmessage
we assume to be blind alerts and so we set the blindContainer.innerText
accordingly.How do we go about sending the blind alerts? In the previous chapter we introduced the idea of
Game
so our CLI code could call a Game
and everything else would be taken care of including scheduling blind alerts. This turned out to be a good separation of concern.type Game interface {
Start(numberOfPlayers int)
Finish(winner string)
}
When the user was prompted in the CLI for number of players it would
Start
the game which would kick off the blind alerts and when the user declared the winner they would Finish
. This is the same requirements we have now, just a different way of getting the inputs; so we should look to re-use this concept if we can.Our "real" implementation of
Game
is TexasHoldem
type TexasHoldem struct {
alerter BlindAlerter
store PlayerStore
}
By sending in a
BlindAlerter
TexasHoldem
can schedule blind alerts to be sent to wherevertype BlindAlerter interface {
ScheduleAlertAt(duration time.Duration, amount int)
}
And as a reminder, here is our implementation of the
BlindAlerter
we use in the CLI.func StdOutAlerter(duration time.Duration, amount int) {
time.AfterFunc(duration, func() {
fmt.Fprintf(os.Stdout, "Blind is now %d\n", amount)
})
}
This works in CLI because we always want to send the alerts to
os.Stdout
but this won't work for our web server. For every request we get a new http.ResponseWriter
which we then upgrade to *websocket.Conn
. So we can't know when constructing our dependencies where our alerts need to go.For that reason we need to change
BlindAlerter.ScheduleAlertAt
so that it takes a destination for the alerts so that we can re-use it in our webserver.Open
blind_alerter.go
and add the parameter to io.Writer
type BlindAlerter interface {
ScheduleAlertAt(duration time.Duration, amount int, to io.Writer)
}
type BlindAlerterFunc func(duration time.Duration, amount int, to io.Writer)
func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int, to io.Writer) {
a(duration, amount, to)
}
The idea of a
StdoutAlerter
doesn't fit our new model so just rename it to Alerter
func Alerter(duration time.Duration, amount int, to io.Writer) {
time.AfterFunc(duration, func() {
fmt.Fprintf(to, "Blind is now %d\n", amount)
})
}
If you try and compile, it will fail in
TexasHoldem
because it is calling ScheduleAlertAt
without a destination, to get things compiling again for now hard-code it to os.Stdout
.Try and run the tests and they will fail because
SpyBlindAlerter
no longer implements BlindAlerter
, fix this by updating the signature of ScheduleAlertAt
, run the tests and we should still be green.It doesn't make any sense for
TexasHoldem
to know where to send blind alerts. Let's now update Game
so that when you start a game you declare where the alerts should go.type Game interface {
Start(numberOfPlayers int, alertsDestination io.Writer)
Finish(winner string)
}
Let the compiler tell you what you need to fix. The change isn't so bad:
- Update
TexasHoldem
so it properly implementsGame
- In
CLI
when we start the game, pass in ourout
property (cli.game.Start(numberOfPlayers, cli.out)
) - In
TexasHoldem
's test i usegame.Start(5, io.Discard)
to fix the compilation problem and configure the alert output to be discarded
If you've got everything right, everything should be green! Now we can try and use
Game
within Server
.The requirements of
CLI
and Server
are the same! It's just the delivery mechanism is different.Let's take a look at our
CLI
test for inspiration.t.Run("start game with 3 players and finish game with 'Chris' as winner", func(t *testing.T) {
game := &GameSpy{}
out := &bytes.Buffer{}
in := userSends("3", "Chris wins")
poker.NewCLI(in, out, game).PlayPoker()
assertMessagesSentToUser(t, out, poker.PlayerPrompt)
assertGameStartedWith(t, game, 3)
assertFinishCalledWith(t, game, "Chris")
})
It looks like we should be able to test drive out a similar outcome using
GameSpy
Replace the old websocket test with the following
t.Run("start a game with 3 players and declare Ruth the winner", func(t *testing.T) {
game := &poker.GameSpy{}
winner := "Ruth"
server := httptest.NewServer(mustMakePlayerServer(t, dummyPlayerStore, game))
ws := mustDialWS(t, "ws"+strings.TrimPrefix(server.URL, "http")+"/ws")
defer server.Close()
defer ws.Close()
writeWSMessage(t, ws, "3")
writeWSMessage(t, ws, winner)
time.Sleep(10 * time.Millisecond)
assertGameStartedWith(t, game, 3)
assertFinishCalledWith(t, game, winner)
})
- As discussed we create a spy
Game
and pass it intomustMakePlayerServer
(be sure to update the helper to support this). - We then send the web socket messages for a game.
- Finally we assert that the game is started and finished with what we expect.
You'll have a number of compilation errors around
mustMakePlayerServer
in other tests. Introduce an unexported variable dummyGame
and use it through all the tests that aren't compilingvar (
dummyGame = &GameSpy{}
)
The final error is where we are trying to pass in
Game
to NewPlayerServer
but it doesn't support it yet./server_test.go:21:38: too many arguments in call to "github.com/quii/learn-go-with-tests/WebSockets/v2".NewPlayerServer
have ("github.com/quii/learn-go-with-tests/WebSockets/v2".PlayerStore, "github.com/quii/learn-go-with-tests/WebSockets/v2".Game)
want ("github.com/quii/learn-go-with-tests/WebSockets/v2".PlayerStore)
Just add it as an argument for now just to get the test running
func NewPlayerServer(store PlayerStore, game Game) (*PlayerServer, error)
Finally!
=== RUN TestGame/start_a_game_with_3_players_and_declare_Ruth_the_winner
--- FAIL: TestGame (0.01s)
--- FAIL: TestGame/start_a_game_with_3_players_and_declare_Ruth_the_winner (0.01s)
server_test.go:146: wanted Start called with 3 but got 0
server_test.go:147: expected finish called with 'Ruth' but got ''
FAIL
We need to add
Game
as a field to PlayerServer
so that it can use it when it gets requests.type PlayerServer struct {
store PlayerStore
http.Handler
template *template.Template
game Game
}
(We already have a method called
game
so rename that to playGame
)Next lets assign it in our constructor
func NewPlayerServer(store PlayerStore, game Game) (*PlayerServer, error) {
p := new(PlayerServer)
tmpl, err := template.ParseFiles(htmlTemplatePath)
if err != nil {
return nil, fmt.Errorf("problem opening %s %v", htmlTemplatePath, err)
}
p.game = game
// etc
}
Now we can use our
Game
within webSocket
.func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) {
conn, _ := wsUpgrader.Upgrade(w, r, nil)
_, numberOfPlayersMsg, _ := conn.ReadMessage()
numberOfPlayers, _ := strconv.Atoi(string(numberOfPlayersMsg))
p.game.Start(numberOfPlayers, io.Discard) //todo: Don't discard the blinds messages!
_, winner, _ := conn.ReadMessage()
p.game.Finish(string(winner))
}
Hooray! The tests pass.
We are not going to send the blind messages anywhere just yet as we need to have a think about that. When we call
game.Start
we send in io.Discard
which will just discard any messages written to it.For now start the web server up. You'll need to update the
main.go
to pass a Game
to the PlayerServer
func main() {
db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatalf("problem opening %s %v", dbFileName, err)
}
store, err := poker.NewFileSystemPlayerStore(db)
if err != nil {
log.Fatalf("problem creating file system player store, %v ", err)
}
game := poker.NewTexasHoldem(poker.BlindAlerterFunc(poker.Alerter), store)
server, err := poker.NewPlayerServer(store, game)
if err != nil {
log.Fatalf("problem creating player server %v", err)
}
log.Fatal(http.ListenAndServe(":5000", server))
}
Discounting the fact we're not getting blind alerts yet, the app does work! We've managed to re-use
Game
with PlayerServer
and it has taken care of all the details. Once we figure out how to send our blind alerts through to the web sockets rather than discarding them it should all work.Before that though, let's tidy up some code.
The way we're using WebSockets is fairly basic and the error handling is fairly naive, so I wanted to encapsulate that in a type just to remove that messiness from the server code. We may wish to revisit it later but for now this'll tidy things up a bit
type playerServerWS struct {
*websocket.Conn
}
func newPlayerServerWS(w http.ResponseWriter, r *http.Request) *playerServerWS {
conn, err := wsUpgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("problem upgrading connection to WebSockets %v\n", err)
}
return &playerServerWS{conn}
}
func (w *playerServerWS) WaitForMsg() string {
_, msg, err := w.ReadMessage()
if err != nil {
log.Printf("error reading from websocket %v\n", err)
}
return string(msg)
}
Now the server code is a bit simplified
func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) {
ws := newPlayerServerWS(w, r)
numberOfPlayersMsg := ws.WaitForMsg()
numberOfPlayers, _ := strconv.Atoi(numberOfPlayersMsg)
p.game.Start(numberOfPlayers, io.Discard) //todo: Don't discard the blinds messages!
winner := ws.WaitForMsg()
p.game.Finish(winner)
}
Once we figure out how to not discard the blind messages we're done.
Sometimes when we're not sure how to do something, it's best just to play around and try things out! Make sure your work is committed first because once we've figured out a way we should drive it through a test.
The problematic line of code we have is
p.game.Start(numberOfPlayers, io.Discard) //todo: Don't discard the blinds messages!
We need to pass in an
io.Writer
for the game to write the blind alerts to.Wouldn't it be nice if we could pass in our
playerServerWS
from before? It's our wrapper around our WebSocket so it feels like we should be able to send that to our Game
to send messages to.Give it a go:
func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) {
ws := newPlayerServerWS(w, r)
numberOfPlayersMsg := ws.WaitForMsg()
numberOfPlayers, _ := strconv.Atoi(numberOfPlayersMsg)
p.game.Start(numberOfPlayers, ws)
//etc...
}
The compiler complains
./server.go:71:14: cannot use ws (type *playerServerWS) as type io.Writer in argument to p.game.Start:
*playerServerWS does not implement io.Writer (missing Write method)
It seems the obvious thing to do, would be to make it so
playerServerWS
does implement io.Writer
. To do so we use the underlying *websocket.Conn
to use WriteMessage
to send the message down the websocketfunc (w *playerServerWS) Write(p []byte) (n int, err error) {
err = w.WriteMessage(websocket.TextMessage, p)
if err != nil {