[SignalR2] 异常处理

SignalR目前使用两种通信模型:Hub和PersistentConnection,而对于这两种通信模型他们在处理异常上面也有点不同,而唯一共同之处就是对于JavaScript客户端而言他们都是返回一个 promise(),认识这一点非常关键,这会让我们少说很多话。

在处理异常之前我们需要先开启日志跟踪开关,即:

  • 针对hub路由服务端 EnableDetailedErrors:指返回详细错误,默认是关闭,如果不开启我们就无法获取到详细的 throw 异常消息,取而代之的是JavaScript客户端脚本的一次友好描述信息。
  • 针对hub客户端 $.connection.hub.logging = true;:开启后会采用 console.log 来输出所有的日志数据。
  • 针对PersistentConnection路由服务端:他可没有这么好的开关,因为对于PersistentConnection他是一个非常底层的通信模型,换句话说,他不像Hub当遇到throw之类的会包裹一个友好的消息包发送给客户端;而对于PersistentConnection是当遇到 throw 时是直接停用连接。所以在处理异常时,我们需要在服务端捕获后并友好的发送一个数据包给客户端。
  • 针对PersistentConnection客户端:没办法他就只有一个 received 事件来处理由服务端发送的任何数据包,你只能自行处理。

针对服务端部分没有什么好讲的,但是一个友好的SignalR是不应该 throw 异常,应该处理好异常再转交给客户端。

主要针对两种通信模型在客户端处理的一些方法。

Hub通信模型

连接时异常

$.connection.hub.start().done(function () {
    // 连接成功时
}).fail(function (res) {
    // 连接失败时
});

记住我上面讲的,基本上所有的返回的对象都是一个 promise(),看 start() 返回的就是一个这样类型,所以我们很自然的可以添加 donefail 事件来处理异常。而这只是针对于连接时,比如:

我们将整个Hub要求登录认证,如果没有登录,那么会返回一个401状态的页面,而 fail 就可以捕获取。

可能由于站点重启倒置重新连接SignalR可能引起的异常。

注:当服务端调用一个客户端不存在的方法时

这一点要非常谨慎,因为不管你是否开始日志跟踪,你都无法判断他是否调用成功。因为当服务端调用一个客户端方法时,实际是使用jQuery的 triggerHandler,而对于此Signal JavaScript客户端库是没有做任何的异常处理。当然应该不会出现这种错误的。

PersistentConnection通信模型

PersistentConnection 始终只有一个 received 事件来接收任何数据包,而 received 返回的是一个 connection 对象,而他有相对应的一个 error 来处理异常。

几种常见的错误

1、 方法名称拼写错误、大小写、无效hub名称

默认情况下服务端方法名称会按驼峰命名法,当然我们可以使用 [HubName("msgHub")][HubMethodName("count")] 来重新定义。对于JavaScript是大小写敏感的,要注意这一点。

2、无效JSON格式

SignalR的服务端和客户端是通过JSON传输数据的,而默认使用的是 window.JSON 来解析客户端的JSON脚本,所以如果对于低版本的浏览器还需要引入第三方JSON库

3、Hub连接开始前至少必须订阅一个供服务端调用客户端方法

原因是消息必须要有一个返回对象,所以不管怎么样都必须在JavaScript客户端有一个相对应供服务端调用客户端方法,否则是无法启用Hub连接的。

4、混淆Hub和PersistentConnection两种通信语法

SignalR使用两种通信模型:Hub和PersistentConnection,这两种不管在服务端还是客户端调用的方式都不同。

JavaScript客户端创建一个PersistentConnection连接

var myConnection = $.connection('/echo');

JavaScript客户端创建一个Hub连接

var myHub = $.connection.MyHub;

C#服务端注册一个PersistentConnection路由

RouteTable.Routes.MapConnection<MyConnection>("my", "/echo");

C#服务端注册hub路由,以下默认是所有Hub

App.MapSignalR();

参考资料

查看 SignalR系列文章

[SignalR2] AngularJS 应用

由于手头有一个AngularJS项目,而其中消息模块就是采用SignalR来推送。AngularJS就像天生需要SignalR一样,因为他们都需要很快。其实更在意的是WebSocket在AngularJS的应用,而当前只是一个小试验,但效果真的非常好。这一篇文章,我们围绕AngularJS中如何应用SignalR,所以你看到的更多会是JavaScript脚本。

