summary refs log tree commit diff stats
path: root/tests/niminaction/Chapter3/ChatApp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/niminaction/Chapter3/ChatApp')
-rw-r--r--tests/niminaction/Chapter3/ChatApp/readme.markdown26
-rw-r--r--tests/niminaction/Chapter3/ChatApp/src/client.nim58
-rw-r--r--tests/niminaction/Chapter3/ChatApp/src/client.nim.cfg1
-rw-r--r--tests/niminaction/Chapter3/ChatApp/src/protocol.nim55
-rw-r--r--tests/niminaction/Chapter3/ChatApp/src/server.nim88
5 files changed, 228 insertions, 0 deletions
diff --git a/tests/niminaction/Chapter3/ChatApp/readme.markdown b/tests/niminaction/Chapter3/ChatApp/readme.markdown
new file mode 100644
index 000000000..200b4df1d
--- /dev/null
+++ b/tests/niminaction/Chapter3/ChatApp/readme.markdown
@@ -0,0 +1,26 @@
+# The ChatApp source code
+
+This directory contains the ChatApp project, which is the project that is
+created as part of Chapter 3 of the Nim in Action book.
+
+To compile run:
+
+```
+nim c src/client
+nim c src/server
+```
+
+You can then run the ``server`` in one terminal by executing ``./src/server``.
+
+After doing so you can execute multiple clients in different terminals and have
+them communicate via the server.
+
+To execute a client, make sure to specify the server address and user name
+on the command line:
+
+```bash
+./src/client localhost Peter
+```
+
+You should then be able to start typing in messages and sending them
+by pressing the Enter key.
\ No newline at end of file
diff --git a/tests/niminaction/Chapter3/ChatApp/src/client.nim b/tests/niminaction/Chapter3/ChatApp/src/client.nim
new file mode 100644
index 000000000..d479ebf43
--- /dev/null
+++ b/tests/niminaction/Chapter3/ChatApp/src/client.nim
@@ -0,0 +1,58 @@
+discard """
+action: compile
+"""
+
+import os, threadpool, asyncdispatch, asyncnet
+import protocol
+
+proc connect(socket: AsyncSocket, serverAddr: string) {.async.} =
+  ## Connects the specified AsyncSocket to the specified address.
+  ## Then receives messages from the server continuously.
+  echo("Connecting to ", serverAddr)
+  # Pause the execution of this procedure until the socket connects to
+  # the specified server.
+  await socket.connect(serverAddr, 7687.Port)
+  echo("Connected!")
+  while true:
+    # Pause the execution of this procedure until a new message is received
+    # from the server.
+    let line = await socket.recvLine()
+    # Parse the received message using ``parseMessage`` defined in the
+    # protocol module.
+    let parsed = parseMessage(line)
+    # Display the message to the user.
+    echo(parsed.username, " said ", parsed.message)
+
+echo("Chat application started")
+# Ensure that the correct amount of command line arguments was specified.
+if paramCount() < 2:
+  # Terminate the client early with an error message if there was not
+  # enough command line arguments specified by the user.
+  quit("Please specify the server address, e.g. ./client localhost username")
+
+# Retrieve the first command line argument.
+let serverAddr = paramStr(1)
+# Retrieve the second command line argument.
+let username = paramStr(2)
+# Initialise a new asynchronous socket.
+var socket = newAsyncSocket()
+
+# Execute the ``connect`` procedure in the background asynchronously.
+asyncCheck connect(socket, serverAddr)
+# Execute the ``readInput`` procedure in the background in a new thread.
+var messageFlowVar = spawn stdin.readLine()
+while true:
+  # Check if the ``readInput`` procedure returned a new line of input.
+  if messageFlowVar.isReady():
+    # If a new line of input was returned, we can safely retrieve it
+    # without blocking.
+    # The ``createMessage`` is then used to create a message based on the
+    # line of input. The message is then sent in the background asynchronously.
+    asyncCheck socket.send(createMessage(username, ^messageFlowVar))
+    # Execute the ``readInput`` procedure again, in the background in a
+    # new thread.
+    messageFlowVar = spawn stdin.readLine()
+
+  # Execute the asyncdispatch event loop, to continue the execution of
+  # asynchronous procedures.
+  asyncdispatch.poll()
diff --git a/tests/niminaction/Chapter3/ChatApp/src/client.nim.cfg b/tests/niminaction/Chapter3/ChatApp/src/client.nim.cfg
new file mode 100644
index 000000000..aed303eef
--- /dev/null
+++ b/tests/niminaction/Chapter3/ChatApp/src/client.nim.cfg
@@ -0,0 +1 @@
+--threads:on
diff --git a/tests/niminaction/Chapter3/ChatApp/src/protocol.nim b/tests/niminaction/Chapter3/ChatApp/src/protocol.nim
new file mode 100644
index 000000000..4c122d4cc
--- /dev/null
+++ b/tests/niminaction/Chapter3/ChatApp/src/protocol.nim
@@ -0,0 +1,55 @@
+import json
+
+type
+  Message* = object
+    username*: string
+    message*: string
+
+  MessageParsingError* = object of Exception
+
+proc parseMessage*(data: string): Message {.raises: [MessageParsingError, KeyError].} =
+  var dataJson: JsonNode
+  try:
+    dataJson = parseJson(data)
+  except JsonParsingError:
+    raise newException(MessageParsingError, "Invalid JSON: " &
+                       getCurrentExceptionMsg())
+  except:
+    raise newException(MessageParsingError, "Unknown error: " &
+                       getCurrentExceptionMsg())
+
+  if not dataJson.hasKey("username"):
+    raise newException(MessageParsingError, "Username field missing")
+
+  result.username = dataJson["username"].getStr()
+  if result.username.len == 0:
+    raise newException(MessageParsingError, "Username field is empty")
+
+  if not dataJson.hasKey("message"):
+    raise newException(MessageParsingError, "Message field missing")
+  result.message = dataJson["message"].getStr()
+  if result.message.len == 0:
+    raise newException(MessageParsingError, "Message field is empty")
+
+proc createMessage*(username, message: string): string =
+  result = $(%{
+    "username": %username,
+    "message": %message
+  }) & "\c\l"
+
+when true:
+  block:
+    let data = """{"username": "dom", "message": "hello"}"""
+    let parsed = parseMessage(data)
+    doAssert parsed.message == "hello"
+    doAssert parsed.username == "dom"
+
+  # Test failure
+  block:
+    try:
+      let parsed = parseMessage("asdasd")
+    except MessageParsingError:
+      doAssert true
+    except:
+      doAssert false
+
diff --git a/tests/niminaction/Chapter3/ChatApp/src/server.nim b/tests/niminaction/Chapter3/ChatApp/src/server.nim
new file mode 100644
index 000000000..fbf0e5110
--- /dev/null
+++ b/tests/niminaction/Chapter3/ChatApp/src/server.nim
@@ -0,0 +1,88 @@
+discard """
+action: compile
+"""
+
+import asyncdispatch, asyncnet
+
+type
+  Client = ref object
+    socket: AsyncSocket
+    netAddr: string
+    id: int
+    connected: bool
+
+  Server = ref object
+    socket: AsyncSocket
+    clients: seq[Client]
+
+proc newServer(): Server =
+  ## Constructor for creating a new ``Server``.
+  Server(socket: newAsyncSocket(), clients: @[])
+
+proc `$`(client: Client): string =
+  ## Converts a ``Client``'s information into a string.
+  $client.id & "(" & client.netAddr & ")"
+
+proc processMessages(server: Server, client: Client) {.async.} =
+  ## Loops while ``client`` is connected to this server, and checks
+  ## whether as message has been received from ``client``.
+  while true:
+    # Pause execution of this procedure until a line of data is received from
+    # ``client``.
+    let line = await client.socket.recvLine()
+
+    # The ``recvLine`` procedure returns ``""`` (i.e. a string of length 0)
+    # when ``client`` has disconnected.
+    if line.len == 0:
+      echo(client, " disconnected!")
+      client.connected = false
+      # When a socket disconnects it must be closed.
+      client.socket.close()
+      return
+
+    # Display the message that was sent by the client.
+    echo(client, " sent: ", line)
+
+    # Send the message to other clients.
+    for c in server.clients:
+      # Don't send it to the client that sent this or to a client that is
+      # disconnected.
+      if c.id != client.id and c.connected:
+        await c.socket.send(line & "\c\l")
+
+proc loop(server: Server, port = 7687) {.async.} =
+  ## Loops forever and checks for new connections.
+
+  # Bind the port number specified by ``port``.
+  server.socket.bindAddr(port.Port)
+  # Ready the server socket for new connections.
+  server.socket.listen()
+  echo("Listening on localhost:", port)
+
+  while true:
+    # Pause execution of this procedure until a new connection is accepted.
+    let (netAddr, clientSocket) = await server.socket.acceptAddr()
+    echo("Accepted connection from ", netAddr)
+
+    # Create a new instance of Client.
+    let client = Client(
+      socket: clientSocket,
+      netAddr: netAddr,
+      id: server.clients.len,
+      connected: true
+    )
+    # Add this new instance to the server's list of clients.
+    server.clients.add(client)
+    # Run the ``processMessages`` procedure asynchronously in the background,
+    # this procedure will continuously check for new messages from the client.
+    asyncCheck processMessages(server, client)
+
+# Check whether this module has been imported as a dependency to another
+# module, or whether this module is the main module.
+when true:
+  # Initialise a new server.
+  var server = newServer()
+  echo("Server initialised!")
+  # Execute the ``loop`` procedure. The ``waitFor`` procedure will run the
+  # asyncdispatch event loop until the ``loop`` procedure finishes executing.
+  waitFor loop(server)