Dreamine.Communication.Sockets 1.0.2
Dreamine.Communication.Sockets 통신 기능과 관련 API를 제공합니다.
로딩중...
검색중...
일치하는것 없음
TcpServerTransport.cs
이 파일의 문서화 페이지로 가기
1using System.Collections.Concurrent;
2using System.IO;
3using System.Net;
4using System.Net.Sockets;
5using Dreamine.Communication.Abstractions.Enums;
6using Dreamine.Communication.Abstractions.Interfaces;
7using Dreamine.Communication.Abstractions.Models;
8using Dreamine.Communication.Core.Framing;
9using Dreamine.Communication.Core.Protocols;
12
14
31public sealed class TcpServerTransport : IMessageTransport, IServerTransportMonitor
32{
50 private readonly IMessageProtocolAdapter _protocolAdapter;
59 private readonly IMessageFrameCodec _frameCodec;
68 private readonly ConcurrentDictionary<Guid, TcpClientConnectionEntry> _clients = new();
69
78 private TcpListener? _listener;
87 private CancellationTokenSource? _serverCts;
96 private Task? _acceptLoopTask;
105 private int _state = (int)ConnectionState.Disconnected;
106
124 : this(
125 options,
126 new DreamineEnvelopeProtocolAdapter(),
127 new LengthPrefixedMessageFrameCodec())
128 {
129 }
130
173 IMessageProtocolAdapter protocolAdapter,
174 IMessageFrameCodec frameCodec)
175 {
176 _options = options ?? throw new ArgumentNullException(nameof(options));
177 _protocolAdapter = protocolAdapter ?? throw new ArgumentNullException(nameof(protocolAdapter));
178 _frameCodec = frameCodec ?? throw new ArgumentNullException(nameof(frameCodec));
179
181 }
182
191 public ConnectionState State => (ConnectionState)Volatile.Read(ref _state);
192
201 public TransportKind Kind => TransportKind.Tcp;
202
211 public int ConnectedClientCount => _clients.Count;
212
222 {
223 get => _options.SendTargetMode;
224 set => _options.SendTargetMode = value;
225 }
226
235 public event EventHandler<MessageEnvelope>? MessageReceived;
236
245 public event EventHandler<int>? ConnectedClientCountChanged;
246
271 public Task ConnectAsync(CancellationToken cancellationToken = default)
272 {
273 if (State is ConnectionState.Listening or ConnectionState.Connecting)
274 {
275 return Task.CompletedTask;
276 }
277
278 cancellationToken.ThrowIfCancellationRequested();
279
280 SetState(ConnectionState.Connecting);
281
282 try
283 {
286
287 var ipAddress = ParseHost(_options.Host);
288 _listener = new TcpListener(ipAddress, _options.Port);
289 _listener.Start(_options.Backlog);
290
291 _serverCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
292
293 SetState(ConnectionState.Listening);
294
295 _acceptLoopTask = Task.Run(
296 () => AcceptLoopAsync(_serverCts.Token),
297 _serverCts.Token);
298
299 return Task.CompletedTask;
300 }
301 catch
302 {
303 SetState(ConnectionState.Faulted);
306
307 _serverCts?.Dispose();
308 _serverCts = null;
309
310 throw;
311 }
312 }
313
338 public async Task DisconnectAsync(CancellationToken cancellationToken = default)
339 {
340 if (State == ConnectionState.Disconnected)
341 {
342 return;
343 }
344
345 SetState(ConnectionState.Disconnecting);
346
347 _serverCts?.Cancel();
348
351
352 if (_acceptLoopTask is not null)
353 {
354 try
355 {
356 await _acceptLoopTask.ConfigureAwait(false);
357 }
358 catch (OperationCanceledException)
359 {
360 }
361 catch (ObjectDisposedException)
362 {
363 }
364 catch (SocketException)
365 {
366 }
367 }
368
369 _serverCts?.Dispose();
370 _serverCts = null;
371 _acceptLoopTask = null;
372
373 cancellationToken.ThrowIfCancellationRequested();
374
375 SetState(ConnectionState.Disconnected);
376 }
377
410 public Task SendAsync(
411 MessageEnvelope message,
412 CancellationToken cancellationToken = default)
413 {
414 return SendAsync(SendTargetMode, message, cancellationToken);
415 }
416
473 public async Task SendAsync(
474 TcpServerSendTargetMode targetMode,
475 MessageEnvelope message,
476 CancellationToken cancellationToken = default)
477 {
478 ArgumentNullException.ThrowIfNull(message);
479
480 if (State != ConnectionState.Listening)
481 {
482 throw new InvalidOperationException("TCP server is not listening.");
483 }
484
485 var payload = _protocolAdapter.Encode(message);
486 var targets = GetTargetClients(targetMode);
487
488 if (targets.Length == 0)
489 {
490 throw new InvalidOperationException("No TCP client is connected to the server.");
491 }
492
493 var sendTasks = targets
494 .Select(target => SendToClientAsync(target, payload, cancellationToken))
495 .ToArray();
496
497 await Task.WhenAll(sendTasks).ConfigureAwait(false);
498 }
499
532 public Task BroadcastAsync(
533 MessageEnvelope message,
534 CancellationToken cancellationToken = default)
535 {
536 return SendAsync(TcpServerSendTargetMode.Broadcast, message, cancellationToken);
537 }
538
539
565 {
566 var entries = _clients.Values
567 .OrderBy(x => x.ConnectedAt)
568 .ToArray();
569
570 return targetMode switch
571 {
573 TcpServerSendTargetMode.FirstClient => entries.Take(1).ToArray(),
574 TcpServerSendTargetMode.LastClient => entries.Reverse().Take(1).ToArray(),
575 _ => entries
576 };
577 }
578
595 public async ValueTask DisposeAsync()
596 {
597 await DisconnectAsync().ConfigureAwait(false);
598 }
599
640 private async Task SendToClientAsync(
642 byte[] payload,
643 CancellationToken cancellationToken)
644 {
645 cancellationToken.ThrowIfCancellationRequested();
646
647 if (!target.Client.Connected)
648 {
649 RemoveClient(target.ClientId, target.Client);
650 return;
651 }
652
653 await target.SendLock.WaitAsync(cancellationToken).ConfigureAwait(false);
654
655 try
656 {
657 if (!target.Client.Connected)
658 {
659 RemoveClient(target.ClientId, target.Client);
660 return;
661 }
662
663 var stream = target.Client.GetStream();
664
665 await _frameCodec.WriteFrameAsync(stream, payload, cancellationToken)
666 .ConfigureAwait(false);
667 }
668 catch
669 {
670 RemoveClient(target.ClientId, target.Client);
671 }
672 finally
673 {
674 target.SendLock.Release();
675 }
676 }
677
702 private async Task AcceptLoopAsync(CancellationToken cancellationToken)
703 {
704 if (_listener is null)
705 {
706 return;
707 }
708
709 try
710 {
711 while (!cancellationToken.IsCancellationRequested &&
712 State == ConnectionState.Listening)
713 {
714 var client = await _listener.AcceptTcpClientAsync(cancellationToken)
715 .ConfigureAwait(false);
716
717 client.ReceiveBufferSize = _options.ReceiveBufferSize;
718 client.SendBufferSize = _options.SendBufferSize;
719
720 var clientId = Guid.NewGuid();
721 var entry = new TcpClientConnectionEntry(
722 clientId,
723 client,
724 DateTimeOffset.UtcNow);
725
726 _clients[clientId] = entry;
728
729 _ = Task.Run(
730 () => ReceiveLoopAsync(clientId, client, cancellationToken),
731 cancellationToken);
732 }
733 }
734 catch (OperationCanceledException)
735 {
736 }
737 catch (ObjectDisposedException)
738 {
739 }
740 catch (SocketException)
741 {
742 if (!cancellationToken.IsCancellationRequested)
743 {
744 SetState(ConnectionState.Faulted);
747 }
748 }
749 catch (IOException)
750 {
751 if (!cancellationToken.IsCancellationRequested)
752 {
753 SetState(ConnectionState.Faulted);
756 }
757 }
758 catch
759 {
760 if (!cancellationToken.IsCancellationRequested)
761 {
762 SetState(ConnectionState.Faulted);
765 }
766 }
767 }
768
809 private async Task ReceiveLoopAsync(
810 Guid clientId,
811 TcpClient client,
812 CancellationToken cancellationToken)
813 {
814 try
815 {
816 var stream = client.GetStream();
817
818 while (!cancellationToken.IsCancellationRequested &&
819 State == ConnectionState.Listening &&
820 client.Connected)
821 {
822 var payload = await _frameCodec.ReadFrameAsync(stream, cancellationToken)
823 .ConfigureAwait(false);
824
825 if (payload is null)
826 {
827 break;
828 }
829
830 var message = _protocolAdapter.Decode(payload);
831 MessageReceived?.Invoke(this, message);
832 }
833 }
834 catch (OperationCanceledException)
835 {
836 }
837 catch (ObjectDisposedException)
838 {
839 }
840 catch (SocketException)
841 {
842 }
843 catch
844 {
845 // 수신 루프 단위 예외는 해당 클라이언트 제거로 처리합니다.
846 }
847 finally
848 {
849 RemoveClient(clientId, client);
850 }
851 }
852
861 private void CleanupListener()
862 {
863 try
864 {
865 _listener?.Stop();
866 }
867 catch
868 {
869 // Ignore cleanup exceptions.
870 }
871
872 _listener = null;
873 }
874
883 private void CleanupClients()
884 {
885 foreach (var pair in _clients.ToArray())
886 {
887 RemoveClient(pair.Key, pair.Value.Client);
888 }
889 }
890
915 private void RemoveClient(Guid clientId, TcpClient client)
916 {
917 var removed = _clients.TryRemove(clientId, out _);
918
919 try
920 {
921 client.Close();
922 client.Dispose();
923 }
924 catch
925 {
926 // Ignore cleanup exceptions.
927 }
928
929 if (removed)
930 {
932 }
933 }
934
951 private void SetState(ConnectionState state)
952 {
953 Interlocked.Exchange(ref _state, (int)state);
954 }
955
968
1001 private static IPAddress ParseHost(string host)
1002 {
1003 if (host == "0.0.0.0")
1004 {
1005 return IPAddress.Any;
1006 }
1007
1008 if (host == "127.0.0.1" ||
1009 host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
1010 {
1011 return IPAddress.Loopback;
1012 }
1013
1014 if (IPAddress.TryParse(host, out var ipAddress))
1015 {
1016 return ipAddress;
1017 }
1018
1019 throw new ArgumentException($"Invalid TCP server host: {host}", nameof(host));
1020 }
1021
1054 private static void ValidateOptions(TcpServerTransportOptions options)
1055 {
1056 ArgumentException.ThrowIfNullOrWhiteSpace(options.Host);
1057
1058 if (options.Port <= 0 || options.Port > 65535)
1059 {
1060 throw new ArgumentOutOfRangeException(nameof(options.Port));
1061 }
1062
1063 if (options.Backlog <= 0)
1064 {
1065 throw new ArgumentOutOfRangeException(nameof(options.Backlog));
1066 }
1067
1068 if (options.ReceiveBufferSize <= 0)
1069 {
1070 throw new ArgumentOutOfRangeException(nameof(options.ReceiveBufferSize));
1071 }
1072
1073 if (options.SendBufferSize <= 0)
1074 {
1075 throw new ArgumentOutOfRangeException(nameof(options.SendBufferSize));
1076 }
1077 }
1078
1087 private sealed class TcpClientConnectionEntry
1088 {
1130 Guid clientId,
1131 TcpClient client,
1132 DateTimeOffset connectedAt)
1133 {
1134 ClientId = clientId;
1135 Client = client ?? throw new ArgumentNullException(nameof(client));
1136 ConnectedAt = connectedAt;
1137 }
1138
1147 public Guid ClientId { get; }
1148
1157 public TcpClient Client { get; }
1158
1167 public DateTimeOffset ConnectedAt { get; }
1168
1177 public SemaphoreSlim SendLock { get; } = new(1, 1);
1178 }
1179
1180}
TcpClientConnectionEntry[] GetTargetClients(TcpServerSendTargetMode targetMode)
Task ConnectAsync(CancellationToken cancellationToken=default)
Task SendAsync(MessageEnvelope message, CancellationToken cancellationToken=default)
async Task SendAsync(TcpServerSendTargetMode targetMode, MessageEnvelope message, CancellationToken cancellationToken=default)
static void ValidateOptions(TcpServerTransportOptions options)
async Task SendToClientAsync(TcpClientConnectionEntry target, byte[] payload, CancellationToken cancellationToken)
async Task AcceptLoopAsync(CancellationToken cancellationToken)
async Task ReceiveLoopAsync(Guid clientId, TcpClient client, CancellationToken cancellationToken)
readonly ConcurrentDictionary< Guid, TcpClientConnectionEntry > _clients
async Task DisconnectAsync(CancellationToken cancellationToken=default)
TcpServerTransport(TcpServerTransportOptions options, IMessageProtocolAdapter protocolAdapter, IMessageFrameCodec frameCodec)
Task BroadcastAsync(MessageEnvelope message, CancellationToken cancellationToken=default)
TcpClientConnectionEntry(Guid clientId, TcpClient client, DateTimeOffset connectedAt)