Looking for help with UDP server arcitecture


(Joseph Lopez DeVictoria) #1

I have been working for the past several years on a simple UDP packet server that I plan to use for a hobbyist video game. I originally wrote the server in C++ using standard sockets with UDP. (my last version of which can be seen here)

My C++ architecture was basically the following:

  • UDP Packets containing serialized game data that were decoded based on an opcode.
  • One blocking collector thread dedicated to pulling packets directly from the socket, and then passing the packet to a worker thread for handling.
  • [X] Worker threads that check the packet opcode, cast the packet to the corresponding packet type, and then call a handler function based on the type.

This seems to me like a very standard starting point for developing a server achitecture, and it seemed to work fairly well. After a while I got a little tired of the growing complexity of my code and wondered if there is a better language that I could be using for developing the server. This led me to Go, and I became interested in learning the language and trying to build my server with it.

I have basically gotten the new version of the server (seen here) up to the same level of functionality that my C++ server had, and I am reaching a few pivotal design decisions that I want some experienced advice on:

Currently my server is basically a clone of what I was doing in C++:

  • I have almost identical packet types for the same purpose.
  • One collector goroutine that simply waits for packets on the socket, and places them into a go chan.
  • [X] handler goroutines that wait for a packet to arrive on the chan and handle them according to their packet type.

Is the right way to do it in Go? I am still learning the intricacies of goroutines and haven’t even set up mutexes/semaphores for the database etc… I’m hoping some advice here will point me in the right direction before I get to far into this architecture.

That being said, it is functional, and I might be already following good design! I look forward to your responses.


(amnon bc) #2

I would have started off by dispensing with the channels and just having Collect launch a new goroutine to handle each packet as it arrives.


(Joseph Lopez DeVictoria) #3

@amnon what do you think the overhead would be of spinning up a new goroutine?

This is a great example where I actually just don’t know what the expectation is within idiomatic go designs.


(amnon bc) #4

The overhead is surprisingly small… Spinning up a new Goroutine for each message is quite idiomatic.
What is your anticipated message rate? I would expect you will be able to consume thousands of messages per second without any problem.

Try it out and benchmark the performance.


(Joseph Lopez DeVictoria) #5

I can give it a try. I also need to create a more scientific bench for collecting average time to process a given packet. Especially if I could mix the packet types and assign more logic behind them, I would be able to converge on an average time to handle.

Another question I have is whether it is worth creating more go-routines than threads. I know the go-routines are not thread dependent, but I assume as the concurrent pile grows, the total latency for execution on any single go-routine gets larger and larger as it tries to schedule it.

Right now I spawn enough only enough go-routines to saturate the maximum number of parallel threads on my PC, which in conjunction with go channels, I thought would limit the latency of any single packet being executed. That would also allow me to maintain some semblance of natural packet order (Instead of explicitly labeling each packet with a number).