架构师_程序员

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2105|回复: 13
打印 上一主题 下一主题

[资料] .net/c# 运用websocket做网站的消息通知[源码]

[复制链接]
跳转到指定楼层
楼主
发表于 2018-2-2 17:58:01 | 只看该作者 回帖奖励 |正序浏览 |阅读模式
websocket在这就不多说了之前小渣渣有帖子已经介绍过了,不了解的可以自行百度或者去这个帖子看(.net/c# 模拟websocket客户端连接[源码]
https://www.itsvse.com/thread-3652-1-1.html)。
好了不多说了,web的消息通知之前看了好多人用的ajax轮训去查的有点浪费资源,也有长连接做的,我用了WebSocket+iNotify.js,
iNotify.js 是一个封装好了的网页消息通知,它用到了Notification()这API接口,这个接口可以在你网站最小化的情况下提示窗口也会在右下角弹出,很好用的。
不过在谷歌浏览器里62版本后需要网站是https的才可以,这个到后面在后面会提到。
还有用到了websocket心跳,在设定的时间里会向服务端要你需要的数据。
好了说了这么多废话,下面把代码贴出来,源码后面附上。
客户端:
heartBeat.js  
  1. var ws;//websocket实例
复制代码
服务端:
  1. public class WebSocket
  2.     {
  3.         private Dictionary<string, Session> SessionPool = new Dictionary<string, Session>();
  4.         private Dictionary<string, string> MsgPool = new Dictionary<string, string>();

  5.         #region 启动WebSocket服务
  6.         /// <summary>
  7.         /// 启动WebSocket服务
  8.         /// </summary>  
  9.         public void start(int port)
  10.         {
  11.             Socket SockeServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  12.             SockeServer.Bind(new IPEndPoint(IPAddress.Any, port));
  13.             SockeServer.Listen(20);
  14.             SockeServer.BeginAccept(new AsyncCallback(Accept), SockeServer);

  15.         }
  16.         #endregion

  17.         #region 处理客户端连接请求
  18.         /// <summary>
  19.         /// 处理客户端连接请求
  20.         /// </summary>
  21.         /// <param name="result"></param>
  22.         private void Accept(IAsyncResult socket)
  23.         {
  24.             // 还原传入的原始套接字
  25.             Socket SockeServer = (Socket)socket.AsyncState;
  26.             // 在原始套接字上调用EndAccept方法,返回新的套接字
  27.             Socket SockeClient = SockeServer.EndAccept(socket);
  28.             byte[] buffer = new byte[4096];
  29.             try
  30.             {
  31.                 //接收客户端的数据
  32.                 SockeClient.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(Recieve), SockeClient);
  33.                 //保存登录的客户端
  34.                 Session session = new Session();
  35.                 session.SockeClient = SockeClient;
  36.                 session.IP = SockeClient.RemoteEndPoint.ToString();
  37.                 session.buffer = buffer;
  38.                 lock (SessionPool)
  39.                 {
  40.                     if (SessionPool.ContainsKey(session.IP))
  41.                     {
  42.                         this.SessionPool.Remove(session.IP);
  43.                     }
  44.                     this.SessionPool.Add(session.IP, session);
  45.                 }
  46.                 //准备接受下一个客户端
  47.                 SockeServer.BeginAccept(new AsyncCallback(Accept), SockeServer);
  48.             }
  49.             catch (Exception ex)
  50.             {
  51.               
  52.             }
  53.         }
  54.         #endregion

  55.         #region 处理接收的数据
  56.         /// <summary>
  57.         /// 处理接受的数据
  58.         /// </summary>
  59.         /// <param name="socket"></param>
  60.         private void Recieve(IAsyncResult socket)
  61.         {
  62.             Socket SockeClient = (Socket)socket.AsyncState;
  63.             string IP = SockeClient.RemoteEndPoint.ToString();
  64.             if (SockeClient == null || !SessionPool.ContainsKey(IP))
  65.             {
  66.                 return;
  67.             }
  68.             try
  69.             {
  70.                 int length = SockeClient.EndReceive(socket);
  71.                 byte[] buffer = SessionPool[IP].buffer;
  72.                 SockeClient.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(Recieve), SockeClient);
  73.                 string msg = Encoding.UTF8.GetString(buffer, 0, length);
  74.                 //  websocket建立连接的时候,除了TCP连接的三次握手,websocket协议中客户端与服务器想建立连接需要一次额外的握手动作
  75.                 if (msg.Contains("Sec-WebSocket-Key"))
  76.                 {
  77.                     SockeClient.Send(PackageHandShakeData(buffer, length));
  78.                     SessionPool[IP].isWeb = true;
  79.                     return;
  80.                 }
  81.                 if (SessionPool[IP].isWeb)
  82.                 {
  83.                     msg = AnalyzeClientData(buffer, length);
  84.                 }

  85.                 byte[] msgBuffer = PackageServerData(msg);
  86.                 //foreach (Session se in SessionPool.Values)
  87.                 //{
  88.                 //    se.SockeClient.Send(msgBuffer, msgBuffer.Length, SocketFlags.None);
  89.                 //}
  90.                 Session currentClient = SessionPool[IP];
  91.                 currentClient.SockeClient.Send(msgBuffer, msgBuffer.Length, SocketFlags.None);
  92.             }
  93.             catch
  94.             {
  95.                 //SockeClient.Disconnect(true);
  96.                 SessionPool.Remove(IP);
  97.             }
  98.         }
  99.         #endregion

  100.         #region 客户端和服务端的响应
  101.         /*
  102.          * 客户端向服务器发送请求
  103.          *
  104.          * GET / HTTP/1.1
  105.          * Origin: https://localhost:1416
  106.          * Sec-WebSocket-Key: vDyPp55hT1PphRU5OAe2Wg==
  107.          * Connection: Upgrade
  108.          * Upgrade: Websocket
  109.          *Sec-WebSocket-Version: 13
  110.          * User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
  111.          * Host: localhost:8064
  112.          * DNT: 1
  113.          * Cache-Control: no-cache
  114.          * Cookie: DTRememberName=admin
  115.          *
  116.          * 服务器给出响应
  117.          *
  118.          * HTTP/1.1 101 Switching Protocols
  119.          * Upgrade: websocket
  120.          * Connection: Upgrade
  121.          * Sec-WebSocket-Accept: xsOSgr30aKL2GNZKNHKmeT1qYjA=
  122.          *
  123.          * 在请求中的“Sec-WebSocket-Key”是随机的,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个魔幻字符串
  124.          * “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用 SHA-1 加密,之后进行 BASE-64编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端
  125.          */
  126.         #endregion

  127.         #region 打包请求连接数据
  128.         /// <summary>
  129.         /// 打包请求连接数据
  130.         /// </summary>
  131.         /// <param name="handShakeBytes"></param>
  132.         /// <param name="length"></param>
  133.         /// <returns></returns>
  134.         private byte[] PackageHandShakeData(byte[] handShakeBytes, int length)
  135.         {
  136.             string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, length);
  137.             string key = string.Empty;
  138.             Regex reg = new Regex(@"Sec\-WebSocket\-Key:(.*?)\r\n");
  139.             Match m = reg.Match(handShakeText);
  140.             if (m.Value != "")
  141.             {
  142.                 key = Regex.Replace(m.Value, @"Sec\-WebSocket\-Key:(.*?)\r\n", "$1").Trim();
  143.             }
  144.             byte[] secKeyBytes = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
  145.             string secKey = Convert.ToBase64String(secKeyBytes);
  146.             var responseBuilder = new StringBuilder();
  147.             responseBuilder.Append("HTTP/1.1 101 Switching Protocols" + "\r\n");
  148.             responseBuilder.Append("Upgrade: websocket" + "\r\n");
  149.             responseBuilder.Append("Connection: Upgrade" + "\r\n");
  150.             responseBuilder.Append("Sec-WebSocket-Accept: " + secKey + "\r\n\r\n");
  151.             return Encoding.UTF8.GetBytes(responseBuilder.ToString());
  152.         }
  153.         #endregion

  154.         #region 处理接收的数据
  155.         /// <summary>
  156.         /// 处理接收的数据
  157.         /// 参考 https://www.cnblogs.com/smark/archive/2012/11/26/2789812.html
  158.         /// </summary>
  159.         /// <param name="recBytes"></param>
  160.         /// <param name="length"></param>
  161.         /// <returns></returns>
  162.         private string AnalyzeClientData(byte[] recBytes, int length)
  163.         {
  164.             int start = 0;
  165.             // 如果有数据则至少包括3位
  166.             if (length < 2) return "";
  167.             // 判断是否为结束针
  168.             bool IsEof = (recBytes[start] >> 7) > 0;
  169.             // 暂不处理超过一帧的数据
  170.             if (!IsEof) return "";
  171.             start++;
  172.             // 是否包含掩码
  173.             bool hasMask = (recBytes[start] >> 7) > 0;
  174.             // 不包含掩码的暂不处理
  175.             if (!hasMask) return "";
  176.             // 获取数据长度
  177.             UInt64 mPackageLength = (UInt64)recBytes[start] & 0x7F;
  178.             start++;
  179.             // 存储4位掩码值
  180.             byte[] Masking_key = new byte[4];
  181.             // 存储数据
  182.             byte[] mDataPackage;
  183.             if (mPackageLength == 126)
  184.             {
  185.                 // 等于126 随后的两个字节16位表示数据长度
  186.                 mPackageLength = (UInt64)(recBytes[start] << 8 | recBytes[start + 1]);
  187.                 start += 2;
  188.             }
  189.             if (mPackageLength == 127)
  190.             {
  191.                 // 等于127 随后的八个字节64位表示数据长度
  192.                 mPackageLength = (UInt64)(recBytes[start] << (8 * 7) | recBytes[start] << (8 * 6) | recBytes[start] << (8 * 5) | recBytes[start] << (8 * 4) | recBytes[start] << (8 * 3) | recBytes[start] << (8 * 2) | recBytes[start] << 8 | recBytes[start + 1]);
  193.                 start += 8;
  194.             }
  195.             mDataPackage = new byte[mPackageLength];
  196.             for (UInt64 i = 0; i < mPackageLength; i++)
  197.             {
  198.                 mDataPackage[i] = recBytes[i + (UInt64)start + 4];
  199.             }
  200.             Buffer.BlockCopy(recBytes, start, Masking_key, 0, 4);
  201.             for (UInt64 i = 0; i < mPackageLength; i++)
  202.             {
  203.                 mDataPackage[i] = (byte)(mDataPackage[i] ^ Masking_key[i % 4]);
  204.             }
  205.             return Encoding.UTF8.GetString(mDataPackage);
  206.         }
  207.         #endregion

  208.         #region 发送数据
  209.         /// <summary>
  210.         /// 把发送给客户端消息打包处理(拼接上谁什么时候发的什么消息)
  211.         /// </summary>
  212.         /// <returns>The data.</returns>
  213.         /// <param name="message">Message.</param>
  214.         private byte[] PackageServerData(string msg)
  215.         {
  216.             byte[] content = null;
  217.             object oj = new { States = 0, Count = 0 }; // 0:没有通知,1:有通知  
  218.             oj = new { States = 0}; // 0:没有通知,1:有通知  
  219.             if (!string.IsNullOrWhiteSpace(msg))
  220.             {
  221.                
  222.                 oj = new
  223.                 {
  224.                     States = 1,                 
  225.                     NContent = "您有新消息", //消息内容
  226.                     PublisherName = "Admin", //发布消息的人
  227.                     PublisherImage ="/Upload/Images/20180131/测试.png",  //发布消息人头像
  228.                 };
  229.             }
  230.             string json = JsonConvert.SerializeObject(oj);
  231.             byte[] temp = Encoding.UTF8.GetBytes(json);
  232.             if (temp.Length < 126)
  233.             {
  234.                 content = new byte[temp.Length + 2];
  235.                 content[0] = 0x81;
  236.                 content[1] = (byte)temp.Length;
  237.                 Buffer.BlockCopy(temp, 0, content, 2, temp.Length);
  238.             }
  239.             else if (temp.Length < 0xFFFF)
  240.             {
  241.                 content = new byte[temp.Length + 4];
  242.                 content[0] = 0x81;
  243.                 content[1] = 126;
  244.                 content[2] = (byte)(temp.Length >> 8 & 0xFF);
  245.                 content[3] = (byte)(temp.Length & 0xFF);
  246.                 Buffer.BlockCopy(temp, 0, content, 4, temp.Length);
  247.             }
  248.             return content;
  249.         }
  250.         #endregion
  251.     }
