MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Visual Basic SignalR实时通讯实现

2024-04-227.0k 阅读

一、SignalR 简介

(一)什么是 SignalR

SignalR 是一个用于 ASP.NET 应用程序的开源库,它简化了向应用程序添加实时 Web 功能的过程。实时 Web 功能是指服务器代码能够即时将更新推送到连接的客户端的功能。传统的 Web 应用程序采用请求 - 响应模型,客户端发起请求,服务器响应请求并返回数据。而实时 Web 应用程序则打破了这种模式,允许服务器主动向客户端发送数据,这对于诸如实时仪表板、聊天应用程序、协作工具等场景非常有用。

(二)SignalR 的工作原理

SignalR 支持多种传输机制,以适应不同的客户端和网络环境。主要的传输方式包括 WebSockets、Server - Sent Events(SSE)和长轮询。当客户端连接到 SignalR 服务器时,SignalR 会自动选择最佳的传输方式。

  1. WebSockets:这是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务器和客户端之间进行实时、双向的通信,是最理想的实时通讯方式。在支持 WebSockets 的浏览器和服务器环境下,SignalR 优先使用 WebSockets 进行传输。
  2. Server - Sent Events(SSE):这是一种允许 Web 服务器将实时更新发送到 Web 浏览器的技术。它基于 HTTP 协议,是单向的,即服务器向客户端发送数据。当 WebSockets 不可用时,SignalR 可能会选择 SSE 作为传输方式。
  3. 长轮询:长轮询是一种模拟实时通信的技术。客户端向服务器发送请求,服务器保持连接打开,直到有数据可用或者连接超时。然后服务器关闭连接,客户端立即重新发起请求。这种方式在不支持 WebSockets 和 SSE 的环境下提供了一种实时通讯的替代方案。

二、在 Visual Basic 项目中引入 SignalR

(一)创建 Visual Basic 项目

首先,打开 Visual Studio,并创建一个新的 Visual Basic 项目。你可以选择创建一个 ASP.NET Web 应用程序项目,这将为我们提供一个基础的 Web 应用程序框架来集成 SignalR。

  1. 打开 Visual Studio。
  2. 选择“新建项目”。
  3. 在“新建项目”对话框中,选择“Visual Basic”>“Web”,然后选择“ASP.NET Web 应用程序”。
  4. 为项目命名并选择项目的保存位置,然后点击“确定”。
  5. 在“新建 ASP.NET Web 应用程序”对话框中,选择合适的模板(如“空”模板,以便我们可以更灵活地添加 SignalR 相关内容),然后点击“确定”。

(二)安装 SignalR NuGet 包

SignalR 可以通过 NuGet 包管理器方便地添加到项目中。

  1. 在“解决方案资源管理器”中,右键点击项目名称,选择“管理 NuGet 程序包”。
  2. 在“NuGet 程序包管理器”窗口中,切换到“浏览”选项卡。
  3. 在搜索框中输入“Microsoft.AspNet.SignalR”,然后在搜索结果中找到“Microsoft.AspNet.SignalR”包并点击“安装”。
  4. NuGet 会自动下载并安装 SignalR 及其相关依赖项到项目中。安装完成后,你可以在“解决方案资源管理器”的“引用”文件夹中看到 SignalR 相关的引用。

三、创建 SignalR Hub

(一)什么是 Hub

Hub 是 SignalR 中的核心概念,它是一个服务器端类,允许客户端和服务器之间进行双向通信。通过 Hub,你可以定义方法,这些方法可以从客户端调用,同时也可以从服务器端调用客户端的方法。

(二)创建 Hub 类

在 Visual Basic 项目中,创建一个新的类文件用于定义 Hub。例如,创建一个名为“ChatHub.vb”的文件。

Imports Microsoft.AspNet.SignalR

Public Class ChatHub
    Inherits Hub
    Public Sub Send(message As String)
        Clients.All.broadcastMessage(Context.ConnectionId, message)
    End Sub
End Class

在上述代码中:

  1. 首先,通过Imports Microsoft.AspNet.SignalR导入 SignalR 命名空间。
  2. 定义了一个ChatHub类,它继承自Hub类。
  3. Send方法是定义在 Hub 中的一个方法,它接受一个字符串参数message。在这个方法中,使用Clients.All.broadcastMessage调用所有连接客户端的broadcastMessage方法,并传递当前连接的ConnectionId和接收到的message。这里的Context.ConnectionId是 SignalR 为每个连接分配的唯一标识符。

四、配置 SignalR

(一)配置 SignalR 路由

