Hub Api 指南 -服务器(c#).md

Hub Api 指南 -服务端(C#)

本文档提供了SignalR版本2的ASP.NET Hub Api的服务端编程介绍,并提供了常用选项的代码示例。原文地址

SignalR Hubs Api 可以使你通过连接在服务端与客户端之间进行远程调用(RPCs)。在服务端代码中定义客户端调用的方法,并且你可以调用这些方法,让他们运行在客户端中。在客户端代码定义服务端调用的方法,并且你可以调用这些方法,让他们运行在服务端中。SingalR为你处理所有客户端到服务端的管道。

SignalR还提供了一个持久连接的低级API。SignalR、Hub和持久连接的介绍请见Signal R 介绍

本文档使用的软件版本

文档版本

关于SignalR更早版本的信息请见SignalR旧版本

怎样注册SignalR中间件

当应用程序开启时调用MapSignalR方法,定义客户端将用于连接到Hub的路由。MapSignalROwinextensions类的一个扩展方法。下面例子颜色如何使用一个OWIN启动类去定义SignalRHub路由。

using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(MyApplication.Startup))]
namespace MyApplication
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // 任何连接或Hub连接和配置都应该在这里
            app.MapSignalR();
        }

    }
}

如果你添加SignalR功能到ASP.NET MVC应用程序,请确保SignalR路在其他路由之前添加,更多信息请见教程:开始学习signalr2和MVC 5

/signalr URL

默认情况,客户端连接到Hub的路由URL都是"/signalr"。(不要将此URL和"/signalr/hubs"URL弄混,后者用于自动生成的JavaScript文件。有关生成代理的更多信息请见SignalR Hubs API Guide - JavaScript客户端 -生成的代理以及它为您做什么。)

可能有此基础URL不能用于SignalR的特殊情况;录入,你有一个名为signalr的文件夹,并且你不想更改它的名称。在这种情况下,你可以更改该基础URL,如下例所示。

服务代码指定URL

app.MapSignalR("/mySignalr",new HubConfiguration());

JavaScript 客户端指定URL(生成代理)

$.connection.hub.url="/mySignalr";
$.connection.hub.start().done(init);// init 连接开启后调用方法

JavaScript 客户端指定URL(没有生成代理)

var connection = $.hubConnection("/mySignalr", { useDefaultPath: false });

.NET 客户端指定URL

var hubConnection = new HubConnection("http://contoso.com/mySignalr", useDefaultUrl: false);

SignalR配置选项

MapSignalR的重载方法可以让你指定一个自定义的URL、自定义依赖项解析器,和一下选项:

  • 在浏览器使用CORS或JSONP启用跨域调用

    例如浏览器页面地址是http://contoso.com/signalr,SignalR连接在相同域,即http://contoso.com/signalr。如果来自http://contoso.com的页面连接到http://fabrikam.com/signalr,这就是跨域连接。由于安全原因,跨域连接默认是禁止的。更多消息请见ASP.NET SignalR Hubs API Guide - JavaScript客户端 - 如何建立跨域连接

  • 启用明细异常信息

    当异常发生时,SignalR默认的处理方式是向客户端发送一个没有详细信息的通知消息。在产品中是不推荐发送详细的异常信息的,因为一些恶意用户可能使用这个信息再次攻击你的应用程序。为了排除故障,你可以使用该选项临时启用更有信息的错误报告。

  • 禁用自动产生JavaScript代理文件

    默认情况下,会生成一个JavaScript文件,其中包含Hub类的代理,以响应URL“/signalr/ Hub”。如果你不想使用JavaScript代理,或者你想手动产生这个文件,并在你的客户端引用该文件,你可以使用该选项去禁用代理的产生。更多详细信息请见SignalR Hubs API Guide - JavaScript客户端 - 如何为SignalR生成的代理创建物理文件

下面示例演示了如何在调用MapSignalR方法时指定SignalR连接URL和其选项。

var hubConfiguration = new HubConfiguration();
hubConfiguration.EnableDetailedErrors = true;
hubConfiguration.EnableJavaScriptProxies=false;
app.MapSignalR('mySignalr',hubConfiguration);

如何创建和使用Hub类

要创建Hub类,需要创建一个类继承Microsoft.Aspnet.Signalr.Hub。下面的例子显示了一个聊天应用程序的简单Hub类。

public class ContosoChatHub:Hub
{
    public async Task NewContosoChatMessage(string name,string message)
    {
        await Clients.all.addNewMessageToPage(name,message);
    }
}

在这里示例中,一个已连接的客户端可以调用NewContosoChatMessage方法,当他调用时,接收到的数据将会广播到所有已连接的客户端。

Hub的生命周期

你不能在你的服务器上自己的代码实中例化Hub类或调用他的方法,所有这些都是通过SignalRHub管道完成的。SignalR在每次需要处理Hub操作(例如客户端连接、断开连接或对服务器进行方法调用)时创建Hub类的新实例。

