Skip to content

Commit

Permalink
More comments and nicer code for ChatApp.
Browse files Browse the repository at this point in the history
  • Loading branch information
dom96 committed Apr 28, 2016
1 parent e5d359b commit 46f4e5d
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 21 deletions.
26 changes: 26 additions & 0 deletions Chapter3/ChatApp/readme.markdown
Original file line number Diff line number Diff line change
@@ -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.
38 changes: 32 additions & 6 deletions Chapter3/ChatApp/src/client.nim
Original file line number Diff line number Diff line change
@@ -1,28 +1,54 @@
import os, threadpool, asyncnet, asyncdispatch, protocol
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")

if paramCount() == 0:
quit("Please specify the server address")

# 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():
let message = createMessage("Anonymous", ^messageFlowVar)
asyncCheck socket.send(message)
# 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()
1 change: 1 addition & 0 deletions Chapter3/ChatApp/src/client.nim.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--threads:on
44 changes: 30 additions & 14 deletions Chapter3/ChatApp/src/protocol.nim
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
import json

type
Message* = object
username*: string
message*: string

proc parseMessage*(data: string): Message =
let dataJson = parseJson(data)
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("username"):
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 = $(%{
Expand All @@ -17,23 +39,17 @@ proc createMessage*(username, message: string): string =

when isMainModule:
block:
let data = """{"username": "John", "message": "Hi!"}"""
let data = """{"username": "dom", "message": "hello"}"""
let parsed = parseMessage(data)
doAssert parsed.username == "John"
doAssert parsed.message == "Hi!"
doAssert parsed.message == "hello"
doAssert parsed.username == "dom"

# Test failure
block:
let data = """foobar"""
try:
let parsed = parseMessage(data)
doAssert false
except JsonParsingError:
let parsed = parseMessage("asdasd")
except MessageParsingError:
doAssert true
except:
doAssert false

block:
let expected = """{"username":"dom","message":"hello"}""" & "\c\l"
doAssert createMessage("dom", "hello") == expected

echo("All tests passed!")
85 changes: 84 additions & 1 deletion Chapter3/ChatApp/src/server.nim
Original file line number Diff line number Diff line change
@@ -1 +1,84 @@
import asyncdispatch, asyncnettype Client = ref object socket: AsyncSocket netAddr: string id: int connected: bool Server = ref object socket: AsyncSocket clients: seq[Client]proc newServer(): Server = Server(socket: newAsyncSocket(), clients: @[])proc `$`(client: Client): string = $client.id & "(" & client.netAddr & ")"proc processMessages(server: Server, client: Client) {.async.} = while true: let line = await client.socket.recvLine() if line.len == 0: echo(client, " disconnected!") client.connected = false client.socket.close() return echo(client, " sent: ", line)proc loop(server: Server, port = 7687) {.async.} = server.socket.bindAddr(port.Port) server.socket.listen() while true: let (netAddr, clientSocket) = await server.socket.acceptAddr() echo("Accepted connection from ", netAddr) let client = Client( socket: clientSocket, netAddr: netAddr, id: server.clients.len, connected: true ) server.clients.add(client) asyncCheck processMessages(server, client)var server = newServer()waitFor loop(server)
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 isMainModule:
# 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)

0 comments on commit 46f4e5d

Please sign in to comment.