在 Global.asax 文件(如果项目中没有,需要手动添加)中配置 SignalR 的路由。打开 Global.asax 文件,并在Application_Start方法中添加以下代码:

Imports Microsoft.AspNet.SignalR

Public Class Global_asax
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        RouteTable.Routes.MapHubs()
    End Sub
End Class

在上述代码中,RouteTable.Routes.MapHubs()方法用于注册 SignalR 的路由。这使得 SignalR 能够处理客户端与 Hub 之间的通信请求。

(二)配置传输方式

SignalR 会自动选择最佳的传输方式,但你也可以根据需要手动配置。在 Web.config 文件中,可以添加以下配置来设置传输方式:

<configuration>
    <appSettings>
        <add key="owin:AppStartup" value="YourNamespace.Startup" />
    </appSettings>
    <system.webServer>
        <modules runAllManagedModulesForAllRequests="true">
            <remove name="FormsAuthentication" />
        </modules>
    </system.webServer>
    <system.web>
        <compilation debug="true" targetFramework="4.5" />
        <httpRuntime targetFramework="4.5" />
    </system.web>
    <system.serviceModel>
        <bindings>
            <webHttpBinding>
                <binding name="WebHttpBinding_ISignalRService" crossDomainScriptAccessEnabled="true" />
            </webHttpBinding>
        </bindings>
        <behaviors>
            <endpointBehaviors>
                <behavior name="WebHttpBehavior_ISignalRService">
                    <webHttp />
                </behavior>
            </endpointBehaviors>
        </behaviors>
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
    </system.serviceModel>
</configuration>

在上述配置中,owin:AppStartup指定了 SignalR 的启动类。同时,crossDomainScriptAccessEnabled="true"配置允许跨域访问,这在某些场景下是必要的。

五、客户端实现

(一)引入 SignalR 客户端库

在客户端(通常是 HTML 页面)中,需要引入 SignalR 客户端库。可以通过以下两种方式引入:

  1. 使用 CDN:在 HTML 的<head>标签中添加以下脚本引用:
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.signalR/2.4.2/jquery.signalR.min.js"></script>
  1. 下载并本地引用:从 SignalR 的官方网站下载客户端库文件,并将其添加到项目的脚本文件夹中,然后在 HTML 页面中通过相对路径引用:
<script src="Scripts/jquery.min.js"></script>
<script src="Scripts/jquery.signalR.min.js"></script>

(二)连接到 Hub 并调用方法

以下是一个简单的 HTML 页面示例,展示如何连接到 SignalR Hub 并调用方法:

<!DOCTYPE html>
<html>

<head>
    <title>SignalR Chat</title>
    <script src="Scripts/jquery.min.js"></script>
    <script src="Scripts/jquery.signalR.min.js"></script>
    <script>
        $(function () {
            var chat = $.connection.chatHub;

            chat.client.broadcastMessage = function (connectionId, message) {
                $('#messages').append('<li><strong>' + connectionId + '</strong>: ' + message + '</li>');
            };

            $.connection.hub.start().done(function () {
                $('#sendButton').click(function () {
                    var message = $('#messageInput').val();
                    chat.server.send(message);
                    $('#messageInput').val('');
                });
            });
        });
    </script>
</head>

<body>
    <ul id="messages"></ul>
    <input type="text" id="messageInput" />
    <input type="button" id="sendButton" value="Send" />
</body>

</html>

在上述代码中:

  1. 首先,通过$.connection.chatHub获取到 Hub 的代理对象。这里的chatHub与服务器端定义的ChatHub相对应。
  2. 定义了chat.client.broadcastMessage方法,这是客户端用于接收服务器广播消息的方法。当服务器调用Clients.All.broadcastMessage时,客户端的这个方法会被触发,并将接收到的connectionIdmessage显示在页面的消息列表中。
  3. 通过$.connection.hub.start().done(function () {... })启动 SignalR 连接。连接成功后,为发送按钮绑定点击事件,当用户点击发送按钮时,获取输入框中的消息,并通过chat.server.send(message)调用服务器端 Hub 的Send方法发送消息,然后清空输入框。

六、处理连接管理

(一)连接和断开连接事件

在服务器端的 Hub 中,可以处理客户端连接和断开连接的事件。例如,在ChatHub.vb中添加以下代码:

Imports Microsoft.AspNet.SignalR

Public Class ChatHub
    Inherits Hub
    Public Sub Send(message As String)
        Clients.All.broadcastMessage(Context.ConnectionId, message)
    End Sub

    Public Overrides Function OnConnected() As Task
        Clients.All.userConnected(Context.ConnectionId)
        Return MyBase.OnConnected()
    End Function

    Public Overrides Function OnDisconnected(ByVal stopCalled As Boolean) As Task
        Clients.All.userDisconnected(Context.ConnectionId)
        Return MyBase.OnDisconnected(stopCalled)
    End Function