因为Hub类的实例是临时的,您不能使用它们来维护从一个方法调用到下一个方法调用的状态。每当服务器接受到客户端调用的方法时,就实例化一个新的Hub类来处理消息。要通过多个连接和方法调用维护状态,请使用其他一些方法,如数据库、Hub类上的静态变量或不继承Hub的其他类。如果使用Hub类上的静态变量等方法将数据持久化到内存中,那么当应用程序域回收时,数据将会丢失。

如果你想从运行在Hub类之外的自己的代码向客户端发送消息,你不能通过实例化一个Hub类的实例来实现,但是你可以通过你的Hub类获取一个SignalR上下文对象引用来实现。更多信息请见该文档后面如何不同或Hub类调用客户端方法和管理组

JavaScript客户端Hub名称标示符

默认情况,JavaScript客户端通过使用类名的驼峰命名规则来引用Hub。SignalR会自动进行此更改,以便JavaScript代码能够遵循JavaScript约定。前面的示例在JavaScript代码中称为contosoChatHub。

服务端

public class ContosoCatHub:hub

JavaScript客户端,使用产生的代理

var contosoChatHubProxy = $.connection.contosoChatHub;

如果你想指定一个不同名称供客户端使用,请添加HubName属性。当你使用HubName属性,JavaScript 客户端上不会更改骆驼大小写的名称。

服务端

[HubName("PascalCaseContosoChatHub")]
public class ContosoChatHub:Hub

JavaScript客户端,使用产生的代理

var contosoChatHubProxy = $.connection.PascalCaseContosoChatHub;

多个Hub

你可以在一个应用程序中定义多个Hub类。当你这样做的时候,连接是共享的但是组是分开的:

  • 所有客户端使用一样的URL("/signalr"或你自己指定的URL)去和服务器创建SignalR连接,并且该连接用于服务器定义的所有HUb。

    对于多个Hub,与在单个类中定义所有Hub功能相比,没有性能差异。

  • 所有Hub获得相同的HTTP请求信息。

    由于所有Hub共享相同的连接,因此服务器获得的唯一 HTTP 请求信息是建立 SignalR 连接的原始 HTTP 请求中的内容。 如果使用连接请求通过指定查询字符串将信息从客户端传递到服务器,你将不能提供不同的查询字符到不同的Hub。所有的Hub将接受到相同的信息。

  • 产生的JavaScript代理文件将在一个文件里包含所有的Hub

    关于JavaScript代理信息请见SignalR Hubs API指南 - JavaScript客户端 - 生成的代理和它为你做了什么

  • 在Hub中定义组

    在SignalR中,您可以定义要广播到连接客户端子集的组。每个Hub分别维护组。例如一个名为“Administrator”的组将包含类ContosoChatHub的一组客户端,而同样名称的组将引用类StockTickerHub的不同客户端集。

强类型Hub

为你客户端可以应用的Hub方法定义一个借口(并且为你的Hub方法启用智能感知),你的Hub继承自Hub<T>(在SignalR 2.1引进),而不是Hub

public class StrongHub:Hub<IClient>
{
    public async Task Send(string message)
    {
        await Clients.All.NewMessage(message);
    }
}

public interface IClient
{
    Task NewMessage(string message);
}

如何在Hub类中定义客户可以调用的方法

在Hub中公开一个你想冲客户端调用的方法,请声明一个公开方法,如下面示例所示:

public class ContosoChatHub:Hub
{
    public async Task NewContosoChatMessage(string name,string message)
    {
        await Clients.All.addNewMessageToPage(name,message);
    }
}
public class StockTickerHub:Hub
{
    public IEnumerable<Stock> GetAllStocks()
    {
        return _stockTicker.GetAllStocks();
    }
}

你可以指定一个返回类型和参数,包含复杂类型和数组,就像在任何 C# 方法中一样。任何你接收到的数据或返回客户端的数据在客户端和服务器端之间使用JSON进行通信,SignalR 会自动处理复杂对象和对象数组的绑定。

avaScript 客户端中方法名称的骆驼大小写

默认情况下,JavaScript客户端使用方法名称的驼峰大小写版本引用Hub方法。SignalR会自动进行次更改,以便JavaScript代码可以符合JavaScript约定。

服务端

public void NewContosoChatMessage(string userName,string message)

使用生成的代理的JavaScript客户端

contosoChatHubProxy.server.newContosoChatMessage(userName,message);

如果要为客户端指定要使用的其他名称,请添加HubMethodName属性。

服务端

[HubMethodName("PascalCaseNewContosoChatMessage")]
public void NewContosoChatMessage(string userName,string message)

使用生成的代理的JavaScript客户端

contosoChatHubProxy.server.PascalCaseNewContosoChatMessage(userName,message);

何时异步执行

如果方法会长时间运行或执行一个包含waiting的工作(例如数据库查询或web服务调用),则通过返回Task(无返回参数)或返回Task<T>对象(指定返回对象类型为T)使Hub方法异步。当你从方法返回Task对象时,SignalR等待Task执行完成,然后将未包装的结果返回给客户端,因此客户端调用的方法代码没有区别。

