devlog/public/posts/buzzer-game/index.html

182 lines
19 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en-us" dir="ltr">
<head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>
Classroom Buzzer App | Coding with Andrzej
</title>
<link rel="alternate" type="application/rss+xml" href="http://localhost:1313//index.xml" title="Coding with Andrzej">
<link rel="stylesheet" href="/css/main.css" />
<link rel="stylesheet" href="/css/syntax.css" />
<link rel="stylesheet" href="/css/defaults.css" />
<script src="/js/main.js"></script>
</head>
<body>
<header>
<a href=http://localhost:1313/><h1>Coding with Andrzej</h1></a>
<nav>
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a aria-current="true" class="ancestor" href="/posts/">Posts</a>
</li>
<li>
<a href="/tags/">Tags</a>
</li>
</ul>
</nav>
</header>
<main>
<h1>Classroom Buzzer App</h1>
<time datetime="2024-11-24T15:23:50&#43;01:00">November 24, 2024</time>
<h2 id="i-started-working-on-a-new-project-today">I started working on a new project today.</h2>
<p>The client is an educational services provider and wants me to develop a tool to facilitate in-person group activities. This tool should:</p>
<ul>
<li>provide a platform which students can log into from a mobile device, in a frictionless process that takes seconds</li>
<li>allow a teacher to assign groups and pairs in</li>
<li>allow the teacher to dynamically reassign groups without repeating combinations</li>
<li>implement a simple score-keeping functionality</li>
<li>be able to run a &lsquo;buzzer&rsquo; game</li>
<li>have a clean, appealing, and user-friendly UI</li>
</ul>
<p>A quick read of this brief should make it clear that what is required here is a server capable of handling multiple, live, two-way connections. The server needs to be able to update the clients whenever the teacher wants to shuffle the groups, and the buzzer game requires that when a student &lsquo;buzzes&rsquo;, state is propagated via the server to all other clients. The solution to this problem is websockets, and a server capable of handling concurrency.</p>
<p>Now I&rsquo;m relatively new to both of these topics, but my project <a href="https://projects.ajstepien.xyz/andrzej/gopaper">GoPaper</a> made use of concurrent goroutines to implement a wallpaper daemon, so I&rsquo;ll be building off that knowledge and developing the server side of this project in Go.</p>
<h2 id="websockets-in-go">WebSockets in Go</h2>
<p>It is possible to build a websocket framework entirely from scratch in Go, but seeing as I don&rsquo;t hate myself <em>quite that much</em>, I&rsquo;ll be using the framework <a href="https://github.com/gorilla/websocket">Gorilla</a>.</p>
<h2 id="getting-started-running-concurrent-websocket-connections">Getting started: running concurrent websocket connections</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;log&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;net/http&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="s">&#34;sync&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/gorilla/websocket&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">webSocketHandler</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">upgrader</span> <span class="nx">websocket</span><span class="p">.</span><span class="nx">Upgrader</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// GLOBALS
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">connections</span> <span class="kt">int</span> <span class="p">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">wg</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">wsh</span> <span class="nx">webSocketHandler</span><span class="p">)</span> <span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">//upgrade http connection to websocket
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">connection</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">wsh</span><span class="p">.</span><span class="nx">upgrader</span><span class="p">.</span><span class="nf">Upgrade</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;Error while upgrading connection to websocket: %s&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">connections</span><span class="o">++</span>
</span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;New websocket connection. There are now %v\n&#34;</span><span class="p">,</span> <span class="nx">connections</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">//actually handle the websocket connection here
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}()</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">webSocketHandler</span> <span class="o">:=</span> <span class="nx">webSocketHandler</span><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">upgrader</span><span class="p">:</span> <span class="nx">websocket</span><span class="p">.</span><span class="nx">Upgrader</span><span class="p">{},</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">port</span> <span class="kt">string</span> <span class="p">=</span> <span class="s">&#34;8080&#34;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">http</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="nx">webSocketHandler</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;listening on port %s...\n&#34;</span><span class="p">,</span> <span class="nx">port</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">(</span><span class="s">&#34;localhost:8080&#34;</span><span class="p">,</span> <span class="kc">nil</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>In this quick-and-dirty bit of code, we create a websocket handler that upgrades an http connection to a websocket connection, and then launches a goroutine. The goroutine doesn&rsquo;t do anything yet, but running ´websocat´ from multiple terminals shows us that we are now capable of handling multiple concurrent websocket connections 👍.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">websocat ws://localhost:8080/
</span></span></code></pre></div><pre tabindex="0"><code>New websocket connection. There are now 1.
</code></pre><pre tabindex="0"><code>New websocket connection. There are now 2.
</code></pre><pre tabindex="0"><code>New websocket connection. There are now 3.
</code></pre><p>We know the connections have been made as either websocat or our Go app would error out otherwise. We should certainly do something to handle closed connections, as this code is a memory leak waiting to happen as things stand, but as a proof of concept, it works.</p>
<p>Now, time to actually do something with these connections! Let&rsquo;s add some logic to that goroutine.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">wsh</span> <span class="nx">webSocketHandler</span><span class="p">)</span> <span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">//upgrade http connection to websocket
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">connection</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">wsh</span><span class="p">.</span><span class="nx">upgrader</span><span class="p">.</span><span class="nf">Upgrade</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;Error while upgrading connection to websocket: %s&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">connections</span><span class="o">++</span>
</span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;New websocket connection. There are now %v\n&#34;</span><span class="p">,</span> <span class="nx">connections</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nx">connection</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">msgType</span><span class="p">,</span> <span class="nx">message</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">connection</span><span class="p">.</span><span class="nf">ReadMessage</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;Error trying to read message from client: %s&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">msgType</span> <span class="o">==</span> <span class="nx">websocket</span><span class="p">.</span><span class="nx">BinaryMessage</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">connection</span><span class="p">.</span><span class="nf">WriteMessage</span><span class="p">(</span><span class="nx">websocket</span><span class="p">.</span><span class="nx">TextMessage</span><span class="p">,</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;this server does not support binary messages&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;Error trying to send message to client: %s&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">return</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;received message from client: %s&#34;</span><span class="p">,</span> <span class="nx">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">connection</span><span class="p">.</span><span class="nf">WriteMessage</span><span class="p">(</span><span class="nx">websocket</span><span class="p">.</span><span class="nx">TextMessage</span><span class="p">,</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;Message received!&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}()</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="heres-what-we-just-added">Here&rsquo;s what we just added:</h3>
<ul>
<li>The <code>defer</code> keyword closes the connection if/when the goroutine returns. In its current state, that would only happen in the case of an error.</li>
<li><code>connection.ReadMessage()</code> blocks until a message is received from the client</li>
<li>If the client tries to send binary data, we bounce an error message back to the client, log an error on the server and return from the goroutine.</li>
<li>If the message is text, we log it on the server and send a thank-you message back to the client. Testing this again with multiple instances of <code>websocat</code> shows that everything is working as it should. We have established two-way channels of communication between a server and multiple clients! 🥳</li>
</ul>
<p>In my next post, I&rsquo;ll begin implementing the business logic.</p>
<div>
<div>Tags:</div>
<ul class="post-tags">
<li><a href="/tags/education/">Education</a></li>
<li><a href="/tags/golang/">Golang</a></li>
<li><a href="/tags/websockets/">Websockets</a></li>
<li><a href="/tags/concurrency/">Concurrency</a></li>
</ul>
</div>
</main>
<footer>
<p>Copyright 2024. All rights reserved.</p>
</footer>
</body>
</html>