public async Task StartAsync(CancellationToken cancellationToken)
        {
            if (IsRunning)
            {
                throw new InvalidOperationException("Server is already running");
            }

            var tcpListener      = _networkOptions.TcpListener;
            var udpAddress       = _networkOptions.UdpAddress;
            var udpListenerPorts = _networkOptions.UdpListenerPorts;

            _logger.Information("Starting - tcp={TcpEndPoint} udp={UdpAddress} udp-port={UdpPorts}",
                                tcpListener, udpAddress, udpListenerPorts);

            try
            {
                _listenerThreads = _threadingOptions.SocketListenerThreadsFactory();
                _workerThreads   = _threadingOptions.SocketWorkerThreadsFactory();

                await new ServerBootstrap()
                .Group(_listenerThreads, _workerThreads)
                .Channel <TcpServerSocketChannel>()
                .Handler(new ActionChannelInitializer <IServerSocketChannel>(ch => { }))
                .ChildHandler(new ActionChannelInitializer <ISocketChannel>(ch =>
                {
                    var coreMessageHandler = new MessageHandler(_serviceProvider,
                                                                new DefaultMessageHandlerResolver(
                                                                    new[] { typeof(AuthenticationHandler).Assembly }, typeof(ICoreMessage)
                                                                    ),
                                                                _schedulerService
                                                                );

                    var internalRmiMessageHandler = new MessageHandler(
                        _serviceProvider,
                        new DefaultMessageHandlerResolver(
                            new[] { typeof(ReliablePingMessage).Assembly }, typeof(IMessage)
                            ),
                        _schedulerService
                        );

                    var rmiMessageHandler = new MessageHandler(
                        _serviceProvider,
                        _serviceProvider.GetRequiredService <IMessageHandlerResolver>(),
                        _schedulerService
                        );

                    coreMessageHandler.UnhandledMessage +=
                        ctx => ctx.Session.Logger.Debug("Unhandled core message={@Message}", ctx.Message);

                    internalRmiMessageHandler.UnhandledMessage +=
                        ctx => ctx.Session.Logger.Debug("Unhandled internal rmi message={@Message}", ctx.Message);

                    rmiMessageHandler.UnhandledMessage += OnUnhandledRmi;

                    ch.Pipeline
                    .AddLast(_serviceProvider.GetRequiredService <SessionHandler>())
                    .AddLast(_serviceProvider.GetRequiredService <ProudFrameDecoder>())
                    .AddLast(_serviceProvider.GetRequiredService <ProudFrameEncoder>())
                    .AddLast(_serviceProvider.GetRequiredService <MessageContextDecoder>())
                    .AddLast(_serviceProvider.GetRequiredService <CoreMessageDecoder>())
                    .AddLast(_serviceProvider.GetRequiredService <CoreMessageEncoder>())
                    .AddLast(new FlowControlHandler(false))
                    .AddLast(Constants.Pipeline.CoreMessageHandlerName, coreMessageHandler)
                    .AddLast(_serviceProvider.GetRequiredService <SendContextEncoder>())
                    .AddLast(_serviceProvider.GetRequiredService <MessageDecoder>())
                    .AddLast(_serviceProvider.GetRequiredService <MessageEncoder>())

                    // MessageHandler discards the message after handling
                    // so internal messages wont reach the rmiMessageHandler
                    .AddLast(Constants.Pipeline.InternalMessageHandlerName, internalRmiMessageHandler)
                    .AddLast(_serviceProvider.GetRequiredService <MessageDecoder>())
                    .AddLast(rmiMessageHandler)
                    .AddLast(_serviceProvider.GetRequiredService <ErrorHandler>());
                }))
                .ChildOption(ChannelOption.TcpNodelay, !_networkOptions.EnableNagleAlgorithm)
                .ChildOption(ChannelOption.AutoRead, false)
                .ChildAttribute(ChannelAttributes.ServiceProvider, _serviceProvider)
                .ChildAttribute(ChannelAttributes.Session, default(ProudSession))
                .BindAsync(_networkOptions.TcpListener);

                if (udpListenerPorts != null)
                {
                    _udpSocketManager.Listen(udpAddress, tcpListener.Address, udpListenerPorts, _workerThreads);
                }
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Unable to start server - tcp={TcpEndPoint} udp={UdpAddress} udp-port={UdpPorts}",
                              tcpListener, udpAddress, udpListenerPorts);
                await ShutdownThreads();

                ex.Rethrow();
            }

            IsRunning = true;
            OnStarted();
            ScheduleRetryUdpOrHolepunchIfRequired();
        }