即時通信常用手段
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
;
}
}
}
}
|