End Class

在上述代码中:

  1. OnConnected方法在客户端成功连接到 Hub 时被调用。在这个方法中,通过Clients.All.userConnected(Context.ConnectionId)调用所有客户端的userConnected方法,通知其他客户端有新用户连接,同时传递新连接用户的ConnectionId
  2. OnDisconnected方法在客户端断开与 Hub 的连接时被调用。同样,通过Clients.All.userDisconnected(Context.ConnectionId)调用所有客户端的userDisconnected方法,通知其他客户端有用户断开连接,并传递断开连接用户的ConnectionId

(二)在客户端处理连接事件

在客户端的 HTML 页面中,也可以处理连接和断开连接的事件。修改之前的 JavaScript 代码如下:

<!DOCTYPE html>
<html>

<head>
    <title>SignalR Chat</title>
    <script src="Scripts/jquery.min.js"></script>
    <script src="Scripts/jquery.signalR.min.js"></script>
    <script>
        $(function () {
            var chat = $.connection.chatHub;

            chat.client.broadcastMessage = function (connectionId, message) {
                $('#messages').append('<li><strong>' + connectionId + '</strong>: ' + message + '</li>');
            };

            chat.client.userConnected = function (connectionId) {
                $('#messages').append('<li><em>' + connectionId +'has connected</em></li>');
            };

            chat.client.userDisconnected = function (connectionId) {
                $('#messages').append('<li><em>' + connectionId +'has disconnected</em></li>');
            };

            $.connection.hub.start().done(function () {
                $('#sendButton').click(function () {
                    var message = $('#messageInput').val();
                    chat.server.send(message);
                    $('#messageInput').val('');
                });
            }).fail(function (error) {
                console.log('Could not connect:'+ error);
            });
        });
    </script>
</head>

<body>
    <ul id="messages"></ul>
    <input type="text" id="messageInput" />
    <input type="button" id="sendButton" value="Send" />
</body>

</html>

在上述代码中,新增了chat.client.userConnectedchat.client.userDisconnected方法,分别用于在客户端接收到有用户连接和断开连接的通知时,在页面上显示相应的提示信息。同时,在$.connection.hub.start()fail回调函数中,处理连接失败的情况,将错误信息打印到控制台。

七、安全性考虑

(一)身份验证

在实时通讯应用中,身份验证是非常重要的。SignalR 可以与 ASP.NET 的身份验证机制集成。例如,如果你使用 Forms 身份验证,可以在 Global.asax 文件中添加以下代码来确保只有经过身份验证的用户才能连接到 Hub:

Imports Microsoft.AspNet.SignalR
Imports System.Web.Security

Public Class Global_asax
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        RouteTable.Routes.MapHubs()
    End Sub

    Sub Application_PostAuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
        If HttpContext.Current.User IsNot Nothing AndAlso HttpContext.Current.User.Identity.IsAuthenticated Then
            Dim formsIdentity = TryCast(HttpContext.Current.User.Identity, FormsIdentity)
            If formsIdentity IsNot Nothing Then
                Dim ticket = formsIdentity.Ticket
                Dim roles = ticket.UserData.Split(",")
                Dim principal = New GenericPrincipal(formsIdentity, roles)
                HttpContext.Current.User = principal
            End If
        End If
    End Sub
End Class

在上述代码中,Application_PostAuthenticateRequest事件在每次请求经过身份验证后触发。通过获取 Forms 身份验证的票据,并解析其中的用户角色信息,创建一个新的GenericPrincipal对象,并将其赋值给HttpContext.Current.User,以便在 SignalR Hub 中进行授权验证。

(二)授权

在 Hub 类中,可以通过重写AuthorizeHubConnection方法来进行授权验证。例如,在ChatHub.vb中添加以下代码:

Imports Microsoft.AspNet.SignalR
Imports System.Web.Security

Public Class ChatHub
    Inherits Hub
    Public Sub Send(message As String)
        Clients.All.broadcastMessage(Context.ConnectionId, message)
    End Sub

    Public Overrides Function OnConnected() As Task
        Clients.All.userConnected(Context.ConnectionId)
        Return MyBase.OnConnected()
    End Function

    Public Overrides Function OnDisconnected(ByVal stopCalled As Boolean) As Task
        Clients.All.userDisconnected(Context.ConnectionId)
        Return MyBase.OnDisconnected(stopCalled)
    End Function

    Public Overrides Function AuthorizeHubConnection(ByVal request As IRequest) As Boolean
        Return HttpContext.Current.User IsNot Nothing AndAlso HttpContext.Current.User.Identity.IsAuthenticated
    End Function
