2020年2月13日 星期四

使用 SignalR 讓 server 主動推播資料給 client

1. 安裝套件: Microsoft.AspNet.SignalR、Microsoft.AspNet.Cors、Microsoft.Owin.Cors

2. Startup.cs 加入
public void Configuration(IAppBuilder app)
{
// 不考慮跨網域連結
        app.MapSignalR();
// 考慮跨網域連結(僅針對MVC,傳統aspx 無效)
var corsPolicy = new CorsPolicy
{
AllowAnyMethod = true,
AllowAnyHeader = true,
SupportsCredentials = true,
};
corsPolicy.Origins.Add("https://somedomain"); // 設定來源網站白名單
var corsOptions = new CorsOptions
{
PolicyProvider = new CorsPolicyProvider
{
PolicyResolver = context =>
{
context.Headers.Add("Access-Control-Allow-Credentials", new[] { "true" });
return Task.FromResult(corsPolicy);
}
}
};
app.Map("/signalr", map =>
{
// Setup the CORS middleware to run before SignalR.
// By default this will allow all origins. You can 
// configure the set of origins and/or http verbs by
// providing a cors options with a different policy.
map.UseCors(corsOptions);                
var hubConfiguration = new HubConfiguration
{
// You can enable JSONP by uncommenting line below.
// JSONP requests are insecure but some older browsers (and some
// versions of IE) require JSONP to work cross domain
// EnableJSONP = true
};
// Run the SignalR pipeline. We're not using MapSignalR
// since this branch already runs under the "/signalr"
// path.
map.RunSignalR(hubConfiguration);
});
}

3. 新增class
public class GetHub : Microsoft.AspNet.SignalR.Hub
    {
private static IHubContext context = GlobalHost.ConnectionManager.GetHubContext<GetHub>();

class ConnectionMapping<T>
{
private readonly Dictionary<T, HashSet<string>> _connections =
new Dictionary<T, HashSet<string>>();

public int Count
{
get
{
return _connections.Count;
}
}

public void Add(T key, string connectionId)
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(key, out connections))
{
connections = new HashSet<string>();
_connections.Add(key, connections);
}

lock (connections)
{
connections.Add(connectionId);
}
}
}

public IEnumerable<string> GetConnections(T key)
{
HashSet<string> connections;
if (_connections.TryGetValue(key, out connections))
{
return connections;
}

return Enumerable.Empty<string>();
}

public void Remove(T key, string connectionId)
{
lock (_connections)
{
HashSet<string> connections;
if (!_connections.TryGetValue(key, out connections))
{
return;
}

lock (connections)
{
connections.Remove(connectionId);

if (connections.Count == 0)
{
_connections.Remove(key);
}
}
}
}
}

private readonly static ConnectionMapping<string> _connections =
   new ConnectionMapping<string>();

public static void SendChatMessage(string who,string name, string message)
{  
foreach (var connectionId in _connections.GetConnections(who))
{
context.Clients.Client(connectionId).Invoke(name, message);
}
}

public override Task OnConnected()
{
string name = Context.User.Identity.Name;

_connections.Add(name, Context.ConnectionId);

return base.OnConnected();
}

public override Task OnDisconnected(bool stopCalled)
{
string name = Context.User.Identity.Name;

_connections.Remove(name, Context.ConnectionId);

return base.OnDisconnected(stopCalled);
}

public override Task OnReconnected()
{
string name = Context.User.Identity.Name;

if (!_connections.GetConnections(name).Contains(Context.ConnectionId))
{
_connections.Add(name, Context.ConnectionId);
}

return base.OnReconnected();
}
    }

4. .cs 針對要推播資料的地方加入以下程式
//推撥給所有人
var hub = Microsoft.AspNet.SignalR.GlobalHost.ConnectionManager.GetHubContext<GetHub>();
hub.Clients.All.發送訊息(msg); 
//推撥給同一網頁前端
string username = HttpContext.Current.User.Identity.Name;
GetHub.SendChatMessage(username, name, message); 
//推播給特定連線id
hub.Clients.Client(connid).Invoke(hubmethod, 回報訊息); 

5. .cshtml 引入 js
<script src="~/Scripts/jquery.signalR-2.4.1.min.js"></script>

6. .js 接收 server 推播
var connection = $.hubConnection('<%: ResolveUrl("~/signalr") %>'); // 若不指定參數會預設為 /signalr,造成在正式環境找不到路徑(若有虛擬目錄) 
ps. asp.net mvc => @Url.Content("~/signalr")
var connid; // 呼叫後端時帶入,以便可以指定回報訊息給此連線
connection.disconnected(function () {
            setTimeout(function () {
                connection.start({ transport:'webSockets'}).done(function () {
                        connid = connection.id;
                    });
            }, 5000); // Restart connection after 5 seconds.
        });
var proxy = connection.createHubProxy('GetHub');
proxy.on('發送訊息', function (msg) {
alert(msg);
});
connection.start({ transport:'webSockets'}).done(function () {// transport:'webSockets'  強制指定傳輸方式為webSockets (否則就算可以使用webSockets但自動選擇時不見得會採用此方式)
                        connid = connection.id;
                    }); 

若為asp.net 網站,需修改 web.config 加入
<add key="owin:AutomaticAppStartup" value="true" />
<system.webServer>
<validation validateIntegratedModeConfiguration="false" /> 

若瀏覽器遇到此錯誤: WebSocket connection to 'ws://*****' failed: Error during WebSocket handshake: net::ERR_CONNECTION_RESET
請修改 web.config httpRuntime 加入 targetFramework="xxx" 
須為4.5以上,若未指定則預設為4.0,而4.0 不支援 WebSocket 傳輸方式,雖然實際會改用 Long Polling ,一樣能正常運作但效能較差

iis 7.5 以前不支援webSockets 方式傳輸(最佳傳輸方式),iis 8.0 以後需要新增伺服器角色"websockets通訊協定"才能支援,確認目前使用何種方式傳輸:
connection.start().done(function () {
    console.log("Connected, transport = " + connection.transport.name);
});

若iis 有設定 工作者處理序數上限>1 則要另外架構 backplane,以便訊息可以在不同 process 間傳遞,參考這裡
 

Entity Framework 建立新物件並儲存後馬上取得關聯資料

使用 CreateProxy 建立物件,不要直接 new var newmodel = _contextXXX.CreateProxy<yyy>(); ... _contextXXX.yyy.Add(newmodel); await _contextXXX.SaveC...