Jump to content
Returning Members: Password Reset Required ×

Recommended Posts

Posted

Upon initially developing my conquer server from scratch, I was not a fan of how people were using lengthy switch statements. I took a traditional Go approach and developed a Routing service for incoming client -> server messages. If you have ever worked with Go http handlers, you probably already know what I am referring to.

The approach is having a predefined list (map) of all available routes and handlers. In my opinion, this makes for cleaner sources and (albeit minor) more time efficient packet handling.

When the server receives, it routes the raw messages based on the packet type (ie [1001]) to the associated handler function along with the client that sent it. I've excluded what happens from there, as the rest is based off how your server handles synchronizing the game state.

Essentially, the use of it would look like this:

server.Router = router.NewRouter()

s.Router.Add(router.NewRoute().Name(msg.MsgConnectType).Process(MsgConnectProcess))

You could organize these routes by type or by their functionality. When looking for a specific handler, instead of digging through files (or worse, a switch statement) you could go to where your router is located and find which handler belongs to the route.

diagram.png.8eb48b2140d7b6593daaf61af1df0c0b.png

I've attached the router below (Go), but this could be ported if desired. If you find any bugs, please let me know!

// Router uses a map to find processes in constant time, if the router
// cannot find the specified Route, it send it to the DefaultRouteProcess
type Router struct {
routes          map[uint16]*Route
NotFoundProcess Process
}

// NewRouter creates a router, instantiates a map, and returns a pointer
// to the router
func NewRouter() *Router {
return &Router{
	routes: make(map[uint16]*Route),
}
}

// Add makes a new entry into the Router's map, the name of the route
// will always be the map location of the route.
func (r *Router) Add(route *Route) {
r.routes[route.name] = route
}

// Process looks up the Route by name, which is supplied by the first
// 2 bytes of the slice, as a uint16. If the process is located, it
// sends off the client and byte slice to the designated process.
func (r *Router) Process(client interface{}, h *packet.Header) {
if route, ok := r.routes[h.Type]; ok {
	if route.process == nil {
		log.Warn(fmt.Sprintf("No process given for %d\n", h.Type))
		return
	}
	route.process(client, h)
	return
}

if r.NotFoundProcess != nil {
	r.NotFoundProcess(client, h)
	return
}
r.defaultNotFoundProcess(client, h)
}

func (r *Router) defaultNotFoundProcess(_ interface{}, h *packet.Header) {
log.Warn(fmt.Sprintf("[Mux] Packet [%d] not implemented!\n", h.Type))
}

// Route is how the router determines what process to run for the given
// client and slice.
type Route struct {
name    uint16
process Process
}

// NewRoute creates a new route, and returns a pointer to that route,
// To add a name and process, it is recommended to use Name() and Process()
func NewRoute() *Route {
route := new(Route)
return route
}

// Name adds a name to the receiver route
func (r *Route) Name(name uint16) *Route {
r.name = name
return r
}

// Process adds a process to the receiver route
func (r *Route) Process(f Process) *Route {
r.process = f
return r
}

// Process defines the function allowed to be used as a Process.
// interface is used as a holder for Client, and a byte slice for passing raw message.
type Process func(interface{}, *packet.Header)

Posted

Nice job with this. It's an interesting idea, for sure. Something to keep in mind though is that this won't necessarily yield any better performance (it might actually be magnitudes slower). In Go, the language compiler makes optimizations automatically on your behalf. In the case of a constant switch, it's likely being implemented as a jump table (I need to double check this). A map, on the other hand, is implemented using a chained hash table. It has to iterate over buckets of increasing sizes... and depending on the size of your map, that could get increasingly more expensive than a simple jump table.

Not to say there's no value in a mux pattern. I do enjoy that. I would try running a benchmark test with the two approaches though and see what performance penalty the mux pattern comes with. Here's the source code to golang's map implementation if you're interested as well: https://go.googlesource.com/go/+/refs/heads/master/src/runtime/map.go. I'd be curious to know what the results are.

PS: I don't know if this is really a tutorial, so I'm going to move this to the Projects section.

Posted

Thanks for the feedback! I did some research and I don't think Go has jump tables for switch statements as of now, unless I was reading the GitHub issue wrong.

I also found this: https://hashrocket.com/blog/posts/switch-vs-map-which-is-the-better-way-to-branch-in-go but it also states maps are slower until larger sets.

I have not benchmarked it yet, but eventually I will get around to it. One issue I have found with using this approach, it that when you hit MsgAction, you start requiring sub-routes or rather lengthy switch statements.

I'd be interested to hear some feedback from the community on other ways to approach this in a clean and also performant way!

Posted
I wouldn't stress over it too much. It might be just micro-optimizing with little benefit. I personally use a switch statement just for instantiation and then rely on an interface for general decode / execute methods, but that doesn't mean I wouldn't be open to suggestions.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...