Reading a string from the terminal is easy.
Receiving a UDP packet is easy and there are ready examples.
Sending a UDP packet is really easy.
So SURELY doing all three at once is also easy.... I just spent many days on working out different ways to do that.
Fortunately since UDP is connectionless, it turns out a simple pair of threads can do it; but this is very non-obvious at first. At least it wasn't to me anyway.
I also got a version to sort of work with many layers asynchronous blah blah blah working. It is about 300 lines line and isn't even slightly readable.
So, for any future UDP client and server writers, here is my very simplified bidirectionial UDP client. Feel free to use as a simple starting point:
import net, threadpool, strutils
const MAX_PACKET_SIZE = 508
const clientAddr = "127.0.0.1"
const clientPort = 8800
const serverAddr = "127.0.0.1"
const serverPort = 9900
proc sendMessage(socket: Socket, message: string) =
socket.sendTo(serverAddr, Port(serverPort), message)
echo "sent: ", message
proc recvMessage(message: string) =
echo "received: ", message
proc onPacket(socket: Socket): string =
var incomingMessage = ""
var srcAddress = ""
var srcPort = Port(0)
let msgLen = socket.recvFrom(incomingMessage, MAX_PACKET_SIZE, srcAddress, srcPort)
echo "packet of size $1 seen from $2:$3".format(msgLen, srcAddress, srcPort)
return incomingMessage
let socket: Socket = newSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
socket.bindAddr(Port(clientPort), clientAddr)
var messageFlowVar = spawn stdin.readLine()
var packetFlowVar = spawn onPacket(socket)
while true:
if messageFlowVar.isReady():
sendMessage(socket, ^messageFlowVar)
messageFlowVar = spawn stdin.readLine()
if packetFlowVar.isReady():
recvMessage(^packetFlowVar)
packetFlowVar = spawn onPacket(socket)
Feel free to convert the if {}.isReady() into while loops and then slip in a os.sleep(50) to make it a tad easier on the CPU.
Next, I'll be trying to avoid pre-assigning the client address and port; but instead capturing the OS-assigned outbound src port on first sendTo so that the other server can consistently see the same client source port over time. If nothing else, that will help with tracking firewall NAT passage. If anyone knows an clean and easy way to do that, let me know.
Sending and receiving UDP while accepting stdin, with just selectors:
import std/[net, selectors, strformat, strutils]
const
cfgMaxPacket = 500
cfgPort = 8800
localhost = "127.0.0.1"
let
socket = newSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
events = newSelector[int]()
socket.bindAddr(Port(cfgPort), localhost)
events.registerHandle(socket.getFd, {Read}, 0)
events.registerHandle(0, {Read}, 0) # stdin
echo &"Listening to stdin and {localhost}:{cfgPort}"
while true:
for got in events.select(-1):
if got.fd == cast[int](socket.getFd):
var
message, srcAddress = ""
srcPort = Port(0)
discard socket.recvFrom(message, cfgMaxPacket, srcAddress, srcPort)
echo "got UDP: ", message.strip
elif got.fd == 0:
# send packet to ourselves
try:
socket.sendTo(localhost, Port(cfgPort), stdin.readLine())
except EOFError:
quit()
What you type interactively is sent and then received over UDP, and then printed. While you're doing this you can nc -u 127.0.0.1 8800 in another window and send packets with netcat that'll be printed in the same manner.
The .strip is for the newlines that netcat will send.
@jrfondren This is brilliant, but please never do cast[int](socket.getFd), you should instead use the safe casting: socket.getFd().int (cast will do whatever you tell it, including converting types that don't make sense which can cause memory corruption).
Also, consider using asyncnet's UDP support instead of raw selectors. You can simply await recvFrom or sendTo nowadays: https://nim-lang.org/docs/asyncnet.html#recvFrom%2CAsyncSocket%2CFutureVar%5Bstring%5D%2Cint%2CFutureVar%5Bstring%5D%2CFutureVar%5BPort%5D.
Anyone know why I cant seem to use sendTo for an off-machine address? Using @jrfonden 's example above, if I set the sendTo address to 192.168.1.44 which is the machine I'm on, it works just fine. (Especially if I'm also running the UDP server on my local machine.)
But, if I set the address to 192.168.1.45, which is the machine sitting next to me on the same network, I get a run-time crash as soon as a send a packet:
$ ./client2
Listening to stdin and 127.0.0.1:8800; transmitting to 192.168.1.45:9900
help
/home/johnd/Projects/LastManHanging/server/client2.nim(30) client2
/home/johnd/.choosenim/toolchains/nim-1.4.0/lib/pure/net.nim(1710) sendTo
/home/johnd/.choosenim/toolchains/nim-1.4.0/lib/pure/net.nim(1699) sendTo
/home/johnd/.choosenim/toolchains/nim-1.4.0/lib/pure/includes/oserr.nim(94) raiseOSError
Error: unhandled exception: Invalid argument [OSError]
The net.nim module at line 1699 (nim version 1.4.0) appears to imply it could not "lookup" the address. Yet, when I simulate the getAddrInfo(address, port, af, socket.sockType, socket.protocol) on line 1682 I absolutely get one IPv4 address back.
In fact any IP address that isn't part of the local network stack does not work. Tried it on two machines if different OS: Linux Mint 20 and PopOS; same effect.
Is there something unique to c-like getAddrInfo call that Nim uses?
I'm kind of stumped.
On the off change that @dom96's advice to use async was part of the problem. I converted it to async:
import asyncdispatch, asyncnet
import std/[net, selectors, strformat, strutils]
const
cfgMaxPacket = 500
clientPort = 8800
clientHost = "127.0.0.1"
serverPort = 9900
serverHost = "192.168.1.45"
proc doit() {.async.} =
let
socket = newAsyncSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
events = newSelector[int]()
socket.bindAddr(Port(clientPort), clientHost)
events.registerHandle(socket.getFd, {Read}, 0)
events.registerHandle(0, {Read}, 0) # stdin
echo &"Listening to stdin and {clientHost}:{clientPort}; transmitting to {serverHost}:{serverPort}"
while true:
for got in events.select(-1):
if got.fd == socket.getFd().int:
var msgDetails = await socket.recvFrom(cfgMaxPacket)
let message = msgDetails.data
echo "got UDP: ", message.strip
elif got.fd == 0:
try:
await socket.sendTo(serverHost, Port(serverPort), stdin.readLine())
except EOFError:
quit()
asyncCheck doit()
runForever()
Sadly that does not fix it. Like before, a local address works for UDP, but everything else breaks.
Not sure, but the async version shouldn't be using the selector anymore. Essentially the async will use selectors internally to dispatch the async when data is ready.
However, your async version also looks like you're reusing the same socket for both local and remote clients. At least with TCP, each IP would need a separate socket. Looking at the docs, it seems you might be able to reuse the same socket with UDP, but you might not want to set bindAddr.
But, if I set the address to 192.168.1.45,
socket.bindAddr(Port(cfgPort), localhost)
This is the problem, change it to empty string which set it to ADDR_ANY in order the bound address able to listen to any address e.g.
socket.bindAddr(Port cfgPort)
Just for completion, this is my code
# sender.nim
import net, strutils
const
# this is the address in the same network but different machine and ip
foreignAddr = "192.168.18.62"
foreignPort = Port 9999
var socket = newSocket(sockType = SOCK_DGRAM, protocol = IPPROTO_UDP)
var running = true
while running:
let msg = stdin.readLine
socket.sendTo(foreignAddr, foreignPort, msg & "\c\L")
if msg.toLowerAscii == "quit":
running = false
and this is the receiver
#receiver.nim
import net, asyncnet, strutils, asyncdispatch
const withSize = 256 # each datagram size
var running = true
var socket = newAsyncSocket(sockType = SOCK_DGRAM, protocol = IPPROTO_UDP)
socket.bindAddr(port = Port 9999)
while running:
let msg = waitfor socket.recvFrom(withSize)
if msg.data.strip.toLowerAscii == "quit":
running = false
echo "message data: ", msg.data
echo "from addr: ", msg.addres
echo "with port: ", msg.port.int
Thank you so much! Binding to the port is fine (and needed for client to get past the NAT routers that 99% of end-users sit behind).
But binding to a specific address, such as the loopback/localhost (127.0.0.1) forces it to an interface that has no route to the destination address. Makes sense. In fact, I used to be a IP network engineer by trade. Kind of embarrassing that I didn't see a basic routing problem. :)
The runtime error is awfully generic though. It should have said something like "No route to host" or something descriptive.
Anyway, leaving the "from" address off fixed it.
For the curious, here is my basic two-way UDP client proof-of-concept. This one is async, uses the select method, and allows one to pass in the server address by command-line.
import asyncdispatch, asyncnet, os
import std/[net, selectors, strformat, strutils]
const
cfgMaxPacket = 508
serverPort = 9900
if paramCount() == 0:
echo "pass a server address"
quit()
let serverHost = paramStr(1)
proc showMessage(message: string) =
let parts = message.split("\n")
for part in parts:
echo "< " & part
proc doit() {.async.} =
let
socket = newAsyncSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
events = newSelector[int]()
events.registerHandle(socket.getFd, {Read}, 0)
events.registerHandle(0, {Read}, 0) # stdin
echo &"Listening to stdin and transmitting to {serverHost}:{serverPort}"
while true:
for got in events.select(-1):
if got.fd == socket.getFd().int:
var msgDetails = await socket.recvFrom(cfgMaxPacket)
let message = msgDetails.data
showMessage(message.strip)
elif got.fd == 0:
try:
await socket.sendTo(serverHost, Port(serverPort), stdin.readLine())
except EOFError:
quit()
asyncCheck doit()
runForever()