fsxNet Wiki

BBS Development & Resources

User Tools

Site Tools


tutorials:crystal_bbs:part_one

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

tutorials:crystal_bbs:part_one [2017/03/19 23:42]
sardaukar
tutorials:crystal_bbs:part_one [2018/03/29 01:58]
Line 1: Line 1:
-===== Crystal BBS - Part One ===== 
  
-==== Getting the ball rolling ==== 
- 
-OK, so let's start with some actual BBS programming! I've chosen Crystal for this tutorial, so you'll need to install it. At the moment this is being written, the current version is 0.21.1. As the language is still in alpha, a lot may break until it reaches 1.0 (scheduled for [[https://​crystal-lang.org/​2016/​12/​29/​crystal-new-year-resolutions-for-2017-1-0.html|later in 2017]]) and I'll try to keep this updated. 
- 
-I won't cover installation,​ since the [[https://​crystal-lang.org/​docs/​installation/​index.html|official docs]] are pretty good at that. You will need a macOS or Linux setup, either native or in a virtual machine. 
- 
-Once you have it installed, it's time to get going. Crystal has a "​skeleton"​ too built-in that creates an initial app and creates a local Git repository as well. Git is not part of the tutorial, but you should always keep your code in check with a source code management system, so have a look at it when you can. 
- 
-To create the initial skeleton, type 
- 
-<​code>​ 
-crystal init app bbs 
-</​code>​ 
- 
-This will create the skeleton inside the ''​bbs''​ subfolder of the current directory. Jump into it and have a look around at the files created. ​ 
- 
-One thing the app creator leaves out is an easy way to compile the project - we //can// just type ''​crystal build src/bbs.cr --release -o bbs_release''​ every time we want to compile, but that would get old really quick. So instead we'll create a ''​Makefile''​ and use good old //make// to perform all these repetitive tasks on the project. Here's how it could look: 
- 
-<code make> 
-.PHONY: clean 
- 
-run: bbs 
- ./​bbs_release 
-clean: 
- rm -f bbs_release 
- 
-all: clean 
- crystal build src/bbs.cr --release -o bbs_release 
- 
-bbs: 
- rm -f bbs_release 
- crystal build src/bbs.cr --release -o bbs_release 
-</​code>​ 
- 
-Make is also out of scope for this tutorial, but it won't get more complicated than this. From now on, just type ''​make''​ in the project'​s root directory to erase the existing binary and compile a new one. If you type it now, you should see: 
- 
-<​code>​ 
-$ make 
-rm -f bbs_release 
-crystal build src/bbs.cr --release -o bbs_release 
-./​bbs_release 
-</​code>​ 
- 
-That last line was our binary running, but of course it doesn'​t do anything yet! 
- 
-Let's get the basics out of the way before we go into the meat of the problem. We want to store configuration settings, so let's do that first, and then move on to the main network loop. 
- 
-Your ''​src/​bbs.cr''​ file should look like this now: 
- 
-<code ruby> 
-require "​./​bbs/​*"​ 
- 
-module Bbs 
-  # TODO Put your code here 
-end 
-</​code>​ 
-  
- 
-Change it to: 
- 
-<code ruby> 
-require "​./​bbs/​*"​ 
- 
-BBS::​Main.new.go! 
-</​code>​ 
- 
-This will call a ''​go!''​ method on the BBS module'​s ''​Main''​ class. Let's create it, but on a separate file. Create a ''​main.cr''​ file inside the ''​src/​bbs/''​ directory with: 
- 
-<code ruby> 
-require "​yaml"​ 
- 
-module BBS 
-  class Main 
-    @config : YAML::Any 
- 
-    def initialize 
-      @config = load_config 
-    end 
- 
-    def go! 
-      puts @config.inspect 
-    end 
- 
-    private def load_config 
-      YAML.parse(File.read("​config.yml"​)) 
-    end 
-  end 
-end 
-</​code>​ 
- 
-This file will be required by ''​src/​bbs.cr''​ (as mandated by its line 1) and will add the class to the BBS module. Let's create a basic config file as ''​config.yml''​ on the root directory of the project (more info on YAML [[https://​en.wikipedia.org/​wiki/​YAML|here]]:​ 
- 
-<code yaml> 
-settings: 
-  port: 2023 
-</​code>​ 
- 
-If you type ''​make''​ now, you should see: 
- 
-<​code>​ 
-$ make 
-rm -f bbs_release 
-crystal build src/bbs.cr --release -o bbs_release 
-./​bbs_release 
-{"​settings"​ => {"​port"​ => "​2023"​}} 
-</​code>​ 
- 
-Yay, we're live with the basics and our settings are being read! Let's recap for a bit, since this class introduces a few important concepts. ​ 
- 
-<code ruby> 
-require "​yaml"​ 
-</​code>​ 
- 
-This line requires in Crystal'​s built in [[https://​crystal-lang.org/​api/​0.21.1/​YAML.html|YAML handling functions]]. 
- 
-<​code>​ 
-@config : YAML::Any 
-</​code>​ 
- 
-This specifies the type of the ''​@config''​ instance variable for the ''​Main''​ class - this is a requirement in Crystal in order to generate runtime code for the variable. You'll notice that Crystal is very much like Ruby, but this is where the line is drawn - Crystal needs //some// type annotations in the code. You might think this is a step back from Ruby or even Python, but as a long time dynamic languages coder I can assure you that once a codebase exceeds a certain size, type safety being enforced by a compiler is a life saver. Also, it makes Crystal a bazillion times faster than Ruby or Python! :D 
- 
-<code ruby> 
-def initialize 
-  @config = load_config 
-end 
-</​code>​ 
- 
-As in Ruby, the constructor method for a class is called ''​initialize''​ - in our case, the constructor just assigns the result of the ''​load_config''​ method to the ''​@config''​ instance variable. 
- 
-<code ruby> 
-def go! 
-  puts @config.inspect 
-end 
-</​code>​ 
- 
-This is the method called in ''​bbs.cr''​ (our main entry point for the whole project)_after instantiating a new instance of the ''​Main''​ class. At this stage, it just prints out the hash parsed from the ''​config.yml''​ file that we created. 
- 
-<code ruby> 
-private def load_config 
-  YAML.parse(File.read("​config.yml"​)) 
-end 
-</​code>​ 
- 
-Finally, the method that parses the configuration file. It's marked as ''​private''​ since there'​s no real need for it being called outside of the ''​Main''​ class. It just returns the output of [[https://​crystal-lang.org/​api/​0.21.1/​YAML.html#​parse%28data%3AString%7CIO%29%3AAny-class-method|YAML.parse]]. Like in Ruby, there'​s "​implicit return"​ so we don't need to write ''​return YAML.parse(File.read("​config.yml"​))''​ here - the last value evaluated in a method is automatically returned. Note that there'​s no error handling whatsoever, as this is just a bare bones example. 
- 
-So far, so good. Now let's make the project do something a bit more interesting. 
- 
-Let's replace the ''​go!''​ method with our BBS server main loop. This means that while the program is running, it will be doing an infinite loop always listening on a socket, and assigning new connections to a handler function. 
- 
-First, we need to require more Crystal built-in functions. This time it will be [[https://​crystal-lang.org/​api/​0.21.1/​Socket.html|Socket]],​ which gives us a generic TCP server for free. So let's add to the top: 
- 
-<code ruby> 
-require "​socket"​ 
-</​code>​ 
- 
-Now, in the main ''​go!''​ method, let's get the config setting for our server port: 
- 
-<code ruby> 
-telnet_port = @config["​settings"​]["​port"​].to_s.to_i 
-<​code>​ 
- 
-The weird ''​.to_s.to_i''​ first converts the ''​YAML::​Any''​ value into a ''​String'',​ then into a ''​Int32''​ (a number, which is what we need). It's not very good looking, so we'll optimize it later before we wrap-up the tutorial, but for now roll with it. Now, let's start the server itself: 
- 
-<code ruby> 
-server = TCPServer.new("​localhost",​ telnet_port) 
-puts "now listening in port #​{telnet_port}"​ 
-loop do 
-  if socket = server.accept?​ 
-    spawn handle_connection(socket) 
-  end 
-end 
-</​code>​ 
- 
-So we start a new TCPServer bound to ''​localhost'',​ on the port stored in the ''​telnet_port''​ variable. We then print a little info message, and go into the meat of this method - the main ''​loop''​. The loop halts on the ''​server.accept?''​ call, which is blocking, until a connection is made to the server. Once a connection is accepted, the ''​socket''​ variable will be assigned all the details to it. With the details in hand, we will ''​spawn''​ a method call to ''​handle_connection''​. Spawning is a concept unique to Crystal, and you can read an intro to it in [[https://​crystal-lang.org/​docs/​guides/​concurrency.html|the official docs on concurrency]],​ but think of it as creating a new thread specifically to handle the connection. 
- 
-Finally, the new handler method: 
- 
-<code ruby> 
-private def handle_connection(socket) 
-  socket << "hello world\n"​ 
-  socket.close 
-end 
-</​code>​ 
- 
-This method will receive the socket info, write a string to it, and then close it. Let's try it out by running ''​make''​ - you should see: 
- 
-<​code>​ 
-$ make 
-... 
-now listening in port 2023 
-</​code>​ 
- 
-Now with a telnet client (the normal command line one in macOS or Linux will do), you can connect to your running server: 
- 
-<​code>​ 
-$ telnet localhost 2023 
-Trying 127.0.0.1... 
-Connected to localhost. 
-Escape character is '​^]'​. 
-hello world 
-Connection closed by foreign host. 
-</​code>​ 
- 
-Success! As expected, you can see the ''​hello world''​ message, and then the socket is closed! 
tutorials/crystal_bbs/part_one.txt ยท Last modified: 2018/03/29 01:58 (external edit)