Introduction
Microsoft .NET contains many enhanced foundation classes that makes writing high performance socket applications easy. This project demonstrates how to use asynchronous sockets to write a high performance yet simple to use TCP tunneling application--mapping ports from one machine to many others.
I was looking for a simple TCP tunneling application but didn't find any on this site. My requirement is a high performance (low CPU utilization) application, that can run as a Windows Service, is easy to configure, and can handle many concurrent ports.
Overview
Using the code
Since WinTunnel is a Windows Service, the first step is to install it. You can pass a command line switch -install to install it as a service using the LocalSystem account. Optionally, you can pass in a user name and password via the -user and -password switches, respectively.
The program needs to read a configuration file called WinTunnel.ini in the current directory or the directory where the binary is located. The content of the configuration file is very simple. The first line defines a service name--can be anything because it is only used for printing debug messages. The second line defines where to listen for client connections. The third line defines the server connection information.
[HTTP]
accept = 80 <====== defines which port to accept the
client connection--
either <Port> or <IP:Port>
connect = 192.168.1.10:80 <====== defines the target of the connection
--must be in the format <IP:Port>
[HTTPS]
accept = 192.168.1.100:443
connect = 192.168.1.125:443
Interesting issues
One interesting problem I encountered is that .NET requires the use of InstallUtil.exe to put the C# service into the Global Assembly Cache (GAC) and register with the Service Control Manager (SCM). In order to just install it by running the binary, a background process is launched that calls InstallUtil.exe with all the required parameters. The output is then redirected to a buffer and printed out on the console--see the code below.
Collapse
static
void launchProcess(String binary, String argument)
{
System.Diagnostics.ProcessStartInfo psInfo =
new System.Diagnostics.ProcessStartInfo(binary, argument);
System.Console.WriteLine();
System.Console.WriteLine(psInfo.FileName +
" " + psInfo.Arguments);
System.Console.WriteLine();
psInfo.RedirectStandardOutput = true;
psInfo.WindowStyle =
System.Diagnostics.ProcessWindowStyle.Hidden;
psInfo.UseShellExecute = false;
System.Diagnostics.Process ps;
ps = System.Diagnostics.Process.Start(psInfo);
System.IO.StreamReader msgOut = ps.StandardOutput;
ps.WaitForExit(5000); if (ps.HasExited)
{
System.Console.WriteLine(msgOut.ReadToEnd());
}
return;
}
In order to debug a Service application, a debug switch was added to the application. Generally, it is hard to debug a service application because there is no console. When the debug switch is set, log messages are printed on the console. In order to properly shutdown the service application in this mode, a Win32 API call is needed to capture the Ctrl-C key press. Once it is captured, an event object is signaled to properly shutdown--see ConsoleEvent.cs for details.
if (m_debug)
{
m_ctrl = new ConsoleCtrl();
m_ctrl.ControlEvent += new
ConsoleCtrl.ControlEventHandler(consoleEventHandler);
}
publicstaticvoid consoleEventHandler(ConsoleCtrl.ConsoleEvent consoleEvent)
{
if (ConsoleCtrl.ConsoleEvent.CTRL_C == consoleEvent)
{
Logger.getInstance().info("Received CTRL-C from" +
" Console. Shutting down...");
WinTunnel.shutdownEvent.Set();
}
else
{
Logger.getInstance().warn("Received unknown" +
" event {0}. Ignoring...", consoleEvent);
}
}
Code discussion
ThreadPool
In order to make the application design simple, the ThreadPool
class was created. It allows the application to be broken down to various tasks and executed by the Thread Pool. This design greatly reduces the need to synchronize--a big performance bottleneck. The idea of the thread pool is that an application should not create many threads. Instead, a small number of them should be created during startup, and reused over and over again until shutdown. Any work that needs to be done in the application will have to be added to a task list, and if there is a free thread, it should pick up the task and execute it.
A good analogy would be with the UPS delivery service. It will be terribly in-efficient if a new worker has to be hired every time a package needs to be delivered. Instead, UPS hires a bunch of them and asks them to deliver packages over and over again.
Similar to the package in the above analogy, a piece of work that needs to be done in the application is called a task. The task must implement the ITask
interface--which has two methods: getName()
and run()
. The getName()
method is used for debugging and returns the details of the task. The run()
method is the entry point for a thread in the thread pool to perform the work.
public
interface ITask
{
void run();
String getName();
}
In this application, there are only three types of tasks. The first task is to listen for client connections. This is implemented by the ProxyClientListenerTask
. When the task executes, it creates a socket, and then calls the asynchronous BeginAccept()
method. The call requires a callback method and also an object as a parameter (this
).
public
void run()
{
listenSocket = new Socket( AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
listenSocket.Bind(m_config.localEP);
listenSocket.Listen(10);
logger.info("[{0}] Waiting for client connection at {1}...",
m_config.serviceName, m_config.localEP.ToString());
listenSocket.BeginAccept( new AsyncCallback(
ProxyClientListenerTask.acceptCallBack), this);
}
The callback is invoked when the client connects to the listening socket. The first thing is to retrieve the passed in object which gets the socket. EndAccept()
is then called, and the second task is created.
Collapse
public
static
void acceptCallBack(IAsyncResult ar)
{
ProxyConnection conn = null;
try
{
ProxyClientListenerTask listener =
(ProxyClientListenerTask) ar.AsyncState;
conn = m_mgr.getConnection();
conn.serviceName = listener.m_config.serviceName;
conn.clientSocket = listener.listenSocket.EndAccept(ar);
logger.info("[{0}] Conn#{1} Accepted new connection." +
" Local: {2}, Remote: {3}.",
conn.serviceName,
conn.connNumber,
conn.clientSocket.LocalEndPoint.ToString(),
conn.clientSocket.RemoteEndPoint.ToString() );
conn.serverEP = listener.m_config.serverEP;
listener.listenSocket.BeginAccept( new AsyncCallback(
ProxyClientListenerTask.acceptCallBack), listener);
ProxyServerConnectTask serverTask =
new ProxyServerConnectTask(conn);
ThreadPool.getInstance().addTask(serverTask);
}
catch (SocketException se)
{
logger.error("[{0}] Conn# {1} Socket Error occurred" +
" when accepting client socket. Error Code is: {2}",
conn.serviceName, conn.connNumber, se.ErrorCode);
if (conn != null)
{
conn.Release();
}
}
catch (Exception e)
{
logger.error("[{0}] Conn# {1} Error occurred when" +
" accepting client socket. Error is: {2}",
conn.serviceName, conn.connNumber, e);
if (conn != null)
{
conn.Release();
}
}
finally
{
conn = null;
}
}
The second task is to connect to the server that the client's connection should be mapped to. This is implemented by the ProxyServerConnectTask
. When the task executes, it creates a new socket, and makes an asynchronous BeginConnect()
to connect to the server.
public
void run()
{
m_conn.serverSocket = new Socket( AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
m_conn.serverSocket.BeginConnect(m_conn.serverEP,
new AsyncCallback(connectCallBack), m_conn);
}
The callback is invoked when the connection to the server is established and the third task is created.
public
static
void connectCallBack(IAsyncResult ar)
{
ProxyConnection conn = (ProxyConnection) ar.AsyncState;
try
{
conn.serverSocket.EndConnect(ar);
logger.info("[{0}] ProxyConnection#{1}--connected " +
"to Server. Server: {2}, Local: {3}.",
conn.serviceName,
conn.connNumber,
conn.serverSocket.RemoteEndPoint,
conn.serverSocket.LocalEndPoint);
ProxySwapDataTask dataTask = new ProxySwapDataTask(conn);
ThreadPool.getInstance().addTask(dataTask);
}
{...removed error handling code...}
}
The third task takes both the client socket and the server socket, and swaps data back and forth between them. This is implemented by the ProxySwapDataTask
. There is a callback sending and receiving data for both the client and the server socket. There are also various checks to detect socket errors. When an error occurs, both the client and the server socket are shutdown.
Collapse
public
void run()
{
if (m_conn.clientSocket == null || m_conn.serverSocket == null)
{
logger.error("[{0}] ProxyConnection#{1}--Either" +
" client socket or server socket is null.",
m_conn.serviceName, m_conn.connNumber);
m_conn.Release();
return;
}
if (m_conn.clientSocket.Connected && m_conn.serverSocket.Connected)
{
m_conn.clientSocket.BeginReceive( m_conn.clientReadBuffer,
0, ProxyConnection.BUFFER_SIZE, 0,
new AsyncCallback(clientReadCallBack), m_conn);
m_conn.serverSocket.BeginReceive( m_conn.serverReadBuffer,
0, ProxyConnection.BUFFER_SIZE, 0,
new AsyncCallback(serverReadCallBack), m_conn);
}
else
{
logger.error("[{0}] ProxyConnection#{1}: Either" +
" the client or server socket got disconnected.",
m_conn.serviceName, m_conn.connNumber );
m_conn.Release();
}
m_conn = null;
}
There is a callback for sending and receiving data for both the client and the server socket. There are also various checks to detect socket errors. When an error occurs, the connection is shutdown, and both the client and the server socket are closed.
End note
Unlike others, this project is the complete source of a fully functioning application. As such, there are various classes such as logging that I don't even cover in the discussion. I only try to point out the interesting things that I encountered when designing and implementing the application. Hopefully, it will be useful for someone doing something similar.
History
- 2006-06-28 - Initial implementation.
- 2006-07-10 - Enhanced to show more debug messages, added more error handling logic, and cleanup of objects. Also updated this documentation.
Han_Jun_Li
|
Click here to view Han_Jun_Li's online profile.
|