├── EasyTcp4Net.UnitTest ├── GlobalUsings.cs ├── EasyTcp4Net.UnitTest.csproj └── EasyTcpClientTest.cs ├── Images └── image.png ├── EasyTcp4Net.WpfTest ├── test.pfx ├── App.xaml ├── App.xaml.cs ├── MainWindow.xaml.cs ├── AssemblyInfo.cs ├── EasyTcp4Net.WpfTest.csproj ├── MainWindow.xaml ├── MainWindowViewModel.cs └── MainWindowViewModel_fixheader.cs ├── EasyTcp4Net.ClientTest ├── test.pfx ├── EasyTcp4Net.ClientTest.csproj └── Program.cs ├── EasyTcp4Net.ServerTest ├── test.pfx ├── EasyTcp4Net.ServerTest.csproj └── Program.cs ├── EasyTcp4Net ├── IPackageFilter.cs ├── ClientDataReceiveEventArgs.cs ├── AbstractPackageFilter.cs ├── ServerDataReceiveEventArgs.cs ├── ClientSideDisConnectEventArgs.cs ├── EasyTcp4Net.csproj ├── ServerSideClientConnectionChangeEventArgs.cs ├── FixedLengthPackageFilter.cs ├── FixedCharPackageFilter.cs ├── SocketExtension.cs ├── FixedHeaderPackageFilter.cs ├── EasyTcpServerOptions.cs ├── EasyTcpClientOptions.cs ├── ClientSession.cs ├── EasyTcpServer.cs └── EasyTcpClient.cs ├── .gitignore ├── EasyTcp4Net.sln └── README.md /EasyTcp4Net.UnitTest/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; -------------------------------------------------------------------------------- /Images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BruceQiu1996/EasyTcp4Net/HEAD/Images/image.png -------------------------------------------------------------------------------- /EasyTcp4Net.WpfTest/test.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BruceQiu1996/EasyTcp4Net/HEAD/EasyTcp4Net.WpfTest/test.pfx -------------------------------------------------------------------------------- /EasyTcp4Net.ClientTest/test.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BruceQiu1996/EasyTcp4Net/HEAD/EasyTcp4Net.ClientTest/test.pfx -------------------------------------------------------------------------------- /EasyTcp4Net.ServerTest/test.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BruceQiu1996/EasyTcp4Net/HEAD/EasyTcp4Net.ServerTest/test.pfx -------------------------------------------------------------------------------- /EasyTcp4Net/IPackageFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace EasyTcp4Net 4 | { 5 | public interface IPackageFilter 6 | { 7 | ReadOnlySequence ResolvePackage(ref ReadOnlySequence sequence); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /EasyTcp4Net/ClientDataReceiveEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace EasyTcp4Net 2 | { 3 | public class ClientDataReceiveEventArgs 4 | { 5 | public ClientDataReceiveEventArgs(Memory packet) 6 | { 7 | Data = packet; 8 | } 9 | 10 | public Memory Data { get; private set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EasyTcp4Net/AbstractPackageFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace EasyTcp4Net 4 | { 5 | public abstract class AbstractPackageFilter : IPackageFilter 6 | { 7 | public AbstractPackageFilter() { } 8 | 9 | public abstract ReadOnlySequence ResolvePackage(ref ReadOnlySequence sequence); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /EasyTcp4Net.WpfTest/App.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /EasyTcp4Net/ServerDataReceiveEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace EasyTcp4Net 2 | { 3 | public class ServerDataReceiveEventArgs 4 | { 5 | public ClientSession Session { get; private set; } 6 | public Memory Data { get; set; } 7 | public ServerDataReceiveEventArgs(ClientSession clientSession, Memory packet) 8 | { 9 | Session = clientSession; 10 | Data = packet; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /EasyTcp4Net.WpfTest/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace EasyTcp4Net.WpfTest 4 | { 5 | /// 6 | /// Interaction logic for App.xaml 7 | /// 8 | public partial class App : Application 9 | { 10 | public App() 11 | { 12 | Startup += (e,a) => 13 | { 14 | var mainWindow = new MainWindow(); 15 | mainWindow.Show(); 16 | }; 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /EasyTcp4Net.WpfTest/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace EasyTcp4Net.WpfTest 4 | { 5 | /// 6 | /// Interaction logic for MainWindow.xaml 7 | /// 8 | public partial class MainWindow : Window 9 | { 10 | public MainWindow() 11 | { 12 | InitializeComponent(); 13 | DataContext = new MainWindowViewModelFixHeader(); 14 | //DataContext = new MainWindowViewModel(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /EasyTcp4Net.ClientTest/EasyTcp4Net.ClientTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /EasyTcp4Net.ServerTest/EasyTcp4Net.ServerTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /EasyTcp4Net.WpfTest/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /EasyTcp4Net/ClientSideDisConnectEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace EasyTcp4Net 4 | { 5 | /// 6 | /// 客户端感知断线事件 7 | /// 8 | public class ClientSideDisConnectEventArgs 9 | { 10 | public DisConnectReason Reason { get; private set; } 11 | public ClientSideDisConnectEventArgs(DisConnectReason disConnectReason) 12 | { 13 | Reason = disConnectReason; 14 | } 15 | } 16 | 17 | public enum DisConnectReason 18 | { 19 | [Description("主动断开")] 20 | Normol, 21 | [Description("服务端断开")] 22 | ServerDown, 23 | [Description("未知")] 24 | UnKnown 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | bin 3 | obj 4 | txtlogs 5 | *.user 6 | *.pubxml 7 | node_modules 8 | /src/ClientApp/package-lock.json 9 | /README.html 10 | /src/ClientApp/dist 11 | /src/ServerApi/.sonarqube 12 | /src/ServerApi/.sonarlint 13 | /src/ServerApi/Services/Adnc.Usr/Adnc.Usr.Migrations/Migrations 14 | /src/ServerApi/Services/Adnc.Maint/Adnc.Maint.Migrations/Migrations 15 | /src/ServerApi/Services/Adnc.Cus/Adnc.Cus.Migrations/Migrations 16 | /src/ServerApi/Services/Adnc.Ord/Adnc.Ord.Migrations/Migrations 17 | /src/ServerApi/Services/Adnc.Whse/Adnc.Whse.Migrations/Migrations 18 | /src/ClientApp/target/npmlist.json 19 | /src/ServerApi/.idea/.idea.Adnc/.idea/.gitignore 20 | /src/ServerApi/.idea/.idea.Adnc/.idea/.name 21 | /src/ServerApi/.idea/.idea.Adnc/.idea/encodings.xml 22 | /src/ServerApi/.idea/.idea.Adnc/.idea/indexLayout.xml 23 | /src/ServerApi/.idea/.idea.Adnc/.idea/vcs.xml 24 | -------------------------------------------------------------------------------- /EasyTcp4Net.UnitTest/EasyTcp4Net.UnitTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /EasyTcp4Net/EasyTcp4Net.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0;net8.0 5 | enable 6 | enable 7 | True 8 | EasyTcp4Net 9 | Bruce Qiu 10 | Bruce Qiu 11 | 1.0.0.1 12 | https://github.com/BruceQiu1996/EasyTcp4Net 13 | A high-performance TCP communication package developed by .NET that supports multiple unpacket strategies 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /EasyTcp4Net/ServerSideClientConnectionChangeEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace EasyTcp4Net 4 | { 5 | /// 6 | /// 服务端某个连接断开的事件 7 | /// 8 | public class ServerSideClientConnectionChangeEventArgs 9 | { 10 | public ClientSession ClientSession { get; set; } 11 | public ConnectsionStatus Status { get; private set; } 12 | /// 13 | /// 事件构造器 14 | /// 15 | /// 客户端连接对象 16 | /// 连接状态 17 | public ServerSideClientConnectionChangeEventArgs(ClientSession clientSession, ConnectsionStatus connectsionStatus) 18 | { 19 | ClientSession = clientSession; 20 | Status = connectsionStatus; 21 | } 22 | } 23 | 24 | public enum ConnectsionStatus 25 | { 26 | [Description("连接")] 27 | Connected, 28 | [Description("断开")] 29 | DisConnected 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /EasyTcp4Net/FixedLengthPackageFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace EasyTcp4Net 4 | { 5 | /// 6 | /// 固定长度报文解析器 7 | /// 8 | public class FixedLengthPackageFilter : AbstractPackageFilter 9 | { 10 | private readonly int _packetSize; 11 | public FixedLengthPackageFilter(int packetSize) 12 | { 13 | _packetSize = packetSize; 14 | } 15 | 16 | public override ReadOnlySequence ResolvePackage(ref ReadOnlySequence sequence) 17 | { 18 | if (sequence.Length < _packetSize) return default; 19 | byte[] bodyLengthBytes = ArrayPool.Shared.Rent(_packetSize); 20 | try 21 | { 22 | var endPosition = sequence.GetPosition(_packetSize); 23 | var data = sequence.Slice(0, endPosition); 24 | sequence = sequence.Slice(endPosition); 25 | 26 | return data; 27 | } 28 | finally 29 | { 30 | ArrayPool.Shared.Return(bodyLengthBytes); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /EasyTcp4Net.WpfTest/EasyTcp4Net.WpfTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0-windows 6 | enable 7 | enable 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /EasyTcp4Net.ServerTest/Program.cs: -------------------------------------------------------------------------------- 1 | namespace EasyTcp4Net.ServerTest 2 | { 3 | internal class Program 4 | { 5 | static void Main(string[] args) 6 | { 7 | Console.WriteLine("Hello World,this is Server"); 8 | EasyTcpServer easyTcpServer = new EasyTcpServer(7001, new EasyTcpServerOptions() 9 | { 10 | IsSsl = true, 11 | PfxCertFilename = "test.pfx", 12 | PfxPassword = "123456", 13 | IdleSessionsCheck = false, 14 | KeepAlive = true, 15 | CheckSessionsIdleMs = 10 * 1000 16 | }); 17 | //参数分别为:数据包头长度,数据包体长度,数据包体长度字节数,是否小端在前 18 | easyTcpServer 19 | .SetReceiveFilter(new FixedCharPackageFilter('\n')); 20 | easyTcpServer.StartListen(); 21 | 22 | int index = 0; 23 | int bytes = 0; 24 | easyTcpServer.OnReceivedData += async (obj, e) => 25 | { 26 | Console.WriteLine($"数据来自:{e.Session.RemoteEndPoint}"); 27 | Console.WriteLine(string.Join(',', e.Data)); 28 | }; 29 | 30 | Console.ReadLine(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /EasyTcp4Net/FixedCharPackageFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace EasyTcp4Net 4 | { 5 | /// 6 | /// 固定字符解析器,不建议使用,除非保证发送数据不会出现和字符相同的字节数据,否则会出现断包的情况 7 | /// 如果需要在项目中使用根据某个字符串解析数据包,建议自行实现ResolvePackage 8 | /// 9 | public class FixedCharPackageFilter : AbstractPackageFilter 10 | { 11 | private readonly char _splitChar; 12 | public FixedCharPackageFilter(char splitChar) 13 | { 14 | _splitChar = splitChar; 15 | try 16 | { 17 | byte chatByte = (byte)_splitChar; 18 | } 19 | catch (Exception) 20 | { 21 | throw new ArgumentException("The char dose not support.You can customize your own parser."); 22 | } 23 | } 24 | 25 | public override ReadOnlySequence ResolvePackage(ref ReadOnlySequence sequence) 26 | { 27 | var position = sequence.PositionOf((byte)_splitChar); 28 | 29 | if (position == null) 30 | { 31 | return default; 32 | } 33 | else 34 | { 35 | var index = sequence.GetPosition(1, position.Value); 36 | var data = sequence.Slice(0, index); 37 | sequence = sequence.Slice(index); 38 | 39 | return data; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /EasyTcp4Net.UnitTest/EasyTcpClientTest.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | 5 | namespace EasyTcp4Net.UnitTest 6 | { 7 | [TestFixture] 8 | public class EasyTcpClientTest 9 | { 10 | [SetUp] 11 | public void Setup() { } 12 | 13 | [Test] 14 | public void Ctor_WhenServerHostIsNull_ArgumentNullException() 15 | { 16 | string serverHost = null; 17 | Assert.Throws(() => { EasyTcpClient easyTcpClientTest = new EasyTcpClient(serverHost, 1000); }); 18 | } 19 | 20 | [Test] 21 | public void Ctor_WhenPortEqualZero_InvalidDataException() 22 | { 23 | string serverHost = "test"; 24 | ushort port = 0; 25 | Assert.Throws(() => { EasyTcpClient easyTcpClientTest = new EasyTcpClient(serverHost, port); }); 26 | } 27 | 28 | [Test] 29 | public void Ctor_SslTrueButFileIsEmpty_SslObjectNull() 30 | { 31 | string serverHost = "test"; 32 | ushort port = 1000; 33 | EasyTcpClientOptions options = new EasyTcpClientOptions() 34 | { 35 | IsSsl = true, 36 | PfxCertFilename = null 37 | }; 38 | 39 | EasyTcpClient easyTcpClient = new EasyTcpClient(serverHost, port, options); 40 | Assert.IsNull(easyTcpClient.Certificate); 41 | } 42 | 43 | [Test] 44 | public void Connect_Outof3Seconds_Exception() 45 | { 46 | string serverHost = "test"; 47 | ushort port = 1000; 48 | EasyTcpClient easyTcpClient = new EasyTcpClient(serverHost, port); 49 | Assert.ThrowsAsync(easyTcpClient.ConnectAsync); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /EasyTcp4Net.ClientTest/Program.cs: -------------------------------------------------------------------------------- 1 | namespace EasyTcp4Net.ClientTest 2 | { 3 | internal class Program 4 | { 5 | async static Task Main(string[] args) 6 | { 7 | 8 | Console.WriteLine("Hello World,this is Client"); 9 | 10 | EasyTcpClient easyTcpClient = new EasyTcpClient("127.0.0.1", 7001, new EasyTcpClientOptions() 11 | { 12 | IsSsl = true, 13 | PfxCertFilename = "test.pfx", 14 | PfxPassword = "123456", 15 | KeepAlive = true, 16 | }); 17 | await Task.Delay(500); 18 | await easyTcpClient.ConnectAsync(); 19 | var a = BitConverter.GetBytes(65536 * 20); 20 | var head = new byte[] { 2, 3, 3, 0, 20, 0, 0 }; 21 | var data = new byte[5128]; 22 | foreach (int index in Enumerable.Range(0, head.Length)) 23 | { 24 | data[index] = head[index]; 25 | } 26 | 27 | 28 | Random random = new Random(); 29 | int count = 0; 30 | foreach (var index in Enumerable.Range(0, 200)) 31 | { 32 | 33 | foreach (var inindex in Enumerable.Range(0, 5120)) 34 | { 35 | data[inindex + 7] = (byte)random.Next(0, 120); 36 | } 37 | data[5127] = (byte)'\n'; 38 | Memory sendm = new Memory(data); 39 | var split = random.Next(100, 1000); 40 | await easyTcpClient.SendAsync(sendm.Slice(0, split)); 41 | await Task.Delay(10); 42 | await easyTcpClient.SendAsync(sendm.Slice(split)); 43 | 44 | Console.WriteLine(++count); 45 | } 46 | 47 | 48 | 49 | Memory bytes = new Memory(data); 50 | //await easyTcpClient.SendAsync(bytes.Slice(0,200)); 51 | 52 | //await Task.Delay(2000); 53 | // await easyTcpClient.SendAsync(bytes.Slice(200)); 54 | ////foreach (var index in Enumerable.Range(0, 200)) 55 | ////{ 56 | 57 | 58 | 59 | ////} 60 | 61 | easyTcpClient.OnReceivedData += (obj, e) => 62 | { 63 | Console.WriteLine(string.Join(',', e.Data)); 64 | Console.WriteLine("\n"); 65 | }; 66 | 67 | Console.Read(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /EasyTcp4Net/SocketExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Net.NetworkInformation; 2 | using System.Net.Sockets; 3 | 4 | namespace EasyTcp4Net 5 | { 6 | internal static class SocketExtension 7 | { 8 | /// 9 | /// 开启Socket的KeepAlive 10 | /// 设置tcp协议的一些KeepAlive参数 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | internal static void SetKeepAlive(this Socket socket, int tcpKeepAliveInterval, int tcpKeepAliveTime, int tcpKeepAliveRetryCount) 17 | { 18 | socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); 19 | socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, tcpKeepAliveInterval); 20 | socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, tcpKeepAliveTime); 21 | socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, tcpKeepAliveRetryCount); 22 | } 23 | 24 | /// 25 | /// 检查套接字是否连接着 26 | /// 27 | /// 28 | /// 29 | internal static bool CheckConnect(this Socket socket) 30 | { 31 | if (socket == null) return false; 32 | 33 | try 34 | { 35 | var state = IPGlobalProperties.GetIPGlobalProperties() 36 | .GetActiveTcpConnections() 37 | .FirstOrDefault(x => 38 | x.LocalEndPoint.Equals(socket.LocalEndPoint) 39 | && x.RemoteEndPoint.Equals(socket.RemoteEndPoint)); 40 | 41 | if (state == default(TcpConnectionInformation) 42 | || state.State == TcpState.Unknown 43 | || state.State == TcpState.FinWait1 //向服务端发起断开请求,进入fin1 44 | || state.State == TcpState.FinWait2 //收到服务器Ack,等待服务器,进入fin2 45 | || state.State == TcpState.Closed 46 | || state.State == TcpState.Closing 47 | || state.State == TcpState.CloseWait) 48 | { 49 | return false; 50 | } 51 | 52 | return !((socket.Poll(0, SelectMode.SelectRead) && (socket.Available == 0)) || !socket.Connected); 53 | } 54 | catch (SocketException) 55 | { 56 | return false; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /EasyTcp4Net.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34511.84 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyTcp4Net", "EasyTcp4Net\EasyTcp4Net.csproj", "{81F203E4-A808-43D3-9B94-69F6715F48C8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyTcp4Net.ServerTest", "EasyTcp4Net.ServerTest\EasyTcp4Net.ServerTest.csproj", "{EBA4E40C-F6BF-4709-ABD4-B8C5F06173FF}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyTcp4Net.ClientTest", "EasyTcp4Net.ClientTest\EasyTcp4Net.ClientTest.csproj", "{E681D07C-38A3-4B0F-AACE-0BD4C085E90C}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyTcp4Net.WpfTest", "EasyTcp4Net.WpfTest\EasyTcp4Net.WpfTest.csproj", "{3242E44E-8824-49A3-8455-9089F51E773E}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyTcp4Net.UnitTest", "EasyTcp4Net.UnitTest\EasyTcp4Net.UnitTest.csproj", "{CDA6145E-0385-406B-A0F2-EAFECCC9F1CF}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {81F203E4-A808-43D3-9B94-69F6715F48C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {81F203E4-A808-43D3-9B94-69F6715F48C8}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {81F203E4-A808-43D3-9B94-69F6715F48C8}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {81F203E4-A808-43D3-9B94-69F6715F48C8}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {EBA4E40C-F6BF-4709-ABD4-B8C5F06173FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {EBA4E40C-F6BF-4709-ABD4-B8C5F06173FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {EBA4E40C-F6BF-4709-ABD4-B8C5F06173FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {EBA4E40C-F6BF-4709-ABD4-B8C5F06173FF}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {E681D07C-38A3-4B0F-AACE-0BD4C085E90C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {E681D07C-38A3-4B0F-AACE-0BD4C085E90C}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {E681D07C-38A3-4B0F-AACE-0BD4C085E90C}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {E681D07C-38A3-4B0F-AACE-0BD4C085E90C}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {3242E44E-8824-49A3-8455-9089F51E773E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {3242E44E-8824-49A3-8455-9089F51E773E}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {3242E44E-8824-49A3-8455-9089F51E773E}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {3242E44E-8824-49A3-8455-9089F51E773E}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {CDA6145E-0385-406B-A0F2-EAFECCC9F1CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {CDA6145E-0385-406B-A0F2-EAFECCC9F1CF}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {CDA6145E-0385-406B-A0F2-EAFECCC9F1CF}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {CDA6145E-0385-406B-A0F2-EAFECCC9F1CF}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {0208ABC8-681B-4FDD-BB27-5AA43CEA23C6} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /EasyTcp4Net/FixedHeaderPackageFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Buffers.Binary; 3 | 4 | namespace EasyTcp4Net 5 | { 6 | /// 7 | /// 固定数据包头解析器 8 | /// 9 | public class FixedHeaderPackageFilter : AbstractPackageFilter 10 | { 11 | private readonly int _headerSize; 12 | private readonly int _bodyLengthIndex; 13 | private readonly int _bodyLengthBytes; 14 | private readonly bool _isLittleEndian; 15 | 16 | /// 17 | /// 固定报文头解析协议 18 | /// 19 | /// 数据报文头的大小 20 | /// 数据包大小在报文头中的位置 21 | /// 数据包大小在报文头中的长度 22 | /// 数据报文大小端。windows中通常是小端,unix通常是大端模式 23 | public FixedHeaderPackageFilter(int headerSize, int bodyLengthIndex, int bodyLengthBytes, bool IsLittleEndian = true) : base() 24 | { 25 | if (bodyLengthBytes <= 0 || bodyLengthBytes > 8) 26 | throw new ArgumentException("The maximum value of bodyLengthBytes is 8 and must greater then zero"); 27 | 28 | if (headerSize <= 0 || bodyLengthIndex < 0) 29 | throw new ArgumentException("Invalid arguments headerSize and bodyLengthIndex"); 30 | 31 | if (headerSize < bodyLengthIndex + bodyLengthBytes) 32 | throw new ArgumentException("Invalid header arguments"); 33 | 34 | _headerSize = headerSize; 35 | _bodyLengthIndex = bodyLengthIndex; 36 | _bodyLengthBytes = bodyLengthBytes; 37 | _isLittleEndian = IsLittleEndian; 38 | } 39 | 40 | /// 41 | /// 解析数据包 42 | /// 43 | /// 44 | public override ReadOnlySequence ResolvePackage(ref ReadOnlySequence sequence) 45 | { 46 | var len = sequence.Length; 47 | if (len < _bodyLengthIndex) return default; 48 | var bodyLengthSequence = sequence.Slice(_bodyLengthIndex, _bodyLengthBytes); 49 | byte[] bodyLengthBytes = ArrayPool.Shared.Rent(_bodyLengthBytes); 50 | try 51 | { 52 | int index = 0; 53 | foreach (var item in bodyLengthSequence) 54 | { 55 | Array.Copy(item.ToArray(), 0, bodyLengthBytes, index, item.Length); 56 | index += item.Length; 57 | } 58 | 59 | long bodyLength = 0; 60 | int offset = 0; 61 | if (!_isLittleEndian) 62 | { 63 | offset = bodyLengthBytes.Length - 1; 64 | foreach (var bytes in bodyLengthBytes) 65 | { 66 | bodyLength += bytes << (offset * 8); 67 | offset--; 68 | } 69 | } 70 | else 71 | { 72 | 73 | foreach (var bytes in bodyLengthBytes) 74 | { 75 | bodyLength += bytes << (offset * 8); 76 | offset++; 77 | } 78 | } 79 | 80 | if (sequence.Length < _headerSize + bodyLength) 81 | return default; 82 | 83 | var endPosition = sequence.GetPosition(_headerSize + bodyLength); 84 | var data = sequence.Slice(0, endPosition); 85 | sequence = sequence.Slice(endPosition); 86 | 87 | return data; 88 | } 89 | finally 90 | { 91 | ArrayPool.Shared.Return(bodyLengthBytes); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | >已发布Nuget:https://www.nuget.org/packages/EasyTcp4Net/ 2 | 或包管理器搜索**EasyTcp4Net** 3 | 4 | > 这是一个基于c# Pipe,ReadonlySequence的高性能Tcp通信库,旨在提供稳定,高效,可靠的tcp通讯服务。 5 | 6 | - [x] 基础的消息通讯 7 | - [x] 重试机制 8 | - [x] 超时机制 9 | - [x] SSL加密通信支持 10 | - [x] KeepAlive 11 | - [x] 流量背压控制 12 | - [x] 粘包和断包处理 (支持固定头处理,固定长度处理,固定字符处理) 13 | - [x] 日志支持 14 | 15 | ### 示例程序 16 | 一个示例的聊天程序,功能包括,文本发送,图片发送,断线重连,消息发送成功确认,消息发送失败提示等。 17 | 地址:https://github.com/BruceQiu1996/EasyChat 18 | 19 | ### Pipe & ReadOnlySequence 20 | ![alt text](./Images/image.png) 21 | 22 | #### 为什么选择 Pipe & ReadOnlySequence 23 | **TCP** 是一个流式面向连接的传输协议,所以源源不断地处理数据,并且在合适的地方进行数据分包,才是我们所关心的。Pipe本身是流水线一样的处理管道,我们只需要把我们收到的数据源源不断地扔到管道里,管道的消费端会帮我们进行数据处理 24 | 25 | 26 | **ReadOnlySequence** 是多组数据的链表结构,更加符合了Tcp的流式传输的特征,并且它强大的多组数据切割能力,可以让我们非常方便的在多数据包中获取正确的数据。 27 | 28 | **Link:** 29 | 30 | 31 | https://learn.microsoft.com/zh-cn/dotnet/api/system.buffers.readonlysequence-1?view=net-7.0 32 | https://learn.microsoft.com/zh-cn/dotnet/api/system.io.pipes?view=net-8.0 33 | 34 | ### 客户端配置(EasyTcpClientOptions) 35 | 36 | | Key | Description | 37 | | ----------- | ----------- | 38 | | NoDelay | 是否不使用 Nagle's算法 避免了过多的小报文的过大TCP头所浪费的带宽 | 39 | | BufferSize | 流数据缓冲区大小 | 40 | | ConnectTimeout | 连接超时时间 | 41 | | ConnectRetryTimes | 连接失败尝试次数 | 42 | | ReadTimeout | 从socket缓冲区读取数据的超时时间 | 43 | | WriteTimeout | 向socket缓冲区写入数据的超时时间 | 44 | | IsSsl | 是否使用ssl连接 | 45 | | PfxCertFilename | ssl证书 | 46 | | PfxPassword | ssl证书密钥 | 47 | | AllowingUntrustedSSLCertificate | 是否允许不受信任的ssl证书 | 48 | | LoggerFactory | 日志工厂 | 49 | | KeepAlive | 是否启动操作系统的tcp keepalive机制 | 50 | | KeepAliveTime | KeepAlive的空闲时长,或者说每次正常发送心跳的周期,默认值为3600s(1小时) | 51 | | KeepAliveProbes | KeepAlive之后设置最大允许发送保活探测包的次数,到达此次数后直接放弃尝试,并关闭连接 | 52 | | KeepAliveIntvl | 没有接收到对方确认,继续发送KeepAlive的发送频率,默认值为60s | 53 | | MaxPipeBufferSize | 待处理数据队列最大缓存,如果有粘包断包的过滤器,要大于单个包的大小,防止卡死 | 54 | 55 | ### 服务端配置(EasyTcpServerOptions) 56 | 57 | | Key | Description | 58 | | ----------- | ----------- | 59 | | NoDelay | 是否不使用 Nagle's算法 避免了过多的小报文的过大TCP头所浪费的带宽 | 60 | | BufferSize | 流数据缓冲区大小 | 61 | | ConnectionsLimit | 最大连接数 | 62 | | BacklogCount | 连接等待/挂起连接数量 | 63 | | IsSsl | 是否使用ssl连接 | 64 | | PfxCertFilename | ssl证书 | 65 | | PfxPassword | ssl证书密钥 | 66 | | AllowingUntrustedSSLCertificate | 是否允许不受信任的ssl证书 | 67 | | MutuallyAuthenticate | 是否双向的ssl验证,标识了客户端是否需要提供证书 | 68 | | CheckCertificateRevocation | 是否检查整数的吊销列表 | 69 | | LoggerFactory | 日志工厂 | 70 | | IdleSessionsCheck | 是否开启空闲连接检查 | 71 | | CheckSessionsIdleMs | 空闲连接检查时间阈值 | 72 | | KeepAlive | 是否启动操作系统的tcp keepalive机制 | 73 | | KeepAliveTime | KeepAlive的空闲时长,或者说每次正常发送心跳的周期,默认值为3600s(1小时) | 74 | | KeepAliveProbes | KeepAlive之后设置最大允许发送保活探测包的次数,到达此次数后直接放弃尝试,并关闭连接 | 75 | | KeepAliveIntvl | 没有接收到对方确认,继续发送KeepAlive的发送频率,默认值为60s | 76 | | MaxPipeBufferSize | 待处理数据队列最大缓存,如果有粘包断包的过滤器,要大于单个包的大小,防止卡死 | 77 | 78 | #### 开启一个Server 79 | ``` 80 | EasyTcpServer _server = new EasyTcpServer(_serverPort); 81 | _server.StartListen(); 82 | ``` 83 | #### 开启一个Server并且配置SSL证书 84 | ```cs 85 | EasyTcpServer easyTcpServer = new EasyTcpServer(7001, new EasyTcpServerOptions() 86 | { 87 | IsSsl = true, 88 | PfxCertFilename = "xxx", 89 | PfxPassword = "xxx", 90 | IdleSessionsCheck = false, 91 | KeepAlive = true, 92 | CheckSessionsIdleMs = 10 * 1000 93 | }); 94 | ``` 95 | #### 开启一个Server,配置数据过滤器处理粘包断包 96 | #### 固定头处理 97 | ```cs 98 | //参数分别为:数据包头长度,数据包体长度所在头下标,数据包体长度字节数,是否小端在前 99 | easyTcpServer.SetReceiveFilter(new FixedHeaderPackageFilter(7, 5, 4, true)); 100 | ``` 101 | #### 固定长度处理 102 | ```cs 103 | //参数分别为:数据包固定长度 104 | easyTcpServer.SetReceiveFilter(new FixedLengthPackageFilter(50)); 105 | ``` 106 | #### 固定字符处理(不推荐) 107 | ```cs 108 | //参数分别为:截取的字符 109 | easyTcpServer.SetReceiveFilter(new FixedCharPackageFilter('\n')); 110 | ``` 111 | 112 | #### 客户端收到数据的回调 113 | ```cs 114 | easyTcpClient.OnReceivedData += (obj, e) => 115 | { 116 | Console.WriteLine(string.Join(',', e.Data)); 117 | Console.WriteLine("\n"); 118 | }; 119 | ``` 120 | 121 | 122 | #### 服务端收到数据的回调 123 | ```cs 124 | easyTcpServer.OnReceivedData += async (obj, e) => 125 | { 126 | Console.WriteLine($"数据来自:{e.Session.RemoteEndPoint}"); 127 | Console.WriteLine(string.Join(',', e.Data)); 128 | }; 129 | ``` 130 | #### 日志配置 131 | 132 | ```cs 133 | var _server = new EasyTcpServer(_serverPort, new EasyTcpServerOptions() 134 | { 135 | ConnectionsLimit = 2, 136 | LoggerFactory = LoggerFactory.Create(options => 137 | { 138 | Log.Logger = new LoggerConfiguration() 139 | .MinimumLevel.Information()//最小的记录等级 140 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information)//对其他日志进行重写,除此之外,目前框架只有微软自带的日志组件 141 | .WriteTo.Console()//输出到控制台 142 | .CreateLogger(); 143 | 144 | options.AddSerilog(); 145 | }) 146 | }); 147 | ``` 148 | -------------------------------------------------------------------------------- /EasyTcp4Net/EasyTcpServerOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace EasyTcp4Net 4 | { 5 | /// 6 | /// tcp server配置类 7 | /// 8 | public class EasyTcpServerOptions 9 | { 10 | public EasyTcpServerOptions() { } 11 | 12 | /// 13 | /// 是否不使用 Nagle's算法 14 | /// 是为了提高实际带宽利用率设计的算法,其做法是合并小的TCP 包为一个 15 | /// 避免了过多的小报文的 过大TCP头所浪费的带宽 16 | /// 17 | public bool NoDelay { get; set; } = true; 18 | 19 | /// 20 | /// 流数据缓冲区大小 21 | /// 单位:字节 22 | /// 默认值:4kb 23 | /// 24 | private int _bufferSize = 4 * 1024; 25 | public int BufferSize 26 | { 27 | get => _bufferSize; 28 | set 29 | { 30 | if (value <= 0) 31 | { 32 | throw new ArgumentException("BufferSize must be greater then zero"); 33 | } 34 | 35 | _bufferSize = value; 36 | } 37 | } 38 | 39 | private int? _connectionsLimit = null; 40 | 41 | /// 42 | /// 最大连接数 43 | /// 默认值:无数量限制 44 | /// 45 | public int? ConnectionsLimit 46 | { 47 | get => _connectionsLimit; 48 | set 49 | { 50 | if (value != null && value.Value <= 0) 51 | { 52 | throw new ArgumentException("ConnectionsLimit must be greater then zero"); 53 | } 54 | 55 | _connectionsLimit = value; 56 | } 57 | } 58 | 59 | /// 60 | /// 连接等待/挂起连接数量 61 | /// 默认值:0 62 | /// 63 | private int? _backlogCount = null; 64 | public int? BacklogCount 65 | { 66 | get => _backlogCount; 67 | set 68 | { 69 | if (value != null && value.Value <= 0) 70 | { 71 | throw new ArgumentException("BacklogCount must be greater then zero"); 72 | } 73 | 74 | _backlogCount = value; 75 | } 76 | } 77 | 78 | /// 79 | /// 是否使用ssl连接 80 | /// 默认值:false 81 | /// 82 | public bool IsSsl { get; set; } = false; 83 | /// 84 | /// ssl证书 85 | /// 86 | public string PfxCertFilename { get; set; } 87 | /// 88 | /// ssl证书密钥 89 | /// 90 | public string PfxPassword { get; set; } 91 | /// 92 | /// 是否允许不受信任的ssl证书 93 | /// 默认值:true 94 | /// 95 | public bool AllowingUntrustedSSLCertificate { get; set; } = true; 96 | /// 97 | /// 是否双向的ssl验证,标识了客户端是否需要提供证书 98 | /// 默认值:false 99 | /// 100 | public bool MutuallyAuthenticate { get; set; } = false; 101 | /// 102 | /// 是否检查整数的吊销列表 103 | /// 默认值:true 104 | /// 105 | public bool CheckCertificateRevocation { get; set; } = true; 106 | /// 107 | /// 日志工厂 108 | /// 109 | public ILoggerFactory LoggerFactory { get; set; } 110 | 111 | /// 112 | /// 是否开启空闲连接检查 113 | /// 默认值: false 114 | /// 115 | public bool IdleSessionsCheck { get; set; } = false; 116 | 117 | private int _checkSessionsIdleMs { get; set; } = 300 * 1000; 118 | /// 119 | /// 空闲连接检查时间阈值 120 | /// 超过这段时间不活跃的连接将会被关闭 121 | /// 默认值:300秒 122 | /// 单位:毫秒 123 | /// 124 | public int CheckSessionsIdleMs 125 | { 126 | get => _checkSessionsIdleMs; 127 | set 128 | { 129 | if (value <= 0) 130 | { 131 | throw new ArgumentException("CheckSessionsIdleMs must be greater then zero"); 132 | } 133 | 134 | _checkSessionsIdleMs = value; 135 | } 136 | } 137 | 138 | /// 139 | /// 是否启动操作系统的tcp keepalive机制 140 | /// 不同操作系统实现keepalive机制并不相同 141 | /// 142 | public bool KeepAlive { get; set; } = false; 143 | 144 | private int _keepAliveTime = 3600; 145 | /// 146 | /// KeepAlive的空闲时长,或者说每次正常发送心跳的周期,默认值为3600s(1小时) 147 | /// 148 | public int KeepAliveTime 149 | { 150 | get => _keepAliveTime; 151 | set 152 | { 153 | if (value <= 0) 154 | { 155 | throw new ArgumentException("KeepAliveTime must be greater then zero"); 156 | } 157 | 158 | _keepAliveTime = value; 159 | } 160 | } 161 | 162 | private int _keepAliveProbes = 9; 163 | /// 164 | /// KeepAlive之后设置最大允许发送保活探测包的次数,到达此次数后直接放弃尝试,并关闭连接 165 | /// 默认值:9次 166 | /// 167 | public int KeepAliveProbes 168 | { 169 | get => _keepAliveProbes; 170 | set 171 | { 172 | if (value <= 0) 173 | { 174 | throw new ArgumentException("KeepAliveProbes must be greater then zero"); 175 | } 176 | 177 | _keepAliveProbes = value; 178 | } 179 | } 180 | 181 | private int _keepAliveIntvl = 60; 182 | /// 183 | /// 没有接收到对方确认,继续发送KeepAlive的发送频率,默认值为60s 184 | /// 单位:秒 185 | /// 默认值 60(1分钟) 186 | /// 187 | public int KeepAliveIntvl 188 | { 189 | get => _keepAliveIntvl; 190 | set 191 | { 192 | if (value <= 0) 193 | { 194 | throw new ArgumentException("KeepAliveIntvl must be greater then zero"); 195 | } 196 | 197 | _keepAliveIntvl = value; 198 | } 199 | } 200 | 201 | private int _maxPipeBufferSize = int.MaxValue; 202 | /// 203 | /// 待处理数据队列最大缓存,如果有粘包断包的过滤器,要大于单个包的大小,防止卡死 204 | /// 用于流量控制,背压 205 | /// 单位:字节 206 | /// 默认值 int.MaxValue 207 | /// 208 | public int MaxPipeBufferSize 209 | { 210 | get => _maxPipeBufferSize; 211 | set 212 | { 213 | if (value <= 0) 214 | { 215 | throw new ArgumentException("MaxPipeBufferSize must be greater then zero"); 216 | } 217 | 218 | _maxPipeBufferSize = value; 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /EasyTcp4Net/EasyTcpClientOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace EasyTcp4Net 4 | { 5 | /// 6 | /// tcp server配置类 7 | /// 8 | public class EasyTcpClientOptions 9 | { 10 | public EasyTcpClientOptions() { } 11 | 12 | /// 13 | /// 是否不使用 Nagle's算法 14 | /// 是为了提高实际带宽利用率设计的算法,其做法是合并小的TCP 包为一个 15 | /// 避免了过多的小报文的过大TCP头所浪费的带宽 16 | /// 17 | public bool NoDelay { get; set; } = true; 18 | /// 19 | /// 流数据缓冲区大小 20 | /// 单位:字节 21 | /// 默认值:4kb 22 | /// 23 | private int _bufferSize = 4 * 1024; 24 | public int BufferSize 25 | { 26 | get => _bufferSize; 27 | set 28 | { 29 | if (value <= 0) 30 | { 31 | throw new ArgumentException("BufferSize must be greater then zero"); 32 | } 33 | 34 | _bufferSize = value; 35 | } 36 | } 37 | 38 | private int _connectTimeout = 30 * 1000; 39 | /// 40 | /// 连接超时时间 41 | /// 单位:毫秒 42 | /// 默认值:30秒 43 | /// 44 | public int ConnectTimeout 45 | { 46 | get => _connectTimeout; 47 | set 48 | { 49 | if (value <= 0) 50 | { 51 | throw new ArgumentException("ConnectTimeout must be greater then zero"); 52 | } 53 | 54 | _connectTimeout = value; 55 | } 56 | } 57 | 58 | /// 59 | /// 连接失败尝试次数 60 | /// 默认值:0(不重试) 61 | /// 62 | private int _connectRetryTimes = 0; 63 | public int ConnectRetryTimes 64 | { 65 | get => _connectRetryTimes; 66 | set 67 | { 68 | if (value < 0) 69 | { 70 | throw new ArgumentException("ConnectRetryTimes must be greater or equal then zero"); 71 | } 72 | 73 | _connectRetryTimes = value; 74 | } 75 | } 76 | 77 | /// 78 | /// 从socket缓冲区读取数据的超时时间 79 | /// 默认值:10秒 80 | /// 单位:毫秒 81 | /// 82 | private int _readTimeout = 10 * 1000; 83 | public int ReadTimeout 84 | { 85 | get => _readTimeout; 86 | set 87 | { 88 | if (value <= 0) 89 | { 90 | throw new ArgumentException("ReadTimeout must be greater then zero"); 91 | } 92 | 93 | _readTimeout = value; 94 | } 95 | } 96 | 97 | /// 98 | /// 向socket缓冲区写入数据的超时时间 99 | /// 默认值:10秒 100 | /// 单位:毫秒 101 | /// 102 | private int _writeTimeout = 10 * 1000; 103 | public int WriteTimeout 104 | { 105 | get => _writeTimeout; 106 | set 107 | { 108 | if (value <= 0) 109 | { 110 | throw new ArgumentException("WriteTimeout must be greater then zero"); 111 | } 112 | 113 | _writeTimeout = value; 114 | } 115 | } 116 | 117 | /// 118 | /// 是否使用ssl连接 119 | /// 默认值:false 120 | /// 121 | public bool IsSsl { get; set; } = false; 122 | /// 123 | /// ssl证书 124 | /// 125 | public string PfxCertFilename { get; set; } 126 | /// 127 | /// ssl证书密钥 128 | /// 129 | public string PfxPassword { get; set; } 130 | /// 131 | /// 是否允许不受信任的ssl证书 132 | /// 默认值:true 133 | /// 134 | public bool AllowingUntrustedSSLCertificate { get; set; } = true; 135 | /// 136 | /// 是否检查整数的吊销列表 137 | /// 默认值:true 138 | /// 139 | public bool CheckCertificateRevocation { get; set; } = true; 140 | /// 141 | /// 日志工厂 142 | /// 143 | public ILoggerFactory LoggerFactory { get; set; } 144 | 145 | /// 146 | /// 是否启动操作系统的tcp keepalive机制 147 | /// 不同操作系统实现keepalive机制并不相同 148 | /// 149 | public bool KeepAlive { get; set; } = false; 150 | 151 | private int _keepAliveTime = 3600; 152 | /// 153 | /// KeepAlive的空闲时长,或者说每次正常发送心跳的周期,默认值为3600s(1小时) 154 | /// 155 | public int KeepAliveTime 156 | { 157 | get => _keepAliveTime; 158 | set 159 | { 160 | if (value <= 0) 161 | { 162 | throw new ArgumentException("KeepAliveTime must be greater then zero"); 163 | } 164 | 165 | _keepAliveTime = value; 166 | } 167 | } 168 | 169 | private int _keepAliveProbes = 9; 170 | /// 171 | /// KeepAlive之后设置最大允许发送保活探测包的次数,到达此次数后直接放弃尝试,并关闭连接 172 | /// 默认值:9次 173 | /// 174 | public int KeepAliveProbes 175 | { 176 | get => _keepAliveProbes; 177 | set 178 | { 179 | if (value <= 0) 180 | { 181 | throw new ArgumentException("KeepAliveProbes must be greater then zero"); 182 | } 183 | 184 | _keepAliveProbes = value; 185 | } 186 | } 187 | 188 | private int _keepAliveIntvl = 60; 189 | /// 190 | /// 没有接收到对方确认,继续发送KeepAlive的发送频率,默认值为60s 191 | /// 单位:秒 192 | /// 默认值 60(1分钟) 193 | /// 194 | public int KeepAliveIntvl 195 | { 196 | get => _keepAliveIntvl; 197 | set 198 | { 199 | if (value <= 0) 200 | { 201 | throw new ArgumentException("KeepAliveIntvl must be greater then zero"); 202 | } 203 | 204 | _keepAliveIntvl = value; 205 | } 206 | } 207 | 208 | private int _maxPipeBufferSize = int.MaxValue; 209 | /// 210 | /// 待处理数据队列最大缓存,如果有粘包断包的过滤器,要大于单个包的大小,防止卡死 211 | /// 用于流量控制,背压 212 | /// 单位:字节 213 | /// 默认值 int.MaxValue 214 | /// 215 | public int MaxPipeBufferSize 216 | { 217 | get => _maxPipeBufferSize; 218 | set 219 | { 220 | if (value <= 0) 221 | { 222 | throw new ArgumentException("MaxPipeBufferSize must be greater then zero"); 223 | } 224 | 225 | _maxPipeBufferSize = value; 226 | } 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /EasyTcp4Net.WpfTest/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 38 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 82 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /EasyTcp4Net.WpfTest/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using CommunityToolkit.Mvvm.Input; 3 | using CommunityToolkit.Mvvm.Messaging; 4 | using System.Collections; 5 | using System.Collections.ObjectModel; 6 | using System.Net; 7 | using System.Net.NetworkInformation; 8 | using System.Windows; 9 | 10 | namespace EasyTcp4Net.WpfTest 11 | { 12 | /// 13 | /// 没有粘包处理的的测试 14 | /// 15 | public class MainWindowViewModel : ObservableObject 16 | { 17 | private int index = 1; 18 | private ushort _serverPort => ushort.Parse(_portText); 19 | 20 | private string _portText; 21 | public string PortText 22 | { 23 | get => _portText; 24 | set => SetProperty(ref _portText, value); 25 | } 26 | 27 | private string _messages; 28 | public string Messages 29 | { 30 | get => _messages; 31 | set => SetProperty(ref _messages, value); 32 | } 33 | 34 | private bool _serverListening; 35 | public bool ServerListening 36 | { 37 | get => _serverListening; 38 | set => SetProperty(ref _serverListening, value); 39 | } 40 | 41 | public ObservableCollection Clients { get; set; } = new ObservableCollection(); 42 | 43 | private Client _selectedClient; 44 | public Client SelectedClient 45 | { 46 | get => _selectedClient; 47 | set => SetProperty(ref _selectedClient, value); 48 | } 49 | 50 | private EasyTcpServer _server; 51 | public RelayCommand LoadCommand { get; set; } 52 | public RelayCommand StartServerCommand { get; set; } 53 | public RelayCommand StopServerCommand { get; set; } 54 | public RelayCommand AddClientCommand { get; set; } 55 | public MainWindowViewModel() 56 | { 57 | LoadCommand = new RelayCommand(() => 58 | { 59 | PortText = PortFilter.GetFirstAvailablePort().ToString(); 60 | _server = new EasyTcpServer(_serverPort); 61 | 62 | _server.OnReceivedData += async (obj, e) => 63 | { 64 | Messages += $"服务端收到:来自{e.Session.RemoteEndPoint.ToString()}:[{System.Text.Encoding.Default.GetString(e.Data.ToArray())}]\n"; 65 | await _server.SendAsync(e.Session, System.Text.Encoding.Default.GetBytes($"服务器表示收到了{e.Session.RemoteEndPoint.ToString()}的数据")); 66 | }; 67 | }); 68 | 69 | WeakReferenceMessenger.Default 70 | .Register(this, "AddMessage", async (x, y) => 71 | { 72 | Messages += y; 73 | }); 74 | 75 | StartServerCommand = new RelayCommand(() => 76 | { 77 | _server!.StartListen(); 78 | ServerListening = true; 79 | }); 80 | 81 | StopServerCommand = new RelayCommand(async () => 82 | { 83 | await _server!.CloseAsync(); 84 | ServerListening = false; 85 | }); 86 | 87 | AddClientCommand = new RelayCommand(() => 88 | { 89 | var newClient = new Client($"客户端{index++}", new EasyTcpClient("127.0.0.1", _serverPort)); 90 | Clients.Add(newClient); 91 | SelectedClient = newClient; 92 | }); 93 | } 94 | } 95 | 96 | public class Client : ObservableObject 97 | { 98 | private string _sendMessage; 99 | public string SendMessage 100 | { 101 | get => _sendMessage; 102 | set => SetProperty(ref _sendMessage, value); 103 | } 104 | 105 | private bool _connected; 106 | public bool Connected 107 | { 108 | get => _connected; 109 | set => SetProperty(ref _connected, value); 110 | } 111 | 112 | public string ClientId { get; set; } 113 | public EasyTcpClient EasyTcpClient { get; set; } 114 | 115 | public AsyncRelayCommand ConnectCommandAsync { get; set; } 116 | public AsyncRelayCommand DisConnectedCommandAsync { get; set; } 117 | public AsyncRelayCommand SendAsync { get; set; } 118 | public Client(string clientid, EasyTcpClient easyTcpClient) 119 | { 120 | EasyTcpClient = easyTcpClient; 121 | ClientId = clientid; 122 | ConnectCommandAsync = new AsyncRelayCommand(async () => 123 | { 124 | try 125 | { 126 | await EasyTcpClient.ConnectAsync(); 127 | Connected = true; 128 | } 129 | catch (Exception ex) 130 | { 131 | MessageBox.Show($"连接失败:{ex}"); 132 | } 133 | }); 134 | DisConnectedCommandAsync = new AsyncRelayCommand(async () => 135 | { 136 | await EasyTcpClient.DisConnectAsync(); 137 | }); 138 | 139 | easyTcpClient.OnDisConnected += (obj, e) => 140 | { 141 | Connected = false; 142 | }; 143 | easyTcpClient.OnReceivedData += (obj, e) => 144 | { 145 | WeakReferenceMessenger.Default 146 | .Send($"{ClientId}:收到[{System.Text.Encoding.Default.GetString(e.Data.ToArray())}]\n", "AddMessage"); 147 | }; 148 | 149 | SendAsync = new AsyncRelayCommand(async () => 150 | { 151 | if (!Connected) 152 | MessageBox.Show("连接断开"); 153 | 154 | if (string.IsNullOrEmpty(SendMessage?.Trim())) 155 | return; 156 | 157 | await EasyTcpClient.SendAsync(System.Text.Encoding.Default.GetBytes(SendMessage.Trim())); 158 | 159 | SendMessage = null; 160 | }); 161 | } 162 | } 163 | 164 | public class PortFilter 165 | { 166 | public static int GetFirstAvailablePort() 167 | { 168 | int MAX_PORT = 65535; 169 | int BEGIN_PORT = 50000; 170 | 171 | for (int i = BEGIN_PORT; i < MAX_PORT; i++) 172 | { 173 | 174 | if (PortIsAvailable(i)) return i; 175 | } 176 | 177 | return -1; 178 | } 179 | 180 | private static IList PortIsUsed() 181 | { 182 | IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); 183 | IPEndPoint[] ipsTCP = ipGlobalProperties.GetActiveTcpListeners(); 184 | IPEndPoint[] ipsUDP = ipGlobalProperties.GetActiveUdpListeners(); 185 | TcpConnectionInformation[] tcpConnInfoArray = ipGlobalProperties.GetActiveTcpConnections(); 186 | IList allPorts = new ArrayList(); 187 | foreach (IPEndPoint ep in ipsTCP) allPorts.Add(ep.Port); 188 | foreach (IPEndPoint ep in ipsUDP) allPorts.Add(ep.Port); 189 | foreach (TcpConnectionInformation conn in tcpConnInfoArray) allPorts.Add(conn.LocalEndPoint.Port); 190 | return allPorts; 191 | } 192 | 193 | private static bool PortIsAvailable(int port) 194 | { 195 | bool isAvailable = true; 196 | IList portUsed = PortIsUsed(); 197 | foreach (int p in portUsed) 198 | { 199 | if (p == port) 200 | { 201 | isAvailable = false; break; 202 | } 203 | } 204 | return isAvailable; 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /EasyTcp4Net.WpfTest/MainWindowViewModel_fixheader.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using CommunityToolkit.Mvvm.Input; 3 | using CommunityToolkit.Mvvm.Messaging; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Xaml.Behaviors.Layout; 6 | using Serilog; 7 | using Serilog.Core; 8 | using Serilog.Events; 9 | using System.Collections.ObjectModel; 10 | using System.IO; 11 | using System.Windows; 12 | 13 | namespace EasyTcp4Net.WpfTest 14 | { 15 | /// 16 | /// 固定数据头测试 17 | /// 18 | public class MainWindowViewModelFixHeader : ObservableObject 19 | { 20 | public static bool IsLittleEndian = false;//默认测试大端在前 21 | private int index = 1; 22 | private ushort _serverPort => ushort.Parse(_portText); 23 | 24 | private string _portText; 25 | public string PortText 26 | { 27 | get => _portText; 28 | set => SetProperty(ref _portText, value); 29 | } 30 | 31 | private string _messages; 32 | public string Messages 33 | { 34 | get => _messages; 35 | set => SetProperty(ref _messages, value); 36 | } 37 | 38 | private bool _serverListening; 39 | public bool ServerListening 40 | { 41 | get => _serverListening; 42 | set => SetProperty(ref _serverListening, value); 43 | } 44 | 45 | public ObservableCollection Clients { get; set; } = new ObservableCollection(); 46 | 47 | private ClientFixHeader _selectedClient; 48 | public ClientFixHeader SelectedClient 49 | { 50 | get => _selectedClient; 51 | set => SetProperty(ref _selectedClient, value); 52 | } 53 | 54 | private EasyTcpServer _server; 55 | public RelayCommand LoadCommand { get; set; } 56 | public RelayCommand StartServerCommand { get; set; } 57 | public RelayCommand StopServerCommand { get; set; } 58 | public RelayCommand AddClientCommand { get; set; } 59 | public MainWindowViewModelFixHeader() 60 | { 61 | LoadCommand = new RelayCommand(() => 62 | { 63 | PortText = PortFilter.GetFirstAvailablePort().ToString(); 64 | _server = new EasyTcpServer(_serverPort,new EasyTcpServerOptions() 65 | { 66 | ConnectionsLimit = 2, 67 | IsSsl = true, 68 | PfxCertFilename = "test.pfx", 69 | PfxPassword = "123456", 70 | MutuallyAuthenticate = true, 71 | LoggerFactory = LoggerFactory.Create(options => 72 | { 73 | Log.Logger = new LoggerConfiguration() 74 | .MinimumLevel.Information()//最小的记录等级 75 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information)//对其他日志进行重写,除此之外,目前框架只有微软自带的日志组件 76 | .WriteTo.Console()//输出到控制台 77 | .CreateLogger(); 78 | 79 | options.AddSerilog(); 80 | }) 81 | },"0.0.0.0"); 82 | _server.SetReceiveFilter(new FixedHeaderPackageFilter(8 + 4, 8, 4, false)); 83 | _server.OnReceivedData += async (obj, e) => 84 | { 85 | var packet = new Pakcet(); 86 | packet.Deserialize(e.Data.Slice(8 + 4).ToArray()); 87 | Messages += $"服务端收到:来自{e.Session.RemoteEndPoint.ToString()}:[{packet.Body}]\n"; 88 | await _server.SendAsync(e.Session, new Pakcet() { Body = $"服务器表示收到了{e.Session.RemoteEndPoint.ToString()}的数据" }.Serialize()); 89 | }; 90 | }); 91 | 92 | WeakReferenceMessenger.Default 93 | .Register(this, "AddMessage", async (x, y) => 94 | { 95 | Messages += y; 96 | }); 97 | 98 | StartServerCommand = new RelayCommand(() => 99 | { 100 | _server!.StartListen(); 101 | ServerListening = true; 102 | }); 103 | 104 | StopServerCommand = new RelayCommand(async () => 105 | { 106 | await _server!.CloseAsync(); 107 | ServerListening = false; 108 | }); 109 | 110 | AddClientCommand = new RelayCommand(() => 111 | { 112 | var newClient = new ClientFixHeader($"客户端{index++}", new EasyTcpClient("127.0.0.1", _serverPort,new EasyTcpClientOptions() 113 | { 114 | IsSsl = true, 115 | PfxCertFilename = "test.pfx", 116 | PfxPassword = "123456" 117 | })); 118 | Clients.Add(newClient); 119 | SelectedClient = newClient; 120 | }); 121 | } 122 | } 123 | 124 | public class ClientFixHeader : ObservableObject 125 | { 126 | private string _sendMessage; 127 | public string SendMessage 128 | { 129 | get => _sendMessage; 130 | set => SetProperty(ref _sendMessage, value); 131 | } 132 | 133 | private bool _connected; 134 | public bool Connected 135 | { 136 | get => _connected; 137 | set => SetProperty(ref _connected, value); 138 | } 139 | 140 | public string ClientId { get; set; } 141 | public EasyTcpClient EasyTcpClient { get; set; } 142 | 143 | public AsyncRelayCommand ConnectCommandAsync { get; set; } 144 | public AsyncRelayCommand DisConnectedCommandAsync { get; set; } 145 | public AsyncRelayCommand SendAsync { get; set; } 146 | public ClientFixHeader(string clientid, EasyTcpClient easyTcpClient) 147 | { 148 | EasyTcpClient = easyTcpClient; 149 | EasyTcpClient.SetReceiveFilter(new FixedHeaderPackageFilter(8 + 4, 8, 4, false)); 150 | ClientId = clientid; 151 | ConnectCommandAsync = new AsyncRelayCommand(async () => 152 | { 153 | try 154 | { 155 | await EasyTcpClient.ConnectAsync(); 156 | Connected = true; 157 | } 158 | catch (Exception ex) 159 | { 160 | MessageBox.Show($"连接失败:{ex}"); 161 | } 162 | }); 163 | DisConnectedCommandAsync = new AsyncRelayCommand(async () => 164 | { 165 | await EasyTcpClient.DisConnectAsync(); 166 | }); 167 | 168 | easyTcpClient.OnDisConnected += (obj, e) => 169 | { 170 | Console.WriteLine("连接已断开"); 171 | Connected = false; 172 | }; 173 | easyTcpClient.OnReceivedData += (obj, e) => 174 | { 175 | var packet = new Pakcet(); 176 | packet.Deserialize(e.Data.Slice(8 + 4).ToArray()); 177 | WeakReferenceMessenger.Default 178 | .Send($"{ClientId}:收到[{packet.Body}]\n", "AddMessage"); 179 | }; 180 | 181 | SendAsync = new AsyncRelayCommand(async () => 182 | { 183 | if (!Connected) 184 | { 185 | MessageBox.Show("连接断开"); 186 | return; 187 | } 188 | 189 | if (string.IsNullOrEmpty(SendMessage?.Trim())) 190 | return; 191 | 192 | await EasyTcpClient.SendAsync(new Pakcet() 193 | { 194 | Body = SendMessage, 195 | }.Serialize()); 196 | 197 | SendMessage = null; 198 | }); 199 | } 200 | } 201 | } 202 | 203 | /// 204 | /// 数据包 205 | /// 206 | public class Pakcet 207 | { 208 | //头 8 + 4 209 | public long Sequence { get; set; } 210 | public int BodyLength { get; set; } 211 | // 212 | public TBody? Body { get; set; } 213 | public void Deserialize(byte[] bodyData) 214 | { 215 | Body = ProtoBufSerializer.DeSerialize(bodyData); 216 | } 217 | 218 | public byte[] Serialize() 219 | { 220 | var bodyArray = ProtoBufSerializer.Serialize(Body); 221 | BodyLength = bodyArray.Length; 222 | byte[] result = new byte[BodyLength + 8 + 4]; 223 | AddInt64(result, 0, Sequence); 224 | AddInt32(result, 8, BodyLength); 225 | Buffer.BlockCopy(bodyArray, 0, result, 8 + 4, bodyArray.Length); 226 | 227 | return result; 228 | } 229 | 230 | //大端模式添加数据 231 | public void AddInt32(byte[] buffer, int startIndex, int v) 232 | { 233 | buffer[startIndex++] = (byte)(v >> 24); 234 | buffer[startIndex++] = (byte)(v >> 16); 235 | buffer[startIndex++] = (byte)(v >> 8); 236 | buffer[startIndex++] = (byte)v; 237 | } 238 | 239 | //大端模式添加数据 240 | public static void AddInt64(byte[] buffer, int startIndex, long v) 241 | { 242 | buffer[startIndex++] = (byte)(v >> 56); 243 | buffer[startIndex++] = (byte)(v >> 48); 244 | buffer[startIndex++] = (byte)(v >> 40); 245 | buffer[startIndex++] = (byte)(v >> 32); 246 | buffer[startIndex++] = (byte)(v >> 24); 247 | buffer[startIndex++] = (byte)(v >> 16); 248 | buffer[startIndex++] = (byte)(v >> 8); 249 | buffer[startIndex++] = (byte)v; 250 | } 251 | } 252 | 253 | public class ProtoBufSerializer 254 | { 255 | public static byte[] Serialize(T serializeObj) 256 | { 257 | try 258 | { 259 | using (var stream = new MemoryStream()) 260 | { 261 | ProtoBuf.Serializer.Serialize(stream, serializeObj); 262 | var result = new byte[stream.Length]; 263 | stream.Position = 0L; 264 | stream.Read(result, 0, result.Length); 265 | return result; 266 | } 267 | } 268 | catch (Exception ex) 269 | { 270 | return null; 271 | } 272 | } 273 | 274 | public static T DeSerialize(byte[] bytes) 275 | { 276 | try 277 | { 278 | using (var stream = new MemoryStream()) 279 | { 280 | stream.Write(bytes, 0, bytes.Length); 281 | stream.Position = 0L; 282 | return ProtoBuf.Serializer.Deserialize(stream); 283 | } 284 | } 285 | catch 286 | { 287 | return default(T); 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /EasyTcp4Net/ClientSession.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.IO.Pipelines; 3 | using System.Net; 4 | using System.Net.Security; 5 | using System.Net.Sockets; 6 | using System.Security.Cryptography.X509Certificates; 7 | 8 | namespace EasyTcp4Net 9 | { 10 | /// 11 | /// 客户端会话对象 12 | /// 13 | public class ClientSession : IDisposable 14 | { 15 | /// 16 | /// 会话id 17 | /// 添加了身份认证后,可以将该值绑定账号身份唯一标识 18 | /// 可以知道session属于哪个用户 19 | /// 多端登录可以知道哪些session属于同一用户 20 | /// 21 | public string SessionId { get; set; } = Guid.NewGuid().ToString(); 22 | /// 23 | /// 对于服务端来说,远程终结点就是客户端的本地套接字终结点 24 | /// 25 | public IPEndPoint RemoteEndPoint => _socket == null ? null : _socket.RemoteEndPoint as IPEndPoint; 26 | /// 27 | /// 对于服务端来说,本地终结点就是客户端在远端对应的套接字终结点 28 | /// 29 | public IPEndPoint LocalEndPoint => _socket == null ? null : _socket.LocalEndPoint as IPEndPoint; 30 | public bool IsSslAuthenticated { get; internal set; } = false; 31 | public DateTime? SslAuthenticatedTime { get; internal set; } = null; 32 | public DateTime LastActiveTime { get; set; } = DateTime.UtcNow; 33 | internal NetworkStream NetworkStream { get; private set; } 34 | internal SslStream SslStream { get; private set; } 35 | public bool IsDisposed { get; private set; } 36 | public bool Connected { get; internal set; } 37 | public PipeReader PipeReader => _pipe.Reader; 38 | public PipeWriter PipeWriter => _pipe.Writer; 39 | 40 | private Task _processDataTask; 41 | private Socket _socket; 42 | private Pipe _pipe; 43 | private int _bufferSize; 44 | private event EventHandler _onReceivedData; 45 | private readonly IPackageFilter _receivePackageFilter; //接收数据包的拦截处理器 46 | private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1); //发送数据的信号量 47 | internal readonly CancellationTokenSource _lifecycleTokenSource; 48 | 49 | /// 50 | /// 创建服务端的与客户端的会话 51 | /// 52 | /// 与客户端连接的套接字 53 | /// 读写缓冲区 54 | /// 内部待处理最大缓冲区,流量控制,背压 55 | /// 接收数据的过滤处理器 56 | /// 发送数据的过滤处理器 57 | public ClientSession(Socket socket, int bufferSize, int maxPipeBufferSize, IPackageFilter receiveFilter, EventHandler onReceivedData) 58 | { 59 | _socket = socket; 60 | _pipe = new Pipe(new PipeOptions(pauseWriterThreshold: maxPipeBufferSize)); 61 | _bufferSize = bufferSize; 62 | NetworkStream = new NetworkStream(socket); 63 | _lifecycleTokenSource = new CancellationTokenSource(); 64 | _receivePackageFilter = receiveFilter; 65 | _onReceivedData = onReceivedData; 66 | _processDataTask = Task.Factory.StartNew(ReadPipeAsync, _lifecycleTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current); 67 | } 68 | 69 | /// 70 | /// 客户端会话的ssl认证 71 | /// 72 | /// ssl证书 73 | /// 是否允许不受信任的证书 74 | /// 双向认证,即客户端是否需要提供证书 75 | /// 是否检查证书列表的吊销列表 76 | /// 任务令牌 77 | /// 78 | internal async Task SslAuthenticateAsync(X509Certificate2 x509Certificate, 79 | bool allowingUntrustedSSLCertificate, 80 | bool mutuallyAuthenticate, 81 | bool checkCertificateRevocation, 82 | CancellationToken cancellationToken) 83 | { 84 | 85 | if (allowingUntrustedSSLCertificate) 86 | { 87 | SslStream = new SslStream(NetworkStream, false, 88 | (obj, certificate, chain, error) => true); 89 | } 90 | else 91 | { 92 | SslStream = new SslStream(NetworkStream, false); 93 | } 94 | 95 | try 96 | { 97 | //serverCertificate:用于对服务器进行身份验证的 X509Certificate 98 | //clientCertificateRequired:一个 Boolean 值,指定客户端是否必须为身份验证提供证书 99 | //checkCertificateRevocation:一个 Boolean 值,指定在身份验证过程中是否检查证书吊销列表 100 | await SslStream.AuthenticateAsServerAsync(new SslServerAuthenticationOptions() 101 | { 102 | ServerCertificate = x509Certificate, 103 | ClientCertificateRequired = mutuallyAuthenticate, 104 | CertificateRevocationCheckMode = checkCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck 105 | }, cancellationToken).ConfigureAwait(false); 106 | 107 | if (!SslStream.IsEncrypted || !SslStream.IsAuthenticated) 108 | { 109 | return false; 110 | } 111 | 112 | if (mutuallyAuthenticate && !SslStream.IsMutuallyAuthenticated) 113 | { 114 | return false; 115 | } 116 | } 117 | catch (Exception) 118 | { 119 | throw; 120 | } 121 | 122 | IsSslAuthenticated = true; 123 | SslAuthenticatedTime = DateTime.UtcNow; 124 | 125 | return true; 126 | } 127 | 128 | /// 129 | /// 读取数据 130 | /// 131 | /// 缓冲区大小 132 | /// 133 | /// 读取到长度为0的数据,默认为断开了 134 | internal async Task ReceiveDataAsync() 135 | { 136 | while (!_lifecycleTokenSource.Token.IsCancellationRequested) 137 | { 138 | if (IsDisposed) 139 | break; 140 | 141 | try 142 | { 143 | if (!IsConnected()) 144 | { 145 | break; 146 | } 147 | 148 | Memory buffer = PipeWriter.GetMemory(_bufferSize); 149 | int readCount; 150 | if (IsSslAuthenticated) 151 | { 152 | readCount = await SslStream.ReadAsync(buffer, _lifecycleTokenSource.Token) 153 | .ConfigureAwait(false); 154 | } 155 | else 156 | { 157 | readCount = await NetworkStream.ReadAsync(buffer, _lifecycleTokenSource.Token) 158 | .ConfigureAwait(false); 159 | } 160 | 161 | if (readCount > 0) 162 | { 163 | LastActiveTime = DateTime.UtcNow; 164 | PipeWriter.Advance(readCount); 165 | } 166 | else 167 | { 168 | throw new SocketException(); 169 | } 170 | FlushResult result = await PipeWriter.FlushAsync().ConfigureAwait(false); 171 | if (result.IsCompleted) 172 | { 173 | break; 174 | } 175 | } 176 | catch (Exception) 177 | { 178 | throw; 179 | } 180 | } 181 | 182 | PipeWriter.Complete(); 183 | } 184 | 185 | internal async Task ReadPipeAsync() 186 | { 187 | while (!_lifecycleTokenSource.Token.IsCancellationRequested) 188 | { 189 | ReadResult result = await PipeReader.ReadAsync(); 190 | ReadOnlySequence buffer = result.Buffer; 191 | ReadOnlySequence data; 192 | do 193 | { 194 | if (_receivePackageFilter != null) 195 | { 196 | data = _receivePackageFilter.ResolvePackage(ref buffer); 197 | } 198 | else 199 | { 200 | data = buffer; 201 | buffer = buffer.Slice(data.Length); 202 | } 203 | 204 | if (!data.IsEmpty) 205 | { 206 | _onReceivedData?.Invoke(this, new ServerDataReceiveEventArgs(this, data.ToArray())); 207 | } 208 | } 209 | while (!data.IsEmpty && buffer.Length > 0); 210 | PipeReader.AdvanceTo(buffer.Start); 211 | } 212 | 213 | PipeReader.Complete(); 214 | } 215 | 216 | public async Task SendAsync(byte[] data) 217 | { 218 | if (data == null || data.Length < 1) 219 | throw new ArgumentNullException(nameof(data)); 220 | 221 | await SendInternalAsync(data); 222 | } 223 | 224 | public async Task SendAsync(Memory data) 225 | { 226 | if (data.IsEmpty || data.Length < 1) 227 | throw new ArgumentNullException(nameof(data)); 228 | 229 | await SendInternalAsync(data); 230 | } 231 | 232 | private async Task SendInternalAsync(Memory data) 233 | { 234 | if (!Connected) 235 | return; 236 | 237 | LastActiveTime = DateTime.UtcNow; 238 | int bytesRemaining = data.Length; 239 | int index = 0; 240 | 241 | try 242 | { 243 | _sendLock.Wait(); 244 | while (bytesRemaining > 0) 245 | { 246 | Memory needSendData = null; 247 | if (bytesRemaining >= _bufferSize) 248 | { 249 | needSendData = data.Slice(index, _bufferSize); 250 | } 251 | else 252 | { 253 | needSendData = data.Slice(index, bytesRemaining); 254 | } 255 | if (IsSslAuthenticated) 256 | { 257 | await SslStream.WriteAsync(needSendData, _lifecycleTokenSource.Token); 258 | } 259 | else 260 | { 261 | await NetworkStream.WriteAsync(needSendData, _lifecycleTokenSource.Token); 262 | } 263 | 264 | index += needSendData.Length; 265 | bytesRemaining -= needSendData.Length; 266 | } 267 | } 268 | finally 269 | { 270 | _sendLock.Release(); 271 | } 272 | } 273 | 274 | /// 275 | /// 判断客户端的会话是否连接着 276 | /// 277 | /// 是否连接,true表示已连接,false表示未连接 278 | internal bool IsConnected() 279 | { 280 | return _socket.CheckConnect(); 281 | } 282 | 283 | public async Task DisposeAsync() 284 | { 285 | if (!IsDisposed) 286 | { 287 | await InternalDispose(); 288 | } 289 | } 290 | 291 | private async Task InternalDispose() 292 | { 293 | _lifecycleTokenSource?.Cancel(); 294 | await _processDataTask; 295 | _socket?.Dispose(); 296 | NetworkStream?.Close(); 297 | NetworkStream?.Dispose(); 298 | SslStream?.Close(); 299 | SslStream?.Dispose(); 300 | IsDisposed = true; 301 | } 302 | 303 | public void Dispose() 304 | { 305 | DisposeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /EasyTcp4Net/EasyTcpServer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System.Collections.Concurrent; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Security.Authentication; 6 | using System.Security.Cryptography.X509Certificates; 7 | 8 | namespace EasyTcp4Net 9 | { 10 | public class EasyTcpServer 11 | { 12 | public bool IsListening { get; private set; } //是否正在监听客户端连接中 13 | public event EventHandler OnReceivedData; 14 | public event EventHandler OnClientConnectionChanged; 15 | private readonly IPAddress _ipAddress = null; //本地监听的ip地址 16 | private readonly ushort _port = 0; //本地监听端口 17 | private Socket _serverSocket; //服务端本地套接字 18 | private readonly EasyTcpServerOptions _options = new();//服务端总配置 19 | private readonly X509Certificate2 _certificate;//ssl证书对象 20 | private readonly SemaphoreSlim _startListenLock = new SemaphoreSlim(1, 1); //开启监听的信号量 21 | private CancellationTokenSource _lifecycleTokenSource; //整个服务端存活的token 22 | private CancellationTokenSource _acceptClientTokenSource;//接受客户端连接的token 23 | private readonly IPEndPoint _localEndPoint; //服务端本地启动的终结点 24 | private readonly ILogger _logger; //日志对象 25 | 26 | private readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); 27 | 28 | private Task _accetpClientsTask = null; 29 | private Task _checkIdleSessionsTask = null; 30 | private IPackageFilter _receivePackageFilter = null; //接收数据包的拦截处理器 31 | /// 32 | /// 创建一个Tcp服务对象 33 | /// 34 | /// 监听的端口 35 | /// 36 | /// 监听的host 37 | /// 1.null or string.empty 默认监听所有的网卡地址 38 | /// 2.如果是域名,转换为ip地址 39 | /// 40 | public EasyTcpServer(ushort port, string host = null) 41 | { 42 | if (port < 0 || port > 65535) 43 | throw new InvalidDataException("Unexcepted port number!"); 44 | 45 | if (string.IsNullOrEmpty(host) || host.Trim() == "*") 46 | { 47 | _ipAddress = IPAddress.Any; 48 | } 49 | else 50 | { 51 | _ipAddress = IPAddress.TryParse(host, out var tempAddress) ? 52 | tempAddress : Dns.GetHostEntry(host).AddressList[0]; 53 | } 54 | 55 | _port = port; 56 | _localEndPoint = new IPEndPoint(_ipAddress, _port); 57 | IsListening = false; 58 | 59 | } 60 | 61 | /// 62 | /// 创建一个Tcp服务对象 63 | /// 64 | /// 服务器对象配置 65 | /// 66 | public EasyTcpServer(ushort port, EasyTcpServerOptions options, string host = null) : this(port, host) 67 | { 68 | _options = options; 69 | if (_options.IsSsl) 70 | { 71 | if (string.IsNullOrEmpty(_options.PfxCertFilename)) 72 | throw new ArgumentNullException(nameof(_options.PfxCertFilename)); 73 | 74 | if (string.IsNullOrEmpty(_options.PfxPassword)) 75 | { 76 | _certificate = new X509Certificate2(_options.PfxCertFilename); 77 | } 78 | else 79 | { 80 | _certificate = new X509Certificate2(_options.PfxCertFilename, _options.PfxPassword); 81 | } 82 | } 83 | _logger = options.LoggerFactory?.CreateLogger(); 84 | } 85 | 86 | /// 87 | /// 开启监听 88 | /// 89 | /// 90 | public void StartListen() 91 | { 92 | try 93 | { 94 | _startListenLock.Wait(); 95 | if (IsListening) 96 | throw new InvalidOperationException("Listener is running !"); 97 | 98 | StartSocketListen(); 99 | var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(_acceptClientTokenSource.Token, 100 | _lifecycleTokenSource.Token); 101 | 102 | //开始接受客户端请求 103 | _accetpClientsTask = Task.Factory 104 | .StartNew(AcceptClientAsync, tokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current); 105 | //开启客户端空闲检查 106 | if (_options.IdleSessionsCheck) 107 | { 108 | _checkIdleSessionsTask = Task.Factory 109 | .StartNew(CheckIdleConnectionsAsync, tokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current); 110 | } 111 | } 112 | catch (Exception ex) 113 | { 114 | if (ex is TaskCanceledException || ex is OperationCanceledException) 115 | { 116 | _logger?.LogInformation("Listener was canceled."); 117 | } 118 | else 119 | { 120 | throw; 121 | } 122 | } 123 | finally 124 | { 125 | _startListenLock.Release(); 126 | } 127 | } 128 | 129 | private void StartSocketListen() 130 | { 131 | _serverSocket = new Socket(_ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); 132 | if (_options.KeepAlive) 133 | _serverSocket.SetKeepAlive(_options.KeepAliveIntvl, _options.KeepAliveTime, _options.KeepAliveProbes); 134 | _serverSocket.NoDelay = _options.NoDelay; 135 | 136 | _lifecycleTokenSource = new CancellationTokenSource(); 137 | _acceptClientTokenSource = new CancellationTokenSource(); 138 | _serverSocket.Bind(_localEndPoint); 139 | if (_options.BacklogCount != null) 140 | { 141 | _serverSocket.Listen(_options.BacklogCount.Value); 142 | } 143 | else 144 | { 145 | _serverSocket.Listen(); 146 | } 147 | 148 | IsListening = true; 149 | } 150 | 151 | /// 152 | /// 添加接收数据的过滤处理器 153 | /// 154 | /// 155 | public void SetReceiveFilter(IPackageFilter filter) 156 | { 157 | if (filter == null) 158 | return; 159 | 160 | _receivePackageFilter = filter; 161 | } 162 | 163 | /// 164 | /// 开启接收客户端的线程 165 | /// 166 | /// 167 | /// 已经取消或者没有启动监听后产生的异常 168 | private async Task AcceptClientAsync() 169 | { 170 | if (!IsListening) 171 | throw new InvalidOperationException(nameof(AcceptClientAsync) + ":listening status error"); 172 | 173 | while (!_acceptClientTokenSource.Token.IsCancellationRequested) 174 | { 175 | ClientSession clientSession = null; 176 | try 177 | { 178 | if (_options.ConnectionsLimit != null && _clients.Count >= _options.ConnectionsLimit) 179 | { 180 | await Task.Delay(200).ConfigureAwait(false); 181 | continue; 182 | } 183 | 184 | if (!IsListening) 185 | { 186 | StartSocketListen(); 187 | } 188 | 189 | var newClientSocket = await _serverSocket.AcceptAsync(_acceptClientTokenSource.Token); 190 | clientSession = new ClientSession(newClientSocket, _options.BufferSize, _options.MaxPipeBufferSize, _receivePackageFilter, OnReceivedData); 191 | if (_options.IsSsl) 192 | { 193 | CancellationTokenSource _sslTimeoutTokenSource = new CancellationTokenSource(); 194 | _sslTimeoutTokenSource.CancelAfter(TimeSpan.FromSeconds(3)); 195 | CancellationTokenSource sslTokenSource = CancellationTokenSource 196 | .CreateLinkedTokenSource(_acceptClientTokenSource.Token, _sslTimeoutTokenSource.Token); 197 | 198 | var result = await clientSession 199 | .SslAuthenticateAsync(_certificate, _options.AllowingUntrustedSSLCertificate, 200 | _options.MutuallyAuthenticate, _options.CheckCertificateRevocation, sslTokenSource.Token); 201 | 202 | if (!result) 203 | { 204 | await clientSession.DisposeAsync(); 205 | continue; 206 | } 207 | } 208 | 209 | if (_options.KeepAlive) newClientSocket.SetKeepAlive(_options.KeepAliveIntvl, _options.KeepAliveTime, _options.KeepAliveProbes); 210 | 211 | _clients.TryAdd(clientSession.RemoteEndPoint.ToString(), clientSession); 212 | clientSession.Connected = true; 213 | OnClientConnectionChanged?.Invoke(this, new ServerSideClientConnectionChangeEventArgs(clientSession, ConnectsionStatus.Connected)); 214 | var _ = Task.Factory.StartNew(async () => 215 | { 216 | await ReceiveClientDataAsync(clientSession); 217 | }); 218 | _logger?.LogInformation($"{clientSession.RemoteEndPoint}:connected."); 219 | 220 | if (_clients.Count >= _options.ConnectionsLimit) 221 | { 222 | _logger?.LogInformation($"The maximum number of connections has been exceeded"); 223 | _serverSocket.Close(); 224 | _serverSocket.Dispose(); 225 | IsListening = false; 226 | } 227 | } 228 | catch (Exception ex) 229 | { 230 | _logger?.LogError(ex.ToString()); 231 | if (ex is TaskCanceledException || ex is OperationCanceledException || ex is AuthenticationException) 232 | { 233 | if (clientSession != null) 234 | { 235 | await clientSession.DisposeAsync(); 236 | } 237 | } 238 | } 239 | } 240 | } 241 | 242 | /// 243 | /// 接收客户端信息 244 | /// 245 | /// 客户端会话对象 246 | /// 247 | private async Task ReceiveClientDataAsync(ClientSession clientSession) 248 | { 249 | try 250 | { 251 | await clientSession.ReceiveDataAsync(); 252 | } 253 | catch (SocketException ex) 254 | { 255 | _logger?.LogError($"Socket reeceive data error:{ex}"); 256 | } 257 | catch (TaskCanceledException ex) 258 | { 259 | _logger?.LogError($"Receive data task is canceled:{ex}"); 260 | } 261 | catch (OperationCanceledException ex) 262 | { 263 | _logger?.LogError($"Receive data task is canceled:{ex}"); 264 | } 265 | catch (Exception ex) 266 | { 267 | _logger?.LogError(ex.ToString()); 268 | } 269 | 270 | _clients.TryRemove(clientSession.RemoteEndPoint.ToString(), out var _); 271 | OnClientConnectionChanged?.Invoke(this, new ServerSideClientConnectionChangeEventArgs(clientSession, ConnectsionStatus.DisConnected)); 272 | await clientSession.DisposeAsync(); 273 | } 274 | 275 | /// 276 | /// 检查空闲的客户端连接 277 | /// 278 | /// 279 | private async Task CheckIdleConnectionsAsync() 280 | { 281 | while (!_lifecycleTokenSource.Token.IsCancellationRequested) 282 | { 283 | var expireTime = DateTime.UtcNow.AddMilliseconds(-1 * _options.CheckSessionsIdleMs); 284 | foreach (var clientEntry in _clients) 285 | { 286 | if (clientEntry.Value.LastActiveTime <= expireTime) 287 | { 288 | // 关闭空闲连接 289 | await DisconnectClientAsync(clientEntry.Value); 290 | } 291 | } 292 | 293 | await Task.Delay(1000).ConfigureAwait(false); 294 | } 295 | } 296 | 297 | public async Task DisconnectClientAsync(ClientSession clientSession) 298 | { 299 | if (!_clients.TryGetValue(clientSession.RemoteEndPoint.ToString(), out var temp)) 300 | { 301 | _logger?.LogInformation("ClientSession does not exist when wanna DisconnectClient"); 302 | } 303 | 304 | if (clientSession != null) 305 | { 306 | if (!clientSession._lifecycleTokenSource.IsCancellationRequested) 307 | { 308 | clientSession._lifecycleTokenSource.Cancel(); 309 | } 310 | 311 | await clientSession.DisposeAsync(); 312 | } 313 | } 314 | 315 | /// 316 | /// Stop accepting new connections. 317 | /// 318 | public async Task CloseAsync() 319 | { 320 | if (!IsListening) 321 | return; 322 | 323 | IsListening = false; 324 | _serverSocket.Close(); 325 | _serverSocket.Dispose(); 326 | _acceptClientTokenSource?.Cancel(); 327 | _lifecycleTokenSource?.Cancel(); 328 | 329 | foreach (var clients in _clients) 330 | { 331 | await clients.Value.DisposeAsync(); 332 | } 333 | 334 | _clients.Clear(); 335 | } 336 | 337 | #region send data 338 | public async Task SendAsync(string sessionId, byte[] data) 339 | { 340 | var sessions = 341 | _clients.Where(x => x.Value.SessionId == sessionId); 342 | 343 | await Parallel.ForEachAsync(sessions, _lifecycleTokenSource.Token, async (item, token) => 344 | { 345 | if (!token.IsCancellationRequested) 346 | { 347 | await item.Value.SendAsync(data); 348 | } 349 | }).ConfigureAwait(false); 350 | } 351 | 352 | public async Task SendAsync(string sessionId, Memory data) 353 | { 354 | var sessions = 355 | _clients.Where(x => x.Value.SessionId == sessionId); 356 | 357 | await Parallel.ForEachAsync(sessions, _lifecycleTokenSource.Token, async (item, token) => 358 | { 359 | if (!token.IsCancellationRequested) 360 | { 361 | await item.Value.SendAsync(data); 362 | } 363 | }).ConfigureAwait(false); 364 | } 365 | 366 | public async Task SendAsync(IPEndPoint endpoint, byte[] data) 367 | { 368 | var result = 369 | _clients.TryGetValue(endpoint.ToString(), out var client); 370 | 371 | if (result) 372 | { 373 | await client.SendAsync(data); 374 | } 375 | } 376 | 377 | public async Task SendAsync(IPEndPoint endpoint,Memory data) 378 | { 379 | var result = 380 | _clients.TryGetValue(endpoint.ToString(), out var client); 381 | 382 | if (result) 383 | { 384 | await client.SendAsync(data); 385 | } 386 | } 387 | 388 | public async Task SendAsync(ClientSession session, byte[] data) 389 | { 390 | if (_clients.Where(x => x.Value == session).Count() > 0) 391 | { 392 | await session.SendAsync(data); 393 | } 394 | } 395 | 396 | public async Task SendAsync(ClientSession session, Memory data) 397 | { 398 | if (_clients.Where(x => x.Value == session).Count() > 0) 399 | { 400 | await session.SendAsync(data); 401 | } 402 | } 403 | #endregion 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /EasyTcp4Net/EasyTcpClient.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System.Buffers; 3 | using System.IO.Pipelines; 4 | using System.Net; 5 | using System.Net.NetworkInformation; 6 | using System.Net.Security; 7 | using System.Net.Sockets; 8 | using System.Security.Cryptography.X509Certificates; 9 | 10 | namespace EasyTcp4Net 11 | { 12 | /// 13 | /// tcp 客户端类 14 | /// 15 | public class EasyTcpClient 16 | { 17 | public bool IsConnected { get; private set; } //客户端是否已经连接上服务 18 | public event EventHandler OnReceivedData; 19 | public event EventHandler OnDisConnected; 20 | private readonly IPAddress _serverIpAddress = null; //服务端的ip地址 21 | private Socket _socket; //客户端本地套接字 22 | private readonly EasyTcpClientOptions _options = new();//客户端总配置 23 | private readonly X509Certificate2 _certificate;//ssl证书对象 24 | private readonly SemaphoreSlim _connectLock = new SemaphoreSlim(1); //开启连接的信号量 25 | private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1); //发送数据的信号量 26 | private CancellationTokenSource _lifecycleTokenSource; //整个客户端存活的token 27 | private CancellationTokenSource _receiveDataTokenSource;//客户端获取数据的token 28 | public IPEndPoint RemoteEndPoint { get; private set; } //服务端的终结点 29 | public IPEndPoint LocalEndPoint { get; private set; } //客户端本地的终结点 30 | private readonly ILogger _logger; //日志对象 31 | private Task _dataReceiveTask = null; 32 | private Task _processDataTask = null; 33 | private NetworkStream _networkStream; 34 | private SslStream _sslStream; 35 | private Pipe _pipe; 36 | private PipeReader _pipeReader => _pipe.Reader; 37 | private PipeWriter _pipeWriter => _pipe.Writer; 38 | private IPackageFilter _receivePackageFilter = null; //接收数据包的拦截处理器 39 | 40 | /// 41 | /// 创建一个Tcp服务对象 42 | /// 43 | /// 监听的端口 44 | /// 45 | /// 监听的host 46 | /// 1.null or string.empty 默认监听所有的网卡地址 47 | /// 2.如果是域名,转换为ip地址 48 | /// 49 | public EasyTcpClient(string serverHost, ushort serverPort) 50 | { 51 | if (string.IsNullOrEmpty(serverHost)) 52 | { 53 | throw new ArgumentNullException(nameof(serverHost)); 54 | } 55 | 56 | if (serverPort > 65535 || serverPort <= 0) 57 | { 58 | throw new InvalidDataException("Server port is invalid."); 59 | } 60 | 61 | if (serverHost.Trim() == "*") 62 | { 63 | _serverIpAddress = IPAddress.Any; 64 | } 65 | else 66 | { 67 | _serverIpAddress = IPAddress.TryParse(serverHost, out var tempAddress) ? 68 | tempAddress : Dns.GetHostEntry(serverHost).AddressList[0]; 69 | } 70 | 71 | LocalEndPoint = new IPEndPoint(IPAddress.Any, 0); 72 | RemoteEndPoint = new IPEndPoint(_serverIpAddress, serverPort); 73 | IsConnected = false; 74 | } 75 | 76 | /// 77 | /// 创建一个Tcp服务对象 78 | /// 79 | /// 服务器对象配置 80 | /// 81 | public EasyTcpClient(string serverHost, ushort serverPort, EasyTcpClientOptions options) : this(serverHost, serverPort) 82 | { 83 | _options = options; 84 | if (_options.IsSsl) 85 | { 86 | if (!string.IsNullOrEmpty(_options.PfxCertFilename)) 87 | { 88 | if (string.IsNullOrEmpty(_options.PfxPassword)) 89 | { 90 | _certificate = new X509Certificate2(_options.PfxCertFilename); 91 | } 92 | else 93 | { 94 | _certificate = new X509Certificate2(_options.PfxCertFilename, _options.PfxPassword); 95 | } 96 | } 97 | } 98 | 99 | _logger = options.LoggerFactory?.CreateLogger(); 100 | } 101 | 102 | /// 103 | /// 客户端连接服务端 104 | /// 105 | /// 106 | /// 连接错误 107 | /// 连接超时 108 | public async Task ConnectAsync() 109 | { 110 | _connectLock.Wait(); 111 | try 112 | { 113 | if (IsConnected) 114 | return; 115 | 116 | _socket = new Socket(IPAddress.Any.AddressFamily, SocketType.Stream, ProtocolType.Tcp); 117 | if (_options.KeepAlive) 118 | _socket.SetKeepAlive(_options.KeepAliveIntvl, _options.KeepAliveTime, _options.KeepAliveProbes); 119 | _socket.NoDelay = _options.NoDelay; 120 | 121 | _pipe = new Pipe(new PipeOptions(pauseWriterThreshold: _options.MaxPipeBufferSize)); 122 | _lifecycleTokenSource = new CancellationTokenSource(); 123 | int retryTimes = 0; 124 | while (retryTimes <= _options.ConnectRetryTimes) 125 | { 126 | try 127 | { 128 | retryTimes++; 129 | CancellationTokenSource connectTokenSource = new CancellationTokenSource(); 130 | connectTokenSource.CancelAfter(_options.ConnectTimeout); 131 | await _socket.ConnectAsync(RemoteEndPoint, connectTokenSource.Token); 132 | _networkStream = new NetworkStream(_socket); 133 | _networkStream.ReadTimeout = _options.ReadTimeout; 134 | _networkStream.WriteTimeout = _options.WriteTimeout; 135 | if (_options.IsSsl) 136 | { 137 | if (_options.AllowingUntrustedSSLCertificate || _certificate == null) 138 | { 139 | _sslStream = new SslStream(_networkStream, false, 140 | (obj, certificate, chain, error) => true); 141 | } 142 | else 143 | { 144 | _sslStream = new SslStream(_networkStream, false); 145 | } 146 | 147 | _sslStream.ReadTimeout = _options.ReadTimeout; 148 | _sslStream.WriteTimeout = _options.WriteTimeout; 149 | 150 | if (_certificate != null) 151 | { 152 | await _sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions() 153 | { 154 | TargetHost = RemoteEndPoint.Address.ToString(), 155 | EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12, 156 | CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck, 157 | ClientCertificates = new X509CertificateCollection() { _certificate } 158 | }, connectTokenSource.Token).ConfigureAwait(false); 159 | } 160 | else//客户端不提供证书 161 | { 162 | await _sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions() 163 | { 164 | TargetHost = "test" 165 | }, connectTokenSource.Token).ConfigureAwait(false); 166 | } 167 | if (!_sslStream.IsEncrypted || !_sslStream.IsAuthenticated) 168 | { 169 | throw new InvalidOperationException("SSL authenticated faild!"); 170 | } 171 | } 172 | 173 | break; 174 | } 175 | catch (Exception ex) 176 | { 177 | try 178 | { 179 | await _socket.DisconnectAsync(true); 180 | } 181 | catch { } 182 | _logger?.LogError($"Connecting {retryTimes} times is faild:{ex}"); 183 | if (_options.ConnectRetryTimes < retryTimes) 184 | throw; 185 | else 186 | { 187 | continue; 188 | } 189 | } 190 | } 191 | } 192 | catch (TaskCanceledException) 193 | { 194 | throw new TimeoutException("Connect remote host was canceled because of timeout !"); 195 | } 196 | catch (OperationCanceledException) 197 | { 198 | throw new TimeoutException("Connect remote host timeout!"); 199 | } 200 | catch (Exception ex) 201 | { 202 | _logger?.LogError(ex.ToString()); 203 | throw; 204 | } 205 | finally 206 | { 207 | _connectLock.Release(); 208 | } 209 | 210 | _receiveDataTokenSource = new CancellationTokenSource(); 211 | IsConnected = true; 212 | CancellationTokenSource ctx = CancellationTokenSource.CreateLinkedTokenSource(_lifecycleTokenSource.Token, _receiveDataTokenSource.Token); 213 | _processDataTask = 214 | Task.Factory.StartNew(ReadPipeAsync, _lifecycleTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current); 215 | _dataReceiveTask = Task.Factory.StartNew(ReceiveDataAsync, 216 | ctx.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current); 217 | } 218 | 219 | /// 220 | /// 客户端循环读取数据 221 | /// 222 | /// 223 | private async Task ReceiveDataAsync() 224 | { 225 | DisConnectReason disConnectReason = default; 226 | while (!_receiveDataTokenSource.Token.IsCancellationRequested) 227 | { 228 | try 229 | { 230 | Memory buffer = _pipeWriter.GetMemory(_options.BufferSize); 231 | int readCount = 0; 232 | if (_options.IsSsl) 233 | { 234 | readCount = await _sslStream.ReadAsync(buffer, _lifecycleTokenSource.Token).ConfigureAwait(false); 235 | } 236 | else 237 | { 238 | readCount = await _networkStream.ReadAsync(buffer, _lifecycleTokenSource.Token).ConfigureAwait(false); 239 | } 240 | 241 | if (readCount > 0) 242 | { 243 | var data = buffer.Slice(0, readCount); 244 | _pipeWriter.Advance(readCount); 245 | } 246 | else 247 | { 248 | if (!IsConnecting()) 249 | { 250 | throw new SocketException(); 251 | } 252 | } 253 | 254 | FlushResult result = await _pipeWriter.FlushAsync().ConfigureAwait(false); 255 | if (result.IsCompleted) 256 | { 257 | break; 258 | } 259 | } 260 | catch (TaskCanceledException) 261 | { 262 | _logger?.LogError($"Task is canceled."); 263 | disConnectReason = DisConnectReason.Normol; 264 | break; 265 | } 266 | catch (Exception ex) 267 | { 268 | _logger?.LogError($"{ex}"); 269 | if (ex is SocketException || ex is IOException) 270 | { 271 | disConnectReason = DisConnectReason.ServerDown; 272 | } 273 | else 274 | { 275 | disConnectReason = DisConnectReason.UnKnown; 276 | } 277 | break; 278 | } 279 | } 280 | 281 | _pipeWriter.Complete(); 282 | await DisConnectInternalAsync(disConnectReason); 283 | } 284 | 285 | internal async Task ReadPipeAsync() 286 | { 287 | while (!_lifecycleTokenSource.Token.IsCancellationRequested) 288 | { 289 | ReadResult result = await _pipeReader.ReadAsync(); 290 | ReadOnlySequence buffer = result.Buffer; 291 | ReadOnlySequence data; 292 | do 293 | { 294 | if (_receivePackageFilter != null) 295 | { 296 | data = _receivePackageFilter.ResolvePackage(ref buffer); 297 | } 298 | else 299 | { 300 | data = buffer; 301 | buffer = buffer.Slice(data.Length); 302 | } 303 | 304 | if (!data.IsEmpty) 305 | { 306 | OnReceivedData?.Invoke(this, new ClientDataReceiveEventArgs(data.ToArray())); 307 | } 308 | } 309 | while (!data.IsEmpty && buffer.Length > 0); 310 | _pipeReader.AdvanceTo(buffer.Start); 311 | } 312 | 313 | _pipeReader.Complete(); 314 | } 315 | 316 | /// 317 | /// 添加接收数据的过滤处理器 318 | /// 319 | /// 320 | public void SetReceiveFilter(IPackageFilter filter) 321 | { 322 | if (filter == null) 323 | return; 324 | 325 | _receivePackageFilter = filter; 326 | } 327 | 328 | public async Task SendAsync(byte[] data) 329 | { 330 | if (data == null || data.Length < 1) 331 | throw new ArgumentNullException(nameof(data)); 332 | 333 | if (!IsConnected) throw new InvalidOperationException("Connection is disconnected"); 334 | await SendInternalAsync(data); 335 | } 336 | 337 | public async Task SendAsync(Memory data) 338 | { 339 | if (data.IsEmpty || data.Length < 1) 340 | throw new ArgumentNullException(nameof(data)); 341 | 342 | if (!IsConnected) throw new InvalidOperationException("Connection is disconnected"); 343 | await SendInternalAsync(data); 344 | } 345 | 346 | private async Task SendInternalAsync(Memory data) 347 | { 348 | int bytesRemaining = data.Length; 349 | int index = 0; 350 | 351 | try 352 | { 353 | _sendLock.Wait(); 354 | while (bytesRemaining > 0) 355 | { 356 | Memory needSendData = null; 357 | if (bytesRemaining >= _options.BufferSize) 358 | { 359 | needSendData = data.Slice(index, _options.BufferSize); 360 | } 361 | else 362 | { 363 | needSendData = data.Slice(index, bytesRemaining); 364 | } 365 | if (_options.IsSsl) 366 | { 367 | await _sslStream.WriteAsync(needSendData, _lifecycleTokenSource.Token); 368 | } 369 | else 370 | { 371 | await _networkStream.WriteAsync(needSendData, _lifecycleTokenSource.Token); 372 | } 373 | index += needSendData.Length; 374 | bytesRemaining -= needSendData.Length; 375 | } 376 | } 377 | catch (IOException ex) 378 | { 379 | OnDisConnected?.Invoke(this, 380 | new ClientSideDisConnectEventArgs(DisConnectReason.ServerDown)); 381 | _logger?.LogError(ex.ToString()); 382 | 383 | throw; 384 | } 385 | catch (Exception ex) 386 | { 387 | if (ex is TaskCanceledException || ex is OperationCanceledException) 388 | { 389 | _logger?.LogError("Send message operation was canceled."); 390 | } 391 | 392 | _logger?.LogError(ex.ToString()); 393 | 394 | throw; 395 | } 396 | finally 397 | { 398 | _sendLock.Release(); 399 | } 400 | } 401 | 402 | private bool IsConnecting() 403 | { 404 | IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties(); 405 | TcpConnectionInformation[] tcpConnections = ipProperties.GetActiveTcpConnections() 406 | .Where(x => x.LocalEndPoint.Equals(LocalEndPoint) && x.RemoteEndPoint.Equals(RemoteEndPoint)).ToArray(); 407 | 408 | if (tcpConnections != null && tcpConnections.Length > 0) 409 | { 410 | TcpState stateOfConnection = tcpConnections.First().State; 411 | if (stateOfConnection == TcpState.Established) 412 | { 413 | return true; 414 | } 415 | } 416 | 417 | return false; 418 | } 419 | 420 | /// 421 | /// 客户端断连 422 | /// 423 | private async Task DisConnectInternalAsync(DisConnectReason disConnectReason) 424 | { 425 | if (!IsConnected) 426 | { 427 | _logger?.LogInformation($"Already disconnected"); 428 | return; 429 | } 430 | 431 | _receiveDataTokenSource?.Cancel(); 432 | _lifecycleTokenSource?.Cancel(); 433 | await _dataReceiveTask; 434 | await _processDataTask; 435 | _socket?.Close(); 436 | _socket?.Dispose(); 437 | _dataReceiveTask?.Dispose(); 438 | _processDataTask?.Dispose(); 439 | IsConnected = false; 440 | OnDisConnected?.Invoke(this, new ClientSideDisConnectEventArgs(disConnectReason: disConnectReason)); 441 | } 442 | 443 | /// 444 | /// 客户端断连 445 | /// 446 | public async Task DisConnectAsync() 447 | { 448 | await DisConnectInternalAsync(DisConnectReason.Normol); 449 | } 450 | 451 | /// 452 | /// For单元测试 453 | /// 454 | public X509Certificate2 Certificate => _certificate;//ssl证书对象 455 | } 456 | } 457 | --------------------------------------------------------------------------------