End Class

在上述代码中,AuthorizeHubConnection方法检查当前用户是否经过身份验证。只有经过身份验证的用户才能成功连接到 Hub,否则连接将被拒绝。

八、性能优化

(一)减少不必要的通信

在设计 SignalR 应用时,应尽量减少不必要的通信。例如,在发送消息时,只向需要接收的客户端发送,而不是向所有客户端广播。可以通过Clients.GroupClients.User等方式实现。

Public Class ChatHub
    Inherits Hub
    Public Sub SendToGroup(message As String, groupName As String)
        Clients.Group(groupName).broadcastMessage(Context.ConnectionId, message)
    End Sub

    Public Sub JoinGroup(groupName As String)
        Groups.Add(Context.ConnectionId, groupName)
    End Sub

    Public Sub LeaveGroup(groupName As String)
        Groups.Remove(Context.ConnectionId, groupName)
    End Sub
End Class

在上述代码中,SendToGroup方法只向指定组的客户端发送消息。JoinGroupLeaveGroup方法用于客户端加入和离开特定的组。

(二)优化传输方式选择

SignalR 自动选择传输方式,但在某些情况下,你可能需要根据应用的特点和目标客户端环境来优化传输方式的选择。例如,如果你的应用主要面向支持 WebSockets 的现代浏览器,可以通过配置尽量优先使用 WebSockets。同时,要注意不同传输方式对服务器资源的消耗,合理调整服务器配置以适应不同的传输需求。

(三)缓存和异步处理

在服务器端处理 SignalR 相关逻辑时,可以使用缓存来减少重复计算和数据库查询。例如,如果某些数据在一段时间内不会发生变化,可以将其缓存起来,避免每次客户端请求时都重新获取。另外,尽可能使用异步方法来处理耗时操作,以避免阻塞 SignalR 的处理线程,提高应用的并发处理能力。

Public Class ChatHub
    Inherits Hub
    Private Shared ReadOnly cache As New Dictionary(Of String, Object)

    Public Async Function GetCachedData() As Task(Of Object)
        If cache.ContainsKey("data") Then
            Return cache("data")
        End If

        Dim data = Await GetDataFromDatabase()
        cache("data") = data
        Return data
    End Function

    Private Async Function GetDataFromDatabase() As Task(Of Object)
        '模拟从数据库获取数据的异步操作
        Await Task.Delay(1000)
        Return "Some data from database"
    End Function
End Class

在上述代码中,GetCachedData方法首先检查缓存中是否存在数据,如果存在则直接返回,否则异步从数据库获取数据并缓存起来。这样可以减少数据库查询的次数,提高应用性能。

九、常见问题及解决方法

(一)连接问题

  1. 无法连接到 SignalR Hub:这可能是由于多种原因导致的。首先,检查网络连接是否正常,确保客户端能够访问服务器。其次,检查 SignalR 的配置是否正确,包括路由配置、传输方式配置等。在客户端,可以通过浏览器的开发者工具查看网络请求,检查是否有与 SignalR 相关的错误信息。在服务器端,可以通过日志记录来排查问题,例如在 Global.asax 文件的Application_Error事件中记录错误日志。
  2. 连接不稳定或频繁断开:这可能与网络环境、传输方式选择或服务器负载有关。如果网络不稳定,可以尝试优化网络设置,或者增加网络重试机制。如果是传输方式选择不当,可以手动配置传输方式,例如强制使用 WebSockets 或长轮询。对于服务器负载过高的情况,需要对服务器进行性能优化,如增加服务器资源、优化代码逻辑等。

(二)消息传递问题

  1. 消息丢失或延迟:消息丢失可能是由于网络抖动或 SignalR 内部处理问题导致的。可以通过增加消息确认机制来解决,例如在客户端发送消息后,等待服务器的确认回复。消息延迟可能与服务器负载、网络延迟或 SignalR 的并发处理能力有关。可以通过优化服务器性能、减少网络延迟和合理配置 SignalR 的并发设置来改善。
  2. 消息重复发送:这可能是由于客户端或服务器端的逻辑错误导致的。在客户端,检查发送消息的逻辑是否正确,避免重复触发发送事件。在服务器端,检查消息处理逻辑,确保不会重复处理相同的消息。可以通过为每条消息添加唯一标识符,并在处理前进行检查来避免重复处理。

(三)跨域问题