项目是以ASP.NET API作为服务端、OAuth2票据令牌认证、AngularJS做为前端、中间全部采用JSON作为数据传输。

依赖jQuery.SignalR.js

这是必须的,而SignalR客户端脚本库又是以插件的形式,所以不管未来如何更新,都不担心脚本的变动而倒置AngularJS的改动,要知道AngularJS项目的JS脚本管理要控制得非常严谨。

Hub代理脚本

其实我在SignalR JavaScript 客户端已经非常详细的描述过关于如果创建代理脚本,但有个缺点就是他是全局式的,而对于AngularJS而言不建议使用这种全局式的,所以我们需要手动的创建代理脚本部分;并且把它当作一个 factory,这样对于多个Controller是可以共享数据的。

这里有个github的项目,完全可以满足我们的需求,非常简单,我把代理贴出来:

angular.module('SignalR', [])
.constant('$', $)
.factory('Hub', ['$', function ($) {
    //This will allow same connection to be used for all Hubs
    //It also keeps connection as singleton.
    var globalConnections = [];

    function initNewConnection(options) {
        var connection = null;
        if (options && options.rootPath) {
            connection = $.hubConnection(options.rootPath, { useDefaultPath: false });
        } else {
            connection = $.hubConnection();
        }

        connection.logging = (options && options.logging ? true : false);
        return connection;
    }

    function getConnection(options) {
        var useSharedConnection = !(options && options.useSharedConnection === false);
        if (useSharedConnection) {
            return typeof globalConnections[options.rootPath] === 'undefined' ?
            globalConnections[options.rootPath] = initNewConnection(options) :
            globalConnections[options.rootPath];
        }
        else {
            return initNewConnection(options);
        }
    }

    return function (hubName, options) {
        var Hub = this;

        Hub.connection = getConnection(options);
        Hub.proxy = Hub.connection.createHubProxy(hubName);

        Hub.on = function (event, fn) {
            Hub.proxy.on(event, fn);
        };
        Hub.invoke = function (method, args) {
            return Hub.proxy.invoke.apply(Hub.proxy, arguments)
        };
        Hub.disconnect = function () {
            Hub.connection.stop();
        };
        Hub.connect = function () {
            return Hub.connection.start(options.transport ? { transport: options.transport } : null);
        };

        if (options && options.listeners) {
            angular.forEach(options.listeners, function (fn, event) {
                Hub.on(event, fn);
            });
        }
        if (options && options.methods) {
            angular.forEach(options.methods, function (method) {
                Hub[method] = function () {
                    var args = $.makeArray(arguments);
                    args.unshift(method);
                    return Hub.invoke.apply(Hub, args);
                };
            });
        }
        if (options && options.queryParams) {
            Hub.connection.qs = options.queryParams;
        }
        if (options && options.errorHandler) {
            Hub.connection.error(options.errorHandler);
        }

        //Adding additional property of promise allows to access it in rest of the application.
        Hub.promise = Hub.connect();
        return Hub;
    };
}]);

使用时先把 SignalR 模块依赖到你的应用中,然后再需要的地方创建 var hub = new Hub('hubname',options);,就可以了。

其中 options 会有一些参数:

  • listeners 服务端调用客户端方法。
  • methods 客户端调用服务端方法。
  • rootPath 服务器地址。
  • queryParamsconnection.qs
  • errorHandlerconnection.error
  • logging 是否启用日志,这里的日志是指 console.log,如果是chrome,则F12就可以查看到了。
  • useSharedConnection 是否共享一个连接,当有两个不同的服务器地址时,还是会按两个连接的。
  • transport 指定使用哪一个方法进行数据传输,比如:'longPolling'['webSockets', 'longPolling']

当然以上是针对大部分项目来设计的,我的项目里面根据这个做法,做了更适合自身项目的修改,但基本上如同上面一样。

票据字符串

在创建连接时把会票据字符串加入 qs中,可能别的项目不是采用OAuth2票据令牌认证,但是作为ASP.NET API项目这种认证方式是无可挑剔。

服务端验证票据

这一点,请详见另一篇文章认证和授权,这里就不再重复。

一个完整的示例

