2 Generic TCP/IP server, initially inspired from:
\r
3 http://msdn.microsoft.com/en-us/library/fx6588te%28vs.71%29.aspx
\r
5 However, the MSDN example fails if you place a long Sleep() in
\r
6 AcceptCallback(), then try to open two client connections during the
\r
7 Sleep(). In that case, it appears that the second call to BeginAccept()
\r
8 executes in the same thread than its caller (StartListening), so no
\r
9 additional connection will be accepted until the last AcceptCallback()
\r
10 finishes. See the workaround in AcceptCallback().
\r
13 // Uncomment to debug BeginAccept/EndAccept
\r
14 //#define DEBUG_ACCEPT_CONNECTION
\r
17 using System.Collections.Generic;
\r
19 using System.Net.Sockets;
\r
20 using System.Runtime.CompilerServices;
\r
21 using System.Threading;
\r
26 [assembly: InternalsVisibleTo("TrotiNet.Test")]
\r
30 /// Implementation of a TCP/IP server
\r
32 public class TcpServer
\r
34 static readonly ILog log = Log.Get();
\r
36 #if DEBUG_ACCEPT_CONNECTION
\r
39 log.Debug("Pause start");
\r
40 System.Threading.Thread.Sleep(3000);
\r
41 log.Debug("Pause end");
\r
46 /// If not null, specify which address the listening socket should
\r
47 /// be bound to. If null, it will default to the loopback address.
\r
49 public IPAddress BindAddress { get; set; }
\r
52 /// Timer that calls CheckSockets regularly
\r
57 /// Set of open sockets, indexed by socket identifier
\r
59 protected Dictionary<int, HttpSocket> ConnectedSockets;
\r
62 /// Set if an error has occured while the server was initializing
\r
63 /// the listening thread
\r
65 public Exception InitListenException { get; protected set; }
\r
68 /// Set when the listening thread has finished its initialization
\r
69 /// (either successfully, or an exception has been thrown)
\r
71 /// <seealso cref="InitListenException"/>
\r
72 /// <seealso cref="IsListening"/>
\r
73 public ManualResetEvent InitListenFinished { get; protected set; }
\r
76 /// Set to true if the listening thread is currently listening
\r
77 /// for incoming connections
\r
79 public bool IsListening { get; protected set; }
\r
82 /// Set to true if the server is about to shut down
\r
84 protected bool IsShuttingDown { get; private set; }
\r
87 /// Incremented at every client connection
\r
91 Thread ListeningThread;
\r
92 ManualResetEvent ListenThreadSwitch;
\r
95 /// Port used for local browser-proxy communication
\r
97 public int LocalPort;
\r
100 /// Called every time a connection is accepted from the browser
\r
101 /// by the proxy. Must return the instance that will handle the
\r
102 /// communication for the new connection.
\r
104 public delegate AbstractProxyLogic OnNewClient(HttpSocket ss);
\r
106 OnNewClient OnClientStart;
\r
111 /// Initialize, but do not start, a multi-threaded TCP server
\r
112 /// listening for localhost connections only
\r
114 /// <param name="localPort">TCP port to listen to</param>
\r
115 /// <param name="bUseIPv6">
\r
116 /// If true, listen on ::1 only. If false, listen on 127.0.0.1 only.
\r
118 public TcpServer(int localPort, bool bUseIPv6)
\r
121 throw new ArgumentException("localPort");
\r
123 LocalPort = localPort;
\r
124 UseIPv6 = bUseIPv6;
\r
126 ConnectedSockets = new Dictionary<int, HttpSocket>();
\r
127 InitListenFinished = new ManualResetEvent(false);
\r
128 ListenThreadSwitch = new ManualResetEvent(false);
\r
129 ListeningThread = null;
\r
133 /// Callback method for accepting new connections
\r
135 void AcceptCallback(IAsyncResult ar)
\r
137 if (IsShuttingDown)
\r
140 // Have we really changed thread?
\r
141 if (ListeningThread.ManagedThreadId ==
\r
142 System.Threading.Thread.CurrentThread.ManagedThreadId)
\r
144 // No! Give me a new thread!
\r
145 new Thread(() => AcceptCallback(ar)).Start();
\r
149 // Get the socket that handles the client request
\r
150 Socket listener = (Socket)ar.AsyncState;
\r
151 Socket handler = listener.EndAccept(ar);
\r
153 // Signal the main thread to continue
\r
154 ListenThreadSwitch.Set();
\r
156 #if DEBUG_ACCEPT_CONNECTION
\r
157 log.Debug("\tAcceptCallback sent signal");
\r
160 // Create the state object
\r
161 HttpSocket state = new HttpSocket(handler);
\r
162 state.id = ++LastClientId;
\r
164 lock (ConnectedSockets)
\r
165 ConnectedSockets[state.id] = state;
\r
167 AbstractProxyLogic proxy = null;
\r
170 proxy = OnClientStart(state);
\r
171 } catch (Exception e) { log.Error(e); }
\r
174 CloseSocket(state);
\r
178 // No need for asynchronous I/O from now on
\r
181 while (proxy.LogicLoop())
\r
182 if (IsShuttingDown || state.IsSocketDead())
\r
185 log.Debug("Shutting down socket");
\r
187 catch (System.Net.Sockets.SocketException) { /* ignore */ }
\r
188 catch (TrotiNet.IoBroken) { /* ignore */ }
\r
189 catch (Exception e)
\r
192 log.Debug("Closing socket on error");
\r
195 CloseSocket(state);
\r
199 /// Close broken sockets
\r
202 /// This function is called regularly to clean up the list of
\r
203 /// connected sockets.
\r
205 void CheckSockets(object eventState)
\r
209 lock (ConnectedSockets)
\r
211 foreach (var kv in ConnectedSockets)
\r
216 HttpSocket state = kv.Value;
\r
217 if (state == null || state.IsSocketDead())
\r
218 ConnectedSockets.Remove(id);
\r
220 catch (Exception e)
\r
231 /// Remove the socket contained in the given state object
\r
232 /// from the connected array list and hash table, then close the
\r
235 virtual protected void CloseSocket(HttpSocket state)
\r
237 HttpSocket actual_state;
\r
238 lock (ConnectedSockets)
\r
240 if (!ConnectedSockets.TryGetValue(state.id, out actual_state))
\r
243 System.Diagnostics.Debug.Assert(actual_state == state);
\r
244 ConnectedSockets.Remove(state.id);
\r
247 state.CloseSocket();
\r
251 /// Spawn a thread that listens to incoming connections
\r
253 public void Start(OnNewClient onConnection)
\r
255 InitListenException = null;
\r
256 InitListenFinished.Reset();
\r
257 IsListening = false;
\r
258 IsShuttingDown = false;
\r
259 OnClientStart = onConnection;
\r
261 ListeningThread = new Thread(StartThread);
\r
262 ListeningThread.Name = "ListenTCP";
\r
263 ListeningThread.IsBackground = true;
\r
264 ListeningThread.Start();
\r
266 const int cleanTimeout = 300 * 1000; // in ms
\r
267 CleanTimer = new Timer(new TimerCallback(CheckSockets), null,
\r
268 cleanTimeout, cleanTimeout);
\r
272 /// Open a listener socket and wait for connections
\r
274 void StartListening(ref Socket ListeningSocket)
\r
276 // Note: Do not catch exceptions until we reach the main
\r
277 // listening loop, because <c>StartThread</c> should
\r
278 // intercept initialization exceptions.
\r
280 // Establish the local endpoint for the socket (only on localhost)
\r
281 IPAddress lb = (BindAddress == null)
\r
282 ? (UseIPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback)
\r
284 IPEndPoint localEndPoint = new IPEndPoint(lb, this.LocalPort);
\r
286 // Create a TCP/IP socket
\r
287 AddressFamily af = UseIPv6 ? AddressFamily.InterNetworkV6 :
\r
288 AddressFamily.InterNetwork;
\r
289 ListeningSocket = new Socket(af, SocketType.Stream,
\r
292 log.Info("Listening to incoming IPv" +
\r
293 (UseIPv6 ? "6" : "4") + " connections on port " + LocalPort);
\r
295 // Bind the socket to the local endpoint and listen for incoming
\r
297 ListeningSocket.Bind(localEndPoint);
\r
298 LocalPort = ((IPEndPoint)ListeningSocket.LocalEndPoint).Port;
\r
299 ListeningSocket.Listen(1000);
\r
301 // Notify that the listening thread is up and running
\r
302 IsListening = true;
\r
303 InitListenFinished.Set();
\r
305 // Main listening loop starts now
\r
308 while (!IsShuttingDown)
\r
310 #if DEBUG_ACCEPT_CONNECTION
\r
311 log.Debug("Reset signal");
\r
314 ListenThreadSwitch.Reset();
\r
315 if (IsShuttingDown)
\r
318 #if DEBUG_ACCEPT_CONNECTION
\r
319 log.Debug("BeginAccept (before)");
\r
322 ListeningSocket.BeginAccept(
\r
323 new AsyncCallback(AcceptCallback), ListeningSocket);
\r
325 #if DEBUG_ACCEPT_CONNECTION
\r
326 log.Debug("Wait signal");
\r
329 // Wait until a connection is made before continuing
\r
330 ListenThreadSwitch.WaitOne();
\r
332 #if DEBUG_ACCEPT_CONNECTION
\r
333 log.Debug("Received signal");
\r
337 catch (Exception e)
\r
343 log.Debug("Stopped listening on port " + LocalPort);
\r
349 Socket ListeningSocket = null;
\r
352 StartListening(ref ListeningSocket);
\r
354 catch (Exception e)
\r
357 IsListening = false;
\r
358 InitListenException = e;
\r
359 InitListenFinished.Set();
\r
360 ListenThreadSwitch.Set();
\r
364 if (ListeningSocket != null)
\r
365 ListeningSocket.Close();
\r
370 /// Stop the listening threads and close the client sockets
\r
374 if (ListeningThread == null)
\r
377 log.Debug("Shutting down server");
\r
378 IsShuttingDown = true;
\r
380 ListenThreadSwitch.Set();
\r
382 CleanTimer.Dispose();
\r
385 if (ListeningThread.IsAlive)
\r
387 // Create a connection to the port to unblock the
\r
389 using (var sock = new Socket(AddressFamily.Unspecified,
\r
390 SocketType.Stream, ProtocolType.Tcp))
\r
394 sock.Connect(new IPEndPoint(IPAddress.Loopback,
\r
397 } catch { /* ignore */ }
\r
400 if (ListeningThread.ThreadState == ThreadState.WaitSleepJoin)
\r
401 ListeningThread.Interrupt();
\r
402 Thread.Sleep(1000);
\r
403 ListeningThread.Abort();
\r
406 ListeningThread = null;
\r
407 IsListening = false;
\r
409 log.Info("Server stopped");
\r