使用Hub异步方法可以避免在使用WebStocket传输模式时阻塞连接。当传输模式是WebStocket且Hub方法同步执行时,从同一客户在Hub上后续方法的调用将被阻塞,直到Hub方法完成。

下面的示例显示编码为同步或异步运行的相同方法,然后是适用于调用任一版本的 JavaScript 客户端代码。

同步

public IEnumerable<Stock> GetAllStocks()
{
    // 从内存中返回数据
    return _stockTicker.GetAllStocks();
}

异步

public async Tasl<IEnumerable<Stock>> GetAllStocks()
{
    // 从Web服务中返回数据
    var uri = Util.getServiceUri("Stocks");
    using(HttpClient httpClient=new HttpClient())
    {
        var respone = await httpClient.GetAsync(uri);
        return (await respone.Connect.ReadAsAsync<IEnumerable<Stock>>());
    }
}

使用产生的代理的JavaScript客户端

stockTickerHubProxy.server.GetAllStocks().done(function(stocks){
    $.each(stocks,function(){
        alter(this.Symbol+' '+this.Price);
    });
});

定义重载

如果你想为一个方法定义重载,每个重载方法的参数数量必须不相同。如果你仅仅通过不同的产生类型区分不同的重载方法,Hub类可以编译,单在客户端尝试调用一个重载方法时,服务端会抛出一个运行时异常。

报告Hub方法调用进度

SignalR 2.1 添加了对 .NET 4.5引进的进度报告模式的支持。为了实现进度报告,需要为你客户端可以访问的的Hub方法定义一个IProgress<T>参数:

public class ProgressHub:Hub
{
    public async Task<string> DoLongRunningThing(IProgress<int> progress)
    {
        for(int i=0;i<=100;i=+5)
        {
            await Task.Delay(200);
            progress.Report(i);
        }
        return "操作完成!";
    }
}

在编写长时间运行的服务器方法时,重要的是使用异步编程模式(如Async/ Await),而不是阻塞Hub线程。

怎样从Hub类中调用客户端方法

从服务端调用客户端方法,在Hub类的方法里使用Clients属性。下面示例演示了服务端代码调用所有已连接客户端的addNewMessageToPage方法,以及定义 JavaScript 客户端中方法的客户端代码。

服务端

public class ContosoChatHub:Hub
{
    public async Task NewContosoChatMessage(string name,string message)
    {
        await Clients.All.addNewMessageToPage(name,message);
    }
}

调用一个客户端方法是异步操作,并且返回Task。使用await:

  • 保证信息发送没有异常
  • 在try-catch代码块中启用捕获和处理异常

使用生产的代理的JavaScript客户端

contosoChatHubProxy.client.addNewMessageToPage=function(name,message){
    // 添加信息到页面
    $('#discussion').append('<li><strong>'+htmlEncode(name)+'<strong>:'+htmlEncode(message)+'</li>');
}

你不能从客户端方法获取到返回的值;诸如 int x = Clients.All.add(1,1) 这样的代码不工作。

你可以在参数中知道复杂类型或数组。下面示例传递一个复杂类型到客户端方法的参数中。

使用复杂对象调用客户端方法的服务端代码

public async Task SendMessage(string name,string message)
{
    await Clients.All.addContosoChatMessageToPage(new ContosoChatMessage(){UserName=name,Message=message});
}

复杂类型服务端代码

public class ContosoChatMessage
{
    public string UserName{get;set;}
    public string Message{get;set;}
}

使用生产的代理的JavaScript客户端

var contosoChatHubProxy = $.connection.contosoChatHub;
contosoChatHubProxy.client.addContosoChatMessageToPage=function(message){
    console.log(message.UserName + ' ' + message.Message);
}

选择那些客户将接收RPC

Clients属性返回一个HubConnectionContext对象,他提供几个指定那些客户端接收到RPC的属性:

  • 所有连接的客户端

    Clients.All.addContosoChatMessageToPage(name,message);
  • 只有当前调用客户端

    Clients.Caller.addContosoChatMessageToPage(name,message);
  • 除了当前调用客户端的所有客户端

    Clients.Others.addContosoChatMessageToPage(name,message);
  • 通过连接ID识别的指定客户端

    Clients.Client(Context.ConnectionId).addContosoChatMessageToPage(name,message);

    这个示例在当前调用客户端中调用addContosoChatMessageToPage方法,效果与使用Clients.Caller相同。

  • 除指定客户端的所有已连接客户端,通过连接ID识别

    Clients.AllExcept(connectionId1,connectionId2).addContosoChatMessageToPage(name,message);
  • 指定组的所有已连接客户端

    Clients.Group(goupName).addContosoChatMessageToPage(name,message);
  • 所有一连接的客户端,排除一定的组

    Clients.OthersInGroup(groupName).addContosoChatMessageToPage(name,message);
  • 指定用户,通过userId识别

    Clients.User(userid).addContosoChatMessageToPage(name,message);

    默认情况,这是 IPrincipal.Identity.Name,但是可以通过向全局主机注册IUserIdProvider的实现来更改。

  • 连接ID列表中所有的客户端和组

    Clients.Clients(ConnectionIds).addContosoChatMessageToPage(name,message);
  • 一个组列表

    Clients.Groups(GroupIds).addContosoChatMessageToPage(name,message);
  • 用户名称

    Clients.Client(UserName).addContosoChatMessageToPage(name,message);
  • 一个用户名称列表(SignalR2.1引进)

    Clients.Users(new string[]{"myUser1","myUser2"}).addContosoChatMessageToPage(name,message);