1、首先创建一个factory的Hub代理类

app.constant('$', $).factory('hub', ['$', '$localStorage', function ($, $localStorage) {
    var authData = $localStorage.authorizationData;
    //This will allow same connection to be used for all Hubs
    //It also keeps connection as singleton.
    var globalConnection = null;

    function initNewConnection(options) {
        var connection = null;
        if (options && options.rootPath) {
            connection = $.hubConnection(options.rootPath, {
                useDefaultPath: false,
                qs: { Bearer: authData.token }
            });
        } else {
            connection = $.hubConnection();
        }

        connection.logging = (options && options.logging ? true : false);
        return connection;
    }

    function getConnection(options) {
        var useSharedConnection = !(options && options.useSharedConnection === false);
        if (useSharedConnection) {
            return globalConnection === null ? globalConnection = initNewConnection(options) : globalConnection;
        }
        else {
            return initNewConnection(options);
        }
    }

    return function (hubName, options) {
        var Hub = this;

        Hub.connection = getConnection(options);
        Hub.proxy = Hub.connection.createHubProxy(hubName);

        Hub.on = function (event, fn) {
            Hub.proxy.on(event, fn);
        };
        Hub.invoke = function (method, args) {
            return Hub.proxy.invoke.apply(Hub.proxy, arguments)
        };
        Hub.disconnect = function () {
            Hub.connection.stop();
        };
        Hub.connect = function () {
            Hub.connection.start();
        };

        if (options && options.listeners) {
            angular.forEach(options.listeners, function (fn, event) {
                Hub.on(event, fn);
            });
        }
        if (options && options.methods) {
            angular.forEach(options.methods, function (method) {
                Hub[method] = function () {
                    var args = $.makeArray(arguments);
                    args.unshift(method);
                    return Hub.invoke.apply(Hub, args);
                };
            });
        }
        if (options && options.errorHandler) {
            Hub.connection.error(options.errorHandler);
        }
        //Adding additional property of promise allows to access it in rest of the application.
        Hub.promise = Hub.connection.start();
        return Hub;
    };
}]);

由于票据字符串是放在LocalStorage当中,所以我注入了 $localStorage 来获取我的 token,并把他放在 qs 中。其他并没有什么不一样。

2、Controller层使用

首先我需要将我创建的factory注入到我需要的使用的Controller中,比如:

app.controller('DemoCtrl', [ 'Hub', function(Hub) {
    var msgHub = new Hub('msgHub', {
        logging: true, // 启用日志
        rootPath: 'app', // 服务器地址
        listeners: { // 服务端调用客户端方法
            get: function (r) {
                console.log(r);
            }
        },
        methods: ['echo'], // 客户端调用服务端方法。
        errorHandler: function (error) { // 异常处理
            console.error(error);
        }
    });
}]);

总结

这里唯一我们要注意的就是你所采用的登录认证的方式,因为这里我的确吃了一些亏,所以务必要清楚。

查看 SignalR系列文章

[SignalR2] 认证和授权

SignalR自身不提供任何用户认证特征,相反,是直接使用现有且基于(Claims-based)声明认证系统(关于这方面知识详见参考资料),非常明了,不解释,看代码中的验证代码:

protected virtual bool UserAuthorized(IPrincipal user)
{
    if (user == null)
    {
        return false;
    }

    if (!user.Identity.IsAuthenticated)
    {
        return false;
    }

    if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase))
    {
        return false;
    }

    if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
    {
        return false;
    }

    return true;
}

当我们采用ASP.NET认证系统时,会自动处理认证信息,所以我们只需要关心我们的授权部分。

Authorize 属性

[Authorize] 属性来指定哪里用户可以访问Hub或Hub下的某个方法,这种办法如同ASP.NET MVC完全一样,它包括三个属性:

  • [Authorize] 只允许授权用户访问。
  • [Authorize(Roles = "Admin,Manager")] 指定拥有 Admin、Manager 角色。
  • [Authorize(Users = "user1,user2")] 指定用户名为 user1、user2 访问。
  • [Authorize(RequireOutgoing=false)] 当设置为false后可以限定某些人在服务端中调用,但所有人都可以接收消息。

当然我们也可以直接在 Startup.cs 调用 GlobalHost.HubPipeline.RequireAuthentication();,这相当于所有Hub都需要用户认证。

