• 用.NET实现WebSocket后端


    以下内容都摘自其它网页,作为学习的笔记,其中可能存在修改,具体可查看来源页。

    WebSocket

    概述

    来源:https://www.runoob.com/html/html5-websocket.html

    WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

    WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

    在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

    现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

    HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

    客户端部分

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

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

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

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

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

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

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <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="~/Scripts/jquery-3.3.1.min.js"></script>
        <script>
            var ws;
            $().ready(function () {
                $('#conn').click(function () {
                    ws = new WebSocket('ws://' + window.location.hostname + ':' + window.location.port + '/api/values/' + $("#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="to" type="text" />
            <input id="send" type="button" value="发送" /><br />
            <div id="msg">
            </div>
        </div>
    </body>
    </html>
    View Code

    服务器端部分

    服务器端使用WebAPI,原例子是用的Handler来做,主要用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。记录当前离线消息的时间、内容)

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Web;
    using System.Web.Http;
    using System.Net.WebSockets;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web.WebSockets;
    
    namespace WebSocketService.Controllers
    {
        public class ValuesController : ApiController
        {
            private static Dictionary<string, WebSocket> CONNECT_POOL = new Dictionary<string, WebSocket>();//用户连接池
            private static Dictionary<string, List<MessageInfo>> MESSAGE_POOL = new Dictionary<string, List<MessageInfo>>();//离线消息池
            //[Route]
            [HttpGet]
            public HttpResponseMessage Get(string user)
            {
                //在服务端接受web socket请求,传入的函数作为web socket的处理函数,待web socket建立后该函数会被调用,
                //在该函数中可以对web socket进行消息收发
                HttpContext.Current.AcceptWebSocketRequest(ProcessRequest);
    
                //构造同意切换至web socket的response
                return Request.CreateResponse(HttpStatusCode.SwitchingProtocols);
            }
            private async Task ProcessRequest(AspNetWebSocketContext context)
            {
                WebSocket socket = context.WebSocket;
                //HttpContextBase context2 = (HttpContextBase)Request.Properties["MS_HttpContext"];//获取传统context
                //HttpRequestBase request = context2.Request;//定义传统request对象
                string user = context.RawUrl.Substring(context.RawUrl.LastIndexOf('/') + 1);// request.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
                                {
                                    await 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);
                }
            }
    
            /// <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; }
            }
            // POST api/values
            public void Post([FromBody]string value)
            {
            }
    
            // PUT api/values/5
            public void Put(int id, [FromBody]string value)
            {
            }
    
            // DELETE api/values/5
            public void Delete(int id)
            {
            }
        }
    }
    View Code

    PS:对Dictionary的操作可以写两个方案Add和Remove,并用lock,这样子并发会稳定点

    在本地测试过程中遇到的问题:

    1、WebSocket请求提示握手失败,这个是因为IIS没有安装WebSocket协议,在如下目录安装

     

     来源:https://www.cnblogs.com/Tsong/p/6385585.html

  • 相关阅读:
    【MongoDB】Re02 文档CRUD
    【Zookeeper】Win平台伪集群搭建
    【HalfLife】 Steam 安装半条命模组
    【Kafka】03 Shell 操作
    【Kafka】02 原生集群部署
    【Hessian】轻量级分布式通信组件
    【MySQL】主从配置
    【Excel】VBA编程 02 访问MySQL数据库
    【MongoDB】Re01 安装与基础操作
    【Oracle】SQL笔记
  • 原文地址:https://www.cnblogs.com/hsybs/p/13517403.html
Copyright © 2020-2023  润新知