forked from aspnet/AzureSignalR-samples
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
494 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Realtime Sign-in Example using Azure SignalR Service | ||
|
||
[Not working after latest SignalR update, still working on a fix] | ||
|
||
This sample application shows how to build a realtime application using Azure SignalR Service and serverless architecture. When you open homepage of the application, you will see how many people has visited this page (and their OS and browser distribution) and the page will auto update when others open the same page. | ||
|
||
## How Does It Work | ||
|
||
The application is built on top of Azure SignalR Service, Functions and Storage. There is no web server needed in this sample. | ||
|
||
Here is a diagram that illustrates the structure of this appliaction: | ||
|
||
![architecture](../../docs/images/signin.png) | ||
|
||
1. When user opens the homepage, a HTTP call will be made to an API exposed by Azure Function HTTP trigger, which will record your information and save it to Azure table storage. | ||
2. This API also returns a url and token for browser to connect to Azure SignalR Service. | ||
3. Then the API calculate statistics information (number of visits, OS and browser distribution) and use Azure SignalR Service to broadcast to all clients so browser can do a realtime update without need to do a refresh. | ||
4. The static content (homepage, scripts) are stored in Azure blob storage and exposed to user through Azure Function proxy. | ||
|
||
## How to Deploy to Azure | ||
|
||
TO BE ADDED |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
|
||
<head> | ||
<!-- Required meta tags --> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||
|
||
<!-- Bootstrap CSS --> | ||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" | ||
crossorigin="anonymous"> | ||
|
||
<title>Azure SignalR Sign-in Sample</title> | ||
</head> | ||
|
||
<body> | ||
<div id="main" class="container collapse"> | ||
<div class="row"> | ||
<div class="col-12"> | ||
<h1 class="text-center"><span id="count"></span> people have visited this page!</h1> | ||
</div> | ||
</div> | ||
<div class="row"> | ||
<div class="col-12 col-lg-6"> | ||
<canvas id="chartByOS" width="400" height="400"></canvas> | ||
</div> | ||
<div class="col-12 col-lg-6"> | ||
<canvas id="chartByBrowser" width="400" height="400"></canvas> | ||
</div> | ||
</div> | ||
<div class="row justify-content-center"> | ||
<div class="col-2"><div id="qrcode" class="d-none d-lg-block"></div></div> | ||
</div> | ||
</div> | ||
|
||
<!-- Optional JavaScript --> | ||
<!-- jQuery first, then Popper.js, then Bootstrap JS --> | ||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" | ||
crossorigin="anonymous"></script> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.0/umd/popper.min.js" integrity="sha384-cs/chFZiN24E4KMATLdqdvsezGxaGsi4hLGOzlXwp5UZB1LY//20VyM2taTB4QvJ" | ||
crossorigin="anonymous"></script> | ||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" | ||
crossorigin="anonymous"></script> | ||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.min.js"></script> | ||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@0.3.0"></script> | ||
<script src="scripts/signalr.min.js"></script> | ||
<script src="scripts/qrcode.min.js"></script> | ||
<script> | ||
var chartByOS, chartByBrowser; | ||
|
||
function prepareData(data) { | ||
var list = []; | ||
for (var label in data) list.push([label, data[label]]); | ||
list.sort((x, y) => x[0] > y[0] ? 1 : x[0] == y[0] ? 0 : -1); | ||
return { | ||
labels: list.map(i => i[0]), | ||
values: list.map(i => i[1]) | ||
}; | ||
} | ||
|
||
function createChart(element, data, title) { | ||
var backgroundColors = [ | ||
'rgba(255, 99, 132, 0.2)', | ||
'rgba(54, 162, 235, 0.2)', | ||
'rgba(255, 206, 86, 0.2)', | ||
'rgba(75, 192, 192, 0.2)', | ||
'rgba(153, 102, 255, 0.2)', | ||
'rgba(255, 159, 64, 0.2)' | ||
]; | ||
var borderColors = [ | ||
'rgba(255,99,132,1)', | ||
'rgba(54, 162, 235, 1)', | ||
'rgba(255, 206, 86, 1)', | ||
'rgba(75, 192, 192, 1)', | ||
'rgba(153, 102, 255, 1)', | ||
'rgba(255, 159, 64, 1)' | ||
]; | ||
var sorted = prepareData(data); | ||
var ctx = element.getContext('2d'); | ||
return new Chart(ctx, { | ||
type: 'bar', | ||
data: { | ||
labels: sorted.labels, | ||
datasets: [{ | ||
label: '# of Visitors', | ||
data: sorted.values, | ||
backgroundColor: backgroundColors, | ||
borderColor: borderColors, | ||
borderWidth: 1 | ||
}] | ||
}, | ||
options: { | ||
plugins: { | ||
datalabels: { | ||
color: 'black', | ||
font: { | ||
size: '20', | ||
weight: 'bold' | ||
}, | ||
formatter: Math.round | ||
} | ||
}, | ||
title: { | ||
display: true, | ||
text: title | ||
}, | ||
scales: { | ||
yAxes: [{ | ||
ticks: { | ||
beginAtZero: true | ||
} | ||
}] | ||
} | ||
} | ||
}); | ||
} | ||
|
||
function updateChart(chart, data) { | ||
var sorted = prepareData(data); | ||
chart.data.labels = sorted.labels; | ||
chart.data.datasets[0].data = sorted.values; | ||
chart.update(); | ||
} | ||
|
||
function signIn(url) { | ||
return new Promise((resolve, reject) => { | ||
var xhr = new XMLHttpRequest(); | ||
xhr.open('GET', url, true); | ||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); | ||
xhr.send(); | ||
xhr.onload = () => { | ||
if (xhr.status >= 200 && xhr.status < 300) { | ||
resolve(JSON.parse(xhr.response || xhr.responseText)); | ||
} | ||
else { | ||
reject(new Error(xhr.statusText)); | ||
} | ||
}; | ||
|
||
xhr.onerror = () => { | ||
reject(new Error(xhr.statusText)); | ||
} | ||
}); | ||
} | ||
|
||
function initPage(stat) { | ||
$('#count').text(stat.totalNumber); | ||
chartByOS = createChart(document.getElementById("chartByOS"), stat.byOS, '# of Visitors by OS'); | ||
chartByBrowser = createChart(document.getElementById("chartByBrowser"), stat.byBrowser, '# of Visitors by Browser'); | ||
$("#main").show(); | ||
} | ||
|
||
function updatePage(stat) { | ||
$('#count').text(stat.totalNumber); | ||
updateChart(chartByOS, stat.byOS); | ||
updateChart(chartByBrowser, stat.byBrowser); | ||
} | ||
|
||
function startConnection(url, accessToken, configureConnection) { | ||
return function start(transport) { | ||
console.log(`Starting connection using ${signalR.TransportType[transport]} transport`); | ||
var connection = new signalR.HubConnection(url, { transport: transport, accessTokenFactory: () => accessToken }); | ||
if (configureConnection && typeof configureConnection === 'function') { | ||
configureConnection(connection); | ||
} | ||
|
||
return connection.start() | ||
.then(function () { | ||
return connection; | ||
}) | ||
.catch(function (error) { | ||
console.log(`Cannot start the connection use ${signalR.TransportType[transport]} transport. ${error.message}`); | ||
if (transport !== signalR.TransportType.LongPolling) { | ||
return start(transport + 1); | ||
} | ||
|
||
return Promise.reject(error); | ||
}); | ||
}(signalR.TransportType.WebSockets); | ||
} | ||
|
||
function bindConnectionMessage(connection) { | ||
connection.on('updateSignInStats', updatePage); | ||
} | ||
|
||
new QRCode(document.getElementById("qrcode"), window.location.href); | ||
signIn("https://kenchensigninfun2.azurewebsites.net/signin") | ||
// signIn("/signin") | ||
.then(function (result) { | ||
initPage(result.Stats); | ||
return startConnection(result.AuthInfo.ServiceUrl, result.AuthInfo.AccessToken, bindConnectionMessage); | ||
}); | ||
/*.then(onConnected) | ||
.catch(onConnectionError);*/ | ||
</script> | ||
</body> | ||
|
||
</html> |
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<configuration> | ||
<packageSources> | ||
<add key="azure-signalr-dev" value="https://www.myget.org/F/azure-signalr-dev/api/v3/index.json" /> | ||
<add key="aspnetcore-dev" value="https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" /> | ||
</packageSources> | ||
</configuration> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<TargetFramework>net461</TargetFramework> | ||
</PropertyGroup> | ||
<ItemGroup> | ||
<PackageReference Include="Microsoft.Azure.WebJobs" Version="2.2.0" /> | ||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="2.2.0" /> | ||
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="1.1.0" /> | ||
<PackageReference Include="Microsoft.Azure.SignalR" Version="1.0.0-preview-10001" /> | ||
<PackageReference Include="UAParser" Version="3.0.0" /> | ||
<PackageReference Include="WindowsAzure.Storage" Version="7.2.1" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<Reference Include="Microsoft.CSharp" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<None Update="host.json"> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
</None> | ||
<None Update="local.settings.json"> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
<CopyToPublishDirectory>Never</CopyToPublishDirectory> | ||
</None> | ||
<None Update="proxies.json"> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
</None> | ||
<None Update="signin\function.json"> | ||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
</None> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
|
||
Microsoft Visual Studio Solution File, Format Version 12.00 | ||
# Visual Studio 15 | ||
VisualStudioVersion = 15.0.27004.2005 | ||
MinimumVisualStudioVersion = 10.0.40219.1 | ||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RealtimeSignIn", "RealtimeSignIn.csproj", "{D0547646-AA6D-4759-89C8-1FB966C1A1F7}" | ||
EndProject | ||
Global | ||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
Debug|Any CPU = Debug|Any CPU | ||
Release|Any CPU = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
{D0547646-AA6D-4759-89C8-1FB966C1A1F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{D0547646-AA6D-4759-89C8-1FB966C1A1F7}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{D0547646-AA6D-4759-89C8-1FB966C1A1F7}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{D0547646-AA6D-4759-89C8-1FB966C1A1F7}.Release|Any CPU.Build.0 = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(SolutionProperties) = preSolution | ||
HideSolutionNode = FALSE | ||
EndGlobalSection | ||
GlobalSection(ExtensibilityGlobals) = postSolution | ||
SolutionGuid = {3C3EC256-4B06-41F4-88B3-90EE0C1B7086} | ||
EndGlobalSection | ||
EndGlobal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Threading.Tasks; | ||
using Microsoft.Azure.WebJobs; | ||
using Microsoft.Azure.WebJobs.Extensions.Http; | ||
using Microsoft.Azure.WebJobs.Host; | ||
using Microsoft.WindowsAzure.Storage; | ||
using Microsoft.WindowsAzure.Storage.Table; | ||
using UAParser; | ||
|
||
namespace RealtimeSignIn | ||
{ | ||
public class SignInInfo : TableEntity | ||
{ | ||
public string OS { get; set; } | ||
public string Browser { get; set; } | ||
|
||
public SignInInfo(string os, string browser) | ||
{ | ||
PartitionKey = "SignIn"; | ||
RowKey = Guid.NewGuid().ToString(); | ||
OS = os; | ||
Browser = browser; | ||
} | ||
|
||
public SignInInfo() | ||
{ | ||
} | ||
} | ||
|
||
class SignInStats | ||
{ | ||
public int totalNumber; | ||
public Dictionary<string, int> byOS = new Dictionary<string, int>(); | ||
public Dictionary<string, int> byBrowser = new Dictionary<string, int>(); | ||
} | ||
|
||
class SignInResult | ||
{ | ||
public AuthInfo AuthInfo; | ||
public SignInStats Stats; | ||
} | ||
|
||
public static class SignInFunction | ||
{ | ||
[FunctionName("signin")] | ||
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]HttpRequestMessage req, TraceWriter log) | ||
{ | ||
var ua = Parser.GetDefault().Parse(req.Headers.UserAgent.ToString()); | ||
var os = ua.OS.Family; | ||
var browser = ua.UserAgent.Family; | ||
|
||
/* | ||
var os = req.GetQueryNameValuePairs().FirstOrDefault(q => q.Key == "os").Value; | ||
var browser = req.GetQueryNameValuePairs().FirstOrDefault(q => q.Key == "browser").Value; | ||
dynamic data = await req.Content.ReadAsAsync<object>(); | ||
os = os ?? data?.os; | ||
browser = browser ?? data?.browser; | ||
if (os == null) return req.CreateErrorResponse(HttpStatusCode.BadRequest, "Missing os"); | ||
if (browser == null) return req.CreateErrorResponse(HttpStatusCode.BadRequest, "Missing browser");*/ | ||
|
||
var account = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("TableConnectionString")); | ||
var client = account.CreateCloudTableClient(); | ||
var table = client.GetTableReference("SignInInfo"); | ||
var newInfo = new SignInInfo(os, browser); | ||
var insert = TableOperation.Insert(newInfo); | ||
table.Execute(insert); | ||
|
||
var query = new TableQuery<SignInInfo>(); | ||
var stats = new SignInStats(); | ||
foreach (var info in table.ExecuteQuery(query)) | ||
{ | ||
stats.totalNumber++; | ||
if (!stats.byBrowser.ContainsKey(info.Browser)) stats.byBrowser[info.Browser] = 0; | ||
if (!stats.byOS.ContainsKey(info.OS)) stats.byOS[info.OS] = 0; | ||
stats.byBrowser[info.Browser]++; | ||
stats.byOS[info.OS]++; | ||
} | ||
|
||
var result = new SignInResult() | ||
{ | ||
AuthInfo = SignInHub.GetAuthInfo(), | ||
Stats = stats | ||
}; | ||
|
||
await SignInHub.UpdateSignInStats(stats); | ||
|
||
return req.CreateResponse(HttpStatusCode.OK, result, "application/json"); | ||
} | ||
} | ||
} |
Oops, something went wrong.