自定义 Authorize

可以直接继承 AuthorizeAttribute 类,其中我们可以重写 UserAuthorized 来调整我们的授权逻辑。

其实整个SignalR的认证就是调用ASP.NET的认证体系,SignalR只是重写的授权这一部分,但授权也非常简单无非就是是否验证成功、角色授权等等。

以下我会提几下特殊的情况:

杜绝 Session

很多项目都会使用Session来保存用户认证信息,但确保不要这么做,在SignalR官网默认也是建议不使用Session,当然你可以使用它,但你启用后他会打破双向通信,也就是说你将无法体验双向通信功能。具体原因我未证实但应该是由于Session会导致每一次请求数据进行一次序列化这完全不符合双向通信的原则嘛。

ASP.NET API中的OAuth2票据令牌认证

这个比较特殊是在于对于默认发送票据字符串是靠header,而对于webSocket是不允许添加header的。所以这里面我提供另一种解决办法:

  1. 请求时将令牌数据放到URI中,这样就可以解决webSocket请求的问题。
  2. 自定义一个 Authorize
  3. 根据票据字符串返回具体票据对象,同时判断票据是否有效。
  4. 将有效的票据存入 request.Environment["server.User"],以便于后面使用。这里的 Environment 实际就是 OWIN 的参数,而关于 OWIN 的好处可以见参考资料。
  5. 当调用Hub方法时,我们重新构建一个 HubCallerContext,当然是先将票据对象写入才重新构建的,这样子我们的上下文是一个带有 Context.User。

以下是完整代码:

public class QueryStringBearerAuthorizeAttribute : AuthorizeAttribute
{
    public override bool AuthorizeHubConnection(Microsoft.AspNet.SignalR.Hubs.HubDescriptor hubDescriptor, IRequest request)
    {
        var token = request.QueryString.Get("Bearer");
        if (String.IsNullOrEmpty(token)) return false;

        var ticket = Startup.OAuthOptions.AccessTokenFormat.Unprotect(token);

        if (ticket != null && ticket.Identity != null && ticket.Identity.IsAuthenticated)
        {
            // set the authenticated user principal into environment so that it can be used in the future
            request.Environment["server.User"] = new ClaimsPrincipal(ticket.Identity);
            return true;
        }

        return false;
    }

    public override bool AuthorizeHubMethodInvocation(Microsoft.AspNet.SignalR.Hubs.IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
    {
        var connectionId = hubIncomingInvokerContext.Hub.Context.ConnectionId;
        // check the authenticated user principal from environment
        var environment = hubIncomingInvokerContext.Hub.Context.Request.Environment;
        var principal = environment["server.User"] as ClaimsPrincipal;
        if (principal != null && principal.Identity != null && principal.Identity.IsAuthenticated)
        {
            // create a new HubCallerContext instance with the principal generated from token
            // and replace the current context so that in hubs we can retrieve current user identity
            hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(environment), connectionId);
            return true;
        }
        else
        {
            return false;
        }
    }
}

对于客户端我们需要将票据字符串放到一个 Bearer 里面。

$.connection.hub.start({ qs: { Bearer: 'xxxxxx' } });

总结

SignalR的认证和ASP.NET完全是一起的,所以关于这一点完全没有任何学习成本。但最好采用claims-based identity认证方式,同时杜绝在SignalR里面使用Session。

查看 SignalR系列文章

参考资料

  1. 【Jesse Liu】ASP.NET Identity登录原理 – Claims-based认证和OWIN
  2. Authentication and Authorization for SignalR Hubs

[SignalR2] 服务端主动推送

服务端主动推送,其实我们一直这么用,比如:当我们从客户端调用服务端方法时,绝大部分服务端也会有相应的响应一个客户端方法。

而把它作为独立一部分来讲,是因为我想通过一个示例,来表现服务端主动推送的能力。对于大访问量而言批量推送也不能按常规的做法,关于这一点,我想会再发表新的文章来说明。

现在我制作一下监控服务器CPU状态的示例,这里我会采用 PersistentConnection 来创建一个消息通道。SignalR分别支持 PersistentConnectionHub API接口两种,他们两者有者很大的不同,同样也会在其他文章说明。

一、CPU状态模型类

