socket
HPSocket
public class TcpBaseMethod { ///// <summary> ///// 线程池 ///// </summary> //private static readonly HPSocket.Thread.ThreadPool ThreadPool = new HPSocket.Thread.ThreadPool(); public event Action<string> ReciveEvent; private ITcpServer<string> server; private ConcurrentDictionary<IntPtr, IntPtr> connList = new ConcurrentDictionary<IntPtr, IntPtr>(); public TcpBaseMethod(string ip, ushort port) { // TcpServer-TestEcho-Adapter 适配 TcpClient-TestEcho 和 TcpAgent-TestEcho, 可以使用这两个客户端连接到当前服务端 IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties(); IPEndPoint[] ipEndPoints = ipProperties.GetActiveTcpListeners(); foreach (var item in ipEndPoints) { if (item.Port == port) { //端口已有监听,不需要再次打开 return; } } server = new TcpServer<string> { Address = ip, Port = port, // 指定数据接收适配器 DataReceiveAdapter = new PacketDataReceiveAdapter(), }; { // 准备监听事件 server.OnPrepareListen += Server_OnPrepareListen; // 连接进入 server.OnAccept += Server_OnAccept; // 不需要在绑定OnReceive事件 // 这里解析请求体事件的数据,就是PacketDataReceiveAdapter.ParseRequestBody()返回的数据 server.OnParseRequestBody += Server_OnParseRequestBody; // 连接关闭 server.OnClose += Server_OnClose; // 启动服务 server.Start(); //// 等待线程池任务全部结束 //ThreadPool.Wait(); //// 等待服务结束 //server.Wait(); //timer.Stop(); //timer.Close(); } } private HandleResult Server_OnClose(IServer sender, IntPtr connId, SocketOperation socketOperation, int errorCode) { Console.WriteLine($"OnClose({connId}) -> operation: {socketOperation}, code: {errorCode}"); IntPtr ptr; connList.TryRemove(connId, out ptr); return HandleResult.Ok; } private HandleResult Server_OnParseRequestBody(ITcpServer sender, IntPtr connId, string obj) { return ProcessPacket(sender, connId, obj); } private HandleResult Server_OnPrepareListen(IServer sender, IntPtr listen) { Console.WriteLine($"OnPrepareListen({listen})"); return HandleResult.Ok; } private HandleResult Server_OnAccept(IServer sender, IntPtr connId, IntPtr client) { Console.WriteLine($"OnAccept({connId}) -> socket handle: {client}"); //add by hyq if (connList.Count(c => c.Key == connId) == 0) connList.TryAdd(connId, client); return HandleResult.Ok; } //private HandleResult Server_OnAccept(IServer sender, IntPtr connId, IntPtr client) //{ // Console.WriteLine($"OnAccept({connId}) -> socket handle: {client}"); // //add by hyq // if (connList.Count(c => c.Value == client) == 0) // connList.TryAdd(connId, client); // return HandleResult.Ok; //} private static readonly object objLock = new object(); /// <summary> /// packet对象转含包头的完整bytes /// </summary> /// <param name="packet"></param> /// <returns></returns> public bool ToSendBytes(object packet) { // 对象转json var json = JsonConvert.SerializeObject(packet); ////记录日志(测试用) //string filepath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"logs\\socketLog_{DateTime.Now.ToString("yyyyMMdd")}.log"); ////using (System.IO.FileStream fs = new System.IO.FileStream(filepath, System.IO.FileMode.OpenOrCreate)) ////{ // using (System.IO.StreamWriter br = new System.IO.StreamWriter(filepath,true)) // { // br.WriteLine(string.Format("[{0}{1}]", DateTime.Now.ToString("HH:mm:ss"), json)); // br.Flush(); // } ////} Console.WriteLine(string.Format("[{0}]send:{1}", DateTime.Now, json)); // json到bytes var data = Encoding.UTF8.GetBytes(json); // 包头转字节数组 int32为4字节,最大2147483647 if (data.Length > 2147483647) { // 过滤超过2147483647的数据,包头会大于4字节 return false; } var header = BitConverter.GetBytes(data.Length); // 两端字节序要保持一致 // 如果当前环境不是小端字节序 if (!BitConverter.IsLittleEndian) { // 翻转字节数组, 变为小端字节序 Array.Reverse(header); } //var bytesList = new List<byte>(); //// 加入包头 //bytesList.AddRange(header); //// 加入真实数据 //bytesList.AddRange(data); //// 包头+数据 var bytes = header.Concat(data).ToArray(); //string tempSendFile = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "socketSendTemp.dat"); //System.IO.File.WriteAllBytes(tempSendFile, bytes); ////var bytes = bytesList.ToArray(); lock (objLock) { //向所有发送 foreach (var obj in connList) { // 断开连接的跳过 if (!server.IsConnected(obj.Key)){ continue; } //6K一段的发送 int size = 6144; int nums = ((bytes.Length - 1) / size) + 1; for (var n = 0; n < nums; n++) { int start = n * size; var buffer = bytes.Skip(start).Take(size).ToArray(); if (!server.Send(obj.Key, buffer, buffer.Length)) { return false; } } //if(!server.Send(obj.Key, bytes, bytes.Length)) { // return false; //} } } //using (System.IO.FileStream fs = new System.IO.FileStream(@"C:\job\bin.bin", System.IO.FileMode.CreateNew)) //{ // using (System.IO.BinaryWriter br = new System.IO.BinaryWriter(fs)) // { // br.Write(bytes); // br.Flush(); // } //} return true; } /// <summary> /// 处理接收到的数据包 /// </summary> /// <param name="sender"></param> /// <param name="id"></param> /// <param name="packet"></param> /// <returns></returns> private HandleResult ProcessPacket(IServer sender, IntPtr id, string packet) { try { //记录日志(测试用) string filepath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"logs\\socketLog_{DateTime.Now.ToString("yyyyMMdd")}.log"); //using (System.IO.FileStream fs = new System.IO.FileStream(filepath, System.IO.FileMode.OpenOrCreate)) //{ using (System.IO.StreamWriter br = new System.IO.StreamWriter(filepath, true)) { br.WriteLine(string.Format("[{0}{1}]", DateTime.Now.ToString("HH:mm:ss"), packet)); br.Flush(); } } catch { } var result = HandleResult.Ok; //Console.WriteLine(packet); ReciveEvent(packet); //switch (packet.Type) //{ // case PacketType.Echo: // 假如回显是一个非耗时操作, 在这处理 // { // // 回显数据 // var data = PacketHelper.ToSendBytes(packet); // result = sender.Send(id, data, data.Length) ? HandleResult.Ok : HandleResult.Error; // break; // } // case PacketType.Time: // 假如获取服务器时间是耗时操作, 将该操作放入队列 // { // // 向线程池提交任务 // if (!ThreadPool.Submit(ThreadProc, new { sender, id, packet })) // 线程池传参, 这里使用的是匿名对象 // { // result = HandleResult.Error; // } // break; // } // default: // result = HandleResult.Error; // break; //} return result; } /// <summary> /// 是否启动 /// </summary> /// <returns></returns> public bool IsOpen() { return server != null && server.HasStarted ? true : false; } /// <summary> /// 关闭连接 /// </summary> /// <returns></returns> public void Close() { // 先关闭事件,避免资源无法释放 server.OnPrepareListen -= Server_OnPrepareListen; server.OnAccept -= Server_OnAccept; server.OnParseRequestBody -= Server_OnParseRequestBody; server.OnClose -= Server_OnClose; server.Stop(); server.Dispose(); } }
/// <summary> /// 固定包头数据接收适配器 /// </summary> public class PacketDataReceiveAdapter : FixedHeaderDataReceiveAdapter<string> { /// <summary> /// 调用父类构造函数,指定固定包头的长度及最大封包长度 /// </summary> public PacketDataReceiveAdapter() : base( headerSize: 4, // 这里指定4字节包头 maxPacketSize: 0 // 这里指定最大封包长度不能超过4K ) { } /// <summary> /// 获取请求体长度 /// <remarks>子类必须覆盖此方法</remarks> /// </summary> /// <param name="header">包头,header的长度是构造函数里指定的长度,当接收到了指定长度的包头再调用此方法</param> /// <returns>返回包体长度</returns> protected override int GetBodySize(byte[] header) { // 根据业务场景来适配字节序, 两端字节序要保持一致 // 如果当前环境不是小端字节序 if (!BitConverter.IsLittleEndian) { // 转换为小端字节序 Array.Reverse(header); } // 因为包头是4字节,所以直接转int并返回 return BitConverter.ToInt32(header, 0); } /// <summary> /// 解析请求体 /// <remarks>子类必须覆盖此方法</remarks> /// </summary> /// <param name="header">包头</param> /// <param name="data">包体</param> /// <returns></returns> protected override string ParseRequestBody(byte[] header, byte[] data) { // 将data反序列化为对象 // 这里是Packet类的对象 return Encoding.UTF8.GetString(data);//JsonConvert.DeserializeObject<Packet>(Encoding.UTF8.GetString(data)); } }
服务端处理业务:
TcpDevices.TcpHelper.TcpBaseMethod tcpServer; tcpServer = new TcpDevices.TcpHelper.TcpBaseMethod(dyIp, (ushort)dyPort); tcpServer.ReciveEvent += TcpServer_ReciveEvent; statusTimer = new System.Timers.Timer(); statusTimer.Elapsed += StatusTimer_Elapsed; statusTimer.Interval = 3 * 1000;//3S statusTimer.AutoReset = true; //处理业务 private void TcpServer_ReciveEvent(string obj) { Console.WriteLine(string.Format("[{0}]rev:{1}", DateTime.Now, Newtonsoft.Json.JsonConvert.SerializeObject(obj))); var res = Newtonsoft.Json.JsonConvert.DeserializeObject<CMDResultModel>(obj); if (res.cmd == FaceAntiSpoofingCMD.FA_WebCamera_OneToOne) { cmdQueue.TryAdd(FaceAntiSpoofingCMD.FA_WebCamera_OneToOne + "_" + res.data.TaskID, res); } else if (res.cmd == FaceAntiSpoofingCMD.FA_WebCamera_GetStatus) { _IsReady = res.code == 0; lastHeartbeatTime = DateTime.Now; } }
客户端类似:TcpClient
===============================================
以下位另外的例子:TcpClient可以配合TcpServer,TcpServer-AsyncQueue,TcpServer-Adapter 3种使用
TcpServer-Adapter
/// <summary> /// 固定包头长度数据适配器示例 /// </summary> class Program { /// <summary> /// 线程池 /// </summary> private static readonly HPSocket.Thread.ThreadPool ThreadPool = new HPSocket.Thread.ThreadPool(); static void Main(string[] args) { Console.Title = "TcpServer-TestEcho-Adapter"; // TcpServer-TestEcho-Adapter 适配 TcpClient-TestEcho 和 TcpAgent-TestEcho, 可以使用这两个客户端连接到当前服务端 using (ITcpServer<Packet> server = new TcpServer<Packet> { Address = "0.0.0.0", Port = 5555, // 指定数据接收适配器 DataReceiveAdapter = new PacketDataReceiveAdapter(), }) { // 定时器 var timer = new Timer(5000) { AutoReset = true, }; // 定时输出线程池任务数 timer.Elapsed += (sender, eventArgs) => { if (ThreadPool.HasStarted) { Console.WriteLine($"线程池当前在执行的任务数: {ThreadPool.TaskCount}, 任务队列数: {ThreadPool.QueueSize}"); } }; timer.Start(); // 准备监听事件 server.OnPrepareListen += (sender, listen) => { Console.WriteLine($"OnPrepareListen({listen})"); return HandleResult.Ok; }; // 连接进入 server.OnAccept += (sender, id, client) => { Console.WriteLine($"OnAccept({id}) -> socket handle: {client}"); return HandleResult.Ok; }; // 不需要在绑定OnReceive事件 // 这里解析请求体事件的packet就是PacketDataReceiveAdapter.ParseRequestBody()返回的数据 server.OnParseRequestBody += (sender, id, packet) => { Console.WriteLine($"OnParseRequestBody({id}) -> type: {packet.Type}, data: {packet.Data}"); // 处理业务逻辑 return ProcessPacket(sender, id, packet); }; // 连接关闭 server.OnClose += (sender, id, operation, code) => { Console.WriteLine($"OnClose({id}) -> operation: {operation}, code: {code}"); return HandleResult.Ok; }; // 启动线程池 ThreadPool.Start(2, HPSocket.Thread.RejectedPolicy.WaitFor); // 启动服务 server.Start(); // 等待线程池任务全部结束 ThreadPool.Wait(); // 等待服务结束 server.Wait(); timer.Stop(); timer.Close(); } } private static HandleResult ProcessPacket(IServer sender, IntPtr id, Packet packet) { var result = HandleResult.Ignore; switch (packet.Type) { case PacketType.Echo: // 假如回显是一个非耗时操作, 在这处理 { // 回显数据 var data = PacketHelper.ToSendBytes(packet); result = sender.Send(id, data, data.Length) ? HandleResult.Ok : HandleResult.Error; break; } case PacketType.Time: // 假如获取服务器时间是耗时操作, 将该操作放入队列 { // 向线程池提交任务 if (!ThreadPool.Submit(ThreadProc, new { sender, id, packet })) // 线程池传参, 这里使用的是匿名对象 { result = HandleResult.Error; } break; } default: result = HandleResult.Error; break; } return result; } /// <summary> /// 线程池回调函数 /// </summary> /// <param name="obj"></param> static void ThreadProc(object obj) { // 解构动态对象 dynamic dyObj = obj; IServer sender = dyObj.sender; IntPtr id = dyObj.id; Packet packet = dyObj.packet; // 如果连接已经断开了(可能被踢了) // 它的任务就不做了(根据自己业务需求来, 也许你的任务就是要完成每个连接的所有任务, 每个包都要处理, 不管连接断开与否, 就不要写这个判断, 但是你回发包的时候要判断是否有效连接) if (!sender.IsConnected(id)) { return; } if (packet.Type != PacketType.Time) { return; } // 模拟耗时操作 System.Threading.Thread.Sleep(6000); // 组织回包 packet = new Packet { Type = PacketType.Time, Data = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), }; // 发送数据 var data = PacketHelper.ToSendBytes(packet); if (!sender.Send(id, data, data.Length)) { // 发送失败断开连接 sender.Disconnect(id); } } }
/// <summary> /// 固定包头数据接收适配器 /// </summary> public class PacketDataReceiveAdapter : FixedHeaderDataReceiveAdapter<Packet> { /// <summary> /// 构造函数调用父类构造, 指定固定包头的长度及最大封包长度 /// </summary> public PacketDataReceiveAdapter() : base( headerSize: 4,// 这里指定4字节包头 maxPacketSize: 0x1000 // 这里指定最大封包长度不能超过4K ) { } /// <summary> /// 获取请求体长度方法 /// <remarks>子类必须覆盖此方法</remarks> /// </summary> /// <param name="header">包头, header的长度是构造函数里指定的长度, 构造函数里指定包头长度多少, 父类就等到了指定长度在调用此方法</param> /// <returns>返回真实长度</returns> protected override int GetBodySize(byte[] header) { // 适配大小端请根据自己的业务场景来, 两端字节序要保持一致 // 如果当前环境不是小端字节序 if (!BitConverter.IsLittleEndian) { // 翻转字节数组, 变为小端字节序 Array.Reverse(header); } // 因为我是4字节包头, 所以直接转int并返回 return BitConverter.ToInt32(header, 0); } /// <summary> /// 解析请求体 /// <remarks>子类必须覆盖此方法</remarks> /// </summary> /// <param name="header">包头</param> /// <param name="data">包体</param> /// <returns></returns> protected override Packet ParseRequestBody(byte[] header, byte[] data) { // 将data转换为泛型传入的类型对象 // 我这里是Packet类的对象 return JsonConvert.DeserializeObject<Packet>(Encoding.UTF8.GetString(data)); } }
model
/// <summary> /// 封包 /// </summary> public class Packet { /// <summary> /// 封包类型 /// </summary> public PacketType Type { get; set; } /// <summary> /// 数据 /// </summary> public string Data { get; set; } }
======
TcpServer-AsyncQueue
public partial class FormServer : Form { delegate void AddLogHandler(string log); #pragma warning disable IDE0069 // 应释放可释放的字段 private readonly ITcpServer _server = new TcpServer(); /// <summary> /// 异步消费者队列 /// </summary> private AsyncQueue<TaskInfo> _asyncQueue; /// <summary> /// 定时器 /// </summary> private readonly Timer _timer = new Timer(5000) { AutoReset = true, }; #pragma warning restore IDE0069 // 应释放可释放的字段 /// <summary> /// 最大封包长度 /// </summary> private const int MaxPacketSize = 4096; public FormServer() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { // 演示设置server属性 // 缓冲区大小应根据实际业务设置, 并发数和包大小都是考虑条件 // 都是小包的情况下4K合适, 刚好是一个内存分页(在非托管内存, 相关知识查百度) // 大包比较多的情况下, 应根据并发数设置比较大但是又不会爆内存的值 _server.SocketBufferSize = 4096; // 4K // server绑定地址和端口 _server.Address = "0.0.0.0"; _server.Port = 5555; // 演示绑定事件 _server.OnPrepareListen += OnPrepareListen; _server.OnAccept += OnAccept; _server.OnReceive += OnReceive; _server.OnClose += OnClose; _server.OnShutdown += OnShutdown; _asyncQueue = new AsyncQueue<TaskInfo>(2, TaskTaskProc); // 定时输出线程池任务数 _timer.Elapsed += (_, args) => { if (_server.HasStarted) { AddLog($"异步消费者队列的消费者数: {_asyncQueue.ConsumerCount}, 未执行任务队列数: {_asyncQueue.Count}"); } }; _timer.Start(); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { if (_server.HasStarted) { MessageBox.Show(@"请先停止服务", @"服务正在运行:", MessageBoxButtons.OK, MessageBoxIcon.Warning); e.Cancel = true; return; } // 停止消费并释放资源 _asyncQueue.Dispose(); // 停止并释放定时器 _timer.Stop(); _timer.Close(); // 停止释放服务器 _server.Dispose(); e.Cancel = false; } // ReSharper disable once InconsistentNaming private HandleResult OnPrepareListen(IServer sender, IntPtr listen) { AddLog($"OnPrepareListen({sender.Address}:{sender.Port}), listen: {listen}, hp-socket version: {sender.Version}"); return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnAccept(IServer sender, IntPtr connId, IntPtr client) { // 获取客户端地址 if (!sender.GetRemoteAddress(connId, out var ip, out var port)) { return HandleResult.Error; } AddLog($"OnAccept({connId}), ip: {ip}, port: {port}"); // 设置附加数据, 用来做粘包处理 sender.SetExtra(connId, new ClientInfo { ConnId = connId, PacketData = new List<byte>(), }); return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnReceive(IServer sender, IntPtr connId, byte[] data) { AddLog($"OnReceive({connId}), data length: {data.Length}"); var client = sender.GetExtra<ClientInfo>(connId); if (client == null) { return HandleResult.Error; } client.PacketData.AddRange(data); // 总长度小于包头 if (client.PacketData.Count < sizeof(int)) { return HandleResult.Ok; } HandleResult result; const int headerLength = sizeof(int); do { // 取头部字节得到包头 var packetHeader = client.PacketData.GetRange(0, headerLength).ToArray(); // 两端字节序要保持一致 // 如果当前环境不是小端字节序 if (!BitConverter.IsLittleEndian) { // 翻转字节数组, 变为小端字节序 Array.Reverse(packetHeader); } // 得到包头指向的数据长度 var dataLength = BitConverter.ToInt32(packetHeader, 0); // 完整的包长度(含包头和完整数据的大小) var fullLength = dataLength + headerLength; if (dataLength < 0 || fullLength > MaxPacketSize) { result = HandleResult.Error; break; } // 如果来的数据小于一个完整的包 if (client.PacketData.Count < fullLength) { // 下次数据到达处理 result = HandleResult.Ignore; break; } // 是不是一个完整的包(包长 = 实际数据长度 + 包头长度) if (client.PacketData.Count == fullLength) { // 得到完整包并处理 var fullData = client.PacketData.GetRange(headerLength, dataLength).ToArray(); result = OnProcessFullPacket(sender, client, fullData); // 清空缓存 client.PacketData.Clear(); break; } // 如果来的数据比一个完整的包长 if (client.PacketData.Count > fullLength) { // 先得到完整包并处理 var fullData = client.PacketData.GetRange(headerLength, dataLength).ToArray(); result = OnProcessFullPacket(sender, client, fullData); if (result == HandleResult.Error) { break; } // 再移除已经读了的数据 client.PacketData.RemoveRange(0, fullLength); // 剩余的数据下个循环处理 } } while (true); return result; } // ReSharper disable once InconsistentNaming private HandleResult OnClose(IServer sender, IntPtr connId, SocketOperation socketOperation, int errorCode) { AddLog($"OnClose({connId}), socket operation: {socketOperation}, error code: {errorCode}"); var client = sender.GetExtra<ClientInfo>(connId); if (client != null) { sender.RemoveExtra(connId); client.PacketData.Clear(); return HandleResult.Error; } return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnShutdown(IServer sender) { AddLog($"OnShutdown({sender.Address}:{sender.Port})"); return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnProcessFullPacket(IServer sender, ClientInfo client, byte[] data) { // 这里来的都是完整的包, 但是这里不做耗时操作, 仅把数据放入队列 var packet = JsonConvert.DeserializeObject<Packet>(Encoding.UTF8.GetString(data)); var result = HandleResult.Ok; switch (packet.Type) { case PacketType.Echo: // 假如回显是一个非耗时操作, 在这处理 { // 组织packet为一个json var json = JsonConvert.SerializeObject(new Packet { Type = packet.Type, Data = packet.Data, }); // AddLog($"{DateTime.Now}:{packet}...."); // json转字节数组 var bytes = Encoding.UTF8.GetBytes(json); // 先发包头 if (!SendPacketHeader(sender, client.ConnId, bytes.Length)) { result = HandleResult.Error; break; } // 再发实际数据 if (!sender.Send(client.ConnId, bytes, bytes.Length)) { result = HandleResult.Error; } // 至此完成回显 break; } case PacketType.Time: // 假如获取服务器时间是耗时操作, 将该操作放入队列 { // 向异步队列添加数据 if (!_asyncQueue.Enqueue(new TaskInfo { Client = client, Packet = packet, })) { result = HandleResult.Error; } break; } default: result = HandleResult.Error; break; } return result; } /// <summary> /// 线程池任务回调函数 /// </summary> /// <param name="taskInfo">任务参数</param> private void TaskTaskProc(TaskInfo taskInfo) { // 如果连接已经断开了(可能被踢了) // 它的任务就不做了(根据自己业务需求来, 也许你的任务就是要完成每个连接的所有任务, 每个包都要处理, 不管连接断开与否, 就不要写这个判断, 但是你回发包的时候要判断是否有效连接) if (!_server.IsConnected(taskInfo.Client.ConnId)) { return; } // 在这里处理耗时任务逻辑 switch (taskInfo.Packet.Type) { case PacketType.Time: { // 模拟耗时操作 Thread.Sleep(6000); // 组织packet为一个json var json = JsonConvert.SerializeObject(new Packet { Type = PacketType.Time, Data = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), }); // json转字节数组 var bytes = Encoding.UTF8.GetBytes(json); // 先发包头 if (!SendPacketHeader(_server, taskInfo.Client.ConnId, bytes.Length)) { // 发送失败断开连接 _server.Disconnect(taskInfo.Client.ConnId); break; } // 再发实际数据 if (!_server.Send(taskInfo.Client.ConnId, bytes, bytes.Length)) { _server.Disconnect(taskInfo.Client.ConnId); } // 至此完成当前任务 break; } } } /// <summary> /// 发送包头 /// <para>固定包头网络字节序</para> /// </summary> /// <param name="sender">服务器组件</param> /// <param name="connId">连接id</param> /// <param name="dataLength">实际数据长度</param> /// <returns></returns> private bool SendPacketHeader(IServer sender, IntPtr connId, int dataLength) { // 包头转字节数组 var packetHeaderBytes = BitConverter.GetBytes(dataLength); // 两端字节序要保持一致 // 如果当前环境不是小端字节序 if (!BitConverter.IsLittleEndian) { // 翻转字节数组, 变为小端字节序 Array.Reverse(packetHeaderBytes); } return sender.Send(connId, packetHeaderBytes, packetHeaderBytes.Length); } private async void BtnSwitchService_Click(object sender, EventArgs e) { try { if (btnSwitchService.Text == @"启动") { // 启动服务 if (!_server.Start()) { throw new Exception($"error code: {_server.ErrorCode}, error message: {_server.ErrorMessage}"); } btnSwitchService.Text = @"停止"; // 等待服务停止 await _server.WaitAsync(); // 停止以后还原按钮标题 btnSwitchService.Text = @"启动"; btnSwitchService.Enabled = true; } else { btnSwitchService.Enabled = false; // 等待服务停止 await _server.StopAsync(); } } catch (Exception ex) { AddLog($"exception: {ex.Message}"); } } private void BtnClear_Click(object sender, EventArgs e) { txtLog.Text = ""; } private void AddLog(string log) { if (txtLog.IsDisposed) { return; } // 从ui线程去操作ui if (txtLog.InvokeRequired) { txtLog.Invoke(new AddLogHandler(AddLog), log); } else { txtLog.AppendText($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {log}\r\n"); } } }
==========================
TcpServer
public partial class FormServer : Form { delegate void AddLogHandler(string log); #pragma warning disable IDE0069 // 应释放可释放的字段 private readonly ITcpServer _server = new TcpServer(); /// <summary> /// 线程池 /// </summary> private readonly ThreadPool _threadPool = new ThreadPool(); /// <summary> /// 定时器 /// </summary> private readonly Timer _timer = new Timer(5000) { AutoReset = true, }; #pragma warning restore IDE0069 // 应释放可释放的字段 /// <summary> /// 线程池回调函数 /// </summary> private TaskProcEx _taskTaskProc; /// <summary> /// 最大封包长度 /// </summary> private const int MaxPacketSize = 4096; public FormServer() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { // 演示设置server属性 // 缓冲区大小应根据实际业务设置, 并发数和包大小都是考虑条件 // 都是小包的情况下4K合适, 刚好是一个内存分页(在非托管内存, 相关知识查百度) // 大包比较多的情况下, 应根据并发数设置比较大但是又不会爆内存的值 _server.SocketBufferSize = 4096; // 4K // server绑定地址和端口 _server.Address = "0.0.0.0"; _server.Port = 5555; // 演示绑定事件 _server.OnPrepareListen += OnPrepareListen; _server.OnAccept += OnAccept; _server.OnReceive += OnReceive; _server.OnClose += OnClose; _server.OnShutdown += OnShutdown; // 线程池相关设置 _threadPool.OnStartup += ThreadPoolOnStartup; // 线程池启动事件 _threadPool.OnShutdown += ThreadPoolOnShutdown; // 线程池退出事件 _threadPool.OnWorkerThreadStart += ThreadPoolOnWorkerThreadStart; // 工作线程启动事件 _threadPool.OnWorkerThreadEnd += ThreadPoolOnWorkerThreadEnd; // 工作线程关闭事件 // 线程池回调函数 _taskTaskProc = TaskTaskProc; // 定时输出线程池任务数 _timer.Elapsed += (_, args) => { if (_server.HasStarted && _threadPool.HasStarted) { AddLog($"线程池当前在执行的任务数: {_threadPool.TaskCount}, 任务队列数: {_threadPool.QueueSize}"); } }; _timer.Start(); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { if (_server.HasStarted) { MessageBox.Show(@"请先停止服务", @"服务正在运行:", MessageBoxButtons.OK, MessageBoxIcon.Warning); e.Cancel = true; return; } // 停止并释放线程池, 会等待所有任务结束 _threadPool.Dispose(); // 停止并释放定时器 _timer.Stop(); _timer.Close(); // 停止释放服务器 _server.Dispose(); e.Cancel = false; } private void ThreadPoolOnStartup(ThreadPool threadPool) { AddLog("ThreadPoolOnStartup()"); } private void ThreadPoolOnShutdown(ThreadPool threadPool) { AddLog("ThreadPoolOnShutdown()"); } private void ThreadPoolOnWorkerThreadStart(ThreadPool threadPool, ulong threadId) { // 如果要在当前事件中操作ui, 请另开Task, 如下所示 var id = threadId; Task.Run(() => { AddLog($"ThreadPoolOnWorkerThreadStart({id})"); }); } private void ThreadPoolOnWorkerThreadEnd(ThreadPool threadPool, ulong threadId) { AddLog($"ThreadPoolOnWorkerThreadEnd({threadId},{Thread.CurrentThread.ManagedThreadId})"); } // ReSharper disable once InconsistentNaming private HandleResult OnPrepareListen(IServer sender, IntPtr listen) { AddLog($"OnPrepareListen({sender.Address}:{sender.Port}), listen: {listen}, hp-socket version: {sender.Version}"); return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnAccept(IServer sender, IntPtr connId, IntPtr client) { // 获取客户端地址 if (!sender.GetRemoteAddress(connId, out var ip, out var port)) { return HandleResult.Error; } AddLog($"OnAccept({connId}), ip: {ip}, port: {port}"); // 设置附加数据, 用来做粘包处理 sender.SetExtra(connId, new ClientInfo { ConnId = connId, PacketData = new List<byte>(), }); return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnReceive(IServer sender, IntPtr connId, byte[] data) { AddLog($"OnReceive({connId}), data length: {data.Length}"); var client = sender.GetExtra<ClientInfo>(connId); if (client == null) { return HandleResult.Error; } client.PacketData.AddRange(data); // 总长度小于包头 if (client.PacketData.Count < sizeof(int)) { return HandleResult.Ok; } HandleResult result; const int headerLength = sizeof(int); do { // 取头部字节得到包头 var packetHeader = client.PacketData.GetRange(0, headerLength).ToArray(); // 两端字节序要保持一致 // 如果当前环境不是小端字节序 if (!BitConverter.IsLittleEndian) { // 翻转字节数组, 变为小端字节序 Array.Reverse(packetHeader); } // 得到包头指向的数据长度 var dataLength = BitConverter.ToInt32(packetHeader, 0); // 完整的包长度(含包头和完整数据的大小) var fullLength = dataLength + headerLength; if (dataLength < 0 || fullLength > MaxPacketSize) { result = HandleResult.Error; break; } // 如果来的数据小于一个完整的包 if (client.PacketData.Count < fullLength) { // 下次数据到达处理 result = HandleResult.Ignore; break; } // 是不是一个完整的包(包长 = 实际数据长度 + 包头长度) if (client.PacketData.Count == fullLength) { // 得到完整包并处理 var fullData = client.PacketData.GetRange(headerLength, dataLength).ToArray(); result = OnProcessFullPacket(sender, client, fullData); // 清空缓存 client.PacketData.Clear(); break; } // 如果来的数据比一个完整的包长 if (client.PacketData.Count > fullLength) { // 先得到完整包并处理 var fullData = client.PacketData.GetRange(headerLength, dataLength).ToArray(); result = OnProcessFullPacket(sender, client, fullData); if (result == HandleResult.Error) { break; } // 再移除已经读了的数据 client.PacketData.RemoveRange(0, fullLength); // 剩余的数据下个循环处理 } } while (true); return result; } // ReSharper disable once InconsistentNaming private HandleResult OnClose(IServer sender, IntPtr connId, SocketOperation socketOperation, int errorCode) { AddLog($"OnClose({connId}), socket operation: {socketOperation}, error code: {errorCode}"); var client = sender.GetExtra<ClientInfo>(connId); if (client != null) { sender.RemoveExtra(connId); client.PacketData.Clear(); return HandleResult.Error; } return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnShutdown(IServer sender) { AddLog($"OnShutdown({sender.Address}:{sender.Port})"); return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnProcessFullPacket(IServer sender, ClientInfo client, byte[] data) { // 这里来的都是完整的包, 但是这里不做耗时操作, 仅把数据放入队列 var packet = JsonConvert.DeserializeObject<Packet>(Encoding.UTF8.GetString(data)); var result = HandleResult.Ok; switch (packet.Type) { case PacketType.Echo: // 假如回显是一个非耗时操作, 在这处理 { // 组织packet为一个json var json = JsonConvert.SerializeObject(new Packet { Type = packet.Type, Data = packet.Data, }); // json转字节数组 var bytes = Encoding.UTF8.GetBytes(json); // 先发包头 if (!SendPacketHeader(sender, client.ConnId, bytes.Length)) { result = HandleResult.Error; break; } // 再发实际数据 if (!sender.Send(client.ConnId, bytes, bytes.Length)) { result = HandleResult.Error; } // 至此完成回显 break; } case PacketType.Time: // 假如获取服务器时间是耗时操作, 将该操作放入队列 { // 向线程池提交任务 if (!_threadPool.Submit(_taskTaskProc, new TaskInfo { Client = client, Packet = packet, })) { result = HandleResult.Error; } break; } default: result = HandleResult.Error; break; } return result; } /// <summary> /// 线程池任务回调函数 /// </summary> /// <param name="obj">任务参数</param> private void TaskTaskProc(object obj) { AddLog($"TaskTaskProc() 线程id: {Thread.CurrentThread.ManagedThreadId}"); if (!(obj is TaskInfo taskInfo)) { return; } // 如果连接已经断开了(可能被踢了) // 它的任务就不做了(根据自己业务需求来, 也许你的任务就是要完成每个连接的所有任务, 每个包都要处理, 不管连接断开与否, 就不要写这个判断, 但是你回发包的时候要判断是否有效连接) if (!_server.IsConnected(taskInfo.Client.ConnId)) { return; } // 在这里处理耗时任务逻辑 switch (taskInfo.Packet.Type) { case PacketType.Time: { // 模拟耗时操作 Thread.Sleep(6000); // 组织packet为一个json var json = JsonConvert.SerializeObject(new Packet { Type = PacketType.Time, Data = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), }); // json转字节数组 var bytes = Encoding.UTF8.GetBytes(json); // 先发包头 if (!SendPacketHeader(_server, taskInfo.Client.ConnId, bytes.Length)) { // 发送失败断开连接 _server.Disconnect(taskInfo.Client.ConnId); break; } // 再发实际数据 if (!_server.Send(taskInfo.Client.ConnId, bytes, bytes.Length)) { _server.Disconnect(taskInfo.Client.ConnId); } // 至此完成当前任务 break; } } } /// <summary> /// 发送包头 /// <para>固定包头网络字节序</para> /// </summary> /// <param name="sender">服务器组件</param> /// <param name="connId">连接id</param> /// <param name="dataLength">实际数据长度</param> /// <returns></returns> private bool SendPacketHeader(IServer sender, IntPtr connId, int dataLength) { // 包头转字节数组 var packetHeaderBytes = BitConverter.GetBytes(dataLength); // 两端字节序要保持一致 // 如果当前环境不是小端字节序 if (!BitConverter.IsLittleEndian) { // 翻转字节数组, 变为小端字节序 Array.Reverse(packetHeaderBytes); } return sender.Send(connId, packetHeaderBytes, packetHeaderBytes.Length); } private async void BtnSwitchService_Click(object sender, EventArgs e) { try { if (btnSwitchService.Text == @"启动") { // 2个线程处理耗时操作, 作为相对耗时的任务, 可根据业务需求多开线程处理 if (!_threadPool.Start(2, RejectedPolicy.WaitFor)) { btnSwitchService.Enabled = false; throw new Exception($"线程池启动失败, 错误码: {_threadPool.SysErrorCode}"); } // 启动服务 if (!_server.Start()) { throw new Exception($"error code: {_server.ErrorCode}, error message: {_server.ErrorMessage}"); } btnSwitchService.Text = @"停止"; // 等待线程池停止 await _threadPool.WaitAsync(); // 等待服务停止 await _server.WaitAsync(); // 停止以后还原按钮标题 btnSwitchService.Text = @"启动"; btnSwitchService.Enabled = true; } else { btnSwitchService.Enabled = false; // 停止并等待线程池任务全部完成 await _threadPool.StopAsync(); // 等待服务停止 await _server.StopAsync(); } } catch (Exception ex) { AddLog($"exception: {ex.Message}"); } } private void BtnClear_Click(object sender, EventArgs e) { txtLog.Text = ""; } private void AddLog(string log) { if (txtLog.IsDisposed) { return; } // 从ui线程去操作ui if (txtLog.InvokeRequired) { txtLog.Invoke(new AddLogHandler(AddLog), log); } else { txtLog.AppendText($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {log}\r\n"); } } }
TcpClient
public partial class FormClient : Form { delegate void AddLogHandler(string log); #pragma warning disable IDE0069 // 应释放可释放的字段 private readonly ITcpClient _client = new TcpClient(); #pragma warning restore IDE0069 // 应释放可释放的字段 /// <summary> /// 封包, 做粘包用 /// </summary> private readonly List<byte> _packetData = new List<byte>(); /// <summary> /// 最大封包长度 /// </summary> private const int MaxPacketSize = 4096; public FormClient() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { // 演示设置client属性 // 缓冲区大小 _client.SocketBufferSize = 4096; // 4K // 异步连接 _client.Async = true; // 要连接的服务器地址和端口(也可以调用Connect()方法时传入服务器ip和端口) // 例如: _client.Connect("127.0.0.1", 555) _client.Address = "127.0.0.1"; _client.Port = 5555; // 事件绑定 _client.OnPrepareConnect += OnPrepareConnect; _client.OnConnect += OnConnect; _client.OnReceive += OnReceive; _client.OnClose += OnClose; } private void FormClient_FormClosing(object sender, FormClosingEventArgs e) { if (_client.HasStarted) { MessageBox.Show(@"请先断开与服务器的连接", @"正在通信:", MessageBoxButtons.OK, MessageBoxIcon.Warning); e.Cancel = true; return; } // 停止并释放客户端 _client.Dispose(); e.Cancel = false; } // ReSharper disable once InconsistentNaming private HandleResult OnPrepareConnect(IClient sender, IntPtr socket) { AddLog($"OnPrepareConnect({sender.Address}:{sender.Port}), socket handle: {socket}, hp-socket version: {sender.Version}"); return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnConnect(IClient sender) { AddLog("OnConnect()"); _packetData.Clear(); return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnReceive(IClient sender, byte[] data) { AddLog($"OnReceive(), data length: {data.Length}"); _packetData.AddRange(data); // 总长度小于包头 if (_packetData.Count < sizeof(int)) { return HandleResult.Ok; } HandleResult result; const int headerLength = sizeof(int); do { // 取头部字节得到包头 var packetHeader = _packetData.GetRange(0, headerLength).ToArray(); // 两端字节序要保持一致 // 如果当前环境不是小端字节序 if (!BitConverter.IsLittleEndian) { // 翻转字节数组, 变为小端字节序 Array.Reverse(packetHeader); } // 得到包头指向的数据长度 var dataLength = BitConverter.ToInt32(packetHeader, 0); // 完整的包长度(含包头和完整数据的大小) var fullLength = dataLength + headerLength; if (dataLength < 0 || fullLength > MaxPacketSize) { result = HandleResult.Error; break; } // 如果来的数据小于一个完整的包 if (_packetData.Count < fullLength) { // 下次数据到达处理 result = HandleResult.Ignore; break; } // 是不是一个完整的包(包长 = 实际数据长度 + 包头长度) if (_packetData.Count == fullLength) { // 得到完整包并处理 var fullData = _packetData.GetRange(headerLength, dataLength).ToArray(); result = OnProcessFullPacket(fullData); // 清空缓存 _packetData.Clear(); break; } // 如果来的数据比一个完整的包长 if (_packetData.Count > fullLength) { // 先得到完整包并处理 var fullData = _packetData.GetRange(headerLength, dataLength).ToArray(); result = OnProcessFullPacket(fullData); if (result == HandleResult.Error) { break; } // 再移除已经读了的数据 _packetData.RemoveRange(0, fullLength); // 剩余的数据下个循环处理 } } while (true); return result; } // ReSharper disable once InconsistentNaming private HandleResult OnClose(IClient sender, SocketOperation socketOperation, int errorCode) { _packetData.Clear(); AddLog($"OnClose(), socket operation: {socketOperation}, error code: {errorCode}"); return HandleResult.Ok; } // ReSharper disable once InconsistentNaming private HandleResult OnProcessFullPacket(byte[] data) { // 这里来的都是完整的包 var packet = JsonConvert.DeserializeObject<Packet>(Encoding.UTF8.GetString(data)); var result = HandleResult.Ok; switch (packet.Type) { case PacketType.Echo: // 回显是个字符串显示操作 { AddLog($"OnProcessFullPacket(), type: {packet.Type}, content: {packet.Data}"); break; } case PacketType.Time: // 获取服务器时间依然是个字符串操作^_^ { AddLog($"OnProcessFullPacket(), type: {packet.Type}, time: {packet.Data}"); break; } default: result = HandleResult.Error; break; } return result; } /// <summary> /// 发送包头 /// <para>固定包头网络字节序</para> /// </summary> /// <param name="dataLength">实际数据长度</param> /// <returns></returns> private bool SendPacketHeader(int dataLength) { // 包头转字节数组 var packetHeaderBytes = BitConverter.GetBytes(dataLength); // 两端字节序要保持一致 // 如果当前环境不是小端字节序 if (!BitConverter.IsLittleEndian) { // 翻转字节数组, 变为小端字节序 Array.Reverse(packetHeaderBytes); } return _client.Send(packetHeaderBytes, packetHeaderBytes.Length); } /// <summary> /// 发送数据 /// </summary> /// <param name="type"></param> /// <param name="data"></param> private void Send(PacketType type, string data) { if (!_client.HasStarted) { return; } // 组织封包, 取得要发送的数据 var packet = new Packet { Type = type, Data = data, }; var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(packet)); // 发送包头, 发送失败断开连接 if (!SendPacketHeader(bytes.Length)) { _client.Stop(); return; } // 发送实际数据, 发送失败断开连接 if (!_client.Send(bytes, bytes.Length)) { _client.Stop(); } } private async void BtnConnectSwitch_Click(object sender, EventArgs e) { try { if (btnConnectSwitch.Text == @"连接") { // 连接到目标服务器 if (!_client.Connect()) { throw new Exception($"error code: {_client.ErrorCode}, error message: {_client.ErrorMessage}"); } btnConnectSwitch.Text = @"断开"; // 等待服务停止 await _client.WaitAsync(); // 停止以后还原按钮标题 btnConnectSwitch.Text = @"连接"; } else { // 断开连接 await _client.StopAsync(); } } catch (Exception ex) { AddLog($"exception: {ex.Message}"); } } private void BtnSend_Click(object sender, EventArgs e) { Send(PacketType.Echo, txtContent.Text.Trim()); } private void BtnTest_Click(object sender, EventArgs e) { var txt = txtContent.Text.Trim(); Send(PacketType.Echo, txt); Send(PacketType.Echo, txt + new Random(Guid.NewGuid().GetHashCode()).NextDouble()); Send(PacketType.Echo, txt + new Random(Guid.NewGuid().GetHashCode()).Next(1000, 1000000)); Send(PacketType.Echo, txt + Guid.NewGuid()); } private void BtnGetServerTime_Click(object sender, EventArgs e) { Send(PacketType.Time, null); } private void BtnClear_Click(object sender, EventArgs e) { txtLog.Text = ""; } private void AddLog(string log) { if (txtLog.IsDisposed) { return; } // 从ui线程去操作ui if (txtLog.InvokeRequired) { txtLog.Invoke(new AddLogHandler(AddLog), log); } else { txtLog.AppendText($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {log}\r\n"); } } }