方法名称无编译时验证

你指定的方法名称被解析为一个动态对象,这意味着它无法只能感知和编译时验证。表达式在运行时求值。当方法在调用执行是,SignalR发送忙伐名称和参数值到客户端,并且如果客户端有名称匹配的方法,就会调用该方法并将参数值传递给方法。如果在客户端没有找到匹配的方法,不会产生异常。有关SignalR在调用客户端方法时在后台传输给客户端的数据格式的信息,请见Signalr介绍

不区分大小写的方法名称匹配

方法名匹配是不区分大小写的。例如,客户端的AddContosoChatMessageToPageaddcontosochatmessagetopageaddContosoChatMessageToPage方法在服务器上都可以用 Clients.All.addContosoChatMessageToPage 执行。

异步执行

你可以异步调用方法。在对客户端进行方法调用之后出现的任何代码都将立即执行,而无需等待SignalR完成向客户端传输数据,除非您指定后续的代码行应该等待方法完成。下面的代码示例演示了如何顺序执行两个客户端方法。

使用Await(.NET 4.5)

public async Task NewContosoChatMessage(string name,string message)
{
    await Clients.Others.addContosoChatMessageToPage(data);
    await Clents.Caller.notifyMessageSent();
}

如果你使用await,则在执行下一行代码之前要等待客户端方法完成,这并不意味着客户端会在下一行代码执行之前收到消息。客户端方法的调用“完成”仅仅意味着SignalR已完成了发送消息所需的所有操作。如果你需要确认客户端已接受到消息,你必须自己编写那个机制。例如你可以在你的Hub编写MessageReceived方法,在客户端上的addContosoChatMessageToPage方法中,你可以在对客户端做了任何你需要做的工作之后调用MessageReceived。在Hub 的MessageReceived中,你对依赖于原始方法调用的实际客户端接收和处理做任何工作。

如何使用字符串变量作为方法名

如果你想使用字符串变量作为方法名条用客户端方法,则强制Clients.All(或Clients.Others、等Clients.Caller)调用IClientProxyInvoke(方法名称、args...)

public async Task NewContosoChatMessage(string name,string message)
{
    string methodToCall="addContosoChatMessageToPage";
    IClientProxy proxy = Clients.All;

    await proxy.Invoke(methodToCall,name,message);
}

如何从Hub类管理组成员身份

组在SignalR提供一个将信息广播到指定的已连接客户端子集的方法。一个组可以有多个客户端,一个客户端可以是多个组的成员。客户端和组是多对多关系。

管理组成员身份,请使用Hub类Groups属性听的AddRemove方法。下面示例演示了在了Hub中使用Groups.AddGroups.Remove定义的客户端代码调用的方法,并在JavaScript客户代码中调用他们。

服务端

public class ContosoChatHub:hub
{
    public Task JoinGroup(string groupName)
    {
        return Groups.Add(Context.ConnectionId,groupName);
    }

    public Task LeaveGroup(string groupName)
    {
        return Groups.Remove(Context.ConnectionId,groupName);
    }
}

使用生成的代理的JavaScript客户端

contosoChatHubProxy.server.joinGroup(groupName);
contosoChatHubProxy.server.leaveGroup(groupName);

你不必显示的创建组。实际上组在第一次用指定的名称调用Groups.Add时就自动创建了,并且当您从其成员身份中删除最后一个连接时,该组将被删除。

没有获取一个组所有成员列表或所有组列表的API。SignalR基于发布/订阅模式发送消息到客户端和组,并且服务端没有维持组或组成员列表。这有助于最大可能的扩展性,因为每当将节点添加到 Web farm时,SignalR 维护的任何状态都必须传播到新节点。

异步执行添加和删除方法

Groups.AddGroups.Remove方法异步执行。如果你想添加一个客户端到组并且使用组立即发送一个消息到客户端,你必须确保首先完成Groups.Add方法。下面代码演示了如何去做。

将客户端添加到组中,然后向该客户端发送消息

public async Task JoinGroup(string grouName)
{
    await Groups.Add(Context.ConnectionId,groupName);
    await Clients.Group(groupName).addContosoChatMessageToPage(Context.ConnectionId+"added to group");
}

