182 lines
19 KiB
HTML
182 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en-us" dir="ltr">
|
||
<head><script src="/livereload.js?mindelay=10&v=2&port=1313&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+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 ‘buzzer’ 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 ‘buzzes’, 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’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’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’t hate myself <em>quite that much</em>, I’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">"log"</span>
|
||
</span></span><span class="line"><span class="cl"> <span class="s">"net/http"</span>
|
||
</span></span><span class="line"><span class="cl"> <span class="s">"sync"</span>
|
||
</span></span><span class="line"><span class="cl">
|
||
</span></span><span class="line"><span class="cl"> <span class="s">"github.com/gorilla/websocket"</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">"Error while upgrading connection to websocket: %s"</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">"New websocket connection. There are now %v\n"</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">"8080"</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">"/"</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">"listening on port %s...\n"</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">"localhost:8080"</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’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’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">"Error while upgrading connection to websocket: %s"</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">"New websocket connection. There are now %v\n"</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">"Error trying to read message from client: %s"</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">"this server does not support binary messages"</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">"Error trying to send message to client: %s"</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">"received message from client: %s"</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">"Message received!"</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’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’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>
|