├── .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 | 
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 |
--------------------------------------------------------------------------------