在项目里面引入了websocket,想着建立长连接后,前后端便能做到双工通信,这样执行时间比较长的操作,就不用担心超时了,还可以加上比较顺滑的滚动条效果。
当实现了传统的websocket方式后,想着继续试试之前用到的signalR,这样开始了部署signalR服务的过程,部署了signalR方式,紧接着开始了即时通信的改版优化,一发不可收拾,截止目前为止,即时通信算告一段落,基础功能也就有了,支持私聊、群组聊、自定义群组,发送图片、视频、文字等信息。
这一过程完全搭载在自己的NAS上面,采用自带的k3s部署,由提交代码,jekins构建、自动生成docker、上传到阿里云仓库,最后部署k3s,整整一套全流程,有时间后续慢慢介绍,在自建的工作台上面实现即时通信,只是试试工作台的产出效率如何,就当一个实验品,还有快速搭建工具类的网页,支持手机端采集照片,快速搭建门户网站等等,现在先说一下部署的signalR服务吧。
场景
在前端发起建立websocket请求,并与signalR服务连接,采用网关的方式去建立连接。其他的方法通过管理平台的边界服务进行管理
需要的服务
前端、网关、SignalR、管理平台(已经搭建好的,需要权限认证)
搭建网关
搭建网关(需要部署一个网关服务),使用app.UseWebSockets();监听websocket,修改配置文件,如下
{
"DownstreamPathTemplate": "/{catchAll}",
"DownstreamScheme": "ws",
"DownstreamHostAndPorts": [
{
"Host": "ansheng-signalr-nodeport.ansheng",
"Port": "80"
}
],
"UpstreamPathTemplate": "/gateway/{catchAll}",
"UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ]
}
搭建signalR服务
- 配置服务(ConfigureServices)里面添加SignalR服务
services.AddSignalR();
- 为了获取UserId,使用UserId进行发送、接收消息,添加IUserIdProvider实现类UserIdProvider
public class UserIdProvider : IUserIdProvider
{
public string GetUserId(Microsoft.AspNetCore.SignalR.HubConnectionContext connection)
{
return connection.GetHttpContext().Request.Query["userid"];
}
}
- 进行依赖注入
services.TryAddScoped(typeof(IUserIdProvider), typeof(UserIdProvider));
- 配置端口入口
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<MsgHub>("msghub");
});
- 实现MsgHub类
public class MsgHub : Hub
{
private IHttpContextAccessor _httpContextAccessor;
private IUserIdProvider UserIdProvider;
public MsgHub(IHttpContextAccessor httpContextAccessor, IUserIdProvider userIdProvider)
{
_httpContextAccessor = httpContextAccessor;
UserIdProvider = userIdProvider;
}
public async Task ConnectServer(string ConnectionId)
{
await Clients.User(Context.UserIdentifier).SendAsync("Connect", "已成功连接服务");
}
public async Task SendMessage(string message)
{
await Clients.User(Context.UserIdentifier).SendAsync("ReceiveMessage", message);
}
}
注意这里的Context.UserIdentifier当我们实现了IUserIdProvider的GetUserId方法后,这里获取的就是前端请求传递的userid。
设置前端
现在就需要前端发起websocket请求,并建立连接
在SignalRService.ts里面实现InitSignalR方法,代码里面的ApiGatewayServiceUrl就是配置的网关地址
InitSignalR(Id:string){
const connection = new signalR.HubConnectionBuilder()
.withUrl(`${environment.ApiGatewayServiceUrl}msghub?userid=${Id}`)
.configureLogging(signalR.LogLevel.Information)
.build();
connection.on('connect', Connect);
connection.on('receivemessage', ShowReceiveMessage);
async function start() {
try {
Object.defineProperty(WebSocket, 'OPEN', { value: 1, });
await connection.start();
connection.send('ConnectServer', connection.connectionId);
console.log("SignalR Connected.");
} catch (err) {
console.log(err);
setTimeout(start, 5000);
}
};
connection.onclose(async () => {
await start();
});
start();
function Connect(msg: string) {
console.dir(msg);
}
function ShowReceiveMessage(msg:string){
console.log(msg);
}
}
注意:这里的Object.defineProperty(WebSocket, 'OPEN', { value: 1, });一定是显式的明确webSocket是打开状态,因为当前依赖的signalR组件依赖这个状态值,不指明的话,会报如下错误
signalr.js:2273 Unhandled Promise rejection: WebSocket is not in the OPEN state ; Zone: <root> ; Task: null ; Value: WebSocket is not in the OPEN state undefined
大概意思是
这个错误消息通常表示尝试在WebSocket连接尚未打开(即,它正在CONNECTING状态,或者已经被CLOSING或CLOSED)时发送消息。
对于WebSocket API来说,只有当WebSocket实例的readyState属性是WebSocket.OPEN(值为1)时,你才可以通过WebSocket实例安全地发送消息。其他所有状态都可能导致INVALID_STATE_ERR异常。
最后配置
在lucky里面配置转发,将暴露的外网域名地址转发到内网(网关的节点地址),这样实现外网访问通过网关与signalR服务建立连接。当建立连接后,后续的用户添加群组,退出群组等操作,均通过调用边界服务接口(为了控制权限)实现。
注意:这里的反向代理设置时,一定加上跨域,并且指明允许跨域的地址。
为什么单独部署一个SignalR服务,这样是希望消息总线有单独配置资源的服务,如果应用在实际的生产环境,方便资源的追加,有效支持即时通信服务的正常运行。
当前的实现并没有实现对用户的认证,这个可以在后期加上。