public void Connect(string ip, int port) { // not if already started if (Connecting || Connected) { Log.Warning("Telepathy Client can not create connection because an existing connection is connecting or connected"); return; } // overwrite old thread's state object. create a new one to avoid // data races where an old dieing thread might still modify the // current state! fixes all the flaky tests! state = new ClientConnectionState(MaxMessageSize); // We are connecting from now until Connect succeeds or fails state.Connecting = true; // create a TcpClient with perfect IPv4, IPv6 and hostname resolving // support. // // * TcpClient(hostname, port): works but would connect (and block) // already // * TcpClient(AddressFamily.InterNetworkV6): takes Ipv4 and IPv6 // addresses but only connects to IPv6 servers (e.g. Telepathy). // does NOT connect to IPv4 servers (e.g. Mirror Booster), even // with DualMode enabled. // * TcpClient(): creates IPv4 socket internally, which would force // Connect() to only use IPv4 sockets. // // => the trick is to clear the internal IPv4 socket so that Connect // resolves the hostname and creates either an IPv4 or an IPv6 // socket as needed (see TcpClient source) state.client.Client = null; // clear internal IPv4 socket until Connect() // client.Connect(ip, port) is blocking. let's call it in the thread // and return immediately. // -> this way the application doesn't hang for 30s if connect takes // too long, which is especially good in games // -> this way we don't async client.BeginConnect, which seems to // fail sometimes if we connect too many clients too fast state.receiveThread = new Thread(() => { ReceiveThreadFunction(state, ip, port, MaxMessageSize, NoDelay, SendTimeout, ReceiveTimeout, ReceiveQueueLimit); }); state.receiveThread.IsBackground = true; state.receiveThread.Start(); }
// the thread function // STATIC to avoid sharing state! // => pass ClientState object. a new one is created for each new thread! // => avoids data races where an old dieing thread might still modify // the current thread's state :/ static void ReceiveThreadFunction(ClientConnectionState state, string ip, int port, int MaxMessageSize, bool NoDelay, int SendTimeout, int ReceiveTimeout, int ReceiveQueueLimit) { Thread sendThread = null; // absolutely must wrap with try/catch, otherwise thread // exceptions are silent try { // connect (blocking) state.client.Connect(ip, port); state.Connecting = false; // volatile! // set socket options after the socket was created in Connect() // (not after the constructor because we clear the socket there) state.client.NoDelay = NoDelay; state.client.SendTimeout = SendTimeout; state.client.ReceiveTimeout = ReceiveTimeout; // start send thread only after connected // IMPORTANT: DO NOT SHARE STATE ACROSS MULTIPLE THREADS! sendThread = new Thread(() => { ThreadFunctions.SendLoop(0, state.client, state.sendPipe, state.sendPending); }); sendThread.IsBackground = true; sendThread.Start(); // run the receive loop // (receive pipe is shared across all loops) ThreadFunctions.ReceiveLoop(0, state.client, MaxMessageSize, state.receivePipe, ReceiveQueueLimit); } catch (SocketException exception) { // this happens if (for example) the ip address is correct // but there is no server running on that ip/port Log.Info("Client Recv: failed to connect to ip=" + ip + " port=" + port + " reason=" + exception); // add 'Disconnected' event to receive pipe so that the caller // knows that the Connect failed. otherwise they will never know state.receivePipe.Enqueue(0, EventType.Disconnected, default); } catch (ThreadInterruptedException) { // expected if Disconnect() aborts it } catch (ThreadAbortException) { // expected if Disconnect() aborts it } catch (ObjectDisposedException) { // expected if Disconnect() aborts it and disposed the client // while ReceiveThread is in a blocking Connect() call } catch (Exception exception) { // something went wrong. probably important. Log.Error("Client Recv Exception: " + exception); } // sendthread might be waiting on ManualResetEvent, // so let's make sure to end it if the connection // closed. // otherwise the send thread would only end if it's // actually sending data while the connection is // closed. sendThread?.Interrupt(); // Connect might have failed. thread might have been closed. // let's reset connecting state no matter what. state.Connecting = false; // if we got here then we are done. ReceiveLoop cleans up already, // but we may never get there if connect fails. so let's clean up // here too. state.client?.Close(); }