组成员身份持久性

SignalR跟踪连接而不是用户,如果希望用户每次建立连接时都在同一个组中,则必须在用户每次建立新连接时都调用Groups.Add

在暂时失去连接后,有时SignalR可以自动恢复连接。在这种情况下,SignalR将恢复相同的连接,而不是建立新连接,所以客户端组成员身份也将自动恢复。即使临时中断是由于服务器重新启动或故障造成的,这也是可能的,因为每个客户端连接状态(包括组成员身份)都是往返于客户端的。如果在连接超时之前,服务器宕机并被新服务器替换,客户端可以自动重新连接到新服务器,并重新注册到其所属的组中。

当连接丢失、超时或客户端断开连接(例如当浏览器导航到新的页面)后无法自动恢复连接时,组成员身份将丢失。下一次用户连接将是一个新连接。 要在同一用户建立新连接时维护组成员身份,应用程序必须跟踪用户和组之间的关联,并在每次用户建立新连接时还原组成员身份。

更多关于连接和重新连接的信息,请见本文到稍后内容如何处理Hub类中的连接生命周期事件

单一用户组

使用 SignalR 的应用程序通常必须跟踪用户和连接之间的关联,以便知道哪个用户发送消息以及哪些用户应该接收。 组用于执行此操作的两个常用模式之一。

  • 单一用户组

    可以将用户名指定为组名,并且每当用户连接或再次连接时添加当前连接ID到组。将发送给用户的信息发送给组。这种方法的缺陷是,组不为您提供一种方法来了解用户是联机还是脱机

  • 跟踪用户名称和连接ID之间的关联

    你可以存储每个用户名和一个或多个连接ID的关系到字典或数据库,并且在用户每次连接或端口连接是修改存储的数据。发送消息到用户时需要指定连接ID。这种方法的确定是占用太多的内存。

如何处理Hub类中的连接生命周期事件

处理练级什么周期时间的典型原因是跟踪用户是否已连接,并且跟踪用户名称和连接ID之间的关系。在客户端连接或断开连接时运行你在Hub类中重写的OnConnectedOnDisconneectedOnReconnected虚拟方法,就像下面示例所示。

public class ContosoChatHub:Hub
{
    public override Task OnConnected()
    {
        /***
         * 在这里添加你的代码
         * 例如:在聊天应用中,记录当前连接ID和用户名之间的关联。并标记用户在线。
         * 在此方法中的代码完成后客户端被告知连接已建立。
         * 例如在JavaScript客户端,执行start().done回调函数
        **/
        return base.OnConnected();
    }

    public override Task OnDisconnected()
    {
        /**
         * 在这里添加你的代码
         * 例如:在聊天应用中,标记用户已离线,删除当前连接ID与用户名之间的关联。
        **/
        return base.OnDisconnected();
    }

    public override Task OnReconnected()
    {
        /**
         * 在这里添加你的代码
         * 例如:在聊天应用中,在用户经过一段时间不活动后你可能需要将用户标记为离线;在这种情况下,将用户标记为再次在线。
        **/
        return base.OnReconnected();
    }
}

什么时候调用OnConnected、OnDisconnected、OnReconnected

每当浏览器导航到新页面,都必须建立一个新的连接,这意味着SignalR将执行OnDisconnected方法接着执行Onconnected方法。SignalR每当新连接建立是都会建立一个新连接ID。

OnReconnected方法在连接出现临时中断(SignalR可以自动从中恢复)时调用,例如在连接超时之前临时断开并重新连接一条网线。当客户端断开连接并且SignalR不能自动恢复连接,OnDisconnected方法会被调用。例如浏览器导航到新的页面。因此,给定客户端可能的事件顺序是OnConnectedOnReconnectedOnDisconnected或者OnConnectedOnDisconnected。对于给定的连接没有这种情况OnConnectedOnDisconnectedOnReconnected

OnDisconnected方法在某些场景中不会被调用,例如当服务端宕机或App域被回收。当另一个服务器上线或App域完成其回收时,一些客户端也许可以重新练级并触发OnReconnected事件。

更多信息请见理解并处理SignalR连接生命周期时间

调用者未填充状态

从服务端调用连接生命周期事件处理方法,这意味着您放在客户端上state对象中的任何状态都不会填充到服务端上的Caller属性中。有关state对象和Caler属性的更多类容请见本文档后面的如何在客户端和Hub类之间传递状态

如何从Context属性获取客户端的信息

