SSE kind of works - duplicate messages
This commit is contained in:
parent
27ddb7de90
commit
32ed7ca0c4
|
|
@ -45,9 +45,10 @@ namespace WebApplication95
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: don't leak HttpContext to ConnectionManager
|
||||||
public string GetConnectionId(HttpContext context)
|
public string GetConnectionId(HttpContext context)
|
||||||
{
|
{
|
||||||
// REVIEW: Only check the query string for longpolling
|
|
||||||
var id = context.Request.Query["id"];
|
var id = context.Request.Query["id"];
|
||||||
|
|
||||||
if (!StringValues.IsNullOrEmpty(id))
|
if (!StringValues.IsNullOrEmpty(id))
|
||||||
|
|
|
||||||
|
|
@ -41,83 +41,117 @@ namespace WebApplication95
|
||||||
|
|
||||||
public async Task Execute<TEndPoint>(string path, HttpContext context) where TEndPoint : EndPoint
|
public async Task Execute<TEndPoint>(string path, HttpContext context) where TEndPoint : EndPoint
|
||||||
{
|
{
|
||||||
if (context.Request.Path.StartsWithSegments(path + "/send"))
|
if (context.Request.Path.StartsWithSegments(path + "/getid"))
|
||||||
{
|
{
|
||||||
var connectionId = context.Request.Query["id"];
|
await ProcessGetId(context);
|
||||||
|
}
|
||||||
if (StringValues.IsNullOrEmpty(connectionId))
|
else if (context.Request.Path.StartsWithSegments(path + "/send"))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Missing connection id");
|
await ProcessSend(context);
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionState state;
|
|
||||||
if (_manager.TryGetConnection(connectionId, out state))
|
|
||||||
{
|
|
||||||
// Write the message length
|
|
||||||
await context.Request.Body.CopyToAsync(state.Connection.Input);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var endpoint = (EndPoint)context.RequestServices.GetRequiredService<TEndPoint>();
|
|
||||||
|
|
||||||
var connectionId = _manager.GetConnectionId(context);
|
var endpoint = (EndPoint)context.RequestServices.GetRequiredService<TEndPoint>();
|
||||||
|
|
||||||
// Outgoing channels
|
// Outgoing channels
|
||||||
if (context.Request.Path.StartsWithSegments(path + "/sse"))
|
if (context.Request.Path.StartsWithSegments(path + "/sse"))
|
||||||
{
|
{
|
||||||
ConnectionState state;
|
var connectionState = GetOrCreateConnection(context);
|
||||||
_manager.AddConnection(connectionId, out state);
|
var sse = new ServerSentEvents(connectionState);
|
||||||
|
|
||||||
var sse = new ServerSentEvents(state);
|
var ignore = endpoint.OnConnected(connectionState.Connection);
|
||||||
|
|
||||||
var ignore = endpoint.OnConnected(state.Connection);
|
connectionState.Connection.TransportType = TransportType.ServerSentEvents;
|
||||||
|
|
||||||
state.Connection.TransportType = TransportType.ServerSentEvents;
|
|
||||||
|
|
||||||
await sse.ProcessRequest(context);
|
await sse.ProcessRequest(context);
|
||||||
|
|
||||||
state.Connection.Complete();
|
connectionState.Connection.Complete();
|
||||||
|
|
||||||
_manager.RemoveConnection(connectionId);
|
_manager.RemoveConnection(connectionState.Connection.ConnectionId);
|
||||||
}
|
}
|
||||||
else if (context.Request.Path.StartsWithSegments(path + "/ws"))
|
else if (context.Request.Path.StartsWithSegments(path + "/ws"))
|
||||||
{
|
{
|
||||||
ConnectionState state;
|
var connectionState = GetOrCreateConnection(context);
|
||||||
_manager.AddConnection(connectionId, out state);
|
var ws = new WebSockets(connectionState);
|
||||||
|
|
||||||
var ws = new WebSockets(state);
|
var ignore = endpoint.OnConnected(connectionState.Connection);
|
||||||
|
|
||||||
var ignore = endpoint.OnConnected(state.Connection);
|
connectionState.Connection.TransportType = TransportType.WebSockets;
|
||||||
|
|
||||||
state.Connection.TransportType = TransportType.WebSockets;
|
|
||||||
|
|
||||||
await ws.ProcessRequest(context);
|
await ws.ProcessRequest(context);
|
||||||
|
|
||||||
state.Connection.Complete();
|
connectionState.Connection.Complete();
|
||||||
|
|
||||||
_manager.RemoveConnection(connectionId);
|
_manager.RemoveConnection(connectionState.Connection.ConnectionId);
|
||||||
}
|
}
|
||||||
else if (context.Request.Path.StartsWithSegments(path + "/poll"))
|
else if (context.Request.Path.StartsWithSegments(path + "/poll"))
|
||||||
{
|
{
|
||||||
ConnectionState state;
|
var connectionId = context.Request.Query["id"];
|
||||||
|
ConnectionState connectionState;
|
||||||
bool newConnection = false;
|
bool newConnection = false;
|
||||||
if (_manager.AddConnection(connectionId, out state))
|
if (_manager.AddConnection(connectionId, out connectionState))
|
||||||
{
|
{
|
||||||
newConnection = true;
|
newConnection = true;
|
||||||
var ignore = endpoint.OnConnected(state.Connection);
|
var ignore = endpoint.OnConnected(connectionState.Connection);
|
||||||
|
|
||||||
state.Connection.TransportType = TransportType.LongPolling;
|
connectionState.Connection.TransportType = TransportType.LongPolling;
|
||||||
}
|
}
|
||||||
|
|
||||||
var longPolling = new LongPolling(state);
|
var longPolling = new LongPolling(connectionState);
|
||||||
|
|
||||||
await longPolling.ProcessRequest(newConnection, context);
|
await longPolling.ProcessRequest(newConnection, context);
|
||||||
|
|
||||||
_manager.MarkConnectionDead(connectionId);
|
_manager.MarkConnectionDead(connectionState.Connection.ConnectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ProcessGetId(HttpContext context)
|
||||||
|
{
|
||||||
|
var connectionId = _manager.GetConnectionId(context);
|
||||||
|
ConnectionState state;
|
||||||
|
_manager.AddConnection(connectionId, out state);
|
||||||
|
context.Response.Headers["X-SignalR-ConnectionId"] = connectionId;
|
||||||
|
await context.Response.WriteAsync($"{{ \"connectionId\": \"{connectionId}\" }}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessSend(HttpContext context)
|
||||||
|
{
|
||||||
|
var connectionId = context.Request.Query["id"];
|
||||||
|
if (StringValues.IsNullOrEmpty(connectionId))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Missing connection id");
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionState state;
|
||||||
|
if (_manager.TryGetConnection(connectionId, out state))
|
||||||
|
{
|
||||||
|
// Write the message length
|
||||||
|
await context.Request.Body.CopyToAsync(state.Connection.Input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConnectionState GetOrCreateConnection(HttpContext context)
|
||||||
|
{
|
||||||
|
var connectionId = context.Request.Query["id"];
|
||||||
|
ConnectionState connectionState;
|
||||||
|
|
||||||
|
if (StringValues.IsNullOrEmpty(connectionId))
|
||||||
|
{
|
||||||
|
connectionId = _manager.GetConnectionId(context);
|
||||||
|
_manager.AddConnection(connectionId, out connectionState);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!_manager.TryGetConnection(connectionId, out connectionState))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unknown connection id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectionState;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,33 +104,9 @@ namespace WebApplication95
|
||||||
await Post(async state =>
|
await Post(async state =>
|
||||||
{
|
{
|
||||||
var data = ((ArraySegment<byte>)state);
|
var data = ((ArraySegment<byte>)state);
|
||||||
// + 100 = laziness
|
_context.Response.Headers["X-SignalR-ConnectionId"] = _state.Connection.ConnectionId;
|
||||||
var buffer = new byte[data.Count + _state.Connection.ConnectionId.Length + 100];
|
_context.Response.ContentLength = data.Count;
|
||||||
var at = 0;
|
await _context.Response.Body.WriteAsync(data.Array, 0, data.Count);
|
||||||
buffer[at++] = (byte)'{';
|
|
||||||
buffer[at++] = (byte)'"';
|
|
||||||
buffer[at++] = (byte)'c';
|
|
||||||
buffer[at++] = (byte)'"';
|
|
||||||
buffer[at++] = (byte)':';
|
|
||||||
buffer[at++] = (byte)'"';
|
|
||||||
int count = Encoding.UTF8.GetBytes(_state.Connection.ConnectionId, 0, _state.Connection.ConnectionId.Length, buffer, at);
|
|
||||||
at += count;
|
|
||||||
buffer[at++] = (byte)'"';
|
|
||||||
if (data.Array != null)
|
|
||||||
{
|
|
||||||
buffer[at++] = (byte)',';
|
|
||||||
buffer[at++] = (byte)'"';
|
|
||||||
buffer[at++] = (byte)'d';
|
|
||||||
buffer[at++] = (byte)'"';
|
|
||||||
buffer[at++] = (byte)':';
|
|
||||||
//buffer[at++] = (byte)'"';
|
|
||||||
Buffer.BlockCopy(data.Array, data.Offset, buffer, at, data.Count);
|
|
||||||
}
|
|
||||||
at += data.Count;
|
|
||||||
//buffer[at++] = (byte)'"';
|
|
||||||
buffer[at++] = (byte)'}';
|
|
||||||
_context.Response.ContentLength = at;
|
|
||||||
await _context.Response.Body.WriteAsync(buffer, 0, at);
|
|
||||||
},
|
},
|
||||||
value);
|
value);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,14 +40,12 @@ namespace WebApplication95
|
||||||
public async Task ProcessRequest(HttpContext context)
|
public async Task ProcessRequest(HttpContext context)
|
||||||
{
|
{
|
||||||
context.Response.ContentType = "text/event-stream";
|
context.Response.ContentType = "text/event-stream";
|
||||||
|
context.Response.Headers["Cache-Control"] = "no-cache";
|
||||||
|
|
||||||
// End the connection if the client goes away
|
// End the connection if the client goes away
|
||||||
context.RequestAborted.Register(state => OnConnectionAborted(state), this);
|
context.RequestAborted.Register(state => OnConnectionAborted(state), this);
|
||||||
|
|
||||||
_context = context;
|
_context = context;
|
||||||
|
|
||||||
await _context.Response.WriteAsync($"data: {_state.Connection.ConnectionId}\n\n");
|
|
||||||
|
|
||||||
// Set the initial TCS when everything is setup
|
// Set the initial TCS when everything is setup
|
||||||
_initTcs.TrySetResult(null);
|
_initTcs.TrySetResult(null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ namespace WebApplication95
|
||||||
else if (result.MessageType == WebSocketMessageType.Close)
|
else if (result.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||||
|
// TODO: needs to remove itself from connection mamanger?
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,49 +4,77 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title></title>
|
<title></title>
|
||||||
<script>
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
var connectionId;
|
function send(connectionId, data) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
function send() {
|
var url = '/chat/send?id=' + connectionId;
|
||||||
var body = document.getElementById('data').value;
|
xhr.open("POST", url, true);
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/json');
|
||||||
var xhr = new XMLHttpRequest();
|
xhr.onreadystatechange = function () {
|
||||||
var url = '/chat/send?id=' + connectionId;
|
if (xhr.readyState == 4 && xhr.status == 200) {
|
||||||
xhr.open("POST", url, true);
|
}
|
||||||
xhr.setRequestHeader('Content-type', 'application/json');
|
|
||||||
xhr.onreadystatechange = function () {
|
|
||||||
if (xhr.readyState == 4 && xhr.status == 200) {
|
|
||||||
}
|
}
|
||||||
|
var data = JSON.stringify(data);
|
||||||
|
xhr.send(data);
|
||||||
}
|
}
|
||||||
var data = JSON.stringify(body);
|
|
||||||
xhr.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
var source = new EventSource('/chat/sse');
|
function xhr(method, url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
xhr.open(method, url);
|
||||||
|
xhr.send();
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
resolve(xhr.response);
|
||||||
|
} else {
|
||||||
|
reject({
|
||||||
|
status: xhr.status,
|
||||||
|
statusText: xhr.statusText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
source.onopen = function () {
|
xhr.onerror = () => {
|
||||||
console.log('Opened!');
|
reject({
|
||||||
};
|
status: xhr.status,
|
||||||
|
statusText: xhr.statusText
|
||||||
source.onerror = function (err) {
|
});
|
||||||
console.log('Error: ' + err.type);
|
};
|
||||||
};
|
});
|
||||||
|
|
||||||
source.onmessage = function (data) {
|
|
||||||
if (!connectionId) {
|
|
||||||
connectionId = data.data;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
var child = document.createElement('li');
|
|
||||||
child.innerText = data.data;
|
xhr('GET', '/chat/getid').then(getidPayload => {
|
||||||
document.getElementById('messages').appendChild(child);
|
let connectionId = JSON.parse(getidPayload).connectionId;
|
||||||
};
|
|
||||||
|
let source = new EventSource(`/chat/sse?id=${connectionId}`);
|
||||||
|
|
||||||
|
source.onopen = function () {
|
||||||
|
console.log('Opened!');
|
||||||
|
};
|
||||||
|
|
||||||
|
source.onerror = function (err) {
|
||||||
|
console.log('Error: ' + err.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
source.onmessage = function (data) {
|
||||||
|
var child = document.createElement('li');
|
||||||
|
child.innerText = data.data;
|
||||||
|
document.getElementById('messages').appendChild(child);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('sendmessage').addEventListener('click', () => {
|
||||||
|
let data = document.getElementById('data').value;
|
||||||
|
send(connectionId, data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Server Sent Events</h1>
|
<h1>Server Sent Events</h1>
|
||||||
<input type="text" id="data" />
|
<input type="text" id="data" />
|
||||||
<input type="button" value="Send" onclick="send()" />
|
<input type="button" id="sendmessage" value="Send" />
|
||||||
|
|
||||||
<ul id="messages">
|
<ul id="messages">
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue