我们知道,在传统的HTTP中,只能客户端主动向服务器端发起请求,服务器端无法主动向客户端发送消息。
有的业务场景下,我们需要服务器端主动向客户端发送消息。
比如 Web聊天室就需要服务器端主动向客户端发送新收到的消息,OA系统就需要服务器端主动向客户端发送请求申请审批结果,站内消息系统就需要服务器端主动通知客户端”有新消息”。
我们可以用长轮询(long polling)来实现这样的功能,也就算浏览器端先向服务器端发送AJAX请求。
但是服务器端不立即给浏览器端发送响应,而是一直挂起这个请求。
直到服务器端有需要推送给客户端的消息,服务器端才把要推送的消息作为响应发送给浏览器端。
由于HTTP并不是为这种长轮询机制设计的,因此长轮询对服务器的资源消耗非常大;
而且由于HTTP是文本传输协议,因此数据传输效率低。
为了实现服务器端向客户端推送消息,在2008年诞生了WebSocket协议,并且该协议在2011年成为国际标准。
目前所有的主流浏览器都已经支持WebSocket协议。
WebSocket基于TCP,支持二进制通信,因此通信效率非常高,它可以让服务器处理大量的并发WebSocket连接;
WebSocket是双工通信,因此服务器可以高效地向客户端推送消息。
ASP.NET Core SingnalR是.NET Core平台中对WebSocket的封装,从而让开发人员可以更简单地进行WebSocket开发。
1. SignalR基本使用
虽然WebSocket是独立于HTTP的,但是我们一般仍然把WebSocket服务器端部署到Web服务器上。
因为我们需要借助HTTP完成初始的握手,并且共享HTTP服务器的端口。
这样就可以避免为WebSocket单独打开新的服务器端口。
因此,SignalR的服务器端一般运行在ASP.NET Core项目中。
SignalR中一个重要的组件是集线器(hub),它用于在WebSocket服务器端和所有客户端之间进行数据交换,所有连接到同一个集线器上的程序都可以互相通信。
我们既可以通过集线器来完成服务器端向客户端的消息推送,也可以完成客户端之间的消息推送,当然WebSocket也允许客户端向服务器端发送消息。
下面通过开发一个简单的聊天室来了解SignalR的基本使用。
第一步:
创建一个WebAPI项目,并且在项目中创建一个继承自Hub类的ChatRoomHub类,所有的客户端和服务器端都通过这个集线器进行通信,如以下代码:
public class ChatRoomHub:Hub
{
public Task SendPublicMessage(string message)
{
//获取唯一标识
string connId = this.Context.ConnectionId;
string receivePublicMessage = $"{connId} {DateTime.Now}:{message}";
//发送消息
return Clients.All.SendAsync("ReceivePublicMessage", receivePublicMessage);
}
}
ChatRoomHub类中定义的方法可以被客户端调用,也就是客户端可以向服务器端发送请求。
方法的参数就是客户端向服务器端传送的消息,参数的个数原则上来讲不受限制,而且参数的类型支持string、int等常用数据类型。
第二步:
编辑Program.cs,在bulider.Build之前调用builder.Services.AddSignalR注册所有SignalR的服务。
在app.MapControllers之前调用app.MapHub<ChatRoomHub>(“/Hubs/ChatRoomHub”)启用SignalR中间件,并且设置客户端请求此路径时,由ChatRoomHub处理。
修改后的Program.cs的主干内容如以下代码所示:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//启用SignalR
builder.Services.AddSignalR();
//跨域
builder.Services.AddCors(policy =>
{
policy.AddPolicy("CorsPolicy", opt => opt
.WithOrigins("http://127.0.0.1:5173")
.AllowAnyHeader()
.AllowAnyMethod().AllowCredentials());
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub");
app.MapControllers();
app.Run();
第三步:
我们需要编写一个静态HTML页面提供交互界面。
按照前后端分离的理念,应把HTML页面放到一个单独的前端项目中。
我们在这里使用Vue项目并按照软件开发工具包 microsoft/singnalr
在SignalR的JavaScript客户端中,我们使用HubConnectionBuilder来创建从客户端到服务器端的连接:
通过withUrl方法来设置服务器端集线器的地址,该地址必须是包含域名等的全路径,必须和服务器端MapHub设置的路径一致;
通过withAutomaticReconnect设置自动重连机制。
虽然withAutomaticReconnect不是必须设置的,但是设置这个选项之后,如果连接被断开,客户端就会尝试重连,使用更方便。
需要注意的是,客户端重连之后,由于这是一个新的连接,因此在服务器端获得的ConnectionId是一个新的值。
对HubConnectionBuilder设置完成后,我们调用build就可以构建完成一个客户端到集线器的连接。
我们通过build获得的到集线器的连接只是逻辑上的连接,还需要调用start方法来实际启动连接。
一旦连接建立完成,我们就可以通过连接对象的invoke函数来调用集线器中的方法,我们也可以通过on函数来注册监听服务器端使用SendAsync的代码。
<script setup>
import { reactive,onMounted } from 'vue';
import* as singnalR from '@microsoft/signalr'
let connection;
const state = reactive({userMessage:"",messages:[]});
//对用户在输入框内的按键进行监听
const txtMsgOnKeypress = async function name(e)
{
if(e.keyCode !=13) return;
//把用户输入消息发送服务器端再转发给连接集线器的全部客户端
await connection.invoke("SendPublicMessage",state.userMessage);
state.userMessage = "";
}
onMounted(async (params) => {
connection = new singnalR.HubConnectionBuilder()
.withUrl('https://localhost:7115/Hubs/ChatRoomHub')
.withAutomaticReconnect().build();
await connection.start();
connection.on('ReceivePublicMessage',msg=>{
state.messages.push(msg);
});
connection.onclose(async () => {
console.log('连接失败. Attempting to reconnect...');
});
return {state,txtMsgOnKeypress};
})
</script>
<template>
<input type="text" v-model="state.userMessage" v-on:keypress = "txtMsgOnKeypress"/>
<div>
<ul>
<li v-for="(msg,index) in state.messages" :key="index">{{ msg }}</li>
</ul>
</div>
</template>
<style scoped>
</style>
接下来启动,然后打开两个聊天室页面,并分别在两个页面中发送一些消息。
我们可以看到A页面发送的消息,在B页面中能立即看到,与之相对应的情况依然可以看到。