Skip to content

Instantly share code, notes, and snippets.

@LaurensVergote
Last active December 19, 2023 10:26
Show Gist options
  • Save LaurensVergote/ba4b4ea1825f2ff5fed3d1aee9214dd8 to your computer and use it in GitHub Desktop.
Save LaurensVergote/ba4b4ea1825f2ff5fed3d1aee9214dd8 to your computer and use it in GitHub Desktop.
NatsClientPortLock
using NATS.Client;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace NatsPortDemo
{
public class NatsClientDemo
{
public static void Main(string[] args)
{
var localHost = Dns.GetHostName();
var availablePort = PortProvider.GetFreePort();
var port = availablePort + 200; //Should give us an approximation on the "next" available ports range
PortProvider.UnlockPort(availablePort);
//1] Start nats-server on port
var natsProcess = Process.Start("nats-server.exe", $"--addr {localHost} --port {port}");
Thread.Sleep(5000); //Wait for nats-server.exe to start up completely
//2] Make a few connections. They should all succeed, client ports taken should be [port+1;port+20]
List<IConnection> connections = new List<IConnection>();
for (int i = 0; i < 20; i++)
{
var options = ConnectionFactory.GetDefaultOptions();
options.AllowReconnect = true; //Not required. default is true already
options.MaxReconnect = Options.ReconnectForever; //Never give up
options.ReconnectWait = 1000; //Try reconnect every ~1s
options.Servers = new string[] { $"nats://{localHost}:{port}" };
try
{
IConnection connection = new ConnectionFactory().CreateConnection(options);
connections.Add(connection);
}
catch (NATSConnectionException nce)
{
Debugger.Launch();
}
catch (Exception ex)
{
Debugger.Launch();
}
}
Thread.Sleep(5000); //Let the connections settle in a bit, don't want to scare them
//Print port usage for baseline. Should see 20 Established connections, all to the same port (the one nats-server.exe is hosting on)
PrintPortUsage(port);
//Stop the nats-server process. This forces the connections into the reconnection loop.
natsProcess.Kill();
//At some point in the reconnection loop, 1 of the connections will grab "port" as its local endpoint and manage to establish a connection.
//This blocks the nats-server process from starting again on that port.
//After a while, You'll see that a connection gets established between <localhost>:<port> and <localhost>:<port>
//This Established connection persists, which should be avoided.
//Might have to rerun the process a few times to trigger it. It's possible that other processes in the mean time grab the port for a second to do a http request or something.
//Rerun if you notice the logged LocalEndpoint port getting bigger than the RemoteEndpoint port and no "Established" connection is seen.
while (true)
{
Thread.Sleep(5000);
PrintPortUsage(port);
}
}
private static void PrintPortUsage(int remotePort)
{
IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties();
IPEndPoint[] endPoints = ipProperties.GetActiveTcpListeners();
//Filter on remote port, we can be pretty confident the only process trying to connect to this port is us.
var tcpConnections =
ipProperties.GetActiveTcpConnections()
.Where(c => c.RemoteEndPoint.Port == remotePort);
string horizontalLine = new string('-', 3 + 3 * 20);
string rowFormat = " {0,-20}| {1,-20}| {2,-20}";
Console.WriteLine(); //Give some space
Console.WriteLine(horizontalLine);
Console.WriteLine(rowFormat, "Local Endpoint", "Remote Endpoint", "State");
Console.WriteLine(horizontalLine);
foreach (TcpConnectionInformation info in tcpConnections)
{
Console.WriteLine(rowFormat,
$"{info.LocalEndPoint.Address}:{info.LocalEndPoint.Port}",
$"{info.RemoteEndPoint.Address}:{info.RemoteEndPoint.Port}",
info.State.ToString());
}
Console.WriteLine(horizontalLine);
}
}
public static class PortProvider
{
private static ConcurrentDictionary<int, TcpListener> _lockedPorts = new ConcurrentDictionary<int, TcpListener>();
/// <summary>
/// Don't forget to unlock the port just before you want to use it for another application
/// </summary>
/// <returns></returns>
public static int GetFreePort()
{
var l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
int port = ((IPEndPoint)l.LocalEndpoint).Port;
_lockedPorts.TryAdd(port, l);
//l.Stop();
return port;
}
/// <summary>
/// Make the port free for use by another application.
/// </summary>
/// <param name="port"></param>
public static void UnlockPort(int port)
{
if (_lockedPorts.TryGetValue(port, out var listener))
listener.Stop();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment