Real-Time Web Apps & .NET - What are your options?

183

Transcript of Real-Time Web Apps & .NET - What are your options?

PHIL @LEGGETTERHead of Developer Relations

2 / 121

@leggetter

3 / 121

@leggetter

What we'll cover

1. Why Real-Time?2. Common Real-Time Use Cases3. What are your options?

How do you choose?.NET examplesPros & Cons

4. The Future of Real-Time

4 / 121

@leggetter

When do we need Realtime?

5 / 121

@leggetter

WCaaS

6 / 121

@leggetter

WCaaS

Data: Is there a timely nature to the data?

6 / 121

@leggetter

7 / 121

@leggetter

User Experience: Is there a timely nature to the

experience?

7 / 121

@leggetter

Realtime is required when there's a Need or

Demand for:

Up to date informationInteraction to maintain engagement (UX)

8 / 121

@leggetter

These aren't new Needs or Demands

But...

9 / 121

@leggetter

These aren't new Needs or Demands

But...

The Internet

9 / 121

@leggetter

Internet

“ a global computer network providing a varietyof information and communication facilities,consisting of interconnected networks usingstandardized communication protocols.

10 / 121

@leggetter

11 / 121

@leggetter

12 / 121

@leggetter

13 / 121

@leggetter

HTTP was better. But many wanted more.

14 / 121

@leggetter

15 / 121

@leggetter

16 / 121

@leggetter

17 / 121

@leggetter

HTTP + Browsers were restrictive

HTTP - request/response paradigmKeeping persistent HTTP connections aliveNo cross-browser XMLHttpRequest2 connection limitNo browser cross origin supportGeneral cross browser incompatibilities

18 / 121

@leggetter

HTTP + Browsers were restrictive

HTTP - request/response paradigmKeeping persistent HTTP connections aliveNo cross-browser XMLHttpRequest2 connection limitNo browser cross origin supportGeneral cross browser incompatibilitiesSo we HACKED! Java Applets, Flash, HTTP Hacks

18 / 121

@leggetter

Then Real-Time Went Mainstream

19 / 121

@leggetter

Social

20 / 121

@leggetter

Technology Advancements

Memory & CPU speed and costThe CloudBrowser standardisation & enhancementsAny client can use the standards

21 / 121

@leggetter

22 / 121

@leggetter

MASSIVE Increase in Internet Usage

23 / 121

@leggetter

25 / 121

@leggetter

Common Real-Time Use Cases

26 / 121

@leggetter

Notifications & Activity Streams

27 / 121

@leggetter

Data Visualizations

28 / 121

@leggetter

29 / 121

Chat@leggetter

30 / 121

@leggetter

Real-Time Location Tracking

31 / 121

@leggetter

Multi-User Collaboration

32 / 121

@leggetter

WebRTC Powered AV Chat

33 / 121

@leggetter

34 / 121

@leggetter

Users expect a real-time UX

34 / 121

@leggetter

Users expect a real-time UX

Without a real-time UX your app appears

broken

34 / 121

@leggetter

Real-time Web Apps & .NET What are your options?

35 / 121

@leggetter

7 Factors to Consider

36 / 121

@leggetter

1. Should you keep on polling?

37 / 121

@leggetter

Polling Calculations

Scenario

1. Site average of 10,000 Users

38 / 121

@leggetter

Polling Calculations

Scenario

1. Site average of 10,000 Users2. Over 1 Hour, with a 10 second polling interval

38 / 121

@leggetter

Polling Calculations

Scenario

1. Site average of 10,000 Users2. Over 1 Hour, with a 10 second polling interval3. Requests from pages load + HTML, CSS, JS, Images for 10k users = 50,000

38 / 121

@leggetter

Polling Calculations

Scenario

1. Site average of 10,000 Users2. Over 1 Hour, with a 10 second polling interval3. Requests from pages load + HTML, CSS, JS, Images for 10k users = 50,0004. Poll requests per user/minute = (60 / 10) = 6

38 / 121

@leggetter

Polling Calculations

Scenario

1. Site average of 10,000 Users2. Over 1 Hour, with a 10 second polling interval3. Requests from pages load + HTML, CSS, JS, Images for 10k users = 50,0004. Poll requests per user/minute = (60 / 10) = 65. Poll requests per user/hour = (6 * 60) = 360

38 / 121

@leggetter

Polling Calculations

