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空閑的時候允許線程先去干別的事情
- 減少了線程數量導致的資源占用,減少了線程切換導致的資源消耗
- 如圖,每個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) { } }