复制代码


ps:如果下载了源码最好把网页部署到本地iis上测试不然弹窗不会弹出来,如果运用到正式项目里要注意了之前提到的谷歌浏览器限制网站必须是Https的弹窗才会弹出来,
那么想要在Https网站实现 前端改用 wss   ( var wsUrl = 'wss://127.0.0.1:4649'; )  服务端也要相应的改下代码了,要用Https协议的处理了,这个我目前还在研究,
不过火狐,360极速模式下是可以用的其他浏览器自行测试。
如果有人研究了wss的可以分享出来,研究一下,感谢各位大佬。

源码下载:
游客,如果您要查看本帖隐藏内容请回复

评分

参与人数 1MB +1 贡献 +1 收起 理由
admin + 1 + 1 很给力!

查看全部评分




上一篇:百度地图JS检索控件功能
下一篇:寻找高端IT架构师合作
帖子永久地址: 

架构师_程序员 - 论坛版权1、本主题所有言论和图片纯属会员个人意见,与本论坛立场无关
2、本站所有主题由该帖子作者发表,该帖子作者与架构师_程序员享有帖子相关版权
3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和架构师_程序员的同意
4、帖子作者须承担一切因本文发表而直接或间接导致的民事或刑事法律责任
5、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责
6、如本帖侵犯到任何版权问题,请立即告知本站,本站将及时予与删除并致以最深的歉意
7、架构师_程序员管理员和版主有权不事先通知发贴者而删除本文

