/// <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(); } }
/// <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(); } }