feat: implement WebSocket synchronization and HTTP recovery
This commit is contained in:
+82
-1
@@ -19,6 +19,7 @@
|
||||
#include "freertos/portmacro.h"
|
||||
#include "http_api.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "sync_service.h"
|
||||
|
||||
#if __has_include("wifi_config.h")
|
||||
#include "wifi_config.h"
|
||||
@@ -40,7 +41,9 @@ static bool s_littlefs_mounted;
|
||||
static httpd_handle_t s_server;
|
||||
static application_t s_application;
|
||||
static http_api_t s_api;
|
||||
static sync_service_t s_sync;
|
||||
static uint16_t s_rejected_input;
|
||||
static esp_timer_handle_t s_sync_timer;
|
||||
#if WIFI_CONFIG_AVAILABLE
|
||||
static esp_timer_handle_t s_reconnect_timer;
|
||||
#endif
|
||||
@@ -90,6 +93,36 @@ static esp_err_t send_api_response(httpd_req_t *request, const http_api_response
|
||||
return httpd_resp_send(request, response->body, response->body_length);
|
||||
}
|
||||
|
||||
static bool websocket_send(void *unused, int client_id, const char *payload, size_t length) {
|
||||
(void)unused;
|
||||
httpd_ws_frame_t frame = {.final = true, .type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)payload, .len = length};
|
||||
const esp_err_t result = httpd_ws_send_frame_async(s_server, client_id, &frame);
|
||||
if (result != ESP_OK) httpd_sess_trigger_close(s_server, client_id);
|
||||
return result == ESP_OK;
|
||||
}
|
||||
|
||||
static void sync_broadcast_work(void *unused) {
|
||||
(void)unused;
|
||||
sync_service_broadcast(&s_sync, websocket_send, NULL);
|
||||
}
|
||||
|
||||
static void queue_state_broadcast(void) {
|
||||
if (s_server != NULL && httpd_queue_work(s_server, sync_broadcast_work, NULL) != ESP_OK) ++s_rejected_input;
|
||||
}
|
||||
|
||||
static void sync_expire_work(void *unused) {
|
||||
(void)unused;
|
||||
int closed[kSessionCapacity] = {0};
|
||||
size_t closed_count = 0U;
|
||||
sync_service_expire(&s_sync, (uint64_t)(esp_timer_get_time() / 1000U), closed, &closed_count);
|
||||
for (size_t index = 0; index < closed_count; ++index) httpd_sess_trigger_close(s_server, closed[index]);
|
||||
}
|
||||
|
||||
static void sync_timer_callback(void *unused) {
|
||||
(void)unused;
|
||||
if (s_server != NULL) httpd_queue_work(s_server, sync_expire_work, NULL);
|
||||
}
|
||||
|
||||
static bool request_has_json_content_type(httpd_req_t *request) {
|
||||
const size_t length = httpd_req_get_hdr_value_len(request, "Content-Type");
|
||||
if (length == 0U || length >= 64U) return false;
|
||||
@@ -139,10 +172,53 @@ static esp_err_t api_handler(httpd_req_t *request) {
|
||||
const http_api_request_t api_request = {.method = request->method == HTTP_GET ? HTTP_API_GET : HTTP_API_POST, .route = route,
|
||||
.content_type_json = request_has_json_content_type(request), .body = body, .body_length = body_length,
|
||||
.session_token = token[0] == '\0' ? NULL : token, .target_too_large = target_too_large};
|
||||
const uint32_t version_before = s_application.lifecycle.game.state.version;
|
||||
if (!http_api_handle(&s_api, &api_request, &response)) return ESP_FAIL;
|
||||
if (s_application.lifecycle.game.state.version != version_before) queue_state_broadcast();
|
||||
return send_api_response(request, &response);
|
||||
}
|
||||
|
||||
static esp_err_t websocket_handler(httpd_req_t *request) {
|
||||
const int client_id = httpd_req_to_sockfd(request);
|
||||
if (request->method == HTTP_GET) {
|
||||
if (sync_service_open(&s_sync, client_id, (uint64_t)(esp_timer_get_time() / 1000U))) return ESP_OK;
|
||||
httpd_sess_trigger_close(s_server, client_id);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
httpd_ws_frame_t frame = {0};
|
||||
if (httpd_ws_recv_frame(request, &frame, 0U) != ESP_OK || frame.type != HTTPD_WS_TYPE_TEXT ||
|
||||
frame.len > kWebSocketFrameCapacity) {
|
||||
++s_rejected_input;
|
||||
sync_service_close(&s_sync, client_id);
|
||||
httpd_sess_trigger_close(s_server, client_id);
|
||||
return ESP_OK;
|
||||
}
|
||||
char input[kWebSocketFrameCapacity + 1U] = {0};
|
||||
frame.payload = (uint8_t *)input;
|
||||
if (httpd_ws_recv_frame(request, &frame, sizeof(input) - 1U) != ESP_OK) {
|
||||
sync_service_close(&s_sync, client_id);
|
||||
httpd_sess_trigger_close(s_server, client_id);
|
||||
return ESP_OK;
|
||||
}
|
||||
char output[kStateMessageCapacity] = {0};
|
||||
size_t output_length = 0U;
|
||||
bool state_changed = false;
|
||||
bool close_client = false;
|
||||
if (!sync_service_receive(&s_sync, client_id, input, frame.len, (uint64_t)(esp_timer_get_time() / 1000U),
|
||||
output, &output_length, &state_changed, &close_client)) return ESP_FAIL;
|
||||
if (output_length > 0U) {
|
||||
httpd_ws_frame_t response = {.final = true, .type = HTTPD_WS_TYPE_TEXT,
|
||||
.payload = (uint8_t *)output, .len = output_length};
|
||||
if (httpd_ws_send_frame(request, &response) != ESP_OK) close_client = true;
|
||||
}
|
||||
if (state_changed) queue_state_broadcast();
|
||||
if (close_client) {
|
||||
sync_service_close(&s_sync, client_id);
|
||||
httpd_sess_trigger_close(s_server, client_id);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#if WIFI_CONFIG_AVAILABLE
|
||||
static void reconnect_timer_callback(void *unused) {
|
||||
(void)unused;
|
||||
@@ -252,7 +328,7 @@ static esp_err_t static_file_handler(httpd_req_t *request) {
|
||||
|
||||
static esp_err_t start_http_server(void) {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.max_uri_handlers = 14U; config.max_open_sockets = kHttpMaxOpenSockets; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true;
|
||||
config.max_uri_handlers = 15U; config.max_open_sockets = kHttpMaxOpenSockets; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true;
|
||||
ESP_RETURN_ON_ERROR(httpd_start(&s_server, &config), kLogTag, "http server start failed");
|
||||
const httpd_uri_t routes[] = {
|
||||
{.uri = "/", .method = HTTP_GET, .handler = root_handler},
|
||||
@@ -266,6 +342,7 @@ static esp_err_t start_http_server(void) {
|
||||
{.uri = "/api/game/rematch", .method = HTTP_POST, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_REMATCH},
|
||||
{.uri = "/api/game/abort", .method = HTTP_POST, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_ABORT},
|
||||
{.uri = "/api/state", .method = HTTP_GET, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_STATE},
|
||||
{.uri = "/ws", .method = HTTP_GET, .handler = websocket_handler, .is_websocket = true},
|
||||
{.uri = "/*", .method = HTTP_GET, .handler = static_file_handler},
|
||||
};
|
||||
for (size_t index = 0; index < sizeof(routes) / sizeof(routes[0]); ++index) ESP_RETURN_ON_ERROR(httpd_register_uri_handler(s_server, &routes[index]), kLogTag, "route registration failed");
|
||||
@@ -284,8 +361,12 @@ void app_main(void) {
|
||||
ESP_ERROR_CHECK(result); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
application_init(&s_application, (random_source_t){.next_u32 = platform_random, .context = NULL});
|
||||
http_api_init(&s_api, &s_application);
|
||||
sync_service_init(&s_sync, &s_application);
|
||||
mount_littlefs();
|
||||
ESP_ERROR_CHECK(start_http_server());
|
||||
const esp_timer_create_args_t sync_timer_args = {.callback = sync_timer_callback, .name = "sync_expire"};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&sync_timer_args, &s_sync_timer));
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(s_sync_timer, 1000000U));
|
||||
result = start_wifi();
|
||||
if (result != ESP_OK) ESP_LOGW(kLogTag, "wifi unavailable: %s", esp_err_to_name(result));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user