C# Socket使用以及DotNetty和Supersocket 框架
1.Socket服务端与客户端通话
1服务端
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace tSocket { class Program { byte[] bytes = new byte[1024]; Socket cSocket; static void Main(string[] args) { Program p = new Program(); //打开链接 p.open(); //向服务端发送消息 Console.WriteLine("请输入你要对服务端发送的消息:"); string mes = Console.ReadLine(); string con = p.messge(mes); Console.WriteLine("接受到服务端的消息:" + con); } byte[] data = new byte[1024]; string messge(string mes) { //将发送的消息转成字节数组 bytes = Encoding.UTF8.GetBytes(mes); //发送 cSocket.Send(bytes); while (true) { //接受服务端发送的消息,放入字节数组 int len = cSocket.Receive(data); //将字节数组转成可读明文 string con = Encoding.UTF8.GetString(data, 0, len); ////返回 return con; } } /// <summary> /// 打开链接 /// </summary> void open() { //创建Socket对象 指定连接方式 cSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //创建IP,端口 IPAddress ip = IPAddress.Parse("10.116.253.10"); int port = 7526; //封装IP和端口 IPEndPoint Ipoint = new IPEndPoint(ip, port); //打开链接 cSocket.Connect(Ipoint); } } }
2.客户端
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace ServerSocket { class Program { static void Main(string[] args) { //创建Socket对象,指定他的链接方式 Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //建立IP string ip = "10.116.253.10"; //创建端口 int prot = 7526;//1~9999 IPAddress IPAdd = IPAddress.Parse(ip); //封装IP和端口 IPEndPoint point = new IPEndPoint(IPAdd, prot); //绑定IP和端口 serverSocket.Bind(point); //开始监听 serverSocket.Listen(100); Console.WriteLine("开始监听!"); int i = 0; while (true) { i++; //接受客户链接 Socket cSocket = serverSocket.Accept(); Console.WriteLine("接受第"+i+"个客户的连接!"); Client c = new Client(cSocket); } } } }
using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ServerSocket { class Client { Socket sSocket; byte[] data = new byte[1024]; Thread t; public Client(Socket cSocket) { //接受客户的连接 sSocket = cSocket; //创建线程 t = new Thread(Mess); //开始线程 t.Start(); } void Mess() { try { while (true) { //将用户发送的数据以一个字节数组装起 int length = sSocket.Receive(data); Console.WriteLine("接受客户端发的消息!"); string mess = Encoding.UTF8.GetString(data, 0, length); if (mess == "con") { string con = "DataSource =."; byte[] bytes = Encoding.UTF8.GetBytes(con); sSocket.Send(bytes); } Console.WriteLine("接到用户的消息:" + mess); } } catch (Exception) { sSocket.Close(); } } } }
2.DotNetty
DotNetty是微软的Azure团队,使用C#实现的Netty的版本发布。不但使用了C#和.Net平台的技术特点,并且保留了Netty原来绝大部分的编程接口。让我们在使用时,完全可以依照Netty官方的教程来学习和使用DotNetty应用程序。
Netty 是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。
优点
- 关注点分离——业务和网络逻辑解耦;
- 模块化和可复用性;
- 可测试性作为首要的要求
历史
- 阻塞Socket通信特点:
- 建立连接要阻塞线程,读取数据要阻塞线程
- 如果要管理多个客户端,就需要为每个客户端建立不同的线程
- 会有大量的线程在休眠状态,等待接收数据,资源浪费
- 每个线程都要占用系统资源
- 线程的切换很耗费系统资源
- 非阻塞Socket(NIO)特点:
- 如图,每个Socket如果需要读写操作,都通过事件通知的方式通知选择器,这样就实现了一个线程管理多个Socket的目的。
- 选择器甚至可以在所有的Socket空闲的时候允许线程先去干别的事情
- 减少了线程数量导致的资源占用,减少了线程切换导致的资源消耗
Netty设计的关键点
异步和事件驱动是Netty设计的关键
核心组件
- Channel:一个连接就是一个Channel
- 回调:通知的基础
官方也提供了一些例子。地址如下
https://github.com/Azure/DotNetty
3.Supersocket
开源地址https://github.com/kerryjiang/SuperSocket
SuperSocket是重量轻的可扩展套接字应用程序框架。您可以使用它轻松构建始终连接的套接字应用程序,而无需考虑如何使用套接字,如何维护套接字连接以及套接字如何工作。这是一个纯C#项目,旨在进行扩展,因此只要以.NET语言开发它们,就可以轻松地将它们集成到您的现有系统中。
首先安装:SuperSocket.Engine
SuperSoket的三大对象:
Session: 每一个用户连接就是一个Session
AppServer: Socket服务器实例
Commands: 客户端向服务器发送消息的命令集合
首先在配置文件加入如下配置
<configSections> <section name="superSocket" type="SuperSocket.SocketEngine.Configuration.SocketServiceConfig, SuperSocket.SocketEngine"/> </configSections> <superSocket> <servers> <server name="ChatSocket" textEncoding="gb2312" serverType="XT.SocketService.AppServer.ChatServer, XT.SocketService" ip="Any" port="2020" maxConnectionNumber="1000"> </server> <!-- 可以配置多个Server--> </servers> </superSocket>
AppServer代码如下
[AuthorisizeFilter] public class ChatServer:AppServer<ChatSession> { protected override bool Setup(IRootConfig rootConfig, IServerConfig config) { Console.WriteLine("准备读取配置文件。。。。"); return base.Setup(rootConfig, config); } protected override void OnStarted() { Console.WriteLine("Chat服务启动。。。"); base.OnStarted(); } protected override void OnStopped() { Console.WriteLine("Chat服务停止。。。"); base.OnStopped(); } /// <summary> /// 新的连接 /// </summary> /// <param name="session"></param> protected override void OnNewSessionConnected(ChatSession session) { Console.WriteLine($"Chat服务新加入的连接:{session.LocalEndPoint.Address.ToString()}"); base.OnNewSessionConnected(session); } }
Session代码如下
/// <summary> /// 表示用户连接 /// </summary> //[AuthorisizeFilter] public class ChatSession : AppSession<ChatSession> { public string Id { get; set; } public string PassWord { get; set; } public bool IsLogin { get; set; } public DateTime LoginTime { get; set; } public DateTime LastHbTime { get; set; } public bool IsOnline { get { return this.LastHbTime.AddSeconds(10) > DateTime.Now; } } /// <summary> /// 消息发送 /// </summary> /// <param name="message"></param> public override void Send(string message) { Console.WriteLine($"准备发送给{this.Id}:{message}"); base.Send(message.Format()); } protected override void OnSessionStarted() { this.Send("Welcome to SuperSocket Chat Server"); } protected override void OnInit() { this.Charset = Encoding.GetEncoding("gb2312"); base.OnInit(); } protected override void HandleUnknownRequest(StringRequestInfo requestInfo) { Console.WriteLine("收到命令:" + requestInfo.Key.ToString()); this.Send("不知道如何处理 " + requestInfo.Key.ToString() + " 命令"); } /// <summary> /// 异常捕捉 /// </summary> /// <param name="e"></param> protected override void HandleException(Exception e) { this.Send($"\n\r异常信息:{ e.Message}"); //base.HandleException(e); } /// <summary> /// 连接关闭 /// </summary> /// <param name="reason"></param> protected override void OnSessionClosed(CloseReason reason) { Console.WriteLine("链接已关闭。。。"); base.OnSessionClosed(reason); } }
Commands代码如下 : 客户端发送消息命令 Check 1 123456
Check 代表类名 ,1代表session.id(会话ID),1代表session.PassWord (会话密码)
public class Check : CommandBase<ChatSession, StringRequestInfo> { public override void ExecuteCommand(ChatSession session, StringRequestInfo requestInfo) { if (requestInfo.Parameters != null && requestInfo.Parameters.Length == 2) { ChatSession oldSession = session.AppServer.GetAllSessions().FirstOrDefault(a => requestInfo.Parameters[0].Equals(a.Id)); if (oldSession != null) // 说过之前有用户用这个Id 登录过 { oldSession.Send("您的账号已经在他处登录,您已经被踢下线了"); oldSession.Close(); } #region 这里就可以连接数据库进行数据验证做登录 ///--------------------- #endregion session.Id = requestInfo.Parameters[0]; session.PassWord = requestInfo.Parameters[1]; session.IsLogin = true; session.LoginTime = DateTime.Now; session.Send("登录成功"); { // 获取当前登录用户的离线消息 ChatDataManager.SendLogin(session.Id, c => { session.Send($"{c.FromId} 给你发送消息:{c.Message} {c.Id}"); }); } } else { session.Send("参数错误"); } } }
离线消息存储的相关类
public class ChatDataManager { /// <summary> /// key是用户id /// List 这个用户的全部消息 /// </summary> private static Dictionary<string, List<ChatModel>> Dictionary = new Dictionary<string, List<ChatModel>>(); public static void Add(string userId, ChatModel model) { if (Dictionary.ContainsKey(userId)) { Dictionary[userId].Add(model); } else { Dictionary[userId] = new List<ChatModel>() { model }; } } public static void Remove(string userId, string modelId) { if (Dictionary.ContainsKey(userId)) { Dictionary[userId] = Dictionary[userId].Where(m => m.Id != modelId).ToList(); } } public static void SendLogin(string userId, Action<ChatModel> action) { if (Dictionary.ContainsKey(userId)) { foreach (var item in Dictionary[userId]) { action.Invoke(item); item.State = 1; } } } }
/// <summary> /// 一条消息的记录 /// </summary> public class ChatModel { /// <summary> /// 每条分配个唯一Id /// </summary> public string Id { get; set; } /// <summary> /// 来源编号 /// </summary> public string FromId { get; set; } /// <summary> /// 目标编号 /// </summary> public string ToId { get; set; } /// <summary> /// 消息内容 /// </summary> public string Message { get; set; } /// <summary> /// 消息时间 /// </summary> public DateTime CreateTime { get; set; } /// <summary> /// 消息状态 0未发送 1已发送待确认 2确认收到 /// </summary> public int State { get; set; } }
基本使用获取离线消息
public class Chat : CommandBase<ChatSession, StringRequestInfo> { public override void ExecuteCommand(ChatSession session, StringRequestInfo requestInfo) { // 还是传递两个参数 1、 要发给谁 ToId 2、消息内容 if (requestInfo.Parameters != null && requestInfo.Parameters.Length == 2) { string toId = requestInfo.Parameters[0]; string message = requestInfo.Parameters[1]; ChatSession toSession = session.AppServer.GetAllSessions().FirstOrDefault(a => toId.Equals(a.Id)); string modelId = Guid.NewGuid().ToString(); if (toSession != null) // 说过之前有用户用这个Id 登录过 { toSession.Send($"{session.Id} 给你发消息:{message} {modelId}"); ChatDataManager.Add(toId, new ChatModel() { FromId = session.Id, ToId = toId, Message = message, Id = modelId, State = 1,// 待确认 CreateTime = DateTime.Now }); } else { ChatDataManager.Add(toId, new ChatModel() { FromId = session.Id, ToId = toId, Message = message, Id = modelId, State = 0,// 未发送 CreateTime = DateTime.Now }); session.Send("消息未发送成功"); } } else { session.Send("参数错误"); } } }
public class Confirm : CommandBase<ChatSession, StringRequestInfo> { public override void ExecuteCommand(ChatSession session, StringRequestInfo requestInfo) { if (requestInfo.Parameters != null && requestInfo.Parameters.Length == 1) { string modelId = requestInfo.Parameters[0]; Console.WriteLine($"用户{session.Id} 已确认,收到消息{modelId}"); ChatDataManager.Remove(session.Id, modelId); } else { session.Send("参数错误"); } } }
心跳检测:主要就是定时发送消息,没接到消息就发起重连
public class HB : CommandBase<ChatSession, StringRequestInfo> { public override void ExecuteCommand(ChatSession session, StringRequestInfo requestInfo) { if (requestInfo.Parameters != null && requestInfo.Parameters.Length == 1) { if ("R".Equals(requestInfo.Parameters[0])) { session.LastHbTime = DateTime.Now; session.Send("R"); } else { session.Send("参数错误"); } } else { session.Send("参数错误"); } } }
SuperSocket的AOP的使用
class AuthorisizeFilterAttribute : CommandFilterAttribute { public override void OnCommandExecuting(CommandExecutingContext commandContext) { ChatSession session = (ChatSession)commandContext.Session; string command = commandContext.CurrentCommand.Name; if (!session.IsLogin) { if (!command.Equals("Check")) { session.Send($"请先登录,再操作"); commandContext.Cancel = true; } else { } } else if (!session.IsOnline) { session.LastHbTime = DateTime.Now; } } public override void OnCommandExecuted(CommandExecutingContext commandContext) { } }