SuperSocket基礎(二)-----一個完整的SocketServer項目緩存
因爲時間關係未能及時更新,關於SuperSocket,對於初學者而言,一個SuperSock的Server真的很差寫。官方文檔寫的很清晰,如何接受客戶端發來的二進制報文並作響應的解析。下面就從一個完整的項目出發,記錄SuperSocket的開發過程。服務器
一、項目場景:現有十多個RTU設備,用來監測自來水管的壓力和流量,須要將十多個傳感器傳來的值接收並作解析來使用。用SuperSocket寫一個Socket服務器,實時監聽客戶端發來的數據報文。session
具體的開發過程:app
1、在vs中新建一個項目,Windows窗體或者控制檯程序均可以,在項目解決方案中鼠標右鍵點擊管理NuGet程序包,在線安裝SuperSocket。ide
2、自定義本身服務器中相關的類,建議類的創建順序:RequestInfo>ReceiveFilter>AppSession>AppServer函數
一、創建一個RequestInfo工具
public class DTRequestInfo : IRequestInfo { /// <summary> /// 構造函數 /// </summary> /// <param name="key">鍵值</param> /// <param name="body">接收的數據體</param> public DTRequestInfo(string key,byte[] body) { this.Key = key; this.Body = body; } public string Key { get;set; } /// <summary> /// 請求信息緩存 /// </summary> public byte[] Body { get; set; } /// <summary> /// 設備ID /// </summary> public string DeviceID { get; set; } }
二、創建一個數據接收過濾器幫助 類,做爲過濾器的繼承的父類,主要的做用是用來接收處理客戶端傳類的二進制字符,返回有效的數據部分。學習
/// <summary> /// 處理獲取的全部數據 /// </summary> /// <typeparam name="TRequestInfo"></typeparam> public abstract class ReceiveFilterHelper<TRequestInfo> : ReceiveFilterBase<TRequestInfo> where TRequestInfo : IRequestInfo { private SearchMarkState<byte> m_BeginSearchState; private SearchMarkState<byte> m_EndSearchState; private bool m_FoundBegin = false; protected TRequestInfo NullRequestInfo = default(TRequestInfo); /// <summary> /// 初始化實例 /// </summary> protected ReceiveFilterHelper() { } /// <summary> /// 過濾指定的會話 /// </summary> /// <param name="readBuffer">數據緩存</param> /// <param name="offset">數據起始位置</param> /// <param name="length">緩存長度</param> /// <param name="toBeCopied"></param> /// <param name="rest"></param> /// <returns></returns> public override TRequestInfo Filter(byte[] readBuffer, int offset, int length, bool toBeCopied, out int rest) { rest = 0; int searchEndMarkOffset; int searchEndMarkLength; //在此處作了處理,將接收到的第一個字符做爲起始過濾標誌,到結束。返回指定長度的數據。 byte[] startMark = new byte[] { readBuffer[offset] }; byte[] endMark = new byte[] {0xff }; m_BeginSearchState = new SearchMarkState<byte>(startMark); m_EndSearchState = new SearchMarkState<byte>(endMark); //上一個開始標記長度 int prevMatched = 0; int totalParsed = 0; if (!m_FoundBegin) { prevMatched = m_BeginSearchState.Matched; int pos = readBuffer.SearchMark(offset, length, m_BeginSearchState, out totalParsed); if (pos < 0) { //不要緩存無效數據 if (prevMatched > 0 || (m_BeginSearchState.Matched > 0 && length != m_BeginSearchState.Matched)) { State = FilterState.Error; return NullRequestInfo; } return NullRequestInfo; } else //找到匹配的開始標記 { //But not at the beginning if (pos != offset) { State = FilterState.Error; return NullRequestInfo; } } //找到開始標記 m_FoundBegin = true; searchEndMarkOffset = pos + m_BeginSearchState.Mark.Length - prevMatched; //This block only contain (part of)begin mark if (offset + length <= searchEndMarkOffset) { AddArraySegment(m_BeginSearchState.Mark, 0, m_BeginSearchState.Mark.Length, false); return NullRequestInfo; } searchEndMarkLength = offset + length - searchEndMarkOffset; } else//Already found begin mark { searchEndMarkOffset = offset; searchEndMarkLength = length; } while (true) { var prevEndMarkMatched = m_EndSearchState.Matched; var parsedLen = 0; var endPos = readBuffer.SearchMark(searchEndMarkOffset, searchEndMarkLength, m_EndSearchState, out parsedLen); //沒有找到結束標記 if (endPos < 0) { rest = 0; if (prevMatched > 0)//還緩存先前匹配的開始標記 AddArraySegment(m_BeginSearchState.Mark, 0, prevMatched, false); AddArraySegment(readBuffer, offset, length, toBeCopied); } //totalParsed += parsedLen; //rest = length - totalParsed; totalParsed = 0; byte[] commandData = new byte[BufferSegments.Count + prevMatched + totalParsed]; if (BufferSegments.Count > 0) BufferSegments.CopyTo(commandData, 0, 0, BufferSegments.Count); if (prevMatched > 0) Array.Copy(m_BeginSearchState.Mark, 0, commandData, BufferSegments.Count, prevMatched); Array.Copy(readBuffer, offset, commandData, BufferSegments.Count + prevMatched, totalParsed); var requestInfo = ProcessMatchedRequest(commandData, 0, commandData.Length); Reset(); return requestInfo; if (prevMatched > 0)//Also cache the prev matched begin mark AddArraySegment(m_BeginSearchState.Mark, 0, prevMatched, false); AddArraySegment(readBuffer, offset, length, toBeCopied); //return NullRequestInfo; } } /// <summary> /// Processes the matched request. /// </summary> /// <param name="readBuffer">The read buffer.</param> /// <param name="offset">The offset.</param> /// <param name="length">The length.</param> /// <returns></returns> protected abstract TRequestInfo ProcessMatchedRequest(byte[] readBuffer, int offset, int length); /// <summary> /// Resets this instance. /// </summary> public override void Reset() { m_BeginSearchState.Matched = 0; m_EndSearchState.Matched = 0; m_FoundBegin = false; base.Reset(); } }
三、創建一個數據接收過濾器,繼承ReceiveFilterHelper類,過來接收並過濾指定的信息內容。this
/// <summary> /// 數據接收過濾器 /// </summary> public class DTReceiveFilter : ReceiveFilterHelper<DTRequestInfo> { /// <summary> /// 重寫方法 /// </summary> /// <param name="readBuffer">過濾以後的數據緩存</param> /// <param name="offset">數據起始位置</param> /// <param name="length">數據緩存長度</param> /// <returns></returns> protected override DTRequestInfo ProcessMatchedRequest(byte[] readBuffer, int offset, int length) { //返回構造函數指定的數據格式 return new DTRequestInfo(Encoding.UTF8.GetString(readBuffer, offset, length), readBuffer); } }
4、創建一個AppSession,用來發送和接收客戶端信息,一個客戶端至關於一個session,這一點很重要,應爲每個RUT設備都有一個固定的編號,須要給session的item中添加Key值用來區分不一樣的客戶端。spa
public class DTSession:AppSession<DTSession,DTRequestInfo> { protected override void HandleException(Exception e) { base.HandleException(e); } protected override void OnSessionStarted() { base.OnSessionStarted(); } protected override int GetMaxRequestLength() { return base.GetMaxRequestLength(); } protected override void HandleUnknownRequest(DTRequestInfo requestInfo) { base.HandleUnknownRequest(requestInfo); } }
五、創建AppServer,自定義適合本身項目的服務。這個項目須要的就是服務端給客戶端RTU發送數據請求指令,客戶端才能作出響應返回數據十六進制的數據報文。
Timer requestTimer = null; public DTServer() : base(new DefaultReceiveFilterFactory<DTReceiveFilter, DTRequestInfo>()) { //定時發送請求壓力的報文 double sendInterval = double.Parse(ConfigurationManager.AppSettings["sendInterval"]); requestTimer = new Timer(sendInterval); requestTimer.Elapsed += RequestTimer_Elapsed; requestTimer.Enabled = true; requestTimer.Start(); } private void RequestTimer_Elapsed(object sender, ElapsedEventArgs e) { //發送請求報文 var sessionList = GetAllSessions(); //Logger.Error(sessionList); foreach (var session in sessionList) { Dictionary<string, string> routs = ConfigManager.GetAllConfig(); try { foreach (var item in routs) { if (item.Key.ToString().Contains("rout2_")) { string routeID = item.Key.ToString().Split('_')[1]; byte[] rout = ConvertHelper.strToToHexByte(routeID); byte[] address = ConvertHelper.strToToHexByte(item.Value.ToString()); /// 合成報文 List<byte> data = new List<byte>(); data.Add(rout[0]); data.Add(0x04);//讀取數據 data.Add(address[0]); data.Add(address[1]); data.Add(address[2]); data.Add(address[3]); byte[] checkcode = CRC16.crc_16(data.ToArray()); data.Add(checkcode[1]); data.Add(checkcode[0]); /// 發送報文 //使用字節抽屜存儲 // ArraySegment<byte> sendData = new ArraySegment<byte>(data.ToArray()); session.Send(data.ToArray(), 0, data.ToArray().Length); // Console.WriteLine("發送數據:" + ConvertHelper.byteToHexStr(data.ToArray())); } } } catch (Exception ex) { //寫入日誌 /// Logger.Info(ex.Message); } } } protected override void OnNewSessionConnected(DTSession session) { base.OnNewSessionConnected(session); //Logger.Error(session.SessionID); } protected override void ExecuteCommand(DTSession session, DTRequestInfo requestInfo) { base.ExecuteCommand(session, requestInfo); } protected override void OnStarted() { base.OnStarted(); } protected override void OnStartup() { base.OnStartup(); } protected override bool Setup(IRootConfig rootConfig, IServerConfig config) { return base.Setup(rootConfig, config); } }
3、以上都爲準備工做,下面具體的使用。
一、在程序啓動時開啓服務,用來監聽客戶端。
appServer = new DTServer(); int Port = int.Parse(ConfigurationManager.AppSettings["Port"].ToString()); if (!appServer.Setup(Port)) { label1.Text = "端口裝載失敗"; return; } if (!appServer.Start()) { label1.Text = "服務啓動失敗"; return; } label1.Text = "壓力採集終端服務已開啓"; //監聽地址 for (int i = 0; i < IpEntry.AddressList.Length; i++) { if (IpEntry.AddressList[i].AddressFamily == AddressFamily.InterNetwork) { label2.Text = "監聽IP:" + IpEntry.AddressList[i]; label3.Text = "端口:" + Port.ToString(); } } //註冊事件 appServer.NewRequestReceived += new SuperSocket.SocketBase.RequestHandler<DTSession, DTRequestInfo>(appServer_NewRecivede);
二、請求接收事件
private void appServer_NewRecivede(DTSession session, DTRequestInfo requestInfo) { byte[] bytes = requestInfo.Body; if (bytes[0] == 0xfe) return; //設備地址 //設備地址 string devAddress = bytes[0].ToString("X"); string dataType = bytes[1].ToString("X"); if (dataType != "4") { string tt = string.Empty; foreach (var item in bytes.Take(4).Reverse()) { tt += item.ToString("X2"); } //resultInfo.DeviceID = tt; session.Items["deviceid"] = tt; return; } //數據長度 int dataLenth = bytes[2]; //數據 byte[] datas = bytes.Skip(3).Take(dataLenth).ToArray(); //從緩衝區截取有效數據 string datastrs = ConvertHelper.byteToHexStr(bytes.Take(dataLenth + 5).ToArray()); //採集值 int value = datas[1] + datas[0] * 256; //數值轉換 //電流 double f = 3.3 / 1023 * value / 150 * 1000; //壓力 double yl = 0.0625 * (f - 4); if (session.Items.Count==0) { return; } //將結果寫入log session.Logger.Info("設備編號:" + session.Items["deviceid"].ToString() + " 壓力值:" + yl.ToString() + " 時間:" + DateTime.Now.ToString() + " 報文:" + datastrs); }
三、利用SocketTool工具進行驗證,發送十六進制字符,並在項目日誌中進行查看結果。
以上就是整個項目中的所有代碼和開發過程。感興趣的可加QQ:1301485237 交流學習。