示例#1
0
        /// <summary>
        /// Create and host new room to the server with creating port mapping to NAT.
        /// This method doesn't create port mapping if first connection test is succeed.
        /// If first connection test is failed, this method try to create port mapping.
        /// The port pair to map is selected from candidates which consists of default port and port candidates from parameter.
        /// The ports which is being used by this computer with indicated protocol is removed from candidates.
        /// Refer NatPortMappingCreator.CreatePortMappingFromCandidatesAsync method if you want to know selection rule of port from candidates.
        /// If second connection test after port mapping is created is failed, this method throws error.
        /// </summary>
        /// <param name="maxPlayerCount"></param>
        /// <param name="discoverNatTimeoutMilliSeconds"></param>
        /// <param name="protocol">The protocol which is used for accept TCP connection of game</param>
        /// <param name="portNumberCandidates">The candidates of port number which is used for accept TCP connection of game</param>
        /// <param name="defaultPortNumber">The port number which is tried to use for accept TCP connection of game first</param>
        /// <param name="password"></param>
        /// <param name="forceToDiscoverNatDevice">force to discover NAT device even if NAT is already discovered if true</param>
        /// <exception cref="ClientErrorException"></exception>
        /// <exception cref="ArgumentException"></exception>
        /// <returns></returns>
        public async Task <CreateRoomWithCreatingPortMappingResult> CreateRoomWithCreatingPortMappingAsync(
            byte maxPlayerCount, TransportProtocol protocol, IEnumerable <ushort> portNumberCandidates,
            ushort defaultPortNumber, int discoverNatTimeoutMilliSeconds = 5000, string password = "",
            bool forceToDiscoverNatDevice = false)
        {
            var portNumberCandidateArray = portNumberCandidates.ToList();

            if (portNumberCandidateArray.Any(
                    portNumberCandidate => !Validator.ValidateGameHostPort(portNumberCandidate)))
            {
                throw new ArgumentException("Dynamic/private port is available.", nameof(portNumberCandidates));
            }

            if (!Validator.ValidateGameHostPort(defaultPortNumber))
            {
                throw new ArgumentException("Dynamic/private port is available.", nameof(defaultPortNumber));
            }

            if (!Validator.ValidateRoomPassword(password))
            {
                throw new ArgumentException(
                          $"A string whose length is more than {RoomConstants.RoomPasswordLength} is not available.",
                          nameof(password));
            }

            await semaphore.WaitAsync().ConfigureAwait(false);

            try
            {
                if (!Connected)
                {
                    throw new ClientErrorException(ClientErrorCode.NotConnected);
                }

                if (IsHostingRoom)
                {
                    throw new ClientErrorException(ClientErrorCode.AlreadyHostingRoom,
                                                   "The client can host only one room.");
                }

                Logger.Log(LogLevel.Info, $"Execute first connection test (Default port: {defaultPortNumber}).");
                var connectionTestSucceed = false;
                try
                {
                    connectionTestSucceed =
                        await ConnectionTestCoreAsync(protocol, defaultPortNumber).ConfigureAwait(false);
                }
                // Consider port already used error as connection test failure
                catch (InvalidOperationException e)
                {
                    Logger.Log(LogLevel.Info, $"Failed to listen the port {defaultPortNumber}. ({e.Message})");
                }
                // Consider socket error as connection test failure
                catch (SocketException e)
                {
                    Logger.Log(LogLevel.Info, $"Failed to listen the port {defaultPortNumber}. ({e.Message})");
                }

                var    isDefaultPortUsed             = true;
                ushort usedPrivatePortFromCandidates = 0;
                ushort usedPublicPortFromCandidates  = 0;
                var    portNumber = defaultPortNumber;

                if (!connectionTestSucceed)
                {
                    // Discover NAT device if need
                    if (forceToDiscoverNatDevice || !PortMappingCreator.IsNatDeviceAvailable ||
                        !PortMappingCreator.IsDiscoverNatDone)
                    {
                        Logger.Log(LogLevel.Info, "Execute discovering NAT device because it is not done.");
                        await PortMappingCreator.DiscoverNatAsync(discoverNatTimeoutMilliSeconds).ConfigureAwait(false);
                    }

                    if (!PortMappingCreator.IsNatDeviceAvailable)
                    {
                        throw new ClientErrorException(ClientErrorCode.CreatingPortMappingFailed,
                                                       "Failed to discover NAT device.");
                    }

                    Logger.Log(LogLevel.Info, "Try to create port mapping because connection test is failed.");
                    // Create port candidates
                    Logger.Log(LogLevel.Debug, $"Port candidates: [{string.Join(",", portNumberCandidateArray)}]");
                    if (!portNumberCandidateArray.Contains(defaultPortNumber))
                    {
                        portNumberCandidateArray.Add(defaultPortNumber);
                    }

                    // Remove used ports in this machine
                    var privatePortCandidates =
                        NetworkHelper.FilterPortsByAvailability(protocol, portNumberCandidateArray).ToArray();
                    Logger.Log(LogLevel.Debug, $"Private port candidates: [{string.Join(",", privatePortCandidates)}]");
                    var publicPortCandidates = portNumberCandidateArray;
                    Logger.Log(LogLevel.Debug, $"Public port candidates: [{string.Join(",", publicPortCandidates)}]");

                    isDefaultPortUsed = false;
                    (usedPrivatePortFromCandidates, usedPublicPortFromCandidates) = await PortMappingCreator
                                                                                    .CreatePortMappingFromCandidatesAsync(protocol, privatePortCandidates, publicPortCandidates)
                                                                                    .ConfigureAwait(false);

                    portNumber = usedPublicPortFromCandidates;
                    Logger.Log(LogLevel.Info,
                               $"Port mapping is created or reused in NAT. (privatePortNumber: {usedPrivatePortFromCandidates}, publicPortNumber: {usedPublicPortFromCandidates})");

                    Logger.Log(LogLevel.Info, "Execute second connection test.");

                    try
                    {
                        connectionTestSucceed =
                            await ConnectionTestCoreAsync(protocol, portNumber).ConfigureAwait(false);
                    }
                    // Consider port already used error as connection test failure
                    catch (InvalidOperationException e)
                    {
                        Logger.Log(LogLevel.Info, $"Failed to listen the port {defaultPortNumber}. ({e.Message})");
                    }
                    // Consider socket error as connection test failure
                    catch (SocketException e)
                    {
                        Logger.Log(LogLevel.Info, $"Failed to listen the port {defaultPortNumber}. ({e.Message})");
                    }

                    if (!connectionTestSucceed)
                    {
                        throw new ClientErrorException(ClientErrorCode.NotReachable);
                    }

                    Logger.Log(LogLevel.Info, "Second connection test is succeeded. Start to create room.");
                }

                var createRoomResult = await CreateRoomCoreAsync(portNumber, maxPlayerCount, password)
                                       .ConfigureAwait(false);

                return(new CreateRoomWithCreatingPortMappingResult(isDefaultPortUsed, usedPrivatePortFromCandidates,
                                                                   usedPublicPortFromCandidates, createRoomResult));
            }
            finally
            {
                semaphore.Release();
            }
        }