码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
推荐
 楼主| 发表于 2018-2-2 18:00:16 | 只看该作者
额发出来发现心疼js没写完,补发一下
heartBeat.js  
  1.     var ws;//websocket实例
  2.     var lockReconnect = false;//避免重复连接
  3.    //var wsUrl = 'wss://127.0.0.1:4649';
  4.     var wsUrl = 'ws://127.0.0.1:4649';

  5.     var iN = new iNotify({
  6.         effect: 'flash',
  7.         message: "有消息了",
  8.         notification: {
  9.             title: "通知!",
  10.             body: '您来了一条新消息',

  11.         },
  12.         //标题闪烁,或者滚动速度
  13.         interval: 500,
  14.         //可选,默认绿底白字的  Favicon
  15.         updateFavicon: {
  16.             // favicon 字体颜色
  17.             textColor: "#fff",
  18.             //背景颜色,设置背景颜色透明,将值设置为“transparent”
  19.             backgroundColor: "#FFFAFA"
  20.         },
  21.     });
  22.    
  23.    
  24.         function createWebSocket(url) {
  25.             try {
  26.                 ws = new WebSocket(url);
  27.                 //ws.Certificate = new X509Certificate2("server.pfx");
  28.                 initEventHandle();
  29.             } catch (e) {
  30.                 reconnect(url);
  31.             }     
  32.         }

  33.         function initEventHandle() {
  34.             ws.onclose = function () {
  35.                 reconnect(wsUrl);
  36.             };
  37.             ws.onerror = function () {
  38.                 reconnect(wsUrl);
  39.             };
  40.             ws.onopen = function () {
  41.                 //console.log('链接成功!');
  42.                 //心跳检测重置
  43.                 heartCheck.reset().start();
  44.             };
  45.             ws.onmessage = function (event) {
  46.                 var data = event.data;//来自服务器的数据  
  47.                 var json = $.parseJSON(data);
  48.                 //console.log(json);
  49.                 if (json.States == 1) {
  50.                     iN.setFavicon(1).setTitle('新消息').notify({
  51.                         title: json.PublisherName,
  52.                         body: json.NContent,                     
  53.                         icon: json.PublisherImage
  54.                         //    onclick: function () {  //点击事件
  55.                         //        taskDetails({ taskKey: tkey, key: pkey });
  56.                         //    }
  57.                     });
  58.                     setTimeout(function () {
  59.                         iN.setTitle()
  60.                     }, 5000);


  61.                 }            //如果获取到消息,心跳检测重置
  62.                 //拿到任何消息都说明当前连接是正常的
  63.                 heartCheck.reset().start();
  64.             }
  65.         }

  66.         function reconnect(url) {
  67.             if(lockReconnect) return;
  68.             lockReconnect = true;
  69.             //没连接上会一直重连,设置延迟避免请求过多
  70.             setTimeout(function () {
  71.                 createWebSocket(url);
  72.                 lockReconnect = false;
  73.             }, 2000);
  74.         }

  75.    
  76.         //心跳检测
  77.         var heartCheck = {
  78.             timeout: 10000,//60秒(60000)
  79.             timeoutObj: null,
  80.             serverTimeoutObj: null,
  81.             reset: function(){
  82.                 clearTimeout(this.timeoutObj);
  83.                 clearTimeout(this.serverTimeoutObj);
  84.                 return this;
  85.             },
  86.             start: function(){
  87.                 var self = this;
  88.                 this.timeoutObj = setTimeout(function(){
  89.                     //这里发送一个心跳,后端收到后,返回一个心跳消息,
  90.                     //onmessage拿到返回的心跳就说明连接正常
  91.                     ws.send("HeartBeat");
  92.                     //console.log("HeartBeat");
  93.                     self.serverTimeoutObj = setTimeout(function(){//如果超过一定时间还没重置,说明后端主动断开了
  94.                         ws.close();//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
  95.                     }, self.timeout)
  96.                 }, this.timeout)
  97.             }
  98.         }

  99.         createWebSocket(wsUrl);
