├── .github └── workflows │ ├── build.yml │ └── docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs └── nettyBanner.png ├── examples ├── chatclient.nim ├── chatclientthreads.nim ├── chatclientthreads.nims ├── chatserver.nim ├── client.nim ├── eater │ ├── data │ │ ├── Changa-Bold.ttf │ │ ├── star.png │ │ └── tile.png │ ├── eaterclient.nim │ └── eaterserver.nim └── server.nim ├── netty.nimble ├── src ├── netty.nim └── netty │ └── timeseries.nim └── tests ├── config.nims ├── test.nim ├── test_timeseries.nim └── vtune.nim /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Github Actions 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | os: [ubuntu-latest, windows-latest] 9 | 10 | runs-on: ${{ matrix.os }} 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: jiro4989/setup-nim-action@v1 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | - run: nimble test -y 18 | - run: nimble test --gc:orc -y 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | nim-version: 'stable' 8 | nim-src: src/${{ github.event.repository.name }}.nim 9 | deploy-dir: .gh-pages 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: jiro4989/setup-nim-action@v1 16 | with: 17 | nim-version: ${{ env.nim-version }} 18 | - run: nimble install -Y 19 | - run: nimble doc --index:on --project --git.url:https://github.com/${{ github.repository }} --git.commit:master --out:${{ env.deploy-dir }} ${{ env.nim-src }} 20 | - name: "Copy to index.html" 21 | run: cp ${{ env.deploy-dir }}/${{ github.event.repository.name }}.html ${{ env.deploy-dir }}/index.html 22 | - name: Deploy documents 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ${{ env.deploy-dir }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore files with no extention: 2 | * 3 | !*/ 4 | !*.* 5 | 6 | # normal ignores: 7 | *.exe 8 | nimcache 9 | *.flippy 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Andre von Houck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Netty - reliable UDP connection for Nim. 4 | 5 | `nimble install netty` 6 | 7 | ![Github Actions](https://github.com/treeform/netty/workflows/Github%20Actions/badge.svg) 8 | 9 | [API reference](https://treeform.github.io/netty) 10 | 11 | ## About 12 | 13 | Netty is a reliable connection over UDP aimed at games. Normally UDP packets can get duplicated, dropped, or come out of order. Netty makes sure packets are not duplicated, re-sends them if they get dropped, and all packets come in order. UDP packets might also get split if they are above 512 bytes and also can fail to be sent if they are bigger than 1-2k. Netty breaks up big packets and sends them in pieces making sure each piece comes reliably in order. Finally sometimes it's impossible for two clients to communicate direclty with TCP because of NATs, but Netty provides hole punching which allows them to connect. 14 | 15 | ### Documentation 16 | 17 | API reference: https://treeform.github.io/netty 18 | 19 | ## Is Netty a implementation of TCP? 20 | 21 | TCP is really bad for short latency sensitive messages. TCP was designed for throughput (downloading files) not latency (games). Netty will resend stuff faster than TCP, Netty will not buffer and you also get nat punch-through (which TCP does not have). Netty is basically "like TCP but for games". You should not be using Netty if you are will be sending large mount of data. By default Netty is capped at 250K of data in flight. 22 | 23 | ## Features: 24 | 25 | | feature | TCP | UDP | Netty | 26 | | ------------------------- | ----- | -------- | ------- | 27 | | designed for low latency | no | yes | yes | 28 | | designed for throughput | yes | no | no | 29 | | packet framing | no | yes | yes | 30 | | packet ordering | yes | no | yes | 31 | | packet splitting | yes | no | yes | 32 | | packet retry | yes | no | yes | 33 | | packet reduplication | yes | no | yes | 34 | | hole punch through | no | yes | yes | 35 | | connection handling | yes | no | yes | 36 | | congestion control | yes | no | yes | 37 | 38 | 39 | # Echo Server/Client example 40 | 41 | ## server.nim 42 | 43 | ```nim 44 | import netty 45 | 46 | # listen for a connection on localhost port 1999 47 | var server = newReactor("127.0.0.1", 1999) 48 | echo "Listenting for UDP on 127.0.0.1:1999" 49 | # main loop 50 | while true: 51 | # must call tick to both read and write 52 | server.tick() 53 | # usually there are no new messages, but if there are 54 | for msg in server.messages: 55 | # print message data 56 | echo "GOT MESSAGE: ", msg.data 57 | # echo message back to the client 58 | server.send(msg.conn, "you said:" & msg.data) 59 | ``` 60 | 61 | ## client.nim 62 | 63 | ```nim 64 | import netty 65 | 66 | # create connection 67 | var client = newReactor() 68 | # connect to server 69 | var c2s = client.connect("127.0.0.1", 1999) 70 | # send message on the connection 71 | client.send(c2s, "hi") 72 | # main loop 73 | while true: 74 | # must call tick to both read and write 75 | client.tick() 76 | # usually there are no new messages, but if there are 77 | for msg in client.messages: 78 | # print message data 79 | echo "GOT MESSAGE: ", msg.data 80 | ``` 81 | 82 | # Chat Server/Client example 83 | 84 | ## chatserver.nim 85 | 86 | ```nim 87 | import netty 88 | 89 | var server = newReactor("127.0.0.1", 2001) 90 | echo "Listenting for UDP on 127.0.0.1:2001" 91 | while true: 92 | server.tick() 93 | for connection in server.newConnections: 94 | echo "[new] ", connection.address 95 | for connection in server.deadConnections: 96 | echo "[dead] ", connection.address 97 | for msg in server.messages: 98 | echo "[msg]", msg.data 99 | # send msg data to all connections 100 | for connection in server.connections: 101 | server.send(connection, msg.data) 102 | ``` 103 | 104 | ## chatclient.nim 105 | 106 | ```nim 107 | import netty 108 | 109 | var client = newReactor() 110 | var connection = client.connect("127.0.0.1", 2001) 111 | 112 | # get persons name 113 | echo "what is your name?" 114 | var name = readLine(stdin) 115 | echo "note: press enter to see if people sent you things" 116 | 117 | while true: 118 | client.tick() 119 | for msg in client.messages: 120 | echo msg.data 121 | 122 | # wait for user to type a line 123 | let line = readLine(stdin) 124 | if line.len > 0: 125 | client.send(connection, name & ":" & line) 126 | ``` 127 | -------------------------------------------------------------------------------- /docs/nettyBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/netty/4de6839c581787b6e57a171dd50a4dbb86f7b164/docs/nettyBanner.png -------------------------------------------------------------------------------- /examples/chatclient.nim: -------------------------------------------------------------------------------- 1 | import netty, os 2 | 3 | var client = newReactor() 4 | var connection = client.connect("127.0.0.1", 2001) 5 | 6 | # get persons name 7 | echo "what is your name?" 8 | var name = readLine(stdin) 9 | echo "note: press enter to see if people sent you things" 10 | 11 | while true: 12 | client.tick() 13 | for msg in client.messages: 14 | echo msg.data 15 | 16 | # wait for user to type a line 17 | let line = readLine(stdin) 18 | if line.len > 0: 19 | client.send(connection, name & ":" & line) 20 | sleep(1) 21 | -------------------------------------------------------------------------------- /examples/chatclientthreads.nim: -------------------------------------------------------------------------------- 1 | # nim c -r --threads:on --tlsEmulation:off tests\chatclientthreads 2 | 3 | import netty, os, terminal 4 | 5 | var client = newReactor() 6 | var connection = client.connect("127.0.0.1", 2001) 7 | 8 | # get persons name 9 | echo "what is your name?" 10 | var name = readLine(stdin) 11 | 12 | # handle ctrl-c correclty on windows by stopping all threads 13 | proc handleCtrlC() {.noconv.} = 14 | setupForeignThreadGc() 15 | quit(1) 16 | setControlCHook(handleCtrlC) 17 | 18 | # create a thread that just reads a single chart 19 | var 20 | thread: Thread[tuple[a: int]] 21 | singleChar: char 22 | proc readSingleChar(interval: tuple[a: int]) {.thread.} = 23 | while true: 24 | singleChar = getch() 25 | createThread(thread, readSingleChar, (0, )) 26 | 27 | # main loop 28 | var line: string 29 | while true: 30 | client.tick() 31 | for msg in client.messages: 32 | # we got a message, rase current line user is typing 33 | stdout.eraseLine() 34 | # write message line 35 | echo msg.data 36 | # write back the line was typing 37 | writeStyled(line) 38 | 39 | if singleChar != char(0): 40 | if singleChar == char(8): 41 | # handle backspace 42 | line.setLen(line.len - 1) 43 | else: 44 | line.add(singleChar) 45 | # a char got added, erase current line 46 | stdout.eraseLine() 47 | # write the line again 48 | writeStyled(line & " ") 49 | # put cursor in right spot 50 | stdout.setCursorXPos(line.len) 51 | 52 | if singleChar == char(13): 53 | # handle sending 54 | client.send(connection, name & ":" & line) 55 | # clear line, reset eveything 56 | # server should echo the line back 57 | line = "" 58 | stdout.eraseLine() 59 | stdout.setCursorXPos(0) 60 | 61 | # reset character 62 | singleChar = char(0) 63 | sleep(1) 64 | -------------------------------------------------------------------------------- /examples/chatclientthreads.nims: -------------------------------------------------------------------------------- 1 | --threads:on 2 | --tlsEmulation:off 3 | -------------------------------------------------------------------------------- /examples/chatserver.nim: -------------------------------------------------------------------------------- 1 | import netty, os 2 | 3 | var server = newReactor("127.0.0.1", 2001) 4 | echo "Listenting for UDP on 127.0.0.1:2001" 5 | while true: 6 | server.tick() 7 | for connection in server.newConnections: 8 | echo "[new] ", connection.address 9 | for connection in server.deadConnections: 10 | echo "[dead] ", connection.address 11 | for msg in server.messages: 12 | echo "[msg]", msg.data 13 | # send msg data to all connections 14 | for connection in server.connections: 15 | server.send(connection, msg.data) 16 | sleep(1) 17 | -------------------------------------------------------------------------------- /examples/client.nim: -------------------------------------------------------------------------------- 1 | import netty, os 2 | 3 | # create connection 4 | var client = newReactor() 5 | # connect to server 6 | var c2s = client.connect("127.0.0.1", 1999) 7 | # send message on the connection 8 | client.send(c2s, "hi") 9 | # main loop 10 | while true: 11 | # must call tick to both read and write 12 | client.tick() 13 | # usually there are no new messages, but if there are 14 | for msg in client.messages: 15 | # print message data 16 | echo "GOT MESSAGE: ", msg.data 17 | sleep(1) 18 | -------------------------------------------------------------------------------- /examples/eater/data/Changa-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/netty/4de6839c581787b6e57a171dd50a4dbb86f7b164/examples/eater/data/Changa-Bold.ttf -------------------------------------------------------------------------------- /examples/eater/data/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/netty/4de6839c581787b6e57a171dd50a4dbb86f7b164/examples/eater/data/star.png -------------------------------------------------------------------------------- /examples/eater/data/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/netty/4de6839c581787b6e57a171dd50a4dbb86f7b164/examples/eater/data/tile.png -------------------------------------------------------------------------------- /examples/eater/eaterclient.nim: -------------------------------------------------------------------------------- 1 | import fidget, fidget/opengl/base, fidget/opengl/context, netty, flatty, 2 | random, strformat, tables, vmath, bumpy 3 | 4 | randomize() 5 | 6 | type 7 | Player = object 8 | pos: Vec2 9 | vel: Vec2 10 | 11 | Packet = object 12 | id: int 13 | player: Player 14 | 15 | var 16 | myId = rand(0 .. 10000) 17 | me: Player 18 | client = newReactor() 19 | connection = client.connect("127.0.0.1", 2001) 20 | 21 | others: Table[int, Player] 22 | othersSmoothPos: Table[int, Vec2] 23 | 24 | debugPosSeq: seq[Vec2] 25 | 26 | client.debug.dropRate = 0.01 27 | client.debug.readLatency = 0.1 28 | client.debug.sendLatency = 0.1 29 | 30 | loadFont("Changa Bold", "Changa-Bold.ttf") 31 | 32 | proc tickMain() = 33 | # Note: this function runs at 240hz. 34 | #echo focused 35 | if focused and mouse.pos.overlaps(rect(vec2(0, 0), windowFrame)): 36 | if ((windowFrame / 2) - mouse.pos).length > 0: 37 | me.vel -= dir(windowFrame / 2, mouse.pos) * 0.1 38 | me.vel *= 0.9 # friction 39 | me.pos += me.vel 40 | 41 | if frameCount mod 10 == 0: 42 | client.send(connection, Packet(id: myId, player: me).toFlatty()) 43 | 44 | for msg in client.messages: 45 | var p = msg.data.fromFlatty(Packet) 46 | debugPosSeq.add(p.player.pos) 47 | if debugPosSeq.len > 100: 48 | debugPosSeq = debugPosSeq[1..^1] 49 | if p.id != myId: 50 | others[p.id] = p.player 51 | if p.id notin othersSmoothPos: 52 | othersSmoothPos[p.id] = p.player.pos 53 | 54 | client.tick() 55 | 56 | for id, other in others.mpairs: 57 | othersSmoothPos[id] = lerp(othersSmoothPos[id], other.pos, 0.01) + other.vel 58 | 59 | proc drawMain() = 60 | # Note: this functions runs at monitor refresh rate (usually 60hz). 61 | clearColorBuffer(color(1, 1, 1, 1)) 62 | 63 | ctx.saveTransform() 64 | ctx.translate(-me.pos + windowFrame / 2) 65 | 66 | for x in 0 .. 10: 67 | for y in 0 .. 10: 68 | ctx.drawSprite("data/tile.png", vec2(x.float32 * 100, y.float32 * 100)) 69 | 70 | ctx.drawSprite("data/star.png", me.pos) 71 | 72 | for id, other in others.pairs: 73 | ctx.drawSprite("data/star.png", othersSmoothPos[id], color = color(1, 0, 0, 1)) 74 | 75 | for pos in debugPosSeq: 76 | ctx.drawSprite("data/star.png", pos, color = color(0, 1, 0, 1), scale = 0.05) 77 | 78 | ctx.restoreTransform() 79 | 80 | font "Changa Bold", 20, 200, 40, hLeft, vTop 81 | text "networks": 82 | box 30, 30, 400, 600 83 | fill "#202020" 84 | textAlign hLeft, vTop 85 | characters &""" 86 | Fps: {1/avgFrameTime} 87 | Network: 88 | avgLatency: {(connection.stats.latencyTs.avg()*1000).int} ms 89 | maxLatency: {(connection.stats.latencyTs.max()*1000).int} ms 90 | dropRate: {client.debug.dropRate*100} % 91 | inFlight: {connection.stats.inFlight} bytes 92 | throughput: {connection.stats.throughputTs.avg().int} bytes 93 | """ 94 | 95 | startFidget( 96 | draw = drawMain, 97 | tick = tickMain, 98 | w = 1280, 99 | h = 800, 100 | openglVersion = (4, 1), 101 | msaa = msaa4x, 102 | mainLoopMode = RepaintSplitUpdate 103 | ) 104 | -------------------------------------------------------------------------------- /examples/eater/eaterserver.nim: -------------------------------------------------------------------------------- 1 | import netty, os 2 | 3 | var server = newReactor("127.0.0.1", 2001) 4 | echo "Listenting for UDP on ", server.address 5 | while true: 6 | server.tick() 7 | for connection in server.newConnections: 8 | echo "[new] ", connection.address 9 | for connection in server.deadConnections: 10 | echo "[dead] ", connection.address 11 | for msg in server.messages: 12 | # send packet data to all connections 13 | for connection in server.connections: 14 | server.send(connection, msg.data) 15 | sleep(1) 16 | -------------------------------------------------------------------------------- /examples/server.nim: -------------------------------------------------------------------------------- 1 | import netty, os 2 | 3 | # listen for a connection on localhost port 1999 4 | var server = newReactor("127.0.0.1", 1999) 5 | echo "Listenting for UDP on 127.0.0.1:1999" 6 | # main loop 7 | while true: 8 | # must call tick to both read and write 9 | server.tick() 10 | # usually there are no new messages, but if there are 11 | for msg in server.messages: 12 | # print message data 13 | echo "GOT MESSAGE: ", msg.data 14 | # echo message back to the client 15 | server.send(msg.conn, "you said:" & msg.data) 16 | sleep(1) 17 | -------------------------------------------------------------------------------- /netty.nimble: -------------------------------------------------------------------------------- 1 | version = "0.2.1" 2 | author = "Andre von Houck" 3 | description = "Netty is a reliable UDP connection for games in Nim." 4 | license = "MIT" 5 | srcDir = "src" 6 | 7 | requires "nim >= 0.19.0" 8 | requires "flatty >= 0.3.4" 9 | -------------------------------------------------------------------------------- /src/netty.nim: -------------------------------------------------------------------------------- 1 | import flatty/binny, hashes, nativesockets, net, netty/timeseries, random, 2 | sequtils, std/monotimes, strformat, times, os 3 | 4 | export Port, timeseries 5 | 6 | const 7 | partMagic = 0xFFDDFF33.uint32 8 | ackMagic = 0xFF33FF11.uint32 9 | disconnectMagic = 0xFF77FF99.uint32 10 | punchMagic = 0x00000000.uint32 11 | headerSize = 4 + 4 + 4 + 2 + 2 12 | ackTime = 0.250 ## Seconds to wait before sending the packet again. 13 | connTimeout = 10.00 ## Seconds to wait until timing-out the connection. 14 | defaultMaxUdpPacket = 508 - headerSize 15 | defaultMaxInFlight = 25_000 16 | 17 | type 18 | Address* = object 19 | ## A host/port of the client. 20 | host*: string 21 | port*: Port 22 | 23 | DebugConfig* = object 24 | tickTime*: float64 ## Override the time processed by calls to tick. 25 | dropRate*: float32 ## [0, 1] % simulated drop rate. 26 | readLatency*: float32 ## Min simulated read latency in seconds. 27 | sendLatency*: float32 ## Min simulated send latency in seconds. 28 | maxUdpPacket*: int ## Max size of each outgoing UDP packet in bytes. 29 | 30 | Reactor* = ref object 31 | ## Main networking system that can open or receive connections. 32 | r: Rand 33 | id*: uint32 34 | address*: Address 35 | socket: Socket 36 | time: float64 37 | maxInFlight*: int ## Max bytes in-flight on the socket. 38 | debug*: DebugConfig 39 | 40 | connections*: seq[Connection] 41 | newConnections*: seq[Connection] ## New connections since last tick. 42 | deadConnections*: seq[Connection] ## Dead connections since last tick. 43 | messages*: seq[Message] 44 | 45 | ConnectionStats* = object 46 | inFlight*: int ## How many bytes are currently in flight. 47 | inQueue*: int ## How many bytes are currently waiting to be sent. 48 | saturated*: bool ## If this conn cannot send until it receives acks. 49 | latencyTs*: TimeSeries 50 | throughputTs*: TimedSamples 51 | 52 | Connection* = ref object 53 | id*: uint32 54 | reactorId*: uint32 55 | address*: Address 56 | stats*: ConnectionStats 57 | lastActiveTime*: float64 58 | 59 | sendParts: seq[Part] ## Parts queued to be sent. 60 | recvParts: seq[Part] ## Parts that have been read from the socket. 61 | sendSequenceNum: uint32 ## Next message sequence num when sending. 62 | recvSequenceNum: uint32 ## Next message sequence number to receive. 63 | 64 | Part = ref object 65 | ## Part of a Message. 66 | sequenceNum: uint32 ## The message sequence number. 67 | connId: uint32 ## The id of the connection this belongs to. 68 | numParts: uint16 ## How many parts there are to the Message this part of. 69 | partNum: uint16 ## The part of the Message this is. 70 | data: string 71 | 72 | # Sending 73 | queuedTime: float64 74 | sentTime: float64 75 | acked: bool 76 | ackedTime: float64 77 | 78 | Message* = object 79 | conn*: Connection 80 | sequenceNum*: uint32 81 | data*: string 82 | 83 | func initAddress*(host: string, port: int): Address = 84 | result.host = host 85 | result.port = Port(port) 86 | 87 | func `$`*(address: Address): string = 88 | ## Address to string. 89 | &"{address.host}:{address.port.int}" 90 | 91 | func `$`*(conn: Connection): string = 92 | ## Connection to string. 93 | &"Connection({conn.address}, id:{conn.id}, reactor: {conn.reactorId})" 94 | 95 | func `$`*(part: Part): string = 96 | ## Part to string. 97 | &"Part({part.sequenceNum}:{part.partNum}/{part.numParts} ACK:{part.acked})" 98 | 99 | func `$`*(msg: Message): string = 100 | ## Message to string. 101 | &"Message(from: {msg.conn.address} #{msg.sequenceNum}, size:{msg.data.len})" 102 | 103 | func hash*(x: Address): Hash = 104 | ## Computes a hash for the address. 105 | hash((x.host, x.port)) 106 | 107 | func genId(reactor: Reactor): uint32 {.inline.} = 108 | reactor.r.rand(0u32..uint32.high).uint32 109 | 110 | func newConnection(reactor: Reactor, address: Address): Connection = 111 | result = Connection() 112 | result.id = reactor.genId() 113 | result.reactorId = reactor.id 114 | result.address = address 115 | 116 | result.stats.latencyTs = newTimeSeries() 117 | result.stats.throughputTs = newTimedSamples() 118 | 119 | func getConn(reactor: Reactor, connId: uint32): Connection = 120 | for conn in reactor.connections: 121 | if conn.id == connId: 122 | return conn 123 | 124 | func read(reactor: Reactor, conn: Connection): (bool, Message) = 125 | if conn.recvParts.len == 0: 126 | return 127 | 128 | let 129 | sequenceNum = conn.recvSequenceNum 130 | numParts = conn.recvParts[0].numParts 131 | 132 | if conn.recvParts.len < numParts.int: 133 | return 134 | 135 | var good = true 136 | for i in 0.uint16 ..< numParts: 137 | if conn.recvParts[i].ackedTime + reactor.debug.readLatency > reactor.time: 138 | break 139 | 140 | if not(conn.recvParts[i].sequenceNum == sequenceNum and 141 | conn.recvParts[i].numParts == numParts and 142 | conn.recvParts[i].partNum == i): 143 | good = false 144 | break 145 | 146 | if not good: 147 | return 148 | 149 | result[0] = true 150 | result[1].conn = conn 151 | result[1].sequenceNum = sequenceNum 152 | 153 | for i in 0.uint16 ..< numParts: 154 | result[1].data.add(conn.recvParts[i].data) 155 | 156 | inc conn.recvSequenceNum 157 | conn.recvParts.delete(0, numParts - 1) 158 | 159 | func divideAndSend(reactor: Reactor, conn: Connection, data: string) = 160 | ## Divides a packet into parts and gets it ready to be sent. 161 | assert data.len != 0 162 | conn.stats.inQueue += data.len 163 | 164 | var 165 | parts: seq[Part] 166 | partNum: uint16 167 | at: int 168 | 169 | while at < data.len: 170 | var part = Part() 171 | part.sequenceNum = conn.sendSequenceNum 172 | part.connId = conn.id 173 | part.partNum = partNum 174 | inc partNum 175 | 176 | let maxAt = min(at + reactor.debug.maxUdpPacket, data.len) 177 | part.data = data[at ..< maxAt] 178 | at = maxAt 179 | parts.add(part) 180 | 181 | assert parts.len < high(uint16).int 182 | 183 | for part in parts.mitems: 184 | part.numParts = parts.len.uint16 185 | part.queuedTime = reactor.time 186 | 187 | conn.sendParts.add(parts) 188 | inc conn.sendSequenceNum 189 | 190 | proc rawSend(reactor: Reactor, address: Address, packet: string) = 191 | ## Low level send to a socket. 192 | if reactor.debug.dropRate != 0: 193 | if reactor.r.rand(1.0) <= reactor.debug.dropRate: 194 | return 195 | try: 196 | reactor.socket.sendTo(address.host, address.port, packet) 197 | except: 198 | return 199 | 200 | proc sendNeededParts(reactor: Reactor) = 201 | for conn in reactor.connections: 202 | var 203 | inFlight: int 204 | saturated: bool 205 | for part in conn.sendParts: 206 | if inFlight + part.data.len > reactor.maxInFlight: 207 | saturated = true 208 | break 209 | 210 | if part.acked or (part.sentTime + ackTime > reactor.time): 211 | continue 212 | 213 | if part.queuedTime + reactor.debug.sendLatency > reactor.time: 214 | continue 215 | 216 | inFlight += part.data.len 217 | conn.stats.inQueue -= part.data.len 218 | 219 | part.sentTime = reactor.time 220 | 221 | var packet = newStringOfCap(headerSize + part.data.len) 222 | packet.addUint32(partMagic) 223 | packet.addUint32(part.sequenceNum) 224 | packet.addUint32(part.connId) 225 | packet.addUint16(part.partNum) 226 | packet.addUint16(part.numParts) 227 | packet.addStr(part.data) 228 | 229 | reactor.rawSend(conn.address, packet) 230 | 231 | conn.stats.inFlight = inFlight 232 | conn.stats.saturated = saturated 233 | 234 | proc sendSpecial( 235 | reactor: Reactor, conn: Connection, part: Part, magic: uint32 236 | ) = 237 | assert reactor.id == conn.reactorId 238 | assert conn.id == part.connId 239 | 240 | var packet = newStringOfCap(headerSize) 241 | packet.addUint32(magic) 242 | packet.addUint32(part.sequenceNum) 243 | packet.addUint32(part.connId) 244 | packet.addUint16(part.partNum) 245 | packet.addUint16(part.numParts) 246 | 247 | reactor.rawSend(conn.address, packet) 248 | 249 | func deleteAckedParts(reactor: Reactor) = 250 | for conn in reactor.connections: 251 | var pos, bytesAcked: int 252 | for part in conn.sendParts: 253 | if not part.acked: 254 | break 255 | inc pos 256 | bytesAcked += part.data.len 257 | 258 | if pos > 0: 259 | var minTime = float64.high 260 | for i in 0 ..< pos: 261 | let part = conn.sendParts[i] 262 | minTime = min(minTime, part.queuedTime) 263 | 264 | conn.stats.latencyTs.add((reactor.time - minTime).float32) 265 | conn.sendParts.delete(0, pos - 1) 266 | 267 | conn.stats.throughputTs.add(reactor.time, bytesAcked.float64) 268 | 269 | proc readParts(reactor: Reactor) = 270 | var 271 | buf = newStringOfCap(reactor.debug.maxUdpPacket + headerSize) 272 | host: string 273 | port: Port 274 | 275 | for _ in 0 ..< 1000: 276 | var byteLen: int 277 | try: 278 | byteLen = reactor.socket.recvFrom( 279 | buf, reactor.debug.maxUdpPacket + headerSize, host, port 280 | ) 281 | except: 282 | when defined(nettyMagicSleep): 283 | sleep(1) 284 | break 285 | 286 | let address = initAddress(host, port.int) 287 | 288 | var magic = buf.readUint32(0) 289 | if magic == disconnectMagic: 290 | let connId = buf.readUint32(4) 291 | var conn = reactor.getConn(connId) 292 | if conn != nil: 293 | reactor.deadConnections.add(conn) 294 | reactor.connections.delete(reactor.connections.find(conn)) 295 | continue 296 | 297 | if magic == punchMagic: 298 | # echo &"Received punch through from {address}" 299 | continue 300 | 301 | if byteLen < headerSize: 302 | # A valid packet will have at least the header. 303 | # echo &"Received packet of invalid size {reactor.address}" 304 | break 305 | 306 | var part = Part() 307 | part.sequenceNum = buf.readUint32(4) 308 | part.connId = buf.readUint32(8) 309 | part.partNum = buf.readUint16(12) 310 | part.numParts = buf.readUint16(14) 311 | part.data = buf.readStr(16, buf.len - 1) 312 | 313 | var conn = reactor.getConn(part.connId) 314 | if conn == nil: 315 | if magic == partMagic and part.sequenceNum == 0 and part.partNum == 0: 316 | conn = newConnection(reactor, address) 317 | conn.id = part.connId 318 | reactor.connections.add(conn) 319 | reactor.newConnections.add(conn) 320 | else: 321 | continue 322 | 323 | if reactor.debug.dropRate > 0.0: 324 | if reactor.r.rand(1.0) <= reactor.debug.dropRate: 325 | continue 326 | 327 | conn.lastActiveTime = reactor.time 328 | 329 | if magic == partMagic: 330 | part.acked = true 331 | part.ackedTime = reactor.time 332 | reactor.sendSpecial(conn, part, ackMagic) 333 | 334 | if part.sequenceNum < conn.recvSequenceNum: 335 | continue 336 | 337 | var pos: int 338 | for p in conn.recvParts: 339 | if p.sequenceNum > part.sequenceNum: 340 | break 341 | 342 | if p.sequenceNum == part.sequenceNum: 343 | if p.partNum > part.partNum: 344 | break 345 | 346 | if p.partNum == part.partNum: 347 | # Duplicate 348 | pos = -1 349 | assert p.data == part.data 350 | break 351 | 352 | inc pos 353 | 354 | if pos != -1: # If not a duplicate 355 | conn.recvParts.insert(part, pos) 356 | 357 | elif magic == ackMagic: 358 | for p in conn.sendParts: 359 | if p.sequenceNum == part.sequenceNum and 360 | p.numParts == part.numParts and 361 | p.partNum == part.partNum: 362 | if not p.acked: 363 | p.acked = true 364 | p.ackedTime = reactor.time 365 | 366 | else: 367 | # Unrecognized packet 368 | discard 369 | 370 | func combineParts(reactor: Reactor) = 371 | for conn in reactor.connections.mitems: 372 | while true: 373 | let (gotMsg, msg) = reactor.read(conn) 374 | if gotMsg: 375 | reactor.messages.add(msg) 376 | else: 377 | break 378 | 379 | func timeoutConnections(reactor: Reactor) = 380 | ## See if any connections have timed out. 381 | var i = 0 382 | while i < reactor.connections.len: 383 | let conn = reactor.connections[i] 384 | if conn.lastActiveTime + connTimeout <= reactor.time: 385 | reactor.deadConnections.add(conn) 386 | reactor.connections.delete(i) 387 | continue 388 | inc i 389 | 390 | proc tick*(reactor: Reactor) = 391 | if reactor.debug.tickTime != 0: 392 | reactor.time = reactor.debug.tickTime 393 | else: 394 | reactor.time = epochTime() 395 | 396 | reactor.newConnections.setLen(0) 397 | reactor.deadConnections.setLen(0) 398 | reactor.messages.setLen(0) 399 | 400 | reactor.sendNeededParts() 401 | reactor.readParts() 402 | reactor.combineParts() 403 | reactor.deleteAckedParts() 404 | reactor.timeoutConnections() 405 | 406 | func connect*(reactor: Reactor, address: Address): Connection = 407 | ## Starts a new connection to an address. 408 | result = newConnection(reactor, address) 409 | result.reactorId = reactor.id 410 | result.lastActiveTime = reactor.time 411 | reactor.connections.add(result) 412 | reactor.newConnections.add(result) 413 | 414 | func connect*(reactor: Reactor, host: string, port: int): Connection = 415 | ## Starts a new connection to host and port. 416 | reactor.connect(initAddress(host, port)) 417 | 418 | func send*(reactor: Reactor, conn: Connection, data: string) = 419 | assert reactor.id == conn.reactorId 420 | reactor.divideAndSend(conn, data) 421 | 422 | proc sendMagic( 423 | reactor: Reactor, 424 | address: Address, 425 | magic: uint32, 426 | connId: uint32, 427 | extra = "" 428 | ) = 429 | var packet = newStringOfCap(4 + 4 + extra.len) 430 | packet.addUint32(magic) 431 | packet.addUint32(connId) 432 | packet.addStr(extra) 433 | 434 | reactor.socket.sendTo(address.host, address.port, packet) 435 | 436 | proc disconnect*(reactor: Reactor, conn: Connection) = 437 | ## Disconnects the connection. 438 | assert reactor.id == conn.reactorId 439 | for i in 0 .. 10: 440 | reactor.sendMagic(conn.address, disconnectMagic, conn.id) 441 | reactor.deadConnections.add(conn) 442 | let index = reactor.connections.find(conn) 443 | if index != -1: 444 | reactor.connections.delete(index) 445 | 446 | proc punchThrough*(reactor: Reactor, address: Address) = 447 | ## Tries to punch through to host/port. 448 | for i in 0 .. 10: 449 | reactor.sendMagic(address, punchMagic, 0, "punch through") 450 | 451 | proc punchThrough*(reactor: Reactor, host: string, port: int) = 452 | ## Tries to punch through to host/port. 453 | reactor.punchThrough(initAddress(host, port)) 454 | 455 | proc newReactor*(address: Address): Reactor = 456 | ## Creates a new reactor with address. 457 | result = Reactor() 458 | result.r = initRand(getMonoTime().ticks) 459 | result.id = result.genId() 460 | result.maxInFlight = defaultMaxInFlight 461 | 462 | result.address = address 463 | result.socket = newSocket( 464 | Domain.AF_INET, 465 | SockType.SOCK_DGRAM, 466 | Protocol.IPPROTO_UDP, 467 | buffered = false 468 | ) 469 | result.socket.getFd().setBlocking(false) 470 | result.socket.bindAddr(result.address.port, result.address.host) 471 | 472 | let (_, portLocal) = result.socket.getLocalAddr() 473 | result.address.port = portLocal 474 | 475 | result.debug.maxUdpPacket = defaultMaxUdpPacket 476 | 477 | result.tick() 478 | 479 | proc newReactor*(host: string, port: int): Reactor = 480 | ## Creates a new reactor with host and port. 481 | newReactor(initAddress(host, port)) 482 | 483 | proc newReactor*(): Reactor = 484 | ## Creates a new reactor with system chosen address. 485 | newReactor("", 0) 486 | -------------------------------------------------------------------------------- /src/netty/timeseries.nim: -------------------------------------------------------------------------------- 1 | type 2 | TimeSeries* = ref object 3 | ## Helps you time stuff over multiple frames. 4 | at: Natural 5 | filled: Natural 6 | data: seq[float64] 7 | 8 | func newTimeSeries*(max: Natural = 1000): TimeSeries = 9 | ## Create new time series. 10 | result = TimeSeries() 11 | result.data = newSeq[float64](max) 12 | 13 | func add*(timeSeries: var TimeSeries, value: float64) = 14 | ## Add sample to time series. 15 | if timeSeries.at >= timeSeries.data.len: 16 | timeSeries.at = 0 17 | timeSeries.data[timeSeries.at] = value 18 | inc timeSeries.at 19 | timeSeries.filled = max(timeSeries.filled, timeSeries.at) 20 | 21 | func avg*(timeSeries: TimeSeries): float64 = 22 | ## Get average value of the time series samples. 23 | var total: float64 24 | for sample in timeSeries.data[0 ..< timeSeries.filled]: 25 | total += sample 26 | if timeSeries.filled > 0: 27 | return total / timeSeries.filled.float64 28 | 29 | func max*(timeSeries: TimeSeries): float64 = 30 | ## Get max value of the time series samples. 31 | timeSeries.data.max() 32 | 33 | type 34 | TimedSamples* = ref object 35 | ## Helps you time values of stuff over multiple frames. 36 | at: Natural 37 | filled: Natural 38 | data: seq[(float64, float64)] 39 | 40 | func newTimedSamples*(max: Natural = 1000): TimedSamples = 41 | ## Create new timed sample series. 42 | result = TimedSamples() 43 | result.data = newSeq[(float64, float64)](max) 44 | 45 | func add*(timedSamples: var TimedSamples, time: float64, value: float64) = 46 | ## Add sample value to the series. 47 | if timedSamples.at >= timedSamples.data.len: 48 | timedSamples.at = 0 49 | timedSamples.data[timedSamples.at] = (time, value) 50 | inc timedSamples.at 51 | timedSamples.filled = max(timedSamples.filled, timedSamples.at) 52 | 53 | func avg*(timedSamples: TimedSamples): float64 = 54 | ## Get average value of the values in the samples. 55 | var 56 | total: float64 57 | earliest = high(float64) 58 | latest = 0.float64 59 | for (time, value) in timedSamples.data[0 ..< timedSamples.filled]: 60 | total += value 61 | earliest = min(earliest, time) 62 | latest = max(latest, time) 63 | 64 | if timedSamples.filled > 0: 65 | let delta = latest - earliest 66 | if delta > 0: 67 | return total / delta 68 | else: 69 | return total 70 | 71 | func max*(timedSamples: TimedSamples): float64 = 72 | ## Get max value of the values in the samples. 73 | if timedSamples.data.len > 0: 74 | result = timedSamples.data[0][1] 75 | for (time, value) in timedSamples.data[0 ..< timedSamples.filled]: 76 | result = max(result, value) 77 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | --path:"../src" 2 | # On macOS localhost writes take longer, so sleep in strategic places 3 | # to make it like Windows and Linux. 4 | --define:"nettyMagicSleep" 5 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import flatty/binny, os, strformat 2 | 3 | include netty 4 | 5 | var nextPortNumber = 3000 6 | proc nextPort(): int = 7 | ## Use next port, so that we don't reuse ports during test. 8 | result = nextPortNumber 9 | inc nextPortNumber 10 | 11 | block: 12 | # Text simple send. 13 | var server = newReactor("127.0.0.1", nextPort()) 14 | var client = newReactor() 15 | var c2s = client.connect(server.address) 16 | client.send(c2s, "hi") 17 | client.tick() 18 | server.tick() 19 | doAssert server.messages.len == 1 20 | doAssert server.messages[0].data == "hi" 21 | 22 | block: 23 | # Tewxt sends and acks. 24 | 25 | var server = newReactor("127.0.0.1", nextPort()) 26 | var client = newReactor("127.0.0.1", nextPort()) 27 | 28 | # connect 29 | var c2s = client.connect(server.address) 30 | 31 | # client --------- 'hey you' ----------> server 32 | 33 | client.send(c2s, "hey you") 34 | client.tick() 35 | server.tick() 36 | 37 | # server should have message 38 | doAssert server.messages.len == 1 39 | 40 | # client should have part ACK:false 41 | doAssert client.connections[0].sendParts.len == 1 42 | doAssert client.connections[0].recvParts.len == 0 43 | 44 | server.tick() # get message, ack message 45 | client.tick() # get ack 46 | 47 | # client should not have any parts now, acked parts deleted 48 | doAssert client.connections[0].sendParts.len == 0 49 | doAssert client.connections[0].recvParts.len == 0 50 | 51 | # id should match 52 | doAssert server.connections[0].id == client.connections[0].id 53 | 54 | block: 55 | # Text single client disconnect. 56 | var server = newReactor("127.0.0.1", nextPort()) 57 | var client = newReactor() 58 | client.debug.tickTime = 1.0 59 | client.tick() 60 | var c2s = client.connect(server.address) 61 | client.send(c2s, "hi") 62 | client.tick() 63 | server.tick() 64 | client.tick() 65 | doAssert len(server.messages) == 1, $server.messages.len 66 | doAssert len(server.connections) == 1, $server.connections.len 67 | client.debug.tickTime = 1.0 + connTimeout 68 | client.tick() 69 | doAssert len(client.deadConnections) == 1 70 | doAssert len(client.connections) == 0 71 | 72 | block: 73 | # Text large message. 74 | 75 | var server = newReactor("127.0.0.1", nextPort()) 76 | var client = newReactor("127.0.0.1", nextPort()) 77 | 78 | doAssert client.debug.maxUdpPacket == 492 79 | var buffer = "large:" 80 | for i in 0 ..< 1000: 81 | buffer.add "" 82 | doAssert buffer.len == 6006 83 | var c2s = client.connect(server.address) 84 | client.send(c2s, buffer) 85 | 86 | for i in 0 ..< 10: 87 | client.tick() 88 | server.tick() 89 | 90 | for msg in server.messages: 91 | # large packets match 92 | doAssert msg.data == buffer 93 | 94 | block: 95 | # Stress test many messages. 96 | 97 | var dataToSend = newSeq[string]() 98 | 99 | for i in 0 ..< 1000: 100 | dataToSend.add &"data #{i}, its cool!" 101 | 102 | # stress 103 | var server = newReactor("127.0.0.1", nextPort()) 104 | var client = newReactor("127.0.0.1", nextPort()) 105 | var c2s = client.connect(server.address) 106 | for d in dataToSend: 107 | client.send(c2s, d) 108 | for i in 0 ..< 1000: 109 | client.tick() 110 | server.tick() 111 | sleep(1) 112 | for msg in server.messages: 113 | var index = dataToSend.find(msg.data) 114 | # make sure message is there 115 | doAssert index != -1 116 | dataToSend.delete(index) 117 | if dataToSend.len == 0: break 118 | # make sure all messages made it 119 | doAssert dataToSend.len == 0, &"datatoSend.len: {datatoSend.len}" 120 | 121 | block: 122 | # Stress test many messages with packet loss 10%. 123 | 124 | var dataToSend = newSeq[string]() 125 | for i in 0 ..< 1000: 126 | dataToSend.add &"data #{i}, its cool!" 127 | 128 | # stress 129 | var server = newReactor("127.0.0.1", nextPort()) 130 | var client = newReactor("127.0.0.1", nextPort()) 131 | client.debug.dropRate = 0.2 # 20% packet loss rate is broken for most things 132 | 133 | var c2s = client.connect(server.address) 134 | for d in dataToSend: 135 | client.send(c2s, d) 136 | for i in 0 ..< 1000: 137 | client.tick() 138 | server.tick() 139 | sleep(2) 140 | for msg in server.messages: 141 | var index = dataToSend.find(msg.data) 142 | # make sure message is there 143 | doAssert index != -1 144 | dataToSend.delete(index) 145 | if dataToSend.len == 0: break 146 | # make sure all messages made it 147 | doAssert dataToSend.len == 0 148 | 149 | block: 150 | # Stress test many clients. 151 | 152 | var dataToSend = newSeq[string]() 153 | for i in 0 ..< 100: 154 | dataToSend.add &"data #{i}, its cool!" 155 | 156 | # stress 157 | var server = newReactor("127.0.0.1", nextPort()) 158 | for d in dataToSend: 159 | var client = newReactor() 160 | var c2s = client.connect(server.address) 161 | client.send(c2s, d) 162 | client.tick() 163 | 164 | server.debug.tickTime = 1.0 165 | server.tick() 166 | 167 | doAssert len(server.connections) == 100 168 | doAssert len(server.newConnections) == 100 169 | 170 | for msg in server.messages: 171 | var index = dataToSend.find(msg.data) 172 | # make sure message is there 173 | doAssert index != -1 174 | dataToSend.delete(index) 175 | # make sure all messages made it 176 | doAssert dataToSend.len == 0 177 | 178 | server.debug.tickTime = 1.0 + connTimeout 179 | server.tick() 180 | 181 | doAssert len(server.connections) == 0, $server.connections.len 182 | doAssert len(server.deadConnections) == 100, $server.deadConnections.len 183 | 184 | block: 185 | # Test punch through. 186 | var server = newReactor("127.0.0.1", nextPort()) 187 | var client = newReactor() 188 | var c2s = client.connect(server.address) 189 | client.punchThrough(server.address) 190 | client.send(c2s, "hi") 191 | client.tick() 192 | server.tick() 193 | doAssert server.messages.len == 1 194 | doAssert server.messages[0].data == "hi" 195 | 196 | block: 197 | # Test maxUdpPacket and maxInFlight. 198 | 199 | var server = newReactor("127.0.0.1", nextPort()) 200 | var client = newReactor("127.0.0.1", nextPort()) 201 | 202 | client.debug.maxUdpPacket = 100 203 | client.maxInFlight = 10_000 204 | 205 | var buffer = "large:" 206 | for i in 0 ..< 1000: 207 | buffer.add "" 208 | 209 | var c2s = client.connect(server.address) 210 | client.send(c2s, buffer) 211 | client.send(c2s, buffer) 212 | 213 | doAssert c2s.sendParts.len == 122 214 | 215 | client.tick() # can only send 100 parts due to maxInFlight and maxUdpPacket 216 | 217 | doAssert c2s.stats.saturated == true 218 | 219 | server.tick() # receives 100 parts, sends acks back 220 | 221 | doAssert server.messages.len == 1, &"len: {server.messages.len}" 222 | doAssert c2s.stats.inFlight < client.maxInFlight, 223 | &"stats.inFlight: {c2s.stats.inFlight}" 224 | doAssert c2s.stats.saturated == true 225 | 226 | client.tick() # process the 100 acks, 22 parts left in flight 227 | 228 | doAssert c2s.sendParts.len == 22 229 | doAssert c2s.stats.inFlight == 2106, &"stats.inFlight: {c2s.stats.inFlight}" 230 | doAssert c2s.stats.saturated == false 231 | 232 | server.tick() # process the last 22 parts, send 22 acks 233 | 234 | doAssert server.messages.len == 1, &"len: {server.messages.len}" 235 | 236 | client.tick() # receive the 22 acks 237 | 238 | doAssert c2s.sendParts.len == 0 239 | doAssert c2s.stats.inFlight == 0, &"stats.inFlight: {c2s.stats.inFlight}" 240 | doAssert c2s.stats.saturated == false 241 | doAssert c2s.stats.latencyTs.avg() > 0 242 | doAssert c2s.stats.throughputTs.avg() > 0 243 | 244 | block: 245 | # Test retry. 246 | 247 | var server = newReactor("127.0.0.1", nextPort()) 248 | var client = newReactor("127.0.0.1", nextPort()) 249 | 250 | var c2s = client.connect(server.address) 251 | client.send(c2s, "test") 252 | 253 | client.tick() 254 | 255 | doAssert c2s.sendParts.len == 1 256 | 257 | let firstSentTime = c2s.sendParts[0].sentTime 258 | 259 | client.debug.tickTime = epochTime() + ackTime 260 | 261 | client.tick() 262 | 263 | doAssert c2s.sendParts[0].sentTime != firstSentTime # We sent the part again 264 | 265 | block: 266 | # Test junk data. 267 | 268 | var server = newReactor("127.0.0.1", nextPort()) 269 | var client = newReactor("127.0.0.1", nextPort()) 270 | 271 | var c2s = client.connect(server.address) 272 | 273 | client.rawSend(c2s.address, "asdf") 274 | 275 | client.tick() 276 | server.tick() 277 | 278 | # No new connection, no crash 279 | doAssert server.newConnections.len == 0 280 | doAssert server.connections.len == 0 281 | 282 | var msg = "" 283 | msg.addUint32(partMagic) 284 | msg.addStr("aasdfasdfaasdfaasdfasdfsdfsdasdfasdfsaasdfasdffsadfaasdfasdfa") 285 | 286 | client.rawSend(c2s.address, msg) 287 | 288 | client.tick() 289 | server.tick() 290 | 291 | # No new connection, no crash 292 | doAssert server.newConnections.len == 0 293 | doAssert server.connections.len == 0 294 | 295 | block: 296 | # Text disconnect packet. 297 | var server = newReactor("127.0.0.1", nextPort()) 298 | var client = newReactor() 299 | var c2s = client.connect(server.address) 300 | 301 | client.send(c2s, "hi") 302 | client.tick() 303 | server.tick() 304 | 305 | doAssert len(server.messages) == 1 306 | doAssert len(server.connections) == 1 307 | doAssert len(client.connections) == 1 308 | 309 | client.disconnect(c2s) 310 | 311 | doAssert len(client.deadConnections) == 1 312 | doAssert len(client.connections) == 0 313 | 314 | client.tick() 315 | server.tick() 316 | 317 | doAssert len(server.deadConnections) == 1 318 | doAssert len(server.connections) == 0 319 | 320 | block: 321 | # Test mange larger messages. 322 | var server = newReactor("127.0.0.1", nextPort()) 323 | var client = newReactor("127.0.0.1", nextPort()) 324 | 325 | client.maxInFlight = 1000 326 | 327 | var buffer = "" 328 | for i in 0 ..< 10_000: 329 | buffer.add "F" 330 | 331 | var c2s = client.connect(server.address) 332 | 333 | for p in 0 ..< 20: 334 | client.send(c2s, buffer) 335 | 336 | var gotNumber = 0 337 | 338 | while true: 339 | client.tick() 340 | server.tick() 341 | gotNumber += server.messages.len 342 | for msg in server.messages: 343 | doAssert msg.data == buffer 344 | if client.connections.len == 0: 345 | break 346 | 347 | doAssert gotNumber == 20 348 | 349 | block: 350 | var server = newReactor("127.0.0.1", nextPort()) 351 | for i in 0 ..< 100: 352 | server.tick() 353 | for msg in server.messages: 354 | echo "GOT MESSAGE: ", msg.data 355 | server.send(msg.conn, "you said:" & msg.data) 356 | if i == 10: 357 | var client = newReactor() 358 | var c2s = client.connect(server.address) 359 | client.send(c2s, "hi") 360 | client.disconnect(c2s) 361 | client.disconnect(c2s) 362 | -------------------------------------------------------------------------------- /tests/test_timeseries.nim: -------------------------------------------------------------------------------- 1 | import netty/timeSeries 2 | 3 | block: 4 | var ts = newTimeSeries() 5 | assert ts.max() == 0.0 6 | assert ts.avg() == 0.0 7 | 8 | block: 9 | var ts = newTimeSeries() 10 | ts.add(123.0) 11 | assert ts.max() == 123.0 12 | assert ts.avg() == 123.0 13 | 14 | block: 15 | var ts = newTimeSeries() 16 | ts.add(1.0) 17 | ts.add(2.0) 18 | ts.add(3.0) 19 | assert ts.max() == 3.0 20 | assert ts.avg() == 2.0 21 | 22 | block: 23 | var ts = newTimeSeries(10) 24 | for i in 0 ..< 100: 25 | ts.add(i.float64) 26 | assert ts.max() == 99.0 27 | assert ts.avg() == 94.5 28 | 29 | block: 30 | var ts = newTimedSamples() 31 | assert ts.max() == 0.0 32 | assert ts.avg() == 0.0 33 | 34 | block: 35 | var ts = newTimedSamples() 36 | ts.add(123.0, 123.0) 37 | assert ts.max() == 123.0 38 | assert ts.avg() == 123.0 39 | 40 | block: 41 | var ts = newTimedSamples() 42 | ts.add(1.0, 10.0) 43 | ts.add(2.0, 20.0) 44 | ts.add(3.0, 30.0) 45 | assert ts.max() == 30.0 46 | assert ts.avg() == 30.0 47 | 48 | block: 49 | var ts = newTimedSamples(10) 50 | for i in 0 ..< 100: 51 | ts.add(i.float64, 10) 52 | assert ts.max() == 10.0 53 | assert ts.avg() == 11.11111111111111 54 | 55 | block: 56 | var ts = newTimedSamples(10) 57 | for i in 0 ..< 100: 58 | ts.add(i.float64, i.float64 * 10) 59 | assert ts.max() == 990 60 | assert ts.avg() == 1050 61 | -------------------------------------------------------------------------------- /tests/vtune.nim: -------------------------------------------------------------------------------- 1 | import netty, strformat 2 | 3 | var server = newReactor("127.0.0.1", 2002) 4 | var client = newReactor("127.0.0.1", 2003) 5 | 6 | var c2s = client.connect(server.address) 7 | 8 | var l: int 9 | 10 | for i in 0 ..< 1000: 11 | block: 12 | var buffer = "large:" 13 | for i in 0 ..< 1000: 14 | buffer.add "" 15 | 16 | client.send(c2s, buffer) 17 | 18 | for i in 0 ..< 10: 19 | client.tick() 20 | server.tick() 21 | 22 | var a, b, c: Message 23 | for msg in server.messages: 24 | a = msg 25 | b = msg 26 | b = a 27 | c = a 28 | l += (a.data.len + b.data.len + c.data.len) 29 | 30 | block: 31 | var dataToSend = newSeq[string]() 32 | for i in 0 ..< 1000: 33 | dataToSend.add &"data #{i}, its cool!" 34 | 35 | for d in dataToSend: 36 | client.send(c2s, d) 37 | for i in 0 ..< 1000: 38 | client.tick() 39 | server.tick() 40 | 41 | var a, b, c: Message 42 | for msg in server.messages: 43 | var index = dataToSend.find(msg.data) 44 | # make sure message is there 45 | assert index != -1 46 | dataToSend.delete(index) 47 | a = msg 48 | b = msg 49 | b = a 50 | c = a 51 | l += (a.data.len + b.data.len + c.data.len) 52 | if dataToSend.len == 0: break 53 | # make sure all messages made it 54 | assert dataToSend.len == 0, &"datatoSend.len: {datatoSend.len}" 55 | 56 | echo l 57 | --------------------------------------------------------------------------------