diff options
Diffstat (limited to 'tests/niminaction/Chapter3/ChatApp')
-rw-r--r-- | tests/niminaction/Chapter3/ChatApp/readme.markdown | 26 | ||||
-rw-r--r-- | tests/niminaction/Chapter3/ChatApp/src/client.nim | 58 | ||||
-rw-r--r-- | tests/niminaction/Chapter3/ChatApp/src/client.nim.cfg | 1 | ||||
-rw-r--r-- | tests/niminaction/Chapter3/ChatApp/src/protocol.nim | 55 | ||||
-rw-r--r-- | tests/niminaction/Chapter3/ChatApp/src/server.nim | 88 |
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) |