Scenario

1. Site average of 10,000 Users2. Over 1 Hour, with a 10 second polling interval3. Requests from pages load + HTML, CSS, JS, Images for 10k users = 50,0004. Poll requests per user/minute = (60 / 10) = 65. Poll requests per user/hour = (6 * 60) = 3606. Poll requests site wide per hour = (360 * 10,000) = 3,600,000

38 / 121

@leggetter

Polling Calculations

Scenario

1. Site average of 10,000 Users2. Over 1 Hour, with a 10 second polling interval3. Requests from pages load + HTML, CSS, JS, Images for 10k users = 50,0004. Poll requests per user/minute = (60 / 10) = 65. Poll requests per user/hour = (6 * 60) = 3606. Poll requests site wide per hour = (360 * 10,000) = 3,600,000

With polling the site would need to handle 3.65 Million requests per hour

Or 50k HTTP requests + maintain 10k persistent connections?

38 / 121

@leggetter

Cache - clients keep pollingPush Proxy solutions

fanout.iostreamdata.io

Quick Win solutions

39 / 121

@leggetter

2. Use an existing solution

Don't reinvent the wheel

Unless you've a unique use case

40 / 121

@leggetter

Why use an existing solution?

Connection fallback/upgrade hacks still requiredWebSocket: 91% of connectionsHTTP fallback: 9% of connections

41 / 121

@leggetter

Why use an existing solution?

Connection fallback/upgrade hacks still requiredWebSocket: 91% of connectionsHTTP fallback: 9% of connections

Support/Community

41 / 121

@leggetter

Why use an existing solution?

Connection fallback/upgrade hacks still requiredWebSocket: 91% of connectionsHTTP fallback: 9% of connections

Support/CommunityMaintenance

41 / 121

@leggetter

Why use an existing solution?

Connection fallback/upgrade hacks still requiredWebSocket: 91% of connectionsHTTP fallback: 9% of connections

Support/CommunityMaintenanceFuture features

41 / 121

@leggetter

Why use an existing solution?

Connection fallback/upgrade hacks still requiredWebSocket: 91% of connectionsHTTP fallback: 9% of connections

Support/CommunityMaintenanceFuture featuresScaling

41 / 121

@leggetter

42 / 121

@leggetter

j.mp/realtime-tech-guide

42 / 121

@leggetter

3. Use languages you're comfortable

with

43 / 121

@leggetter

3. Use languages you're comfortable

with

43 / 121

@leggetter

Solutions by language