Usage 占用率、Time当时间。

public class CPUUsageInfo
{
    public double Usage { get; set; }
    public DateTime Time { get; set; }
}

二、创建一个继承于 PersistentConnection 消息连接通道

public class PerfmonConnection : PersistentConnection
{
}

三、注册连接通道

目前2X版本,必须使用 app.MapSignalR 来注册连接通道,他提供一个可以指定连接对象的泛型入口,参数分别是连接时用的服务器地址 /perfmon,以及我们需要一些属性配置。

app.MapSignalR<PerfmonConnection>("/perfmon", new ConnectionConfiguration()
{
    EnableJSONP = false
});

四、读取服务器CPU使用率

这里我是在 Application_Start 上做一个 Task 任务。其中我们可以 GlobalHost.ConnectionManager 来获取我们创建的连接消息实例对象,紧接者再通过 Broadcast 进行通知。完整代码如下:

Task.Factory.StartNew(() =>
{
    IConnection connection = GlobalHost.ConnectionManager.GetConnectionContext<PerfmonConnection>().Connection;

    var counter = new PerformanceCounter();
    counter.CategoryName = "Processor";
    counter.CounterName = "% Processor Time";
    counter.InstanceName = "_Total";

    while (true)
    {
        var item = new CPUUsageInfo()
        {
            Time = DateTime.Now,
            Usage = Math.Round(counter.NextValue(), 2)
        };

        connection.Broadcast(item);

        System.Threading.Thread.Sleep(1000);
    }
});

注意这里 connection.Broadcast 并无指明到底是谁,换言之是对所有连接的客户端进行群发,而关于权限等可以在 PerfmonConnection 重写相应的方法来满足我们的需求。

五、客户端脚本

$.connection('/perfmon').received(function (item) { 
    console.log(item);
}).start();

非常简单的三行代码,这跟hub连接不一样,PersistentConnection 相对于 Hub 更加底层,也就是说 Hub 是继承于 PersistentConnection 并添加一些更加复杂但对于我们更加易用的方法。

为了让数据表现上更好看,我这里引用 Highcharts 做一下修饰,所以完整的客户端脚本是:

<div id="container" style="min-width:700px;height:400px"></div>
<script type="text/javascript" src="http://cdn.hcharts.cn/highstock/2.0.3/highstock.js"></script>
Highcharts.setOptions({
    global: {
        useUTC: false
    }
});

var chartOptions = {
    chart: {
        renderTo: 'container',
        animation: Highcharts.svg, // don't animate in old IE               
        marginRight: 10
    },
    rangeSelector: {
        buttons: [{
            count: 1,
            type: 'minute',
            text: '1M'
        }, {
            count: 5,
            type: 'minute',
            text: '5M'
        }, {
            type: 'all',
            text: 'All'
        }],
        inputEnabled: false,
        selected: 0
    },

    title: {
        text: 'CPU usage history'
    },

    exporting: {
        enabled: false
    },

    series: [{
        name: 'CPU Usage in %',
        data: (function () {
            // generate an array of random data
            var data = [], time = (new Date()).getTime(), i;

            for (i = -999; i <= 0; i++) {
                data.push([
                    time + i * 1000,
                    Math.round(Math.random() * 100)
                ]);
            }
            return data;
        })()
    }]
};

var chart = new Highcharts.StockChart(chartOptions);

var perfmonConn = $.connection('/perfmon').received(function (item) {
    chart.series[0].addPoint([(new Date(item.Time)).getTime(), item.Usage], true, true);
}).start();

六、Using a Hub instance not created by the HubPipeline is unsupported 错误

假设有个 MessgeHub 下有人 Get 服务端方法,这个方法会结果是向所有用户主动推送一个数值。按正常逻辑的方式我们可能会这么写:

new MessageHub().Get();

很好,你就会收到这个异常,明确的说你是并不支持这种hub实例,因为他无法创建 HubPipeline,由于 MessageHub 下会继承 BaseHub 且会有一个叫 Context 属性并且可以重新给他设定一个实例;这样我想可以通过自定义一个 HubPipeline 并赋值给它,可这问题就变得非常复杂。