复制代码

码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
推荐
 楼主| 发表于 2018-2-2 18:02:34 | 只看该作者
如果你是本地测试谷歌浏览器也可以用,限制Https网站只有你发布正式网站谷歌会限制
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
推荐
 楼主| 发表于 2018-2-5 09:11:50 | 只看该作者
小渣渣 发表于 2018-2-2 20:11
微软 有SignalR 为啥还要自己写

研究一下
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
13#
发表于 2018-6-1 09:13:07 | 只看该作者
谢谢分享
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
回复

使用道具 举报

12#
发表于 2018-4-25 10:17:45 | 只看该作者
收藏,万一用得上
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
11#
发表于 2018-3-17 11:45:14 | 只看该作者
谢谢分享哦, 看看
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
10#
发表于 2018-3-14 14:17:43 | 只看该作者
挺熟哦要收费?
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
9#
发表于 2018-3-13 17:13:03 | 只看该作者
sdfasdfasdfasfdasdfasdf
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
8#
发表于 2018-3-7 19:38:13 | 只看该作者
研究一下
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
回复

使用道具 举报

7#
发表于 2018-2-5 16:27:55 | 只看该作者
不错,正好需要呢
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
5#
发表于 2018-2-3 14:08:06 | 只看该作者
谢谢分享,来看看
码农网,只发表在实践过程中,遇到的技术难题,不误导他人。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

免责声明:
码农网所发布的一切软件、编程资料或者文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。

Mail To:help@itsvse.com

QQ|Archiver|手机版|小黑屋|架构师 ( 鲁ICP备14021824号-2 )|网站地图

GMT+8, 2018-6-18 19:46

Powered by Discuz! X3.4

© 2001-2014 Comsenz Inc.

快速回复 返回顶部 返回列表