[Development] Multiclient UDP server

Thiago Macieira thiago.macieira at intel.com
Fri Aug 18 22:37:52 CEST 2017


Related to the DTLS discussion, but I'll try to keep this free from crypto for 
now.

The question is how to write a multi-client server operating on UDP, not TCP. 
It would be nice to understand SCTP case too (and maybe DCCP in the future).

On TCP, this is easy:
1) you create a socket, bind it and then put it in LISTENING mode
2) every time a new TCP connection arrives, you accept() a new socket
3) the server socket is from now on independent of the client

In our API, the QTcpServer creates new QTcpSockets and they're from that point 
on independent.

In UDP, that's different. There are two ways to proceed:

A) single UDP socket in unconnected mode

The code identifies each different client by the source address. 
QNetworkDatagram makes this easy, since it holds for you the source address 
and using makeReply() creates a QNetworkDatagram that you can use to reply.

Pros: uses a single socket, so limits the resource usage.

Cons: scalability. Since it's the same socket, only one thread can read from 
it at a time. You may pass the QNetworkDatagram to another thread to process 
the data, but you need to centralise sending of replies (QUdpSocket 
limitation, not of the OS API).

We don't need to implement anything to support this: that is simply QUdpSocket 
as it is today.

B) multiple UDP sockets

This is similar to TCP. Whenever a new client is detected, we create a new UDP 
socket and put it in connected mode with that particular client. Once you do 
that, this socket will only receive datagrams from that client, whereas the 
unconnected socket will not.

Pros: scalability, since you can move the QUdpSocket to another thread; the 
connected datagram socket has another advantage that it can relay ICMP errors 
like "port unreachable" to the upper level, whereas the unconnected one won't. 
I also believe the connected mode socket can do Path MTU calculation.

Cons: more resources, but more importantly, there's a race condition: the 
unconnected socket which you received the initial packet from the new client 
on may have queued more datagrams. Those datagrams can be:
 - from the same client
 - from other clients

So we are in a catch-22 situation: if you connect() this socket and create a 
new unconnected one, any packets from other clients will not be seen. If you 
create a new socket and connect() it, you may miss any packets from the same 
client. I think that for both CoAP and DTLS, we'd opt for connect()ing the new 
socket, since the client wouldn't be sending new commands until it receives a 
reply from us. That reply resolves the problem of race condition.

[food for thought: what happens if we receive a retransmission?]


So questions:
 - do we attempt to provide a "QUdpServer" or augment QUdpSocket's API to 
support (B) above? Or do we provide only a QDtlsServer API that implements 
(B)? This "QUdpServer" would be useful to implement a unencrypted CoAP server 
("coap"). It's not very difficult to rewrite the code, but the choice becomes 
hairy if we want to support both unencrypted and encrypted CoAP ("coaps").

 - do we need a QDtlsSocket at all, or just a wrapper on top of QUdpSocket? 
One advantage of having the class is that you could implement both "coap" and 
"coaps" clients on top of a plain QUdpSocket pointer, not having to worry  
about the encryption.

-- 
Thiago Macieira - thiago.macieira (AT) intel.com
  Software Architect - Intel Open Source Technology Center




More information about the Development mailing list