获取关于客户端的信息,请使用Hub类的Context属性。Context属性返回一个HubCallerContext对象,提供以下信息:

  • 调用客户端的连接ID

    string connectionID=Context.ConnectionId;

    连接ID是一个由SignalR分配的Guid(你不能在你的代码中指定它的值)。每个连接都有一个连接ID,如果你的应用程序中有多个Hub,所有的Hub都是用相同的连接ID。

  • HTTP头部数据

    System.Collections.Specialized.NameValueCollection headers = Context.Request.Headers;

    你也可以从Context.Headers获取到HTTP headers。重复引用相同东西的原因是先创建了Context.Headers,然后添加的Context.Headers属性,为了向后兼容而保留了Context.Headers

  • Query string 数据

    System.Collections.Specialized.NameValueCollection queryString =Context.Request.QueryString;
    string parameterValue=queryString["parametername"];

    你也可以从Context.QueryString获取QueryString数据。

    在此属性获得的QueryString一个用于建立SignalR连接的HTTP请求。你可以通过配置连接添加QueryString属性到客户端,这是将客户端数据从客户端传递到服务端的一种方便的方式。下面代码演示了当你使用产生的代理时,添加QueryString到JavaScript客户端的一种方法。

    $.connection.hub.qs={"version":"1.0"};

    更多关于设置QueryString参数的信息,请查阅JavaScript客户端.NET客户端的API指南。

    你可以使用在QueryString数据中查到用于连接传输的方法,以及SignalR内部使用的一些其他值:

    string transportMethod=queryString["transport"];

    transportMethod的值可能是“webStockets”、“serverSentEvents”、“foeverFrame”或者“longPolling”。注意,如果你在OnConnected事件处理方法中检查这个值,在某些情况下可能你最初获得的transport value 不是连接最终协商的transport方法。在这种情况下,OnConnected事件处理方法会抛出异常,当最终协商transport方法建立时,OnConnected事件处理方法会再次被调用。

  • Cookies

    System.Collections.Generic.IDictionary<string,Cookie> cookies = Context.Request.Cookies;

    你可以从Context.Request.Cookies获取到cookies。

  • 用户信息

    System.Security.Principal.IPrincipal user = Context.User;
  • 请求的HttpContext对象

    System.Web.HttpContextBase httpContext = Context.Request.GetHttpContext();

    使用Context.Request.GetHttpContext方法而不是HttpContext.Current获取SignalR连接中的HttpContext对象。

在Hub类和客户端之间如何传递状态

客户端代理提供一个state对象,您可以在其中存储你想在方法每次调用时传输到服务端的数据。在服务端上,在被客户端调用的Hub方法的Clients.Caller属性中访问这个数据。连接生命周期事件处理方法OnConnectedOnDisconnectedOnReconnected不填充Clients.Caller属性。

在状state对象和Clients.Caller属性中创建或更新数据是双向的。您可以更新服务器中的值,并将它们传递回客户机。

下面示例演示了JavaScript客户端代码存储每次调用方法时传输到服务端的状态。

contosoChatHubProxy.state.userName="Fadi Fakhouri";
contosoChatHubProxy.state.computerName = "fadivm1";

下面代码演示了.NET客户端等效的代码

contosoChatHubProxy["userName"] = "Fadi Fakhouri";
chatHubProxy["computerName"] = "fadivm1";

在你的Hub类中,你可以在Clients.Caller属性访问这个数据。下面的示例演示了检索前一个示例中引用的状态的代码。

public async Task NewContosoChatMessage(string data)
{
    string userName = Clients.Caller.userName;
    string computerName = Clients.Caller.conputerName;
    await Clients.Others.addContosoChatMessageToPage(message,userName,computerName);
}

备注

这种持久化状态的机制不适用于大量数据,因为在stateClients.Caller属性放置的所有内容会随每个方法调用一起循环。它对于较小的项目(如用户名或计数器)很有用。

在VB.NET或强类型hub中,调用者状态对象无法通过Clients.Caller访问,而是使用Clients.CallerState访问(在SignalR2.1中引入)。

C#使用客户端状态

public async Task NewContosoChatMessage(string data)
{
    string userName = Clients.CallerState.userName;
    string computerName=Clients.CallerState.computerName;
    await Clients.Others.addContosoChatMessageToPage(data,userName,computerName);
}

Visual Basic 使用客户端状态

public Async Function NewContosoChatMessage(message as String) As Task
    Dim userName as String = Clients.CallerState.userName
    Dim computerName as String = Clients.CallerState.computerName
    Await Clients.Others.addContosoChatMessageToPage(message,userName,computerName)
End Sub

在Hub类中如何处理异常

