fsxNet Wiki

BBS Development & Resources

User Tools

Site Tools


tutorials:crystal_bbs:part_two

Differences

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

Link to this comparison view

tutorials:crystal_bbs:part_two [2017/04/09 14:11]
sardaukar
tutorials:crystal_bbs:part_two [2018/03/29 01:58]
Line 1: Line 1:
-===== Crystal BBS - Part Two ===== 
- 
-==== Negotiating Telnet features with the client ==== 
- 
-OK, so now we have a basic Telnet hello world example going - let's move forward. 
- 
-The plan for the tutorial at this stage is to handle feature negotiation in this part, and input on the next one. Haven'​t decided what to do after that, but probably a state machine interaction post or two, and then you should know most of what it takes to build a basic old-school BBS system. So, on to feature negotiation. 
- 
-I've been using [[https://​syncterm.bbsdev.net/​|SyncTerm]] as my terminal problem for these posts. When connecting to the prototype BBS, I noticed the feature negotiation flow using the "​toggle options"​ telnet client trick from the [[tutorials:​crystal_bbs:​part_zero|intro post]]. We'll focus on those being requested, and not the whole spectrum of options available. IANA has [[http://​www.iana.org/​assignments/​telnet-options/​telnet-options.xhtml|a list]] of possible options. 
- 
-Our prototype will: 
- 
-  * show local echo 
-  * suppress Go-Ahead 
-  * ask for the user's terminal type 
-  * set the connection to binary-mode 
-  * work out NAWS (Negotiate About Window Size) 
- 
-All of this will be handled at the start of every connection, so let's revisit our connection handler method: 
- 
-<code ruby> 
-private def handle_connection(socket) 
-  socket << "hello world\n"​ 
-  socket.close 
-end 
-</​code>​ 
- 
-Pretty bare at this moment. Let's start by telling the client we'll be handling feature negotiation. Also, to hold the connection info, let's introduce a new variable in the mix.  
- 
-<code ruby> 
-private def handle_connection(socket) 
-  socket << "​detecting client features..."​ 
-  ​ 
-  conn = ConnHash.new 
-  ​ 
-  socket.close 
-end 
-</​code>​ 
- 
-We could write down the whole Crystal type, but since it's cumbersome we'll alias it to a shorter name (in this case, ''​ConnHash''​). To keep our main file uncluttered,​ let's move this to a separate file. 
- 
-<code ruby> 
-module BBS 
-  alias ConnHash = Hash(Symbol,​ Bool | String | Int32) 
-end 
-</​code>​ 
- 
-Save this in the same folder as ''​main.cr''​ and name it ''​aliases.cr''​ since we'll be adding more aliases to this file as needed. Next, require it on ''​main.cr'':​ 
- 
-<code ruby> 
-require "​./​aliases"​ 
-</​code>​ 
- 
-The first option we want to negotiate is WILL ECHO to signal the client we want user input to be shown locally. That messag'​s IAC byte array will look like: 
- 
-  * 1st byte - 0xff (255) / IAC 
-  * 2nd byte - 0xfb (251) / WILL 
-  * 3rd byte - 0x01 (  1) / ECHO 
- 
-We could create the byte array "by hand" for each message, but let's make it a utility method: 
- 
-<code ruby> 
-private def iac(*args) 
-  Slice(UInt8).new(args.size + 1) do |i|  
-    i == 0 ? 255.to_u8 : args[i - 1].to_u8 
-  end 
-end 
-</​code>​ 
- 
-Looks weird, but it's quite simple. We start off by [[https://​crystal-lang.org/​docs/​syntax_and_semantics/​splats_and_tuples.html|splatting]] the arguments passed. This means we'll be able to handle a variable list of arguments, which serves us since some IAC messages have more arguments than others. We then create a byte array (''​Slice(UInt8)''​ in Crystal terms) with a size equal to the size of the arguments passed plus one - since we don't want to pass the IAC byte in every method call, we just insert it in the array, thus the plus one being added to the size, making room for it.  
- 
-In the block'​s main line, we use the ''​i''​ variable to loop through the array being constructed. When we're at the ''​0''​ index, we insert 255 (IAC) as an unsigned 8bit value, and if we're not at index ''​0'',​ we just pass the arguments from the method into the byte array. Ingenious, and gets the job done.  
- 
-However, the ''​255''​ here does not make for very clean code, and we hate that, so let's create a file to house all of our constants. Save this as ''​constants.cr''​ in the same folder as our ''​main.cr''​ file: 
- 
-<code ruby> 
-module BBS 
-  IAC  = 255 
-  DONT = 254 
-  DO   = 253 
-  WONT = 252 
-  WILL = 251 
-  SB   = 250 
-  SE   = 240 
- 
-  BINARY ​       =  0 
-  ECHO          =  1 
-  SEND          =  1 # in options context 
-  SUP_GOAHEAD ​  ​= ​ 3 
-  TERMINAL_TYPE = 24 
-  NAWS          = 31 
-end 
-</​code>​ 
- 
-As before, don't forget to require it in the main file: 
- 
-<code ruby> 
-require "​./​constants"​ 
-</​code>​ 
- 
-With this file in place, we can now change the IAC method'​s source to something a bit more readable: 
- 
-<code ruby> 
-private def iac(*args) 
-  Slice(UInt8).new(args.size + 1) do |i| 
-    i == 0 ? IAC.to_u8 : args[i - 1].to_u8 
-  end 
-end 
-</​code>​ 
- 
-Alright, with all this in place, let's abstract the negotiating to its own method on the main handler method: 
- 
-<code ruby> 
-private def handle_connection(socket) 
-  socket << "​detecting client features..."​ 
- 
-  conn = ConnHash.new 
- 
-  negotiate(iac(WILL,​ ECHO), :echo, socket, conn) 
- 
-  socket.close 
-end 
-</​code>​ 
- 
  
tutorials/crystal_bbs/part_two.txt ยท Last modified: 2018/03/29 01:58 (external edit)