├── .gitignore ├── LICENSE ├── README.md ├── example ├── authoritativeChatClient │ ├── authoritativeExample.nimble │ ├── client.nim │ ├── client.nims │ └── server.nim ├── chatclient │ ├── client.nim │ ├── client.nims │ └── server.nim └── pong │ ├── assets │ ├── font.png │ └── font.png.dat │ ├── build.nimble │ ├── client.nim │ └── server.nim ├── nettyrpc.nimble ├── src ├── nettyrpc.nim └── nettyrpc │ └── nettystream.nim └── tests ├── config.nims └── tnettystream.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | nimblecache/ 3 | htmldocs/ 4 | server 5 | client 6 | *.exe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jason Beetham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nettyrpc 2 | Implements an RPC-like system for Nim 3 | 4 | nettyrpc is a RPC module for game development. nettyrpc is built on top of the netty library, and sends serialized data over a modified UDP connection that's capped at 250k of in-flight data. Netty performs packet ordering and packet resending like a TCP connection, but it's all done over UDP. Check the netty library for more information about how netty utilizes UDP, and it's limitations. 5 | 6 | https://github.com/treeform/netty 7 | 8 | nettyrpc can be set up to run authoritative and non-authoritative servers by the use of `{.networked.}` or `{.relayed.}` procedures. 9 | 10 | A `{.relayed.}` RPC is sent directly to the server, no server-side processing is done and the RPC is sent to every connected client on the server where it is processed client-side. Relayed RPCs have a `isLocal` bool that can be used to control what the caller runs in a relayed procedure vs what the receivers run. Unlike `{.networked.}` procedures, relayed procedures can be called like any other procedure. 11 | 12 | `{.networked.}` RPCs are sent to the server where the server processes the data, and depending on how you write your server code, the server can forward the data to an individual client, all clients, or process the data server-side without any forwarding. Networked RPCs must be called through the `rpc` procedures provided by nettyrpc, ie. `rpc("someRemoteProc", (arg1, arg2, arg3))` or `rpc(conn, "someRemoteProc", (arg1, arg2, arg3))`. It's important that all the procedure call arguments are wrapped in a `tuple` when using the `rpc` procedures. 13 | 14 | `{.networked.}` RPCs contain [a netty `Connection` object](https://github.com/treeform/netty/blob/master/src/netty.nim#L52) that represents the RPC caller's connection. This is accessed through the `conn` variable that is automatically added to any `{.networked.}` procedures. It is important to note that the `Connection` object that is provided to the client-side script will always point back to the server. The server's `{.networked.}` procedures must pass this information on to clients if the clients need to know which client is responsible for the original remote procedure call. 15 | 16 | ## Uses 17 | 18 | Mostly game developement, but any networked application that does not require a ton of throughput. UDP is not designed for throughput, however this library could be used to negotiate a TCP connection for larger data transfers. 19 | 20 | ## Examples 21 | 22 | Here is what a typical `{.relayed.}` procedure looks like on the client-side. 23 | 24 | ```nim 25 | proc send(name, message: string) {.relayed.} = 26 | if not isLocal: # If remote client runs procedure 27 | eraseLine() 28 | echo fmt"{getClockStr()} {name} says: {message}" 29 | else: # If local client runs procedure. 30 | eraseLine() 31 | echo fmt"{getClockStr()} You said: {message}" 32 | ``` 33 | 34 | Remember that `{.relayed.}` procedures do not require any server-side RPCs to be defined, so we only need to modify our client's code. 35 | 36 | A `{.networked.}` procedure requires us to implement server-side RPCs so the server can process the request and dispatch RPCs manually. 37 | 38 | ### A Tiny Example 39 | 40 | Here is a client with a `{.networked.}` procedure. It attempts to run the `join` procedure located on the server, and the server then processes the message and calls the `welcome` procedure on all connected clients. 41 | 42 | __client.nim__ 43 | 44 | ```nim 45 | import netty, nettyrpc 46 | import std/[strutils, strformat] 47 | 48 | # Define our client-side RPC. 49 | proc welcome(name: string, id: int) {.networked.} = 50 | ## Display a welcome message. 51 | echo fmt"{id}:{name} has joined the server" 52 | 53 | # Set up nettyrpc to run as a client. 54 | let client = newReactor() 55 | nettyrpc.client = client.connect("127.0.0.1", 1999) 56 | nettyrpc.reactor = client 57 | 58 | # Call the `join` procedure on the server with an argument 59 | # representing our name, along with an id. 60 | rpc("join", ("Bobby Bouche", 1)) 61 | 62 | while true: 63 | # rpcTick handles calling/sending procedures. This must be called to send 64 | # and receive RPCs. 65 | client.rpcTick() 66 | ``` 67 | 68 | Here is the server code we need to handle the call to `join`, and then calling the `welcome` procedure on each connected client. It also sends the same welcome message to the caller of the RPC (to demonstrate direct calling of a RP) 69 | 70 | __server.nim__ 71 | 72 | ```nim 73 | import netty, nettyrpc, os 74 | 75 | # Define server-side RPC 76 | proc join(name: string, id: int) {.networked.} = 77 | # Do server side logic, maybe we log the event or something like that. 78 | rpc("welcome", (name, id)) # Call `welcome` procedure on all connected clients. 79 | rpc(conn, "welcome", (name, id)) # Call `welcome` procedure on the client that called `join`. 80 | 81 | 82 | # listen for a connection on localhost port 1999 83 | var server = newReactor("127.0.0.1", 1999) 84 | 85 | # Set up nettyrpc as a server. 86 | nettyrpc.reactor = server 87 | 88 | # main loop 89 | while true: 90 | server.rpcTick(server=true) # rpcTick handles dispatching of procedures. Must be called. 91 | for msg in server.messages: # Display messages since last tick. 92 | echo msg 93 | ``` 94 | 95 | Check the examples folder in this repo for the full documented examples. 96 | -------------------------------------------------------------------------------- /example/authoritativeChatClient/authoritativeExample.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "RattleyCooper" 5 | description = "An authoritative chat server/client example for nettyrpc" 6 | license = "MIT" 7 | installFiles = @["server.nim", "client.nim", "client.nims"] 8 | 9 | # Dependencies 10 | requires "nim >= 1.4.0" 11 | requires "nettyrpc >= 0.2.0" 12 | -------------------------------------------------------------------------------- /example/authoritativeChatClient/client.nim: -------------------------------------------------------------------------------- 1 | import netty, nettyrpc 2 | import std/[times, parseopt, strutils, strformat, terminal] 3 | import tables 4 | 5 | 6 | var 7 | clientId: uint32 8 | 9 | 10 | ### This is a somewhat simple chat client, very broken 11 | ### -p is Port 12 | ### -n is Name 13 | ### -i is Ip 14 | proc getParams: (string, string, int) = 15 | for kind, key, val in getOpt(): 16 | case key: 17 | of "p", "port": 18 | result[2] = parseint(val) 19 | of "ip", "i": 20 | result[1] = val 21 | of "name", "n": 22 | result[0] = val 23 | 24 | proc set_client_id(theClientId: uint32) {.networked.} = 25 | ## Set the client's internal clientId to the server-side connection id. 26 | echo "setting client id: " & $theClientId 27 | clientId = theClientId 28 | 29 | proc display_chat(name, message: string, originId: uint32) {.networked.} = 30 | ## Display chat message from the server. Checks local clientId against originId 31 | ## and displays appropriate message depending on where the RPC originated. 32 | 33 | eraseLine() 34 | if originId != clientId: 35 | echo fmt"{getClockStr()} {name} says: {message}" 36 | else: 37 | # eraseLine() 38 | echo fmt"{getClockStr()} You said: {message}" 39 | 40 | proc ping() {.relayed.} = 41 | ## Inclusion of relayed procedure to demonstrate that they can be used together. 42 | if isLocal: 43 | echo "local pong" 44 | else: 45 | echo "remote pong" 46 | 47 | proc send_chat(name, msg: string) = 48 | # If you want client actions to take place instantly, create a procedure 49 | # to handle client-side logic and then dispatch the RPC. 50 | # echo fmt"{getClockStr()} You said: {msg}" 51 | rpc("send_chat", (name: name, msg: msg)) 52 | 53 | proc inputLoop(input: ptr Channel[string]) {.thread.} = 54 | echo "Started input loop" 55 | var msg = "" 56 | while true: 57 | msg.add stdin.readLine() 58 | input[].send(msg) 59 | msg.setLen(0) 60 | 61 | 62 | let 63 | (name, ip, port) = getParams() 64 | client = newReactor() 65 | 66 | nettyrpc.client = client.connect(ip, port) 67 | nettyrpc.reactor = client 68 | 69 | doAssert(ip != "", "You must set an ip via -i=127.0.0.1") 70 | doAssert(port != 0, "You must set a port via -p=1999") 71 | doAssert(name != "", "You must use a nickname via -n=someNickname") 72 | 73 | rpc("join") # Join the server and set clientId. 74 | ping() 75 | 76 | var 77 | worker: Thread[ptr Channel[string]] 78 | input: Channel[string] 79 | input.open 80 | worker.createThread(inputLoop, input.addr) 81 | echo fmt"Hello {name}" 82 | 83 | echo "starting tick" 84 | while true: 85 | client.rpcTick() 86 | let (gotInput, msg) = input.tryRecv 87 | if gotInput: 88 | send_chat(name, msg) 89 | -------------------------------------------------------------------------------- /example/authoritativeChatClient/client.nims: -------------------------------------------------------------------------------- 1 | switch("threads", "on") 2 | switch("gc", "orc") -------------------------------------------------------------------------------- /example/authoritativeChatClient/server.nim: -------------------------------------------------------------------------------- 1 | import netty, nettyrpc, os 2 | 3 | 4 | proc join() {.networked.} = 5 | ## Join server and send client's server-side connection id to caller. 6 | ## This uses info from the `conn` variable that's added to all networked 7 | ## procedures. 8 | rpc(conn, "set_client_id", (theClientId: conn.id)) 9 | 10 | 11 | proc send_chat(name: string, msg: string) {.networked.} = 12 | ## Dispatch chat messages to all connected clients regardless of who sent it. 13 | rpc("display_chat", (name: name, msg: msg, originId: conn.id)) 14 | 15 | 16 | var server = newReactor("127.0.0.1", 1999) # listen for a connection on localhost port 1999 17 | nettyrpc.reactor = server # Set nettyrpc reactor to server. 18 | 19 | while true: 20 | server.rpcTick(server=true) # tick RPCs 21 | for msg in server.messages: # Display messages since last tick. 22 | echo msg 23 | sleep(30) 24 | -------------------------------------------------------------------------------- /example/chatclient/client.nim: -------------------------------------------------------------------------------- 1 | import netty, nettyrpc 2 | import std/[times, parseopt, strutils, strformat, terminal] 3 | 4 | ### This is a somewhat simple chat client, very broken 5 | ### -p is Port 6 | ### -n is Name 7 | ### -i is Ip 8 | 9 | proc getParams: (string, string, int) = 10 | for kind, key, val in getOpt(): 11 | case key: 12 | of "p", "port": 13 | result[2] = parseint(val) 14 | of "ip", "i": 15 | result[1] = val 16 | of "name", "n": 17 | result[0] = val 18 | 19 | proc send(name, message: string) {.relayed.} = 20 | if not isLocal: 21 | eraseLine() 22 | echo fmt"{getClockStr()} {name} says: {message}" 23 | 24 | proc inputLoop(input: ptr Channel[string]) {.thread.} = 25 | echo "Started input loop" 26 | var msg = "" 27 | while true: 28 | msg.add stdin.readLine() 29 | input[].send(msg) 30 | msg.setLen(0) 31 | 32 | 33 | let 34 | (name, ip, port) = getParams() 35 | client = newReactor() 36 | nettyrpc.client = client.connect(ip, port) 37 | nettyrpc.reactor = client 38 | 39 | var 40 | worker: Thread[ptr Channel[string]] 41 | input: Channel[string] 42 | input.open 43 | worker.createThread(inputLoop, input.addr) 44 | echo fmt"Hello {name}" 45 | 46 | while true: 47 | client.rpcTick() 48 | let (gotInput, msg) = input.tryRecv 49 | if gotInput: 50 | name.send(msg) 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /example/chatclient/client.nims: -------------------------------------------------------------------------------- 1 | switch("threads", "on") 2 | switch("gc", "orc") -------------------------------------------------------------------------------- /example/chatclient/server.nim: -------------------------------------------------------------------------------- 1 | import netty, nettyrpc, os 2 | 3 | # listen for a connection on localhost port 1999 4 | var server = newReactor("127.0.0.1", 1999) 5 | 6 | # Set nettyrpc reactor to server. 7 | nettyrpc.reactor = server 8 | 9 | # main loop 10 | while true: 11 | server.rpcTick(server=true) 12 | for msg in server.messages: # Display messages since last tick. 13 | echo msg 14 | 15 | # rpcTick(server=true) will automatically loop through messages 16 | # and dispatch/relay RPCs. It's the equivelant to the following 17 | # relay code: 18 | # 19 | # for msg in server.messages: 20 | # for client in server.connections: 21 | # if(msg.conn != client): 22 | # server.send(client, msg.data) 23 | # sleep(30) 24 | -------------------------------------------------------------------------------- /example/pong/assets/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beef331/nettyrpc/fdb294bced1a890939a1c0dab4008e3ae9c8a06e/example/pong/assets/font.png -------------------------------------------------------------------------------- /example/pong/assets/font.png.dat: -------------------------------------------------------------------------------- 1 | !"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ{}~ -------------------------------------------------------------------------------- /example/pong/build.nimble: -------------------------------------------------------------------------------- 1 | version = "0.1.0" 2 | author = "Jason" 3 | description = "TestProject" 4 | license = "MIT" 5 | 6 | requires "nico" 7 | requires "netty" 8 | requires "nettyrpc" 9 | 10 | task make, "Makes client and server": 11 | exec "nim c -d:debug ./server" 12 | exec "nim c -d:debug ./client" 13 | 14 | task tBuild, "tests": 15 | makeTask() 16 | try: 17 | exec "killall server" 18 | except: discard 19 | exec "sleep 1; ./server & sleep 1; ./client & sleep 1; ./client" -------------------------------------------------------------------------------- /example/pong/client.nim: -------------------------------------------------------------------------------- 1 | import netty, nettyrpc, nico, random, times 2 | import nico/vec 3 | 4 | randomize(now().nanosecond) 5 | let id = rand(1u32..10000000u32) 6 | 7 | var 8 | client = newReactor() 9 | c2s = client.connect("127.0.0.1", 1999) 10 | otherID = 0u32 11 | shouldHandshake = false 12 | otherLeft = false 13 | 14 | type 15 | GameState = enum 16 | gsWaiting, gsReady, gsPlaying, gsName 17 | Paddle = object 18 | y, color: int 19 | local: bool 20 | Ball = object 21 | owner: int32 22 | dirX, dirY: float32 23 | x, y, vel: float32 24 | 25 | # send message on the connection 26 | nettyrpc.reactor = client 27 | nettyrpc.client = c2s 28 | 29 | var 30 | currentState = gsName 31 | otherState = gsName 32 | yourName = "" 33 | otherName = "" 34 | paddles: array[2, Paddle] 35 | ball = Ball() 36 | leftScore, rightScore = 0 37 | 38 | proc lobbyFull: bool = otherID != 0 39 | 40 | proc join(a: uint32, name: string, isLeft = false) {.relayed.} = 41 | if not isLocal: 42 | # Attempt to handshake 43 | if(a != otherID): shouldHandshake = true 44 | otherID = a 45 | otherLeft = isLeft 46 | otherName = name 47 | 48 | proc changeState(gs: GameState) {.relayed.} = 49 | if isLocal: 50 | currentState = gs 51 | else: 52 | otherState = gs 53 | 54 | proc updatePaddle(p: Paddle) {.relayed.} = 55 | if(not isLocal): 56 | for x in paddles.mitems: 57 | if(not x.local): 58 | x.y = p.y 59 | 60 | proc updateBall(inBall: Ball) {.relayed.} = 61 | ball = inBall 62 | 63 | proc ballScored(leftScr, rightScr: int) {.relayed.} = 64 | leftScore = leftScr 65 | rightScore = rightScr 66 | 67 | proc init = 68 | loadFont(0, "font.png") 69 | 70 | proc generateBallDirection(b: var Ball) = 71 | ## Generates a random vector for the ball to travel 72 | let 73 | x = rand(0..1) * 2 - 1 74 | vect = vec2f(x, rand(-0.8..0.8)).normalized() 75 | ball.dirX = vect.x 76 | ball.dirY = vect.y 77 | ball.vel = 50 78 | 79 | proc update(dt: float32) = 80 | client.rpcTick() 81 | 82 | if(currentState == gsWaiting and otherID != 0 and keypr(K_RETURN)): 83 | changeState(gsReady) 84 | elif(currentState == gsReady and otherID != 0 and keypr(K_RETURN)): 85 | changeState(gsWaiting) 86 | elif currentState == gsName: 87 | for ch in {'a'..'z', ' '}: 88 | if ch.ord.Keycode.keypr: 89 | yourName.add ch 90 | if keypr(K_BACKSPACE) and yourName.len > 0: 91 | yourName = yourName[0.. 1: 93 | join(id, yourname) 94 | changeState(gsWaiting) 95 | 96 | if(currentState == gsReady and otherState in {gsReady, gsPlaying}): 97 | paddles[0] = Paddle(y: screenHeight.div(2) - 10, local: not otherLeft, color: 3) 98 | paddles[1] = Paddle(y: screenHeight.div(2) - 10, local: otherLeft, color: 4) 99 | ball = Ball(x: screenWidth.div(2).float32, y: screenHeight.div(2).float32) 100 | #Easy way to decide who is the "server" 101 | if(id > otherID): 102 | ball.generateBallDirection 103 | ball.owner = id 104 | changeState(gsPlaying) 105 | 106 | if(currentState == gsPlaying): 107 | if(ball.owner == id): 108 | #Ball collision 109 | if(ball.y + 2 >= paddles[0].y and ball.y - 2 <= paddles[0].y + 110 | 20 and ball.x - 2 <= 10): 111 | ball.dirX *= -1 112 | 113 | if(ball.y + 2 >= paddles[1].y and ball.y - 2 <= paddles[1].y + 114 | 20 and ball.x + 2 >= screenWidth - 10): 115 | ball.dirX *= -1 116 | 117 | if(ball.y - 2 <= 0): 118 | ball.dirY *= -1 119 | 120 | if(ball.y + 2 >= screenHeight): 121 | ball.dirY *= -1 122 | 123 | ball.x += ball.dirX * ball.vel * dt 124 | ball.y += ball.dirY * ball.vel * dt 125 | updateBall(ball) 126 | if(ball.x >= screenWidth or ball.x <= 0): 127 | let 128 | newLeftScore = if(ball.x >= screenWidth): 129 | leftScore + 1 130 | else: 131 | leftScore 132 | newRightScore = if(ball.x <= 0): 133 | rightScore + 1 134 | else: 135 | rightScore 136 | ballScored(newLeftScore, newRightScore) 137 | ball.x = screenWidth / 2 138 | ball.y = screenHeight / 2 139 | ball.generateBallDirection 140 | ball.updateBall 141 | 142 | for x in paddles.mitems: 143 | if(x.local): 144 | if(key(K_UP)): 145 | x.y -= 1 146 | if(key(K_DOWN)): 147 | x.y += 1 148 | x.y = clamp(x.y, 0, screenHeight-20) 149 | updatePaddle(x) 150 | 151 | #If we joined first we're left cause yes 152 | if(shouldHandshake): 153 | join(id, yourname, not otherLeft) 154 | shouldHandshake = false 155 | 156 | proc draw() = 157 | cls() 158 | setColor(5) 159 | 160 | if(currentState == gsWaiting and lobbyFull()): 161 | printc("Press enter when ready", screenWidth.div(2), 0) 162 | if(currentState == gsReady): 163 | printc("Waiting for other player", screenWidth.div(2), 0) 164 | if currentState == gsName: 165 | printc(yourName, screenWidth.div(2), 0) 166 | setColor(4) 167 | printc("Enter your name", screenWidth.div(2), 30) 168 | printc("Then press enter", screenWidth.div(2), 40) 169 | printc($currentState, screenWidth.div(2), screenHeight - 30) 170 | 171 | if(currentState == gsPlaying): 172 | setcolor(paddles[0].color) 173 | print($leftScore, 10, 10, 3) 174 | setColor(paddles[1].color) 175 | print($rightScore, screenWidth - 22, 10, 3) 176 | let 177 | leftName = if otherLeft: 178 | otherName 179 | else: 180 | yourName 181 | rightName = if otherLeft: 182 | yourName 183 | else: 184 | otherName 185 | 186 | setColor(paddles[0].color) 187 | rectfill(1, paddles[0].y, 6, paddles[0].y + 20) 188 | print(leftName, 0, screenHeight - 10) 189 | setColor(paddles[1].color) 190 | printr(rightName, screenWidth, screenHeight - 10) 191 | rectfill(screenWidth - 6, paddles[1].y, screenHeight - 1, paddles[1].y + 20) 192 | setColor(10) 193 | circfill(ball.x, ball.y, 2) 194 | 195 | nico.init("Blah", "blah") 196 | nico.createWindow("myApp", 128, 128, 4, false) 197 | nico.run(init, update, draw) -------------------------------------------------------------------------------- /example/pong/server.nim: -------------------------------------------------------------------------------- 1 | import netty, nettyrpc, os 2 | 3 | # listen for a connection on localhost port 1999 4 | var server = newReactor("127.0.0.1", 1999) 5 | 6 | nettyrpc.reactor = server 7 | 8 | # main loop 9 | while true: 10 | server.rpcTick(server=true) 11 | # rpcTick(server=true) will automatically loop through messages 12 | # and dispatch/relay RPCs. It's the equivelant to the following 13 | # relay code: 14 | # 15 | # for msg in server.messages: 16 | # echo msg 17 | # for client in server.connections: 18 | # if(msg.conn != client): 19 | # server.send(client, msg.data) 20 | sleep(10) 21 | -------------------------------------------------------------------------------- /nettyrpc.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.2.0" 4 | author = "Jason" 5 | description = "An RPC abstraction utilizing Netty's reliable UDP" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.4.0" 13 | requires "netty >= 0.2.1" -------------------------------------------------------------------------------- /src/nettyrpc.nim: -------------------------------------------------------------------------------- 1 | import std/[macros, macrocache, tables, hashes] 2 | import netty 3 | import nettyrpc/nettystream 4 | 5 | export nettystream 6 | 7 | type 8 | NettyRpcException = object of CatchableError 9 | Strings* = string or static string 10 | MessageType = enum 11 | Networked, Relayed 12 | var 13 | compEventCount{.compileTime.} = 0u16 14 | relayedEvents: array[uint16, proc(data: var NettyStream)] ## Ugly method of holding procedures 15 | managedEvents: Table[Hash, proc(data: var NettyStream, conn: Connection)] ## Uglier method of holding procedures 16 | reactor*: Reactor 17 | client*: Connection 18 | sendBuffer = NettyStream() 19 | relayBuffer = NettyStream() 20 | sendAllBuffer = NettyStream() 21 | directSends: Table[Connection, seq[NettyStream]] 22 | 23 | proc hash(conn: Connection): Hash = 24 | var h: Hash = 0 25 | h = h !& hash(conn.id) 26 | h = h !& hash(conn.address.host) 27 | result = !$h 28 | 29 | proc send*(conn: Connection, message: var NettyStream) = 30 | ## Sends the RPC message to the server to process directly. 31 | if reactor.isNil: 32 | raise newException(NettyRpcException, "Reactor is not set") 33 | if message.pos > 0: 34 | reactor.send(conn, message.getBuffer) 35 | message.clear() 36 | 37 | proc sendall*(message: string, exclude: Connection = nil) = 38 | ## Sends the RPC message to the server to process 39 | if reactor.isNil: 40 | raise newException(NettyRpcException, "Reactor is not set") 41 | 42 | if message.len > 0: 43 | for conn in reactor.connections: 44 | if conn != exclude: 45 | reactor.send(conn, message) 46 | 47 | proc sendall*(message: var NettyStream, exclude: Connection = nil) = 48 | ## Sends the RPC message to the server to process 49 | if reactor.isNil: 50 | raise newException(NettyRpcException, "Reactor is not set") 51 | if message.pos > 0: 52 | var d = message.getBuffer 53 | for conn in reactor.connections: 54 | if conn != exclude: 55 | reactor.send(conn, d) 56 | message.clear() 57 | 58 | proc writeRpcHeader(procName: Strings, ns: var NettyStream) {.inline.} = 59 | ## Write the MessageType and hashed procedure name to the send buffer. 60 | ns.write(MessageType.Networked) 61 | when procName.type is static string: 62 | const hashedName = hash(procName) # Calculate at compile time 63 | ns.write(hashedName) 64 | else: 65 | ns.write(hash(procName)) 66 | 67 | proc addDirectSend(conn: Connection, ns: var NettyStream) = 68 | if directSends.hasKey(conn): 69 | directSends[conn].add(ns) 70 | else: 71 | directSends[conn] = newSeq[NettyStream]() 72 | directSends[conn].add(ns) 73 | 74 | proc rpc*(conn: Connection, procName: Strings, vargs: tuple) = 75 | ## Send a rpc to a specific connection. 76 | var nb = NettyStream() 77 | writeRpcHeader(procName, nb) 78 | for k, v in vargs.fieldPairs: 79 | nb.write(v) 80 | addDirectSend(conn, nb) 81 | 82 | proc rpc*(procName: Strings, vargs: tuple) = 83 | ## Send a rpc to all connected clients. 84 | writeRpcHeader(procName, sendAllBuffer) 85 | for k, v in vargs.fieldPairs: 86 | sendAllBuffer.write(v) 87 | 88 | proc rpc*(conn: Connection, procName: Strings) = 89 | ## Send a rpc to a specific connection. 90 | var nb = NettyStream() 91 | writeRpcHeader(procName, nb) 92 | addDirectSend(conn, nb) 93 | 94 | proc rpc*(procName: Strings) = 95 | ## Send a rpc to all connected clients. 96 | writeRpcHeader(procName, sendAllBuffer) 97 | 98 | proc rpcTick*(sock: Reactor, server: bool = false) = 99 | sock.tick() 100 | if server: 101 | for k, s in directSends.pairs: # Direct sends from the server. 102 | var directSendStream = NettyStream() 103 | for stream in s: 104 | directSendStream.addToBuffer(stream.getBuffer) 105 | reactor.send(k, directSendStream.getBuffer) 106 | directSends.clear() 107 | else: 108 | if relayBuffer.pos > 0: 109 | sendBuffer.write(MessageType.Relayed) # Id 110 | sendBuffer.write(relayBuffer.getBuffer) # Writes len, message 111 | relayBuffer.clear() 112 | client.send(sendBuffer) # Relayed client 113 | 114 | sendall(sendAllBuffer) # Send to all connections 115 | 116 | var theBuffer = NettyStream() 117 | for msg in reactor.messages: 118 | theBuffer.clear() 119 | theBuffer.addToBuffer(msg.data) 120 | 121 | while(not theBuffer.atEnd): 122 | let 123 | start = theBuffer.pos 124 | messageType = theBuffer.read(MessageType) 125 | case messageType: 126 | of MessageType.Networked: 127 | let managedId = theBuffer.read(Hash) 128 | if managedEvents.hasKey(managedId): 129 | managedEvents[managedId](theBuffer, msg.conn) 130 | 131 | of MessageType.Relayed: 132 | let messageLength = theBuffer.read(int64) 133 | if server: 134 | let 135 | theEnd = theBuffer.pos + messageLength 136 | str = theBuffer.getBuffer[start..= ns.buffer.high 82 | proc size*(ns: NettyStream): int = ns.buffer.len 83 | proc clear*(ns: var NettyStream) = 84 | ns.pos = 0 85 | ns.buffer.setLen(0) -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /tests/tnettystream.nim: -------------------------------------------------------------------------------- 1 | import std/[unittest] 2 | import nettyrpc/nettystream 3 | 4 | suite "nettystream": 5 | test "object": 6 | type Test = object 7 | a: int 8 | b: string 9 | c: seq[int] 10 | let t = Test(a: 42, b: "Hello", c: @[10, 30, 40]) 11 | var ns = NettyStream() 12 | ns.write(t) 13 | ns.pos = 0 14 | var test: Test 15 | ns.read(test) 16 | assert t == test 17 | 18 | test "ref object": 19 | type Test = ref object 20 | a: int 21 | b: string 22 | c: seq[int] 23 | let t = Test(a: 42, b: "Hello", c: @[10, 30, 40]) 24 | var ns = NettyStream() 25 | ns.write(t) 26 | ns.pos = 0 27 | var test: Test 28 | ns.read(test) 29 | assert t[] == test[] 30 | 31 | test "enum": 32 | type Colour = enum 33 | red, green, yellow, indigo, violet 34 | var ns = NettyStream() 35 | for x in Colour.low..Colour.high: 36 | ns.write(x) 37 | ns.pos = 0 38 | var c: Colour 39 | for x in Colour.low..Colour.high: 40 | ns.read(c) 41 | assert c == x 42 | 43 | test "array": 44 | var ns = Nettystream() 45 | let data = [1, 2, 3, 4, 10, 50] 46 | ns.write(data) 47 | ns.pos = 0 48 | var test: data.type 49 | ns.read(test) 50 | assert test == data 51 | 52 | test "ints": 53 | var ns = Nettystream() 54 | ns.write(3170893824) 55 | ns.write(-13333333) 56 | ns.write(32132132132) 57 | ns.pos = 0 58 | var t: int 59 | ns.read t 60 | assert t == 3170893824 61 | ns.read t 62 | assert t == -13333333 63 | ns.read t 64 | assert t == 32132132132 65 | 66 | test "tuple": 67 | let a = (a: 1, b: 1.0, c: -1) 68 | var ns = NettyStream() 69 | ns.write(a) 70 | ns.pos = 0 71 | var b: a.type # Type mismatch error 72 | ns.read(b) 73 | assert a == b 74 | 75 | let c = (1, 1.0, -1) 76 | ns = NettyStream() 77 | ns.write(a) 78 | ns.pos = 0 79 | var d: a.type # Type mismatch error 80 | ns.read(d) 81 | assert c == d --------------------------------------------------------------------------------