示例#2
0
        /// <summary>
        /// Check if other machine can connect to this machine with the port for game without semaphore.
        /// </summary>
        /// <param name="protocol">The protocol which is used for accept TCP connection of game</param>
        /// <param name="portNumber">The port number which is used for accept TCP connection of game</param>
        /// <exception cref="ClientErrorException"></exception>
        /// <exception cref="InvalidOperationException">The port is already used by other connection or listener</exception>
        /// <exception cref="SocketException">Failed to create a socket with the port.</exception>
        /// <returns></returns>
        private async Task <bool> ConnectionTestCoreAsync(TransportProtocol protocol, ushort portNumber)
        {
            TcpListener tcpListener       = null;
            UdpClient   udpClient         = null;
            Task        task              = null;
            var         cancelTokenSource = new CancellationTokenSource();

            try
            {
                if (!Connected)
                {
                    throw new ClientErrorException(ClientErrorCode.NotConnected);
                }

                if (!NetworkHelper.CheckPortAvailability(protocol, portNumber))
                {
                    throw new InvalidOperationException(
                              $"The port \"{portNumber}\" is already used by other {portNumber} connection or listener.");
                }

                Logger.Log(LogLevel.Info,
                           $"Start {protocol} connectable test to my port {portNumber} from the server.");

                // Accept both IPv4 and IPv6
                if (protocol == TransportProtocol.Tcp)
                {
                    tcpListener = new TcpListener(IPAddress.IPv6Any, portNumber);
                    tcpListener.Server.SetSocketOption(
                        SocketOptionLevel.IPv6,
                        SocketOptionName.IPv6Only,
                        0);
                    tcpListener.Start();
                    Logger.Log(LogLevel.Debug, "TCP listener for connectable test is started.");

                    async Task Func(CancellationToken cancellationToken)
                    {
                        try
                        {
                            using (var socket = await Task.Run(tcpListener.AcceptSocketAsync, cancellationToken)
                                                .ConfigureAwait(false))
                            {
                                // Echo received message
                                var buffer = new ArraySegment <byte>(new byte[64]);
                                var size   = await Task
                                             .Run(
                                    async() => await socket.ReceiveAsync(buffer, SocketFlags.None)
                                    .ConfigureAwait(false), cancellationToken).ConfigureAwait(false);

                                Logger.Log(LogLevel.Debug,
                                           $"A test message from the server is received: \"{System.Text.Encoding.ASCII.GetString(buffer.ToArray())}\"");

                                var replyData = new ArraySegment <byte>(buffer.ToArray(), 0, size);
                                await Task.Run(async() =>
                                {
                                    await socket.SendAsync(replyData, SocketFlags.None).ConfigureAwait(false);
                                }, cancellationToken).ConfigureAwait(false);

                                Logger.Log(LogLevel.Debug, "The received message is sent to the server.");

                                socket.Shutdown(SocketShutdown.Both);
                                socket.Disconnect(false);
                                Logger.Log(LogLevel.Debug, "TCP listener for connectable test is shut-downed.");
                            }
                        }
                        // ObjectDisposedException is thrown when we cancel accepting after we started accepting.
                        catch (ObjectDisposedException)
                        {
                            Logger.Log(LogLevel.Debug, "TCP listener for connectable test is disposed.");
                        }
                    }

                    task = Func(cancelTokenSource.Token);
                }
                else
                {
                    udpClient = new UdpClient(portNumber);
                    Logger.Log(LogLevel.Debug, "TCP client for connectable test is created.");

                    async Task Func(UdpClient pUdpClient, CancellationToken cancellationToken)
                    {
                        try
                        {
                            var serverAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address;
                            while (true)
                            {
                                var receiveResult = await Task.Run(pUdpClient.ReceiveAsync, cancellationToken)
                                                    .ConfigureAwait(false);

                                Logger.Log(LogLevel.Debug,
                                           $"A message is received: \"{System.Text.Encoding.ASCII.GetString(receiveResult.Buffer)}\"");
                                if (cancellationToken.IsCancellationRequested)
                                {
                                    return;
                                }

                                // reply only if the message is from the server
                                if (receiveResult.RemoteEndPoint.Address.EqualsIpAddressSource(serverAddress))
                                {
                                    // Echo received message
                                    await Task.Run(async() =>
                                    {
                                        await pUdpClient.SendAsync(receiveResult.Buffer, receiveResult.Buffer.Length,
                                                                   receiveResult.RemoteEndPoint).ConfigureAwait(false);
                                    }, cancellationToken).ConfigureAwait(false);

                                    Logger.Log(LogLevel.Debug, "The received message is sent to the server.");
                                }
                                else
                                {
                                    Logger.Log(LogLevel.Debug,
                                               $"The sender ({receiveResult.RemoteEndPoint.Address}) of the message is not the server ({serverAddress}), so this message is ignored.");
                                }
                            }
                        }
                        // ObjectDisposedException is thrown when we cancel accepting after we started accepting.
                        catch (ObjectDisposedException)
                        {
                            Logger.Log(LogLevel.Debug, "UDP client for connectable test is disposed.");
                        }
                    }

                    task = Func(udpClient, cancelTokenSource.Token);
                }

                var requestBody = new ConnectionTestRequestMessage {
                    Protocol = protocol, PortNumber = portNumber
                };
                Logger.Log(LogLevel.Info,
                           $"Send ConnectionTestRequest. ({nameof(requestBody.Protocol)}: {requestBody.Protocol}, {nameof(requestBody.PortNumber)}: {requestBody.PortNumber})");
                await SendRequestAsync(requestBody).ConfigureAwait(false);

                var reply = await ReceiveReplyAsync <ConnectionTestReplyMessage>().ConfigureAwait(false);

                Logger.Log(LogLevel.Info, $"Receive ConnectionTestReply. ({nameof(reply.Succeed)}: {reply.Succeed})");
                return(reply.Succeed);
            }
            finally
            {
                cancelTokenSource.Cancel();
                tcpListener?.Stop();
                udpClient?.Close();
                if (task != null)
                {
                    await Task.WhenAll(task).ConfigureAwait(false);
                }

                task?.Dispose();
                udpClient?.Dispose();
                cancelTokenSource.Dispose();
            }
        }
        /// <summary>
        /// Check if other machine can connect to this machine with the port for game without semaphore.
        /// </summary>
        /// <param name="protocol">The protocol which is used for accept TCP connection of game</param>
        /// <param name="portNumber">The port number which is used for accept TCP connection of game</param>
        /// <exception cref="ClientErrorException"></exception>
        /// <exception cref="InvalidOperationException">The port is already used by other connection or listener</exception>
        /// <returns></returns>
        private async Task <bool> ConnectionTestCoreAsync(TransportProtocol protocol, ushort portNumber)
        {
            TcpListener tcpListener       = null;
            UdpClient   udpClient         = null;
            Task        task              = null;
            var         cancelTokenSource = new CancellationTokenSource();

            try
            {
                if (!Connected)
                {
                    throw new ClientErrorException(ClientErrorCode.NotConnected);
                }

                if (!NetworkHelper.CheckPortAvailability(protocol, portNumber))
                {
                    throw new InvalidOperationException(
                              $"The port \"{portNumber}\" is already used by other {portNumber} connection or listener.");
                }

                // Accept both IPv4 and IPv6
                if (protocol == TransportProtocol.Tcp)
                {
                    tcpListener = new TcpListener(IPAddress.IPv6Any, portNumber);
                    tcpListener.Server.SetSocketOption(
                        SocketOptionLevel.IPv6,
                        SocketOptionName.IPv6Only,
                        0);
                    tcpListener.Start();

                    async Task Func(CancellationToken cancellationToken)
                    {
                        try
                        {
                            while (!cancellationToken.IsCancellationRequested)
                            {
                                using (await Task.Run(tcpListener.AcceptSocketAsync, cancellationToken)
                                       .ConfigureAwait(false))
                                {
                                }
                            }
                        }
                        // ObjectDisposedException is thrown when we cancel accepting after we started accepting.
                        catch (ObjectDisposedException)
                        {
                        }
                    }

                    task = Func(cancelTokenSource.Token);
                }
                else
                {
                    udpClient = new UdpClient(portNumber);

                    async Task Func(UdpClient pUdpClient, CancellationToken cancellationToken)
                    {
                        try
                        {
                            var serverAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address;
                            while (true)
                            {
                                var receiveResult = await Task.Run(pUdpClient.ReceiveAsync, cancellationToken)
                                                    .ConfigureAwait(false);

                                if (cancellationToken.IsCancellationRequested)
                                {
                                    return;
                                }

                                // reply only if the message is from the server
                                if (Equals(receiveResult.RemoteEndPoint.Address, serverAddress))
                                {
                                    await Task.Run(async() =>
                                    {
                                        await pUdpClient.SendAsync(receiveResult.Buffer, receiveResult.Buffer.Length,
                                                                   receiveResult.RemoteEndPoint).ConfigureAwait(false);
                                    }, cancellationToken).ConfigureAwait(false);
                                }
                            }
                        }
                        // ObjectDisposedException is thrown when we cancel accepting after we started accepting.
                        catch (ObjectDisposedException)
                        {
                        }
                    }

                    task = Func(udpClient, cancelTokenSource.Token);
                }

                var requestBody = new ConnectionTestRequestMessage {
                    Protocol = protocol, PortNumber = portNumber
                };
                Logger.Log(LogLevel.Info,
                           $"Send ConnectionTestRequest. ({nameof(requestBody.Protocol)}: {requestBody.Protocol}, {nameof(requestBody.PortNumber)}: {requestBody.PortNumber})");
                await SendRequestAsync(requestBody).ConfigureAwait(false);

                var reply = await ReceiveReplyAsync <ConnectionTestReplyMessage>().ConfigureAwait(false);

                Logger.Log(LogLevel.Info, $"Receive ConnectionTestReply. ({nameof(reply.Succeed)}: {reply.Succeed})");
                return(reply.Succeed);
            }
            finally
            {
                cancelTokenSource.Cancel();
                tcpListener?.Stop();
                udpClient?.Close();
                if (task != null)
                {
                    await Task.WhenAll(task).ConfigureAwait(false);
                }

                task?.Dispose();
                udpClient?.Dispose();
                cancelTokenSource.Dispose();
            }
        }