处理Hub类方法发生中的异常,首选确保你“observe”了使用await异步操作(例如调用客户端方法)的任何异常。然后下面一个或多个方法:

  • 使用将方法代码包在try-catch代码块中,并且记录异常对象。出于调试目的,您可以将异常发送到客户端,但是由于安全原因,在产品中发送明详细的异常信息到客户端是不推荐的。

  • 创建Hub管道模块来处理OnIncomingError方法。下面代码演示了一个记录异常的管道模块,跟着的代码在Startup.css中注册模块到Hub管道。

    public class ErrorHandlingPipelineModeule:HubPipelineModule
    {
        protected override void OnIncomingError(ExceptionContext exceptionContext, IHubIncomingInvokerContext invokeConext)
        {
            Debug.WriteLine("=> Exception " + exceptionContext.Error.Message);
            if(exceptionContext.Error.InnerException != null)
            {
                Debug.WriteLine("=> Inner Exception " + exceptionContext.Error.InnerException.Message);
            }
            base.OnIncomingError(exceptionContext,invokerContext);
        }
    }
    public void Configuration(IAppBuilder app)
    {
        // 任何连接或Hub连接和配置都应该在这里
        GlobaHost.HubPipeline.AddModule(new ErrorHandlingPipelineModeule());
        app.MapSignalR();
    }
  • 使用HubException类(在SignalR2中引进)。这个异常可以从任何Hub调用引发。HubError构造函数就收一个字符串信息和一个存储额外异常数据的对象。SignalR将自动序列化异常并将它发送给客户端,将用于Hub方法调用被拒绝或失败。

    下面代码将演示在Hub调用期间如何抛出HubException,和如何在JavaScript客户和.NET客户端处理异常。

    服务端代码演示HubException类

    public class MyHub:Hub
    {
        public async Task Send(string message)
        {
            if(message.Contains("<script>"))
            {
                throw new HubException("此消息将流向客户端", new {user = Context.User.Identity.Name, message = message});
            }
    
            await Chillents.All.send(message);
        }
    }

    JavaScript客户端代码,演示在Hub中抛出HubException异常时的响应

    myHub.server.send("<script>")
        .fail(function(e){
            if(e.source === 'HubException')
            {
                console.log(e.message + ' : ' + e.data.user);
            }
        });

    .NET 客户端代码,演示在Hub中抛出HubException异常时的响应

    try
    {
        await myHub.Invoke("Send","<script>");
    }
    catch(HubException ex)
    {
        Console.WriteLine(ex.Message);
    }

更多关于Hub管理模块的信息请见本文档如何自定义Hub管道

如何启用跟踪

添加system.diagonstice元素到你的Web.config文件,启用服务端跟踪,就像下面所示:

<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit https://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <connectionStrings>
    <add name="SignalRSamples" connectionString="Data Source=(local);Initial Catalog=SignalRSamples;Integrated Security=SSPI;Asynchronous Processing=True;" />
    <add name="SignalRSamplesWithMARS" connectionString="Data Source=(local);Initial Catalog=SignalRSamples;Integrated Security=SSPI;Asynchronous Processing=True;MultipleActiveResultSets=true;" />
  </connectionStrings>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true" />
  </system.webServer>
  <system.diagnostics>
    <sources>
      <source name="SignalR.SqlMessageBus">
        <listeners>
          <add name="SignalR-Bus" />
        </listeners>
      </source>
     <source name="SignalR.ServiceBusMessageBus">
        <listeners>
            <add name="SignalR-Bus" />
        </listeners>
     </source>
     <source name="SignalR.ScaleoutMessageBus">
        <listeners>
            <add name="SignalR-Bus" />
        </listeners>
      </source>
      <source name="SignalR.Transports.WebSocketTransport">
        <listeners>
          <add name="SignalR-Transports" />
        </listeners>
      </source>
      <source name="SignalR.Transports.ServerSentEventsTransport">
          <listeners>
              <add name="SignalR-Transports" />
          </listeners>
      </source>
      <source name="SignalR.Transports.ForeverFrameTransport">
          <listeners>
              <add name="SignalR-Transports" />
          </listeners>
      </source>
      <source name="SignalR.Transports.LongPollingTransport">
        <listeners>
            <add name="SignalR-Transports" />
        </listeners>
      </source>
      <source name="SignalR.Transports.TransportHeartBeat">
        <listeners>
            <add name="SignalR-Transports" />
        </listeners>
      </source>
    </sources>
    <switches>
      <add name="SignalRSwitch" value="Verbose" />
    </switches>
    <sharedListeners>
      <add name="SignalR-Transports" 
           type="System.Diagnostics.TextWriterTraceListener" 
           initializeData="transports.log.txt" />
        <add name="SignalR-Bus"
           type="System.Diagnostics.TextWriterTraceListener"
           initializeData="bus.log.txt" />
    </sharedListeners>
    <trace autoflush="true" />
  </system.diagnostics>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="v11.0" />
      </parameters>
    </defaultConnectionFactory>
  </entityFramework>
</configuration>

当你在Visual Studio运行应用程序时,可以在Output窗口中查看日志。

如何在Hub类之外调用客户端方法和管理组

若要从与Hub类不同的类中调用客户端方法,请获取一个Hub的SignalR上下文对象的引用,并使用它调用客户端的方法或管理组。

下面示例中StockTicker类获取上下文对象,并将上下文对象存在类的实例中,类实例存储到静态属性,并且在单例实例使用上下文调用已连接到名为StockTickerHubHub的客户端上的updateStockPrice方法。

