视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
.NET实现WebSocket服务端即时通信实例
2020-11-27 22:35:55 责编:小采
文档

即时通信常用手段

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).服务端获取连接对象并存储到连接池中

CONNECT_POOL.Add(user, socket); 

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

代码如下:
WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None);

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

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

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

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

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

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客户端接收到推送过来的消息

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下即可)

<!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="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。记录当前离线消息的时间、内容)

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;
 }
 }
 }
}

下载本文
显示全文
专题