PHP: Ratchet, dNode-phpJava: Netty, JettyJavaScript (Node.JS): Faye, Socket.IO (Engine.IO), Primus.io.NET (C#): SignalR, XSocketsPython: Lots of options built on TornadoRuby: em-websocket, FayeLanguage agnostic: most hosted services

44 / 121

@leggetter

4. Mobile Friendly?

45 / 121

@leggetter

Mobile Friendly

Only some have mobile libraries

46 / 121

@leggetter

Mobile Friendly

Only some have mobile librariesHow much data are you sending?

46 / 121

@leggetter

Mobile Friendly

Only some have mobile librariesHow much data are you sending?SSL required on 3/4G networks

46 / 121

@leggetter

5. Application/Solution Communication Patterns

47 / 121

@leggetter

5. Application/Solution Communication Patterns

How does the client/server & client/client communicate

47 / 121

@leggetter

Simple Messaging

48 / 121

@leggetter

0:00 49 / 121

@leggetter

Internet ^5 Machine

0:00 49 / 121

@leggetter

Internet ^5 Machine

Simple Messaging

// client

var ws = new WebSocket('wss://localhost/');

50 / 121

@leggetter

Simple Messaging

// client

var ws = new WebSocket('wss://localhost/');

ws.onmessage = function(evt) { var data = JSON.parse(evt.data);

50 / 121

@leggetter

Simple Messaging

// client

var ws = new WebSocket('wss://localhost/');

ws.onmessage = function(evt) { var data = JSON.parse(evt.data);

// ^5 performHighFive();};

50 / 121

@leggetter

Simple Messaging

// client

var ws = new WebSocket('wss://localhost/');

ws.onmessage = function(evt) { var data = JSON.parse(evt.data);

// ^5 performHighFive();};

// server

server.on('connection', function(socket){

50 / 121

@leggetter

Simple Messaging

// client

var ws = new WebSocket('wss://localhost/');

ws.onmessage = function(evt) { var data = JSON.parse(evt.data);

// ^5 performHighFive();};

// server

server.on('connection', function(socket){

socket.send(JSON.stringify({action: 'high-5'}));});

50 / 121

@leggetter

Simple Messaging

using Nexmo.Api;

// SMSvar results = SMS.Send(new SMS.SMSRequest { from = "15555551212", to = "17775551212", text = "this is a test" });

// Voicevar result = Voice.TextToSpeech(new Voice.TextToSpeechCallCommand { to = "17775551212", from = "15555551212", text = "Hello from Nexmo" });

51 / 121

@leggetter

PubSub

52 / 121

@leggetter

53 / 121

@leggetter

PubSub

// client

var client = new Faye.Client('http://localhost:8000/faye');

54 / 121

@leggetter

PubSub

// client

var client = new Faye.Client('http://localhost:8000/faye');

client.subscribe('/leggetter-updates', function(data) {

54 / 121

@leggetter

PubSub

// client

var client = new Faye.Client('http://localhost:8000/faye');

client.subscribe('/leggetter-updates', function(data) {

console.log(data.text);});

54 / 121

@leggetter

PubSub

// client

var client = new Faye.Client('http://localhost:8000/faye');

client.subscribe('/leggetter-updates', function(data) {

console.log(data.text);});

client.subscribe('/leggetter-dm-notifications', function(data) { console.log(data.count);});

54 / 121

@leggetter

PubSub

// client

var client = new Faye.Client('http://localhost:8000/faye');

client.subscribe('/leggetter-updates', function(data) {

console.log(data.text);});

client.subscribe('/leggetter-dm-notifications', function(data) { console.log(data.count);});

// server

server.publish('/leggetter-updates', {text: 'Hello DevWeek!'});

54 / 121

@leggetter

PubSub

// client

var client = new Faye.Client('http://localhost:8000/faye');

client.subscribe('/leggetter-updates', function(data) {

console.log(data.text);});

client.subscribe('/leggetter-dm-notifications', function(data) { console.log(data.count);});

// server

server.publish('/leggetter-updates', {text: 'Hello DevWeek!'});

server.publish('/leggetter-dm-notifications', {count: 2});

54 / 121

@leggetter

Evented PubSub

55 / 121

@leggetter

Evented PubSub

// client

var updates = io('/leggetter-updates');

56 / 121

@leggetter

Evented PubSub

// client

var updates = io('/leggetter-updates');updates.on('created', function (data) { // Add activity to UI});

56 / 121

@leggetter

Evented PubSub

// client

var updates = io('/leggetter-updates');updates.on('created', function (data) { // Add activity to UI});updates.on('updated', function(data) { // Update activity});updates.on('deleted', function(data) { // Remove activity});

56 / 121

@leggetter

Evented PubSub

// client

var updates = io('/leggetter-updates');updates.on('created', function (data) { // Add activity to UI});updates.on('updated', function(data) { // Update activity});updates.on('deleted', function(data) { // Remove activity});

// server

var io = require('socket.io')();var updates = io.of('/leggetter-updates');

56 / 121

@leggetter

Evented PubSub

// client

var updates = io('/leggetter-updates');updates.on('created', function (data) { // Add activity to UI});updates.on('updated', function(data) { // Update activity});updates.on('deleted', function(data) { // Remove activity});

// server

var io = require('socket.io')();var updates = io.of('/leggetter-updates');updates.emit('created', {text: 'PubSub Rocks!', id: 1});

56 / 121

@leggetter

Evented PubSub

// client

var updates = io('/leggetter-updates');updates.on('created', function (data) { // Add activity to UI});updates.on('updated', function(data) { // Update activity});updates.on('deleted', function(data) { // Remove activity});

// server

var io = require('socket.io')();var updates = io.of('/leggetter-updates');updates.emit('created', {text: 'PubSub Rocks!', id: 1});updates.emit('updated', {text: 'Evented PubSub Rocks!', id: 1});updates.emit('deleted', {id: 1});

56 / 121

@leggetter

PubSub vs. Evented PubSub

57 / 121

@leggetter

58 / 121

@leggetter

59 / 121

@leggetter

PubSub

client.subscribe('devexp-channel', function(data) { if(data.eventType === 'chat-message') { addMessage(data.message); }

60 / 121

@leggetter

PubSub

client.subscribe('devexp-channel', function(data) { if(data.eventType === 'chat-message') { addMessage(data.message); } else if(data.eventType === 'channel-purposed-changed') { updateRoomTitle(data.purpose); } else if(/* and so on */) { }})

60 / 121

@leggetter

PubSub

client.subscribe('devexp-channel', function(data) { if(data.eventType === 'chat-message') { addMessage(data.message); } else if(data.eventType === 'channel-purposed-changed') { updateRoomTitle(data.purpose); } else if(/* and so on */) { }})

Evented PubSub

var devexp = io('/devexp-channel');devexp.on('chat-message', addMessage);devexp.on('channel-purposed-changed', updateChannelPurpose);

60 / 121

@leggetter

PubSub

client.subscribe('devexp-channel', function(data) { if(data.eventType === 'chat-message') { addMessage(data.message); } else if(data.eventType === 'channel-purposed-changed') { updateRoomTitle(data.purpose); } else if(/* and so on */) { }})

Evented PubSub

var devexp = io('/devexp-channel');devexp.on('chat-message', addMessage);devexp.on('channel-purposed-changed', updateChannelPurpose);devexp.on('current-topic-changed', updateChannelTopic);devexp.on('user-online', userOnline);devexp.on('user-offline', userOffline);

60 / 121

@leggetter

DataSync

61 / 121

@leggetter

62 / 121

@leggetter

Data Sync

// client

var ref = new Firebase("https://app.firebaseio.com/doc1/lines");

63 / 121

@leggetter

Data Sync

// client

var ref = new Firebase("https://app.firebaseio.com/doc1/lines");

ref.on('child_added', function(childSnapshot, prevChildKey) { // code to handle new child.});

63 / 121

@leggetter

Data Sync

// client

var ref = new Firebase("https://app.firebaseio.com/doc1/lines");

ref.on('child_added', function(childSnapshot, prevChildKey) { // code to handle new child.});

ref.on('child_changed', function(childSnapshot, prevChildKey) { // code to handle child data changes.});

63 / 121

@leggetter

Data Sync

// client

var ref = new Firebase("https://app.firebaseio.com/doc1/lines");

ref.on('child_added', function(childSnapshot, prevChildKey) { // code to handle new child.});

ref.on('child_changed', function(childSnapshot, prevChildKey) { // code to handle child data changes.});

ref.on('child_removed', function(oldChildSnapshot) { // code to handle child removal.});

63 / 121

@leggetter

Data Sync

// client

var ref = new Firebase("https://app.firebaseio.com/doc1/lines");

ref.on('child_added', function(childSnapshot, prevChildKey) { // code to handle new child.});

ref.on('child_changed', function(childSnapshot, prevChildKey) { // code to handle child data changes.});

ref.on('child_removed', function(oldChildSnapshot) { // code to handle child removal.});

ref.push({ 'editor_id': 'leggetter', 'text': 'Nexmo Rocks!' });

63 / 121

@leggetter

Data Sync

// client

var ref = new Firebase("https://app.firebaseio.com/doc1/lines");

ref.on('child_added', function(childSnapshot, prevChildKey) { // code to handle new child.});

ref.on('child_changed', function(childSnapshot, prevChildKey) { // code to handle child data changes.});

ref.on('child_removed', function(oldChildSnapshot) { // code to handle child removal.});

ref.push({ 'editor_id': 'leggetter', 'text': 'Nexmo Rocks!' });

Framework handles updates to other clients

63 / 121

@leggetter

RMI (aka RPC)

64 / 121

@leggetter

65 / 121

@leggetter

RMI

// clientvar chat = $.connection.chatHub;

66 / 121

@leggetter

RMI

// clientvar chat = $.connection.chatHub;

chat.client.broadcastMessage = function (name, message) { // handle message};

66 / 121

@leggetter

RMI

// clientvar chat = $.connection.chatHub;

chat.client.broadcastMessage = function (name, message) { // handle message};

chat.server.send( 'me', 'hello world' );

66 / 121

@leggetter

RMI

// clientvar chat = $.connection.chatHub;

chat.client.broadcastMessage = function (name, message) { // handle message};

chat.server.send( 'me', 'hello world' );

$.connection.hub.start(); // async

66 / 121

@leggetter

RMI

// clientvar chat = $.connection.chatHub;

chat.client.broadcastMessage = function (name, message) { // handle message};

chat.server.send( 'me', 'hello world' );

$.connection.hub.start(); // async

// serverpublic class ChatHub : Hub{

66 / 121

@leggetter

RMI

// clientvar chat = $.connection.chatHub;

chat.client.broadcastMessage = function (name, message) { // handle message};

chat.server.send( 'me', 'hello world' );

$.connection.hub.start(); // async

// serverpublic class ChatHub : Hub{ public void Send(string name, string message) {

66 / 121

@leggetter

RMI

// clientvar chat = $.connection.chatHub;

chat.client.broadcastMessage = function (name, message) { // handle message};

chat.server.send( 'me', 'hello world' );

$.connection.hub.start(); // async

// serverpublic class ChatHub : Hub{ public void Send(string name, string message) { // Call the broadcastMessage method to update clients. Clients.All.broadcastMessage(name, message); }}

66 / 121

@leggetter

67 / 121

@leggetter

68 / 121

@leggetter

69 / 121

@leggetter

70 / 121

@leggetter

71 / 121

@leggetter

6. Deployment & ArchitectureConsiderations

72 / 121

@leggetter

Code

https://github.com/leggetter/realtime-dotnet-examplesShort link: http://j.mp/rt-dotnet-ex

73 / 121

@leggetter

Self Hosted (Tightly Coupled) 74 / 121

@leggetter

.NET Self-Hosted Real-Time options

SignalRXSockets

75 / 121

@leggetter

76 / 121

@leggetter

Self-Hosted Demo 1: ASP.NET + SignalR (Tightly Coupled)76 / 121

@leggetter

What we'll look at:

References\Microsoft.AspNet.SignalR.*

Scripts\jquery.signalR*.js

App_Start\SignalRStartup.cs

Controllers\HomeController.cs

Hubs\ChatHub.cs

Views\Home\SignalR.cshtml

Script\chat\SignalRChat.js

77 / 121

@leggetter

Pros

.NETSimple integrationMS SupportedjQuery Dependency

Cons

Tightly coupledRMI onlySelf-ScalingScaling (realtime + HTTP)

Self-Hosted Demo 1: Pro & Cons

78 / 121

@leggetter

79 / 121

@leggetter

Self-Hosted Demo 2: ASP.NET + XSockets (Tightly Coupled)79 / 121

@leggetter

What we'll look at:

References\XSockets.*

App_Start\XSocketsStartup.cs

Controllers\HomeController.cs

XSockets\ChatController.cs

Views\Home\XSockets.cshtml

Scripts\XSockets.latest.js

Script\chat\XSocketsChat.cs

80 / 121

@leggetter

Pros

.NETSimple integrationCommunication patterns

PubSub/EventedRMI

Licensed

Cons

Tightly coupledSelf-ScalingScaling (realtime + HTTP)Licensed

Self-Hosted Demo 2: Pro & Cons

81 / 121

@leggetter

Self-Hosted: .NET + Message Queue (Loosely Coupled)82 / 121

@leggetter

83 / 121

@leggetter

Pros

.NETMaps well to PubSubLoosely coupledCould use another runtime

Cons

How does it fit with RMI/SignalR?Multiple componentsSelf-scalingQueue routing questionsIn: HTTP. Out: WebSocket

Self-Hosted: .NET + Message Queue - Pro &

Cons

84 / 121

@leggetter

85 / 121

@leggetter

Self-Hosted: ASP.NET + Faye

(Loosely Coupled)85 / 121

@leggetter

Pros

PubSubConnection fallbackRedis Queue supportSimple integration

Cons

Not .NET(?)You need to scale

Self-Hosted + Faye: Pros & Cons

86 / 121

@leggetter

.NET Hosted Real-Time options

AblyFirebaseFanoutPubNubPusherRealtime.coSyncano

87 / 121

@leggetter

88 / 121

@leggetter

Hosted Demo: Pusher88 / 121

@leggetter

What we'll look at:

References\PusherServer

Controllers\HomeController.cs

Views\Home\Pusher.cshtml

Script\chat\PusherChat.js

Pusher Debug Console

89 / 121

@leggetter

Pros

Simple & powerfulInstantly scalableManaged & dedicatedDirect integration. No overhead.

Cons

3rd party relianceDifficult to influence functionality

Hosted - Pros & Cons

90 / 121

@leggetter

Why use a hosted service?

Scenario

1. Site average of 10,000 Users

91 / 121

@leggetter

Why use a hosted service?

Scenario

1. Site average of 10,000 Users2. Over 1 Hour, no polling

91 / 121

@leggetter

Why use a hosted service?

Scenario

1. Site average of 10,000 Users2. Over 1 Hour, no polling3. Requests from pages load + HTML, CSS, JS, Images for 10k users = 50,000

91 / 121

@leggetter

Why use a hosted service?

Scenario

1. Site average of 10,000 Users2. Over 1 Hour, no polling3. Requests from pages load + HTML, CSS, JS, Images for 10k users = 50,0004. That's it! Total: 50,000

91 / 121

@leggetter

Why use a hosted service?

Scenario

1. Site average of 10,000 Users2. Over 1 Hour, no polling3. Requests from pages load + HTML, CSS, JS, Images for 10k users = 50,0004. That's it! Total: 50,000

Your servers handle 50k requests per hour instead of 3.6M

You offload the polling or persistent connections to the service

91 / 121

@leggetter

7. Self-Hosted v Hosted

"Build vs. Buy"

92 / 121

@leggetter

Build vs. Buy - Costs

baremetrics.com/calculator

93 / 121

@leggetter

How do you choose?

7 Realtime Framework Considerations

1. Should you keep on polling?2. Use an Existing Solution3. Use a language you're comfortable with4. Do you need native mobile support?5. Simple Messaging, PubSub/Evented, RMI or DataSync6. Architectural considerations7. Hosted v Self-Hosted (Build vs. Buy)

94 / 121

@leggetter

Future

95 / 121

@leggetter

Network Infrastructure & Protocols

ReliabilitySpeedBeyond HTTPHTTP2

96 / 121

@leggetter

98 / 121

@leggetter

FirebaseGitHubIron.ioMailChimp

MailJetPagerDutyNexmoSendGrid

Real-Time APIs

99 / 121

@leggetter

100 / 121

@leggetter

More "Things"!

101 / 121

@leggetter

The Physical Web

102 / 121

@leggetter

IoT, Apps & Developers

103 / 121

@leggetter

A thing can be anything

104 / 121

@leggetter

A thing can be anything

SensorsAppliancesVehiclesSmart PhonesDevices (Arduino, Electric Imp, Raspberry Pi etc.)

104 / 121

@leggetter

A thing can be anything

SensorsAppliancesVehiclesSmart PhonesDevices (Arduino, Electric Imp, Raspberry Pi etc.)ServersBrowsersApps: Native, Web, running anywhere

104 / 121

@leggetter

The Majority of code we'll write will still be

for "Apps"

ConfiguringMonitoringInteractingApp Logic

105 / 121

@leggetter

Real-Time Use Case Evolution

Notifications & SignallingActivity StreamsData Viz & PollsChatCollaborationMultiplayer Games

106 / 121

@leggetter

Notifications/Activity Streams -> Actions

107 / 121

@leggetterThe end of apps as we know it - Intercom

Subscriptions

108 / 121

@leggetter

Personalised Event Streams

109 / 121

@leggetter

Unified UIs

110 / 121

@leggetter

Chat & Bots for Everything and the rise of the .ai domain 111 / 121

@leggetter

600M MAUs10M integrationsapp-within-an-app modeltaxi, order food, tickets, games etc.

WeChat

112 / 121

@leggetter

Chat Integrations

113 / 121

@leggetter

SiriGoogle Now

Microso� CortanaFacebook M

Chat "Virtual Assistants"

114 / 121

@leggetter

115 / 121

@leggetter

Chat has evolved. Chat is now a platform!

116 / 121

@leggetter

Multi-Device Experiences

117 / 121

@leggetter

Ben Foxall - A conceptual future for the multi-device web (FutureJS 2014)

118 / 121

@leggetter

You need Real-Time!

There are lots of options.

Make the choice that's right for you.

I hope this helps!

119 / 121

@leggetter

Resources

Real-time Tech Guidegithub.com/leggetter/realtime-dotnet-examplesTools, Tips and Techniques for Developing Real-time AppsNexmo

120 / 121

@leggetter

Real-time Web Apps & .NET

What are your options?

Questions?

PHIL @LEGGETTERHead of Developer Relations

121 / 121

@leggetter