如果客户端和服务器端不在同一个域下,可能会遇到跨域问题。解决跨域问题的方法有多种,如在服务器端配置 CORS(Cross - Origin Resource Sharing)。在 Web.config 文件中,可以添加以下配置:

<configuration>
    <system.webServer>
        <httpProtocol>
            <customHeaders>
                <add name="Access - Control - Allow - Origin" value="*" />
                <add name="Access - Control - Allow - Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
                <add name="Access - Control - Allow - Headers" value="Content - Type" />
            </customHeaders>
        </httpProtocol>
    </system.webServer>
</configuration>

在上述配置中,Access - Control - Allow - Origin设置为*表示允许所有来源的跨域请求。在实际应用中,应根据安全需求设置具体的允许来源。同时,也可以使用 SignalR 的跨域配置选项来解决跨域问题,例如在MapHubs方法中设置跨域相关参数。

十、与其他技术的集成

(一)与数据库集成

在许多实时通讯应用中,需要与数据库进行交互,例如存储聊天记录、用户信息等。可以使用 ADO.NET、Entity Framework 等技术来实现与数据库的集成。

以 ADO.NET 为例,假设要将聊天消息存储到 SQL Server 数据库中,可以在ChatHub.vb中添加以下代码:

Imports System.Data.SqlClient

Public Class ChatHub
    Inherits Hub
    Public Sub Send(message As String)
        Clients.All.broadcastMessage(Context.ConnectionId, message)
        SaveMessageToDatabase(Context.ConnectionId, message)
    End Sub

    Private Sub SaveMessageToDatabase(connectionId As String, message As String)
        Dim connectionString = "your_connection_string"
        Using connection As New SqlConnection(connectionString)
            Dim query = "INSERT INTO ChatMessages (ConnectionId, Message) VALUES (@ConnectionId, @Message)"
            Using command As New SqlCommand(query, connection)
                command.Parameters.AddWithValue("@ConnectionId", connectionId)
                command.Parameters.AddWithValue("@Message", message)
                connection.Open()
                command.ExecuteNonQuery()
            End Using
        End Using
    End Sub
End Class

在上述代码中,SaveMessageToDatabase方法使用 ADO.NET 将聊天消息插入到数据库的ChatMessages表中。

(二)与第三方服务集成

SignalR 可以与许多第三方服务集成,如推送通知服务、日志记录服务等。例如,要集成推送通知服务,可以使用 Firebase Cloud Messaging(FCM)。首先,在项目中安装 FCM 相关的 NuGet 包,然后在 Hub 类中添加发送推送通知的逻辑。

Imports System.Net.Http
Imports System.Text
Imports Newtonsoft.Json

Public Class ChatHub
    Inherits Hub
    Public Sub Send(message As String)
        Clients.All.broadcastMessage(Context.ConnectionId, message)
        SendPushNotification(message)
    End Sub

    Private Async Sub SendPushNotification(message As String)
        Dim fcmServerKey = "your_fcm_server_key"
        Dim fcmUrl = "https://fcm.googleapis.com/fcm/send"
        Dim deviceTokens = GetDeviceTokens() '假设该方法获取设备令牌列表

        For Each deviceToken In deviceTokens
            Dim notification = New
            With {
               .to = deviceToken,
               .notification = New
                With {
                   .title = "New Message",
                   .body = message
                }
            }

            Dim json = JsonConvert.SerializeObject(notification)
            Using client As New HttpClient()
                client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", "key=" & fcmServerKey)
                client.DefaultRequestHeaders.TryAddWithoutValidation("Content - Type", "application/json")
                Dim response = Await client.PostAsync(fcmUrl, New StringContent(json, Encoding.UTF8, "application/json"))
                If response.IsSuccessStatusCode Then
                    Dim result = Await response.Content.ReadAsStringAsync()
                End If
            End Using
        Next
    End Sub

    Private Function GetDeviceTokens() As List(Of String)
        '假设从数据库或其他存储中获取设备令牌列表
        Return New List(Of String) From {"device_token_1", "device_token_2"}
    End Function
End Class

在上述代码中,SendPushNotification方法使用 FCM 的 API 向设备发送推送通知。通过将消息内容构建成 FCM 要求的 JSON 格式,并使用 HttpClient 发送 POST 请求到 FCM 服务器。

通过以上内容,你可以全面了解在 Visual Basic 中使用 SignalR 实现实时通讯的各个方面,包括 SignalR 的基本概念、项目搭建、Hub 的创建与配置、客户端实现、连接管理、安全性、性能优化、常见问题解决以及与其他技术的集成等。希望这些信息能帮助你开发出高效、稳定的实时通讯应用程序。