.Net Core中使用WebSocket
一、WebSocket是什么
初次接触WebSocket,大家都会问:我们已经有了HTTP协议,为什么还需要WebSocket?
因为HTTP协议中通信只能由客户端发起,而WebSocket协议中服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,实现了浏览器与服务器全双工通信(full-duplex),WebSocket属于服务器推送技术的一种。
WebSocket是HTML5的一种新协议,它使用JavaScript调用浏览器的API发出一个WebSocket请求至服务器,复用HTTP的握手通道经过一次握手和服务器建立了TCP通讯,因为它本质上是一个TCP连接,所以数据传输的稳定性强和数据传输量比较小。
二、WebSocket的优势
建立在TCP协议之上,服务器端的实现比较容易
与HTTP协议有着良好的兼容性,默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易被屏蔽能通过各种HTTP代理服务器。
数据格式比较轻量,性能开销小。连接创建后,ws客户端和服务器端进行数据交换时,协议控制的数据包头部较小。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。而HTTP协议每次通信都需要携带完整的头部。
可以发送文本,也可以发送二进制数据。
没有同源限制,客户端可以与任意服务器通信。
协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
支持扩展。ws协议定义了扩展,用户可以扩展协议或者实现自定义的子协议。(比如支持自定义压缩算法等)
三、在.NET Core中利用WebSocket实现简易在线聊天室
因为WebSocket复用了HTTP的握手通道与服务器建立连接,所以WebSocket的握手就是一次http请求,因此我们就可以使用一个middleware来识别并拦截WebSocket请求,把客户端与服务器建立的WebSocket连接统一进行管理,其实微软已经帮我们简单的封装过了。
1、新建WebsocketClientCollection类对客户端与服务器建立的WebSocket连接进行统一管理
public class WebsocketClientCollection{
private static List<WebsocketClient> _clients = new List<WebsocketClient>();
public static void Add(WebsocketClient client)
{
_clients.Add(client);
}
public static void Remove(WebsocketClient client)
{
_clients.Remove(client);
}
public static WebsocketClient Get(string clientId)
{
var client = _clients.FirstOrDefault(c => c.Id == clientId);
return client;
}
public static List<WebsocketClient> GetAll()
{
return _clients;
}
public static List<WebsocketClient> GetClientsByRoomNo(string roomNo)
{
var client = _clients.Where(c => c.RoomNo == roomNo);
return client.ToList();
}
}
3、新建WebsocketHandlerMiddleware并识别和接收WebSocket请求;WebsocketHandlerMiddleware就是我们管理WebSocket连接的入口,我们可以在Invoke()方法中先用context.WebSockets.IsWebSocketRequest来识别WebSocket请求,然后调用context.WebSockets.AcceptWebSocketAsync()方法把请求转换为WebSocket连接。
public async Task Invoke(HttpContext context)
{
if (context.Request.Path == "/ws")
{
//仅当网页执行new WebSocket("ws://localhost:5000/ws")时,后台会执行此逻辑
if (context.WebSockets.IsWebSocketRequest)
{
//后台成功接收到连接请求并建立连接后,前台的webSocket.onopen = function (event){}才执行
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
string clientId = Guid.NewGuid().ToString(); ;
var wsClient = new WebsocketClient
{
Id = clientId,
WebSocket = webSocket
};
try
{
await Handle(wsClient);
}
catch (Exception ex)
{
logger.LogError(ex, "Echo websocket client {0} err .", clientId);
await context.Response.WriteAsync("closed");
}
}
else
{
context.Response.StatusCode = 404;
}
}
else
{
await next(context);
}
}
4、在Handle()方法中循环接收客户端发送到后台的消息
private async Task Handle(WebsocketClient websocketClient)
{
WebsocketClientCollection.Add(websocketClient);
logger.LogInformation($"Websocket client added.");
WebSocketReceiveResult clientData = null;
do
{
var buffer = new byte[1024 * 1];
//客户端与服务器成功建立连接后,服务器会循环异步接收客户端发送的消息,收到消息后就会执行Handle(WebsocketClient websocketClient)中的do{}while;直到客户端断开连接
//不同的客户端向服务器发送消息后台执行do{}while;时,websocketClient实参是不同的,它与客户端一一对应
//同一个客户端向服务器多次发送消息后台执行do{}while;时,websocketClient实参是相同的
clientData = await websocketClient.WebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (clientData.MessageType == WebSocketMessageType.Text && !clientData.CloseStatus.HasValue)
{
var msgString = Encoding.UTF8.GetString(buffer);
logger.LogInformation($"Websocket client ReceiveAsync message {msgString}.");
var message = JsonConvert.DeserializeObject<Message>(msgString);
message.SendClientId = websocketClient.Id;
HandleMessage(message);
}
} while (!clientData.CloseStatus.HasValue);
//关掉使用WebSocket连接的网页/调用webSocket.close()后,与之对应的后台会跳出循环
WebsocketClientCollection.Remove(websocketClient);
logger.LogInformation($"Websocket client closed.");
}
5、在HandleMessage()方法中对客户端发送到后台的消息进行解析并处理,最后推送处理结果到客户端
private void HandleMessage(Message message)
{
var client = WebsocketClientCollection.Get(message.SendClientId);
switch (message.action)
{
case "join":
client.RoomNo = message.roomNo;
client.SendMessageAsync($"{message.nick} join room {client.RoomNo} success .");
logger.LogInformation($"Websocket client {message.SendClientId} join room {client.RoomNo}.");
break;
case "send_to_room":
if (string.IsNullOrEmpty(client.RoomNo))
{
break;
}
var clients = WebsocketClientCollection.GetClientsByRoomNo(client.RoomNo);
clients.ForEach(c =>
{
c.SendMessageAsync(message.nick + " : " + message.msg);
});
logger.LogInformation($"Websocket client {message.SendClientId} send message {message.msg} to room {client.RoomNo}");
break;
case "leave":
#region 通过把连接的RoomNo置空模拟关闭连接
var roomNo = client.RoomNo;
client.RoomNo = "";
#endregion
#region 后台关闭连接
//client.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
//WebsocketClientCollection.Remove(client);
#endregion
client.SendMessageAsync($"{message.nick} leave room {roomNo} success .");
logger.LogInformation($"Websocket client {message.SendClientId} leave room {roomNo}");
break;
default:
break;
}
}
6、在startup中配置中间件
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(60),
ReceiveBufferSize = 1 * 1024
});
app.UseMiddleware<WebsocketHandlerMiddleware>();
7、修改index.cshtml来实现一个简单的聊天室UI
<div style="margin-bottom:5px;">
room no: <input type="text" id="txtRoomNo" value="99999" />
<button id="btnJoin">join room</button>
<button id="btnLeave">leave room</button>
<button id="btnDisConnect">DisConnect</button>
</div>
<div style="margin-bottom:5px;">
nick name: <input type="text" id="txtNickName" value="batman" />
</div>
<div style="height:300px;width:600px">
<textarea style="height:100%;width:100%" id="msgList"></textarea>
<div style="text-align: right">
<input type="text" id="txtMsg" value="" /> <button id="btnSend">send</button>
</div>
</div>
8、使用JavaScript来处理WebSocket连接并与服务器进行通信,现代浏览器已经都支持WebSocket协议,JavaScript运行时也内置了WebSocket类,我们仅仅需要new一个WebSocket对象出来就可以利用他与后台进行双工通信。
var server = "ws://localhost:5000";//若开启了https则这里是wss
var webSocket = new WebSocket(server + "/ws");
//前台向后台发送连接请求,后台成功接收并建立连接后才会触发此事件
webSocket.onopen = function (event) {
console.log("Connection opened...");
$("#msgList").val("WebSocket connection opened");
};
//后台向前台发送消息,前台成功接收后会触发此事件
webSocket.onmessage = function (event) {
console.log("Received message: " + event.data);
if (event.data) {
var content = $('#msgList').val();
content = content + '\r\n' + event.data;
$('#msgList').val(content);
}
};
//后台关闭连接后/前台关闭连接后都会触发此事件
webSocket.onclose = function (event) {
console.log("Connection closed...");
var content = $('#msgList').val();
content = content + '\r\nWebSocket connection closed';
$('#msgList').val(content);
};
$('#btnJoin').on('click', function () {
var roomNo = $('#txtRoomNo').val();
var nick = $('#txtNickName').val();
if (!roomNo) {
alert("请输入RoomNo");
return;
}
var msg = {
action: 'join',
roomNo: roomNo,
nick: nick
};
if (CheckWebSocketConnected(webSocket)) {
webSocket.send(JSON.stringify(msg));
}
});
$('#btnSend').on('click', function () {
var message = $('#txtMsg').val();
var nick = $('#txtNickName').val();
if (!message) {
alert("请输入发生的内容");
return;
}
if (CheckWebSocketConnected(webSocket)) {
webSocket.send(JSON.stringify({
action: 'send_to_room',
msg: message,
nick: nick
}));
}
});
$('#btnLeave').on('click', function () {
var nick = $('#txtNickName').val();
var msg = {
action: 'leave',
roomNo: '',
nick: nick
};
if (CheckWebSocketConnected(webSocket)) {
webSocket.send(JSON.stringify(msg));
}
});
$("#btnDisConnect").on("click", function () {
if (CheckWebSocketConnected(webSocket)) {
//部分浏览器调用close()方法关闭WebSocket时不支持传参
//webSocket.close(001, "closeReason");
webSocket.close();
}
});
9、至此我们的聊天室已经搭建完成了,项目运行之后我们启动两个页面,进入相同的房间号就能聊天了
四、常见问题解答
1、Error during WebSocket handshake: Unexpected response code: 404;当VS设置使用IIS Express启动,但IIS没安装WebSocket时,会出现这个错误,解决方法有两个:①IIS安装WebSocket,②设置为项目自托管启动。