Skip to content

Commit ec96b00

Browse files
committed
[增加] Http 模块新增 HttpResponse、HttpResponseEncoder、HttpKeepAliveFilter、ServerSentEventWriter、HttpExtensions 等 HTTP 服务功能
1 parent 4f86d49 commit ec96b00

8 files changed

Lines changed: 963 additions & 0 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using GameFrameX.SuperSocket.Http;
5+
using GameFrameX.SuperSocket.Server.Abstractions.Session;
6+
using SuperSocket.Connection;
7+
8+
namespace SuperSocket.Http
9+
{
10+
/// <summary>
11+
/// Extension methods for easier HTTP functionality usage.
12+
/// </summary>
13+
public static class HttpExtensions
14+
{
15+
/// <summary>
16+
/// Sends an HTTP response to the session.
17+
/// </summary>
18+
/// <param name="session">The session to send the response to.</param>
19+
/// <param name="response">The HTTP response to send.</param>
20+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
21+
/// <returns>A task that represents the asynchronous operation.</returns>
22+
public static async ValueTask SendHttpResponseAsync(
23+
this IAppSession session,
24+
HttpResponse response,
25+
CancellationToken cancellationToken = default)
26+
{
27+
await session.SendAsync(HttpResponseEncoder.Instance, response, cancellationToken);
28+
}
29+
30+
/// <summary>
31+
/// Sends a simple HTTP response with the specified status and body.
32+
/// </summary>
33+
/// <param name="session">The session to send the response to.</param>
34+
/// <param name="statusCode">The HTTP status code.</param>
35+
/// <param name="body">The response body.</param>
36+
/// <param name="contentType">The content type. Defaults to "text/plain".</param>
37+
/// <param name="keepAlive">Whether to keep the connection alive.</param>
38+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
39+
/// <returns>A task that represents the asynchronous operation.</returns>
40+
public static async ValueTask SendHttpResponseAsync(
41+
this IAppSession session,
42+
int statusCode,
43+
string body = "",
44+
string contentType = "text/plain",
45+
bool keepAlive = true,
46+
CancellationToken cancellationToken = default)
47+
{
48+
var response = new HttpResponse(statusCode);
49+
response.SetContentType(contentType);
50+
response.Body = body;
51+
response.KeepAlive = keepAlive;
52+
53+
await session.SendHttpResponseAsync(response, cancellationToken);
54+
}
55+
56+
/// <summary>
57+
/// Sends a JSON HTTP response.
58+
/// </summary>
59+
/// <param name="session">The session to send the response to.</param>
60+
/// <param name="jsonData">The JSON data to send.</param>
61+
/// <param name="statusCode">The HTTP status code. Defaults to 200.</param>
62+
/// <param name="keepAlive">Whether to keep the connection alive.</param>
63+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
64+
/// <returns>A task that represents the asynchronous operation.</returns>
65+
public static async ValueTask SendJsonResponseAsync(
66+
this IAppSession session,
67+
string jsonData,
68+
int statusCode = 200,
69+
bool keepAlive = true,
70+
CancellationToken cancellationToken = default)
71+
{
72+
await session.SendHttpResponseAsync(statusCode, jsonData, "application/json", keepAlive, cancellationToken);
73+
}
74+
75+
/// <summary>
76+
/// Creates a Server-Sent Events writer for the session.
77+
/// </summary>
78+
/// <param name="session">The session to create the SSE writer for.</param>
79+
/// <param name="options">Optional SSE configuration options.</param>
80+
/// <returns>A new ServerSentEventWriter instance.</returns>
81+
public static ServerSentEventWriter CreateSSEWriter(this IAppSession session, ServerSentEventsOptions options = null)
82+
{
83+
return new ServerSentEventWriter(session.Connection, options);
84+
}
85+
86+
/// <summary>
87+
/// Starts a Server-Sent Events stream for the session.
88+
/// </summary>
89+
/// <param name="session">The session to start SSE for.</param>
90+
/// <param name="options">Optional SSE configuration options.</param>
91+
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
92+
/// <returns>A new ServerSentEventWriter instance that has already sent the initial response.</returns>
93+
public static async ValueTask<ServerSentEventWriter> StartSSEAsync(
94+
this IAppSession session,
95+
ServerSentEventsOptions options = null,
96+
CancellationToken cancellationToken = default)
97+
{
98+
var writer = session.CreateSSEWriter(options);
99+
await writer.SendInitialResponseAsync(cancellationToken);
100+
return writer;
101+
}
102+
103+
/// <summary>
104+
/// Checks if the HTTP request accepts Server-Sent Events.
105+
/// </summary>
106+
/// <param name="request">The HTTP request to check.</param>
107+
/// <returns>True if the request accepts SSE, false otherwise.</returns>
108+
public static bool IsSSERequest(this HttpRequest request)
109+
{
110+
return request.AcceptsEventStream;
111+
}
112+
113+
/// <summary>
114+
/// Checks if the HTTP request wants to keep the connection alive.
115+
/// </summary>
116+
/// <param name="request">The HTTP request to check.</param>
117+
/// <returns>True if keep-alive is requested, false otherwise.</returns>
118+
public static bool IsKeepAliveRequest(this HttpRequest request)
119+
{
120+
return request.KeepAlive;
121+
}
122+
}
123+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using System.Buffers;
3+
using GameFrameX.SuperSocket.Http;
4+
using GameFrameX.SuperSocket.ProtoBase;
5+
using SuperSocket.ProtoBase;
6+
7+
namespace SuperSocket.Http
8+
{
9+
/// <summary>
10+
/// Represents a pipeline filter for parsing HTTP requests with keep-alive support.
11+
/// This filter can handle multiple requests over a single connection.
12+
/// </summary>
13+
public class HttpKeepAliveFilter : IPipelineFilter<HttpRequest>
14+
{
15+
private readonly HttpPipelineFilter _innerFilter;
16+
private bool _connectionClosed = false;
17+
18+
/// <summary>
19+
/// Initializes a new instance of the <see cref="HttpKeepAliveFilter"/> class.
20+
/// </summary>
21+
public HttpKeepAliveFilter()
22+
{
23+
_innerFilter = new HttpPipelineFilter();
24+
}
25+
26+
/// <summary>
27+
/// Gets or sets the package decoder for the HTTP request.
28+
/// </summary>
29+
public IPackageDecoder<HttpRequest> Decoder
30+
{
31+
get => _innerFilter.Decoder;
32+
set => _innerFilter.Decoder = value;
33+
}
34+
35+
/// <summary>
36+
/// Gets or sets the next pipeline filter in the chain.
37+
/// </summary>
38+
public IPipelineFilter<HttpRequest> NextFilter
39+
{
40+
get => _connectionClosed ? null : this;
41+
set { } // Keep-alive filter manages its own chain
42+
}
43+
44+
/// <summary>
45+
/// Gets or sets the context associated with the pipeline filter.
46+
/// </summary>
47+
public object Context
48+
{
49+
get => _innerFilter.Context;
50+
set => _innerFilter.Context = value;
51+
}
52+
53+
/// <summary>
54+
/// Filters the data stream to parse an HTTP request with keep-alive support.
55+
/// </summary>
56+
/// <param name="reader">The sequence reader for the data stream.</param>
57+
/// <returns>The parsed <see cref="HttpRequest"/>, or <c>null</c> if more data is needed.</returns>
58+
public HttpRequest Filter(ref SequenceReader<byte> reader)
59+
{
60+
if (_connectionClosed)
61+
return null;
62+
63+
var request = _innerFilter.Filter(ref reader);
64+
65+
if (request != null)
66+
{
67+
// Reset the inner filter for the next request
68+
_innerFilter.Reset();
69+
70+
// Check if this request should close the connection
71+
if (!request.KeepAlive)
72+
{
73+
_connectionClosed = true;
74+
}
75+
}
76+
77+
return request;
78+
}
79+
80+
/// <summary>
81+
/// Resets the state of the pipeline filter.
82+
/// </summary>
83+
public void Reset()
84+
{
85+
_innerFilter.Reset();
86+
_connectionClosed = false;
87+
}
88+
89+
/// <summary>
90+
/// Marks the connection as closed, preventing further request processing.
91+
/// </summary>
92+
public void CloseConnection()
93+
{
94+
_connectionClosed = true;
95+
}
96+
}
97+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System;
2+
3+
namespace SuperSocket.Http
4+
{
5+
/// <summary>
6+
/// Configuration options for HTTP Keep-Alive functionality.
7+
/// </summary>
8+
public class HttpKeepAliveOptions
9+
{
10+
/// <summary>
11+
/// Gets or sets the timeout for keep-alive connections in seconds.
12+
/// Default is 30 seconds.
13+
/// </summary>
14+
public int KeepAliveTimeoutSeconds { get; set; } = 30;
15+
16+
/// <summary>
17+
/// Gets or sets the maximum number of requests allowed per connection.
18+
/// Default is 1000. Set to 0 for unlimited.
19+
/// </summary>
20+
public int MaxRequestsPerConnection { get; set; } = 1000;
21+
22+
/// <summary>
23+
/// Gets or sets a value indicating whether keep-alive is enabled by default.
24+
/// Default is true for HTTP/1.1.
25+
/// </summary>
26+
public bool EnableKeepAlive { get; set; } = true;
27+
}
28+
29+
/// <summary>
30+
/// Configuration options for Server-Sent Events functionality.
31+
/// </summary>
32+
public class ServerSentEventsOptions
33+
{
34+
/// <summary>
35+
/// Gets or sets the heartbeat interval in seconds.
36+
/// Default is 30 seconds. Set to 0 to disable heartbeat.
37+
/// </summary>
38+
public int HeartbeatIntervalSeconds { get; set; } = 30;
39+
40+
/// <summary>
41+
/// Gets or sets the retry interval suggested to clients in milliseconds.
42+
/// Default is 3000 (3 seconds).
43+
/// </summary>
44+
public int DefaultRetryIntervalMs { get; set; } = 3000;
45+
46+
/// <summary>
47+
/// Gets or sets a value indicating whether CORS headers should be automatically added.
48+
/// Default is true.
49+
/// </summary>
50+
public bool EnableCors { get; set; } = true;
51+
52+
/// <summary>
53+
/// Gets or sets the CORS origin header value.
54+
/// Default is "*" (allow all origins).
55+
/// </summary>
56+
public string CorsOrigin { get; set; } = "*";
57+
58+
/// <summary>
59+
/// Gets or sets the allowed CORS headers.
60+
/// Default is "Cache-Control".
61+
/// </summary>
62+
public string CorsAllowedHeaders { get; set; } = "Cache-Control";
63+
}
64+
}

src/GameFrameX.SuperSocket.Http/HttpRequest.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ public class HttpRequest
3333
/// </summary>
3434
public string Body { get; set; }
3535

36+
/// <summary>
37+
/// Gets a value indicating whether the client requests a keep-alive connection.
38+
/// </summary>
39+
public bool KeepAlive => GetKeepAliveFromHeaders();
40+
41+
/// <summary>
42+
/// Gets a value indicating whether this request supports Server-Sent Events.
43+
/// </summary>
44+
public bool AcceptsEventStream =>
45+
Items?["Accept"]?.Contains("text/event-stream") == true ||
46+
Items?["Accept"]?.Contains("*/*") == true;
47+
48+
/// <summary>
49+
/// Gets the value of the Last-Event-ID header for SSE reconnection.
50+
/// </summary>
51+
public string LastEventId => Items?["Last-Event-ID"];
52+
3653
/// <summary>
3754
/// Initializes a new instance of the <see cref="HttpRequest"/> class with the specified method, path, version, and items.
3855
/// </summary>
@@ -47,5 +64,17 @@ public HttpRequest(string method, string path, string httpVersion, NameValueColl
4764
HttpVersion = httpVersion;
4865
Items = items;
4966
}
67+
68+
private bool GetKeepAliveFromHeaders()
69+
{
70+
var connection = Items?["Connection"];
71+
if (string.IsNullOrEmpty(connection))
72+
{
73+
// HTTP/1.1 defaults to keep-alive, HTTP/1.0 defaults to close
74+
return HttpVersion?.Contains("1.1") == true;
75+
}
76+
77+
return connection.ToLowerInvariant().Contains("keep-alive");
78+
}
5079
}
5180
}

0 commit comments

Comments
 (0)