Last Friday a coworker was so kind to organize a fun session at work where we had to write bots
that participated in a game. The basic idea is to
program a bot that plays against other bots, all communicating via UDP with a server that supervises
the game.
Even though many of our developers are rather avid C++ users, this problem involved two reoccurring
tasks: UDP communication and string handling, both topics that some would claim are not the
particular strong points of C++ (up to the point that it is getting ridiculous)1. So most of us
settled on Ruby or Python, both of which shine especially with string processing.
Haskell supposedly also makes it easy to work with datagram sockets but it always takes me some time
to get it right so this time I wanted to take the time to write down a small UDP sample for future
reference. (there is an abundance of TCP examples out there, but less so for UDP)
Here is a pretty minimal UDP client application in haskell that just connects a datagram socket and
sends some data.
I retrieve the address-information for the server I want to talk to (getAddrInfo
), get a socket
(socket
) and associate it with the server ip and port (connect
). Now I have a socket that can be
used to send datagram packets.
For completeness I wrap the code with bracket
just to be sure clean up is done properly in every
case. Don’t mind the withSocketsDo
, this is only necessary on windows for some initialization
stuff but usually is included to make the code platform agnostic.
module Main where
import Network.Socket
import Control.Exception
port = "3000"
main = withSocketsDo $ bracket getSocket sClose talk
where getSocket = do
(serveraddr:_) <- getAddrInfo Nothing (Just "127.0.0.1") (Just port)
s <- socket (addrFamily serveraddr) Datagram defaultProtocol
connect s (addrAddress serveraddr) >> return s
talk s = do
send s "Hello, world!"
recv s 1024 >>= \msg -> putStrLn $ "Received " ++ msg
Even though in our scenario writing a server was not required, it proved valuable for test purposes
and I include it for reference.
Here, we build a socket with the address-info and bind it to our own ip on our port (getaddrinfo
,
socket
, bindSocket
). Having bound the socket, we can receive from it and send s.th. back to the
client that sent us a message.
module Main where
import Control.Monad (unless)
import Network.Socket
import Control.Exception
port = "3000"
main = withSocketsDo $ bracket connectMe sClose handler
where
connectMe = do
(serveraddr:_) <- getAddrInfo
(Just (defaultHints {addrFlags = [AI_PASSIVE]}))
Nothing (Just port)
sock <- socket (addrFamily serveraddr) Datagram defaultProtocol
bindSocket sock (addrAddress serveraddr) >> return sock
handler :: Socket -> IO ()
handler conn = do
(msg,n,d) <- recvFrom conn 1024
putStrLn $ "< " ++ msg
unless (null msg) $ sendTo conn msg d >> handler conn
$ runghc client Received Hello, world!
For our bot contest I paired with a ruby guy and a simple UPD client is very easy, even though it does not include any options to setup the address-info or handle graceful shutdown in case of exceptions:
require 'socket'
port = 3000
sock = UDPSocket.new
data = 'Hello Server'
sock.send(data, 0, '127.0.0.1', port)
resp = sock.recv(1024)
p resp
sock.close
$ ruby client.rb "Hello Server"
Pretty interesting to see that even developers with a strong C/C++ background had problems getting started. But then again it’s definitely more work to get a working client using the posix api. For the fun, here is the code:
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <iostream>
using namespace std;
#define SERVERPORT "3000"
#define SERVERIP "127.0.0.1"
enum { MAX_BUFFER_SIZE = 1024 };
int main(int argc, char* argv[])
{
struct addrinfo hints, *servinfo, *p;
const char* msg = "Hello World";
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
getaddrinfo(SERVERIP, SERVERPORT, &hints, &servinfo);
int sockfd;
for (p = servinfo; p != NULL; p = p->ai_next)
{
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == -1)
{
continue;
}
break;
}
if (p == NULL)
{
cerr << "client: could not bind socket" << endl;
return 2;
}
int sent = sendto(sockfd, msg, strlen(msg), 0, p->ai_addr, p->ai_addrlen);
cout << "sent " << sent << " bytes to " << SERVERIP << endl;
char buffer[MAX_BUFFER_SIZE];
int bytesReceived = recv( sockfd, buffer, MAX_BUFFER_SIZE-1, 0 );
buffer[bytesReceived]= '\0';
cout << "received back " << bytesReceived << " bytes: " << buffer << endl;
freeaddrinfo(servinfo);
close(sockfd);
return 0;
}
$ clang++ -Wall client.cpp -o c $ ./c sent 11 bytes to 127.0.0.1 received back 11 bytes: Hello World
Besides all the technical details, it is a great way to spend a Friday afternoon and more companies should consider allowing for such fun events!
-
Yes, I know about asio and boost string algorithms ↩