• .NET实现WebSocket服务端即时通信实例


    即时通信常用手段

    1.第三方平台 谷歌、腾讯 环信等多如牛毛,其中谷歌即时通信是免费的,但免费就是免费的并不好用。其他的一些第三方一般收费的,使用要则限流(1s/限制x条消息)要么则限制用户数。

    但稳定性什么都还不错,又能将服务压力甩出

    2.System.Net.Sockets.Socket,也能写一套较好的服务器端。在.NET 4.5之前用较多,使用起来麻烦。需要对数据包进行解析等操作(但貌似网上有对超长包的处理方法)

    3.System.Net.WebSockets.WebSocket,这个,是.NET 4.5出来的东西,对服务器环境也有所要求,IIS8及以上。意味着Windows Server2008R2自带的IIS不支持,Windows8及Server2012以上自带的IIS可以。本文主要将这种方式的实例

    完整流程

    1).客户端请求连接

    复制代码 代码如下:

    ws = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + '/Handler1.ashx?user=' + $("#user").val());

    2).服务端获取连接对象并存储到连接池中

    1
    CONNECT_POOL.Add(user, socket);

    3).连接对象开始监听(每个客户端与服务器保存长链接)

    复制代码 代码如下:

    WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None);

    4).客户端A发送消息给B

    1
    ws.send($("#to").val() + "|" + $('#content').val());

    5).服务端A的连接对象监听到来自A的消息

    1
    string userMsg = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);

    6).解析消息体(B|你好我是A)得到接收者ID,根据接收者ID到连接池中查找B的服务端连接对象,并通过B的连接对象将消息推送给B客户端

    1
    2
    3
    WebSocket destSocket = CONNECT_POOL[descUser];
     
    await destSocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);

    7).服务端A连接对象继续监听

    复制代码 代码如下:

    WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None);

    8).B客户端接收到推送过来的消息

    1
    2
    3
    4
    5
    ws.onmessage = function (evt) {
     
      $('#msg').append('<p>' + evt.data + '</p>');
     
    }

    下面则是完整代码

     客户端部分

    客户端异常简单,正常情况直接用WebSocket,然后监听WebSocket的几个事件就ok。连接的时候可将当前连接者的ID传入(用户编号),发送消息的时候 采用 “接收者ID|我是消息内容” 这种方式,如“A|A你好,我是B!”

    但如用移动端使用还是有一些常见的场景需要处理下的

    1:手机关屏幕,IOS关掉屏幕的时候WebSocket会立即失去连接,Android则会等待一段时间才会失去连接。服务器端能检测到失去连接

    2:网络不稳定,断网情况WebSocket也不会立即失去连接,服务器端不能知道。(可以服务端设计心跳机制,定时给连接池中的用户发送消息,来检测用户是否保持连接)

    3:其他等等...(突然关机、后台结束应用)

    无论哪种,客户端在发送消息(或者网络恢复连接、亮屏)的时候可以先判断ws的状态,如果不是连接状态则需要重连(new下即可)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    <!DOCTYPE html>
    <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
     <title></title>
     <script src="jquery-1.11.3.min.js"></script>
     <script>
     var ws;
     $().ready(function () {
      $('#conn').click(function () {
      ws = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + '/Handler1.ashx?user=' + $("#user").val());
      $('#msg').append('<p>正在连接</p>');
     
      ws.onopen = function () {
       $('#msg').append('<p>已经连接</p>');
      }
      ws.onmessage = function (evt) {
       $('#msg').append('<p>' + evt.data + '</p>');
      }
      ws.onerror = function (evt) {
       $('#msg').append('<p>' + JSON.stringify(evt) + '</p>');
      }
      ws.onclose = function () {
       $('#msg').append('<p>已经关闭</p>');
      }
      });
     
      $('#close').click(function () {
      ws.close();
      });
     
      $('#send').click(function () {
      if (ws.readyState == WebSocket.OPEN) {
       ws.send($("#to").val() + "|" + $('#content').val());
      }
      else {
       $('#tips').text('连接已经关闭');
      }
      });
     
     });
     </script>
    </head>
    <body>
     <div>
     <input id="user" type="text" />
     <input id="conn" type="button" value="连接" />
     <input id="close" type="button" value="关闭"/><br />
     <span id="tips"></span>
     <input id="content" type="text" />
     <input id="send" type="button" value="发送"/><br />
     <input id="to" type="text" />目的用户
     <div id="msg">
     </div>
     </div>
    </body>
    </html>

    服务器端部分

    服务器端使用Handler(也可用WebAPI)来做,主要用WebSocket的类来实现。代码中都有相对详细的注释,这边只说一些需要注意的问题

    1:Dictionary<string,WebSocket> CONNECT_POOL:用户连接池。请求Handler的时候会将当前连接者的用户ID传入,服务器端维护着所有已连接的用户ID和当前用户的WebSocket连接对象

    2:Dictionary<string,List<MessageInfo>> MESSAGE_POOL:离线消息池。如果A->B发送消息,B当前因为某种原因没在线(突然断网/黑屏等原因),会将这条消息先保存起来(2天),待B连接后立马将B的离线消息推送给他。(2:MessageInfo:离线Entity。记录当前离线消息的时间、内容)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net.WebSockets;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web;
    using System.Web.WebSockets;
     
    namespace WebApplication1
    {
     /// <summary>
     /// 离线消息
     /// </summary>
     public class MessageInfo
     {
     public MessageInfo(DateTime _MsgTime, ArraySegment<byte> _MsgContent)
     {
      MsgTime = _MsgTime;
      MsgContent = _MsgContent;
     }
     public DateTime MsgTime { get; set; }
     public ArraySegment<byte> MsgContent { get; set; }
     }
     
     /// <summary>
     /// Handler1 的摘要说明
     /// </summary>
     public class Handler1 : IHttpHandler
     {
     private static Dictionary<string, WebSocket> CONNECT_POOL = new Dictionary<string, WebSocket>();//用户连接池
     private static Dictionary<string, List<MessageInfo>> MESSAGE_POOL = new Dictionary<string, List<MessageInfo>>();//离线消息池
     
     public void ProcessRequest(HttpContext context)
     {
      if (context.IsWebSocketRequest)
      {
      context.AcceptWebSocketRequest(ProcessChat);
      }
     }
     
     private async Task ProcessChat(AspNetWebSocketContext context)
     {
      WebSocket socket = context.WebSocket;
      string user = context.QueryString["user"].ToString();
     
      try
      {
      #region 用户添加连接池
      //第一次open时,添加到连接池中
      if (!CONNECT_POOL.ContainsKey(user))
       CONNECT_POOL.Add(user, socket);//不存在,添加
      else
       if (socket != CONNECT_POOL[user])//当前对象不一致,更新
       CONNECT_POOL[user] = socket;
      #endregion
     
      #region 离线消息处理
      if (MESSAGE_POOL.ContainsKey(user))
      {
       List<MessageInfo> msgs = MESSAGE_POOL[user];
       foreach (MessageInfo item in msgs)
       {
       await socket.SendAsync(item.MsgContent, WebSocketMessageType.Text, true, CancellationToken.None);
       }
       MESSAGE_POOL.Remove(user);//移除离线消息
      }
      #endregion
     
      string descUser = string.Empty;//目的用户
      while (true)
      {
       if (socket.State == WebSocketState.Open)
       {
       ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[2048]);
       WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None);
        
       #region 消息处理(字符截取、消息转发)
       try
       {
        #region 关闭Socket处理,删除连接池
        if (socket.State != WebSocketState.Open)//连接关闭
        {
        if (CONNECT_POOL.ContainsKey(user)) CONNECT_POOL.Remove(user);//删除连接池
        break;
        }
        #endregion
     
        string userMsg = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);//发送过来的消息
        string[] msgList = userMsg.Split('|');
        if (msgList.Length == 2)
        {
        if (msgList[0].Trim().Length > 0)
         descUser = msgList[0].Trim();//记录消息目的用户
        buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(msgList[1]));
        }
        else
        buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(userMsg));
     
        if (CONNECT_POOL.ContainsKey(descUser))//判断客户端是否在线
        {
        WebSocket destSocket = CONNECT_POOL[descUser];//目的客户端
        if (destSocket != null && destSocket.State == WebSocketState.Open)
         await destSocket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
        }
        else
        {
        Task.Run(() =>
        {
         if (!MESSAGE_POOL.ContainsKey(descUser))//将用户添加至离线消息池中
         MESSAGE_POOL.Add(descUser, new List<MessageInfo>());
         MESSAGE_POOL[descUser].Add(new MessageInfo(DateTime.Now, buffer));//添加离线消息
        });
        }
       }
       catch (Exception exs)
       {
        //消息转发异常处理,本次消息忽略 继续监听接下来的消息
       }
       #endregion
       }
       else
       {
       break;
       }
      }//while end
      }
      catch (Exception ex)
      {
      //整体异常处理
      if (CONNECT_POOL.ContainsKey(user)) CONNECT_POOL.Remove(user);
      }
     }
     
     public bool IsReusable
     {
      get
      {
      return false;
      }
     }
     }
    }
  • 相关阅读:
    今天是元旦啊
    [待解决]python 函数加括号和不加括号的区别
    Jupyter Notebook的快捷键列表误操作发现的新大陆
    Series选择和过滤
    做鸢尾花切片练习中的'&'问题:(&,|)和(and,or)
    报错合集
    关于随机数种子seed的问题尽量使用numpy下的seed
    pandas创建Series序列/hashable
    在jupyter notebook中插入截图
    xml反序列化时,如何生成与之对应的类文件
  • 原文地址:https://www.cnblogs.com/heroxiaohuihui/p/WebSocket.html
Copyright © 2020-2023  润新知