什么是Websocket
我們在傳統的客戶端程式要實作實時雙工通訊第一想到的技術就是socket通訊,但是在web體系是用不了socket通訊技術的,因為http被設計成無狀態,每次跟服務器通訊完成后就會斷開連接,
在沒有websocket之前web系統如果要做雙工通訊往往使用http long polling技術,http long polling 每次往服務器發送請求后,服務端不會立刻回傳資訊來結束請求,而是一直掛著直到有資料需要回傳,或者等待超時了才會回傳,客戶端在結束上一次請求后立刻再發送一次請求,如此反復,http long polling雖然能實作web系統的雙工通訊,但是有個很大的問題,就是基于http協議客戶端每次發送請求都需要攜帶巨大的頭部,在并發互動少量資料的時候非常不劃算,對服務器資源的消耗也是巨大的,
websocket很好的改善了以上問題,它基于tcp重新設計了一套協議,同時又兼容http,默認跟http一樣使用80/443埠,websocket鏈接建立本質上就是一次http請求,直接使用http協議的upgrade頭來標識這是一次websocket請求,服務端回復101狀態碼表示“握手”成功,
//客戶端請求
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
//服務端回應
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/
使用asp.net core來處理websocket
上面我們簡單的了解了websocket,那么如何來使用asp.net core處理websocket呢?因為websocket的握手就是一次http請求,那么我們就可以使用一個middleware來攔截websocket的請求,把建立的鏈接統一進行管理,其實微軟已經幫我們簡單的封裝過了,
新建一個asp.net core網站

新建WebsocketHandlerMiddleware中間件
這個中間件就是我們管理websocket鏈接的入口,我們呼叫context.WebSockets.AcceptWebSocketAsync()方法把請求轉換為websocket鏈接,
在Invoke方法內接收websocket鏈接
public async Task Invoke(HttpContext context)
{
if (context.Request.Path == "/ws")
{
if (context.WebSockets.IsWebSocketRequest)
{
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);
}
}
在Hanle方法等待客戶端的訊息
private async Task Handle(WebsocketClient webSocket)
{
WebsocketClientCollection.Add(webSocket);
_logger.LogInformation($"Websocket client added.");
WebSocketReceiveResult result = null;
do
{
var buffer = new byte[1024 * 1];
result = await webSocket.WebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text && !result.CloseStatus.HasValue)
{
var msgString = Encoding.UTF8.GetString(buffer);
_logger.LogInformation($"Websocket client ReceiveAsync message {msgString}.");
var message = JsonConvert.DeserializeObject<Message>(msgString);
message.SendClientId = webSocket.Id;
MessageRoute(message);
}
}
while (!result.CloseStatus.HasValue);
WebsocketClientCollection.Remove(webSocket);
_logger.LogInformation($"Websocket client closed.");
}
在MessageRoute方法內對客戶端的訊息進行轉發
對客戶端的訊息定義幾個標準的action,對不同的action進行特定的處理,比如加入房間、離開房間、在房間內廣播訊息等,
private void MessageRoute(Message message)
{
var client = WebsocketClientCollection.Get(message.SendClientId);
switch (message.action)
{
case "join":
client.RoomNo = message.msg;
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.GetRoomClients(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":
var roomNo = client.RoomNo;
client.RoomNo = "";
client.SendMessageAsync($"{message.nick} leave room {roomNo} success .");
_logger.LogInformation($"Websocket client {message.SendClientId} leave room {roomNo}");
break;
default:
break;
}
}
新建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> GetRoomClients(string roomNo)
{
var client = _clients.Where(c => c.RoomNo == roomNo);
return client.ToList();
}
}
在Startup中使用中間件
有了上面的中間件,我們需要use一下,
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(60),
ReceiveBufferSize = 1* 1024
});
app.UseMiddleware<WebsocketHandlerMiddleware>();
到此我們的服務端基本完成了,下面進行客戶端html跟JavaScript的撰寫,
撰寫客戶端界面
修改index.cshtml來實作一個簡單的聊天室ui,
<div style="margin-bottom:5px;">
room no: <input type="text" id="txtRoomNo" value=https://www.cnblogs.com/kklldog/p/"8888"/>