/**
 * 对于完整的示例,请转到
 * http://www.asp.net/signalr/overview/getting-started/tutorial-server-broadcast-with-aspnet-signalr
 * 这个示例只显示了与获取和使用SignalR上下文相关的代码。
**/
public class StockTicker
{
    private readonly static LaZy<StockTicker> _instance = new LaZy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>()));

    private IHubContext _context;

    private StockTicker(IHubContext context)
    {
        _context=context;
    }

    // 被定时器对象调用
    private void UpdateStockPrices(object state)
    {
        foreach (var stock in _sotcks.Values)
        {
            if(TryUpdateStockPrice(stock))
            {
                _context.Clients.All.updateStockPrice(stock);
            }
        }
    }
}

如果您需要在长生存期对象中多次使用上下文,请获取一次其引用并保存,而不是每次都去获取。获取一次上下以已确保SignalR在你的Hub方法进行客户单方法调用时以同样的顺序发送消息到客户端。关于演示如何使用SignalR Hub上下文的教程,请见服务器广播与ASP.NET SignalR

调用客户端方法

你可以指定哪些客户端将就收RPC,但是与从Hub类调用时相比,您的选项更少。这是因为上下文与来自客户机的特定调用没有关联,所以任何需要知道当前连接ID的方法都是不可用的(例如Clients.OthersClients.CallerClients.OthersInGroup)。一下选项是有效的:

  • 所有连接的客户端

    context.Clients.All.addContosoChatMessageToPage(name,message);
  • 由连接ID标识的特定客户端

    Context.Clients.Client(connectionID).addContosoChatMessageToPage(name,message);
  • 所有已连接客户排除通过连接ID指定的客户

    Context.Clients.AllExcept(connectionId1,connectionId2).addContosoChatMessageToPage(name,message);
  • 指定组中所有已连接客户

    Context.Clients.Group(groupName).addContosoChatMessageToPage(name,message);
  • 指定组所有已连接客户,配出通过连接ID指定的客户

    Context.Clients.Group(groupName,connectionId1,connectionId2).addContosoChatMessageToPage(name,message);

如果你在Hub类的方法调用non-Hub类,你可以传递当前连接ID将其用于Clients.ClientClients.AllExceptClients.Group去模拟Clients.CallerClients.OthersClients.OthersInGroup。在下面示例中,MoveShapeHub类传递连接ID到Broadcaster类,所以Broadcaster类可以模拟Clients.Others

/**
 * 完整示例请见
 * http://www.asp.net/signalr/overview/signalr-20/getting-started-with-signalr-20/tutorial-server-broadcast-with-signalr-20
 * 这个示例只显示了将连接ID传递给non-Hub类的代码,以模拟Clients.Others。
**/
public class MoveShapeHub : Hub
{
    // 未显示的代码将一个单例的广播实例放在这个变量中。
    private Brodcaster _broadcaster;

    public void UpdateModel(ShapeModel clientModel)
    {
        clientModel.LastUpdateBy = Context.ConnectionId;
        // 在我们的广播器中更新形状模型
        _broadcaster.UpdateShape(clientModel);
    }
}

public class Broadcaster
{
    public Brodcaster()
    {
        _hubContext = GlobaHost.ConnectionManager.GetHubContext<MoveShapeHub>();
    }

    public void UpdateShape(ShapeModel clientModel)
    {
        _model = clientModel;
        _modelUpdated=true;
    }

    // 被定时器对象调用
    public void BrodcastShape(object state)
    {
        if(_modelUpdated)
        {
            _hubContext.Clients.AllExcept(_model.LastUpdateBy).updateShape(_model);
            _modelUpdated=false;
        }
    }
}

管理组成员身份

对于管理组,你可以使用与Hub类中相同的选项。

  • 添加客户到组

    Context.Groups.Add(connectionId,groupName);
  • 从组中删除客户

    Context.Groups.Remove(connectionId,groupName);

如何自定义Hub管道

SignalR允许你注入你自己的代码到Hub管道。下面示例显示了一个自定义Hub管道模块,记录了每个从客户端接收到的传入方法调用和在客户端调用传出方法调用:

public class LogginPipelineModule:HubPipelineModule
{
    protected override bool OnBeforeIncoming(IHubIncomingInvokerContext context)
    {
        Debug.WriteLine("=> Invoking " + context.MethodDescriptor.Name + " on hub " + context.MethodDescriptor.Hub.Name);

        return base.OnBeforeIncoming(context);
    }
    protected override bool OnBeforeOutgoing(IHubOutgoingInvokerContext  context)
    {
        Debug.WriteLine("<= Invoking " + context.Invocation.Method + " on client hub " + context.Invocation.Hub);

        return base.OnBeforeOutgoing(context);
    }
}

下面Startup.css文件中的代码注册模组到Hub管道中运行

public void Configuration(IAppBuilder app)
{
    GlobaHost.HubPipeline.AddModule(new LoggingPipelineModule());
    app.MapSignalR();
}

更多你可以重写的方法列表,请见HubPipelineModule Methods

posted @ 2020-05-22 16:54  $("#阿飞")  阅读(1259)  评论(0编辑  收藏  举报