初次接触WebSocket,大家都会问:我们已经有了HTTP协议,为什么还需要WebSocket?
因为HTTP协议中通信只能由客户端发起,而WebSocket协议中服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,实现了浏览器与服务器全双工通信(full-duplex),WebSocket属于服务器推送技术的一种。
WebSocket是HTML5的一种新协议,它使用JavaScript调用浏览器的API发出一个WebSocket请求至服务器,复用HTTP的握手通道经过一次握手和服务器建立了TCP通讯,因为它本质上是一个TCP连接,所以数据传输的稳定性强和数据传输量比较小。
因为WebSocket复用了HTTP的握手通道与服务器建立连接,所以WebSocket的握手就是一次http请求,因此我们就可以使用一个middleware来识别并拦截WebSocket请求,把客户端与服务器建立的WebSocket连接统一进行管理,其实微软已经帮我们简单的封装过了。
1、创建Core框架的Web项目
2、新建WebsocketClientCollection类对客户端与服务器建立的WebSocket连接进行统一管理
public class WebsocketClientCollection
{
private static List _clients = new List();
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 GetAll()
{
return _clients;
}
public static List 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(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(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();
7、修改index.cshtml来实现一个简单的聊天室UI
room no:
nick name:
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,②设置为项目自托管启动。
本文借鉴了https://www.cnblogs.com/kklldog/p/core-for-websocket.html,在此基础上对代码做了完善并加上了自己的理解,如果觉得本文对您有帮助的话,请点赞、评论鼓励下,谢谢。