@@ -19,7 +19,7 @@ namespace JT1078.Gateway.TestNormalHosting.Services | |||||
{ | { | ||||
PackageConsumer.OnMessage((Message) => | PackageConsumer.OnMessage((Message) => | ||||
{ | { | ||||
}); | }); | ||||
return Task.CompletedTask; | return Task.CompletedTask; | ||||
} | } | ||||
@@ -11,9 +11,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{04C0F72A | |||||
EndProject | EndProject | ||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JT1078.Gateway.Abstractions", "JT1078.Gateway.Abstractions\JT1078.Gateway.Abstractions.csproj", "{EE50F2A6-5F28-4640-BC20-44A8BED8F311}" | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JT1078.Gateway.Abstractions", "JT1078.Gateway.Abstractions\JT1078.Gateway.Abstractions.csproj", "{EE50F2A6-5F28-4640-BC20-44A8BED8F311}" | ||||
EndProject | EndProject | ||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JT1078.Gateway.InMemoryMQ", "JT1078.Gateway.InMemoryMQ\JT1078.Gateway.InMemoryMQ.csproj", "{C7554790-0B3B-490F-B2F1-436C1FD0B4DD}" | |||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JT1078.Gateway.InMemoryMQ", "JT1078.Gateway.InMemoryMQ\JT1078.Gateway.InMemoryMQ.csproj", "{C7554790-0B3B-490F-B2F1-436C1FD0B4DD}" | |||||
EndProject | EndProject | ||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JT1078.Gateway.TestNormalHosting", "JT1078.Gateway.Tests\JT1078.Gateway.TestNormalHosting\JT1078.Gateway.TestNormalHosting.csproj", "{6E2DAA64-E2A1-4459-BE61-E807B4EF2CCE}" | |||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JT1078.Gateway.TestNormalHosting", "JT1078.Gateway.Tests\JT1078.Gateway.TestNormalHosting\JT1078.Gateway.TestNormalHosting.csproj", "{6E2DAA64-E2A1-4459-BE61-E807B4EF2CCE}" | |||||
EndProject | EndProject | ||||
Global | Global | ||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
@@ -13,15 +13,6 @@ namespace JT1078.Gateway.Configurations | |||||
public int SoBacklog { get; set; } = 8192; | public int SoBacklog { get; set; } = 8192; | ||||
public int MiniNumBufferSize { get; set; } = 8096; | public int MiniNumBufferSize { get; set; } = 8096; | ||||
/// <summary> | /// <summary> | ||||
/// http写超时 | |||||
/// </summary> | |||||
public int HttpWriterIdleTimeSeconds { get; set; } = 60; | |||||
/// <summary> | |||||
/// http写超时 | |||||
/// 默认30s检查一次 | |||||
/// </summary> | |||||
public int HttpWriterTimeoutCheckTimeSeconds { get; set; } = 30; | |||||
/// <summary> | |||||
/// Tcp读超时 | /// Tcp读超时 | ||||
/// 默认10分钟检查一次 | /// 默认10分钟检查一次 | ||||
/// </summary> | /// </summary> | ||||
@@ -51,6 +51,19 @@ namespace JT1078.Gateway.Extensions | |||||
context.Response.Close(); | context.Response.Close(); | ||||
} | } | ||||
public static async ValueTask HttpSendFirstChunked(this JT1078HttpContext context, ReadOnlyMemory<byte> buffer) | |||||
{ | |||||
context.Context.Response.StatusCode = (int)HttpStatusCode.OK; | |||||
context.Context.Response.SendChunked = true; | |||||
await context.Context.Response.OutputStream.WriteAsync(buffer); | |||||
} | |||||
public static async ValueTask HttpSendChunked(this JT1078HttpContext context, ReadOnlyMemory<byte> buffer) | |||||
{ | |||||
context.Context.Response.StatusCode = (int)HttpStatusCode.OK; | |||||
await context.Context.Response.OutputStream.WriteAsync(buffer); | |||||
} | |||||
public static async ValueTask HttpClose(this JT1078HttpContext context) | public static async ValueTask HttpClose(this JT1078HttpContext context) | ||||
{ | { | ||||
byte[] b = Encoding.UTF8.GetBytes("close"); | byte[] b = Encoding.UTF8.GetBytes("close"); | ||||
@@ -76,5 +89,12 @@ namespace JT1078.Gateway.Extensions | |||||
{ | { | ||||
await context.WebSocketContext.WebSocket.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None); | await context.WebSocketContext.WebSocket.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None); | ||||
} | } | ||||
private static ReadOnlyMemory<byte> Hello = Encoding.UTF8.GetBytes("hello,jt1078"); | |||||
public static async ValueTask WebSocketSendHelloAsync(this JT1078HttpContext context) | |||||
{ | |||||
await context.WebSocketContext.WebSocket.SendAsync(Hello, WebSocketMessageType.Text, true, CancellationToken.None); | |||||
} | |||||
} | } | ||||
} | } |
@@ -54,7 +54,6 @@ namespace JT1078.Gateway | |||||
{ | { | ||||
builder.JT1078Builder.Services.AddSingleton<IJT1078Authorization, JT1078AuthorizationDefault>(); | builder.JT1078Builder.Services.AddSingleton<IJT1078Authorization, JT1078AuthorizationDefault>(); | ||||
builder.JT1078Builder.Services.AddSingleton<JT1078HttpSessionManager>(); | builder.JT1078Builder.Services.AddSingleton<JT1078HttpSessionManager>(); | ||||
builder.JT1078Builder.Services.AddHostedService<JT1078HttpWriterTimeoutJob>(); | |||||
builder.JT1078Builder.Services.AddHostedService<JT1078HttpServer>(); | builder.JT1078Builder.Services.AddHostedService<JT1078HttpServer>(); | ||||
return builder; | return builder; | ||||
} | } | ||||
@@ -109,23 +109,24 @@ namespace JT1078.Gateway | |||||
int.TryParse(channel, out int channelNo); | int.TryParse(channel, out int channelNo); | ||||
if (context.Request.IsWebSocketRequest) | if (context.Request.IsWebSocketRequest) | ||||
{ | { | ||||
HttpListenerWebSocketContext wsContext = await context.AcceptWebSocketAsync(null); | |||||
HttpListenerWebSocketContext wsContext = await context.AcceptWebSocketAsync(null, keepAliveInterval:TimeSpan.FromSeconds(5)); | |||||
var jT1078HttpContext = new JT1078HttpContext(context, wsContext,principal); | var jT1078HttpContext = new JT1078HttpContext(context, wsContext,principal); | ||||
jT1078HttpContext.Sim = sim; | jT1078HttpContext.Sim = sim; | ||||
jT1078HttpContext.ChannelNo = channelNo; | jT1078HttpContext.ChannelNo = channelNo; | ||||
SessionManager.TryAdd(jT1078HttpContext); | SessionManager.TryAdd(jT1078HttpContext); | ||||
await wsContext.WebSocket.SendAsync(Encoding.UTF8.GetBytes("hello,jt1078"), WebSocketMessageType.Text, true, CancellationToken.None); | |||||
await Task.Factory.StartNew(async(state) => | |||||
await jT1078HttpContext.WebSocketSendHelloAsync(); | |||||
await Task.Factory.StartNew(async(state) => | |||||
{ | { | ||||
//https://www.bejson.com/httputil/websocket/ | //https://www.bejson.com/httputil/websocket/ | ||||
//ws://localhost:15555?token=22&sim=1221&channel=1 | //ws://localhost:15555?token=22&sim=1221&channel=1 | ||||
var websocketContext = state as JT1078HttpContext; | var websocketContext = state as JT1078HttpContext; | ||||
while(websocketContext.WebSocketContext.WebSocket.State == WebSocketState.Open || | |||||
websocketContext.WebSocketContext.WebSocket.State == WebSocketState.Connecting) | |||||
while (websocketContext.WebSocketContext.WebSocket.State == WebSocketState.Open || | |||||
websocketContext.WebSocketContext.WebSocket.State == WebSocketState.Connecting) | |||||
{ | { | ||||
var buffer = ArrayPool<byte>.Shared.Rent(256); | var buffer = ArrayPool<byte>.Shared.Rent(256); | ||||
try | try | ||||
{ | { | ||||
//客户端主动断开需要有个线程去接收通知,不然会客户端会卡死直到超时 | |||||
WebSocketReceiveResult receiveResult = await websocketContext.WebSocketContext.WebSocket.ReceiveAsync(buffer, CancellationToken.None); | WebSocketReceiveResult receiveResult = await websocketContext.WebSocketContext.WebSocket.ReceiveAsync(buffer, CancellationToken.None); | ||||
if (receiveResult.EndOfMessage) | if (receiveResult.EndOfMessage) | ||||
{ | { | ||||
@@ -145,9 +146,9 @@ namespace JT1078.Gateway | |||||
ArrayPool<byte>.Shared.Return(buffer); | ArrayPool<byte>.Shared.Return(buffer); | ||||
} | } | ||||
} | } | ||||
if (Logger.IsEnabled(LogLevel.Trace)) | |||||
if (Logger.IsEnabled(LogLevel.Information)) | |||||
{ | { | ||||
Logger.LogTrace($"[ws close]:{websocketContext}"); | |||||
Logger.LogInformation($"[ws close]:{websocketContext.SessionId}-{websocketContext.Sim}-{websocketContext.ChannelNo}-{websocketContext.StartTime:yyyyMMddhhmmss}"); | |||||
} | } | ||||
SessionManager.TryRemove(websocketContext.SessionId); | SessionManager.TryRemove(websocketContext.SessionId); | ||||
}, jT1078HttpContext); | }, jT1078HttpContext); | ||||
@@ -1,66 +0,0 @@ | |||||
using JT1078.Gateway.Configurations; | |||||
using JT1078.Gateway.Sessions; | |||||
using Microsoft.Extensions.Hosting; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
namespace JT1078.Gateway.Jobs | |||||
{ | |||||
internal class JT1078HttpWriterTimeoutJob : BackgroundService | |||||
{ | |||||
private readonly ILogger Logger; | |||||
private readonly JT1078HttpSessionManager SessionManager; | |||||
private readonly IOptionsMonitor<JT1078Configuration> Configuration; | |||||
public JT1078HttpWriterTimeoutJob( | |||||
IOptionsMonitor<JT1078Configuration> jT1078ConfigurationAccessor, | |||||
ILoggerFactory loggerFactory, | |||||
JT1078HttpSessionManager jT1078SessionManager | |||||
) | |||||
{ | |||||
SessionManager = jT1078SessionManager; | |||||
Logger = loggerFactory.CreateLogger<JT1078TcpReceiveTimeoutJob>(); | |||||
Configuration = jT1078ConfigurationAccessor; | |||||
} | |||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | |||||
{ | |||||
while (!stoppingToken.IsCancellationRequested) | |||||
{ | |||||
try | |||||
{ | |||||
foreach (var item in SessionManager.GetAll()) | |||||
{ | |||||
//过滤掉websocket的方式,无论是客户端主动断开还是关闭浏览器都会有断开通知的情况,所以就只需要判断http的写超时 | |||||
if (!item.IsWebSocket) | |||||
{ | |||||
if (item.ActiveTime.AddSeconds(Configuration.CurrentValue.HttpWriterIdleTimeSeconds) < DateTime.Now) | |||||
{ | |||||
SessionManager.TryRemove(item.SessionId); | |||||
} | |||||
} | |||||
} | |||||
if (Logger.IsEnabled(LogLevel.Information)) | |||||
{ | |||||
Logger.LogInformation($"[Http Check Writer Timeout]"); | |||||
Logger.LogInformation($"[Http Session Online Count]:{SessionManager.SessionCount}"); | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
Logger.LogError(ex, $"[Http Writer Timeout]"); | |||||
} | |||||
finally | |||||
{ | |||||
await Task.Delay(TimeSpan.FromSeconds(Configuration.CurrentValue.HttpWriterTimeoutCheckTimeSeconds), stoppingToken); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -22,24 +22,24 @@ namespace JT1078.Gateway.Metadata | |||||
return Context.Request.IsWebSocketRequest; | return Context.Request.IsWebSocketRequest; | ||||
} | } | ||||
} | } | ||||
public DateTime ActiveTime { get; set; } | |||||
public DateTime StartTime { get; set; } | public DateTime StartTime { get; set; } | ||||
public bool SendChunked { get; set; } | |||||
public JT1078HttpContext(HttpListenerContext context, IPrincipal user) | public JT1078HttpContext(HttpListenerContext context, IPrincipal user) | ||||
{ | { | ||||
Context = context; | Context = context; | ||||
User = user; | User = user; | ||||
ActiveTime = DateTime.Now; | |||||
StartTime = DateTime.Now; | StartTime = DateTime.Now; | ||||
SessionId = Guid.NewGuid().ToString("N"); | SessionId = Guid.NewGuid().ToString("N"); | ||||
SendChunked = false; | |||||
} | } | ||||
public JT1078HttpContext(HttpListenerContext context, HttpListenerWebSocketContext webSocketContext, IPrincipal user) | public JT1078HttpContext(HttpListenerContext context, HttpListenerWebSocketContext webSocketContext, IPrincipal user) | ||||
{ | { | ||||
Context = context; | Context = context; | ||||
WebSocketContext = webSocketContext; | WebSocketContext = webSocketContext; | ||||
User = user; | User = user; | ||||
ActiveTime = DateTime.Now; | |||||
StartTime = DateTime.Now; | StartTime = DateTime.Now; | ||||
SessionId = Guid.NewGuid().ToString("N"); | SessionId = Guid.NewGuid().ToString("N"); | ||||
SendChunked = false; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,5 +1,6 @@ | |||||
using JT1078.Gateway.Extensions; | using JT1078.Gateway.Extensions; | ||||
using JT1078.Gateway.Metadata; | using JT1078.Gateway.Metadata; | ||||
using Microsoft.Extensions.Logging; | |||||
using System; | using System; | ||||
using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
@@ -15,10 +16,11 @@ namespace JT1078.Gateway.Sessions | |||||
public class JT1078HttpSessionManager | public class JT1078HttpSessionManager | ||||
{ | { | ||||
public ConcurrentDictionary<string, JT1078HttpContext> Sessions { get; } | public ConcurrentDictionary<string, JT1078HttpContext> Sessions { get; } | ||||
public JT1078HttpSessionManager() | |||||
private ILogger Logger; | |||||
public JT1078HttpSessionManager(ILoggerFactory loggerFactory) | |||||
{ | { | ||||
Sessions = new ConcurrentDictionary<string, JT1078HttpContext>(); | Sessions = new ConcurrentDictionary<string, JT1078HttpContext>(); | ||||
Logger = loggerFactory.CreateLogger<JT1078HttpSessionManager>(); | |||||
} | } | ||||
public bool TryAdd(JT1078HttpContext httpContext) | public bool TryAdd(JT1078HttpContext httpContext) | ||||
@@ -53,16 +55,163 @@ namespace JT1078.Gateway.Sessions | |||||
} | } | ||||
} | } | ||||
public void SendHttpChunk(byte[] data) | |||||
private void remove(string sessionId) | |||||
{ | |||||
if (Sessions.TryRemove(sessionId, out JT1078HttpContext session)) | |||||
{ | |||||
//todo:session close notice | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// 发送音视频数据 | |||||
/// </summary> | |||||
/// <param name="sim"></param> | |||||
/// <param name="channelNo"></param> | |||||
/// <param name="data"></param> | |||||
public void SendAVData(string sim,int channelNo,byte[] data) | |||||
{ | |||||
var contexts = Sessions.Select(s => s.Value).Where(w => w.Sim == sim && w.ChannelNo == channelNo).ToList(); | |||||
ParallelLoopResult parallelLoopResult= Parallel.ForEach(contexts, async(context) => | |||||
{ | |||||
if (context.IsWebSocket) | |||||
{ | |||||
try | |||||
{ | |||||
await context.WebSocketSendBinaryAsync(data); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
if (Logger.IsEnabled(LogLevel.Information)) | |||||
{ | |||||
Logger.LogInformation($"[ws close]:{context.SessionId}-{context.Sim}-{context.ChannelNo}-{context.StartTime:yyyyMMddhhmmss}"); | |||||
} | |||||
remove(context.SessionId); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
if (!context.SendChunked) | |||||
{ | |||||
context.SendChunked = true; | |||||
Sessions.TryUpdate(context.SessionId, context, context); | |||||
try | |||||
{ | |||||
await context.HttpSendFirstChunked(data); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
if (Logger.IsEnabled(LogLevel.Information)) | |||||
{ | |||||
Logger.LogInformation($"[http close]:{context.SessionId}-{context.Sim}-{context.ChannelNo}-{context.StartTime:yyyyMMddhhmmss}"); | |||||
} | |||||
remove(context.SessionId); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
try | |||||
{ | |||||
await context.HttpSendChunked(data); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
if (Logger.IsEnabled(LogLevel.Information)) | |||||
{ | |||||
Logger.LogInformation($"[http close]:{context.SessionId}-{context.Sim}-{context.ChannelNo}-{context.StartTime:yyyyMMddhhmmss}"); | |||||
} | |||||
remove(context.SessionId); | |||||
} | |||||
} | |||||
} | |||||
}); | |||||
if (parallelLoopResult.IsCompleted) | |||||
{ | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// 发送音视频数据到websocket | |||||
/// </summary> | |||||
/// <param name="sim"></param> | |||||
/// <param name="channelNo"></param> | |||||
/// <param name="data"></param> | |||||
public void SendAVData2WebSocket(string sim, int channelNo, byte[] data) | |||||
{ | |||||
var contexts = Sessions.Select(s => s.Value).Where(w => w.Sim == sim && w.ChannelNo == channelNo && w.IsWebSocket).ToList(); | |||||
ParallelLoopResult parallelLoopResult = Parallel.ForEach(contexts, async (context) => | |||||
{ | |||||
if (context.IsWebSocket) | |||||
{ | |||||
try | |||||
{ | |||||
await context.WebSocketSendBinaryAsync(data); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
if (Logger.IsEnabled(LogLevel.Information)) | |||||
{ | |||||
Logger.LogInformation($"[ws close]:{context.SessionId}-{context.Sim}-{context.ChannelNo}-{context.StartTime:yyyyMMddhhmmss}"); | |||||
} | |||||
remove(context.SessionId); | |||||
} | |||||
} | |||||
}); | |||||
if (parallelLoopResult.IsCompleted) | |||||
{ | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// 发送音视频数据到Http Chunked中 | |||||
/// </summary> | |||||
/// <param name="sim"></param> | |||||
/// <param name="channelNo"></param> | |||||
/// <param name="data"></param> | |||||
public void SendAVData2HttpChunked(string sim, int channelNo, byte[] data) | |||||
{ | { | ||||
//todo:set http chunk | |||||
//todo:session close notice | |||||
//byte[] b = Encoding.UTF8.GetBytes("ack"); | |||||
//context.Response.StatusCode = 200; | |||||
//context.Response.KeepAlive = true; | |||||
//context.Response.ContentLength64 = b.Length; | |||||
//await context.Response.OutputStream.WriteAsync(b, 0, b.Length); | |||||
//context.Response.Close(); | |||||
var contexts = Sessions.Select(s => s.Value).Where(w => w.Sim == sim && w.ChannelNo == channelNo && !w.IsWebSocket).ToList(); | |||||
ParallelLoopResult parallelLoopResult = Parallel.ForEach(contexts, async (context) => | |||||
{ | |||||
if (!context.SendChunked) | |||||
{ | |||||
context.SendChunked = true; | |||||
Sessions.TryUpdate(context.SessionId, context, context); | |||||
try | |||||
{ | |||||
await context.HttpSendFirstChunked(data); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
if (Logger.IsEnabled(LogLevel.Information)) | |||||
{ | |||||
Logger.LogInformation($"[http close]:{context.SessionId}-{context.Sim}-{context.ChannelNo}-{context.StartTime:yyyyMMddhhmmss}"); | |||||
} | |||||
remove(context.SessionId); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
try | |||||
{ | |||||
await context.HttpSendChunked(data); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
if (Logger.IsEnabled(LogLevel.Information)) | |||||
{ | |||||
Logger.LogInformation($"[http close]:{context.SessionId}-{context.Sim}-{context.ChannelNo}-{context.StartTime:yyyyMMddhhmmss}"); | |||||
} | |||||
remove(context.SessionId); | |||||
} | |||||
} | |||||
}); | |||||
if (parallelLoopResult.IsCompleted) | |||||
{ | |||||
} | |||||
} | } | ||||
public int SessionCount | public int SessionCount | ||||