TcpServer.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Diagnostics;
  4. using System.Net;
  5. using System.Net.Sockets;
  6. using System.Text;
  7. using System.Threading;
  8. namespace NetCoreServer
  9. {
  10. /// <summary>
  11. /// TCP server is used to connect, disconnect and manage TCP sessions
  12. /// </summary>
  13. /// <remarks>Thread-safe</remarks>
  14. public class TcpServer : IDisposable
  15. {
  16. /// <summary>
  17. /// Initialize TCP server with a given IP address and port number
  18. /// </summary>
  19. /// <param name="address">IP address</param>
  20. /// <param name="port">Port number</param>
  21. public TcpServer(IPAddress address, int port) : this(new IPEndPoint(address, port)) {}
  22. /// <summary>
  23. /// Initialize TCP server with a given IP address and port number
  24. /// </summary>
  25. /// <param name="address">IP address</param>
  26. /// <param name="port">Port number</param>
  27. public TcpServer(string address, int port) : this(new IPEndPoint(IPAddress.Parse(address), port)) {}
  28. /// <summary>
  29. /// Initialize TCP server with a given DNS endpoint
  30. /// </summary>
  31. /// <param name="endpoint">DNS endpoint</param>
  32. public TcpServer(DnsEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Host, endpoint.Port) {}
  33. /// <summary>
  34. /// Initialize TCP server with a given IP endpoint
  35. /// </summary>
  36. /// <param name="endpoint">IP endpoint</param>
  37. public TcpServer(IPEndPoint endpoint) : this(endpoint as EndPoint, endpoint.Address.ToString(), endpoint.Port) {}
  38. /// <summary>
  39. /// Initialize TCP server with a given endpoint, address and port
  40. /// </summary>
  41. /// <param name="endpoint">Endpoint</param>
  42. /// <param name="address">Server address</param>
  43. /// <param name="port">Server port</param>
  44. private TcpServer(EndPoint endpoint, string address, int port)
  45. {
  46. Id = Guid.NewGuid();
  47. Address = address;
  48. Port = port;
  49. Endpoint = endpoint;
  50. }
  51. /// <summary>
  52. /// Server Id
  53. /// </summary>
  54. public Guid Id { get; }
  55. /// <summary>
  56. /// TCP server address
  57. /// </summary>
  58. public string Address { get; }
  59. /// <summary>
  60. /// TCP server port
  61. /// </summary>
  62. public int Port { get; }
  63. /// <summary>
  64. /// Endpoint
  65. /// </summary>
  66. public EndPoint Endpoint { get; private set; }
  67. /// <summary>
  68. /// Number of sessions connected to the server
  69. /// </summary>
  70. public long ConnectedSessions { get { return Sessions.Count; } }
  71. /// <summary>
  72. /// Number of bytes pending sent by the server
  73. /// </summary>
  74. public long BytesPending { get { return _bytesPending; } }
  75. /// <summary>
  76. /// Number of bytes sent by the server
  77. /// </summary>
  78. public long BytesSent { get { return _bytesSent; } }
  79. /// <summary>
  80. /// Number of bytes received by the server
  81. /// </summary>
  82. public long BytesReceived { get { return _bytesReceived; } }
  83. /// <summary>
  84. /// Option: acceptor backlog size
  85. /// </summary>
  86. /// <remarks>
  87. /// This option will set the listening socket's backlog size
  88. /// </remarks>
  89. public int OptionAcceptorBacklog { get; set; } = 1024;
  90. /// <summary>
  91. /// Option: dual mode socket
  92. /// </summary>
  93. /// <remarks>
  94. /// Specifies whether the Socket is a dual-mode socket used for both IPv4 and IPv6.
  95. /// Will work only if socket is bound on IPv6 address.
  96. /// </remarks>
  97. public bool OptionDualMode { get; set; }
  98. /// <summary>
  99. /// Option: keep alive
  100. /// </summary>
  101. /// <remarks>
  102. /// This option will setup SO_KEEPALIVE if the OS support this feature
  103. /// </remarks>
  104. public bool OptionKeepAlive { get; set; }
  105. /// <summary>
  106. /// Option: TCP keep alive time
  107. /// </summary>
  108. /// <remarks>
  109. /// The number of seconds a TCP connection will remain alive/idle before keepalive probes are sent to the remote
  110. /// </remarks>
  111. public int OptionTcpKeepAliveTime { get; set; } = -1;
  112. /// <summary>
  113. /// Option: TCP keep alive interval
  114. /// </summary>
  115. /// <remarks>
  116. /// The number of seconds a TCP connection will wait for a keepalive response before sending another keepalive probe
  117. /// </remarks>
  118. public int OptionTcpKeepAliveInterval { get; set; } = -1;
  119. /// <summary>
  120. /// Option: TCP keep alive retry count
  121. /// </summary>
  122. /// <remarks>
  123. /// The number of TCP keep alive probes that will be sent before the connection is terminated
  124. /// </remarks>
  125. public int OptionTcpKeepAliveRetryCount { get; set; } = -1;
  126. /// <summary>
  127. /// Option: no delay
  128. /// </summary>
  129. /// <remarks>
  130. /// This option will enable/disable Nagle's algorithm for TCP protocol
  131. /// </remarks>
  132. public bool OptionNoDelay { get; set; }
  133. /// <summary>
  134. /// Option: reuse address
  135. /// </summary>
  136. /// <remarks>
  137. /// This option will enable/disable SO_REUSEADDR if the OS support this feature
  138. /// </remarks>
  139. public bool OptionReuseAddress { get; set; }
  140. /// <summary>
  141. /// Option: enables a socket to be bound for exclusive access
  142. /// </summary>
  143. /// <remarks>
  144. /// This option will enable/disable SO_EXCLUSIVEADDRUSE if the OS support this feature
  145. /// </remarks>
  146. public bool OptionExclusiveAddressUse { get; set; }
  147. /// <summary>
  148. /// Option: receive buffer size
  149. /// </summary>
  150. public int OptionReceiveBufferSize { get; set; } = 8192;
  151. /// <summary>
  152. /// Option: send buffer size
  153. /// </summary>
  154. public int OptionSendBufferSize { get; set; } = 8192;
  155. #region Start/Stop server
  156. // Server acceptor
  157. private Socket _acceptorSocket;
  158. private SocketAsyncEventArgs _acceptorEventArg;
  159. // Server statistic
  160. internal long _bytesPending;
  161. internal long _bytesSent;
  162. internal long _bytesReceived;
  163. /// <summary>
  164. /// Is the server started?
  165. /// </summary>
  166. public bool IsStarted { get; private set; }
  167. /// <summary>
  168. /// Is the server accepting new clients?
  169. /// </summary>
  170. public bool IsAccepting { get; private set; }
  171. /// <summary>
  172. /// Create a new socket object
  173. /// </summary>
  174. /// <remarks>
  175. /// Method may be override if you need to prepare some specific socket object in your implementation.
  176. /// </remarks>
  177. /// <returns>Socket object</returns>
  178. protected virtual Socket CreateSocket()
  179. {
  180. return new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
  181. }
  182. /// <summary>
  183. /// Start the server
  184. /// </summary>
  185. /// <returns>'true' if the server was successfully started, 'false' if the server failed to start</returns>
  186. public virtual bool Start()
  187. {
  188. Debug.Assert(!IsStarted, "TCP server is already started!");
  189. if (IsStarted)
  190. return false;
  191. // Setup acceptor event arg
  192. _acceptorEventArg = new SocketAsyncEventArgs();
  193. _acceptorEventArg.Completed += OnAsyncCompleted;
  194. // Create a new acceptor socket
  195. _acceptorSocket = CreateSocket();
  196. // Update the acceptor socket disposed flag
  197. IsSocketDisposed = false;
  198. // Apply the option: reuse address
  199. _acceptorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, OptionReuseAddress);
  200. // Apply the option: exclusive address use
  201. _acceptorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, OptionExclusiveAddressUse);
  202. // Apply the option: dual mode (this option must be applied before listening)
  203. if (_acceptorSocket.AddressFamily == AddressFamily.InterNetworkV6)
  204. _acceptorSocket.DualMode = OptionDualMode;
  205. // Bind the acceptor socket to the endpoint
  206. _acceptorSocket.Bind(Endpoint);
  207. // Refresh the endpoint property based on the actual endpoint created
  208. Endpoint = _acceptorSocket.LocalEndPoint;
  209. // Call the server starting handler
  210. OnStarting();
  211. // Start listen to the acceptor socket with the given accepting backlog size
  212. _acceptorSocket.Listen(OptionAcceptorBacklog);
  213. // Reset statistic
  214. _bytesPending = 0;
  215. _bytesSent = 0;
  216. _bytesReceived = 0;
  217. // Update the started flag
  218. IsStarted = true;
  219. // Call the server started handler
  220. OnStarted();
  221. // Perform the first server accept
  222. IsAccepting = true;
  223. StartAccept(_acceptorEventArg);
  224. return true;
  225. }
  226. /// <summary>
  227. /// Stop the server
  228. /// </summary>
  229. /// <returns>'true' if the server was successfully stopped, 'false' if the server is already stopped</returns>
  230. public virtual bool Stop()
  231. {
  232. Debug.Assert(IsStarted, "TCP server is not started!");
  233. if (!IsStarted)
  234. return false;
  235. // Stop accepting new clients
  236. IsAccepting = false;
  237. // Reset acceptor event arg
  238. _acceptorEventArg.Completed -= OnAsyncCompleted;
  239. // Call the server stopping handler
  240. OnStopping();
  241. try
  242. {
  243. // Close the acceptor socket
  244. _acceptorSocket.Close();
  245. // Dispose the acceptor socket
  246. _acceptorSocket.Dispose();
  247. // Dispose event arguments
  248. _acceptorEventArg.Dispose();
  249. // Update the acceptor socket disposed flag
  250. IsSocketDisposed = true;
  251. }
  252. catch (ObjectDisposedException) {}
  253. // Disconnect all sessions
  254. DisconnectAll();
  255. // Update the started flag
  256. IsStarted = false;
  257. // Call the server stopped handler
  258. OnStopped();
  259. return true;
  260. }
  261. /// <summary>
  262. /// Restart the server
  263. /// </summary>
  264. /// <returns>'true' if the server was successfully restarted, 'false' if the server failed to restart</returns>
  265. public virtual bool Restart()
  266. {
  267. if (!Stop())
  268. return false;
  269. while (IsStarted)
  270. Thread.Yield();
  271. return Start();
  272. }
  273. #endregion
  274. #region Accepting clients
  275. /// <summary>
  276. /// Start accept a new client connection
  277. /// </summary>
  278. private void StartAccept(SocketAsyncEventArgs e)
  279. {
  280. // Socket must be cleared since the context object is being reused
  281. e.AcceptSocket = null;
  282. // Async accept a new client connection
  283. if (!_acceptorSocket.AcceptAsync(e))
  284. ProcessAccept(e);
  285. }
  286. /// <summary>
  287. /// Process accepted client connection
  288. /// </summary>
  289. private void ProcessAccept(SocketAsyncEventArgs e)
  290. {
  291. if (e.SocketError == SocketError.Success)
  292. {
  293. // Create a new session to register
  294. var session = CreateSession();
  295. // Register the session
  296. RegisterSession(session);
  297. // Connect new session
  298. session.Connect(e.AcceptSocket);
  299. }
  300. else
  301. SendError(e.SocketError);
  302. // Accept the next client connection
  303. if (IsAccepting)
  304. StartAccept(e);
  305. }
  306. /// <summary>
  307. /// This method is the callback method associated with Socket.AcceptAsync()
  308. /// operations and is invoked when an accept operation is complete
  309. /// </summary>
  310. private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e)
  311. {
  312. if (IsSocketDisposed)
  313. return;
  314. ProcessAccept(e);
  315. }
  316. #endregion
  317. #region Session factory
  318. /// <summary>
  319. /// Create TCP session factory method
  320. /// </summary>
  321. /// <returns>TCP session</returns>
  322. protected virtual TcpSession CreateSession() { return new TcpSession(this); }
  323. #endregion
  324. #region Session management
  325. /// <summary>
  326. /// Server sessions
  327. /// </summary>
  328. protected readonly ConcurrentDictionary<Guid, TcpSession> Sessions = new ConcurrentDictionary<Guid, TcpSession>();
  329. /// <summary>
  330. /// Disconnect all connected sessions
  331. /// </summary>
  332. /// <returns>'true' if all sessions were successfully disconnected, 'false' if the server is not started</returns>
  333. public virtual bool DisconnectAll()
  334. {
  335. if (!IsStarted)
  336. return false;
  337. // Disconnect all sessions
  338. foreach (var session in Sessions.Values)
  339. session.Disconnect();
  340. return true;
  341. }
  342. /// <summary>
  343. /// Find a session with a given Id
  344. /// </summary>
  345. /// <param name="id">Session Id</param>
  346. /// <returns>Session with a given Id or null if the session it not connected</returns>
  347. public TcpSession FindSession(Guid id)
  348. {
  349. // Try to find the required session
  350. return Sessions.TryGetValue(id, out TcpSession result) ? result : null;
  351. }
  352. /// <summary>
  353. /// Register a new session
  354. /// </summary>
  355. /// <param name="session">Session to register</param>
  356. internal void RegisterSession(TcpSession session)
  357. {
  358. // Register a new session
  359. Sessions.TryAdd(session.Id, session);
  360. }
  361. /// <summary>
  362. /// Unregister session by Id
  363. /// </summary>
  364. /// <param name="id">Session Id</param>
  365. internal void UnregisterSession(Guid id)
  366. {
  367. // Unregister session by Id
  368. Sessions.TryRemove(id, out TcpSession _);
  369. }
  370. #endregion
  371. #region Multicasting
  372. /// <summary>
  373. /// Multicast data to all connected sessions
  374. /// </summary>
  375. /// <param name="buffer">Buffer to multicast</param>
  376. /// <returns>'true' if the data was successfully multicasted, 'false' if the data was not multicasted</returns>
  377. public virtual bool Multicast(byte[] buffer) => Multicast(buffer.AsSpan());
  378. /// <summary>
  379. /// Multicast data to all connected clients
  380. /// </summary>
  381. /// <param name="buffer">Buffer to multicast</param>
  382. /// <param name="offset">Buffer offset</param>
  383. /// <param name="size">Buffer size</param>
  384. /// <returns>'true' if the data was successfully multicasted, 'false' if the data was not multicasted</returns>
  385. public virtual bool Multicast(byte[] buffer, long offset, long size) => Multicast(buffer.AsSpan((int)offset, (int)size));
  386. /// <summary>
  387. /// Multicast data to all connected clients
  388. /// </summary>
  389. /// <param name="buffer">Buffer to send as a span of bytes</param>
  390. /// <returns>'true' if the data was successfully multicasted, 'false' if the data was not multicasted</returns>
  391. public virtual bool Multicast(ReadOnlySpan<byte> buffer)
  392. {
  393. if (!IsStarted)
  394. return false;
  395. if (buffer.IsEmpty)
  396. return true;
  397. // Multicast data to all sessions
  398. foreach (var session in Sessions.Values)
  399. session.SendAsync(buffer);
  400. return true;
  401. }
  402. /// <summary>
  403. /// Multicast text to all connected clients
  404. /// </summary>
  405. /// <param name="text">Text string to multicast</param>
  406. /// <returns>'true' if the text was successfully multicasted, 'false' if the text was not multicasted</returns>
  407. public virtual bool Multicast(string text) => Multicast(Encoding.UTF8.GetBytes(text));
  408. /// <summary>
  409. /// Multicast text to all connected clients
  410. /// </summary>
  411. /// <param name="text">Text to multicast as a span of characters</param>
  412. /// <returns>'true' if the text was successfully multicasted, 'false' if the text was not multicasted</returns>
  413. public virtual bool Multicast(ReadOnlySpan<char> text) => Multicast(Encoding.UTF8.GetBytes(text.ToArray()));
  414. #endregion
  415. #region Server handlers
  416. /// <summary>
  417. /// Handle server starting notification
  418. /// </summary>
  419. protected virtual void OnStarting() {}
  420. /// <summary>
  421. /// Handle server started notification
  422. /// </summary>
  423. protected virtual void OnStarted() {}
  424. /// <summary>
  425. /// Handle server stopping notification
  426. /// </summary>
  427. protected virtual void OnStopping() {}
  428. /// <summary>
  429. /// Handle server stopped notification
  430. /// </summary>
  431. protected virtual void OnStopped() {}
  432. /// <summary>
  433. /// Handle session connecting notification
  434. /// </summary>
  435. /// <param name="session">Connecting session</param>
  436. protected virtual void OnConnecting(TcpSession session) {}
  437. /// <summary>
  438. /// Handle session connected notification
  439. /// </summary>
  440. /// <param name="session">Connected session</param>
  441. protected virtual void OnConnected(TcpSession session) {}
  442. /// <summary>
  443. /// Handle session disconnecting notification
  444. /// </summary>
  445. /// <param name="session">Disconnecting session</param>
  446. protected virtual void OnDisconnecting(TcpSession session) {}
  447. /// <summary>
  448. /// Handle session disconnected notification
  449. /// </summary>
  450. /// <param name="session">Disconnected session</param>
  451. protected virtual void OnDisconnected(TcpSession session) {}
  452. /// <summary>
  453. /// Handle error notification
  454. /// </summary>
  455. /// <param name="error">Socket error code</param>
  456. protected virtual void OnError(SocketError error) {}
  457. internal void OnConnectingInternal(TcpSession session) { OnConnecting(session); }
  458. internal void OnConnectedInternal(TcpSession session) { OnConnected(session); }
  459. internal void OnDisconnectingInternal(TcpSession session) { OnDisconnecting(session); }
  460. internal void OnDisconnectedInternal(TcpSession session) { OnDisconnected(session); }
  461. #endregion
  462. #region Error handling
  463. /// <summary>
  464. /// Send error notification
  465. /// </summary>
  466. /// <param name="error">Socket error code</param>
  467. private void SendError(SocketError error)
  468. {
  469. // Skip disconnect errors
  470. if ((error == SocketError.ConnectionAborted) ||
  471. (error == SocketError.ConnectionRefused) ||
  472. (error == SocketError.ConnectionReset) ||
  473. (error == SocketError.OperationAborted) ||
  474. (error == SocketError.Shutdown))
  475. return;
  476. OnError(error);
  477. }
  478. #endregion
  479. #region IDisposable implementation
  480. /// <summary>
  481. /// Disposed flag
  482. /// </summary>
  483. public bool IsDisposed { get; private set; }
  484. /// <summary>
  485. /// Acceptor socket disposed flag
  486. /// </summary>
  487. public bool IsSocketDisposed { get; private set; } = true;
  488. // Implement IDisposable.
  489. public void Dispose()
  490. {
  491. Dispose(true);
  492. GC.SuppressFinalize(this);
  493. }
  494. protected virtual void Dispose(bool disposingManagedResources)
  495. {
  496. // The idea here is that Dispose(Boolean) knows whether it is
  497. // being called to do explicit cleanup (the Boolean is true)
  498. // versus being called due to a garbage collection (the Boolean
  499. // is false). This distinction is useful because, when being
  500. // disposed explicitly, the Dispose(Boolean) method can safely
  501. // execute code using reference type fields that refer to other
  502. // objects knowing for sure that these other objects have not been
  503. // finalized or disposed of yet. When the Boolean is false,
  504. // the Dispose(Boolean) method should not execute code that
  505. // refer to reference type fields because those objects may
  506. // have already been finalized."
  507. if (!IsDisposed)
  508. {
  509. if (disposingManagedResources)
  510. {
  511. // Dispose managed resources here...
  512. Stop();
  513. }
  514. // Dispose unmanaged resources here...
  515. // Set large fields to null here...
  516. // Mark as disposed.
  517. IsDisposed = true;
  518. }
  519. }
  520. #endregion
  521. }
  522. }