OSDN Git Service

8080が使用中のときにポート番号を自動割り当てする
[kancollesniffer/KancolleSniffer.git] / TrotiNet / TcpServer.cs
1 /*\r
2     Generic TCP/IP server, initially inspired from:\r
3     http://msdn.microsoft.com/en-us/library/fx6588te%28vs.71%29.aspx\r
4 \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
11 */\r
12 \r
13 // Uncomment to debug BeginAccept/EndAccept\r
14 //#define DEBUG_ACCEPT_CONNECTION\r
15 \r
16 using System;\r
17 using System.Collections.Generic;\r
18 using System.Net;\r
19 using System.Net.Sockets;\r
20 using System.Runtime.CompilerServices;\r
21 using System.Threading;\r
22 #if LOG4NET\r
23 using log4net;\r
24 #endif\r
25 \r
26 [assembly: InternalsVisibleTo("TrotiNet.Test")]\r
27 namespace TrotiNet\r
28 {\r
29     /// <summary>\r
30     /// Implementation of a TCP/IP server\r
31     /// </summary>\r
32     public class TcpServer\r
33     {\r
34         static readonly ILog log = Log.Get();\r
35 \r
36 #if DEBUG_ACCEPT_CONNECTION\r
37         void Pause()\r
38         {\r
39             log.Debug("Pause start");\r
40             System.Threading.Thread.Sleep(3000);\r
41             log.Debug("Pause end");\r
42         }\r
43 #endif\r
44 \r
45         /// <summary>\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
48         /// </summary>\r
49         public IPAddress BindAddress { get; set; }\r
50 \r
51         /// <summary>\r
52         /// Timer that calls CheckSockets regularly\r
53         /// </summary>\r
54         Timer CleanTimer;\r
55 \r
56         /// <summary>\r
57         /// Set of open sockets, indexed by socket identifier\r
58         /// </summary>\r
59         protected Dictionary<int, HttpSocket> ConnectedSockets;\r
60 \r
61         /// <summary>\r
62         /// Set if an error has occured while the server was initializing\r
63         /// the listening thread\r
64         /// </summary>\r
65         public Exception InitListenException { get; protected set; }\r
66 \r
67         /// <summary>\r
68         /// Set when the listening thread has finished its initialization\r
69         /// (either successfully, or an exception has been thrown)\r
70         /// </summary>\r
71         /// <seealso cref="InitListenException"/>\r
72         /// <seealso cref="IsListening"/>\r
73         public ManualResetEvent InitListenFinished { get; protected set; }\r
74 \r
75         /// <summary>\r
76         /// Set to true if the listening thread is currently listening\r
77         /// for incoming connections\r
78         /// </summary>\r
79         public bool IsListening { get; protected set; }\r
80 \r
81         /// <summary>\r
82         /// Set to true if the server is about to shut down\r
83         /// </summary>\r
84         protected bool IsShuttingDown { get; private set; }\r
85 \r
86         /// <summary>\r
87         /// Incremented at every client connection\r
88         /// </summary>\r
89         int LastClientId;\r
90 \r
91         Thread ListeningThread;\r
92         ManualResetEvent ListenThreadSwitch;\r
93 \r
94         /// <summary>\r
95         /// Port used for local browser-proxy communication\r
96         /// </summary>\r
97         public int LocalPort;\r
98 \r
99         /// <summary>\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
103         /// </summary>\r
104         public delegate AbstractProxyLogic OnNewClient(HttpSocket ss);\r
105 \r
106         OnNewClient OnClientStart;\r
107 \r
108         bool UseIPv6;\r
109 \r
110         /// <summary>\r
111         /// Initialize, but do not start, a multi-threaded TCP server\r
112         /// listening for localhost connections only\r
113         /// </summary>\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
117         /// </param>\r
118         public TcpServer(int localPort, bool bUseIPv6)\r
119         {\r
120             if (localPort < 0)\r
121                 throw new ArgumentException("localPort");\r
122 \r
123             LocalPort = localPort;\r
124             UseIPv6 = bUseIPv6;\r
125 \r
126             ConnectedSockets = new Dictionary<int, HttpSocket>();\r
127             InitListenFinished = new ManualResetEvent(false);\r
128             ListenThreadSwitch = new ManualResetEvent(false);\r
129             ListeningThread = null;\r
130         }\r
131 \r
132         /// <summary>\r
133         /// Callback method for accepting new connections\r
134         /// </summary>\r
135         void AcceptCallback(IAsyncResult ar)\r
136         {\r
137             if (IsShuttingDown)\r
138                 return;\r
139 \r
140             // Have we really changed thread?\r
141             if (ListeningThread.ManagedThreadId ==\r
142                 System.Threading.Thread.CurrentThread.ManagedThreadId)\r
143             {\r
144                 // No! Give me a new thread!\r
145                 new Thread(() => AcceptCallback(ar)).Start();\r
146                 return;\r
147             }\r
148 \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
152 \r
153             // Signal the main thread to continue\r
154             ListenThreadSwitch.Set();\r
155 \r
156 #if DEBUG_ACCEPT_CONNECTION\r
157             log.Debug("\tAcceptCallback sent signal");\r
158 #endif\r
159 \r
160             // Create the state object\r
161             HttpSocket state = new HttpSocket(handler);\r
162             state.id = ++LastClientId;\r
163 \r
164             lock (ConnectedSockets)\r
165                 ConnectedSockets[state.id] = state;\r
166 \r
167             AbstractProxyLogic proxy = null;\r
168             try\r
169             {\r
170                 proxy = OnClientStart(state);\r
171             } catch (Exception e) { log.Error(e); }\r
172             if (proxy == null)\r
173             {\r
174                 CloseSocket(state);\r
175                 return;\r
176             }\r
177 \r
178             // No need for asynchronous I/O from now on\r
179             try\r
180             {\r
181                 while (proxy.LogicLoop())\r
182                     if (IsShuttingDown || state.IsSocketDead())\r
183                         break;\r
184 \r
185                 log.Debug("Shutting down socket");\r
186             }\r
187             catch (System.Net.Sockets.SocketException) { /* ignore */ }\r
188             catch (TrotiNet.IoBroken) { /* ignore */ }\r
189             catch (Exception e)\r
190             {\r
191                 log.Error(e);\r
192                 log.Debug("Closing socket on error");\r
193             }\r
194 \r
195             CloseSocket(state);\r
196         }\r
197 \r
198         /// <summary>\r
199         /// Close broken sockets\r
200         /// </summary>\r
201         /// <remarks>\r
202         /// This function is called regularly to clean up the list of\r
203         /// connected sockets.\r
204         /// </remarks>\r
205         void CheckSockets(object eventState)\r
206         {\r
207             try\r
208             {\r
209                 lock (ConnectedSockets)\r
210                 {\r
211                     foreach (var kv in ConnectedSockets)\r
212                     {\r
213                         try\r
214                         {\r
215                             int id = kv.Key;\r
216                             HttpSocket state = kv.Value;\r
217                             if (state == null || state.IsSocketDead())\r
218                                 ConnectedSockets.Remove(id);\r
219                         }\r
220                         catch (Exception e)\r
221                         {\r
222                             log.Error(e);\r
223                         }\r
224                     }\r
225                 }\r
226             }\r
227             catch { }\r
228         }\r
229 \r
230         /// <summary>\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
233         /// socket\r
234         /// </summary>\r
235         virtual protected void CloseSocket(HttpSocket state)\r
236         {\r
237             HttpSocket actual_state;\r
238             lock (ConnectedSockets)\r
239             {\r
240                 if (!ConnectedSockets.TryGetValue(state.id, out actual_state))\r
241                     return;\r
242 \r
243                 System.Diagnostics.Debug.Assert(actual_state == state);\r
244                 ConnectedSockets.Remove(state.id);\r
245             }\r
246 \r
247             state.CloseSocket();\r
248         }\r
249 \r
250         /// <summary>\r
251         /// Spawn a thread that listens to incoming connections\r
252         /// </summary>\r
253         public void Start(OnNewClient onConnection)\r
254         {\r
255             InitListenException = null;\r
256             InitListenFinished.Reset();\r
257             IsListening = false;\r
258             IsShuttingDown = false;\r
259             OnClientStart = onConnection;\r
260 \r
261             ListeningThread = new Thread(StartThread);\r
262             ListeningThread.Name = "ListenTCP";\r
263             ListeningThread.IsBackground = true;\r
264             ListeningThread.Start();\r
265 \r
266             const int cleanTimeout = 300 * 1000; // in ms\r
267             CleanTimer = new Timer(new TimerCallback(CheckSockets), null,\r
268                 cleanTimeout, cleanTimeout);\r
269         }\r
270 \r
271         /// <summary>\r
272         /// Open a listener socket and wait for connections\r
273         /// </summary>\r
274         void StartListening(ref Socket ListeningSocket)\r
275         {\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
279 \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
283                 : BindAddress;\r
284             IPEndPoint localEndPoint = new IPEndPoint(lb, this.LocalPort);\r
285 \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
290                 ProtocolType.Tcp);\r
291 \r
292             log.Info("Listening to incoming IPv" +\r
293                 (UseIPv6 ? "6" : "4") + " connections on port " + LocalPort);\r
294 \r
295             // Bind the socket to the local endpoint and listen for incoming\r
296             // connections.\r
297             ListeningSocket.Bind(localEndPoint);\r
298             LocalPort = ((IPEndPoint)ListeningSocket.LocalEndPoint).Port;\r
299             ListeningSocket.Listen(1000);\r
300 \r
301             // Notify that the listening thread is up and running\r
302             IsListening = true;\r
303             InitListenFinished.Set();\r
304 \r
305             // Main listening loop starts now\r
306             try\r
307             {\r
308                 while (!IsShuttingDown)\r
309                 {\r
310 #if DEBUG_ACCEPT_CONNECTION\r
311                     log.Debug("Reset signal");\r
312 #endif\r
313 \r
314                     ListenThreadSwitch.Reset();\r
315                     if (IsShuttingDown)\r
316                         break;\r
317 \r
318 #if DEBUG_ACCEPT_CONNECTION\r
319                     log.Debug("BeginAccept (before)");\r
320 #endif\r
321 \r
322                     ListeningSocket.BeginAccept(\r
323                         new AsyncCallback(AcceptCallback), ListeningSocket);\r
324 \r
325 #if DEBUG_ACCEPT_CONNECTION\r
326                     log.Debug("Wait signal");\r
327 #endif\r
328 \r
329                     // Wait until a connection is made before continuing\r
330                     ListenThreadSwitch.WaitOne();\r
331 \r
332 #if DEBUG_ACCEPT_CONNECTION\r
333                     log.Debug("Received signal");\r
334 #endif\r
335                 }\r
336             }\r
337             catch (Exception e)\r
338             {\r
339                 log.Error(e);\r
340             }\r
341             finally\r
342             {\r
343                 log.Debug("Stopped listening on port " + LocalPort);\r
344             }\r
345         }\r
346 \r
347         void StartThread()\r
348         {\r
349             Socket ListeningSocket = null;\r
350             try\r
351             {\r
352                 StartListening(ref ListeningSocket);\r
353             }\r
354             catch (Exception e)\r
355             {\r
356                 log.Error(e);\r
357                 IsListening = false;\r
358                 InitListenException = e;\r
359                 InitListenFinished.Set();\r
360                 ListenThreadSwitch.Set();\r
361             }\r
362             finally\r
363             {\r
364                 if (ListeningSocket != null)\r
365                     ListeningSocket.Close();\r
366             }\r
367         }\r
368 \r
369         /// <summary>\r
370         /// Stop the listening threads and close the client sockets\r
371         /// </summary>\r
372         public void Stop()\r
373         {\r
374             if (ListeningThread == null)\r
375                 return;\r
376 \r
377             log.Debug("Shutting down server");\r
378             IsShuttingDown = true;\r
379 \r
380             ListenThreadSwitch.Set();\r
381 \r
382             CleanTimer.Dispose();\r
383             CleanTimer = null;\r
384 \r
385             if (ListeningThread.IsAlive)\r
386             {\r
387                 // Create a connection to the port to unblock the\r
388                 // listener thread\r
389                 using (var sock = new Socket(AddressFamily.Unspecified,\r
390                     SocketType.Stream, ProtocolType.Tcp))\r
391                 {\r
392                     try\r
393                     {\r
394                         sock.Connect(new IPEndPoint(IPAddress.Loopback,\r
395                             this.LocalPort));\r
396                         sock.Close();\r
397                     } catch { /* ignore */ }\r
398                 }\r
399 \r
400                 if (ListeningThread.ThreadState == ThreadState.WaitSleepJoin)\r
401                     ListeningThread.Interrupt();\r
402                 Thread.Sleep(1000);\r
403                 ListeningThread.Abort();\r
404             }\r
405             \r
406             ListeningThread = null;\r
407             IsListening = false;\r
408 \r
409             log.Info("Server stopped");\r
410         }\r
411     }\r
412 }\r