而往往这种主动推送会发生在不同情况下,所以最好的做好是将要推送的内容最一层封装,然后在我们需要主送向客户端推送时,可以这么做:

 Microsoft.AspNet.SignalR.GlobalHost.ConnectionManager.GetHubContext<MessgeHub>().Clients.All.Get(1);

总结

上面示例有没有发现一个很好玩的事情,你看 PerfmonConnection 简单到一句代码也不需要写,唯一需要创建一个 Task 任务做一下数据包裹,然后发给客户端接收。因此如果你想让现有功能享受SignalR带来的双向通信好处,那么压根就不需要写太多代码,是不是很愉快呢?

查看 SignalR系列文章

[SignalR2] SignalR JavaScript 客户端

jQuery.SignalR.js必须依赖jQuery.js,也是以jQuery插件的形式存在。

基本使用

connection连接

$.connection( url ) 创建一个新连接并返回一个 connection 对象,connection 对象下会有很多属性、事件等等,以便于我们调试、查看连接情况等等。

connection.received( handler(data) )

监听每次接收的数据,data是一JSON对象,例:

{ "A": [], "H": "msgHub", "M": "pushGet" }

其中A为所接收的数据,H为Hub名称,M为方法名称,具体使用:

$.connection.msgHub.connection.received(function (data) {
    console.log(data);
});

connection.error( handler(error) )

错误处理。

connection.stateChanged( handler(change) )

连接状态更改时发生,比如当前新连接或关闭连接时。

connection.reconnected( handler() )

重新连接。

connection.starting( handler )

在要进行 /negotiate 连接前发生,或者我们也可以说他初始完成后准备开始连接时。

connection.start()

如果单纯 start() 则会按默认来启动,并返回一个带有 promise() 的对象;也可以指定一个function做为回调。或传递一些配置参数,参数包括:

  • transport:用来指定我们采用哪一种数据传输,默认为:auto,包括:["webSockets", "foreverFrame", "serverSentEvents", "longPolling"]
  • callback:回调函数。
  • waitForPageLoad:是否等待网页完成加载后才开始连接,默认:true。

以上是比较重要的几个事件,另外还有 connection.idconnection.messageId 等属性,具体请查阅参考资料链接。

hub相关

当建立连接后,可以指定一个或多个Hub,而不管是客户端调用服务端方法或服务端调用客户端方法,我们都需要创建一个代理类对象,而这一切虽然会由SignalR来自动生成,但还是需要有所了解。

  1. 客户端调用服务端方法是执行hub对象下的 invoke 方法。对应于自动生成的Hub代理类的 server 属性。
  2. 服务端调用客户端方法是将方法绑定到 on 对象下,这样在接收数据时,只需要根据提供的JSON对象下的 H 和 M 属性就可以确认应该执行哪个函数。对应于自动生成的Hub代理类的 client 属性。

创建Hub代理

每一个Hub的类名做为客户端的代理类名,当然也可以使用 [HubName("msgHub")] 重新自定名称。相对于的客户端代码会是:

// 自动生成的引用方式
var msgHug = $.connection.msgHub;
// 或手写创建一个Hub代理对象
var msgHug = connection.createHubProxy('msgHub');

定义客户端调用服务端方法或服务端调用客户端方法

自动生成的Hub代理类

默认会根据Hub类下的公共方法自动生成一个调用服务端方法的代理脚本,可以直接引用。而对于服务端调用客户端方法的部分,依然还是需要进行手工扩展,比如:

$.extend(msgHub.client, {
    pushNow: function (res) {
        $('#J_Now').html(res);
    },
    pushGet: function (res) {
        $('#J_GetCount').html(JSON.stringify(res));
    }
});

手工创建

如前我所说的,我们只需要将我们的相应方法绑定到相应的对象中即可。比如以下是绑定供服务端调用客户端的两个方法 pushNowpushGet

msgHub.on('pushNow', function (res) {
    $('#J_Now').html(res);
});
msgHub.on('pushGet', function (res) {
    $('#J_GetCount').html(JSON.stringify(res));
});

对于客户端调用服务端方法只需要将 on 更改为 invoke 即可。

指定请求时QueryString参数

$.connection.hub.qs = {'version': '1.0' } 会将我们指定的对象转化成URL参数的形式一并发送到服务端,同时我们可以在服务端的Hub里面 Context.QueryString['version'] 接收。

解析篇

SignalR Javascript 创建两个插件,分别:$.hubConnection$.signalR

$.hubConnection:负责Hub管理,包括:注册、接收数据时的回调等等。
$.signalR:负责数据连接,比如:启动连接、发送数据、接收数据等等。

当我们创建一个 $.hubConnection 的同时也会创建一个相应的 $.signalR 并赋给 connection 属性,因此可以在一个 Hub 对象中访问,比如:msgHub.connection

当一切数据都准好后,接下来重点看一下启动时他做了些什么事:

  • 检查是否有 window.JSON 对象,如果没有我们还需要引用 json2.js,特别是IE8以下。
  • 检查 transport 配置属性值是否正确。
  • 检查当前连接状态,如果为 disconnected,更改为 connecting,此时会触发一个 changeState 事件。
  • 创建连接超时处理事件,当我们一次请求超时会尝试一次重新连接(默认超时时长是30秒)。
  • 进行一次 /negotiate 请求,用于获取连接token、服务器对webSocket的支持情况、连接超时时长等等。
  • 开始构建应该采用哪一种数据传输方式,这里有个判断就是当 /negotiateTryWebSocketsfalse 时,会将剔除webSocket连接方式。
  • 初始化完成 transport 后,会调用 start() 方法。此时会根据不同的 transport 真正的创建连接。
  • 最后会再发请求一次 /start 来检查是否创建成功。

transport

我们一直在提到数据传输,SignalR共提供四种方案,我们可以随意指定按某一种或由自动按 ["webSockets", "foreverFrame", "serverSentEvents", "longPolling"] 顺序来检测应该采用哪一种方案。

当为 auto 时会按以下规则来决定采用哪种方案:

  1. config.jsonp = true 时采用:longPolling。
  2. /negotiateTryWebSocketsfalse 剔除:webSocket。
  3. 接下按剩下的依次尝试连接。

当然我们也可以自己创建一个 transport,只需要符合规范,并把创建的挂勾到 $.signalR.transports 下。

以是基于jquery.signalR-2.1.2.js版本分析,其实整体的结构非常清晰,而且阅读起来非常简单。

参考资料

查看 SignalR系列文章

[SignalR2] WebSocket部署问题和优化若干问题

虽然关于部署会非常简单,也没有太多细节可以说。而了解一些相关性资料,这对于优化非常有帮助。

支持问题

我不知道算不算在澄清一个事实,那便是按微软给出的文档中必须是IIS8+才支持WebSocket协议,但好多资料文章都写着可以运用第三方扩展,我找过,但连接扩展文件的连接都失效。

WebSocket协议安装

默认情况下IIS是不会自动安装WebSocket协议的,介绍WIN8和WIN12的安装办法

web.config的 <webSocket> 节点

当IIS支持WebSocket协议后,我们就可以在web.config里面的 system.webServer/webSocket 下配置:

  • enabled:启用是否支持WebSocket。

  • pingInterval:WebSocket有支持ping/pong,用于持续连接(keep-alive)、心跳、网络状态检测、延迟测量等。但目前好像很多浏览器没有支持这一特性。

  • receiveBufferLimit:接收数据的缓存区大小。默认:4194304B。

SignalR 配置

Startup.cs 中,我们可以调用 GlobalHost.Configuration 来配置一些参数,以便于更高效的使用SignalR。

DefaultMessageBufferSize

用于缓冲消息数量,默认值:每个连接的每个Hub缓冲1000条。言下之意就是,当我们大量群发消息时,假如我们同一时刻向2000个用户发送消息,这可能会由于客户端接收响应比较慢,可能会倒置只有1500个用户会收到消息。

所以我们可以调整这个数字更大。方法也很简单:

GlobalHost.Configuration.DefaultMessageBufferSize = 2000;

对于大数据量的发送这种办法不非是完美的解决办法,最好是通过发送Ack包来检查是否发送成功,然后有节制性的逐批发送。

MaxIncomingWebSocketMessageSize

允许最大接收消息大小,默认是64KB。

IIS配置

WebSocket本身是一个协议,所以长连接或短连接这取决于IIS的配置。正因为如此,还得了解一下IIS上几个重要指标的配置。比如:每个应用程序的并发请求量、应用池请求队列大小等等。

参考资料

查看 SignalR系列文章

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top