diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7db5d03..abda04a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,8 @@ "Bash(find /c/Users/64187/Desktop/Workspace/OmniSocketGo -type f -name *.go)", "Bash(git status:*)", "Bash(git fetch:*)", - "Bash(git pull:*)" + "Bash(git pull:*)", + "Bash(wc:*)" ] } } diff --git a/c/Makefile b/c/Makefile new file mode 100644 index 0000000..3968094 --- /dev/null +++ b/c/Makefile @@ -0,0 +1,67 @@ +CC ?= gcc +CFLAGS ?= -std=c11 -Wall -Wextra -O2 -pthread -D_GNU_SOURCE +CPPFLAGS ?= -Iinclude -Ithird_party/cjson -Ithird_party/kcp +LDFLAGS ?= -pthread + +BIN_DIR := bin +SRC_DIR := src +CMD_DIR := cmd + +COMMON_SRCS := \ + $(SRC_DIR)/omni_common.c \ + $(SRC_DIR)/protocol.c \ + $(SRC_DIR)/latencylog.c \ + $(SRC_DIR)/tx_timestamp_debug.c \ + $(SRC_DIR)/kcp_packet_debug.c \ + $(SRC_DIR)/kcp_session_stats.c \ + $(SRC_DIR)/linux_timestamping.c \ + $(SRC_DIR)/interactive.c \ + $(SRC_DIR)/transport_udp.c \ + $(SRC_DIR)/transport_kcp.c \ + $(SRC_DIR)/server_udp_relay.c \ + $(SRC_DIR)/server_udp_hub.c \ + $(SRC_DIR)/server_kcp_hub.c \ + $(SRC_DIR)/peer_udp_client.c \ + $(SRC_DIR)/peer_kcp_client.c \ + third_party/cjson/cJSON.c \ + third_party/kcp/ikcp.c + +TARGETS := \ + $(BIN_DIR)/udpserver \ + $(BIN_DIR)/udppeer \ + $(BIN_DIR)/udpping \ + $(BIN_DIR)/udprelay \ + $(BIN_DIR)/kcpserver \ + $(BIN_DIR)/kcppeer \ + $(BIN_DIR)/kcpping + +all: $(TARGETS) + +$(BIN_DIR): + mkdir -p $(BIN_DIR) + +$(BIN_DIR)/udpserver: $(CMD_DIR)/udpserver.c $(COMMON_SRCS) | $(BIN_DIR) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDFLAGS) + +$(BIN_DIR)/udppeer: $(CMD_DIR)/udppeer.c $(COMMON_SRCS) | $(BIN_DIR) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDFLAGS) + +$(BIN_DIR)/udpping: $(CMD_DIR)/udpping.c $(COMMON_SRCS) | $(BIN_DIR) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDFLAGS) + +$(BIN_DIR)/udprelay: $(CMD_DIR)/udprelay.c $(COMMON_SRCS) | $(BIN_DIR) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDFLAGS) + +$(BIN_DIR)/kcpserver: $(CMD_DIR)/kcpserver.c $(COMMON_SRCS) | $(BIN_DIR) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDFLAGS) + +$(BIN_DIR)/kcppeer: $(CMD_DIR)/kcppeer.c $(COMMON_SRCS) | $(BIN_DIR) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDFLAGS) + +$(BIN_DIR)/kcpping: $(CMD_DIR)/kcpping.c $(COMMON_SRCS) | $(BIN_DIR) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $^ $(LDFLAGS) + +clean: + rm -rf $(BIN_DIR) + +.PHONY: all clean diff --git a/c/README.md b/c/README.md new file mode 100644 index 0000000..3e98a38 --- /dev/null +++ b/c/README.md @@ -0,0 +1,88 @@ +# OmniSocketC + +Linux-only C11 implementation of the UDP/KCP transport stack from `OmniSocketGo`. + +This subtree is intentionally standalone. The Go code stays in place as the behavior reference, while the C implementation builds its own binaries under `c/bin/`. + +## Build + +```bash +cd c +make +``` + +Build outputs: + +- `c/bin/udpserver` +- `c/bin/udppeer` +- `c/bin/udpping` +- `c/bin/udprelay` +- `c/bin/kcpserver` +- `c/bin/kcppeer` +- `c/bin/kcpping` + +## Run On Different Machines + +Server `D` runs the KCP hub on `0.0.0.0:10909`: + +```bash +./c/bin/kcpserver -listen 0.0.0.0:10909 \ + -kcp-ts-debug-log logs/d-kcp-ts.jsonl \ + -kcp-session-stats-log logs/d-kcp-stats.jsonl +``` + +Relay `C` runs a raw UDP forwarder to `D`: + +```bash +./c/bin/kcpserver -mode=relay -listen 0.0.0.0:10909 -relay-remote 172.21.32.15:10909 +``` + +Peer `A` dials `D` through relay `C`: + +```bash +./c/bin/kcppeer -id peer-a -server 172.21.32.15:10909 -relay-via 106.55.173.235:10909 \ + -inbox-dir inbox/a \ + -latency-log logs/a-latency.jsonl \ + -kcp-ts-debug-log logs/a-kcp-ts.jsonl \ + -kcp-session-stats-log logs/a-kcp-stats.jsonl +``` + +Peer `B` dials `D` directly: + +```bash +./c/bin/kcppeer -id peer-b -server 81.70.156.140:10909 \ + -inbox-dir inbox/b \ + -latency-log logs/b-latency.jsonl \ + -kcp-ts-debug-log logs/b-kcp-ts.jsonl \ + -kcp-session-stats-log logs/b-kcp-stats.jsonl +``` + +Optional ping / echo tools: + +```bash +./c/bin/kcpping -id peer-a -server 106.55.173.235:10909 -echo +./c/bin/kcpping -id peer-b -server 81.70.156.140:10909 -to peer-a -count 20 -interval 100ms +./c/bin/udpserver -listen 0.0.0.0:9001 +./c/bin/udppeer -id peer-a -server 127.0.0.1:9001 +./c/bin/udpping -id pinger -server 127.0.0.1:9001 -to peer-a -count 20 +``` + +## Interactive Commands + +`udppeer` and `kcppeer` support the same interactive shell: + +```text +help +text peer-b hello +text peer-a hi +file peer-a /tmp/test125.bin +quit +``` + +## Notes + +- The C project targets Linux only. +- It preserves the Go wire format for UDP datagrams and KCP stream frames. +- It keeps runtime JSONL logging, UDP TX timestamp debug, KCP packet debug, and KCP session stats. +- Offline `latencysummary` and HTML chart generation are intentionally not migrated. +- No automated C tests are included in this subtree; validation is expected to happen on Linux via `make` and manual smoke tests. diff --git a/c/cmd/kcppeer.c b/c/cmd/kcppeer.c new file mode 100644 index 0000000..7690adf --- /dev/null +++ b/c/cmd/kcppeer.c @@ -0,0 +1,334 @@ +#include "cli_parse.h" +#include "interactive.h" +#include "peer_kcp_client.h" + +#include + +typedef struct kcppeer_receive_ctx { + kcp_client_t *client; + const char *inbox_dir; + volatile int stop_requested; + int rc; +} kcppeer_receive_ctx_t; + +static void kcppeer_usage(FILE *out) { + fprintf(out, "usage: kcppeer [-id peer-a] [-server 127.0.0.1:9002] [-relay-via addr]\n"); + fprintf(out, " [-to peer] [-text msg | -file path] [-bind-ip ip] [-bind-device dev]\n"); + fprintf(out, " [-inbox-dir dir] [-latency-log path] [-kcp-ts-debug-log path]\n"); + fprintf(out, " [-kcp-session-stats-log path] [-kcp-session-stats-interval 100ms]\n"); + fprintf(out, " [-interactive[=true|false]]\n"); +} + +static void *kcppeer_receive_thread_main(void *arg) { + kcppeer_receive_ctx_t *ctx = (kcppeer_receive_ctx_t *) arg; + + for (;;) { + message_t msg; + char persisted_path[512]; + + protocol_message_init(&msg); + if (kcp_client_receive(ctx->client, &msg) != 0) { + protocol_message_clear(&msg); + ctx->rc = ctx->stop_requested ? 0 : -1; + return NULL; + } + + switch (msg.type) { + case MSG_TYPE_TEXT: + if (kcp_client_persist_message(ctx->client, &msg, ctx->inbox_dir, persisted_path, sizeof(persisted_path)) != 0) { + fprintf(stderr, "kcppeer: persist text from %s to %s failed\n", msg.from, msg.to); + protocol_message_clear(&msg); + ctx->rc = -1; + return NULL; + } + fprintf(stderr, "received text from %s to %s and persisted to %s\n", msg.from, msg.to, persisted_path); + break; + case MSG_TYPE_FILE: + if (kcp_client_persist_message(ctx->client, &msg, ctx->inbox_dir, persisted_path, sizeof(persisted_path)) != 0) { + fprintf(stderr, "kcppeer: persist file from %s to %s failed\n", msg.from, msg.to); + protocol_message_clear(&msg); + ctx->rc = -1; + return NULL; + } + fprintf(stderr, "received file from %s to %s: %s (%lu bytes) -> %s\n", msg.from, msg.to, msg.file_name, (unsigned long) msg.body_len, persisted_path); + break; + case MSG_TYPE_ERROR: + fprintf(stderr, "received error from %s to %s: %.*s\n", msg.from, msg.to, (int) msg.body_len, msg.body == NULL ? "" : (const char *) msg.body); + break; + default: + fprintf(stderr, "received unexpected message type %s from %s\n", protocol_message_type_name(msg.type), msg.from); + protocol_message_clear(&msg); + ctx->rc = -1; + return NULL; + } + protocol_message_clear(&msg); + } +} + +int main(int argc, char **argv) { + const char *peer_id = "peer-a"; + const char *server_addr = "127.0.0.1:9002"; + const char *relay_via = ""; + const char *target_peer = ""; + const char *text = ""; + const char *file_path = ""; + const char *bind_ip = ""; + const char *bind_device = ""; + const char *inbox_dir = "inbox"; + const char *latency_log_path = ""; + const char *packet_log_path = ""; + const char *stats_log_path = ""; + const char *stats_interval_raw = ""; + int stats_interval_ms = KCP_DEFAULT_STATS_INTERVAL_MS; + int interactive = 1; + latency_logger_t *latency_logger = NULL; + kcp_packet_debug_logger_t *packet_logger = NULL; + kcp_session_stats_logger_t *stats_logger = NULL; + kcp_client_t *client = NULL; + kcppeer_receive_ctx_t receive_ctx; + pthread_t receive_thread; + int receive_thread_started = 0; + int i; + int rc = 1; + + memset(&receive_ctx, 0, sizeof(receive_ctx)); + + for (i = 1; i < argc; ++i) { + const char *value = NULL; + int handled; + + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-id", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -id requires a value\n"); + return 1; + } else if (handled) { + peer_id = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-server", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -server requires a value\n"); + return 1; + } else if (handled) { + server_addr = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-relay-via", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -relay-via requires a value\n"); + return 1; + } else if (handled) { + relay_via = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-to", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -to requires a value\n"); + return 1; + } else if (handled) { + target_peer = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-text", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -text requires a value\n"); + return 1; + } else if (handled) { + text = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-file", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -file requires a value\n"); + return 1; + } else if (handled) { + file_path = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-bind-ip", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -bind-ip requires a value\n"); + return 1; + } else if (handled) { + bind_ip = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-bind-device", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -bind-device requires a value\n"); + return 1; + } else if (handled) { + bind_device = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-inbox-dir", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -inbox-dir requires a value\n"); + return 1; + } else if (handled) { + inbox_dir = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-latency-log", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -latency-log requires a value\n"); + return 1; + } else if (handled) { + latency_log_path = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-kcp-ts-debug-log", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -kcp-ts-debug-log requires a value\n"); + return 1; + } else if (handled) { + packet_log_path = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-kcp-session-stats-log", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -kcp-session-stats-log requires a value\n"); + return 1; + } else if (handled) { + stats_log_path = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-kcp-session-stats-interval", &value)) < 0) { + fprintf(stderr, "kcppeer: flag -kcp-session-stats-interval requires a value\n"); + return 1; + } else if (handled) { + stats_interval_raw = value; + continue; + } + if ((handled = cli_parse_bool_flag(argv[i], "-interactive", &interactive)) < 0) { + fprintf(stderr, "kcppeer: invalid -interactive value\n"); + return 1; + } else if (handled) { + continue; + } + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + kcppeer_usage(stdout); + return 0; + } + fprintf(stderr, "kcppeer: unknown argument %s\n", argv[i]); + kcppeer_usage(stderr); + return 1; + } + + if (text[0] != '\0' && file_path[0] != '\0') { + fprintf(stderr, "kcppeer: only one of -text or -file may be specified\n"); + return 1; + } + if ((text[0] != '\0' || file_path[0] != '\0') && target_peer[0] == '\0') { + fprintf(stderr, "kcppeer: flag -to is required when sending text or file\n"); + return 1; + } + if (kcp_session_stats_parse_interval_ms(stats_interval_raw, &stats_interval_ms) != 0) { + fprintf(stderr, "kcppeer: invalid -kcp-session-stats-interval value %s\n", stats_interval_raw); + return 1; + } + + if (latency_log_path[0] != '\0') { + latency_logger = latencylog_open_jsonl(latency_log_path); + if (latency_logger == NULL) { + fprintf(stderr, "kcppeer: open latency logger %s failed\n", latency_log_path); + goto cleanup; + } + } + if (packet_log_path[0] != '\0') { + packet_logger = kcp_packet_debug_open_jsonl(packet_log_path); + if (packet_logger == NULL) { + fprintf(stderr, "kcppeer: open kcp packet debug logger %s failed\n", packet_log_path); + goto cleanup; + } + } + if (stats_log_path[0] != '\0') { + stats_logger = kcp_session_stats_open_jsonl(stats_log_path); + if (stats_logger == NULL) { + fprintf(stderr, "kcppeer: open kcp session stats logger %s failed\n", stats_log_path); + goto cleanup; + } + } + + client = kcp_client_dial(server_addr, relay_via, peer_id, bind_ip, bind_device, latency_logger, packet_logger, stats_logger, stats_interval_ms); + if (client == NULL) { + fprintf(stderr, "kcppeer: dial kcp server %s failed\n", server_addr); + goto cleanup; + } + if (relay_via[0] != '\0') { + fprintf(stderr, "opened KCP session as %s; logical server=%s, actual dial target=%s via relay; register not yet confirmed\n", kcp_client_id(client), server_addr, relay_via); + } else { + fprintf(stderr, "opened KCP session as %s; logical server=%s, actual dial target=%s; register not yet confirmed\n", kcp_client_id(client), server_addr, server_addr); + } + + receive_ctx.client = client; + receive_ctx.inbox_dir = inbox_dir; + if (pthread_create(&receive_thread, NULL, kcppeer_receive_thread_main, &receive_ctx) != 0) { + fprintf(stderr, "kcppeer: create receive thread failed\n"); + goto cleanup; + } + receive_thread_started = 1; + + if (target_peer[0] != '\0' && text[0] != '\0') { + if (kcp_client_send_text(client, target_peer, text) != 0) { + fprintf(stderr, "kcppeer: send text to %s failed\n", target_peer); + goto cleanup; + } + fprintf(stderr, "sent text to %s\n", target_peer); + } + if (target_peer[0] != '\0' && file_path[0] != '\0') { + if (kcp_client_send_file_path(client, target_peer, file_path) != 0) { + fprintf(stderr, "kcppeer: send file %s to %s failed\n", file_path, target_peer); + goto cleanup; + } + fprintf(stderr, "sent file %s to %s\n", file_path, target_peer); + } + + if (interactive) { + char line[2048]; + char prompt[128]; + + snprintf(prompt, sizeof(prompt), "%s> ", kcp_client_id(client)); + interactive_print_help(stdout, "KCP"); + while (fputs(prompt, stdout) >= 0 && fflush(stdout) == 0 && fgets(line, sizeof(line), stdin) != NULL) { + interactive_command_t command; + char err[128]; + + omni_trim_newline(line); + if (interactive_parse_command(line, &command, err, sizeof(err)) != 0) { + if (strstr(err, "empty command") == NULL) { + fprintf(stderr, "%s\n", err); + } + continue; + } + if (command.type == INTERACTIVE_CMD_HELP) { + interactive_print_help(stdout, "KCP"); + continue; + } + if (command.type == INTERACTIVE_CMD_QUIT) { + break; + } + if (command.type == INTERACTIVE_CMD_TEXT) { + if (kcp_client_send_text(client, command.to, command.value) != 0) { + fprintf(stderr, "kcppeer: send text to %s failed\n", command.to); + continue; + } + fprintf(stderr, "sent text to %s\n", command.to); + continue; + } + if (command.type == INTERACTIVE_CMD_FILE) { + if (kcp_client_send_file_path(client, command.to, command.value) != 0) { + fprintf(stderr, "kcppeer: send file %s to %s failed\n", command.value, command.to); + continue; + } + fprintf(stderr, "sent file %s to %s\n", command.value, command.to); + continue; + } + } + } + + rc = 0; + +cleanup: + receive_ctx.stop_requested = 1; + kcp_client_close(client); + if (receive_thread_started) { + pthread_join(receive_thread, NULL); + if (rc == 0 && receive_ctx.rc != 0) { + rc = 1; + } + } + kcp_client_free(client); + kcp_session_stats_close(stats_logger); + kcp_packet_debug_close(packet_logger); + latencylog_close(latency_logger); + return rc; +} diff --git a/c/cmd/kcpping.c b/c/cmd/kcpping.c new file mode 100644 index 0000000..42442d8 --- /dev/null +++ b/c/cmd/kcpping.c @@ -0,0 +1,788 @@ +#include "cli_parse.h" +#include "peer_kcp_client.h" + +#include "cJSON.h" + +#include +#include + +typedef struct kcp_ping_message_node { + struct kcp_ping_message_node *next; + message_t msg; +} kcp_ping_message_node_t; + +typedef struct kcp_ping_receiver_ctx { + kcp_client_t *client; + pthread_mutex_t mu; + kcp_ping_message_node_t *head; + kcp_ping_message_node_t *tail; + volatile int stop_requested; + int closed; + int rc; +} kcp_ping_receiver_ctx_t; + +typedef struct kcp_pending_ping { + struct kcp_pending_ping *next; + uint64_t seq; + int64_t deadline_ns; +} kcp_pending_ping_t; + +typedef struct kcp_ping_tracker { + kcp_pending_ping_t *pending; + int pending_count; + int sent; + int duplicates; + uint64_t max_seq_sent; + int64_t *samples_ns; + size_t sample_count; + size_t sample_cap; +} kcp_ping_tracker_t; + +static volatile sig_atomic_t g_kcpping_stop = 0; + +static void kcpping_on_signal(int signo) { + (void) signo; + g_kcpping_stop = 1; +} + +static void kcpping_usage(FILE *out) { + fprintf(out, "usage: kcpping [-id pinger] [-server 127.0.0.1:9002] [-to peer] [-echo]\n"); + fprintf(out, " [-count 100] [-interval 100ms] [-size 64] [-timeout 3s]\n"); + fprintf(out, " [-bind-ip ip] [-bind-device dev] [-latency-log path]\n"); +} + +static int kcp_ping_compare_i64(const void *left, const void *right) { + const int64_t *a = (const int64_t *) left; + const int64_t *b = (const int64_t *) right; + if (*a < *b) { + return -1; + } + if (*a > *b) { + return 1; + } + return 0; +} + +static double kcp_ping_sqrt(double value) { + double x = value; + int i; + + if (value <= 0.0) { + return 0.0; + } + if (x < 1.0) { + x = 1.0; + } + for (i = 0; i < 16; ++i) { + x = 0.5 * (x + value / x); + } + return x; +} + +static int kcp_ping_build_payload(uint64_t seq, int64_t ts_ns, int size, char **out_body, size_t *out_len) { + cJSON *root = NULL; + char *json = NULL; + char *pad = NULL; + size_t base_len; + size_t pad_len; + + *out_body = NULL; + *out_len = 0; + + root = cJSON_CreateObject(); + if (root == NULL) { + return -1; + } + cJSON_AddNumberToObject(root, "seq", (double) seq); + cJSON_AddNumberToObject(root, "ts_ns", (double) ts_ns); + cJSON_AddStringToObject(root, "pad", ""); + json = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (json == NULL) { + return -1; + } + base_len = strlen(json); + cJSON_free(json); + if ((int) base_len > size) { + errno = EMSGSIZE; + return -1; + } + + pad_len = (size_t) size - base_len; + pad = (char *) malloc(pad_len + 1U); + if (pad == NULL) { + return -1; + } + memset(pad, 'A', pad_len); + pad[pad_len] = '\0'; + + root = cJSON_CreateObject(); + if (root == NULL) { + free(pad); + return -1; + } + cJSON_AddNumberToObject(root, "seq", (double) seq); + cJSON_AddNumberToObject(root, "ts_ns", (double) ts_ns); + cJSON_AddStringToObject(root, "pad", pad); + free(pad); + json = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (json == NULL) { + return -1; + } + if ((int) strlen(json) != size) { + cJSON_free(json); + errno = EINVAL; + return -1; + } + *out_body = json; + *out_len = (size_t) size; + return 0; +} + +static int kcp_ping_parse_payload(const uint8_t *body, size_t body_len, uint64_t *seq, int64_t *ts_ns) { + char *text; + cJSON *root; + const cJSON *seq_item; + const cJSON *ts_item; + + if (body == NULL || seq == NULL || ts_ns == NULL) { + errno = EINVAL; + return -1; + } + text = (char *) malloc(body_len + 1U); + if (text == NULL) { + return -1; + } + memcpy(text, body, body_len); + text[body_len] = '\0'; + root = cJSON_Parse(text); + free(text); + if (root == NULL) { + errno = EPROTO; + return -1; + } + seq_item = cJSON_GetObjectItemCaseSensitive(root, "seq"); + ts_item = cJSON_GetObjectItemCaseSensitive(root, "ts_ns"); + if (!cJSON_IsNumber(seq_item) || !cJSON_IsNumber(ts_item) || seq_item->valuedouble <= 0 || ts_item->valuedouble <= 0) { + cJSON_Delete(root); + errno = EPROTO; + return -1; + } + *seq = (uint64_t) seq_item->valuedouble; + *ts_ns = (int64_t) ts_item->valuedouble; + cJSON_Delete(root); + return 0; +} + +static void kcp_ping_receiver_ctx_init(kcp_ping_receiver_ctx_t *ctx, kcp_client_t *client) { + memset(ctx, 0, sizeof(*ctx)); + ctx->client = client; + pthread_mutex_init(&ctx->mu, NULL); +} + +static void kcp_ping_receiver_ctx_destroy(kcp_ping_receiver_ctx_t *ctx) { + kcp_ping_message_node_t *node; + kcp_ping_message_node_t *next; + + if (ctx == NULL) { + return; + } + for (node = ctx->head; node != NULL; node = next) { + next = node->next; + protocol_message_clear(&node->msg); + free(node); + } + pthread_mutex_destroy(&ctx->mu); +} + +static void *kcpping_receive_thread_main(void *arg) { + kcp_ping_receiver_ctx_t *ctx = (kcp_ping_receiver_ctx_t *) arg; + + for (;;) { + message_t msg; + kcp_ping_message_node_t *node; + + protocol_message_init(&msg); + if (kcp_client_receive(ctx->client, &msg) != 0) { + protocol_message_clear(&msg); + pthread_mutex_lock(&ctx->mu); + ctx->rc = ctx->stop_requested ? 0 : -1; + ctx->closed = 1; + pthread_mutex_unlock(&ctx->mu); + return NULL; + } + + node = (kcp_ping_message_node_t *) calloc(1, sizeof(*node)); + if (node == NULL) { + protocol_message_clear(&msg); + pthread_mutex_lock(&ctx->mu); + ctx->rc = -1; + ctx->closed = 1; + pthread_mutex_unlock(&ctx->mu); + return NULL; + } + node->msg = msg; + + pthread_mutex_lock(&ctx->mu); + if (ctx->tail == NULL) { + ctx->head = node; + } else { + ctx->tail->next = node; + } + ctx->tail = node; + pthread_mutex_unlock(&ctx->mu); + } +} + +static int kcp_ping_receiver_pop(kcp_ping_receiver_ctx_t *ctx, message_t *out_msg) { + kcp_ping_message_node_t *node; + + pthread_mutex_lock(&ctx->mu); + node = ctx->head; + if (node != NULL) { + ctx->head = node->next; + if (ctx->head == NULL) { + ctx->tail = NULL; + } + } + pthread_mutex_unlock(&ctx->mu); + + if (node == NULL) { + return 0; + } + *out_msg = node->msg; + free(node); + return 1; +} + +static int kcp_ping_receiver_status(kcp_ping_receiver_ctx_t *ctx, int *closed, int *rc) { + pthread_mutex_lock(&ctx->mu); + *closed = ctx->closed; + *rc = ctx->rc; + pthread_mutex_unlock(&ctx->mu); + return 0; +} + +static void kcp_ping_tracker_init(kcp_ping_tracker_t *tracker) { + memset(tracker, 0, sizeof(*tracker)); +} + +static void kcp_ping_tracker_destroy(kcp_ping_tracker_t *tracker) { + kcp_pending_ping_t *pending; + kcp_pending_ping_t *next; + + for (pending = tracker->pending; pending != NULL; pending = next) { + next = pending->next; + free(pending); + } + free(tracker->samples_ns); +} + +static int kcp_ping_tracker_mark_sent(kcp_ping_tracker_t *tracker, uint64_t seq, int64_t sent_at_ns, int64_t timeout_ns) { + kcp_pending_ping_t *pending = (kcp_pending_ping_t *) calloc(1, sizeof(*pending)); + if (pending == NULL) { + return -1; + } + pending->seq = seq; + pending->deadline_ns = sent_at_ns + timeout_ns; + pending->next = tracker->pending; + tracker->pending = pending; + tracker->pending_count++; + tracker->sent++; + tracker->max_seq_sent = seq; + return 0; +} + +static kcp_pending_ping_t *kcp_ping_tracker_find_pending(kcp_ping_tracker_t *tracker, uint64_t seq, kcp_pending_ping_t **out_prev) { + kcp_pending_ping_t *prev = NULL; + kcp_pending_ping_t *cur; + + for (cur = tracker->pending; cur != NULL; cur = cur->next) { + if (cur->seq == seq) { + if (out_prev != NULL) { + *out_prev = prev; + } + return cur; + } + prev = cur; + } + if (out_prev != NULL) { + *out_prev = NULL; + } + return NULL; +} + +static int kcp_ping_tracker_add_sample(kcp_ping_tracker_t *tracker, int64_t rtt_ns) { + int64_t *next_samples; + size_t next_cap; + + if (tracker->sample_count == tracker->sample_cap) { + next_cap = tracker->sample_cap == 0 ? 16U : tracker->sample_cap * 2U; + next_samples = (int64_t *) realloc(tracker->samples_ns, next_cap * sizeof(*next_samples)); + if (next_samples == NULL) { + return -1; + } + tracker->samples_ns = next_samples; + tracker->sample_cap = next_cap; + } + tracker->samples_ns[tracker->sample_count++] = rtt_ns; + return 0; +} + +static int kcp_ping_tracker_observe_reply(kcp_ping_tracker_t *tracker, uint64_t seq, int64_t sent_ts_ns, int64_t received_ts_ns, int *disposition, int64_t *rtt_ns) { + kcp_pending_ping_t *prev = NULL; + kcp_pending_ping_t *pending; + + if (seq == 0 || seq > tracker->max_seq_sent) { + *disposition = 2; + *rtt_ns = 0; + return 0; + } + pending = kcp_ping_tracker_find_pending(tracker, seq, &prev); + if (pending == NULL) { + tracker->duplicates++; + *disposition = 1; + *rtt_ns = 0; + return 0; + } + if (prev == NULL) { + tracker->pending = pending->next; + } else { + prev->next = pending->next; + } + tracker->pending_count--; + free(pending); + + *rtt_ns = received_ts_ns - sent_ts_ns; + if (*rtt_ns < 0) { + *rtt_ns = 0; + } + if (kcp_ping_tracker_add_sample(tracker, *rtt_ns) != 0) { + return -1; + } + *disposition = 0; + return 0; +} + +static void kcp_ping_tracker_expire(kcp_ping_tracker_t *tracker, int64_t now_ns, FILE *out) { + kcp_pending_ping_t *prev = NULL; + kcp_pending_ping_t *cur = tracker->pending; + + while (cur != NULL) { + if (cur->deadline_ns <= now_ns) { + kcp_pending_ping_t *next = cur->next; + fprintf(out, "seq=%" PRIu64 " timeout\n", cur->seq); + if (prev == NULL) { + tracker->pending = next; + } else { + prev->next = next; + } + free(cur); + tracker->pending_count--; + cur = next; + continue; + } + prev = cur; + cur = cur->next; + } +} + +static int64_t kcp_ping_percentile_ns(const int64_t *sorted, size_t count, double percentile) { + size_t index; + double raw_index; + + if (count == 0) { + return 0; + } + if (percentile <= 0.0) { + return sorted[0]; + } + if (percentile >= 1.0) { + return sorted[count - 1]; + } + raw_index = percentile * (double) count; + index = (size_t) raw_index; + if ((double) index < raw_index) { + index++; + } + if (index > 0) { + index--; + } + if (index >= count) { + index = count - 1; + } + return sorted[index]; +} + +static void kcp_ping_print_summary(FILE *out, const char *target, const kcp_ping_tracker_t *tracker) { + int received = (int) tracker->sample_count; + double loss_pct = tracker->sent == 0 ? 0.0 : ((double) (tracker->sent - received) * 100.0 / (double) tracker->sent); + + fprintf(out, "--- %s kcp ping statistics ---\n", target); + fprintf(out, "%d packets transmitted, %d received, %d duplicates, %.2f%% packet loss\n", tracker->sent, received, tracker->duplicates, loss_pct); + if (tracker->sample_count == 0) { + fprintf(out, "rtt min/avg/max/p50/p95/p99 = n/a/n/a/n/a/n/a/n/a/n/a, stddev=n/a\n"); + return; + } + + { + int64_t *sorted = (int64_t *) malloc(tracker->sample_count * sizeof(*sorted)); + size_t i; + double sum = 0.0; + double variance = 0.0; + double avg; + int64_t min_ns; + int64_t max_ns; + int64_t p50_ns; + int64_t p95_ns; + int64_t p99_ns; + + if (sorted == NULL) { + fprintf(out, "rtt summary unavailable: memory allocation failed\n"); + return; + } + memcpy(sorted, tracker->samples_ns, tracker->sample_count * sizeof(*sorted)); + qsort(sorted, tracker->sample_count, sizeof(*sorted), kcp_ping_compare_i64); + for (i = 0; i < tracker->sample_count; ++i) { + sum += (double) sorted[i]; + } + avg = sum / (double) tracker->sample_count; + for (i = 0; i < tracker->sample_count; ++i) { + double delta = (double) sorted[i] - avg; + variance += delta * delta; + } + variance /= (double) tracker->sample_count; + + min_ns = sorted[0]; + max_ns = sorted[tracker->sample_count - 1]; + p50_ns = kcp_ping_percentile_ns(sorted, tracker->sample_count, 0.50); + p95_ns = kcp_ping_percentile_ns(sorted, tracker->sample_count, 0.95); + p99_ns = kcp_ping_percentile_ns(sorted, tracker->sample_count, 0.99); + + fprintf( + out, + "rtt min/avg/max/p50/p95/p99 = %.2fms/%.2fms/%.2fms/%.2fms/%.2fms/%.2fms, stddev=%.2fms\n", + (double) min_ns / 1000000.0, + avg / 1000000.0, + (double) max_ns / 1000000.0, + (double) p50_ns / 1000000.0, + (double) p95_ns / 1000000.0, + (double) p99_ns / 1000000.0, + kcp_ping_sqrt(variance) / 1000000.0 + ); + free(sorted); + } +} + +static int kcp_ping_expiry_poll_ms(int timeout_ms) { + int interval = timeout_ms / 4; + if (interval < 10) { + return 10; + } + if (interval > 100) { + return 100; + } + return interval; +} + +int main(int argc, char **argv) { + const char *peer_id = "pinger"; + const char *server_addr = "127.0.0.1:9002"; + const char *target_peer = ""; + const char *bind_ip = ""; + const char *bind_device = ""; + const char *latency_log_path = ""; + int echo_mode = 0; + int count = 100; + int interval_ms = 100; + int size = 64; + int timeout_ms = 3000; + latency_logger_t *latency_logger = NULL; + kcp_client_t *client = NULL; + kcp_ping_receiver_ctx_t receiver_ctx; + pthread_t receiver_thread; + int receiver_ctx_initialized = 0; + int receiver_thread_started = 0; + kcp_ping_tracker_t tracker; + int i; + int rc = 1; + + kcp_ping_tracker_init(&tracker); + memset(&receiver_ctx, 0, sizeof(receiver_ctx)); + + for (i = 1; i < argc; ++i) { + const char *value = NULL; + int handled; + + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-id", &value)) < 0) { + fprintf(stderr, "kcpping: flag -id requires a value\n"); + return 1; + } else if (handled) { + peer_id = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-server", &value)) < 0) { + fprintf(stderr, "kcpping: flag -server requires a value\n"); + return 1; + } else if (handled) { + server_addr = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-to", &value)) < 0) { + fprintf(stderr, "kcpping: flag -to requires a value\n"); + return 1; + } else if (handled) { + target_peer = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-count", &value)) < 0) { + fprintf(stderr, "kcpping: flag -count requires a value\n"); + return 1; + } else if (handled) { + count = atoi(value); + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-interval", &value)) < 0) { + fprintf(stderr, "kcpping: flag -interval requires a value\n"); + return 1; + } else if (handled) { + if (omni_parse_duration_ms(value, interval_ms, &interval_ms) != 0) { + fprintf(stderr, "kcpping: invalid -interval value %s\n", value); + return 1; + } + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-size", &value)) < 0) { + fprintf(stderr, "kcpping: flag -size requires a value\n"); + return 1; + } else if (handled) { + size = atoi(value); + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-timeout", &value)) < 0) { + fprintf(stderr, "kcpping: flag -timeout requires a value\n"); + return 1; + } else if (handled) { + if (omni_parse_duration_ms(value, timeout_ms, &timeout_ms) != 0) { + fprintf(stderr, "kcpping: invalid -timeout value %s\n", value); + return 1; + } + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-bind-ip", &value)) < 0) { + fprintf(stderr, "kcpping: flag -bind-ip requires a value\n"); + return 1; + } else if (handled) { + bind_ip = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-bind-device", &value)) < 0) { + fprintf(stderr, "kcpping: flag -bind-device requires a value\n"); + return 1; + } else if (handled) { + bind_device = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-latency-log", &value)) < 0) { + fprintf(stderr, "kcpping: flag -latency-log requires a value\n"); + return 1; + } else if (handled) { + latency_log_path = value; + continue; + } + if ((handled = cli_parse_bool_flag(argv[i], "-echo", &echo_mode)) < 0) { + fprintf(stderr, "kcpping: invalid -echo value\n"); + return 1; + } else if (handled) { + continue; + } + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + kcpping_usage(stdout); + return 0; + } + fprintf(stderr, "kcpping: unknown argument %s\n", argv[i]); + kcpping_usage(stderr); + return 1; + } + + if (peer_id[0] == '\0' || server_addr[0] == '\0') { + fprintf(stderr, "kcpping: flags -id and -server are required\n"); + return 1; + } + if (!echo_mode && target_peer[0] == '\0') { + fprintf(stderr, "kcpping: flag -to is required unless -echo is set\n"); + return 1; + } + if (count < 0 || interval_ms <= 0 || size <= 0 || timeout_ms <= 0) { + fprintf(stderr, "kcpping: invalid numeric flag value\n"); + return 1; + } + + signal(SIGINT, kcpping_on_signal); + + if (latency_log_path[0] != '\0') { + latency_logger = latencylog_open_jsonl(latency_log_path); + if (latency_logger == NULL) { + fprintf(stderr, "kcpping: open latency logger %s failed\n", latency_log_path); + goto cleanup; + } + } + client = kcp_client_dial(server_addr, NULL, peer_id, bind_ip, bind_device, latency_logger, NULL, NULL, KCP_DEFAULT_STATS_INTERVAL_MS); + if (client == NULL) { + fprintf(stderr, "kcpping: dial kcp server %s failed\n", server_addr); + goto cleanup; + } + + if (echo_mode) { + while (!g_kcpping_stop) { + message_t msg; + + protocol_message_init(&msg); + if (kcp_client_receive(client, &msg) != 0) { + protocol_message_clear(&msg); + if (g_kcpping_stop) { + break; + } + fprintf(stderr, "kcpping: receive failed in echo mode\n"); + goto cleanup; + } + if (msg.type == MSG_TYPE_TEXT) { + char *text = (char *) malloc(msg.body_len + 1U); + if (text == NULL) { + protocol_message_clear(&msg); + goto cleanup; + } + memcpy(text, msg.body, msg.body_len); + text[msg.body_len] = '\0'; + if (kcp_client_send_text(client, msg.from, text) != 0) { + free(text); + protocol_message_clear(&msg); + fprintf(stderr, "kcpping: echo send back to %s failed\n", msg.from); + goto cleanup; + } + free(text); + } else if (msg.type == MSG_TYPE_ERROR) { + fprintf(stderr, "server error: %.*s\n", (int) msg.body_len, msg.body == NULL ? "" : (const char *) msg.body); + } else { + fprintf(stderr, "unexpected message type %s from %s ignored\n", protocol_message_type_name(msg.type), msg.from); + } + protocol_message_clear(&msg); + } + rc = 0; + goto cleanup; + } + + fprintf(stdout, "KCP PING %s via %s (payload=%d bytes, KCP)\n", target_peer, server_addr, size); + kcp_ping_receiver_ctx_init(&receiver_ctx, client); + receiver_ctx_initialized = 1; + if (pthread_create(&receiver_thread, NULL, kcpping_receive_thread_main, &receiver_ctx) != 0) { + fprintf(stderr, "kcpping: create receive thread failed\n"); + goto cleanup; + } + receiver_thread_started = 1; + + { + uint64_t next_seq = 1; + int stop_sending = 0; + int64_t next_send_at_ns = omni_now_unix_nano(); + int poll_ms = kcp_ping_expiry_poll_ms(timeout_ms); + int64_t timeout_ns = (int64_t) timeout_ms * 1000000LL; + + while (!g_kcpping_stop || tracker.pending_count > 0 || !stop_sending) { + int64_t now_ns = omni_now_unix_nano(); + message_t msg; + int popped; + int receiver_closed; + int receiver_status_rc; + + if (!stop_sending && now_ns >= next_send_at_ns) { + char *payload = NULL; + size_t payload_len = 0; + + if (count > 0 && tracker.sent >= count) { + stop_sending = 1; + } else { + if (kcp_ping_build_payload(next_seq, now_ns, size, &payload, &payload_len) != 0) { + fprintf(stderr, "kcpping: build payload for seq=%" PRIu64 " failed\n", next_seq); + free(payload); + goto cleanup; + } + if (kcp_client_send_text(client, target_peer, payload) != 0) { + fprintf(stderr, "kcpping: send ping seq=%" PRIu64 " failed\n", next_seq); + free(payload); + goto cleanup; + } + free(payload); + if (kcp_ping_tracker_mark_sent(&tracker, next_seq, now_ns, timeout_ns) != 0) { + goto cleanup; + } + next_seq++; + next_send_at_ns = now_ns + (int64_t) interval_ms * 1000000LL; + if (count > 0 && tracker.sent >= count) { + stop_sending = 1; + } + } + } + + kcp_ping_tracker_expire(&tracker, now_ns, stdout); + + do { + popped = kcp_ping_receiver_pop(&receiver_ctx, &msg); + if (popped == 1) { + if (msg.type == MSG_TYPE_TEXT) { + uint64_t seq; + int64_t sent_ts_ns; + int disposition; + int64_t rtt_ns; + + if (kcp_ping_parse_payload(msg.body, msg.body_len, &seq, &sent_ts_ns) != 0) { + fprintf(stderr, "ignore non-ping text message from %s\n", msg.from); + } else if (kcp_ping_tracker_observe_reply(&tracker, seq, sent_ts_ns, omni_now_unix_nano(), &disposition, &rtt_ns) != 0) { + protocol_message_clear(&msg); + goto cleanup; + } else if (disposition == 0) { + fprintf(stdout, "seq=%" PRIu64 " rtt=%.2fms\n", seq, (double) rtt_ns / 1000000.0); + } else if (disposition == 1) { + fprintf(stderr, "seq=%" PRIu64 " duplicate or late reply ignored\n", seq); + } else { + fprintf(stderr, "seq=%" PRIu64 " unexpected reply ignored\n", seq); + } + } else if (msg.type == MSG_TYPE_ERROR) { + fprintf(stderr, "server error: %.*s\n", (int) msg.body_len, msg.body == NULL ? "" : (const char *) msg.body); + } else { + fprintf(stderr, "unexpected message type %s from %s ignored\n", protocol_message_type_name(msg.type), msg.from); + } + protocol_message_clear(&msg); + } + } while (popped == 1); + + kcp_ping_receiver_status(&receiver_ctx, &receiver_closed, &receiver_status_rc); + if (receiver_closed && receiver_status_rc != 0) { + fprintf(stderr, "kcpping: receive loop failed\n"); + goto cleanup; + } + if ((g_kcpping_stop || stop_sending) && tracker.pending_count == 0) { + break; + } + usleep((useconds_t) poll_ms * 1000U); + } + } + + kcp_ping_print_summary(stdout, target_peer, &tracker); + rc = 0; + +cleanup: + receiver_ctx.stop_requested = 1; + kcp_client_close(client); + if (receiver_thread_started) { + pthread_join(receiver_thread, NULL); + kcp_ping_receiver_ctx_destroy(&receiver_ctx); + } else if (receiver_ctx_initialized) { + kcp_ping_receiver_ctx_destroy(&receiver_ctx); + } + kcp_client_free(client); + latencylog_close(latency_logger); + kcp_ping_tracker_destroy(&tracker); + return rc; +} diff --git a/c/cmd/kcpserver.c b/c/cmd/kcpserver.c new file mode 100644 index 0000000..c73b1ed --- /dev/null +++ b/c/cmd/kcpserver.c @@ -0,0 +1,223 @@ +#include "cli_parse.h" +#include "server_kcp_hub.h" +#include "server_udp_relay.h" + +static void kcpserver_usage(FILE *out) { + fprintf(out, "usage: kcpserver [-mode hub|relay] [-listen addr] [-bind-device dev]\n"); + fprintf(out, " [-latency-log path] [-kcp-ts-debug-log path]\n"); + fprintf(out, " [-kcp-session-stats-log path] [-kcp-session-stats-interval 100ms]\n"); + fprintf(out, " [-relay-remote addr] [-relay-listen addr] [-relay-peer addr]\n"); +} + +int main(int argc, char **argv) { + const char *mode = "hub"; + const char *listen_addr = ":9002"; + const char *bind_device = ""; + const char *latency_log_path = ""; + const char *packet_log_path = ""; + const char *stats_log_path = ""; + const char *stats_interval_raw = ""; + const char *relay_listen_alias = ""; + const char *relay_remote_addr = ""; + const char *relay_peer_alias = ""; + int stats_interval_ms = KCP_DEFAULT_STATS_INTERVAL_MS; + int i; + int rc = 1; + + latency_logger_t *latency_logger = NULL; + kcp_packet_debug_logger_t *packet_logger = NULL; + kcp_session_stats_logger_t *stats_logger = NULL; + kcp_listener_t *listener = NULL; + kcp_hub_t *hub = NULL; + udp_relay_t *relay = NULL; + + for (i = 1; i < argc; ++i) { + const char *value = NULL; + int handled; + + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-mode", &value)) < 0) { + fprintf(stderr, "kcpserver: flag -mode requires a value\n"); + return 1; + } else if (handled) { + mode = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-listen", &value)) < 0) { + fprintf(stderr, "kcpserver: flag -listen requires a value\n"); + return 1; + } else if (handled) { + listen_addr = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-bind-device", &value)) < 0) { + fprintf(stderr, "kcpserver: flag -bind-device requires a value\n"); + return 1; + } else if (handled) { + bind_device = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-latency-log", &value)) < 0) { + fprintf(stderr, "kcpserver: flag -latency-log requires a value\n"); + return 1; + } else if (handled) { + latency_log_path = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-kcp-ts-debug-log", &value)) < 0) { + fprintf(stderr, "kcpserver: flag -kcp-ts-debug-log requires a value\n"); + return 1; + } else if (handled) { + packet_log_path = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-kcp-session-stats-log", &value)) < 0) { + fprintf(stderr, "kcpserver: flag -kcp-session-stats-log requires a value\n"); + return 1; + } else if (handled) { + stats_log_path = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-kcp-session-stats-interval", &value)) < 0) { + fprintf(stderr, "kcpserver: flag -kcp-session-stats-interval requires a value\n"); + return 1; + } else if (handled) { + stats_interval_raw = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-relay-listen", &value)) < 0) { + fprintf(stderr, "kcpserver: flag -relay-listen requires a value\n"); + return 1; + } else if (handled) { + relay_listen_alias = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-relay-remote", &value)) < 0) { + fprintf(stderr, "kcpserver: flag -relay-remote requires a value\n"); + return 1; + } else if (handled) { + relay_remote_addr = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-relay-peer", &value)) < 0) { + fprintf(stderr, "kcpserver: flag -relay-peer requires a value\n"); + return 1; + } else if (handled) { + relay_peer_alias = value; + continue; + } + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + kcpserver_usage(stdout); + return 0; + } + fprintf(stderr, "kcpserver: unknown argument %s\n", argv[i]); + kcpserver_usage(stderr); + return 1; + } + + if (kcp_session_stats_parse_interval_ms(stats_interval_raw, &stats_interval_ms) != 0) { + fprintf(stderr, "kcpserver: invalid -kcp-session-stats-interval value %s\n", stats_interval_raw); + return 1; + } + + if (relay_peer_alias[0] != '\0' && relay_remote_addr[0] != '\0' && strcmp(relay_peer_alias, relay_remote_addr) != 0) { + fprintf(stderr, "kcpserver: flags -relay-remote and -relay-peer must match when both are set\n"); + return 1; + } + if (relay_remote_addr[0] == '\0' && relay_peer_alias[0] != '\0') { + relay_remote_addr = relay_peer_alias; + } + if (relay_peer_alias[0] != '\0') { + fprintf(stderr, "warning: flag -relay-peer is deprecated; use -relay-remote instead\n"); + } + if (relay_listen_alias[0] != '\0') { + if (strcmp(mode, "relay") != 0) { + fprintf(stderr, "kcpserver: flag -relay-listen may only be used in relay mode\n"); + return 1; + } + if (listen_addr[0] != '\0' && strcmp(listen_addr, ":9002") != 0 && strcmp(listen_addr, relay_listen_alias) != 0) { + fprintf(stderr, "kcpserver: flags -listen and -relay-listen must match when both are set in relay mode\n"); + return 1; + } + listen_addr = relay_listen_alias; + fprintf(stderr, "warning: flag -relay-listen is deprecated; use -listen with -mode=relay instead\n"); + } + + if (strcmp(mode, "hub") == 0) { + if (relay_remote_addr[0] != '\0') { + fprintf(stderr, "kcpserver: flag -relay-remote may only be used in relay mode\n"); + return 1; + } + if (latency_log_path[0] != '\0') { + latency_logger = latencylog_open_jsonl(latency_log_path); + if (latency_logger == NULL) { + fprintf(stderr, "kcpserver: open latency logger %s failed\n", latency_log_path); + goto cleanup; + } + } + if (packet_log_path[0] != '\0') { + packet_logger = kcp_packet_debug_open_jsonl(packet_log_path); + if (packet_logger == NULL) { + fprintf(stderr, "kcpserver: open packet debug logger %s failed\n", packet_log_path); + goto cleanup; + } + } + if (stats_log_path[0] != '\0') { + stats_logger = kcp_session_stats_open_jsonl(stats_log_path); + if (stats_logger == NULL) { + fprintf(stderr, "kcpserver: open session stats logger %s failed\n", stats_log_path); + goto cleanup; + } + } + listener = kcp_listener_listen(listen_addr, bind_device, packet_logger, OMNI_NODE_ROLE_SERVER, "hub"); + if (listener == NULL) { + fprintf(stderr, "kcpserver: listen on %s failed\n", listen_addr); + goto cleanup; + } + hub = kcp_hub_new(latency_logger, stats_logger, stats_interval_ms); + if (hub == NULL) { + fprintf(stderr, "kcpserver: create hub failed\n"); + goto cleanup; + } + fprintf(stderr, "kcp hub listening on %s\n", listen_addr); + if (kcp_hub_serve_listener(hub, listener) != 0) { + fprintf(stderr, "kcpserver: serve listener failed\n"); + goto cleanup; + } + rc = 0; + goto cleanup; + } + + if (strcmp(mode, "relay") == 0) { + if (bind_device[0] != '\0') { + fprintf(stderr, "kcpserver: flag -bind-device is not supported in relay mode\n"); + return 1; + } + if (relay_remote_addr[0] == '\0') { + fprintf(stderr, "kcpserver: flag -relay-remote is required in relay mode\n"); + return 1; + } + relay = udp_relay_open(listen_addr, relay_remote_addr); + if (relay == NULL) { + fprintf(stderr, "kcpserver: open udp relay %s -> %s failed\n", listen_addr, relay_remote_addr); + goto cleanup; + } + fprintf(stderr, "udp relay listening on %s and forwarding to %s\n", listen_addr, relay_remote_addr); + if (udp_relay_serve(relay) != 0) { + fprintf(stderr, "kcpserver: udp relay stopped with error\n"); + goto cleanup; + } + rc = 0; + goto cleanup; + } + + fprintf(stderr, "kcpserver: unsupported -mode=%s; want hub or relay\n", mode); + +cleanup: + udp_relay_free(relay); + kcp_hub_free(hub); + kcp_listener_free(listener); + kcp_session_stats_close(stats_logger); + kcp_packet_debug_close(packet_logger); + latencylog_close(latency_logger); + return rc; +} diff --git a/c/cmd/udppeer.c b/c/cmd/udppeer.c new file mode 100644 index 0000000..a3aa5c9 --- /dev/null +++ b/c/cmd/udppeer.c @@ -0,0 +1,282 @@ +#include "cli_parse.h" +#include "interactive.h" +#include "peer_udp_client.h" + +#include + +typedef struct udppeer_receive_ctx { + udp_client_t *client; + const char *inbox_dir; + volatile int stop_requested; + int rc; +} udppeer_receive_ctx_t; + +static void udppeer_usage(FILE *out) { + fprintf(out, "usage: udppeer [-id peer-a] [-server 127.0.0.1:9001] [-to peer] [-text msg | -file path]\n"); + fprintf(out, " [-bind-ip ip] [-inbox-dir dir] [-latency-log path] [-tx-ts-debug-log path]\n"); + fprintf(out, " [-interactive[=true|false]]\n"); +} + +static void *udppeer_receive_thread_main(void *arg) { + udppeer_receive_ctx_t *ctx = (udppeer_receive_ctx_t *) arg; + + for (;;) { + message_t msg; + char persisted_path[512]; + + protocol_message_init(&msg); + if (udp_client_receive(ctx->client, &msg) != 0) { + protocol_message_clear(&msg); + ctx->rc = ctx->stop_requested ? 0 : -1; + return NULL; + } + + switch (msg.type) { + case MSG_TYPE_TEXT: + if (udp_client_persist_message(ctx->client, &msg, ctx->inbox_dir, persisted_path, sizeof(persisted_path)) != 0) { + fprintf(stderr, "udppeer: persist text from %s to %s failed\n", msg.from, msg.to); + protocol_message_clear(&msg); + ctx->rc = -1; + return NULL; + } + fprintf(stderr, "received text from %s to %s and persisted to %s\n", msg.from, msg.to, persisted_path); + break; + case MSG_TYPE_FILE: + if (udp_client_persist_message(ctx->client, &msg, ctx->inbox_dir, persisted_path, sizeof(persisted_path)) != 0) { + fprintf(stderr, "udppeer: persist file from %s to %s failed\n", msg.from, msg.to); + protocol_message_clear(&msg); + ctx->rc = -1; + return NULL; + } + fprintf(stderr, "received file from %s to %s: %s (%lu bytes) -> %s\n", msg.from, msg.to, msg.file_name, (unsigned long) msg.body_len, persisted_path); + break; + case MSG_TYPE_ERROR: + fprintf(stderr, "received error from %s to %s: %.*s\n", msg.from, msg.to, (int) msg.body_len, msg.body == NULL ? "" : (const char *) msg.body); + break; + default: + fprintf(stderr, "received unexpected message type %s from %s\n", protocol_message_type_name(msg.type), msg.from); + protocol_message_clear(&msg); + ctx->rc = -1; + return NULL; + } + protocol_message_clear(&msg); + } +} + +int main(int argc, char **argv) { + const char *peer_id = "peer-a"; + const char *server_addr = "127.0.0.1:9001"; + const char *target_peer = ""; + const char *text = ""; + const char *file_path = ""; + const char *bind_ip = ""; + const char *inbox_dir = "inbox"; + const char *latency_log_path = ""; + const char *tx_debug_log_path = ""; + int interactive = 1; + latency_logger_t *latency_logger = NULL; + tx_timestamp_debug_logger_t *debug_logger = NULL; + udp_client_t *client = NULL; + udppeer_receive_ctx_t receive_ctx; + pthread_t receive_thread; + int receive_thread_started = 0; + int i; + int rc = 1; + + memset(&receive_ctx, 0, sizeof(receive_ctx)); + + for (i = 1; i < argc; ++i) { + const char *value = NULL; + int handled; + + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-id", &value)) < 0) { + fprintf(stderr, "udppeer: flag -id requires a value\n"); + return 1; + } else if (handled) { + peer_id = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-server", &value)) < 0) { + fprintf(stderr, "udppeer: flag -server requires a value\n"); + return 1; + } else if (handled) { + server_addr = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-to", &value)) < 0) { + fprintf(stderr, "udppeer: flag -to requires a value\n"); + return 1; + } else if (handled) { + target_peer = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-text", &value)) < 0) { + fprintf(stderr, "udppeer: flag -text requires a value\n"); + return 1; + } else if (handled) { + text = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-file", &value)) < 0) { + fprintf(stderr, "udppeer: flag -file requires a value\n"); + return 1; + } else if (handled) { + file_path = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-bind-ip", &value)) < 0) { + fprintf(stderr, "udppeer: flag -bind-ip requires a value\n"); + return 1; + } else if (handled) { + bind_ip = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-inbox-dir", &value)) < 0) { + fprintf(stderr, "udppeer: flag -inbox-dir requires a value\n"); + return 1; + } else if (handled) { + inbox_dir = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-latency-log", &value)) < 0) { + fprintf(stderr, "udppeer: flag -latency-log requires a value\n"); + return 1; + } else if (handled) { + latency_log_path = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-tx-ts-debug-log", &value)) < 0) { + fprintf(stderr, "udppeer: flag -tx-ts-debug-log requires a value\n"); + return 1; + } else if (handled) { + tx_debug_log_path = value; + continue; + } + if ((handled = cli_parse_bool_flag(argv[i], "-interactive", &interactive)) < 0) { + fprintf(stderr, "udppeer: invalid -interactive value\n"); + return 1; + } else if (handled) { + continue; + } + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + udppeer_usage(stdout); + return 0; + } + fprintf(stderr, "udppeer: unknown argument %s\n", argv[i]); + udppeer_usage(stderr); + return 1; + } + + if (text[0] != '\0' && file_path[0] != '\0') { + fprintf(stderr, "udppeer: only one of -text or -file may be specified\n"); + return 1; + } + if ((text[0] != '\0' || file_path[0] != '\0') && target_peer[0] == '\0') { + fprintf(stderr, "udppeer: flag -to is required when sending text or file\n"); + return 1; + } + + if (latency_log_path[0] != '\0') { + latency_logger = latencylog_open_jsonl(latency_log_path); + if (latency_logger == NULL) { + fprintf(stderr, "udppeer: open latency logger %s failed\n", latency_log_path); + goto cleanup; + } + } + if (tx_debug_log_path[0] != '\0') { + debug_logger = tx_timestamp_debug_open_jsonl(tx_debug_log_path); + if (debug_logger == NULL) { + fprintf(stderr, "udppeer: open tx timestamp debug logger %s failed\n", tx_debug_log_path); + goto cleanup; + } + } + + client = udp_client_dial(server_addr, peer_id, bind_ip, latency_logger, debug_logger, tx_debug_log_path[0] != '\0'); + if (client == NULL) { + fprintf(stderr, "udppeer: dial udp server %s failed\n", server_addr); + goto cleanup; + } + fprintf(stderr, "connected to %s as %s (UDP)\n", server_addr, udp_client_id(client)); + + receive_ctx.client = client; + receive_ctx.inbox_dir = inbox_dir; + if (pthread_create(&receive_thread, NULL, udppeer_receive_thread_main, &receive_ctx) != 0) { + fprintf(stderr, "udppeer: create receive thread failed\n"); + goto cleanup; + } + receive_thread_started = 1; + + if (target_peer[0] != '\0' && text[0] != '\0') { + if (udp_client_send_text(client, target_peer, text) != 0) { + fprintf(stderr, "udppeer: send text to %s failed\n", target_peer); + goto cleanup; + } + fprintf(stderr, "sent text to %s\n", target_peer); + } + if (target_peer[0] != '\0' && file_path[0] != '\0') { + if (udp_client_send_file_path(client, target_peer, file_path) != 0) { + fprintf(stderr, "udppeer: send file %s to %s failed\n", file_path, target_peer); + goto cleanup; + } + fprintf(stderr, "sent file %s to %s\n", file_path, target_peer); + } + + if (interactive) { + char line[2048]; + char prompt[128]; + + snprintf(prompt, sizeof(prompt), "%s> ", udp_client_id(client)); + interactive_print_help(stdout, "UDP"); + while (fputs(prompt, stdout) >= 0 && fflush(stdout) == 0 && fgets(line, sizeof(line), stdin) != NULL) { + interactive_command_t command; + char err[128]; + + omni_trim_newline(line); + if (interactive_parse_command(line, &command, err, sizeof(err)) != 0) { + if (strstr(err, "empty command") == NULL) { + fprintf(stderr, "%s\n", err); + } + continue; + } + if (command.type == INTERACTIVE_CMD_HELP) { + interactive_print_help(stdout, "UDP"); + continue; + } + if (command.type == INTERACTIVE_CMD_QUIT) { + break; + } + if (command.type == INTERACTIVE_CMD_TEXT) { + if (udp_client_send_text(client, command.to, command.value) != 0) { + fprintf(stderr, "udppeer: send text to %s failed\n", command.to); + continue; + } + fprintf(stderr, "sent text to %s\n", command.to); + continue; + } + if (command.type == INTERACTIVE_CMD_FILE) { + if (udp_client_send_file_path(client, command.to, command.value) != 0) { + fprintf(stderr, "udppeer: send file %s to %s failed\n", command.value, command.to); + continue; + } + fprintf(stderr, "sent file %s to %s\n", command.value, command.to); + continue; + } + } + } + + rc = 0; + +cleanup: + receive_ctx.stop_requested = 1; + udp_client_close(client); + if (receive_thread_started) { + pthread_join(receive_thread, NULL); + if (rc == 0 && receive_ctx.rc != 0) { + rc = 1; + } + } + udp_client_free(client); + tx_timestamp_debug_close(debug_logger); + latencylog_close(latency_logger); + return rc; +} diff --git a/c/cmd/udpping.c b/c/cmd/udpping.c new file mode 100644 index 0000000..1b652da --- /dev/null +++ b/c/cmd/udpping.c @@ -0,0 +1,780 @@ +#include "cli_parse.h" +#include "peer_udp_client.h" + +#include "cJSON.h" + +#include +#include + +typedef struct ping_message_node { + struct ping_message_node *next; + message_t msg; +} ping_message_node_t; + +typedef struct ping_receiver_ctx { + udp_client_t *client; + pthread_mutex_t mu; + ping_message_node_t *head; + ping_message_node_t *tail; + volatile int stop_requested; + int closed; + int rc; +} ping_receiver_ctx_t; + +typedef struct pending_ping { + struct pending_ping *next; + uint64_t seq; + int64_t deadline_ns; +} pending_ping_t; + +typedef struct ping_tracker { + pending_ping_t *pending; + int pending_count; + int sent; + int duplicates; + uint64_t max_seq_sent; + int64_t *samples_ns; + size_t sample_count; + size_t sample_cap; +} ping_tracker_t; + +static volatile sig_atomic_t g_udpping_stop = 0; + +static void udpping_on_signal(int signo) { + (void) signo; + g_udpping_stop = 1; +} + +static void udpping_usage(FILE *out) { + fprintf(out, "usage: udpping [-id pinger] [-server 127.0.0.1:9001] [-to peer] [-echo]\n"); + fprintf(out, " [-count 100] [-interval 100ms] [-size 64] [-timeout 3s]\n"); + fprintf(out, " [-bind-ip ip] [-latency-log path]\n"); +} + +static int ping_compare_i64(const void *left, const void *right) { + const int64_t *a = (const int64_t *) left; + const int64_t *b = (const int64_t *) right; + if (*a < *b) { + return -1; + } + if (*a > *b) { + return 1; + } + return 0; +} + +static double ping_sqrt(double value) { + double x = value; + int i; + + if (value <= 0.0) { + return 0.0; + } + if (x < 1.0) { + x = 1.0; + } + for (i = 0; i < 16; ++i) { + x = 0.5 * (x + value / x); + } + return x; +} + +static int ping_build_payload(uint64_t seq, int64_t ts_ns, int size, char **out_body, size_t *out_len) { + cJSON *root = NULL; + char *json = NULL; + char *pad = NULL; + size_t base_len; + size_t pad_len; + + *out_body = NULL; + *out_len = 0; + + root = cJSON_CreateObject(); + if (root == NULL) { + return -1; + } + cJSON_AddNumberToObject(root, "seq", (double) seq); + cJSON_AddNumberToObject(root, "ts_ns", (double) ts_ns); + cJSON_AddStringToObject(root, "pad", ""); + json = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (json == NULL) { + return -1; + } + base_len = strlen(json); + cJSON_free(json); + if ((int) base_len > size) { + errno = EMSGSIZE; + return -1; + } + + pad_len = (size_t) size - base_len; + pad = (char *) malloc(pad_len + 1U); + if (pad == NULL) { + return -1; + } + memset(pad, 'A', pad_len); + pad[pad_len] = '\0'; + + root = cJSON_CreateObject(); + if (root == NULL) { + free(pad); + return -1; + } + cJSON_AddNumberToObject(root, "seq", (double) seq); + cJSON_AddNumberToObject(root, "ts_ns", (double) ts_ns); + cJSON_AddStringToObject(root, "pad", pad); + free(pad); + json = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (json == NULL) { + return -1; + } + if ((int) strlen(json) != size) { + cJSON_free(json); + errno = EINVAL; + return -1; + } + *out_body = json; + *out_len = (size_t) size; + return 0; +} + +static int ping_parse_payload(const uint8_t *body, size_t body_len, uint64_t *seq, int64_t *ts_ns) { + char *text; + cJSON *root; + const cJSON *seq_item; + const cJSON *ts_item; + + if (body == NULL || seq == NULL || ts_ns == NULL) { + errno = EINVAL; + return -1; + } + text = (char *) malloc(body_len + 1U); + if (text == NULL) { + return -1; + } + memcpy(text, body, body_len); + text[body_len] = '\0'; + root = cJSON_Parse(text); + free(text); + if (root == NULL) { + errno = EPROTO; + return -1; + } + seq_item = cJSON_GetObjectItemCaseSensitive(root, "seq"); + ts_item = cJSON_GetObjectItemCaseSensitive(root, "ts_ns"); + if (!cJSON_IsNumber(seq_item) || !cJSON_IsNumber(ts_item) || seq_item->valuedouble <= 0 || ts_item->valuedouble <= 0) { + cJSON_Delete(root); + errno = EPROTO; + return -1; + } + *seq = (uint64_t) seq_item->valuedouble; + *ts_ns = (int64_t) ts_item->valuedouble; + cJSON_Delete(root); + return 0; +} + +static void ping_receiver_ctx_init(ping_receiver_ctx_t *ctx, udp_client_t *client) { + memset(ctx, 0, sizeof(*ctx)); + ctx->client = client; + pthread_mutex_init(&ctx->mu, NULL); +} + +static void ping_receiver_ctx_destroy(ping_receiver_ctx_t *ctx) { + ping_message_node_t *node; + ping_message_node_t *next; + + if (ctx == NULL) { + return; + } + for (node = ctx->head; node != NULL; node = next) { + next = node->next; + protocol_message_clear(&node->msg); + free(node); + } + pthread_mutex_destroy(&ctx->mu); +} + +static void *udpping_receive_thread_main(void *arg) { + ping_receiver_ctx_t *ctx = (ping_receiver_ctx_t *) arg; + + for (;;) { + message_t msg; + ping_message_node_t *node; + + protocol_message_init(&msg); + if (udp_client_receive(ctx->client, &msg) != 0) { + protocol_message_clear(&msg); + pthread_mutex_lock(&ctx->mu); + ctx->rc = ctx->stop_requested ? 0 : -1; + ctx->closed = 1; + pthread_mutex_unlock(&ctx->mu); + return NULL; + } + + node = (ping_message_node_t *) calloc(1, sizeof(*node)); + if (node == NULL) { + protocol_message_clear(&msg); + pthread_mutex_lock(&ctx->mu); + ctx->rc = -1; + ctx->closed = 1; + pthread_mutex_unlock(&ctx->mu); + return NULL; + } + node->msg = msg; + + pthread_mutex_lock(&ctx->mu); + if (ctx->tail == NULL) { + ctx->head = node; + } else { + ctx->tail->next = node; + } + ctx->tail = node; + pthread_mutex_unlock(&ctx->mu); + } +} + +static int ping_receiver_pop(ping_receiver_ctx_t *ctx, message_t *out_msg) { + ping_message_node_t *node; + + pthread_mutex_lock(&ctx->mu); + node = ctx->head; + if (node != NULL) { + ctx->head = node->next; + if (ctx->head == NULL) { + ctx->tail = NULL; + } + } + pthread_mutex_unlock(&ctx->mu); + + if (node == NULL) { + return 0; + } + *out_msg = node->msg; + free(node); + return 1; +} + +static int ping_receiver_status(ping_receiver_ctx_t *ctx, int *closed, int *rc) { + pthread_mutex_lock(&ctx->mu); + *closed = ctx->closed; + *rc = ctx->rc; + pthread_mutex_unlock(&ctx->mu); + return 0; +} + +static void ping_tracker_init(ping_tracker_t *tracker) { + memset(tracker, 0, sizeof(*tracker)); +} + +static void ping_tracker_destroy(ping_tracker_t *tracker) { + pending_ping_t *pending; + pending_ping_t *next; + + for (pending = tracker->pending; pending != NULL; pending = next) { + next = pending->next; + free(pending); + } + free(tracker->samples_ns); +} + +static int ping_tracker_mark_sent(ping_tracker_t *tracker, uint64_t seq, int64_t sent_at_ns, int64_t timeout_ns) { + pending_ping_t *pending = (pending_ping_t *) calloc(1, sizeof(*pending)); + if (pending == NULL) { + return -1; + } + pending->seq = seq; + pending->deadline_ns = sent_at_ns + timeout_ns; + pending->next = tracker->pending; + tracker->pending = pending; + tracker->pending_count++; + tracker->sent++; + tracker->max_seq_sent = seq; + return 0; +} + +static pending_ping_t *ping_tracker_find_pending(ping_tracker_t *tracker, uint64_t seq, pending_ping_t **out_prev) { + pending_ping_t *prev = NULL; + pending_ping_t *cur; + + for (cur = tracker->pending; cur != NULL; cur = cur->next) { + if (cur->seq == seq) { + if (out_prev != NULL) { + *out_prev = prev; + } + return cur; + } + prev = cur; + } + if (out_prev != NULL) { + *out_prev = NULL; + } + return NULL; +} + +static int ping_tracker_add_sample(ping_tracker_t *tracker, int64_t rtt_ns) { + int64_t *next_samples; + size_t next_cap; + + if (tracker->sample_count == tracker->sample_cap) { + next_cap = tracker->sample_cap == 0 ? 16U : tracker->sample_cap * 2U; + next_samples = (int64_t *) realloc(tracker->samples_ns, next_cap * sizeof(*next_samples)); + if (next_samples == NULL) { + return -1; + } + tracker->samples_ns = next_samples; + tracker->sample_cap = next_cap; + } + tracker->samples_ns[tracker->sample_count++] = rtt_ns; + return 0; +} + +static int ping_tracker_observe_reply(ping_tracker_t *tracker, uint64_t seq, int64_t sent_ts_ns, int64_t received_ts_ns, int *disposition, int64_t *rtt_ns) { + pending_ping_t *prev = NULL; + pending_ping_t *pending; + + if (seq == 0 || seq > tracker->max_seq_sent) { + *disposition = 2; + *rtt_ns = 0; + return 0; + } + pending = ping_tracker_find_pending(tracker, seq, &prev); + if (pending == NULL) { + tracker->duplicates++; + *disposition = 1; + *rtt_ns = 0; + return 0; + } + if (prev == NULL) { + tracker->pending = pending->next; + } else { + prev->next = pending->next; + } + tracker->pending_count--; + free(pending); + + *rtt_ns = received_ts_ns - sent_ts_ns; + if (*rtt_ns < 0) { + *rtt_ns = 0; + } + if (ping_tracker_add_sample(tracker, *rtt_ns) != 0) { + return -1; + } + *disposition = 0; + return 0; +} + +static void ping_tracker_expire(ping_tracker_t *tracker, int64_t now_ns, FILE *out) { + pending_ping_t *prev = NULL; + pending_ping_t *cur = tracker->pending; + + while (cur != NULL) { + if (cur->deadline_ns <= now_ns) { + pending_ping_t *next = cur->next; + fprintf(out, "seq=%" PRIu64 " timeout\n", cur->seq); + if (prev == NULL) { + tracker->pending = next; + } else { + prev->next = next; + } + free(cur); + tracker->pending_count--; + cur = next; + continue; + } + prev = cur; + cur = cur->next; + } +} + +static int64_t ping_percentile_ns(const int64_t *sorted, size_t count, double percentile) { + size_t index; + double raw_index; + + if (count == 0) { + return 0; + } + if (percentile <= 0.0) { + return sorted[0]; + } + if (percentile >= 1.0) { + return sorted[count - 1]; + } + raw_index = percentile * (double) count; + index = (size_t) raw_index; + if ((double) index < raw_index) { + index++; + } + if (index > 0) { + index--; + } + if (index >= count) { + index = count - 1; + } + return sorted[index]; +} + +static void ping_print_summary(FILE *out, const char *target, const ping_tracker_t *tracker) { + int received = (int) tracker->sample_count; + double loss_pct = tracker->sent == 0 ? 0.0 : ((double) (tracker->sent - received) * 100.0 / (double) tracker->sent); + + fprintf(out, "--- %s udp ping statistics ---\n", target); + fprintf(out, "%d packets transmitted, %d received, %d duplicates, %.2f%% packet loss\n", tracker->sent, received, tracker->duplicates, loss_pct); + if (tracker->sample_count == 0) { + fprintf(out, "rtt min/avg/max/p50/p95/p99 = n/a/n/a/n/a/n/a/n/a/n/a, stddev=n/a\n"); + return; + } + + { + int64_t *sorted = (int64_t *) malloc(tracker->sample_count * sizeof(*sorted)); + size_t i; + double sum = 0.0; + double variance = 0.0; + double avg; + int64_t min_ns; + int64_t max_ns; + int64_t p50_ns; + int64_t p95_ns; + int64_t p99_ns; + + if (sorted == NULL) { + fprintf(out, "rtt summary unavailable: memory allocation failed\n"); + return; + } + memcpy(sorted, tracker->samples_ns, tracker->sample_count * sizeof(*sorted)); + qsort(sorted, tracker->sample_count, sizeof(*sorted), ping_compare_i64); + for (i = 0; i < tracker->sample_count; ++i) { + sum += (double) sorted[i]; + } + avg = sum / (double) tracker->sample_count; + for (i = 0; i < tracker->sample_count; ++i) { + double delta = (double) sorted[i] - avg; + variance += delta * delta; + } + variance /= (double) tracker->sample_count; + + min_ns = sorted[0]; + max_ns = sorted[tracker->sample_count - 1]; + p50_ns = ping_percentile_ns(sorted, tracker->sample_count, 0.50); + p95_ns = ping_percentile_ns(sorted, tracker->sample_count, 0.95); + p99_ns = ping_percentile_ns(sorted, tracker->sample_count, 0.99); + + fprintf( + out, + "rtt min/avg/max/p50/p95/p99 = %.2fms/%.2fms/%.2fms/%.2fms/%.2fms/%.2fms, stddev=%.2fms\n", + (double) min_ns / 1000000.0, + avg / 1000000.0, + (double) max_ns / 1000000.0, + (double) p50_ns / 1000000.0, + (double) p95_ns / 1000000.0, + (double) p99_ns / 1000000.0, + ping_sqrt(variance) / 1000000.0 + ); + free(sorted); + } +} + +static int ping_expiry_poll_ms(int timeout_ms) { + int interval = timeout_ms / 4; + if (interval < 10) { + return 10; + } + if (interval > 100) { + return 100; + } + return interval; +} + +int main(int argc, char **argv) { + const char *peer_id = "pinger"; + const char *server_addr = "127.0.0.1:9001"; + const char *target_peer = ""; + const char *bind_ip = ""; + const char *latency_log_path = ""; + int echo_mode = 0; + int count = 100; + int interval_ms = 100; + int size = 64; + int timeout_ms = 3000; + latency_logger_t *latency_logger = NULL; + udp_client_t *client = NULL; + ping_receiver_ctx_t receiver_ctx; + pthread_t receiver_thread; + int receiver_ctx_initialized = 0; + int receiver_thread_started = 0; + ping_tracker_t tracker; + int i; + int rc = 1; + + ping_tracker_init(&tracker); + memset(&receiver_ctx, 0, sizeof(receiver_ctx)); + + for (i = 1; i < argc; ++i) { + const char *value = NULL; + int handled; + + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-id", &value)) < 0) { + fprintf(stderr, "udpping: flag -id requires a value\n"); + return 1; + } else if (handled) { + peer_id = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-server", &value)) < 0) { + fprintf(stderr, "udpping: flag -server requires a value\n"); + return 1; + } else if (handled) { + server_addr = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-to", &value)) < 0) { + fprintf(stderr, "udpping: flag -to requires a value\n"); + return 1; + } else if (handled) { + target_peer = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-count", &value)) < 0) { + fprintf(stderr, "udpping: flag -count requires a value\n"); + return 1; + } else if (handled) { + count = atoi(value); + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-interval", &value)) < 0) { + fprintf(stderr, "udpping: flag -interval requires a value\n"); + return 1; + } else if (handled) { + if (omni_parse_duration_ms(value, interval_ms, &interval_ms) != 0) { + fprintf(stderr, "udpping: invalid -interval value %s\n", value); + return 1; + } + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-size", &value)) < 0) { + fprintf(stderr, "udpping: flag -size requires a value\n"); + return 1; + } else if (handled) { + size = atoi(value); + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-timeout", &value)) < 0) { + fprintf(stderr, "udpping: flag -timeout requires a value\n"); + return 1; + } else if (handled) { + if (omni_parse_duration_ms(value, timeout_ms, &timeout_ms) != 0) { + fprintf(stderr, "udpping: invalid -timeout value %s\n", value); + return 1; + } + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-bind-ip", &value)) < 0) { + fprintf(stderr, "udpping: flag -bind-ip requires a value\n"); + return 1; + } else if (handled) { + bind_ip = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-latency-log", &value)) < 0) { + fprintf(stderr, "udpping: flag -latency-log requires a value\n"); + return 1; + } else if (handled) { + latency_log_path = value; + continue; + } + if ((handled = cli_parse_bool_flag(argv[i], "-echo", &echo_mode)) < 0) { + fprintf(stderr, "udpping: invalid -echo value\n"); + return 1; + } else if (handled) { + continue; + } + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + udpping_usage(stdout); + return 0; + } + fprintf(stderr, "udpping: unknown argument %s\n", argv[i]); + udpping_usage(stderr); + return 1; + } + + if (peer_id[0] == '\0' || server_addr[0] == '\0') { + fprintf(stderr, "udpping: flags -id and -server are required\n"); + return 1; + } + if (!echo_mode && target_peer[0] == '\0') { + fprintf(stderr, "udpping: flag -to is required unless -echo is set\n"); + return 1; + } + if (count < 0 || interval_ms <= 0 || size <= 0 || timeout_ms <= 0) { + fprintf(stderr, "udpping: invalid numeric flag value\n"); + return 1; + } + + signal(SIGINT, udpping_on_signal); + + if (latency_log_path[0] != '\0') { + latency_logger = latencylog_open_jsonl(latency_log_path); + if (latency_logger == NULL) { + fprintf(stderr, "udpping: open latency logger %s failed\n", latency_log_path); + goto cleanup; + } + } + client = udp_client_dial(server_addr, peer_id, bind_ip, latency_logger, NULL, 0); + if (client == NULL) { + fprintf(stderr, "udpping: dial udp server %s failed\n", server_addr); + goto cleanup; + } + + if (echo_mode) { + while (!g_udpping_stop) { + message_t msg; + + protocol_message_init(&msg); + if (udp_client_receive(client, &msg) != 0) { + protocol_message_clear(&msg); + if (g_udpping_stop) { + break; + } + fprintf(stderr, "udpping: receive failed in echo mode\n"); + goto cleanup; + } + if (msg.type == MSG_TYPE_TEXT) { + char *text = (char *) malloc(msg.body_len + 1U); + if (text == NULL) { + protocol_message_clear(&msg); + goto cleanup; + } + memcpy(text, msg.body, msg.body_len); + text[msg.body_len] = '\0'; + if (udp_client_send_text(client, msg.from, text) != 0) { + free(text); + protocol_message_clear(&msg); + fprintf(stderr, "udpping: echo send back to %s failed\n", msg.from); + goto cleanup; + } + free(text); + } else if (msg.type == MSG_TYPE_ERROR) { + fprintf(stderr, "server error: %.*s\n", (int) msg.body_len, msg.body == NULL ? "" : (const char *) msg.body); + } else { + fprintf(stderr, "unexpected message type %s from %s ignored\n", protocol_message_type_name(msg.type), msg.from); + } + protocol_message_clear(&msg); + } + rc = 0; + goto cleanup; + } + + fprintf(stdout, "UDP PING %s via %s (payload=%d bytes, UDP)\n", target_peer, server_addr, size); + ping_receiver_ctx_init(&receiver_ctx, client); + receiver_ctx_initialized = 1; + if (pthread_create(&receiver_thread, NULL, udpping_receive_thread_main, &receiver_ctx) != 0) { + fprintf(stderr, "udpping: create receive thread failed\n"); + goto cleanup; + } + receiver_thread_started = 1; + + { + uint64_t next_seq = 1; + int stop_sending = 0; + int64_t next_send_at_ns = omni_now_unix_nano(); + int poll_ms = ping_expiry_poll_ms(timeout_ms); + int64_t timeout_ns = (int64_t) timeout_ms * 1000000LL; + + while (!g_udpping_stop || tracker.pending_count > 0 || !stop_sending) { + int64_t now_ns = omni_now_unix_nano(); + message_t msg; + int popped; + int receiver_closed; + int receiver_status_rc; + + if (!stop_sending && now_ns >= next_send_at_ns) { + char *payload = NULL; + size_t payload_len = 0; + + if (count > 0 && tracker.sent >= count) { + stop_sending = 1; + } else { + if (ping_build_payload(next_seq, now_ns, size, &payload, &payload_len) != 0) { + fprintf(stderr, "udpping: build payload for seq=%" PRIu64 " failed\n", next_seq); + free(payload); + goto cleanup; + } + if (udp_client_send_text(client, target_peer, payload) != 0) { + fprintf(stderr, "udpping: send ping seq=%" PRIu64 " failed\n", next_seq); + free(payload); + goto cleanup; + } + free(payload); + if (ping_tracker_mark_sent(&tracker, next_seq, now_ns, timeout_ns) != 0) { + goto cleanup; + } + next_seq++; + next_send_at_ns = now_ns + (int64_t) interval_ms * 1000000LL; + if (count > 0 && tracker.sent >= count) { + stop_sending = 1; + } + } + } + + ping_tracker_expire(&tracker, now_ns, stdout); + + do { + popped = ping_receiver_pop(&receiver_ctx, &msg); + if (popped == 1) { + if (msg.type == MSG_TYPE_TEXT) { + uint64_t seq; + int64_t sent_ts_ns; + int disposition; + int64_t rtt_ns; + + if (ping_parse_payload(msg.body, msg.body_len, &seq, &sent_ts_ns) != 0) { + fprintf(stderr, "ignore non-ping text message from %s\n", msg.from); + } else if (ping_tracker_observe_reply(&tracker, seq, sent_ts_ns, omni_now_unix_nano(), &disposition, &rtt_ns) != 0) { + protocol_message_clear(&msg); + goto cleanup; + } else if (disposition == 0) { + fprintf(stdout, "seq=%" PRIu64 " rtt=%.2fms\n", seq, (double) rtt_ns / 1000000.0); + } else if (disposition == 1) { + fprintf(stderr, "seq=%" PRIu64 " duplicate or late reply ignored\n", seq); + } else { + fprintf(stderr, "seq=%" PRIu64 " unexpected reply ignored\n", seq); + } + } else if (msg.type == MSG_TYPE_ERROR) { + fprintf(stderr, "server error: %.*s\n", (int) msg.body_len, msg.body == NULL ? "" : (const char *) msg.body); + } else { + fprintf(stderr, "unexpected message type %s from %s ignored\n", protocol_message_type_name(msg.type), msg.from); + } + protocol_message_clear(&msg); + } + } while (popped == 1); + + ping_receiver_status(&receiver_ctx, &receiver_closed, &receiver_status_rc); + if (receiver_closed && receiver_status_rc != 0) { + fprintf(stderr, "udpping: receive loop failed\n"); + goto cleanup; + } + if ((g_udpping_stop || stop_sending) && tracker.pending_count == 0) { + break; + } + usleep((useconds_t) poll_ms * 1000U); + } + } + + ping_print_summary(stdout, target_peer, &tracker); + rc = 0; + +cleanup: + receiver_ctx.stop_requested = 1; + udp_client_close(client); + if (receiver_thread_started) { + pthread_join(receiver_thread, NULL); + ping_receiver_ctx_destroy(&receiver_ctx); + } else if (receiver_ctx_initialized) { + ping_receiver_ctx_destroy(&receiver_ctx); + } + udp_client_free(client); + latencylog_close(latency_logger); + ping_tracker_destroy(&tracker); + return rc; +} diff --git a/c/cmd/udprelay.c b/c/cmd/udprelay.c new file mode 100644 index 0000000..57cf5b2 --- /dev/null +++ b/c/cmd/udprelay.c @@ -0,0 +1,59 @@ +#include "cli_parse.h" +#include "server_udp_relay.h" + +static void udprelay_usage(FILE *out) { + fprintf(out, "usage: udprelay [-listen addr] [-upstream addr]\n"); +} + +int main(int argc, char **argv) { + const char *listen_addr = ":9003"; + const char *upstream_addr = "127.0.0.1:9002"; + udp_relay_t *relay = NULL; + int i; + int rc = 1; + + for (i = 1; i < argc; ++i) { + const char *value = NULL; + int handled; + + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-listen", &value)) < 0) { + fprintf(stderr, "udprelay: flag -listen requires a value\n"); + return 1; + } else if (handled) { + listen_addr = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-upstream", &value)) < 0) { + fprintf(stderr, "udprelay: flag -upstream requires a value\n"); + return 1; + } else if (handled) { + upstream_addr = value; + continue; + } + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + udprelay_usage(stdout); + return 0; + } + fprintf(stderr, "udprelay: unknown argument %s\n", argv[i]); + udprelay_usage(stderr); + return 1; + } + + relay = udp_relay_open(listen_addr, upstream_addr); + if (relay == NULL) { + fprintf(stderr, "udprelay: open relay %s -> %s failed\n", listen_addr, upstream_addr); + goto cleanup; + } + + fprintf(stderr, "udp relay listening on %s, upstream %s\n", listen_addr, upstream_addr); + if (udp_relay_serve(relay) != 0) { + fprintf(stderr, "udprelay: relay serve failed\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + udp_relay_free(relay); + return rc; +} diff --git a/c/cmd/udpserver.c b/c/cmd/udpserver.c new file mode 100644 index 0000000..978fd36 --- /dev/null +++ b/c/cmd/udpserver.c @@ -0,0 +1,88 @@ +#include "cli_parse.h" +#include "server_udp_hub.h" + +static void udpserver_usage(FILE *out) { + fprintf(out, "usage: udpserver [-listen addr] [-latency-log path] [-tx-ts-debug-log path]\n"); +} + +int main(int argc, char **argv) { + const char *listen_addr = ":9001"; + const char *latency_log_path = ""; + const char *tx_debug_log_path = ""; + latency_logger_t *latency_logger = NULL; + tx_timestamp_debug_logger_t *debug_logger = NULL; + udp_hub_t *hub = NULL; + int enable_timestamping = 0; + int i; + int rc = 1; + + for (i = 1; i < argc; ++i) { + const char *value = NULL; + int handled; + + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-listen", &value)) < 0) { + fprintf(stderr, "udpserver: flag -listen requires a value\n"); + return 1; + } else if (handled) { + listen_addr = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-latency-log", &value)) < 0) { + fprintf(stderr, "udpserver: flag -latency-log requires a value\n"); + return 1; + } else if (handled) { + latency_log_path = value; + continue; + } + if ((handled = cli_parse_value_flag(argc, argv, &i, argv[i], "-tx-ts-debug-log", &value)) < 0) { + fprintf(stderr, "udpserver: flag -tx-ts-debug-log requires a value\n"); + return 1; + } else if (handled) { + tx_debug_log_path = value; + continue; + } + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + udpserver_usage(stdout); + return 0; + } + fprintf(stderr, "udpserver: unknown argument %s\n", argv[i]); + udpserver_usage(stderr); + return 1; + } + + if (latency_log_path[0] != '\0') { + latency_logger = latencylog_open_jsonl(latency_log_path); + if (latency_logger == NULL) { + fprintf(stderr, "udpserver: open latency logger %s failed\n", latency_log_path); + goto cleanup; + } + } + if (tx_debug_log_path[0] != '\0') { + debug_logger = tx_timestamp_debug_open_jsonl(tx_debug_log_path); + if (debug_logger == NULL) { + fprintf(stderr, "udpserver: open tx timestamp debug logger %s failed\n", tx_debug_log_path); + goto cleanup; + } + enable_timestamping = 1; + } + + hub = udp_hub_open(listen_addr, latency_logger, debug_logger, enable_timestamping); + if (hub == NULL) { + fprintf(stderr, "udpserver: listen on %s failed\n", listen_addr); + goto cleanup; + } + + fprintf(stderr, "udp server listening on %s\n", listen_addr); + if (udp_hub_serve(hub) != 0) { + fprintf(stderr, "udpserver: serve failed\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + udp_hub_free(hub); + tx_timestamp_debug_close(debug_logger); + latencylog_close(latency_logger); + return rc; +} diff --git a/c/include/cli_parse.h b/c/include/cli_parse.h new file mode 100644 index 0000000..11bbf78 --- /dev/null +++ b/c/include/cli_parse.h @@ -0,0 +1,57 @@ +#ifndef OMNI_CLI_PARSE_H +#define OMNI_CLI_PARSE_H + +#include "omni_common.h" + +static int cli_parse_bool_text(const char *raw, int *out_value) { + if (raw == NULL || out_value == NULL) { + errno = EINVAL; + return -1; + } + if (strcmp(raw, "1") == 0 || strcmp(raw, "true") == 0 || strcmp(raw, "yes") == 0 || strcmp(raw, "on") == 0) { + *out_value = 1; + return 0; + } + if (strcmp(raw, "0") == 0 || strcmp(raw, "false") == 0 || strcmp(raw, "no") == 0 || strcmp(raw, "off") == 0) { + *out_value = 0; + return 0; + } + errno = EINVAL; + return -1; +} + +static int cli_parse_value_flag(int argc, char **argv, int *index, const char *arg, const char *flag, const char **out_value) { + size_t flag_len = strlen(flag); + + if (strcmp(arg, flag) == 0) { + if (*index + 1 >= argc) { + errno = EINVAL; + return -1; + } + *out_value = argv[++(*index)]; + return 1; + } + if (strncmp(arg, flag, flag_len) == 0 && arg[flag_len] == '=') { + *out_value = arg + flag_len + 1; + return 1; + } + return 0; +} + +static int cli_parse_bool_flag(const char *arg, const char *flag, int *out_value) { + size_t flag_len = strlen(flag); + + if (strcmp(arg, flag) == 0) { + *out_value = 1; + return 1; + } + if (strncmp(arg, flag, flag_len) == 0 && arg[flag_len] == '=') { + if (cli_parse_bool_text(arg + flag_len + 1, out_value) != 0) { + return -1; + } + return 1; + } + return 0; +} + +#endif diff --git a/c/include/interactive.h b/c/include/interactive.h new file mode 100644 index 0000000..775f456 --- /dev/null +++ b/c/include/interactive.h @@ -0,0 +1,30 @@ +#ifndef OMNI_INTERACTIVE_H +#define OMNI_INTERACTIVE_H + +#include "omni_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum interactive_command_type { + INTERACTIVE_CMD_HELP = 0, + INTERACTIVE_CMD_QUIT = 1, + INTERACTIVE_CMD_TEXT = 2, + INTERACTIVE_CMD_FILE = 3 +} interactive_command_type_t; + +typedef struct interactive_command { + interactive_command_type_t type; + char to[OMNI_MAX_PEER_ID]; + char value[1024]; +} interactive_command_t; + +int interactive_parse_command(const char *line, interactive_command_t *command, char *err, size_t err_len); +void interactive_print_help(FILE *out, const char *transport_name); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/kcp_packet_debug.h b/c/include/kcp_packet_debug.h new file mode 100644 index 0000000..45632ee --- /dev/null +++ b/c/include/kcp_packet_debug.h @@ -0,0 +1,49 @@ +#ifndef OMNI_KCP_PACKET_DEBUG_H +#define OMNI_KCP_PACKET_DEBUG_H + +#include "omni_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct kcp_packet_debug_segment { + uint8_t cmd; + uint32_t sn; + uint32_t una; + uint8_t frg; + uint16_t wnd; + uint32_t len; +} kcp_packet_debug_segment_t; + +typedef struct kcp_packet_debug_record { + char event[OMNI_MAX_EVENT_NAME]; + char node_role[OMNI_MAX_NODE_ROLE]; + char node_id[OMNI_MAX_PEER_ID]; + char local_addr[OMNI_MAX_ADDR_TEXT]; + char remote_addr[OMNI_MAX_ADDR_TEXT]; + int packet_bytes; + int has_udp_tx_id; + uint32_t udp_tx_id; + int has_kcp_conv; + uint32_t kcp_conv; + int64_t ts_unix_nano; + kcp_packet_debug_segment_t *segments; + size_t segment_count; +} kcp_packet_debug_record_t; + +typedef struct kcp_packet_debug_logger { + omni_file_logger_t file_logger; + int enabled; +} kcp_packet_debug_logger_t; + +kcp_packet_debug_logger_t *kcp_packet_debug_open_jsonl(const char *path); +void kcp_packet_debug_close(kcp_packet_debug_logger_t *logger); +int kcp_packet_debug_log(kcp_packet_debug_logger_t *logger, const kcp_packet_debug_record_t *record); +void kcp_packet_debug_record_clear(kcp_packet_debug_record_t *record); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/kcp_session_stats.h b/c/include/kcp_session_stats.h new file mode 100644 index 0000000..d95f01e --- /dev/null +++ b/c/include/kcp_session_stats.h @@ -0,0 +1,78 @@ +#ifndef OMNI_KCP_SESSION_STATS_H +#define OMNI_KCP_SESSION_STATS_H + +#include "omni_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define KCP_SESSION_STATS_RECORD_SESSION_SAMPLE "session_sample" +#define KCP_SESSION_STATS_RECORD_PROCESS_SAMPLE "process_snmp_sample" + +typedef struct kcp_session_stats_record { + char record_type[32]; + char node_role[OMNI_MAX_NODE_ROLE]; + char node_id[OMNI_MAX_PEER_ID]; + char local_addr[OMNI_MAX_ADDR_TEXT]; + char remote_addr[OMNI_MAX_ADDR_TEXT]; + int has_conv; + uint32_t conv; + int64_t ts_unix_nano; + char sample_reason[32]; + int has_rto_ms; + uint32_t rto_ms; + int has_srtt_ms; + int32_t srtt_ms; + int has_srttvar_ms; + int32_t srttvar_ms; + int has_bytes_sent; + uint64_t bytes_sent; + int has_bytes_received; + uint64_t bytes_received; + int has_in_pkts; + uint64_t in_pkts; + int has_out_pkts; + uint64_t out_pkts; + int has_in_segs; + uint64_t in_segs; + int has_out_segs; + uint64_t out_segs; + int has_retrans_segs; + uint64_t retrans_segs; + int has_fast_retrans_segs; + uint64_t fast_retrans_segs; + int has_early_retrans_segs; + uint64_t early_retrans_segs; + int has_lost_segs; + uint64_t lost_segs; + int has_repeat_segs; + uint64_t repeat_segs; + int has_in_errs; + uint64_t in_errs; + int has_kcp_in_errs; + uint64_t kcp_in_errs; + int has_ring_buffer_snd_queue; + uint64_t ring_buffer_snd_queue; + int has_ring_buffer_rcv_queue; + uint64_t ring_buffer_rcv_queue; + int has_ring_buffer_snd_buffer; + uint64_t ring_buffer_snd_buffer; + int has_curr_estab; + uint64_t curr_estab; +} kcp_session_stats_record_t; + +typedef struct kcp_session_stats_logger { + omni_file_logger_t file_logger; + int enabled; +} kcp_session_stats_logger_t; + +kcp_session_stats_logger_t *kcp_session_stats_open_jsonl(const char *path); +void kcp_session_stats_close(kcp_session_stats_logger_t *logger); +int kcp_session_stats_log(kcp_session_stats_logger_t *logger, const kcp_session_stats_record_t *record); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/latencylog.h b/c/include/latencylog.h new file mode 100644 index 0000000..809f515 --- /dev/null +++ b/c/include/latencylog.h @@ -0,0 +1,51 @@ +#ifndef OMNI_LATENCYLOG_H +#define OMNI_LATENCYLOG_H + +#include "protocol.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define EVENT_A_APP_PREP_BEGIN "A_APP_PREP_BEGIN" +#define EVENT_A_TX_SCHED "A_TX_SCHED" +#define EVENT_A_TX_SOFTWARE "A_TX_SOFTWARE" +#define EVENT_A_TX_HARDWARE "A_TX_HARDWARE" +#define EVENT_B_RX_HARDWARE "B_RX_HARDWARE" +#define EVENT_B_RX_SOFTWARE "B_RX_SOFTWARE" +#define EVENT_B_APP_RECV "B_APP_RECV" +#define EVENT_B_PERSIST_BEGIN "B_PERSIST_BEGIN" +#define EVENT_B_PERSIST_END "B_PERSIST_END" +#define EVENT_SEND_HANDOFF_BEGIN "send_handoff_begin" +#define EVENT_SEND_HANDOFF_END "send_handoff_end" + +typedef struct latency_event { + int64_t ts_unix_nano; + char node_role[OMNI_MAX_NODE_ROLE]; + char node_id[OMNI_MAX_PEER_ID]; + char event[OMNI_MAX_EVENT_NAME]; + message_type_t message_type; + uint64_t message_id; + char from[OMNI_MAX_PEER_ID]; + char to[OMNI_MAX_PEER_ID]; + char file_name[OMNI_MAX_FILE_NAME]; + int body_size; +} latency_event_t; + +typedef struct latency_logger { + omni_file_logger_t file_logger; + int enabled; +} latency_logger_t; + +latency_logger_t *latencylog_open_jsonl(const char *path); +void latencylog_close(latency_logger_t *logger); +int latencylog_log_event(latency_logger_t *logger, const latency_event_t *event); +int latencylog_is_business_message(const message_t *msg); +void latencylog_log_message_event(latency_logger_t *logger, const char *node_role, const char *node_id, const char *event_name, const message_t *msg); +void latencylog_log_message_event_at(latency_logger_t *logger, const char *node_role, const char *node_id, const char *event_name, int64_t ts_unix_nano, const message_t *msg); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/linux_timestamping.h b/c/include/linux_timestamping.h new file mode 100644 index 0000000..0cd2572 --- /dev/null +++ b/c/include/linux_timestamping.h @@ -0,0 +1,25 @@ +#ifndef OMNI_LINUX_TIMESTAMPING_H +#define OMNI_LINUX_TIMESTAMPING_H + +#include "omni_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct omni_tx_timestamp_event { + char event_name[OMNI_MAX_EVENT_NAME]; + int64_t ts_unix_nano; + uint32_t ee_info; + uint32_t ee_data; +} omni_tx_timestamp_event_t; + +int linux_timestamping_enable_udp_socket(int fd, int enable_rx); +int64_t linux_timestamping_parse_rx_timestamp(const struct msghdr *msg); +int linux_timestamping_parse_tx_timestamp(const struct msghdr *msg, omni_tx_timestamp_event_t *out_event); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/omni_common.h b/c/include/omni_common.h new file mode 100644 index 0000000..7f8bfbb --- /dev/null +++ b/c/include/omni_common.h @@ -0,0 +1,67 @@ +#ifndef OMNI_COMMON_H +#define OMNI_COMMON_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define OMNI_NODE_ROLE_PEER "peer" +#define OMNI_NODE_ROLE_SERVER "server" + +#define OMNI_MAX_PEER_ID 64 +#define OMNI_MAX_NODE_ROLE 16 +#define OMNI_MAX_EVENT_NAME 64 +#define OMNI_MAX_FILE_NAME 256 +#define OMNI_MAX_ADDR_TEXT 128 +#define OMNI_MAX_FRAME_SIZE (8U * 1024U * 1024U) + +#define OMNI_ARRAY_LEN(x) (sizeof(x) / sizeof((x)[0])) + +typedef struct omni_file_logger { + FILE *file; + pthread_mutex_t mutex; +} omni_file_logger_t; + +int64_t omni_now_unix_nano(void); +uint32_t omni_now_millis32(void); + +int omni_set_nonblocking(int fd, int enabled); +int omni_parse_sockaddr(const char *raw, int passive, struct sockaddr_storage *addr, socklen_t *addr_len, int *family_out); +int omni_clone_sockaddr(const struct sockaddr *src, socklen_t src_len, struct sockaddr_storage *dst, socklen_t *dst_len); +const char *omni_sockaddr_to_string(const struct sockaddr *addr, socklen_t addr_len, char *buffer, size_t buffer_len); + +int omni_bind_device(int fd, const char *device); +int omni_ensure_dir(const char *path); +int omni_ensure_parent_dir(const char *path); +int omni_read_file(const char *path, uint8_t **out, size_t *out_len); +int omni_write_full_fd(int fd, const uint8_t *data, size_t len); +int omni_append_file(const char *path, const uint8_t *data, size_t len); +int omni_write_file(const char *path, const uint8_t *data, size_t len); +int omni_random_u32(uint32_t *out); + +char *omni_strdup(const char *src); +char *omni_strdup_printf(const char *fmt, ...); +char *omni_json_escape(const char *src); +char *omni_json_escape_bytes(const uint8_t *src, size_t len); +int omni_utf8_valid(const uint8_t *data, size_t len); +void omni_trim_newline(char *line); +int omni_parse_duration_ms(const char *raw, int default_ms, int *out_ms); +double omni_duration_ms_to_ns(double ms); +const char *omni_path_base_name(const char *path); + +void omni_file_logger_init(omni_file_logger_t *logger, FILE *file); +void omni_file_logger_destroy(omni_file_logger_t *logger); +int omni_file_logger_write_line(omni_file_logger_t *logger, const char *line); + +#endif diff --git a/c/include/peer_kcp_client.h b/c/include/peer_kcp_client.h new file mode 100644 index 0000000..f1be8f8 --- /dev/null +++ b/c/include/peer_kcp_client.h @@ -0,0 +1,25 @@ +#ifndef OMNI_PEER_KCP_CLIENT_H +#define OMNI_PEER_KCP_CLIENT_H + +#include "transport_kcp.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct kcp_client kcp_client_t; + +kcp_client_t *kcp_client_dial(const char *server_addr, const char *dial_addr, const char *peer_id, const char *bind_ip, const char *bind_device, latency_logger_t *logger, kcp_packet_debug_logger_t *packet_logger, kcp_session_stats_logger_t *stats_logger, int stats_interval_ms); +const char *kcp_client_id(const kcp_client_t *client); +int kcp_client_send_text(kcp_client_t *client, const char *to, const char *text); +int kcp_client_send_file_path(kcp_client_t *client, const char *to, const char *path); +int kcp_client_receive(kcp_client_t *client, message_t *out_msg); +int kcp_client_persist_message(kcp_client_t *client, const message_t *msg, const char *inbox_dir, char *out_path, size_t out_path_len); +int kcp_client_close(kcp_client_t *client); +void kcp_client_free(kcp_client_t *client); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/peer_udp_client.h b/c/include/peer_udp_client.h new file mode 100644 index 0000000..fc8f64f --- /dev/null +++ b/c/include/peer_udp_client.h @@ -0,0 +1,25 @@ +#ifndef OMNI_PEER_UDP_CLIENT_H +#define OMNI_PEER_UDP_CLIENT_H + +#include "transport_udp.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct udp_client udp_client_t; + +udp_client_t *udp_client_dial(const char *server_addr, const char *peer_id, const char *bind_ip, latency_logger_t *logger, tx_timestamp_debug_logger_t *debug_logger, int enable_timestamping); +const char *udp_client_id(const udp_client_t *client); +int udp_client_send_text(udp_client_t *client, const char *to, const char *text); +int udp_client_send_file_path(udp_client_t *client, const char *to, const char *path); +int udp_client_receive(udp_client_t *client, message_t *out_msg); +int udp_client_persist_message(udp_client_t *client, const message_t *msg, const char *inbox_dir, char *out_path, size_t out_path_len); +int udp_client_close(udp_client_t *client); +void udp_client_free(udp_client_t *client); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/protocol.h b/c/include/protocol.h new file mode 100644 index 0000000..82560c6 --- /dev/null +++ b/c/include/protocol.h @@ -0,0 +1,61 @@ +#ifndef OMNI_PROTOCOL_H +#define OMNI_PROTOCOL_H + +#include "omni_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum message_type { + MSG_TYPE_TEXT = 0, + MSG_TYPE_FILE = 1, + MSG_TYPE_REGISTER = 2, + MSG_TYPE_ERROR = 3, + MSG_TYPE_INVALID = 255 +} message_type_t; + +#define SERVER_PEER_ID "server" + +typedef struct message { + message_type_t type; + uint64_t id; + char from[OMNI_MAX_PEER_ID]; + char to[OMNI_MAX_PEER_ID]; + char file_name[OMNI_MAX_FILE_NAME]; + uint8_t *body; + size_t body_len; +} message_t; + +typedef struct protocol_frame_decoder { + uint8_t *buffer; + size_t len; + size_t cap; +} protocol_frame_decoder_t; + +const char *protocol_message_type_name(message_type_t type); +int protocol_message_type_from_name(const char *raw, message_type_t *out); + +void protocol_message_init(message_t *msg); +void protocol_message_clear(message_t *msg); +int protocol_message_copy(message_t *dst, const message_t *src); + +int protocol_validate_message(const message_t *msg, char *err, size_t err_len); + +int protocol_encode_message_datagram(const message_t *msg, uint8_t **out, size_t *out_len); +int protocol_decode_message_datagram(const uint8_t *data, size_t data_len, message_t *out_msg, char *err, size_t err_len); + +int protocol_encode_message_stream(const message_t *msg, uint8_t **out, size_t *out_len); +int protocol_decode_message_stream_payload(const uint8_t *payload, size_t payload_len, message_t *out_msg, char *err, size_t err_len); + +void protocol_frame_decoder_init(protocol_frame_decoder_t *decoder); +void protocol_frame_decoder_reset(protocol_frame_decoder_t *decoder); +void protocol_frame_decoder_destroy(protocol_frame_decoder_t *decoder); +int protocol_frame_decoder_feed(protocol_frame_decoder_t *decoder, const uint8_t *data, size_t data_len); +int protocol_frame_decoder_next(protocol_frame_decoder_t *decoder, uint8_t **payload, size_t *payload_len); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/server_kcp_hub.h b/c/include/server_kcp_hub.h new file mode 100644 index 0000000..dd4117f --- /dev/null +++ b/c/include/server_kcp_hub.h @@ -0,0 +1,26 @@ +#ifndef OMNI_SERVER_KCP_HUB_H +#define OMNI_SERVER_KCP_HUB_H + +#include "transport_kcp.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct kcp_hub kcp_hub_t; + +kcp_hub_t *kcp_hub_new(latency_logger_t *logger, kcp_session_stats_logger_t *stats_logger, int stats_interval_ms); +int kcp_hub_serve_listener(kcp_hub_t *hub, kcp_listener_t *listener); +int kcp_hub_serve_session(kcp_hub_t *hub, kcp_conn_t *conn); + +int kcp_hub_set_relay(kcp_hub_t *hub, int relay_fd, const struct sockaddr *peer_addr, socklen_t peer_addr_len, int learn_peer); +int kcp_hub_serve_relay(kcp_hub_t *hub); + +int kcp_hub_close(kcp_hub_t *hub); +void kcp_hub_free(kcp_hub_t *hub); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/server_udp_hub.h b/c/include/server_udp_hub.h new file mode 100644 index 0000000..7baed3c --- /dev/null +++ b/c/include/server_udp_hub.h @@ -0,0 +1,21 @@ +#ifndef OMNI_SERVER_UDP_HUB_H +#define OMNI_SERVER_UDP_HUB_H + +#include "transport_udp.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct udp_hub udp_hub_t; + +udp_hub_t *udp_hub_open(const char *listen_addr, latency_logger_t *logger, tx_timestamp_debug_logger_t *debug_logger, int enable_timestamping); +int udp_hub_serve(udp_hub_t *hub); +int udp_hub_close(udp_hub_t *hub); +void udp_hub_free(udp_hub_t *hub); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/server_udp_relay.h b/c/include/server_udp_relay.h new file mode 100644 index 0000000..1c7728e --- /dev/null +++ b/c/include/server_udp_relay.h @@ -0,0 +1,21 @@ +#ifndef OMNI_SERVER_UDP_RELAY_H +#define OMNI_SERVER_UDP_RELAY_H + +#include "omni_common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct udp_relay udp_relay_t; + +udp_relay_t *udp_relay_open(const char *listen_addr, const char *upstream_addr); +int udp_relay_serve(udp_relay_t *relay); +int udp_relay_close(udp_relay_t *relay); +void udp_relay_free(udp_relay_t *relay); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/transport_kcp.h b/c/include/transport_kcp.h new file mode 100644 index 0000000..202c287 --- /dev/null +++ b/c/include/transport_kcp.h @@ -0,0 +1,43 @@ +#ifndef OMNI_TRANSPORT_KCP_H +#define OMNI_TRANSPORT_KCP_H + +#include "kcp_packet_debug.h" +#include "kcp_session_stats.h" +#include "latencylog.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define KCP_NODELAY 1 +#define KCP_INTERVAL 10 +#define KCP_RESEND 2 +#define KCP_NC 1 +#define KCP_WND_SIZE 256 +#define KCP_MTU 1400 +#define KCP_DEFAULT_STATS_INTERVAL_MS 100 + +typedef struct kcp_conn kcp_conn_t; +typedef struct kcp_listener kcp_listener_t; + +kcp_conn_t *kcp_conn_dial(const char *server_addr, const char *bind_ip, const char *bind_device, kcp_packet_debug_logger_t *packet_logger, latency_logger_t *logger, const char *node_role, const char *node_id, kcp_session_stats_logger_t *stats_logger, int stats_interval_ms); +int kcp_conn_configure_runtime(kcp_conn_t *conn, latency_logger_t *logger, const char *node_role, const char *node_id, kcp_session_stats_logger_t *stats_logger, int stats_interval_ms); +int kcp_conn_send(kcp_conn_t *conn, const message_t *msg); +int kcp_conn_receive(kcp_conn_t *conn, message_t *out_msg); +int kcp_conn_close(kcp_conn_t *conn); +void kcp_conn_free(kcp_conn_t *conn); +uint32_t kcp_conn_conv(const kcp_conn_t *conn); +int kcp_conn_local_addr(const kcp_conn_t *conn, struct sockaddr_storage *addr, socklen_t *addr_len); + +kcp_listener_t *kcp_listener_listen(const char *listen_addr, const char *bind_device, kcp_packet_debug_logger_t *packet_logger, const char *node_role, const char *node_id); +kcp_conn_t *kcp_listener_accept(kcp_listener_t *listener); +int kcp_listener_close(kcp_listener_t *listener); +void kcp_listener_free(kcp_listener_t *listener); + +int kcp_session_stats_parse_interval_ms(const char *raw, int *out_ms); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/transport_udp.h b/c/include/transport_udp.h new file mode 100644 index 0000000..54e7155 --- /dev/null +++ b/c/include/transport_udp.h @@ -0,0 +1,30 @@ +#ifndef OMNI_TRANSPORT_UDP_H +#define OMNI_TRANSPORT_UDP_H + +#include "latencylog.h" +#include "linux_timestamping.h" +#include "tx_timestamp_debug.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct udp_conn udp_conn_t; + +udp_conn_t *udp_conn_dial(const char *server_addr, const char *bind_ip, const char *bind_device, int enable_timestamping, latency_logger_t *logger, const char *node_role, const char *node_id, tx_timestamp_debug_logger_t *debug_logger); +udp_conn_t *udp_conn_bind(const char *listen_addr, const char *bind_device, int enable_timestamping, latency_logger_t *logger, const char *node_role, const char *node_id, tx_timestamp_debug_logger_t *debug_logger); + +int udp_conn_send(udp_conn_t *conn, const message_t *msg); +int udp_conn_send_to(udp_conn_t *conn, const message_t *msg, const struct sockaddr *addr, socklen_t addr_len); +int udp_conn_receive(udp_conn_t *conn, message_t *out_msg, struct sockaddr_storage *addr, socklen_t *addr_len); + +int udp_conn_fd(const udp_conn_t *conn); +int udp_conn_local_addr(const udp_conn_t *conn, struct sockaddr_storage *addr, socklen_t *addr_len); +int udp_conn_close(udp_conn_t *conn); +void udp_conn_free(udp_conn_t *conn); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/include/tx_timestamp_debug.h b/c/include/tx_timestamp_debug.h new file mode 100644 index 0000000..c5795ca --- /dev/null +++ b/c/include/tx_timestamp_debug.h @@ -0,0 +1,51 @@ +#ifndef OMNI_TX_TIMESTAMP_DEBUG_H +#define OMNI_TX_TIMESTAMP_DEBUG_H + +#include "protocol.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define TX_TIMESTAMP_DEBUG_RECORD_SEND_CHUNK "send_chunk" +#define TX_TIMESTAMP_DEBUG_RECORD_ERRQUEUE_EVENT "errqueue_event" + +typedef struct tx_timestamp_debug_record { + char record_type[32]; + char node_role[OMNI_MAX_NODE_ROLE]; + char node_id[OMNI_MAX_PEER_ID]; + message_type_t message_type; + uint64_t message_id; + char from[OMNI_MAX_PEER_ID]; + char to[OMNI_MAX_PEER_ID]; + char file_name[OMNI_MAX_FILE_NAME]; + int body_size; + char phase[32]; + int send_call_index; + int frame_offset_start; + int frame_offset_end; + int bytes_written; + uint32_t expected_tx_id; + int read_index; + char event_name[OMNI_MAX_EVENT_NAME]; + int64_t ts_unix_nano; + uint32_t ee_info; + uint32_t ee_data; + int matched_send_call_index; + int selected_for_latency; +} tx_timestamp_debug_record_t; + +typedef struct tx_timestamp_debug_logger { + omni_file_logger_t file_logger; + int enabled; +} tx_timestamp_debug_logger_t; + +tx_timestamp_debug_logger_t *tx_timestamp_debug_open_jsonl(const char *path); +void tx_timestamp_debug_close(tx_timestamp_debug_logger_t *logger); +int tx_timestamp_debug_log(tx_timestamp_debug_logger_t *logger, const tx_timestamp_debug_record_t *record); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/c/src/interactive.c b/c/src/interactive.c new file mode 100644 index 0000000..14f5a89 --- /dev/null +++ b/c/src/interactive.c @@ -0,0 +1,77 @@ +#include "interactive.h" + +#include + +static void interactive_skip_spaces(const char **cursor) { + while (**cursor != '\0' && isspace((unsigned char) **cursor)) { + (*cursor)++; + } +} + +int interactive_parse_command(const char *line, interactive_command_t *command, char *err, size_t err_len) { + const char *cursor = line; + char action[16]; + size_t action_len = 0; + size_t to_len = 0; + size_t value_len; + + if (line == NULL || command == NULL) { + snprintf(err, err_len, "interactive: invalid command"); + return -1; + } + memset(command, 0, sizeof(*command)); + interactive_skip_spaces(&cursor); + while (*cursor != '\0' && !isspace((unsigned char) *cursor) && action_len + 1 < sizeof(action)) { + action[action_len++] = *cursor++; + } + action[action_len] = '\0'; + if (action_len == 0) { + snprintf(err, err_len, "interactive: empty command"); + return -1; + } + if (strcmp(action, "help") == 0) { + command->type = INTERACTIVE_CMD_HELP; + return 0; + } + if (strcmp(action, "quit") == 0) { + command->type = INTERACTIVE_CMD_QUIT; + return 0; + } + + interactive_skip_spaces(&cursor); + while (*cursor != '\0' && !isspace((unsigned char) *cursor) && to_len + 1 < sizeof(command->to)) { + command->to[to_len++] = *cursor++; + } + command->to[to_len] = '\0'; + interactive_skip_spaces(&cursor); + if (command->to[0] == '\0' || *cursor == '\0') { + snprintf(err, err_len, "interactive: missing target or value"); + return -1; + } + + value_len = strlen(cursor); + if (value_len >= sizeof(command->value)) { + snprintf(err, err_len, "interactive: value too long"); + return -1; + } + snprintf(command->value, sizeof(command->value), "%s", cursor); + + if (strcmp(action, "text") == 0) { + command->type = INTERACTIVE_CMD_TEXT; + return 0; + } + if (strcmp(action, "file") == 0) { + command->type = INTERACTIVE_CMD_FILE; + return 0; + } + snprintf(err, err_len, "interactive: unknown command %s", action); + return -1; +} + +void interactive_print_help(FILE *out, const char *transport_name) { + fprintf(out, "interactive mode commands (%s):\n", transport_name); + fprintf(out, " help show this help\n"); + fprintf(out, " text send one text message\n"); + fprintf(out, " file send one file\n"); + fprintf(out, " quit exit this process\n"); +} diff --git a/c/src/kcp_packet_debug.c b/c/src/kcp_packet_debug.c new file mode 100644 index 0000000..33d958e --- /dev/null +++ b/c/src/kcp_packet_debug.c @@ -0,0 +1,166 @@ +#include "kcp_packet_debug.h" + +kcp_packet_debug_logger_t *kcp_packet_debug_open_jsonl(const char *path) { + kcp_packet_debug_logger_t *logger; + FILE *file; + if (path == NULL || path[0] == '\0') { + return NULL; + } + if (omni_ensure_parent_dir(path) != 0) { + return NULL; + } + file = fopen(path, "ab"); + if (file == NULL) { + return NULL; + } + logger = (kcp_packet_debug_logger_t *) calloc(1, sizeof(*logger)); + if (logger == NULL) { + fclose(file); + return NULL; + } + omni_file_logger_init(&logger->file_logger, file); + logger->enabled = 1; + return logger; +} + +void kcp_packet_debug_close(kcp_packet_debug_logger_t *logger) { + if (logger == NULL) { + return; + } + if (logger->file_logger.file != NULL) { + fclose(logger->file_logger.file); + } + omni_file_logger_destroy(&logger->file_logger); + free(logger); +} + +void kcp_packet_debug_record_clear(kcp_packet_debug_record_t *record) { + if (record == NULL) { + return; + } + free(record->segments); + memset(record, 0, sizeof(*record)); +} + +int kcp_packet_debug_log(kcp_packet_debug_logger_t *logger, const kcp_packet_debug_record_t *record) { + char *event = NULL; + char *node_role = NULL; + char *node_id = NULL; + char *local_addr = NULL; + char *remote_addr = NULL; + char *segments_json = NULL; + char *tx_id_text = NULL; + char *conv_text = NULL; + char *line = NULL; + size_t i; + size_t cap = 128U; + size_t len = 0U; + + if (logger == NULL || record == NULL || !logger->enabled) { + return 0; + } + + event = omni_json_escape(record->event); + node_role = omni_json_escape(record->node_role); + node_id = omni_json_escape(record->node_id); + local_addr = omni_json_escape(record->local_addr); + remote_addr = omni_json_escape(record->remote_addr); + if (event == NULL || node_role == NULL || node_id == NULL || local_addr == NULL || remote_addr == NULL) { + free(event); + free(node_role); + free(node_id); + free(local_addr); + free(remote_addr); + return -1; + } + + segments_json = (char *) malloc(cap); + if (segments_json == NULL) { + free(event); + free(node_role); + free(node_id); + free(local_addr); + free(remote_addr); + return -1; + } + segments_json[len++] = '['; + for (i = 0; i < record->segment_count; ++i) { + int written; + while (len + 96U > cap) { + char *next = (char *) realloc(segments_json, cap * 2U); + if (next == NULL) { + free(event); + free(node_role); + free(node_id); + free(local_addr); + free(remote_addr); + free(segments_json); + return -1; + } + segments_json = next; + cap *= 2U; + } + written = snprintf( + segments_json + len, + cap - len, + "%s{\"cmd\":%u,\"sn\":%u,\"una\":%u,\"frg\":%u,\"wnd\":%u,\"len\":%u}", + i == 0 ? "" : ",", + record->segments[i].cmd, + record->segments[i].sn, + record->segments[i].una, + record->segments[i].frg, + record->segments[i].wnd, + record->segments[i].len + ); + len += (size_t) written; + } + segments_json[len++] = ']'; + segments_json[len] = '\0'; + + tx_id_text = record->has_udp_tx_id ? omni_strdup_printf("%u", record->udp_tx_id) : omni_strdup("null"); + conv_text = record->has_kcp_conv ? omni_strdup_printf("%u", record->kcp_conv) : omni_strdup("null"); + if (tx_id_text == NULL || conv_text == NULL) { + free(event); + free(node_role); + free(node_id); + free(local_addr); + free(remote_addr); + free(segments_json); + free(tx_id_text); + free(conv_text); + return -1; + } + + line = omni_strdup_printf( + "{\"event\":\"%s\",\"node_role\":\"%s\",\"node_id\":\"%s\",\"local_addr\":\"%s\",\"remote_addr\":\"%s\",\"packet_bytes\":%d,\"udp_tx_id\":%s,\"kcp_conv\":%s,\"segments\":%s,\"ts_unix_nano\":%" PRId64 "}", + event, + node_role, + node_id, + local_addr, + remote_addr, + record->packet_bytes, + tx_id_text, + conv_text, + segments_json, + record->ts_unix_nano + ); + + free(event); + free(node_role); + free(node_id); + free(local_addr); + free(remote_addr); + free(segments_json); + free(tx_id_text); + free(conv_text); + + if (line == NULL) { + return -1; + } + if (omni_file_logger_write_line(&logger->file_logger, line) != 0) { + free(line); + return -1; + } + free(line); + return 0; +} diff --git a/c/src/kcp_session_stats.c b/c/src/kcp_session_stats.c new file mode 100644 index 0000000..fee9cc3 --- /dev/null +++ b/c/src/kcp_session_stats.c @@ -0,0 +1,258 @@ +#include "kcp_session_stats.h" + +static int kcp_session_stats_append(char **line, size_t *len, const char *suffix) { + size_t suffix_len; + char *next; + + if (line == NULL || len == NULL || suffix == NULL) { + errno = EINVAL; + return -1; + } + suffix_len = strlen(suffix); + next = (char *) realloc(*line, *len + suffix_len + 1U); + if (next == NULL) { + return -1; + } + memcpy(next + *len, suffix, suffix_len + 1U); + *line = next; + *len += suffix_len; + return 0; +} + +static int kcp_session_stats_appendf(char **line, size_t *len, const char *fmt, ...) { + va_list args; + va_list copy; + int needed; + char *buffer; + + if (line == NULL || len == NULL || fmt == NULL) { + errno = EINVAL; + return -1; + } + + va_start(args, fmt); + va_copy(copy, args); + needed = vsnprintf(NULL, 0, fmt, copy); + va_end(copy); + if (needed < 0) { + va_end(args); + return -1; + } + + buffer = (char *) malloc((size_t) needed + 1U); + if (buffer == NULL) { + va_end(args); + return -1; + } + vsnprintf(buffer, (size_t) needed + 1U, fmt, args); + va_end(args); + + if (kcp_session_stats_append(line, len, buffer) != 0) { + free(buffer); + return -1; + } + free(buffer); + return 0; +} + +kcp_session_stats_logger_t *kcp_session_stats_open_jsonl(const char *path) { + kcp_session_stats_logger_t *logger; + FILE *file; + if (path == NULL || path[0] == '\0') { + return NULL; + } + if (omni_ensure_parent_dir(path) != 0) { + return NULL; + } + file = fopen(path, "ab"); + if (file == NULL) { + return NULL; + } + logger = (kcp_session_stats_logger_t *) calloc(1, sizeof(*logger)); + if (logger == NULL) { + fclose(file); + return NULL; + } + omni_file_logger_init(&logger->file_logger, file); + logger->enabled = 1; + return logger; +} + +void kcp_session_stats_close(kcp_session_stats_logger_t *logger) { + if (logger == NULL) { + return; + } + if (logger->file_logger.file != NULL) { + fclose(logger->file_logger.file); + } + omni_file_logger_destroy(&logger->file_logger); + free(logger); +} + +int kcp_session_stats_log(kcp_session_stats_logger_t *logger, const kcp_session_stats_record_t *record) { + char *record_type = NULL; + char *node_role = NULL; + char *node_id = NULL; + char *local_addr = NULL; + char *remote_addr = NULL; + char *sample_reason = NULL; + char *line = NULL; + size_t line_len = 0; + + if (logger == NULL || record == NULL || !logger->enabled) { + return 0; + } + record_type = omni_json_escape(record->record_type); + node_role = omni_json_escape(record->node_role); + node_id = omni_json_escape(record->node_id); + local_addr = omni_json_escape(record->local_addr); + remote_addr = omni_json_escape(record->remote_addr); + sample_reason = omni_json_escape(record->sample_reason); + if (record_type == NULL || node_role == NULL || node_id == NULL || local_addr == NULL || remote_addr == NULL || sample_reason == NULL) { + free(record_type); + free(node_role); + free(node_id); + free(local_addr); + free(remote_addr); + free(sample_reason); + return -1; + } + line = omni_strdup(""); + if (line == NULL) { + free(record_type); + free(node_role); + free(node_id); + free(local_addr); + free(remote_addr); + free(sample_reason); + return -1; + } + + if (kcp_session_stats_appendf(&line, &line_len, "{\"record_type\":\"%s\",\"node_role\":\"%s\",\"node_id\":\"%s\",\"ts_unix_nano\":%" PRId64 ",\"sample_reason\":\"%s\"", + record_type, + node_role, + node_id, + record->ts_unix_nano, + sample_reason) != 0) { + goto cleanup; + } + if (record->local_addr[0] != '\0' && + kcp_session_stats_appendf(&line, &line_len, ",\"local_addr\":\"%s\"", local_addr) != 0) { + goto cleanup; + } + if (record->remote_addr[0] != '\0' && + kcp_session_stats_appendf(&line, &line_len, ",\"remote_addr\":\"%s\"", remote_addr) != 0) { + goto cleanup; + } + if (record->has_conv && + kcp_session_stats_appendf(&line, &line_len, ",\"conv\":%u", record->conv) != 0) { + goto cleanup; + } + if (record->has_rto_ms && + kcp_session_stats_appendf(&line, &line_len, ",\"rto_ms\":%u", record->rto_ms) != 0) { + goto cleanup; + } + if (record->has_srtt_ms && + kcp_session_stats_appendf(&line, &line_len, ",\"srtt_ms\":%d", record->srtt_ms) != 0) { + goto cleanup; + } + if (record->has_srttvar_ms && + kcp_session_stats_appendf(&line, &line_len, ",\"srttvar_ms\":%d", record->srttvar_ms) != 0) { + goto cleanup; + } + if (record->has_bytes_sent && + kcp_session_stats_appendf(&line, &line_len, ",\"bytes_sent\":%" PRIu64, record->bytes_sent) != 0) { + goto cleanup; + } + if (record->has_bytes_received && + kcp_session_stats_appendf(&line, &line_len, ",\"bytes_received\":%" PRIu64, record->bytes_received) != 0) { + goto cleanup; + } + if (record->has_in_pkts && + kcp_session_stats_appendf(&line, &line_len, ",\"in_pkts\":%" PRIu64, record->in_pkts) != 0) { + goto cleanup; + } + if (record->has_out_pkts && + kcp_session_stats_appendf(&line, &line_len, ",\"out_pkts\":%" PRIu64, record->out_pkts) != 0) { + goto cleanup; + } + if (record->has_in_segs && + kcp_session_stats_appendf(&line, &line_len, ",\"in_segs\":%" PRIu64, record->in_segs) != 0) { + goto cleanup; + } + if (record->has_out_segs && + kcp_session_stats_appendf(&line, &line_len, ",\"out_segs\":%" PRIu64, record->out_segs) != 0) { + goto cleanup; + } + if (record->has_retrans_segs && + kcp_session_stats_appendf(&line, &line_len, ",\"retrans_segs\":%" PRIu64, record->retrans_segs) != 0) { + goto cleanup; + } + if (record->has_fast_retrans_segs && + kcp_session_stats_appendf(&line, &line_len, ",\"fast_retrans_segs\":%" PRIu64, record->fast_retrans_segs) != 0) { + goto cleanup; + } + if (record->has_early_retrans_segs && + kcp_session_stats_appendf(&line, &line_len, ",\"early_retrans_segs\":%" PRIu64, record->early_retrans_segs) != 0) { + goto cleanup; + } + if (record->has_lost_segs && + kcp_session_stats_appendf(&line, &line_len, ",\"lost_segs\":%" PRIu64, record->lost_segs) != 0) { + goto cleanup; + } + if (record->has_repeat_segs && + kcp_session_stats_appendf(&line, &line_len, ",\"repeat_segs\":%" PRIu64, record->repeat_segs) != 0) { + goto cleanup; + } + if (record->has_in_errs && + kcp_session_stats_appendf(&line, &line_len, ",\"in_errs\":%" PRIu64, record->in_errs) != 0) { + goto cleanup; + } + if (record->has_kcp_in_errs && + kcp_session_stats_appendf(&line, &line_len, ",\"kcp_in_errs\":%" PRIu64, record->kcp_in_errs) != 0) { + goto cleanup; + } + if (record->has_ring_buffer_snd_queue && + kcp_session_stats_appendf(&line, &line_len, ",\"ring_buffer_snd_queue\":%" PRIu64, record->ring_buffer_snd_queue) != 0) { + goto cleanup; + } + if (record->has_ring_buffer_rcv_queue && + kcp_session_stats_appendf(&line, &line_len, ",\"ring_buffer_rcv_queue\":%" PRIu64, record->ring_buffer_rcv_queue) != 0) { + goto cleanup; + } + if (record->has_ring_buffer_snd_buffer && + kcp_session_stats_appendf(&line, &line_len, ",\"ring_buffer_snd_buffer\":%" PRIu64, record->ring_buffer_snd_buffer) != 0) { + goto cleanup; + } + if (record->has_curr_estab && + kcp_session_stats_appendf(&line, &line_len, ",\"curr_estab\":%" PRIu64, record->curr_estab) != 0) { + goto cleanup; + } + if (kcp_session_stats_append(&line, &line_len, "}") != 0) { + goto cleanup; + } + + free(record_type); + free(node_role); + free(node_id); + free(local_addr); + free(remote_addr); + free(sample_reason); + + if (omni_file_logger_write_line(&logger->file_logger, line) != 0) { + free(line); + return -1; + } + free(line); + return 0; + +cleanup: + free(record_type); + free(node_role); + free(node_id); + free(local_addr); + free(remote_addr); + free(sample_reason); + free(line); + return -1; +} diff --git a/c/src/latencylog.c b/c/src/latencylog.c new file mode 100644 index 0000000..147e0ec --- /dev/null +++ b/c/src/latencylog.c @@ -0,0 +1,130 @@ +#include "latencylog.h" + +static void latencylog_fill_event(latency_event_t *event, const char *node_role, const char *node_id, const char *event_name, int64_t ts_unix_nano, const message_t *msg) { + memset(event, 0, sizeof(*event)); + event->ts_unix_nano = ts_unix_nano; + snprintf(event->node_role, sizeof(event->node_role), "%s", node_role == NULL ? "" : node_role); + snprintf(event->node_id, sizeof(event->node_id), "%s", node_id == NULL ? "" : node_id); + snprintf(event->event, sizeof(event->event), "%s", event_name == NULL ? "" : event_name); + event->message_type = msg->type; + event->message_id = msg->id; + snprintf(event->from, sizeof(event->from), "%s", msg->from); + snprintf(event->to, sizeof(event->to), "%s", msg->to); + snprintf(event->file_name, sizeof(event->file_name), "%s", msg->file_name); + event->body_size = (int) msg->body_len; +} + +latency_logger_t *latencylog_open_jsonl(const char *path) { + latency_logger_t *logger; + FILE *file; + if (path == NULL || path[0] == '\0') { + return NULL; + } + if (omni_ensure_parent_dir(path) != 0) { + return NULL; + } + file = fopen(path, "ab"); + if (file == NULL) { + return NULL; + } + logger = (latency_logger_t *) calloc(1, sizeof(*logger)); + if (logger == NULL) { + fclose(file); + return NULL; + } + omni_file_logger_init(&logger->file_logger, file); + logger->enabled = 1; + return logger; +} + +void latencylog_close(latency_logger_t *logger) { + if (logger == NULL) { + return; + } + if (logger->file_logger.file != NULL) { + fclose(logger->file_logger.file); + } + omni_file_logger_destroy(&logger->file_logger); + free(logger); +} + +int latencylog_log_event(latency_logger_t *logger, const latency_event_t *event) { + char *node_role = NULL; + char *node_id = NULL; + char *event_name = NULL; + char *from = NULL; + char *to = NULL; + char *file_name = NULL; + char *line = NULL; + + if (logger == NULL || event == NULL || !logger->enabled) { + return 0; + } + + node_role = omni_json_escape(event->node_role); + node_id = omni_json_escape(event->node_id); + event_name = omni_json_escape(event->event); + from = omni_json_escape(event->from); + to = omni_json_escape(event->to); + file_name = omni_json_escape(event->file_name); + if (node_role == NULL || node_id == NULL || event_name == NULL || from == NULL || to == NULL || file_name == NULL) { + free(node_role); + free(node_id); + free(event_name); + free(from); + free(to); + free(file_name); + return -1; + } + + line = omni_strdup_printf( + "{\"ts_unix_nano\":%" PRId64 ",\"node_role\":\"%s\",\"node_id\":\"%s\",\"event\":\"%s\",\"message_type\":\"%s\",\"message_id\":%" PRIu64 ",\"from\":\"%s\",\"to\":\"%s\",\"file_name\":\"%s\",\"body_size\":%d}", + event->ts_unix_nano, + node_role, + node_id, + event_name, + protocol_message_type_name(event->message_type), + event->message_id, + from, + to, + file_name, + event->body_size + ); + + free(node_role); + free(node_id); + free(event_name); + free(from); + free(to); + free(file_name); + + if (line == NULL) { + return -1; + } + if (omni_file_logger_write_line(&logger->file_logger, line) != 0) { + free(line); + return -1; + } + free(line); + return 0; +} + +int latencylog_is_business_message(const message_t *msg) { + if (msg == NULL) { + return 0; + } + return msg->type == MSG_TYPE_TEXT || msg->type == MSG_TYPE_FILE; +} + +void latencylog_log_message_event(latency_logger_t *logger, const char *node_role, const char *node_id, const char *event_name, const message_t *msg) { + latencylog_log_message_event_at(logger, node_role, node_id, event_name, omni_now_unix_nano(), msg); +} + +void latencylog_log_message_event_at(latency_logger_t *logger, const char *node_role, const char *node_id, const char *event_name, int64_t ts_unix_nano, const message_t *msg) { + latency_event_t event; + if (!latencylog_is_business_message(msg)) { + return; + } + latencylog_fill_event(&event, node_role, node_id, event_name, ts_unix_nano, msg); + (void) latencylog_log_event(logger, &event); +} diff --git a/c/src/linux_timestamping.c b/c/src/linux_timestamping.c new file mode 100644 index 0000000..0a5c910 --- /dev/null +++ b/c/src/linux_timestamping.c @@ -0,0 +1,103 @@ +#include "linux_timestamping.h" +#include "latencylog.h" + +#ifdef __linux__ +#include +#include +#include +#include + +static int64_t linux_timespec_to_ns(const struct timespec *ts) { + if (ts == NULL) { + return 0; + } + return (int64_t) ts->tv_sec * 1000000000LL + ts->tv_nsec; +} + +int linux_timestamping_enable_udp_socket(int fd, int enable_rx) { + int flags = SOF_TIMESTAMPING_SOFTWARE | SOF_TIMESTAMPING_TX_SCHED | SOF_TIMESTAMPING_TX_SOFTWARE | SOF_TIMESTAMPING_OPT_ID | SOF_TIMESTAMPING_OPT_TSONLY; + if (enable_rx) { + flags |= SOF_TIMESTAMPING_RX_SOFTWARE; + } + return setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags)); +} + +int64_t linux_timestamping_parse_rx_timestamp(const struct msghdr *msg) { + struct cmsghdr *cmsg; + const struct scm_timestamping *timestamps; + if (msg == NULL) { + return 0; + } + for (cmsg = CMSG_FIRSTHDR((struct msghdr *) msg); cmsg != NULL; cmsg = CMSG_NXTHDR((struct msghdr *) msg, cmsg)) { + if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPING) { + timestamps = (const struct scm_timestamping *) CMSG_DATA(cmsg); + if (timestamps->ts[0].tv_sec != 0 || timestamps->ts[0].tv_nsec != 0) { + return linux_timespec_to_ns(×tamps->ts[0]); + } + } + } + return 0; +} + +int linux_timestamping_parse_tx_timestamp(const struct msghdr *msg, omni_tx_timestamp_event_t *out_event) { + struct cmsghdr *cmsg; + const struct scm_timestamping *timestamps = NULL; + const struct sock_extended_err *sock_err = NULL; + int64_t timestamp_ns = 0; + + if (msg == NULL || out_event == NULL) { + errno = EINVAL; + return -1; + } + memset(out_event, 0, sizeof(*out_event)); + + for (cmsg = CMSG_FIRSTHDR((struct msghdr *) msg); cmsg != NULL; cmsg = CMSG_NXTHDR((struct msghdr *) msg, cmsg)) { + if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPING) { + timestamps = (const struct scm_timestamping *) CMSG_DATA(cmsg); + } else if ((cmsg->cmsg_level == SOL_IP && cmsg->cmsg_type == IP_RECVERR) || + (cmsg->cmsg_level == SOL_IPV6 && cmsg->cmsg_type == IPV6_RECVERR)) { + sock_err = (const struct sock_extended_err *) CMSG_DATA(cmsg); + } + } + if (timestamps == NULL || sock_err == NULL) { + errno = EAGAIN; + return -1; + } + if (timestamps->ts[0].tv_sec != 0 || timestamps->ts[0].tv_nsec != 0) { + timestamp_ns = linux_timespec_to_ns(×tamps->ts[0]); + snprintf(out_event->event_name, sizeof(out_event->event_name), "%s", EVENT_A_TX_SOFTWARE); + } else if (timestamps->ts[1].tv_sec != 0 || timestamps->ts[1].tv_nsec != 0) { + timestamp_ns = linux_timespec_to_ns(×tamps->ts[1]); + snprintf(out_event->event_name, sizeof(out_event->event_name), "%s", EVENT_A_TX_SCHED); + } else { + errno = EAGAIN; + return -1; + } + out_event->ts_unix_nano = timestamp_ns; + out_event->ee_info = sock_err->ee_info; + out_event->ee_data = sock_err->ee_data; + return 0; +} + +#else + +int linux_timestamping_enable_udp_socket(int fd, int enable_rx) { + (void) fd; + (void) enable_rx; + errno = ENOTSUP; + return -1; +} + +int64_t linux_timestamping_parse_rx_timestamp(const struct msghdr *msg) { + (void) msg; + return 0; +} + +int linux_timestamping_parse_tx_timestamp(const struct msghdr *msg, omni_tx_timestamp_event_t *out_event) { + (void) msg; + (void) out_event; + errno = ENOTSUP; + return -1; +} + +#endif diff --git a/c/src/omni_common.c b/c/src/omni_common.c new file mode 100644 index 0000000..0c7c29c --- /dev/null +++ b/c/src/omni_common.c @@ -0,0 +1,568 @@ +#include "omni_common.h" + +#include +#include +#include +#include +#include + +int64_t omni_now_unix_nano(void) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + return (int64_t) ts.tv_sec * 1000000000LL + ts.tv_nsec; +} + +uint32_t omni_now_millis32(void) { + struct timespec ts; + uint64_t ms; + clock_gettime(CLOCK_MONOTONIC, &ts); + ms = (uint64_t) ts.tv_sec * 1000ULL + (uint64_t) (ts.tv_nsec / 1000000L); + return (uint32_t) (ms & 0xffffffffu); +} + +int omni_set_nonblocking(int fd, int enabled) { + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) { + return -1; + } + if (enabled) { + flags |= O_NONBLOCK; + } else { + flags &= ~O_NONBLOCK; + } + return fcntl(fd, F_SETFL, flags); +} + +int omni_parse_sockaddr(const char *raw, int passive, struct sockaddr_storage *addr, socklen_t *addr_len, int *family_out) { + struct addrinfo hints; + struct addrinfo *result = NULL; + char host_copy[OMNI_MAX_ADDR_TEXT]; + char port_copy[32]; + const char *host = NULL; + const char *service = NULL; + const char *last_colon; + size_t host_len; + + if (raw == NULL || addr == NULL || addr_len == NULL) { + errno = EINVAL; + return -1; + } + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = passive ? AI_PASSIVE : 0; + + last_colon = strrchr(raw, ':'); + if (last_colon == NULL) { + host = passive ? NULL : raw; + service = passive ? raw : "0"; + } else { + host_len = (size_t) (last_colon - raw); + if (host_len >= sizeof(host_copy)) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(host_copy, raw, host_len); + host_copy[host_len] = '\0'; + snprintf(port_copy, sizeof(port_copy), "%s", last_colon + 1); + host = host_len == 0 ? NULL : host_copy; + service = port_copy; + } + + if (getaddrinfo(host, service, &hints, &result) != 0 || result == NULL) { + errno = EINVAL; + return -1; + } + memcpy(addr, result->ai_addr, result->ai_addrlen); + *addr_len = (socklen_t) result->ai_addrlen; + if (family_out != NULL) { + *family_out = result->ai_family; + } + freeaddrinfo(result); + return 0; +} + +int omni_clone_sockaddr(const struct sockaddr *src, socklen_t src_len, struct sockaddr_storage *dst, socklen_t *dst_len) { + if (src == NULL || dst == NULL || dst_len == NULL || src_len > sizeof(*dst)) { + errno = EINVAL; + return -1; + } + memset(dst, 0, sizeof(*dst)); + memcpy(dst, src, src_len); + *dst_len = src_len; + return 0; +} + +const char *omni_sockaddr_to_string(const struct sockaddr *addr, socklen_t addr_len, char *buffer, size_t buffer_len) { + char host[NI_MAXHOST]; + char service[NI_MAXSERV]; + + if (buffer == NULL || buffer_len == 0) { + return ""; + } + if (addr == NULL) { + snprintf(buffer, buffer_len, ""); + return buffer; + } + if (getnameinfo(addr, addr_len, host, sizeof(host), service, sizeof(service), NI_NUMERICHOST | NI_NUMERICSERV) != 0) { + snprintf(buffer, buffer_len, ""); + return buffer; + } + if (addr->sa_family == AF_INET6) { + snprintf(buffer, buffer_len, "[%s]:%s", host, service); + } else { + snprintf(buffer, buffer_len, "%s:%s", host, service); + } + return buffer; +} + +int omni_bind_device(int fd, const char *device) { +#ifdef __linux__ + if (device == NULL || device[0] == '\0') { + return 0; + } + return setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, device, (socklen_t) strlen(device)); +#else + (void) fd; + (void) device; + errno = ENOTSUP; + return -1; +#endif +} + +static int omni_mkdir_single(const char *path) { + if (mkdir(path, 0755) == 0 || errno == EEXIST) { + return 0; + } + return -1; +} + +int omni_ensure_dir(const char *path) { + char tmp[PATH_MAX]; + size_t i; + + if (path == NULL || path[0] == '\0') { + return 0; + } + if (strlen(path) >= sizeof(tmp)) { + errno = ENAMETOOLONG; + return -1; + } + snprintf(tmp, sizeof(tmp), "%s", path); + for (i = 1; tmp[i] != '\0'; ++i) { + if (tmp[i] == '/') { + tmp[i] = '\0'; + if (tmp[0] != '\0' && omni_mkdir_single(tmp) != 0) { + return -1; + } + tmp[i] = '/'; + } + } + return omni_mkdir_single(tmp); +} + +int omni_ensure_parent_dir(const char *path) { + char tmp[PATH_MAX]; + char *slash; + + if (path == NULL || path[0] == '\0') { + return 0; + } + if (strlen(path) >= sizeof(tmp)) { + errno = ENAMETOOLONG; + return -1; + } + snprintf(tmp, sizeof(tmp), "%s", path); + slash = strrchr(tmp, '/'); + if (slash == NULL) { + return 0; + } + if (slash == tmp) { + return omni_mkdir_single("/"); + } + *slash = '\0'; + return omni_ensure_dir(tmp); +} + +int omni_read_file(const char *path, uint8_t **out, size_t *out_len) { + FILE *file; + long size; + uint8_t *buffer; + if (out == NULL || out_len == NULL) { + errno = EINVAL; + return -1; + } + *out = NULL; + *out_len = 0; + file = fopen(path, "rb"); + if (file == NULL) { + return -1; + } + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return -1; + } + size = ftell(file); + if (size < 0) { + fclose(file); + return -1; + } + if (fseek(file, 0, SEEK_SET) != 0) { + fclose(file); + return -1; + } + buffer = (uint8_t *) malloc((size_t) size); + if (size > 0 && buffer == NULL) { + fclose(file); + errno = ENOMEM; + return -1; + } + if ((size_t) size > 0 && fread(buffer, 1, (size_t) size, file) != (size_t) size) { + free(buffer); + fclose(file); + errno = EIO; + return -1; + } + fclose(file); + *out = buffer; + *out_len = (size_t) size; + return 0; +} + +int omni_write_full_fd(int fd, const uint8_t *data, size_t len) { + ssize_t written; + while (len > 0) { + written = write(fd, data, len); + if (written < 0) { + if (errno == EINTR) { + continue; + } + return -1; + } + if (written == 0) { + errno = EIO; + return -1; + } + data += written; + len -= (size_t) written; + } + return 0; +} + +static int omni_write_file_internal(const char *path, const uint8_t *data, size_t len, const char *mode) { + FILE *file; + if (omni_ensure_parent_dir(path) != 0) { + return -1; + } + file = fopen(path, mode); + if (file == NULL) { + return -1; + } + if (len > 0 && fwrite(data, 1, len, file) != len) { + fclose(file); + errno = EIO; + return -1; + } + if (fclose(file) != 0) { + return -1; + } + return 0; +} + +int omni_append_file(const char *path, const uint8_t *data, size_t len) { + return omni_write_file_internal(path, data, len, "ab"); +} + +int omni_write_file(const char *path, const uint8_t *data, size_t len) { + return omni_write_file_internal(path, data, len, "wb"); +} + +int omni_random_u32(uint32_t *out) { + uint8_t *cursor; + size_t remaining; + int fd; + + if (out == NULL) { + errno = EINVAL; + return -1; + } + + fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) { + return -1; + } + + cursor = (uint8_t *) out; + remaining = sizeof(*out); + while (remaining > 0) { + ssize_t n = read(fd, cursor, remaining); + if (n < 0) { + if (errno == EINTR) { + continue; + } + close(fd); + return -1; + } + if (n == 0) { + close(fd); + errno = EIO; + return -1; + } + cursor += n; + remaining -= (size_t) n; + } + close(fd); + + if (*out == 0) { + *out = 1; + } + return 0; +} + +char *omni_strdup(const char *src) { + size_t len; + char *dst; + if (src == NULL) { + return NULL; + } + len = strlen(src); + dst = (char *) malloc(len + 1U); + if (dst == NULL) { + return NULL; + } + memcpy(dst, src, len + 1U); + return dst; +} + +char *omni_strdup_printf(const char *fmt, ...) { + va_list args; + va_list copy; + int needed; + char *buffer; + va_start(args, fmt); + va_copy(copy, args); + needed = vsnprintf(NULL, 0, fmt, copy); + va_end(copy); + if (needed < 0) { + va_end(args); + return NULL; + } + buffer = (char *) malloc((size_t) needed + 1U); + if (buffer == NULL) { + va_end(args); + return NULL; + } + vsnprintf(buffer, (size_t) needed + 1U, fmt, args); + va_end(args); + return buffer; +} + +char *omni_json_escape_bytes(const uint8_t *src, size_t len) { + size_t i; + size_t out_len = 0; + char *out; + char *cursor; + + if (src == NULL) { + if (len == 0) { + return omni_strdup(""); + } + errno = EINVAL; + return NULL; + } + + for (i = 0; i < len; ++i) { + switch (src[i]) { + case '\\': + case '"': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + out_len += 2; + break; + default: + out_len += src[i] < 0x20 ? 6U : 1U; + break; + } + } + out = (char *) malloc(out_len + 1U); + if (out == NULL) { + return NULL; + } + cursor = out; + for (i = 0; i < len; ++i) { + switch (src[i]) { + case '\\': + *cursor++ = '\\'; + *cursor++ = '\\'; + break; + case '"': + *cursor++ = '\\'; + *cursor++ = '"'; + break; + case '\b': + *cursor++ = '\\'; + *cursor++ = 'b'; + break; + case '\f': + *cursor++ = '\\'; + *cursor++ = 'f'; + break; + case '\n': + *cursor++ = '\\'; + *cursor++ = 'n'; + break; + case '\r': + *cursor++ = '\\'; + *cursor++ = 'r'; + break; + case '\t': + *cursor++ = '\\'; + *cursor++ = 't'; + break; + default: + if (src[i] < 0x20) { + snprintf(cursor, 7, "\\u%04x", src[i]); + cursor += 6; + } else { + *cursor++ = (char) src[i]; + } + break; + } + } + *cursor = '\0'; + return out; +} + +char *omni_json_escape(const char *src) { + if (src == NULL) { + return omni_strdup(""); + } + return omni_json_escape_bytes((const uint8_t *) src, strlen(src)); +} + +int omni_utf8_valid(const uint8_t *data, size_t len) { + size_t i = 0; + uint8_t c; + while (i < len) { + c = data[i]; + if (c <= 0x7f) { + i++; + continue; + } + if ((c & 0xe0) == 0xc0) { + if (i + 1 >= len || (data[i + 1] & 0xc0) != 0x80 || c < 0xc2) { + return 0; + } + i += 2; + continue; + } + if ((c & 0xf0) == 0xe0) { + if (i + 2 >= len || (data[i + 1] & 0xc0) != 0x80 || (data[i + 2] & 0xc0) != 0x80) { + return 0; + } + if (c == 0xe0 && data[i + 1] < 0xa0) { + return 0; + } + if (c == 0xed && data[i + 1] >= 0xa0) { + return 0; + } + i += 3; + continue; + } + if ((c & 0xf8) == 0xf0) { + if (i + 3 >= len || (data[i + 1] & 0xc0) != 0x80 || (data[i + 2] & 0xc0) != 0x80 || (data[i + 3] & 0xc0) != 0x80) { + return 0; + } + if (c == 0xf0 && data[i + 1] < 0x90) { + return 0; + } + if (c > 0xf4 || (c == 0xf4 && data[i + 1] >= 0x90)) { + return 0; + } + i += 4; + continue; + } + return 0; + } + return 1; +} + +void omni_trim_newline(char *line) { + size_t len; + if (line == NULL) { + return; + } + len = strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { + line[--len] = '\0'; + } +} + +int omni_parse_duration_ms(const char *raw, int default_ms, int *out_ms) { + char *endptr; + long value; + if (out_ms == NULL) { + errno = EINVAL; + return -1; + } + if (raw == NULL || raw[0] == '\0') { + *out_ms = default_ms; + return 0; + } + value = strtol(raw, &endptr, 10); + if (endptr == raw || value <= 0) { + errno = EINVAL; + return -1; + } + if (*endptr == '\0' || strcmp(endptr, "ms") == 0) { + *out_ms = (int) value; + return 0; + } + if (strcmp(endptr, "s") == 0) { + *out_ms = (int) (value * 1000L); + return 0; + } + errno = EINVAL; + return -1; +} + +double omni_duration_ms_to_ns(double ms) { + return ms * 1000000.0; +} + +const char *omni_path_base_name(const char *path) { + const char *slash; + + if (path == NULL) { + return ""; + } + slash = strrchr(path, '/'); + return slash == NULL ? path : slash + 1; +} + +void omni_file_logger_init(omni_file_logger_t *logger, FILE *file) { + logger->file = file; + pthread_mutex_init(&logger->mutex, NULL); +} + +void omni_file_logger_destroy(omni_file_logger_t *logger) { + pthread_mutex_destroy(&logger->mutex); +} + +int omni_file_logger_write_line(omni_file_logger_t *logger, const char *line) { + int rc = 0; + if (logger == NULL || logger->file == NULL || line == NULL) { + errno = EINVAL; + return -1; + } + pthread_mutex_lock(&logger->mutex); + if (fputs(line, logger->file) == EOF || fputc('\n', logger->file) == EOF || fflush(logger->file) != 0) { + rc = -1; + } + pthread_mutex_unlock(&logger->mutex); + return rc; +} diff --git a/c/src/peer_kcp_client.c b/c/src/peer_kcp_client.c new file mode 100644 index 0000000..8c9b80c --- /dev/null +++ b/c/src/peer_kcp_client.c @@ -0,0 +1,198 @@ +#include "peer_kcp_client.h" + +#include + +struct kcp_client { + char id[OMNI_MAX_PEER_ID]; + char server_addr[OMNI_MAX_ADDR_TEXT]; + kcp_conn_t *conn; + latency_logger_t *logger; + pthread_mutex_t id_mu; + uint64_t next_message_id; +}; + +static int kcp_client_next_message_id(kcp_client_t *client, uint64_t *out_id) { + pthread_mutex_lock(&client->id_mu); + *out_id = ++client->next_message_id; + pthread_mutex_unlock(&client->id_mu); + return 0; +} + +static int kcp_client_persist_message_to_disk(const message_t *msg, const char *inbox_dir, char *out_path, size_t out_path_len) { + char path[512]; + + if (omni_ensure_dir(inbox_dir) != 0) { + return -1; + } + if (msg->type == MSG_TYPE_TEXT) { + char *body = omni_json_escape_bytes(msg->body, msg->body_len); + char *from = omni_json_escape(msg->from); + char *to = omni_json_escape(msg->to); + char *line; + + if (body == NULL || from == NULL || to == NULL) { + free(body); + free(from); + free(to); + return -1; + } + snprintf(path, sizeof(path), "%s/messages.log", inbox_dir); + line = omni_strdup_printf( + "{\"message_type\":\"%s\",\"message_id\":%" PRIu64 ",\"from\":\"%s\",\"to\":\"%s\",\"body\":\"%s\"}\n", + protocol_message_type_name(msg->type), + msg->id, + from, + to, + body + ); + free(body); + free(from); + free(to); + if (line == NULL) { + return -1; + } + if (omni_append_file(path, (const uint8_t *) line, strlen(line)) != 0) { + free(line); + return -1; + } + free(line); + } else if (msg->type == MSG_TYPE_FILE) { + const char *file_name = omni_path_base_name(msg->file_name); + if (file_name[0] == '\0') { + file_name = "unnamed"; + } + snprintf(path, sizeof(path), "%s/%s-%" PRIu64 "-%s", inbox_dir, msg->from, msg->id, file_name); + if (omni_write_file(path, msg->body, msg->body_len) != 0) { + return -1; + } + } else { + errno = EINVAL; + return -1; + } + + if (out_path != NULL && out_path_len > 0) { + snprintf(out_path, out_path_len, "%s", path); + } + return 0; +} + +kcp_client_t *kcp_client_dial(const char *server_addr, const char *dial_addr, const char *peer_id, const char *bind_ip, const char *bind_device, latency_logger_t *logger, kcp_packet_debug_logger_t *packet_logger, kcp_session_stats_logger_t *stats_logger, int stats_interval_ms) { + kcp_client_t *client; + const char *actual_dial_addr = (dial_addr != NULL && dial_addr[0] != '\0') ? dial_addr : server_addr; + message_t register_msg; + + client = (kcp_client_t *) calloc(1, sizeof(*client)); + if (client == NULL) { + return NULL; + } + snprintf(client->id, sizeof(client->id), "%s", peer_id); + snprintf(client->server_addr, sizeof(client->server_addr), "%s", server_addr == NULL ? "" : server_addr); + pthread_mutex_init(&client->id_mu, NULL); + client->logger = logger; + client->conn = kcp_conn_dial(actual_dial_addr, bind_ip, bind_device, packet_logger, logger, OMNI_NODE_ROLE_PEER, peer_id, stats_logger, stats_interval_ms); + if (client->conn == NULL) { + kcp_client_free(client); + return NULL; + } + + protocol_message_init(®ister_msg); + register_msg.type = MSG_TYPE_REGISTER; + register_msg.id = 0; + snprintf(register_msg.from, sizeof(register_msg.from), "%s", peer_id); + snprintf(register_msg.to, sizeof(register_msg.to), "%s", SERVER_PEER_ID); + if (kcp_conn_send(client->conn, ®ister_msg) != 0) { + kcp_client_free(client); + return NULL; + } + return client; +} + +const char *kcp_client_id(const kcp_client_t *client) { + return client == NULL ? "" : client->id; +} + +int kcp_client_send_text(kcp_client_t *client, const char *to, const char *text) { + message_t msg; + uint64_t id; + + protocol_message_init(&msg); + kcp_client_next_message_id(client, &id); + msg.type = MSG_TYPE_TEXT; + msg.id = id; + snprintf(msg.from, sizeof(msg.from), "%s", client->id); + snprintf(msg.to, sizeof(msg.to), "%s", to); + msg.body = (uint8_t *) omni_strdup(text); + if (msg.body == NULL) { + return -1; + } + msg.body_len = strlen((const char *) msg.body); + latencylog_log_message_event(client->logger, OMNI_NODE_ROLE_PEER, client->id, EVENT_A_APP_PREP_BEGIN, &msg); + if (kcp_conn_send(client->conn, &msg) != 0) { + protocol_message_clear(&msg); + return -1; + } + protocol_message_clear(&msg); + return 0; +} + +int kcp_client_send_file_path(kcp_client_t *client, const char *to, const char *path) { + message_t msg; + uint64_t id; + uint8_t *body = NULL; + size_t body_len = 0; + const char *base_name = strrchr(path, '/'); + + if (omni_read_file(path, &body, &body_len) != 0) { + return -1; + } + protocol_message_init(&msg); + kcp_client_next_message_id(client, &id); + msg.type = MSG_TYPE_FILE; + msg.id = id; + snprintf(msg.from, sizeof(msg.from), "%s", client->id); + snprintf(msg.to, sizeof(msg.to), "%s", to); + snprintf(msg.file_name, sizeof(msg.file_name), "%s", base_name == NULL ? path : base_name + 1); + msg.body = body; + msg.body_len = body_len; + latencylog_log_message_event(client->logger, OMNI_NODE_ROLE_PEER, client->id, EVENT_A_APP_PREP_BEGIN, &msg); + if (kcp_conn_send(client->conn, &msg) != 0) { + protocol_message_clear(&msg); + return -1; + } + protocol_message_clear(&msg); + return 0; +} + +int kcp_client_receive(kcp_client_t *client, message_t *out_msg) { + if (kcp_conn_receive(client->conn, out_msg) != 0) { + return -1; + } + latencylog_log_message_event(client->logger, OMNI_NODE_ROLE_PEER, client->id, EVENT_B_APP_RECV, out_msg); + return 0; +} + +int kcp_client_persist_message(kcp_client_t *client, const message_t *msg, const char *inbox_dir, char *out_path, size_t out_path_len) { + if (!latencylog_is_business_message(msg)) { + errno = EINVAL; + return -1; + } + latencylog_log_message_event(client->logger, OMNI_NODE_ROLE_PEER, client->id, EVENT_B_PERSIST_BEGIN, msg); + if (kcp_client_persist_message_to_disk(msg, inbox_dir, out_path, out_path_len) != 0) { + return -1; + } + latencylog_log_message_event(client->logger, OMNI_NODE_ROLE_PEER, client->id, EVENT_B_PERSIST_END, msg); + return 0; +} + +int kcp_client_close(kcp_client_t *client) { + return client == NULL ? 0 : kcp_conn_close(client->conn); +} + +void kcp_client_free(kcp_client_t *client) { + if (client == NULL) { + return; + } + kcp_conn_free(client->conn); + pthread_mutex_destroy(&client->id_mu); + free(client); +} diff --git a/c/src/peer_udp_client.c b/c/src/peer_udp_client.c new file mode 100644 index 0000000..0a5deae --- /dev/null +++ b/c/src/peer_udp_client.c @@ -0,0 +1,181 @@ +#include "peer_udp_client.h" + +#include + +struct udp_client { + char id[OMNI_MAX_PEER_ID]; + udp_conn_t *conn; + latency_logger_t *logger; + pthread_mutex_t id_mu; + uint64_t next_message_id; +}; + +static int client_next_message_id(udp_client_t *client, uint64_t *out_id) { + pthread_mutex_lock(&client->id_mu); + *out_id = ++client->next_message_id; + pthread_mutex_unlock(&client->id_mu); + return 0; +} + +static int client_persist_message_to_disk(const message_t *msg, const char *inbox_dir, char *out_path, size_t out_path_len) { + char path[512]; + if (omni_ensure_dir(inbox_dir) != 0) { + return -1; + } + if (msg->type == MSG_TYPE_TEXT) { + char *body = omni_json_escape_bytes(msg->body, msg->body_len); + char *from = omni_json_escape(msg->from); + char *to = omni_json_escape(msg->to); + char *line; + if (body == NULL || from == NULL || to == NULL) { + free(body); + free(from); + free(to); + return -1; + } + snprintf(path, sizeof(path), "%s/messages.log", inbox_dir); + line = omni_strdup_printf("{\"message_type\":\"%s\",\"message_id\":%" PRIu64 ",\"from\":\"%s\",\"to\":\"%s\",\"body\":\"%s\"}\n", protocol_message_type_name(msg->type), msg->id, from, to, body); + free(body); + free(from); + free(to); + if (line == NULL) { + return -1; + } + if (omni_append_file(path, (const uint8_t *) line, strlen(line)) != 0) { + free(line); + return -1; + } + free(line); + } else if (msg->type == MSG_TYPE_FILE) { + const char *file_name = omni_path_base_name(msg->file_name); + if (file_name[0] == '\0') { + file_name = "unnamed"; + } + snprintf(path, sizeof(path), "%s/%s-%" PRIu64 "-%s", inbox_dir, msg->from, msg->id, file_name); + if (omni_write_file(path, msg->body, msg->body_len) != 0) { + return -1; + } + } else { + errno = EINVAL; + return -1; + } + if (out_path != NULL && out_path_len > 0) { + snprintf(out_path, out_path_len, "%s", path); + } + return 0; +} + +udp_client_t *udp_client_dial(const char *server_addr, const char *peer_id, const char *bind_ip, latency_logger_t *logger, tx_timestamp_debug_logger_t *debug_logger, int enable_timestamping) { + udp_client_t *client; + message_t register_msg; + client = (udp_client_t *) calloc(1, sizeof(*client)); + if (client == NULL) { + return NULL; + } + snprintf(client->id, sizeof(client->id), "%s", peer_id); + pthread_mutex_init(&client->id_mu, NULL); + client->logger = logger; + client->conn = udp_conn_dial(server_addr, bind_ip, NULL, enable_timestamping, logger, OMNI_NODE_ROLE_PEER, peer_id, debug_logger); + if (client->conn == NULL) { + udp_client_free(client); + return NULL; + } + protocol_message_init(®ister_msg); + register_msg.type = MSG_TYPE_REGISTER; + register_msg.id = 0; + snprintf(register_msg.from, sizeof(register_msg.from), "%s", peer_id); + snprintf(register_msg.to, sizeof(register_msg.to), "%s", SERVER_PEER_ID); + if (udp_conn_send(client->conn, ®ister_msg) != 0) { + udp_client_free(client); + return NULL; + } + return client; +} + +const char *udp_client_id(const udp_client_t *client) { + return client == NULL ? "" : client->id; +} + +int udp_client_send_text(udp_client_t *client, const char *to, const char *text) { + message_t msg; + uint64_t id; + protocol_message_init(&msg); + client_next_message_id(client, &id); + msg.type = MSG_TYPE_TEXT; + msg.id = id; + snprintf(msg.from, sizeof(msg.from), "%s", client->id); + snprintf(msg.to, sizeof(msg.to), "%s", to); + msg.body = (uint8_t *) omni_strdup(text); + if (msg.body == NULL) { + return -1; + } + msg.body_len = strlen((const char *) msg.body); + latencylog_log_message_event(client->logger, OMNI_NODE_ROLE_PEER, client->id, EVENT_A_APP_PREP_BEGIN, &msg); + if (udp_conn_send(client->conn, &msg) != 0) { + protocol_message_clear(&msg); + return -1; + } + protocol_message_clear(&msg); + return 0; +} + +int udp_client_send_file_path(udp_client_t *client, const char *to, const char *path) { + message_t msg; + uint64_t id; + uint8_t *body = NULL; + size_t body_len = 0; + const char *base_name = strrchr(path, '/'); + if (omni_read_file(path, &body, &body_len) != 0) { + return -1; + } + protocol_message_init(&msg); + client_next_message_id(client, &id); + msg.type = MSG_TYPE_FILE; + msg.id = id; + snprintf(msg.from, sizeof(msg.from), "%s", client->id); + snprintf(msg.to, sizeof(msg.to), "%s", to); + snprintf(msg.file_name, sizeof(msg.file_name), "%s", base_name == NULL ? path : base_name + 1); + msg.body = body; + msg.body_len = body_len; + latencylog_log_message_event(client->logger, OMNI_NODE_ROLE_PEER, client->id, EVENT_A_APP_PREP_BEGIN, &msg); + if (udp_conn_send(client->conn, &msg) != 0) { + protocol_message_clear(&msg); + return -1; + } + protocol_message_clear(&msg); + return 0; +} + +int udp_client_receive(udp_client_t *client, message_t *out_msg) { + if (udp_conn_receive(client->conn, out_msg, NULL, NULL) != 0) { + return -1; + } + latencylog_log_message_event(client->logger, OMNI_NODE_ROLE_PEER, client->id, EVENT_B_APP_RECV, out_msg); + return 0; +} + +int udp_client_persist_message(udp_client_t *client, const message_t *msg, const char *inbox_dir, char *out_path, size_t out_path_len) { + if (!latencylog_is_business_message(msg)) { + errno = EINVAL; + return -1; + } + latencylog_log_message_event(client->logger, OMNI_NODE_ROLE_PEER, client->id, EVENT_B_PERSIST_BEGIN, msg); + if (client_persist_message_to_disk(msg, inbox_dir, out_path, out_path_len) != 0) { + return -1; + } + latencylog_log_message_event(client->logger, OMNI_NODE_ROLE_PEER, client->id, EVENT_B_PERSIST_END, msg); + return 0; +} + +int udp_client_close(udp_client_t *client) { + return client == NULL ? 0 : udp_conn_close(client->conn); +} + +void udp_client_free(udp_client_t *client) { + if (client == NULL) { + return; + } + udp_conn_free(client->conn); + pthread_mutex_destroy(&client->id_mu); + free(client); +} diff --git a/c/src/protocol.c b/c/src/protocol.c new file mode 100644 index 0000000..fea7d96 --- /dev/null +++ b/c/src/protocol.c @@ -0,0 +1,409 @@ +#include "protocol.h" + +#include "cJSON.h" + +#include + +static const char *protocol_message_type_table[] = { + "text", + "file", + "register", + "error" +}; + +const char *protocol_message_type_name(message_type_t type) { + if ((int) type < 0 || type >= MSG_TYPE_INVALID) { + return "invalid"; + } + return protocol_message_type_table[type]; +} + +int protocol_message_type_from_name(const char *raw, message_type_t *out) { + size_t i; + if (raw == NULL || out == NULL) { + return -1; + } + for (i = 0; i < OMNI_ARRAY_LEN(protocol_message_type_table); ++i) { + if (strcmp(raw, protocol_message_type_table[i]) == 0) { + *out = (message_type_t) i; + return 0; + } + } + return -1; +} + +void protocol_message_init(message_t *msg) { + if (msg == NULL) { + return; + } + memset(msg, 0, sizeof(*msg)); + msg->type = MSG_TYPE_INVALID; +} + +void protocol_message_clear(message_t *msg) { + if (msg == NULL) { + return; + } + free(msg->body); + protocol_message_init(msg); +} + +int protocol_message_copy(message_t *dst, const message_t *src) { + if (dst == NULL || src == NULL) { + errno = EINVAL; + return -1; + } + protocol_message_clear(dst); + memcpy(dst, src, sizeof(*dst)); + dst->body = NULL; + if (src->body_len > 0) { + dst->body = (uint8_t *) malloc(src->body_len); + if (dst->body == NULL) { + protocol_message_init(dst); + errno = ENOMEM; + return -1; + } + memcpy(dst->body, src->body, src->body_len); + } + return 0; +} + +static int protocol_set_err(char *err, size_t err_len, const char *fmt, ...) { + va_list args; + if (err != NULL && err_len > 0) { + va_start(args, fmt); + vsnprintf(err, err_len, fmt, args); + va_end(args); + } + return -1; +} + +int protocol_validate_message(const message_t *msg, char *err, size_t err_len) { + if (msg == NULL) { + return protocol_set_err(err, err_len, "protocol: nil message"); + } + if (msg->from[0] == '\0') { + return protocol_set_err(err, err_len, "protocol: missing from"); + } + if (msg->to[0] == '\0') { + return protocol_set_err(err, err_len, "protocol: missing to"); + } + switch (msg->type) { + case MSG_TYPE_TEXT: + if (msg->file_name[0] != '\0') { + return protocol_set_err(err, err_len, "protocol: unexpected file name"); + } + if (!omni_utf8_valid(msg->body, msg->body_len)) { + return protocol_set_err(err, err_len, "protocol: invalid text body"); + } + break; + case MSG_TYPE_FILE: + if (msg->file_name[0] == '\0') { + return protocol_set_err(err, err_len, "protocol: missing file name"); + } + break; + case MSG_TYPE_REGISTER: + if (strcmp(msg->to, SERVER_PEER_ID) != 0) { + return protocol_set_err(err, err_len, "protocol: invalid register target"); + } + if (msg->file_name[0] != '\0') { + return protocol_set_err(err, err_len, "protocol: unexpected file name"); + } + if (msg->body_len != 0) { + return protocol_set_err(err, err_len, "protocol: unexpected body"); + } + break; + case MSG_TYPE_ERROR: + if (strcmp(msg->from, SERVER_PEER_ID) != 0) { + return protocol_set_err(err, err_len, "protocol: invalid error source"); + } + if (msg->file_name[0] != '\0') { + return protocol_set_err(err, err_len, "protocol: unexpected file name"); + } + if (!omni_utf8_valid(msg->body, msg->body_len)) { + return protocol_set_err(err, err_len, "protocol: invalid text body"); + } + break; + default: + return protocol_set_err(err, err_len, "protocol: invalid message type"); + } + return 0; +} + +static int protocol_build_header_json(const message_t *msg, char **out_json, size_t *out_len) { + cJSON *root; + char *json; + + root = cJSON_CreateObject(); + if (root == NULL) { + errno = ENOMEM; + return -1; + } + cJSON_AddStringToObject(root, "type", protocol_message_type_name(msg->type)); + cJSON_AddNumberToObject(root, "id", (double) msg->id); + cJSON_AddStringToObject(root, "from", msg->from); + cJSON_AddStringToObject(root, "to", msg->to); + if (msg->file_name[0] != '\0') { + cJSON_AddStringToObject(root, "file_name", msg->file_name); + } + cJSON_AddNumberToObject(root, "content_length", (double) msg->body_len); + json = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (json == NULL) { + errno = ENOMEM; + return -1; + } + *out_len = strlen(json); + *out_json = json; + return 0; +} + +int protocol_encode_message_datagram(const message_t *msg, uint8_t **out, size_t *out_len) { + uint8_t *buffer; + char *header_json; + size_t header_len; + uint32_t net_header_len; + char err[128]; + + if (out == NULL || out_len == NULL) { + errno = EINVAL; + return -1; + } + *out = NULL; + *out_len = 0; + if (protocol_validate_message(msg, err, sizeof(err)) != 0) { + errno = EINVAL; + return -1; + } + if (protocol_build_header_json(msg, &header_json, &header_len) != 0) { + return -1; + } + if (4U + header_len + msg->body_len > OMNI_MAX_FRAME_SIZE) { + cJSON_free(header_json); + errno = EMSGSIZE; + return -1; + } + buffer = (uint8_t *) malloc(4U + header_len + msg->body_len); + if (buffer == NULL) { + cJSON_free(header_json); + errno = ENOMEM; + return -1; + } + net_header_len = htonl((uint32_t) header_len); + memcpy(buffer, &net_header_len, 4); + memcpy(buffer + 4, header_json, header_len); + if (msg->body_len > 0) { + memcpy(buffer + 4 + header_len, msg->body, msg->body_len); + } + cJSON_free(header_json); + *out = buffer; + *out_len = 4U + header_len + msg->body_len; + return 0; +} + +static int protocol_copy_string_field(char *dst, size_t dst_len, const cJSON *object, const char *field, int required, char *err, size_t err_len) { + const cJSON *item = cJSON_GetObjectItemCaseSensitive(object, field); + if (item == NULL) { + if (required) { + return protocol_set_err(err, err_len, "protocol: missing %s", field); + } + dst[0] = '\0'; + return 0; + } + if (!cJSON_IsString(item) || item->valuestring == NULL) { + return protocol_set_err(err, err_len, "protocol: invalid %s", field); + } + snprintf(dst, dst_len, "%s", item->valuestring); + return 0; +} + +static int protocol_copy_u64_field(uint64_t *dst, const cJSON *object, const char *field, int required, char *err, size_t err_len) { + const cJSON *item = cJSON_GetObjectItemCaseSensitive(object, field); + if (item == NULL) { + if (required) { + return protocol_set_err(err, err_len, "protocol: missing %s", field); + } + *dst = 0; + return 0; + } + if (!cJSON_IsNumber(item)) { + return protocol_set_err(err, err_len, "protocol: invalid %s", field); + } + *dst = (uint64_t) item->valuedouble; + return 0; +} + +int protocol_decode_message_datagram(const uint8_t *data, size_t data_len, message_t *out_msg, char *err, size_t err_len) { + uint32_t net_header_len; + uint32_t header_len; + char *header_text = NULL; + cJSON *header = NULL; + const cJSON *type_item; + uint64_t content_length = 0; + + if (data == NULL || out_msg == NULL || data_len < 4U) { + return protocol_set_err(err, err_len, "protocol: invalid datagram"); + } + if (data_len > OMNI_MAX_FRAME_SIZE) { + return protocol_set_err(err, err_len, "protocol: frame too large"); + } + + protocol_message_clear(out_msg); + + memcpy(&net_header_len, data, 4); + header_len = ntohl(net_header_len); + if (header_len == 0 || (size_t) header_len > data_len - 4U) { + return protocol_set_err(err, err_len, "protocol: invalid header length"); + } + header_text = (char *) malloc((size_t) header_len + 1U); + if (header_text == NULL) { + errno = ENOMEM; + return -1; + } + memcpy(header_text, data + 4, header_len); + header_text[header_len] = '\0'; + header = cJSON_Parse(header_text); + free(header_text); + if (header == NULL || !cJSON_IsObject(header)) { + if (header != NULL) { + cJSON_Delete(header); + } + return protocol_set_err(err, err_len, "protocol: invalid header json"); + } + type_item = cJSON_GetObjectItemCaseSensitive(header, "type"); + if (type_item == NULL || !cJSON_IsString(type_item) || protocol_message_type_from_name(type_item->valuestring, &out_msg->type) != 0) { + cJSON_Delete(header); + return protocol_set_err(err, err_len, "protocol: invalid message type"); + } + if (protocol_copy_u64_field(&out_msg->id, header, "id", 1, err, err_len) != 0 || + protocol_copy_string_field(out_msg->from, sizeof(out_msg->from), header, "from", 1, err, err_len) != 0 || + protocol_copy_string_field(out_msg->to, sizeof(out_msg->to), header, "to", 1, err, err_len) != 0 || + protocol_copy_string_field(out_msg->file_name, sizeof(out_msg->file_name), header, "file_name", 0, err, err_len) != 0 || + protocol_copy_u64_field(&content_length, header, "content_length", 1, err, err_len) != 0) { + cJSON_Delete(header); + protocol_message_clear(out_msg); + return -1; + } + cJSON_Delete(header); + if ((size_t) content_length != data_len - 4U - (size_t) header_len) { + protocol_message_clear(out_msg); + return protocol_set_err(err, err_len, "protocol: invalid content length"); + } + out_msg->body_len = (size_t) content_length; + if (out_msg->body_len > 0) { + out_msg->body = (uint8_t *) malloc(out_msg->body_len); + if (out_msg->body == NULL) { + protocol_message_clear(out_msg); + errno = ENOMEM; + return -1; + } + memcpy(out_msg->body, data + 4U + header_len, out_msg->body_len); + } + if (protocol_validate_message(out_msg, err, err_len) != 0) { + protocol_message_clear(out_msg); + return -1; + } + return 0; +} + +int protocol_encode_message_stream(const message_t *msg, uint8_t **out, size_t *out_len) { + uint8_t *payload; + uint8_t *buffer; + size_t payload_len; + uint32_t net_len; + + if (protocol_encode_message_datagram(msg, &payload, &payload_len) != 0) { + return -1; + } + buffer = (uint8_t *) malloc(payload_len + 4U); + if (buffer == NULL) { + free(payload); + errno = ENOMEM; + return -1; + } + net_len = htonl((uint32_t) payload_len); + memcpy(buffer, &net_len, 4); + memcpy(buffer + 4, payload, payload_len); + free(payload); + *out = buffer; + *out_len = payload_len + 4U; + return 0; +} + +int protocol_decode_message_stream_payload(const uint8_t *payload, size_t payload_len, message_t *out_msg, char *err, size_t err_len) { + return protocol_decode_message_datagram(payload, payload_len, out_msg, err, err_len); +} + +void protocol_frame_decoder_init(protocol_frame_decoder_t *decoder) { + memset(decoder, 0, sizeof(*decoder)); +} + +void protocol_frame_decoder_reset(protocol_frame_decoder_t *decoder) { + decoder->len = 0; +} + +void protocol_frame_decoder_destroy(protocol_frame_decoder_t *decoder) { + free(decoder->buffer); + memset(decoder, 0, sizeof(*decoder)); +} + +int protocol_frame_decoder_feed(protocol_frame_decoder_t *decoder, const uint8_t *data, size_t data_len) { + uint8_t *next_buffer; + size_t next_cap; + if (decoder->len + data_len > OMNI_MAX_FRAME_SIZE * 2U) { + errno = EMSGSIZE; + return -1; + } + if (decoder->len + data_len > decoder->cap) { + next_cap = decoder->cap == 0 ? 4096U : decoder->cap; + while (next_cap < decoder->len + data_len) { + next_cap *= 2U; + } + next_buffer = (uint8_t *) realloc(decoder->buffer, next_cap); + if (next_buffer == NULL) { + errno = ENOMEM; + return -1; + } + decoder->buffer = next_buffer; + decoder->cap = next_cap; + } + memcpy(decoder->buffer + decoder->len, data, data_len); + decoder->len += data_len; + return 0; +} + +int protocol_frame_decoder_next(protocol_frame_decoder_t *decoder, uint8_t **payload, size_t *payload_len) { + uint32_t net_len; + uint32_t frame_len; + uint8_t *frame; + + if (payload == NULL || payload_len == NULL) { + errno = EINVAL; + return -1; + } + *payload = NULL; + *payload_len = 0; + if (decoder->len < 4U) { + return 0; + } + memcpy(&net_len, decoder->buffer, 4); + frame_len = ntohl(net_len); + if (frame_len == 0 || frame_len > OMNI_MAX_FRAME_SIZE) { + errno = EMSGSIZE; + return -1; + } + if (decoder->len < 4U + frame_len) { + return 0; + } + frame = (uint8_t *) malloc(frame_len); + if (frame == NULL) { + errno = ENOMEM; + return -1; + } + memcpy(frame, decoder->buffer + 4, frame_len); + memmove(decoder->buffer, decoder->buffer + 4U + frame_len, decoder->len - 4U - frame_len); + decoder->len -= 4U + frame_len; + *payload = frame; + *payload_len = frame_len; + return 1; +} diff --git a/c/src/server_kcp_hub.c b/c/src/server_kcp_hub.c new file mode 100644 index 0000000..f6c65d5 --- /dev/null +++ b/c/src/server_kcp_hub.c @@ -0,0 +1,562 @@ +#include "server_kcp_hub.h" + +#include + +#define KCP_RELAY_MAX_DATAGRAM_SIZE (60 * 1024) + +typedef struct kcp_peer_entry { + struct kcp_peer_entry *next; + char peer_id[OMNI_MAX_PEER_ID]; + kcp_conn_t *conn; +} kcp_peer_entry_t; + +typedef struct kcp_session_thread_ctx { + kcp_hub_t *hub; + kcp_conn_t *conn; +} kcp_session_thread_ctx_t; + +struct kcp_hub { + pthread_rwlock_t lock; + kcp_peer_entry_t *peers; + latency_logger_t *logger; + kcp_session_stats_logger_t *stats_logger; + int stats_interval_ms; + int relay_fd; + int relay_configured; + int relay_learn_peer; + struct sockaddr_storage relay_peer_addr; + socklen_t relay_peer_addr_len; + int closed; +}; + +static void kcp_hub_unregister(kcp_hub_t *hub, const char *peer_id, kcp_conn_t *conn) { + kcp_peer_entry_t *prev = NULL; + kcp_peer_entry_t *entry; + + if (hub == NULL || peer_id == NULL || peer_id[0] == '\0') { + return; + } + + pthread_rwlock_wrlock(&hub->lock); + for (entry = hub->peers; entry != NULL; entry = entry->next) { + if (strcmp(entry->peer_id, peer_id) == 0 && entry->conn == conn) { + if (prev == NULL) { + hub->peers = entry->next; + } else { + prev->next = entry->next; + } + free(entry); + break; + } + prev = entry; + } + pthread_rwlock_unlock(&hub->lock); +} + +static kcp_peer_entry_t *kcp_hub_find_peer(kcp_hub_t *hub, const char *peer_id) { + kcp_peer_entry_t *entry; + for (entry = hub->peers; entry != NULL; entry = entry->next) { + if (strcmp(entry->peer_id, peer_id) == 0) { + return entry; + } + } + return NULL; +} + +static int kcp_hub_send_server_error(kcp_conn_t *conn, const char *to, const char *message) { + message_t msg; + protocol_message_init(&msg); + msg.type = MSG_TYPE_ERROR; + snprintf(msg.from, sizeof(msg.from), "%s", SERVER_PEER_ID); + snprintf(msg.to, sizeof(msg.to), "%s", (to == NULL || to[0] == '\0') ? "unknown" : to); + msg.body = (uint8_t *) omni_strdup(message == NULL ? "" : message); + if (msg.body == NULL) { + return -1; + } + msg.body_len = strlen((const char *) msg.body); + if (kcp_conn_send(conn, &msg) != 0) { + protocol_message_clear(&msg); + return -1; + } + protocol_message_clear(&msg); + return 0; +} + +static int kcp_hub_sockaddr_equal(const struct sockaddr *left, socklen_t left_len, const struct sockaddr *right, socklen_t right_len) { + char left_text[OMNI_MAX_ADDR_TEXT]; + char right_text[OMNI_MAX_ADDR_TEXT]; + + if (left == NULL || right == NULL) { + return left == right; + } + return strcmp( + omni_sockaddr_to_string(left, left_len, left_text, sizeof(left_text)), + omni_sockaddr_to_string(right, right_len, right_text, sizeof(right_text)) + ) == 0; +} + +static int kcp_hub_accept_relay_peer(kcp_hub_t *hub, const struct sockaddr *addr, socklen_t addr_len) { + int accepted = 0; + + pthread_rwlock_wrlock(&hub->lock); + if (hub->relay_peer_addr_len == 0 && hub->relay_learn_peer) { + omni_clone_sockaddr(addr, addr_len, &hub->relay_peer_addr, &hub->relay_peer_addr_len); + accepted = 1; + } else if (hub->relay_peer_addr_len == 0) { + accepted = 1; + } else { + accepted = kcp_hub_sockaddr_equal((const struct sockaddr *) &hub->relay_peer_addr, hub->relay_peer_addr_len, addr, addr_len); + } + pthread_rwlock_unlock(&hub->lock); + return accepted; +} + +static int kcp_hub_forward_to_relay(kcp_hub_t *hub, const message_t *msg, int *relay_status) { + uint8_t *payload = NULL; + size_t payload_len = 0; + struct sockaddr_storage relay_addr; + socklen_t relay_addr_len = 0; + int relay_fd = -1; + int relay_configured = 0; + + if (relay_status != NULL) { + *relay_status = 0; + } + if (protocol_encode_message_datagram(msg, &payload, &payload_len) != 0) { + return -1; + } + if (payload_len > KCP_RELAY_MAX_DATAGRAM_SIZE) { + free(payload); + errno = EMSGSIZE; + if (relay_status != NULL) { + *relay_status = 3; + } + return -1; + } + + pthread_rwlock_rdlock(&hub->lock); + relay_fd = hub->relay_fd; + relay_configured = hub->relay_configured; + if (hub->relay_peer_addr_len > 0) { + omni_clone_sockaddr((const struct sockaddr *) &hub->relay_peer_addr, hub->relay_peer_addr_len, &relay_addr, &relay_addr_len); + } + pthread_rwlock_unlock(&hub->lock); + + if (!relay_configured || relay_fd < 0) { + free(payload); + errno = ENOTCONN; + if (relay_status != NULL) { + *relay_status = 1; + } + return -1; + } + if (relay_addr_len == 0) { + free(payload); + errno = EDESTADDRREQ; + if (relay_status != NULL) { + *relay_status = 2; + } + return -1; + } + if (sendto(relay_fd, payload, payload_len, 0, (struct sockaddr *) &relay_addr, relay_addr_len) < 0) { + free(payload); + return -1; + } + free(payload); + return 0; +} + +static int kcp_hub_forward_relay_server_error(kcp_hub_t *hub, const char *to, const char *message) { + message_t msg; + int rc; + + protocol_message_init(&msg); + msg.type = MSG_TYPE_ERROR; + snprintf(msg.from, sizeof(msg.from), "%s", SERVER_PEER_ID); + snprintf(msg.to, sizeof(msg.to), "%s", (to == NULL || to[0] == '\0') ? "unknown" : to); + msg.body = (uint8_t *) omni_strdup(message == NULL ? "" : message); + if (msg.body == NULL) { + return -1; + } + msg.body_len = strlen((const char *) msg.body); + rc = kcp_hub_forward_to_relay(hub, &msg, NULL); + protocol_message_clear(&msg); + return rc; +} + +static int kcp_hub_deliver_to_local_peer(kcp_hub_t *hub, const message_t *msg) { + kcp_conn_t *target_conn = NULL; + int rc; + + pthread_rwlock_rdlock(&hub->lock); + { + kcp_peer_entry_t *entry = kcp_hub_find_peer(hub, msg->to); + if (entry != NULL) { + target_conn = entry->conn; + } + } + pthread_rwlock_unlock(&hub->lock); + + if (target_conn == NULL) { + errno = ENOENT; + return -1; + } + rc = kcp_conn_send(target_conn, msg); + if (rc != 0) { + kcp_hub_unregister(hub, msg->to, target_conn); + kcp_conn_close(target_conn); + return -1; + } + return 0; +} + +static int kcp_hub_deliver_relayed_message(kcp_hub_t *hub, const message_t *msg) { + char *error_text; + + if (kcp_hub_deliver_to_local_peer(hub, msg) == 0) { + return 0; + } + if (errno != ENOENT) { + if (msg->type == MSG_TYPE_ERROR) { + return 0; + } + error_text = omni_strdup_printf("failed to forward to %s", msg->to); + if (error_text == NULL) { + return -1; + } + if (kcp_hub_forward_relay_server_error(hub, msg->from, error_text) != 0) { + free(error_text); + return -1; + } + free(error_text); + return 0; + } + + if (msg->type == MSG_TYPE_ERROR) { + return 0; + } + + error_text = omni_strdup_printf("unknown target: %s", msg->to); + if (error_text == NULL) { + return -1; + } + if (kcp_hub_forward_relay_server_error(hub, msg->from, error_text) != 0) { + free(error_text); + return -1; + } + free(error_text); + return 0; +} + +static int kcp_hub_handle_peer_message(kcp_hub_t *hub, const char *peer_id, kcp_conn_t *conn, message_t *msg) { + char *error_text = NULL; + int relay_status = 0; + + switch (msg->type) { + case MSG_TYPE_TEXT: + case MSG_TYPE_FILE: + snprintf(msg->from, sizeof(msg->from), "%s", peer_id); + if (kcp_hub_deliver_to_local_peer(hub, msg) == 0) { + return 0; + } + if (errno != ENOENT) { + error_text = omni_strdup_printf("failed to forward to %s", msg->to); + if (error_text == NULL) { + return -1; + } + if (kcp_hub_send_server_error(conn, peer_id, error_text) != 0) { + free(error_text); + return -1; + } + free(error_text); + return 0; + } + if (kcp_hub_forward_to_relay(hub, msg, &relay_status) == 0) { + return 0; + } + if (relay_status == 1) { + error_text = omni_strdup_printf("unknown target: %s", msg->to); + } else if (relay_status == 2) { + error_text = omni_strdup("failed to relay to remote peer"); + } else if (relay_status == 3) { + error_text = omni_strdup("message too large for relay udp"); + } else { + error_text = omni_strdup("failed to relay to remote peer"); + } + if (error_text == NULL) { + return -1; + } + if (kcp_hub_send_server_error(conn, peer_id, error_text) != 0) { + free(error_text); + return -1; + } + free(error_text); + return 0; + case MSG_TYPE_REGISTER: + case MSG_TYPE_ERROR: + if (kcp_hub_send_server_error(conn, peer_id, "registered peers can only send text or file messages") != 0) { + return -1; + } + errno = EPROTO; + return -1; + default: + error_text = omni_strdup_printf("unsupported message type: %s", protocol_message_type_name(msg->type)); + if (error_text == NULL) { + return -1; + } + if (kcp_hub_send_server_error(conn, peer_id, error_text) != 0) { + free(error_text); + return -1; + } + free(error_text); + errno = EPROTO; + return -1; + } +} + +static int kcp_hub_register_conn(kcp_hub_t *hub, kcp_conn_t *conn, char *peer_id, size_t peer_id_len) { + message_t msg; + kcp_peer_entry_t *entry; + + protocol_message_init(&msg); + if (kcp_conn_receive(conn, &msg) != 0) { + protocol_message_clear(&msg); + return -1; + } + if (msg.type != MSG_TYPE_REGISTER) { + kcp_hub_send_server_error(conn, msg.from, "first message must be register"); + protocol_message_clear(&msg); + errno = EPROTO; + return -1; + } + + pthread_rwlock_wrlock(&hub->lock); + entry = kcp_hub_find_peer(hub, msg.from); + if (entry != NULL) { + char *error_text; + pthread_rwlock_unlock(&hub->lock); + error_text = omni_strdup_printf("duplicate peer id: %s", msg.from); + if (error_text != NULL) { + (void) kcp_hub_send_server_error(conn, msg.from, error_text); + free(error_text); + } + protocol_message_clear(&msg); + errno = EEXIST; + return -1; + } + + entry = (kcp_peer_entry_t *) calloc(1, sizeof(*entry)); + if (entry == NULL) { + pthread_rwlock_unlock(&hub->lock); + protocol_message_clear(&msg); + return -1; + } + snprintf(entry->peer_id, sizeof(entry->peer_id), "%s", msg.from); + entry->conn = conn; + entry->next = hub->peers; + hub->peers = entry; + pthread_rwlock_unlock(&hub->lock); + + snprintf(peer_id, peer_id_len, "%s", msg.from); + protocol_message_clear(&msg); + return 0; +} + +static void *kcp_hub_session_thread_main(void *arg) { + kcp_session_thread_ctx_t *ctx = (kcp_session_thread_ctx_t *) arg; + kcp_hub_serve_session(ctx->hub, ctx->conn); + free(ctx); + return NULL; +} + +kcp_hub_t *kcp_hub_new(latency_logger_t *logger, kcp_session_stats_logger_t *stats_logger, int stats_interval_ms) { + kcp_hub_t *hub = (kcp_hub_t *) calloc(1, sizeof(*hub)); + if (hub == NULL) { + return NULL; + } + pthread_rwlock_init(&hub->lock, NULL); + hub->logger = logger; + hub->stats_logger = stats_logger; + hub->stats_interval_ms = stats_interval_ms > 0 ? stats_interval_ms : KCP_DEFAULT_STATS_INTERVAL_MS; + hub->relay_fd = -1; + return hub; +} + +int kcp_hub_serve_listener(kcp_hub_t *hub, kcp_listener_t *listener) { + if (hub == NULL || listener == NULL) { + errno = EINVAL; + return -1; + } + while (!hub->closed) { + kcp_conn_t *conn = kcp_listener_accept(listener); + kcp_session_thread_ctx_t *ctx; + pthread_t thread; + + if (conn == NULL) { + if (hub->closed) { + return 0; + } + return -1; + } + ctx = (kcp_session_thread_ctx_t *) calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + kcp_conn_close(conn); + kcp_conn_free(conn); + return -1; + } + ctx->hub = hub; + ctx->conn = conn; + if (pthread_create(&thread, NULL, kcp_hub_session_thread_main, ctx) != 0) { + free(ctx); + kcp_conn_close(conn); + kcp_conn_free(conn); + return -1; + } + pthread_detach(thread); + } + return 0; +} + +int kcp_hub_serve_session(kcp_hub_t *hub, kcp_conn_t *conn) { + char peer_id[OMNI_MAX_PEER_ID]; + int rc = 0; + + if (hub == NULL || conn == NULL) { + errno = EINVAL; + return -1; + } + peer_id[0] = '\0'; + if (kcp_conn_configure_runtime(conn, hub->logger, OMNI_NODE_ROLE_SERVER, "hub", hub->stats_logger, hub->stats_interval_ms) != 0) { + kcp_conn_close(conn); + kcp_conn_free(conn); + return -1; + } + if (kcp_hub_register_conn(hub, conn, peer_id, sizeof(peer_id)) != 0) { + kcp_conn_close(conn); + kcp_conn_free(conn); + return -1; + } + + for (;;) { + message_t msg; + protocol_message_init(&msg); + if (kcp_conn_receive(conn, &msg) != 0) { + protocol_message_clear(&msg); + rc = -1; + break; + } + if (kcp_hub_handle_peer_message(hub, peer_id, conn, &msg) != 0) { + protocol_message_clear(&msg); + rc = -1; + break; + } + protocol_message_clear(&msg); + } + + kcp_hub_unregister(hub, peer_id, conn); + kcp_conn_close(conn); + kcp_conn_free(conn); + return rc; +} + +int kcp_hub_set_relay(kcp_hub_t *hub, int relay_fd, const struct sockaddr *peer_addr, socklen_t peer_addr_len, int learn_peer) { + if (hub == NULL || relay_fd < 0) { + errno = EINVAL; + return -1; + } + pthread_rwlock_wrlock(&hub->lock); + hub->relay_fd = relay_fd; + hub->relay_configured = 1; + hub->relay_learn_peer = learn_peer; + hub->relay_peer_addr_len = 0; + if (peer_addr != NULL && peer_addr_len > 0) { + omni_clone_sockaddr(peer_addr, peer_addr_len, &hub->relay_peer_addr, &hub->relay_peer_addr_len); + } + pthread_rwlock_unlock(&hub->lock); + return 0; +} + +int kcp_hub_serve_relay(kcp_hub_t *hub) { + uint8_t buffer[KCP_RELAY_MAX_DATAGRAM_SIZE]; + + if (hub == NULL) { + errno = EINVAL; + return -1; + } + while (!hub->closed) { + struct sockaddr_storage source; + socklen_t source_len = sizeof(source); + ssize_t n; + message_t msg; + char err[128]; + int relay_fd; + + pthread_rwlock_rdlock(&hub->lock); + relay_fd = hub->relay_fd; + pthread_rwlock_unlock(&hub->lock); + if (relay_fd < 0) { + errno = ENOTCONN; + return -1; + } + + n = recvfrom(relay_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &source, &source_len); + if (n < 0) { + if (hub->closed) { + return 0; + } + if (errno == EINTR) { + continue; + } + return -1; + } + if (!kcp_hub_accept_relay_peer(hub, (struct sockaddr *) &source, source_len)) { + continue; + } + + protocol_message_init(&msg); + if (protocol_decode_message_datagram(buffer, (size_t) n, &msg, err, sizeof(err)) != 0) { + protocol_message_clear(&msg); + continue; + } + if (msg.type != MSG_TYPE_TEXT && msg.type != MSG_TYPE_FILE && msg.type != MSG_TYPE_ERROR) { + protocol_message_clear(&msg); + continue; + } + (void) kcp_hub_deliver_relayed_message(hub, &msg); + protocol_message_clear(&msg); + } + return 0; +} + +int kcp_hub_close(kcp_hub_t *hub) { + if (hub == NULL) { + return 0; + } + if (!hub->closed) { + hub->closed = 1; + if (hub->relay_fd >= 0) { + close(hub->relay_fd); + hub->relay_fd = -1; + } + } + return 0; +} + +void kcp_hub_free(kcp_hub_t *hub) { + kcp_peer_entry_t *entry; + kcp_peer_entry_t *next; + + if (hub == NULL) { + return; + } + kcp_hub_close(hub); + for (entry = hub->peers; entry != NULL; entry = next) { + next = entry->next; + if (entry->conn != NULL) { + kcp_conn_close(entry->conn); + } + free(entry); + } + pthread_rwlock_destroy(&hub->lock); + free(hub); +} diff --git a/c/src/server_udp_hub.c b/c/src/server_udp_hub.c new file mode 100644 index 0000000..885d12a --- /dev/null +++ b/c/src/server_udp_hub.c @@ -0,0 +1,181 @@ +#include "server_udp_hub.h" + +#include + +typedef struct udp_peer_entry { + struct udp_peer_entry *next; + char peer_id[OMNI_MAX_PEER_ID]; + struct sockaddr_storage addr; + socklen_t addr_len; +} udp_peer_entry_t; + +struct udp_hub { + udp_conn_t *conn; + pthread_rwlock_t lock; + udp_peer_entry_t *peers; +}; + +static int udp_addr_equal(const struct sockaddr_storage *a, socklen_t a_len, const struct sockaddr_storage *b, socklen_t b_len) { + return a_len == b_len && memcmp(a, b, a_len) == 0; +} + +static udp_peer_entry_t *udp_hub_find_by_id(udp_hub_t *hub, const char *peer_id) { + udp_peer_entry_t *entry; + for (entry = hub->peers; entry != NULL; entry = entry->next) { + if (strcmp(entry->peer_id, peer_id) == 0) { + return entry; + } + } + return NULL; +} + +static udp_peer_entry_t *udp_hub_find_by_addr(udp_hub_t *hub, const struct sockaddr_storage *addr, socklen_t addr_len) { + udp_peer_entry_t *entry; + for (entry = hub->peers; entry != NULL; entry = entry->next) { + if (udp_addr_equal(&entry->addr, entry->addr_len, addr, addr_len)) { + return entry; + } + } + return NULL; +} + +static int udp_hub_send_error(udp_hub_t *hub, const struct sockaddr_storage *addr, socklen_t addr_len, const char *to, const char *message) { + message_t msg; + protocol_message_init(&msg); + msg.type = MSG_TYPE_ERROR; + msg.id = 0; + snprintf(msg.from, sizeof(msg.from), "%s", SERVER_PEER_ID); + snprintf(msg.to, sizeof(msg.to), "%s", to == NULL || to[0] == '\0' ? "unknown" : to); + msg.body = (uint8_t *) omni_strdup(message); + msg.body_len = msg.body == NULL ? 0 : strlen((const char *) msg.body); + if (msg.body == NULL) { + return -1; + } + if (udp_conn_send_to(hub->conn, &msg, (const struct sockaddr *) addr, addr_len) != 0) { + protocol_message_clear(&msg); + return -1; + } + protocol_message_clear(&msg); + return 0; +} + +udp_hub_t *udp_hub_open(const char *listen_addr, latency_logger_t *logger, tx_timestamp_debug_logger_t *debug_logger, int enable_timestamping) { + udp_hub_t *hub = (udp_hub_t *) calloc(1, sizeof(*hub)); + if (hub == NULL) { + return NULL; + } + hub->conn = udp_conn_bind(listen_addr, NULL, enable_timestamping, logger, OMNI_NODE_ROLE_SERVER, "hub", debug_logger); + if (hub->conn == NULL) { + free(hub); + return NULL; + } + pthread_rwlock_init(&hub->lock, NULL); + return hub; +} + +int udp_hub_serve(udp_hub_t *hub) { + message_t msg; + struct sockaddr_storage addr; + socklen_t addr_len; + udp_peer_entry_t *sender; + udp_peer_entry_t *target; + udp_peer_entry_t *entry; + + if (hub == NULL) { + errno = EINVAL; + return -1; + } + + protocol_message_init(&msg); + for (;;) { + protocol_message_clear(&msg); + if (udp_conn_receive(hub->conn, &msg, &addr, &addr_len) != 0) { + return -1; + } + + if (msg.type == MSG_TYPE_REGISTER) { + pthread_rwlock_wrlock(&hub->lock); + entry = udp_hub_find_by_id(hub, msg.from); + if (entry == NULL) { + entry = (udp_peer_entry_t *) calloc(1, sizeof(*entry)); + if (entry == NULL) { + pthread_rwlock_unlock(&hub->lock); + protocol_message_clear(&msg); + return -1; + } + snprintf(entry->peer_id, sizeof(entry->peer_id), "%s", msg.from); + entry->next = hub->peers; + hub->peers = entry; + } + memcpy(&entry->addr, &addr, sizeof(addr)); + entry->addr_len = addr_len; + pthread_rwlock_unlock(&hub->lock); + continue; + } + if (msg.type != MSG_TYPE_TEXT && msg.type != MSG_TYPE_FILE) { + if (msg.type == MSG_TYPE_ERROR) { + udp_hub_send_error(hub, &addr, addr_len, msg.from, "peers cannot send error messages"); + } else { + char *error_text = omni_strdup_printf("unsupported message type: %s", protocol_message_type_name(msg.type)); + if (error_text != NULL) { + udp_hub_send_error(hub, &addr, addr_len, msg.from, error_text); + free(error_text); + } + } + continue; + } + + pthread_rwlock_rdlock(&hub->lock); + sender = udp_hub_find_by_addr(hub, &addr, addr_len); + if (sender == NULL) { + pthread_rwlock_unlock(&hub->lock); + udp_hub_send_error(hub, &addr, addr_len, msg.from, "not registered; send register first"); + continue; + } + snprintf(msg.from, sizeof(msg.from), "%s", sender->peer_id); + target = udp_hub_find_by_id(hub, msg.to); + if (target == NULL) { + char *error_text; + pthread_rwlock_unlock(&hub->lock); + error_text = omni_strdup_printf("unknown target: %s", msg.to); + if (error_text != NULL) { + udp_hub_send_error(hub, &addr, addr_len, sender->peer_id, error_text); + free(error_text); + } + continue; + } + if (udp_conn_send_to(hub->conn, &msg, (const struct sockaddr *) &target->addr, target->addr_len) != 0) { + char *error_text; + pthread_rwlock_unlock(&hub->lock); + error_text = omni_strdup_printf("failed to forward to %s", msg.to); + if (error_text != NULL) { + udp_hub_send_error(hub, &addr, addr_len, sender->peer_id, error_text); + free(error_text); + } + continue; + } + pthread_rwlock_unlock(&hub->lock); + } +} + +int udp_hub_close(udp_hub_t *hub) { + if (hub == NULL) { + return 0; + } + return udp_conn_close(hub->conn); +} + +void udp_hub_free(udp_hub_t *hub) { + udp_peer_entry_t *entry; + udp_peer_entry_t *next; + if (hub == NULL) { + return; + } + udp_conn_free(hub->conn); + for (entry = hub->peers; entry != NULL; entry = next) { + next = entry->next; + free(entry); + } + pthread_rwlock_destroy(&hub->lock); + free(hub); +} diff --git a/c/src/server_udp_relay.c b/c/src/server_udp_relay.c new file mode 100644 index 0000000..aae84a2 --- /dev/null +++ b/c/src/server_udp_relay.c @@ -0,0 +1,311 @@ +#include "server_udp_relay.h" + +#include +#include + +#define UDP_RELAY_BUF_SIZE (64U * 1024U) + +struct udp_relay { + int downstream_fd; + int upstream_fd; + struct sockaddr_storage upstream_addr; + socklen_t upstream_addr_len; + struct sockaddr_storage client_addr; + socklen_t client_addr_len; + int has_client; + pthread_mutex_t lock; + pthread_mutex_t state_mu; + pthread_cond_t state_cond; + pthread_t downstream_thread; + int downstream_thread_started; + pthread_t upstream_thread; + int upstream_thread_started; + int worker_done; + int worker_rc; + int worker_errno; + int closed; +}; + +static int udp_relay_is_closed(udp_relay_t *relay) { + int closed; + + pthread_mutex_lock(&relay->state_mu); + closed = relay->closed; + pthread_mutex_unlock(&relay->state_mu); + return closed; +} + +static void udp_relay_note_result(udp_relay_t *relay, int rc, int errnum) { + pthread_mutex_lock(&relay->state_mu); + if (!relay->worker_done) { + relay->worker_done = 1; + relay->worker_rc = rc; + relay->worker_errno = errnum; + pthread_cond_signal(&relay->state_cond); + } + pthread_mutex_unlock(&relay->state_mu); +} + +static void udp_relay_record_client(udp_relay_t *relay, const struct sockaddr_storage *addr, socklen_t addr_len) { + pthread_mutex_lock(&relay->lock); + memcpy(&relay->client_addr, addr, sizeof(*addr)); + relay->client_addr_len = addr_len; + relay->has_client = 1; + pthread_mutex_unlock(&relay->lock); +} + +static int udp_relay_copy_client(udp_relay_t *relay, struct sockaddr_storage *addr, socklen_t *addr_len) { + int has_client; + + pthread_mutex_lock(&relay->lock); + has_client = relay->has_client; + if (has_client) { + memcpy(addr, &relay->client_addr, sizeof(*addr)); + *addr_len = relay->client_addr_len; + } + pthread_mutex_unlock(&relay->lock); + return has_client; +} + +static void *udp_relay_forward_downstream_to_upstream(void *arg) { + udp_relay_t *relay = (udp_relay_t *) arg; + uint8_t buffer[UDP_RELAY_BUF_SIZE]; + + for (;;) { + struct sockaddr_storage source; + socklen_t source_len = sizeof(source); + ssize_t n = recvfrom(relay->downstream_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &source, &source_len); + + if (n < 0) { + int errnum = errno; + if (errnum == EINTR) { + continue; + } + if (udp_relay_is_closed(relay)) { + udp_relay_note_result(relay, 0, 0); + } else { + udp_relay_note_result(relay, -1, errnum); + } + return NULL; + } + + udp_relay_record_client(relay, &source, source_len); + for (;;) { + if (send(relay->upstream_fd, buffer, (size_t) n, 0) >= 0) { + break; + } + { + int errnum = errno; + if (errnum == EINTR) { + continue; + } + if (udp_relay_is_closed(relay)) { + udp_relay_note_result(relay, 0, 0); + } else { + udp_relay_note_result(relay, -1, errnum); + } + return NULL; + } + } + } +} + +static void *udp_relay_forward_upstream_to_downstream(void *arg) { + udp_relay_t *relay = (udp_relay_t *) arg; + uint8_t buffer[UDP_RELAY_BUF_SIZE]; + + for (;;) { + struct sockaddr_storage client_addr; + socklen_t client_addr_len = 0; + ssize_t n = recv(relay->upstream_fd, buffer, sizeof(buffer), 0); + + if (n < 0) { + int errnum = errno; + if (errnum == EINTR) { + continue; + } + if (udp_relay_is_closed(relay)) { + udp_relay_note_result(relay, 0, 0); + } else { + udp_relay_note_result(relay, -1, errnum); + } + return NULL; + } + + if (!udp_relay_copy_client(relay, &client_addr, &client_addr_len)) { + continue; + } + + for (;;) { + if (sendto(relay->downstream_fd, buffer, (size_t) n, 0, (struct sockaddr *) &client_addr, client_addr_len) >= 0) { + break; + } + { + int errnum = errno; + if (errnum == EINTR) { + continue; + } + if (udp_relay_is_closed(relay)) { + udp_relay_note_result(relay, 0, 0); + } else { + udp_relay_note_result(relay, -1, errnum); + } + return NULL; + } + } + } +} + +static void udp_relay_join_threads(udp_relay_t *relay) { + if (relay->downstream_thread_started) { + pthread_join(relay->downstream_thread, NULL); + relay->downstream_thread_started = 0; + } + if (relay->upstream_thread_started) { + pthread_join(relay->upstream_thread, NULL); + relay->upstream_thread_started = 0; + } +} + +udp_relay_t *udp_relay_open(const char *listen_addr, const char *upstream_addr) { + struct sockaddr_storage listen_ss; + struct sockaddr_storage upstream_ss; + socklen_t listen_len; + socklen_t upstream_len; + int family; + int fd_listen = -1; + int fd_upstream = -1; + udp_relay_t *relay = NULL; + + if (omni_parse_sockaddr(listen_addr, 1, &listen_ss, &listen_len, &family) != 0 || + omni_parse_sockaddr(upstream_addr, 0, &upstream_ss, &upstream_len, &family) != 0) { + return NULL; + } + fd_listen = socket(listen_ss.ss_family, SOCK_DGRAM, 0); + if (fd_listen < 0) { + return NULL; + } + if (bind(fd_listen, (struct sockaddr *) &listen_ss, listen_len) != 0) { + close(fd_listen); + return NULL; + } + fd_upstream = socket(upstream_ss.ss_family, SOCK_DGRAM, 0); + if (fd_upstream < 0) { + close(fd_listen); + return NULL; + } + if (connect(fd_upstream, (struct sockaddr *) &upstream_ss, upstream_len) != 0) { + close(fd_upstream); + close(fd_listen); + return NULL; + } + relay = (udp_relay_t *) calloc(1, sizeof(*relay)); + if (relay == NULL) { + close(fd_upstream); + close(fd_listen); + return NULL; + } + relay->downstream_fd = fd_listen; + relay->upstream_fd = fd_upstream; + memcpy(&relay->upstream_addr, &upstream_ss, sizeof(upstream_ss)); + relay->upstream_addr_len = upstream_len; + pthread_mutex_init(&relay->lock, NULL); + pthread_mutex_init(&relay->state_mu, NULL); + pthread_cond_init(&relay->state_cond, NULL); + return relay; +} + +int udp_relay_serve(udp_relay_t *relay) { + int thread_rc; + int rc; + int errnum; + + if (relay == NULL) { + errno = EINVAL; + return -1; + } + if (udp_relay_is_closed(relay)) { + errno = ECANCELED; + return -1; + } + + pthread_mutex_lock(&relay->state_mu); + relay->worker_done = 0; + relay->worker_rc = 0; + relay->worker_errno = 0; + pthread_mutex_unlock(&relay->state_mu); + + thread_rc = pthread_create(&relay->downstream_thread, NULL, udp_relay_forward_downstream_to_upstream, relay); + if (thread_rc != 0) { + errno = thread_rc; + return -1; + } + relay->downstream_thread_started = 1; + + thread_rc = pthread_create(&relay->upstream_thread, NULL, udp_relay_forward_upstream_to_downstream, relay); + if (thread_rc != 0) { + errno = thread_rc; + udp_relay_close(relay); + udp_relay_join_threads(relay); + return -1; + } + relay->upstream_thread_started = 1; + + pthread_mutex_lock(&relay->state_mu); + while (!relay->worker_done) { + pthread_cond_wait(&relay->state_cond, &relay->state_mu); + } + rc = relay->worker_rc; + errnum = relay->worker_errno; + pthread_mutex_unlock(&relay->state_mu); + + udp_relay_close(relay); + udp_relay_join_threads(relay); + + if (rc != 0 && errnum != 0) { + errno = errnum; + } + return rc; +} + +int udp_relay_close(udp_relay_t *relay) { + int downstream_fd; + int upstream_fd; + + if (relay == NULL) { + return 0; + } + + pthread_mutex_lock(&relay->state_mu); + if (relay->closed) { + pthread_mutex_unlock(&relay->state_mu); + return 0; + } + relay->closed = 1; + downstream_fd = relay->downstream_fd; + upstream_fd = relay->upstream_fd; + relay->downstream_fd = -1; + relay->upstream_fd = -1; + pthread_cond_broadcast(&relay->state_cond); + pthread_mutex_unlock(&relay->state_mu); + + if (downstream_fd >= 0) { + close(downstream_fd); + } + if (upstream_fd >= 0) { + close(upstream_fd); + } + return 0; +} + +void udp_relay_free(udp_relay_t *relay) { + if (relay == NULL) { + return; + } + udp_relay_close(relay); + udp_relay_join_threads(relay); + pthread_mutex_destroy(&relay->lock); + pthread_cond_destroy(&relay->state_cond); + pthread_mutex_destroy(&relay->state_mu); + free(relay); +} diff --git a/c/src/transport_kcp.c b/c/src/transport_kcp.c new file mode 100644 index 0000000..3eb5e43 --- /dev/null +++ b/c/src/transport_kcp.c @@ -0,0 +1,1705 @@ +#include "transport_kcp.h" + +#include "ikcp.h" +#include "linux_timestamping.h" + +#include +#include +#include +#include + +#define KCP_RECV_CHUNK_SIZE (32U * 1024U) + +typedef struct kcp_packet_debug_pending { + struct kcp_packet_debug_pending *next; + uint32_t tx_id; + struct sockaddr_storage remote_addr; + socklen_t remote_addr_len; + int packet_bytes; + int has_conv; + uint32_t conv; + kcp_packet_debug_segment_t *segments; + size_t segment_count; + int saw_sched; + int saw_software; +} kcp_packet_debug_pending_t; + +typedef struct kcp_socket_debug_state { + int fd; + char node_role[OMNI_MAX_NODE_ROLE]; + char node_id[OMNI_MAX_PEER_ID]; + kcp_packet_debug_logger_t *logger; + pthread_mutex_t write_mu; + pthread_mutex_t pending_mu; + pthread_t errqueue_thread; + int errqueue_thread_started; + uint32_t next_tx_id; + kcp_packet_debug_pending_t *pending_head; + atomic_int closed; +} kcp_socket_debug_state_t; + +typedef struct kcp_session_entry kcp_session_entry_t; +typedef struct kcp_process_sampler kcp_process_sampler_t; + +struct kcp_conn { + ikcpcb *kcp; + int fd; + int is_client; + int owns_socket; + int socket_closed; + atomic_int closed; + struct sockaddr_storage remote_addr; + socklen_t remote_addr_len; + pthread_mutex_t kcp_mu; + pthread_mutex_t close_mu; + pthread_cond_t rx_cond; + pthread_t recv_thread; + int recv_thread_started; + pthread_t update_thread; + int update_thread_started; + pthread_t stats_thread; + int stats_thread_started; + uint64_t pending_bytes_sent; + uint64_t pending_bytes_received; + uint64_t pending_in_pkts; + uint64_t pending_out_pkts; + uint64_t pending_in_segs; + uint64_t pending_out_segs; + uint64_t pending_in_errs; + uint64_t pending_kcp_in_errs; + protocol_frame_decoder_t decoder; + uint8_t scratch[KCP_RECV_CHUNK_SIZE]; + latency_logger_t *logger; + char node_role[OMNI_MAX_NODE_ROLE]; + char node_id[OMNI_MAX_PEER_ID]; + kcp_session_stats_logger_t *stats_logger; + int stats_interval_ms; + kcp_process_sampler_t *process_sampler; + kcp_socket_debug_state_t *sock_state; + struct kcp_listener *listener; + struct kcp_conn *accept_next; + struct kcp_conn *process_next; +}; + +struct kcp_listener { + int fd; + int closed; + pthread_mutex_t lock; + pthread_mutex_t accept_mu; + pthread_cond_t accept_cond; + pthread_t recv_thread; + int recv_thread_started; + kcp_session_entry_t *sessions; + kcp_conn_t *accept_head; + kcp_conn_t *accept_tail; + kcp_socket_debug_state_t sock_state; +}; + +struct kcp_session_entry { + uint32_t conv; + kcp_conn_t *conn; + kcp_session_entry_t *next; +}; + +struct kcp_process_sampler { + kcp_process_sampler_t *next; + kcp_session_stats_logger_t *logger; + char node_role[OMNI_MAX_NODE_ROLE]; + char node_id[OMNI_MAX_PEER_ID]; + int stats_interval_ms; + pthread_mutex_t lock; + pthread_cond_t cond; + pthread_t thread; + int thread_started; + int stopped; + int refcount; + int request_pending; + uint64_t pending_request_id; + uint64_t completed_request_id; + char pending_reason[32]; + kcp_conn_t *members; + uint64_t prev_bytes_sent; + uint64_t prev_bytes_received; + uint64_t prev_in_pkts; + uint64_t prev_out_pkts; + uint64_t prev_in_segs; + uint64_t prev_out_segs; + uint64_t prev_in_errs; + uint64_t prev_kcp_in_errs; + atomic_uint_fast64_t bytes_sent; + atomic_uint_fast64_t bytes_received; + atomic_uint_fast64_t in_pkts; + atomic_uint_fast64_t out_pkts; + atomic_uint_fast64_t in_segs; + atomic_uint_fast64_t out_segs; + atomic_uint_fast64_t in_errs; + atomic_uint_fast64_t kcp_in_errs; + atomic_uint_fast64_t curr_estab; +}; + +static pthread_mutex_t g_kcp_process_sampler_mu = PTHREAD_MUTEX_INITIALIZER; +static kcp_process_sampler_t *g_kcp_process_samplers = NULL; + +static void kcp_parse_packet_segments(const uint8_t *packet, size_t len, uint32_t *conv, kcp_packet_debug_segment_t **segments, size_t *segment_count) { + size_t offset = 0; + size_t count = 0; + kcp_packet_debug_segment_t *items = NULL; + + if (conv != NULL) { + *conv = 0; + } + if (segments != NULL) { + *segments = NULL; + } + if (segment_count != NULL) { + *segment_count = 0; + } + if (len < 4) { + return; + } + if (conv != NULL) { + *conv = (uint32_t) ((unsigned char) packet[0] | + ((unsigned char) packet[1] << 8) | + ((unsigned char) packet[2] << 16) | + ((unsigned char) packet[3] << 24)); + } + while (offset + 24U <= len) { + uint32_t seg_len = (uint32_t) ((unsigned char) packet[offset + 20] | + ((unsigned char) packet[offset + 21] << 8) | + ((unsigned char) packet[offset + 22] << 16) | + ((unsigned char) packet[offset + 23] << 24)); + if (offset + 24U + seg_len > len) { + free(items); + return; + } + if (segments != NULL) { + kcp_packet_debug_segment_t *next = (kcp_packet_debug_segment_t *) realloc(items, (count + 1U) * sizeof(*items)); + if (next == NULL) { + free(items); + return; + } + items = next; + items[count].cmd = packet[offset + 4]; + items[count].frg = packet[offset + 5]; + items[count].wnd = (uint16_t) ((unsigned char) packet[offset + 6] | ((unsigned char) packet[offset + 7] << 8)); + items[count].sn = (uint32_t) ((unsigned char) packet[offset + 12] | + ((unsigned char) packet[offset + 13] << 8) | + ((unsigned char) packet[offset + 14] << 16) | + ((unsigned char) packet[offset + 15] << 24)); + items[count].una = (uint32_t) ((unsigned char) packet[offset + 16] | + ((unsigned char) packet[offset + 17] << 8) | + ((unsigned char) packet[offset + 18] << 16) | + ((unsigned char) packet[offset + 19] << 24)); + items[count].len = seg_len; + } + count++; + offset += 24U + seg_len; + } + if (segments != NULL) { + *segments = items; + } else { + free(items); + } + if (segment_count != NULL) { + *segment_count = count; + } +} + +static uint64_t kcp_counter_diff(uint64_t previous, uint64_t current) { + return current < previous ? 0 : current - previous; +} + +static int kcp_process_sampler_matches(const kcp_process_sampler_t *sampler, kcp_session_stats_logger_t *logger, const char *node_role, const char *node_id, int stats_interval_ms) { + if (sampler == NULL) { + return 0; + } + return sampler->logger == logger && + sampler->stats_interval_ms == stats_interval_ms && + strcmp(sampler->node_role, node_role == NULL ? "" : node_role) == 0 && + strcmp(sampler->node_id, node_id == NULL ? "" : node_id) == 0; +} + +static void kcp_process_sampler_record_send(kcp_process_sampler_t *sampler, int packet_bytes, size_t segments) { + if (sampler == NULL) { + return; + } + atomic_fetch_add_explicit(&sampler->bytes_sent, (uint64_t) packet_bytes, memory_order_relaxed); + atomic_fetch_add_explicit(&sampler->out_pkts, 1, memory_order_relaxed); + atomic_fetch_add_explicit(&sampler->out_segs, (uint64_t) segments, memory_order_relaxed); +} + +static void kcp_process_sampler_record_input(kcp_process_sampler_t *sampler, int packet_bytes, size_t segments) { + if (sampler == NULL) { + return; + } + atomic_fetch_add_explicit(&sampler->bytes_received, (uint64_t) packet_bytes, memory_order_relaxed); + atomic_fetch_add_explicit(&sampler->in_pkts, 1, memory_order_relaxed); + atomic_fetch_add_explicit(&sampler->in_segs, (uint64_t) segments, memory_order_relaxed); +} + +static void kcp_process_sampler_record_error(kcp_process_sampler_t *sampler) { + if (sampler == NULL) { + return; + } + atomic_fetch_add_explicit(&sampler->in_errs, 1, memory_order_relaxed); + atomic_fetch_add_explicit(&sampler->kcp_in_errs, 1, memory_order_relaxed); +} + +static void kcp_conn_record_send(kcp_conn_t *conn, int packet_bytes, size_t segments) { + if (conn == NULL) { + return; + } + if (conn->process_sampler != NULL) { + kcp_process_sampler_record_send(conn->process_sampler, packet_bytes, segments); + return; + } + conn->pending_bytes_sent += (uint64_t) packet_bytes; + conn->pending_out_pkts += 1; + conn->pending_out_segs += (uint64_t) segments; +} + +static void kcp_conn_record_input(kcp_conn_t *conn, int packet_bytes, size_t segments) { + if (conn == NULL) { + return; + } + if (conn->process_sampler != NULL) { + kcp_process_sampler_record_input(conn->process_sampler, packet_bytes, segments); + return; + } + conn->pending_bytes_received += (uint64_t) packet_bytes; + conn->pending_in_pkts += 1; + conn->pending_in_segs += (uint64_t) segments; +} + +static void kcp_conn_record_error(kcp_conn_t *conn) { + if (conn == NULL) { + return; + } + if (conn->process_sampler != NULL) { + kcp_process_sampler_record_error(conn->process_sampler); + return; + } + conn->pending_in_errs += 1; + conn->pending_kcp_in_errs += 1; +} + +static void kcp_process_sampler_curr_estab_inc(kcp_process_sampler_t *sampler) { + if (sampler == NULL) { + return; + } + atomic_fetch_add_explicit(&sampler->curr_estab, 1, memory_order_relaxed); +} + +static void kcp_process_sampler_curr_estab_dec(kcp_process_sampler_t *sampler) { + uint_fast64_t current; + + if (sampler == NULL) { + return; + } + current = atomic_load_explicit(&sampler->curr_estab, memory_order_relaxed); + while (current > 0) { + if (atomic_compare_exchange_weak_explicit(&sampler->curr_estab, ¤t, current - 1U, memory_order_relaxed, memory_order_relaxed)) { + return; + } + } +} + +static void kcp_process_sampler_add_conn(kcp_process_sampler_t *sampler, kcp_conn_t *conn) { + if (sampler == NULL || conn == NULL) { + return; + } + pthread_mutex_lock(&sampler->lock); + conn->process_next = sampler->members; + sampler->members = conn; + pthread_mutex_unlock(&sampler->lock); + kcp_process_sampler_curr_estab_inc(sampler); +} + +static void kcp_process_sampler_remove_conn(kcp_process_sampler_t *sampler, kcp_conn_t *conn) { + kcp_conn_t *prev = NULL; + kcp_conn_t *cur; + + if (sampler == NULL || conn == NULL) { + return; + } + pthread_mutex_lock(&sampler->lock); + for (cur = sampler->members; cur != NULL; cur = cur->process_next) { + if (cur == conn) { + if (prev == NULL) { + sampler->members = cur->process_next; + } else { + prev->process_next = cur->process_next; + } + conn->process_next = NULL; + break; + } + prev = cur; + } + pthread_mutex_unlock(&sampler->lock); + if (cur == conn) { + kcp_process_sampler_curr_estab_dec(sampler); + } +} + +static void kcp_process_sampler_collect_gauges(kcp_process_sampler_t *sampler, uint64_t *snd_queue, uint64_t *rcv_queue, uint64_t *snd_buffer) { + kcp_conn_t *conn; + + if (snd_queue != NULL) { + *snd_queue = 0; + } + if (rcv_queue != NULL) { + *rcv_queue = 0; + } + if (snd_buffer != NULL) { + *snd_buffer = 0; + } + if (sampler == NULL) { + return; + } + + pthread_mutex_lock(&sampler->lock); + for (conn = sampler->members; conn != NULL; conn = conn->process_next) { + pthread_mutex_lock(&conn->kcp_mu); + if (conn->kcp != NULL) { + if (snd_queue != NULL) { + *snd_queue += conn->kcp->nsnd_que; + } + if (rcv_queue != NULL) { + *rcv_queue += conn->kcp->nrcv_que; + } + if (snd_buffer != NULL) { + *snd_buffer += conn->kcp->nsnd_buf; + } + } + pthread_mutex_unlock(&conn->kcp_mu); + } + pthread_mutex_unlock(&sampler->lock); +} + +static void kcp_process_sampler_log_snapshot(kcp_process_sampler_t *sampler, const char *reason) { + kcp_session_stats_record_t record; + uint64_t bytes_sent; + uint64_t bytes_received; + uint64_t in_pkts; + uint64_t out_pkts; + uint64_t in_segs; + uint64_t out_segs; + uint64_t in_errs; + uint64_t kcp_in_errs; + uint64_t snd_queue = 0; + uint64_t rcv_queue = 0; + uint64_t snd_buffer = 0; + + if (sampler == NULL || sampler->logger == NULL) { + return; + } + + bytes_sent = atomic_load_explicit(&sampler->bytes_sent, memory_order_relaxed); + bytes_received = atomic_load_explicit(&sampler->bytes_received, memory_order_relaxed); + in_pkts = atomic_load_explicit(&sampler->in_pkts, memory_order_relaxed); + out_pkts = atomic_load_explicit(&sampler->out_pkts, memory_order_relaxed); + in_segs = atomic_load_explicit(&sampler->in_segs, memory_order_relaxed); + out_segs = atomic_load_explicit(&sampler->out_segs, memory_order_relaxed); + in_errs = atomic_load_explicit(&sampler->in_errs, memory_order_relaxed); + kcp_in_errs = atomic_load_explicit(&sampler->kcp_in_errs, memory_order_relaxed); + kcp_process_sampler_collect_gauges(sampler, &snd_queue, &rcv_queue, &snd_buffer); + + memset(&record, 0, sizeof(record)); + snprintf(record.record_type, sizeof(record.record_type), "%s", KCP_SESSION_STATS_RECORD_PROCESS_SAMPLE); + snprintf(record.node_role, sizeof(record.node_role), "%s", sampler->node_role); + snprintf(record.node_id, sizeof(record.node_id), "%s", sampler->node_id); + snprintf(record.sample_reason, sizeof(record.sample_reason), "%s", reason == NULL ? "" : reason); + record.ts_unix_nano = omni_now_unix_nano(); + + record.has_bytes_sent = 1; + record.bytes_sent = kcp_counter_diff(sampler->prev_bytes_sent, bytes_sent); + record.has_bytes_received = 1; + record.bytes_received = kcp_counter_diff(sampler->prev_bytes_received, bytes_received); + record.has_in_pkts = 1; + record.in_pkts = kcp_counter_diff(sampler->prev_in_pkts, in_pkts); + record.has_out_pkts = 1; + record.out_pkts = kcp_counter_diff(sampler->prev_out_pkts, out_pkts); + record.has_in_segs = 1; + record.in_segs = kcp_counter_diff(sampler->prev_in_segs, in_segs); + record.has_out_segs = 1; + record.out_segs = kcp_counter_diff(sampler->prev_out_segs, out_segs); + record.has_in_errs = 1; + record.in_errs = kcp_counter_diff(sampler->prev_in_errs, in_errs); + record.has_kcp_in_errs = 1; + record.kcp_in_errs = kcp_counter_diff(sampler->prev_kcp_in_errs, kcp_in_errs); + record.has_ring_buffer_snd_queue = 1; + record.ring_buffer_snd_queue = snd_queue; + record.has_ring_buffer_rcv_queue = 1; + record.ring_buffer_rcv_queue = rcv_queue; + record.has_ring_buffer_snd_buffer = 1; + record.ring_buffer_snd_buffer = snd_buffer; + record.has_curr_estab = 1; + record.curr_estab = atomic_load_explicit(&sampler->curr_estab, memory_order_relaxed); + + sampler->prev_bytes_sent = bytes_sent; + sampler->prev_bytes_received = bytes_received; + sampler->prev_in_pkts = in_pkts; + sampler->prev_out_pkts = out_pkts; + sampler->prev_in_segs = in_segs; + sampler->prev_out_segs = out_segs; + sampler->prev_in_errs = in_errs; + sampler->prev_kcp_in_errs = kcp_in_errs; + + (void) kcp_session_stats_log(sampler->logger, &record); +} + +static void *kcp_process_sampler_thread_main(void *arg) { + kcp_process_sampler_t *sampler = (kcp_process_sampler_t *) arg; + + for (;;) { + int has_request = 0; + uint64_t request_id = 0; + char reason[32]; + struct timespec deadline; + + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += sampler->stats_interval_ms / 1000; + deadline.tv_nsec += (long) (sampler->stats_interval_ms % 1000) * 1000000L; + if (deadline.tv_nsec >= 1000000000L) { + deadline.tv_sec += 1; + deadline.tv_nsec -= 1000000000L; + } + + pthread_mutex_lock(&sampler->lock); + while (!sampler->stopped && !sampler->request_pending) { + int wait_rc = pthread_cond_timedwait(&sampler->cond, &sampler->lock, &deadline); + if (wait_rc == ETIMEDOUT) { + break; + } + } + if (sampler->stopped) { + pthread_mutex_unlock(&sampler->lock); + return NULL; + } + if (sampler->request_pending) { + has_request = 1; + request_id = sampler->pending_request_id; + snprintf(reason, sizeof(reason), "%s", sampler->pending_reason); + sampler->request_pending = 0; + } else { + snprintf(reason, sizeof(reason), "%s", "periodic"); + } + pthread_mutex_unlock(&sampler->lock); + + kcp_process_sampler_log_snapshot(sampler, reason); + + if (has_request) { + pthread_mutex_lock(&sampler->lock); + if (request_id > sampler->completed_request_id) { + sampler->completed_request_id = request_id; + } + pthread_cond_broadcast(&sampler->cond); + pthread_mutex_unlock(&sampler->lock); + } + } +} + +static kcp_process_sampler_t *kcp_process_sampler_acquire(kcp_session_stats_logger_t *logger, const char *node_role, const char *node_id, int stats_interval_ms) { + kcp_process_sampler_t *sampler; + + if (logger == NULL) { + return NULL; + } + + pthread_mutex_lock(&g_kcp_process_sampler_mu); + for (sampler = g_kcp_process_samplers; sampler != NULL; sampler = sampler->next) { + if (kcp_process_sampler_matches(sampler, logger, node_role, node_id, stats_interval_ms)) { + sampler->refcount++; + pthread_mutex_unlock(&g_kcp_process_sampler_mu); + return sampler; + } + } + + sampler = (kcp_process_sampler_t *) calloc(1, sizeof(*sampler)); + if (sampler == NULL) { + pthread_mutex_unlock(&g_kcp_process_sampler_mu); + return NULL; + } + + sampler->logger = logger; + sampler->stats_interval_ms = stats_interval_ms > 0 ? stats_interval_ms : KCP_DEFAULT_STATS_INTERVAL_MS; + sampler->refcount = 1; + snprintf(sampler->node_role, sizeof(sampler->node_role), "%s", node_role == NULL ? "" : node_role); + snprintf(sampler->node_id, sizeof(sampler->node_id), "%s", node_id == NULL ? "" : node_id); + pthread_mutex_init(&sampler->lock, NULL); + pthread_cond_init(&sampler->cond, NULL); + if (pthread_create(&sampler->thread, NULL, kcp_process_sampler_thread_main, sampler) != 0) { + pthread_cond_destroy(&sampler->cond); + pthread_mutex_destroy(&sampler->lock); + free(sampler); + pthread_mutex_unlock(&g_kcp_process_sampler_mu); + return NULL; + } + sampler->thread_started = 1; + sampler->next = g_kcp_process_samplers; + g_kcp_process_samplers = sampler; + pthread_mutex_unlock(&g_kcp_process_sampler_mu); + return sampler; +} + +static void kcp_process_sampler_release(kcp_process_sampler_t *sampler) { + kcp_process_sampler_t **cursor; + + if (sampler == NULL) { + return; + } + + pthread_mutex_lock(&g_kcp_process_sampler_mu); + sampler->refcount--; + if (sampler->refcount > 0) { + pthread_mutex_unlock(&g_kcp_process_sampler_mu); + return; + } + for (cursor = &g_kcp_process_samplers; *cursor != NULL; cursor = &(*cursor)->next) { + if (*cursor == sampler) { + *cursor = sampler->next; + break; + } + } + pthread_mutex_unlock(&g_kcp_process_sampler_mu); + + pthread_mutex_lock(&sampler->lock); + sampler->stopped = 1; + pthread_cond_broadcast(&sampler->cond); + pthread_mutex_unlock(&sampler->lock); + if (sampler->thread_started) { + pthread_join(sampler->thread, NULL); + } + pthread_cond_destroy(&sampler->cond); + pthread_mutex_destroy(&sampler->lock); + free(sampler); +} + +static void kcp_process_sampler_request_sample(kcp_process_sampler_t *sampler, const char *reason) { + if (sampler == NULL) { + return; + } + pthread_mutex_lock(&sampler->lock); + if (!sampler->stopped && !sampler->request_pending) { + sampler->request_pending = 1; + sampler->pending_request_id++; + snprintf(sampler->pending_reason, sizeof(sampler->pending_reason), "%s", reason == NULL ? "" : reason); + pthread_cond_broadcast(&sampler->cond); + } + pthread_mutex_unlock(&sampler->lock); +} + +static void kcp_process_sampler_request_sample_and_wait(kcp_process_sampler_t *sampler, const char *reason) { + uint64_t request_id; + + if (sampler == NULL) { + return; + } + pthread_mutex_lock(&sampler->lock); + if (sampler->stopped) { + pthread_mutex_unlock(&sampler->lock); + return; + } + sampler->request_pending = 1; + request_id = ++sampler->pending_request_id; + snprintf(sampler->pending_reason, sizeof(sampler->pending_reason), "%s", reason == NULL ? "" : reason); + pthread_cond_broadcast(&sampler->cond); + while (!sampler->stopped && sampler->completed_request_id < request_id) { + pthread_cond_wait(&sampler->cond, &sampler->lock); + } + pthread_mutex_unlock(&sampler->lock); +} + +static int kcp_socket_debug_log_record(kcp_socket_debug_state_t *state, const char *event_name, const struct sockaddr_storage *remote_addr, socklen_t remote_addr_len, int packet_bytes, int has_tx_id, uint32_t tx_id, int has_conv, uint32_t conv, const kcp_packet_debug_segment_t *segments, size_t segment_count, int64_t ts_unix_nano) { + char local_addr_text[OMNI_MAX_ADDR_TEXT]; + char remote_addr_text[OMNI_MAX_ADDR_TEXT]; + struct sockaddr_storage local_addr; + socklen_t local_addr_len = sizeof(local_addr); + kcp_packet_debug_record_t record; + + if (state->logger == NULL) { + return 0; + } + memset(&record, 0, sizeof(record)); + getsockname(state->fd, (struct sockaddr *) &local_addr, &local_addr_len); + omni_sockaddr_to_string((struct sockaddr *) &local_addr, local_addr_len, local_addr_text, sizeof(local_addr_text)); + omni_sockaddr_to_string((const struct sockaddr *) remote_addr, remote_addr_len, remote_addr_text, sizeof(remote_addr_text)); + snprintf(record.event, sizeof(record.event), "%s", event_name); + snprintf(record.node_role, sizeof(record.node_role), "%s", state->node_role); + snprintf(record.node_id, sizeof(record.node_id), "%s", state->node_id); + snprintf(record.local_addr, sizeof(record.local_addr), "%s", local_addr_text); + snprintf(record.remote_addr, sizeof(record.remote_addr), "%s", remote_addr_text); + record.packet_bytes = packet_bytes; + record.has_udp_tx_id = has_tx_id; + record.udp_tx_id = tx_id; + record.has_kcp_conv = has_conv; + record.kcp_conv = conv; + record.ts_unix_nano = ts_unix_nano; + if (segment_count > 0) { + record.segments = (kcp_packet_debug_segment_t *) calloc(segment_count, sizeof(*record.segments)); + if (record.segments == NULL) { + return -1; + } + memcpy(record.segments, segments, segment_count * sizeof(*segments)); + record.segment_count = segment_count; + } + kcp_packet_debug_log(state->logger, &record); + kcp_packet_debug_record_clear(&record); + return 0; +} + +static void kcp_socket_debug_pending_free(kcp_packet_debug_pending_t *pending) { + while (pending != NULL) { + kcp_packet_debug_pending_t *next = pending->next; + free(pending->segments); + free(pending); + pending = next; + } +} + +static int kcp_socket_debug_reserve_tx(kcp_socket_debug_state_t *state, const struct sockaddr_storage *remote_addr, socklen_t remote_addr_len, const uint8_t *packet, size_t packet_len, uint32_t *out_tx_id) { + kcp_packet_debug_pending_t *pending; + if (state->logger == NULL) { + *out_tx_id = 0; + return 0; + } + pending = (kcp_packet_debug_pending_t *) calloc(1, sizeof(*pending)); + if (pending == NULL) { + return -1; + } + pending->tx_id = state->next_tx_id++; + pending->packet_bytes = (int) packet_len; + memcpy(&pending->remote_addr, remote_addr, sizeof(*remote_addr)); + pending->remote_addr_len = remote_addr_len; + kcp_parse_packet_segments(packet, packet_len, &pending->conv, &pending->segments, &pending->segment_count); + pending->has_conv = packet_len >= 4; + pthread_mutex_lock(&state->pending_mu); + pending->next = state->pending_head; + state->pending_head = pending; + pthread_mutex_unlock(&state->pending_mu); + *out_tx_id = pending->tx_id; + return 0; +} + +static void kcp_socket_debug_rollback_tx(kcp_socket_debug_state_t *state, uint32_t tx_id) { + kcp_packet_debug_pending_t *prev = NULL; + kcp_packet_debug_pending_t *cur; + pthread_mutex_lock(&state->pending_mu); + for (cur = state->pending_head; cur != NULL; cur = cur->next) { + if (cur->tx_id == tx_id) { + if (prev == NULL) { + state->pending_head = cur->next; + } else { + prev->next = cur->next; + } + free(cur->segments); + free(cur); + break; + } + prev = cur; + } + pthread_mutex_unlock(&state->pending_mu); +} + +static void *kcp_socket_debug_errqueue_thread(void *arg) { + kcp_socket_debug_state_t *state = (kcp_socket_debug_state_t *) arg; + uint8_t control[512]; + uint8_t dummy = 0; + struct iovec iov; + struct msghdr msg; + + while (!atomic_load(&state->closed)) { + ssize_t rc; + omni_tx_timestamp_event_t event; + kcp_packet_debug_pending_t *prev = NULL; + kcp_packet_debug_pending_t *cur = NULL; + + memset(&msg, 0, sizeof(msg)); + iov.iov_base = &dummy; + iov.iov_len = sizeof(dummy); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + rc = recvmsg(state->fd, &msg, MSG_ERRQUEUE | MSG_DONTWAIT); + if (rc < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { + usleep(10000); + continue; + } + if (atomic_load(&state->closed)) { + return NULL; + } + usleep(10000); + continue; + } + if (linux_timestamping_parse_tx_timestamp(&msg, &event) != 0) { + continue; + } + pthread_mutex_lock(&state->pending_mu); + for (cur = state->pending_head; cur != NULL; cur = cur->next) { + if (cur->tx_id == event.ee_data) { + break; + } + prev = cur; + } + if (cur != NULL) { + if (strcmp(event.event_name, EVENT_A_TX_SCHED) == 0) { + cur->saw_sched = 1; + } else if (strcmp(event.event_name, EVENT_A_TX_SOFTWARE) == 0) { + cur->saw_software = 1; + } + kcp_socket_debug_log_record(state, event.event_name, &cur->remote_addr, cur->remote_addr_len, cur->packet_bytes, 1, cur->tx_id, cur->has_conv, cur->conv, cur->segments, cur->segment_count, event.ts_unix_nano); + if (cur->saw_sched && cur->saw_software) { + if (prev == NULL) { + state->pending_head = cur->next; + } else { + prev->next = cur->next; + } + free(cur->segments); + free(cur); + } + } + pthread_mutex_unlock(&state->pending_mu); + } + return NULL; +} + +static int kcp_socket_debug_init(kcp_socket_debug_state_t *state, int fd, kcp_packet_debug_logger_t *logger, const char *node_role, const char *node_id) { + memset(state, 0, sizeof(*state)); + state->fd = fd; + state->logger = logger; + snprintf(state->node_role, sizeof(state->node_role), "%s", node_role == NULL ? "" : node_role); + snprintf(state->node_id, sizeof(state->node_id), "%s", node_id == NULL ? "" : node_id); + pthread_mutex_init(&state->write_mu, NULL); + pthread_mutex_init(&state->pending_mu, NULL); + if (logger != NULL) { + if (linux_timestamping_enable_udp_socket(fd, 1) != 0) { + pthread_mutex_destroy(&state->write_mu); + pthread_mutex_destroy(&state->pending_mu); + return -1; + } + if (pthread_create(&state->errqueue_thread, NULL, kcp_socket_debug_errqueue_thread, state) != 0) { + pthread_mutex_destroy(&state->write_mu); + pthread_mutex_destroy(&state->pending_mu); + return -1; + } + state->errqueue_thread_started = 1; + } + return 0; +} + +static void kcp_socket_debug_destroy(kcp_socket_debug_state_t *state) { + atomic_store(&state->closed, 1); + if (state->errqueue_thread_started) { + pthread_join(state->errqueue_thread, NULL); + } + kcp_socket_debug_pending_free(state->pending_head); + pthread_mutex_destroy(&state->write_mu); + pthread_mutex_destroy(&state->pending_mu); +} + +static int kcp_socket_send_packet(kcp_socket_debug_state_t *state, const struct sockaddr_storage *remote_addr, socklen_t remote_addr_len, const uint8_t *packet, size_t packet_len) { + uint32_t tx_id = 0; + ssize_t rc; + if (state->logger != NULL && kcp_socket_debug_reserve_tx(state, remote_addr, remote_addr_len, packet, packet_len, &tx_id) != 0) { + return -1; + } + pthread_mutex_lock(&state->write_mu); + rc = sendto(state->fd, packet, packet_len, 0, (const struct sockaddr *) remote_addr, remote_addr_len); + pthread_mutex_unlock(&state->write_mu); + if (rc < 0 || (size_t) rc != packet_len) { + if (state->logger != NULL) { + kcp_socket_debug_rollback_tx(state, tx_id); + } + return -1; + } + return 0; +} + +static int kcp_output_callback_impl(const char *buf, int len, struct IKCPCB *kcp, void *user) { + kcp_conn_t *conn = (kcp_conn_t *) user; + size_t segment_count = 0; + (void) kcp; + if (conn == NULL || atomic_load(&conn->closed)) { + return -1; + } + kcp_parse_packet_segments((const uint8_t *) buf, (size_t) len, NULL, NULL, &segment_count); + if (kcp_socket_send_packet(conn->sock_state, &conn->remote_addr, conn->remote_addr_len, (const uint8_t *) buf, (size_t) len) != 0) { + return -1; + } + kcp_conn_record_send(conn, len, segment_count); + return len; +} + +static int kcp_conn_attach_process_sampler(kcp_conn_t *conn) { + kcp_process_sampler_t *next_sampler; + kcp_process_sampler_t *previous_sampler; + uint64_t pending_bytes_sent = 0; + uint64_t pending_bytes_received = 0; + uint64_t pending_in_pkts = 0; + uint64_t pending_out_pkts = 0; + uint64_t pending_in_segs = 0; + uint64_t pending_out_segs = 0; + uint64_t pending_in_errs = 0; + uint64_t pending_kcp_in_errs = 0; + + if (conn == NULL) { + errno = EINVAL; + return -1; + } + + next_sampler = kcp_process_sampler_acquire(conn->stats_logger, conn->node_role, conn->node_id, conn->stats_interval_ms); + if (conn->stats_logger != NULL && next_sampler == NULL) { + return -1; + } + + previous_sampler = conn->process_sampler; + if (previous_sampler == next_sampler) { + return 0; + } + + if (next_sampler != NULL) { + kcp_process_sampler_add_conn(next_sampler, conn); + } + pthread_mutex_lock(&conn->kcp_mu); + previous_sampler = conn->process_sampler; + conn->process_sampler = next_sampler; + pending_bytes_sent = conn->pending_bytes_sent; + pending_bytes_received = conn->pending_bytes_received; + pending_in_pkts = conn->pending_in_pkts; + pending_out_pkts = conn->pending_out_pkts; + pending_in_segs = conn->pending_in_segs; + pending_out_segs = conn->pending_out_segs; + pending_in_errs = conn->pending_in_errs; + pending_kcp_in_errs = conn->pending_kcp_in_errs; + conn->pending_bytes_sent = 0; + conn->pending_bytes_received = 0; + conn->pending_in_pkts = 0; + conn->pending_out_pkts = 0; + conn->pending_in_segs = 0; + conn->pending_out_segs = 0; + conn->pending_in_errs = 0; + conn->pending_kcp_in_errs = 0; + pthread_mutex_unlock(&conn->kcp_mu); + if (next_sampler != NULL) { + atomic_fetch_add_explicit(&next_sampler->bytes_sent, pending_bytes_sent, memory_order_relaxed); + atomic_fetch_add_explicit(&next_sampler->bytes_received, pending_bytes_received, memory_order_relaxed); + atomic_fetch_add_explicit(&next_sampler->in_pkts, pending_in_pkts, memory_order_relaxed); + atomic_fetch_add_explicit(&next_sampler->out_pkts, pending_out_pkts, memory_order_relaxed); + atomic_fetch_add_explicit(&next_sampler->in_segs, pending_in_segs, memory_order_relaxed); + atomic_fetch_add_explicit(&next_sampler->out_segs, pending_out_segs, memory_order_relaxed); + atomic_fetch_add_explicit(&next_sampler->in_errs, pending_in_errs, memory_order_relaxed); + atomic_fetch_add_explicit(&next_sampler->kcp_in_errs, pending_kcp_in_errs, memory_order_relaxed); + } + if (previous_sampler != NULL) { + kcp_process_sampler_remove_conn(previous_sampler, conn); + kcp_process_sampler_release(previous_sampler); + } + return 0; +} + +static void kcp_conn_detach_process_sampler(kcp_conn_t *conn) { + kcp_process_sampler_t *sampler; + + if (conn == NULL || conn->process_sampler == NULL) { + return; + } + + sampler = conn->process_sampler; + conn->process_sampler = NULL; + kcp_process_sampler_remove_conn(sampler, conn); + kcp_process_sampler_release(sampler); +} + +static void kcp_log_session_snapshot(kcp_conn_t *conn, const char *reason) { + kcp_session_stats_record_t record; + struct sockaddr_storage local_addr; + socklen_t local_len = sizeof(local_addr); + char local_text[OMNI_MAX_ADDR_TEXT]; + char remote_text[OMNI_MAX_ADDR_TEXT]; + if (conn == NULL || conn->stats_logger == NULL || conn->sock_state == NULL || conn->kcp == NULL) { + return; + } + memset(&record, 0, sizeof(record)); + snprintf(record.record_type, sizeof(record.record_type), "%s", KCP_SESSION_STATS_RECORD_SESSION_SAMPLE); + snprintf(record.node_role, sizeof(record.node_role), "%s", conn->node_role); + snprintf(record.node_id, sizeof(record.node_id), "%s", conn->node_id); + getsockname(conn->sock_state->fd, (struct sockaddr *) &local_addr, &local_len); + omni_sockaddr_to_string((struct sockaddr *) &local_addr, local_len, local_text, sizeof(local_text)); + omni_sockaddr_to_string((struct sockaddr *) &conn->remote_addr, conn->remote_addr_len, remote_text, sizeof(remote_text)); + snprintf(record.local_addr, sizeof(record.local_addr), "%s", local_text); + snprintf(record.remote_addr, sizeof(record.remote_addr), "%s", remote_text); + record.has_conv = 1; + record.conv = conn->kcp->conv; + record.ts_unix_nano = omni_now_unix_nano(); + snprintf(record.sample_reason, sizeof(record.sample_reason), "%s", reason); + pthread_mutex_lock(&conn->kcp_mu); + record.has_rto_ms = 1; + record.rto_ms = conn->kcp->rx_rto; + record.has_srtt_ms = 1; + record.srtt_ms = conn->kcp->rx_srtt; + record.has_srttvar_ms = 1; + record.srttvar_ms = conn->kcp->rx_rttval; + pthread_mutex_unlock(&conn->kcp_mu); + (void) kcp_session_stats_log(conn->stats_logger, &record); +} + +static void *kcp_stats_thread_main(void *arg) { + kcp_conn_t *conn = (kcp_conn_t *) arg; + while (!atomic_load(&conn->closed)) { + usleep((useconds_t) conn->stats_interval_ms * 1000U); + if (!atomic_load(&conn->closed)) { + kcp_log_session_snapshot(conn, "periodic"); + } + } + return NULL; +} + +static int kcp_socket_open_bound(const char *listen_addr, const char *bind_device, struct sockaddr_storage *local_addr, socklen_t *local_len) { + int family; + int fd; + if (omni_parse_sockaddr(listen_addr, 1, local_addr, local_len, &family) != 0) { + return -1; + } + fd = socket(family, SOCK_DGRAM, 0); + if (fd < 0) { + return -1; + } + if (bind_device != NULL && bind_device[0] != '\0' && omni_bind_device(fd, bind_device) != 0) { + close(fd); + return -1; + } + if (bind(fd, (struct sockaddr *) local_addr, *local_len) != 0) { + close(fd); + return -1; + } + return fd; +} + +static int kcp_socket_open_dial(const char *server_addr, const char *bind_ip, const char *bind_device, struct sockaddr_storage *remote_addr, socklen_t *remote_len, int *family_out) { + int family; + struct sockaddr_storage local_addr; + socklen_t local_len; + int fd; + if (omni_parse_sockaddr(server_addr, 0, remote_addr, remote_len, &family) != 0) { + return -1; + } + fd = socket(family, SOCK_DGRAM, 0); + if (fd < 0) { + return -1; + } + if (bind_device != NULL && bind_device[0] != '\0' && omni_bind_device(fd, bind_device) != 0) { + close(fd); + return -1; + } + if (bind_ip != NULL && bind_ip[0] != '\0') { + struct addrinfo hints; + struct addrinfo *result = NULL; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = family; + hints.ai_socktype = SOCK_DGRAM; + if (getaddrinfo(bind_ip, "0", &hints, &result) != 0 || result == NULL) { + close(fd); + errno = EINVAL; + return -1; + } + memcpy(&local_addr, result->ai_addr, result->ai_addrlen); + local_len = (socklen_t) result->ai_addrlen; + freeaddrinfo(result); + if (bind(fd, (struct sockaddr *) &local_addr, local_len) != 0) { + close(fd); + return -1; + } + } + if (family_out != NULL) { + *family_out = family; + } + return fd; +} + +static int kcp_sockaddr_equal(const struct sockaddr_storage *left, socklen_t left_len, const struct sockaddr_storage *right, socklen_t right_len) { + char left_text[OMNI_MAX_ADDR_TEXT]; + char right_text[OMNI_MAX_ADDR_TEXT]; + + if (left == NULL || right == NULL) { + return left == right; + } + return strcmp( + omni_sockaddr_to_string((const struct sockaddr *) left, left_len, left_text, sizeof(left_text)), + omni_sockaddr_to_string((const struct sockaddr *) right, right_len, right_text, sizeof(right_text)) + ) == 0; +} + +static void *kcp_client_recv_thread_main(void *arg) { + kcp_conn_t *conn = (kcp_conn_t *) arg; + uint8_t buffer[64 * 1024]; + uint8_t control[512]; + struct sockaddr_storage source; + struct iovec iov; + struct msghdr msg; + ssize_t n; + uint32_t conv = 0; + kcp_packet_debug_segment_t *segments = NULL; + size_t segment_count = 0; + int64_t rx_ts; + + while (!atomic_load(&conn->closed)) { + memset(&msg, 0, sizeof(msg)); + memset(&source, 0, sizeof(source)); + iov.iov_base = buffer; + iov.iov_len = sizeof(buffer); + msg.msg_name = &source; + msg.msg_namelen = sizeof(source); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + if (conn->sock_state->logger != NULL) { + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + } + n = recvmsg(conn->fd, &msg, 0); + if (n < 0) { + if (errno == EINTR) { + continue; + } + if (atomic_load(&conn->closed)) { + return NULL; + } + return NULL; + } + kcp_parse_packet_segments(buffer, (size_t) n, &conv, &segments, &segment_count); + rx_ts = conn->sock_state->logger != NULL ? linux_timestamping_parse_rx_timestamp(&msg) : 0; + if (rx_ts > 0) { + kcp_socket_debug_log_record(conn->sock_state, EVENT_B_RX_SOFTWARE, &source, msg.msg_namelen, (int) n, 0, 0, 1, conv, segments, segment_count, rx_ts); + } + if (!kcp_sockaddr_equal(&source, msg.msg_namelen, &conn->remote_addr, conn->remote_addr_len)) { + free(segments); + segments = NULL; + segment_count = 0; + continue; + } + pthread_mutex_lock(&conn->kcp_mu); + conn->kcp->current = omni_now_millis32(); + if (ikcp_input(conn->kcp, (const char *) buffer, n) != 0) { + kcp_conn_record_error(conn); + } else { + kcp_conn_record_input(conn, (int) n, segment_count); + } + pthread_mutex_unlock(&conn->kcp_mu); + pthread_cond_broadcast(&conn->rx_cond); + free(segments); + segments = NULL; + segment_count = 0; + } + return NULL; +} + +static void *kcp_update_thread_main(void *arg) { + kcp_conn_t *conn = (kcp_conn_t *) arg; + while (!atomic_load(&conn->closed)) { + pthread_mutex_lock(&conn->kcp_mu); + ikcp_update(conn->kcp, omni_now_millis32()); + pthread_mutex_unlock(&conn->kcp_mu); + usleep(10000); + } + return NULL; +} + +static int kcp_conn_start_stats_thread(kcp_conn_t *conn) { + if (conn == NULL || conn->stats_logger == NULL || conn->stats_thread_started) { + return 0; + } + if (pthread_create(&conn->stats_thread, NULL, kcp_stats_thread_main, conn) != 0) { + return -1; + } + conn->stats_thread_started = 1; + return 0; +} + +static kcp_conn_t *kcp_conn_alloc_common(int fd, const struct sockaddr_storage *remote_addr, socklen_t remote_addr_len, kcp_socket_debug_state_t *sock_state, latency_logger_t *logger, const char *node_role, const char *node_id, kcp_session_stats_logger_t *stats_logger, int stats_interval_ms) { + kcp_conn_t *conn = (kcp_conn_t *) calloc(1, sizeof(*conn)); + uint32_t conv; + + if (conn == NULL) { + return NULL; + } + conn->fd = fd; + memcpy(&conn->remote_addr, remote_addr, sizeof(*remote_addr)); + conn->remote_addr_len = remote_addr_len; + pthread_mutex_init(&conn->kcp_mu, NULL); + pthread_mutex_init(&conn->close_mu, NULL); + pthread_cond_init(&conn->rx_cond, NULL); + protocol_frame_decoder_init(&conn->decoder); + conn->logger = logger; + snprintf(conn->node_role, sizeof(conn->node_role), "%s", node_role == NULL ? "" : node_role); + snprintf(conn->node_id, sizeof(conn->node_id), "%s", node_id == NULL ? "" : node_id); + conn->stats_logger = stats_logger; + conn->stats_interval_ms = stats_interval_ms > 0 ? stats_interval_ms : KCP_DEFAULT_STATS_INTERVAL_MS; + conn->sock_state = sock_state; + if (omni_random_u32(&conv) != 0) { + protocol_frame_decoder_destroy(&conn->decoder); + pthread_cond_destroy(&conn->rx_cond); + pthread_mutex_destroy(&conn->kcp_mu); + pthread_mutex_destroy(&conn->close_mu); + free(conn); + return NULL; + } + conn->kcp = ikcp_create(conv, conn); + if (conn->kcp == NULL) { + protocol_frame_decoder_destroy(&conn->decoder); + pthread_cond_destroy(&conn->rx_cond); + pthread_mutex_destroy(&conn->kcp_mu); + pthread_mutex_destroy(&conn->close_mu); + free(conn); + return NULL; + } + ikcp_setoutput(conn->kcp, kcp_output_callback_impl); + ikcp_wndsize(conn->kcp, KCP_WND_SIZE, KCP_WND_SIZE); + ikcp_setmtu(conn->kcp, KCP_MTU); + ikcp_nodelay(conn->kcp, KCP_NODELAY, KCP_INTERVAL, KCP_RESEND, KCP_NC); + conn->kcp->stream = 1; + if (kcp_conn_attach_process_sampler(conn) != 0) { + ikcp_release(conn->kcp); + protocol_frame_decoder_destroy(&conn->decoder); + pthread_cond_destroy(&conn->rx_cond); + pthread_mutex_destroy(&conn->kcp_mu); + pthread_mutex_destroy(&conn->close_mu); + free(conn); + return NULL; + } + if (kcp_conn_start_stats_thread(conn) != 0) { + kcp_conn_detach_process_sampler(conn); + ikcp_release(conn->kcp); + protocol_frame_decoder_destroy(&conn->decoder); + pthread_cond_destroy(&conn->rx_cond); + pthread_mutex_destroy(&conn->kcp_mu); + pthread_mutex_destroy(&conn->close_mu); + free(conn); + return NULL; + } + if (pthread_create(&conn->update_thread, NULL, kcp_update_thread_main, conn) != 0) { + if (conn->stats_thread_started) { + atomic_store(&conn->closed, 1); + pthread_join(conn->stats_thread, NULL); + } + kcp_conn_detach_process_sampler(conn); + ikcp_release(conn->kcp); + protocol_frame_decoder_destroy(&conn->decoder); + pthread_cond_destroy(&conn->rx_cond); + pthread_mutex_destroy(&conn->kcp_mu); + pthread_mutex_destroy(&conn->close_mu); + free(conn); + return NULL; + } + conn->update_thread_started = 1; + return conn; +} + +kcp_conn_t *kcp_conn_dial(const char *server_addr, const char *bind_ip, const char *bind_device, kcp_packet_debug_logger_t *packet_logger, latency_logger_t *logger, const char *node_role, const char *node_id, kcp_session_stats_logger_t *stats_logger, int stats_interval_ms) { + struct sockaddr_storage remote_addr; + socklen_t remote_len; + int family; + int fd = kcp_socket_open_dial(server_addr, bind_ip, bind_device, &remote_addr, &remote_len, &family); + kcp_conn_t *conn; + kcp_socket_debug_state_t *sock_state; + (void) family; + if (fd < 0) { + return NULL; + } + sock_state = (kcp_socket_debug_state_t *) calloc(1, sizeof(*sock_state)); + if (sock_state == NULL) { + close(fd); + return NULL; + } + if (kcp_socket_debug_init(sock_state, fd, packet_logger, node_role, node_id) != 0) { + free(sock_state); + close(fd); + return NULL; + } + conn = kcp_conn_alloc_common(fd, &remote_addr, remote_len, sock_state, logger, node_role, node_id, stats_logger, stats_interval_ms); + if (conn == NULL) { + kcp_socket_debug_destroy(sock_state); + free(sock_state); + close(fd); + return NULL; + } + conn->is_client = 1; + conn->owns_socket = 1; + if (pthread_create(&conn->recv_thread, NULL, kcp_client_recv_thread_main, conn) != 0) { + kcp_conn_free(conn); + return NULL; + } + conn->recv_thread_started = 1; + return conn; +} + +static void kcp_listener_enqueue_accept(kcp_listener_t *listener, kcp_conn_t *conn) { + pthread_mutex_lock(&listener->accept_mu); + if (listener->accept_tail == NULL) { + listener->accept_head = conn; + } else { + listener->accept_tail->accept_next = conn; + } + listener->accept_tail = conn; + conn->accept_next = NULL; + pthread_cond_signal(&listener->accept_cond); + pthread_mutex_unlock(&listener->accept_mu); +} + +static kcp_conn_t *kcp_listener_find_session(kcp_listener_t *listener, uint32_t conv) { + kcp_session_entry_t *entry; + for (entry = listener->sessions; entry != NULL; entry = entry->next) { + if (entry->conv == conv) { + return entry->conn; + } + } + return NULL; +} + +static int kcp_listener_add_session(kcp_listener_t *listener, uint32_t conv, kcp_conn_t *conn) { + kcp_session_entry_t *entry = (kcp_session_entry_t *) calloc(1, sizeof(*entry)); + if (entry == NULL) { + return -1; + } + entry->conv = conv; + entry->conn = conn; + entry->next = listener->sessions; + listener->sessions = entry; + return 0; +} + +static void kcp_listener_remove_session(kcp_listener_t *listener, kcp_conn_t *conn) { + kcp_session_entry_t *prev_entry = NULL; + kcp_session_entry_t *entry; + kcp_conn_t *prev_accept = NULL; + kcp_conn_t *accept; + + if (listener == NULL || conn == NULL) { + return; + } + + pthread_mutex_lock(&listener->lock); + for (entry = listener->sessions; entry != NULL; entry = entry->next) { + if (entry->conn == conn) { + if (prev_entry == NULL) { + listener->sessions = entry->next; + } else { + prev_entry->next = entry->next; + } + free(entry); + break; + } + prev_entry = entry; + } + pthread_mutex_unlock(&listener->lock); + + pthread_mutex_lock(&listener->accept_mu); + for (accept = listener->accept_head; accept != NULL; accept = accept->accept_next) { + if (accept == conn) { + if (prev_accept == NULL) { + listener->accept_head = accept->accept_next; + } else { + prev_accept->accept_next = accept->accept_next; + } + if (listener->accept_tail == conn) { + listener->accept_tail = prev_accept; + } + conn->accept_next = NULL; + break; + } + prev_accept = accept; + } + pthread_mutex_unlock(&listener->accept_mu); +} + +static void *kcp_listener_recv_thread_main(void *arg) { + kcp_listener_t *listener = (kcp_listener_t *) arg; + uint8_t buffer[64 * 1024]; + uint8_t control[512]; + struct sockaddr_storage source; + struct iovec iov; + struct msghdr msg; + ssize_t n; + uint32_t conv; + kcp_packet_debug_segment_t *segments = NULL; + size_t segment_count = 0; + int64_t rx_ts; + + while (!listener->closed) { + memset(&msg, 0, sizeof(msg)); + memset(&source, 0, sizeof(source)); + iov.iov_base = buffer; + iov.iov_len = sizeof(buffer); + msg.msg_name = &source; + msg.msg_namelen = sizeof(source); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + if (listener->sock_state.logger != NULL) { + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + } + n = recvmsg(listener->fd, &msg, 0); + if (n < 0) { + if (errno == EINTR) { + continue; + } + if (listener->closed) { + return NULL; + } + return NULL; + } + kcp_parse_packet_segments(buffer, (size_t) n, &conv, &segments, &segment_count); + rx_ts = listener->sock_state.logger != NULL ? linux_timestamping_parse_rx_timestamp(&msg) : 0; + if (rx_ts > 0) { + kcp_socket_debug_log_record(&listener->sock_state, EVENT_B_RX_SOFTWARE, &source, msg.msg_namelen, (int) n, 0, 0, 1, conv, segments, segment_count, rx_ts); + } + pthread_mutex_lock(&listener->lock); + { + kcp_conn_t *conn = kcp_listener_find_session(listener, conv); + if (conn == NULL) { + conn = (kcp_conn_t *) calloc(1, sizeof(*conn)); + if (conn != NULL) { + conn->fd = listener->fd; + memcpy(&conn->remote_addr, &source, sizeof(source)); + conn->remote_addr_len = msg.msg_namelen; + pthread_mutex_init(&conn->kcp_mu, NULL); + pthread_mutex_init(&conn->close_mu, NULL); + pthread_cond_init(&conn->rx_cond, NULL); + protocol_frame_decoder_init(&conn->decoder); + snprintf(conn->node_role, sizeof(conn->node_role), "%s", listener->sock_state.node_role); + snprintf(conn->node_id, sizeof(conn->node_id), "%s", listener->sock_state.node_id); + conn->stats_interval_ms = KCP_DEFAULT_STATS_INTERVAL_MS; + conn->sock_state = &listener->sock_state; + conn->listener = listener; + conn->kcp = ikcp_create(conv, conn); + if (conn->kcp != NULL) { + int update_started = 0; + ikcp_setoutput(conn->kcp, kcp_output_callback_impl); + ikcp_wndsize(conn->kcp, KCP_WND_SIZE, KCP_WND_SIZE); + ikcp_setmtu(conn->kcp, KCP_MTU); + ikcp_nodelay(conn->kcp, KCP_NODELAY, KCP_INTERVAL, KCP_RESEND, KCP_NC); + conn->kcp->stream = 1; + if (pthread_create(&conn->update_thread, NULL, kcp_update_thread_main, conn) == 0) { + update_started = 1; + } + if (update_started && kcp_listener_add_session(listener, conv, conn) == 0) { + conn->update_thread_started = 1; + kcp_listener_enqueue_accept(listener, conn); + } else { + atomic_store(&conn->closed, 1); + if (update_started) { + pthread_join(conn->update_thread, NULL); + } + ikcp_release(conn->kcp); + protocol_frame_decoder_destroy(&conn->decoder); + pthread_cond_destroy(&conn->rx_cond); + pthread_mutex_destroy(&conn->kcp_mu); + pthread_mutex_destroy(&conn->close_mu); + free(conn); + conn = NULL; + } + } else { + protocol_frame_decoder_destroy(&conn->decoder); + pthread_cond_destroy(&conn->rx_cond); + pthread_mutex_destroy(&conn->kcp_mu); + pthread_mutex_destroy(&conn->close_mu); + free(conn); + conn = NULL; + } + } + } + if (conn != NULL && conn->kcp != NULL) { + pthread_mutex_lock(&conn->kcp_mu); + conn->kcp->current = omni_now_millis32(); + if (ikcp_input(conn->kcp, (const char *) buffer, n) != 0) { + kcp_conn_record_error(conn); + } else { + kcp_conn_record_input(conn, (int) n, segment_count); + } + pthread_mutex_unlock(&conn->kcp_mu); + pthread_cond_broadcast(&conn->rx_cond); + } + } + pthread_mutex_unlock(&listener->lock); + free(segments); + segments = NULL; + segment_count = 0; + } + return NULL; +} + +kcp_listener_t *kcp_listener_listen(const char *listen_addr, const char *bind_device, kcp_packet_debug_logger_t *packet_logger, const char *node_role, const char *node_id) { + struct sockaddr_storage local_addr; + socklen_t local_len; + int fd = kcp_socket_open_bound(listen_addr, bind_device, &local_addr, &local_len); + kcp_listener_t *listener; + if (fd < 0) { + return NULL; + } + listener = (kcp_listener_t *) calloc(1, sizeof(*listener)); + if (listener == NULL) { + close(fd); + return NULL; + } + listener->fd = fd; + pthread_mutex_init(&listener->lock, NULL); + pthread_mutex_init(&listener->accept_mu, NULL); + pthread_cond_init(&listener->accept_cond, NULL); + if (kcp_socket_debug_init(&listener->sock_state, fd, packet_logger, node_role, node_id) != 0) { + kcp_listener_free(listener); + return NULL; + } + if (pthread_create(&listener->recv_thread, NULL, kcp_listener_recv_thread_main, listener) != 0) { + kcp_listener_free(listener); + return NULL; + } + listener->recv_thread_started = 1; + return listener; +} + +kcp_conn_t *kcp_listener_accept(kcp_listener_t *listener) { + kcp_conn_t *conn; + if (listener == NULL) { + errno = EINVAL; + return NULL; + } + pthread_mutex_lock(&listener->accept_mu); + while (!listener->closed && listener->accept_head == NULL) { + pthread_cond_wait(&listener->accept_cond, &listener->accept_mu); + } + if (listener->closed) { + pthread_mutex_unlock(&listener->accept_mu); + errno = ECANCELED; + return NULL; + } + conn = listener->accept_head; + listener->accept_head = conn->accept_next; + if (listener->accept_head == NULL) { + listener->accept_tail = NULL; + } + conn->accept_next = NULL; + pthread_mutex_unlock(&listener->accept_mu); + return conn; +} + +int kcp_conn_configure_runtime(kcp_conn_t *conn, latency_logger_t *logger, const char *node_role, const char *node_id, kcp_session_stats_logger_t *stats_logger, int stats_interval_ms) { + if (conn == NULL) { + errno = EINVAL; + return -1; + } + pthread_mutex_lock(&conn->close_mu); + conn->logger = logger; + if (node_role != NULL) { + snprintf(conn->node_role, sizeof(conn->node_role), "%s", node_role); + } + if (node_id != NULL) { + snprintf(conn->node_id, sizeof(conn->node_id), "%s", node_id); + } + conn->stats_logger = stats_logger; + if (stats_interval_ms > 0) { + conn->stats_interval_ms = stats_interval_ms; + } else if (conn->stats_interval_ms <= 0) { + conn->stats_interval_ms = KCP_DEFAULT_STATS_INTERVAL_MS; + } + if (kcp_conn_attach_process_sampler(conn) != 0) { + pthread_mutex_unlock(&conn->close_mu); + return -1; + } + pthread_mutex_unlock(&conn->close_mu); + if (kcp_conn_start_stats_thread(conn) != 0) { + pthread_mutex_lock(&conn->close_mu); + kcp_conn_detach_process_sampler(conn); + pthread_mutex_unlock(&conn->close_mu); + return -1; + } + return 0; +} + +int kcp_conn_send(kcp_conn_t *conn, const message_t *msg) { + uint8_t *frame = NULL; + size_t frame_len = 0; + if (conn == NULL || msg == NULL) { + errno = EINVAL; + return -1; + } + if (protocol_encode_message_stream(msg, &frame, &frame_len) != 0) { + return -1; + } + latencylog_log_message_event(conn->logger, conn->node_role, conn->node_id, EVENT_SEND_HANDOFF_BEGIN, msg); + kcp_log_session_snapshot(conn, "send_handoff_begin"); + kcp_process_sampler_request_sample(conn->process_sampler, "send_handoff_begin"); + pthread_mutex_lock(&conn->kcp_mu); + conn->kcp->current = omni_now_millis32(); + if (ikcp_send(conn->kcp, (const char *) frame, (int) frame_len) != 0) { + pthread_mutex_unlock(&conn->kcp_mu); + free(frame); + return -1; + } + ikcp_flush(conn->kcp); + pthread_mutex_unlock(&conn->kcp_mu); + kcp_log_session_snapshot(conn, "send_handoff_end"); + kcp_process_sampler_request_sample(conn->process_sampler, "send_handoff_end"); + latencylog_log_message_event(conn->logger, conn->node_role, conn->node_id, EVENT_SEND_HANDOFF_END, msg); + free(frame); + return 0; +} + +int kcp_conn_receive(kcp_conn_t *conn, message_t *out_msg) { + uint8_t *frame = NULL; + size_t frame_len = 0; + char err[128]; + int next_rc; + if (conn == NULL || out_msg == NULL) { + errno = EINVAL; + return -1; + } + for (;;) { + next_rc = protocol_frame_decoder_next(&conn->decoder, &frame, &frame_len); + if (next_rc < 0) { + return -1; + } + if (next_rc == 1) { + if (protocol_decode_message_stream_payload(frame, frame_len, out_msg, err, sizeof(err)) != 0) { + free(frame); + errno = EPROTO; + return -1; + } + free(frame); + kcp_log_session_snapshot(conn, "receive"); + kcp_process_sampler_request_sample(conn->process_sampler, "receive"); + return 0; + } + pthread_mutex_lock(&conn->kcp_mu); + { + int n = ikcp_recv(conn->kcp, (char *) conn->scratch, (int) sizeof(conn->scratch)); + if (n > 0) { + if (protocol_frame_decoder_feed(&conn->decoder, conn->scratch, (size_t) n) != 0) { + pthread_mutex_unlock(&conn->kcp_mu); + return -1; + } + pthread_mutex_unlock(&conn->kcp_mu); + continue; + } + if (atomic_load(&conn->closed)) { + pthread_mutex_unlock(&conn->kcp_mu); + errno = ECANCELED; + return -1; + } + pthread_cond_wait(&conn->rx_cond, &conn->kcp_mu); + } + pthread_mutex_unlock(&conn->kcp_mu); + } +} + +uint32_t kcp_conn_conv(const kcp_conn_t *conn) { + return conn == NULL || conn->kcp == NULL ? 0 : conn->kcp->conv; +} + +int kcp_conn_local_addr(const kcp_conn_t *conn, struct sockaddr_storage *addr, socklen_t *addr_len) { + socklen_t len = sizeof(*addr); + if (conn == NULL || addr == NULL || addr_len == NULL || conn->sock_state == NULL) { + errno = EINVAL; + return -1; + } + if (getsockname(conn->sock_state->fd, (struct sockaddr *) addr, &len) != 0) { + return -1; + } + *addr_len = len; + return 0; +} + +int kcp_conn_close(kcp_conn_t *conn) { + if (conn == NULL) { + return 0; + } + pthread_mutex_lock(&conn->close_mu); + if (!atomic_load(&conn->closed)) { + kcp_log_session_snapshot(conn, "close"); + kcp_process_sampler_request_sample_and_wait(conn->process_sampler, "close"); + pthread_mutex_lock(&conn->kcp_mu); + atomic_store(&conn->closed, 1); + if (conn->owns_socket && !conn->socket_closed) { + close(conn->fd); + conn->socket_closed = 1; + } + pthread_cond_broadcast(&conn->rx_cond); + pthread_mutex_unlock(&conn->kcp_mu); + } + pthread_mutex_unlock(&conn->close_mu); + return 0; +} + +void kcp_conn_free(kcp_conn_t *conn) { + if (conn == NULL) { + return; + } + kcp_conn_close(conn); + if (conn->recv_thread_started) { + pthread_join(conn->recv_thread, NULL); + } + if (conn->update_thread_started) { + pthread_join(conn->update_thread, NULL); + } + if (conn->stats_thread_started) { + pthread_join(conn->stats_thread, NULL); + } + if (conn->listener != NULL && !conn->listener->closed) { + kcp_listener_remove_session(conn->listener, conn); + } + kcp_conn_detach_process_sampler(conn); + if (conn->owns_socket && conn->sock_state != NULL) { + if (!conn->socket_closed) { + close(conn->fd); + conn->socket_closed = 1; + } + kcp_socket_debug_destroy(conn->sock_state); + free(conn->sock_state); + } + if (conn->kcp != NULL) { + ikcp_release(conn->kcp); + } + protocol_frame_decoder_destroy(&conn->decoder); + pthread_cond_destroy(&conn->rx_cond); + pthread_mutex_destroy(&conn->kcp_mu); + pthread_mutex_destroy(&conn->close_mu); + free(conn); +} + +int kcp_listener_close(kcp_listener_t *listener) { + if (listener == NULL) { + return 0; + } + if (!listener->closed) { + listener->closed = 1; + close(listener->fd); + pthread_cond_broadcast(&listener->accept_cond); + } + return 0; +} + +void kcp_listener_free(kcp_listener_t *listener) { + kcp_session_entry_t *entry; + kcp_session_entry_t *next; + if (listener == NULL) { + return; + } + kcp_listener_close(listener); + if (listener->recv_thread_started) { + pthread_join(listener->recv_thread, NULL); + } + for (entry = listener->sessions; entry != NULL; entry = next) { + next = entry->next; + entry->conn->listener = NULL; + kcp_conn_free(entry->conn); + free(entry); + } + kcp_socket_debug_destroy(&listener->sock_state); + pthread_mutex_destroy(&listener->lock); + pthread_mutex_destroy(&listener->accept_mu); + pthread_cond_destroy(&listener->accept_cond); + free(listener); +} + +int kcp_session_stats_parse_interval_ms(const char *raw, int *out_ms) { + return omni_parse_duration_ms(raw, KCP_DEFAULT_STATS_INTERVAL_MS, out_ms); +} diff --git a/c/src/transport_udp.c b/c/src/transport_udp.c new file mode 100644 index 0000000..c296082 --- /dev/null +++ b/c/src/transport_udp.c @@ -0,0 +1,476 @@ +#include "transport_udp.h" + +#include +#include +#include +#include + +typedef struct udp_pending_tx { + struct udp_pending_tx *next; + uint32_t tx_id; + message_t msg; + int bytes_written; + int saw_sched; + int saw_software; +} udp_pending_tx_t; + +struct udp_conn { + int fd; + int connected; + int timestamping_enabled; + latency_logger_t *logger; + tx_timestamp_debug_logger_t *debug_logger; + char node_role[OMNI_MAX_NODE_ROLE]; + char node_id[OMNI_MAX_PEER_ID]; + pthread_mutex_t write_mu; + pthread_mutex_t pending_mu; + pthread_t errqueue_thread; + int errqueue_thread_started; + uint32_t next_tx_id; + udp_pending_tx_t *pending_head; + uint8_t *recv_buffer; + size_t recv_buffer_cap; + int closed; +}; + +static int udp_open_socket_for_addr(const struct sockaddr *addr, socklen_t addr_len, int bind_device, const char *device) { + int fd; + int reuse = 1; + fd = socket(addr->sa_family, SOCK_DGRAM, 0); + if (fd < 0) { + return -1; + } + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + if (bind_device && omni_bind_device(fd, device) != 0) { + close(fd); + return -1; + } + (void) addr_len; + return fd; +} + +static int udp_resolve_ip_only(const char *ip, int family, struct sockaddr_storage *out, socklen_t *out_len) { + struct addrinfo hints; + struct addrinfo *result = NULL; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = family; + hints.ai_socktype = SOCK_DGRAM; + if (getaddrinfo(ip, "0", &hints, &result) != 0 || result == NULL) { + errno = EINVAL; + return -1; + } + memcpy(out, result->ai_addr, result->ai_addrlen); + *out_len = (socklen_t) result->ai_addrlen; + freeaddrinfo(result); + return 0; +} + +static udp_conn_t *udp_conn_alloc(int fd, int connected, int enable_timestamping, latency_logger_t *logger, const char *node_role, const char *node_id, tx_timestamp_debug_logger_t *debug_logger) { + udp_conn_t *conn = (udp_conn_t *) calloc(1, sizeof(*conn)); + if (conn == NULL) { + return NULL; + } + conn->fd = fd; + conn->connected = connected; + conn->timestamping_enabled = enable_timestamping; + conn->logger = logger; + conn->debug_logger = debug_logger; + snprintf(conn->node_role, sizeof(conn->node_role), "%s", node_role == NULL ? "" : node_role); + snprintf(conn->node_id, sizeof(conn->node_id), "%s", node_id == NULL ? "" : node_id); + conn->recv_buffer = (uint8_t *) malloc(OMNI_MAX_FRAME_SIZE); + if (conn->recv_buffer == NULL) { + free(conn); + return NULL; + } + conn->recv_buffer_cap = OMNI_MAX_FRAME_SIZE; + pthread_mutex_init(&conn->write_mu, NULL); + pthread_mutex_init(&conn->pending_mu, NULL); + return conn; +} + +static void udp_pending_destroy(udp_pending_tx_t *pending) { + while (pending != NULL) { + udp_pending_tx_t *next = pending->next; + protocol_message_clear(&pending->msg); + free(pending); + pending = next; + } +} + +static int udp_debug_log_send_chunk(udp_conn_t *conn, const message_t *msg, int bytes_written, uint32_t tx_id) { + tx_timestamp_debug_record_t record; + if (conn->debug_logger == NULL) { + return 0; + } + memset(&record, 0, sizeof(record)); + snprintf(record.record_type, sizeof(record.record_type), "%s", TX_TIMESTAMP_DEBUG_RECORD_SEND_CHUNK); + snprintf(record.node_role, sizeof(record.node_role), "%s", conn->node_role); + snprintf(record.node_id, sizeof(record.node_id), "%s", conn->node_id); + record.message_type = msg->type; + record.message_id = msg->id; + snprintf(record.from, sizeof(record.from), "%s", msg->from); + snprintf(record.to, sizeof(record.to), "%s", msg->to); + snprintf(record.file_name, sizeof(record.file_name), "%s", msg->file_name); + record.body_size = (int) msg->body_len; + record.send_call_index = 0; + record.frame_offset_start = 0; + record.frame_offset_end = bytes_written > 0 ? bytes_written - 1 : 0; + record.bytes_written = bytes_written; + record.expected_tx_id = tx_id; + return tx_timestamp_debug_log(conn->debug_logger, &record); +} + +static void udp_debug_log_errqueue_event(udp_conn_t *conn, const message_t *msg, const omni_tx_timestamp_event_t *event, uint32_t tx_id, int selected) { + tx_timestamp_debug_record_t record; + if (conn->debug_logger == NULL) { + return; + } + memset(&record, 0, sizeof(record)); + snprintf(record.record_type, sizeof(record.record_type), "%s", TX_TIMESTAMP_DEBUG_RECORD_ERRQUEUE_EVENT); + snprintf(record.node_role, sizeof(record.node_role), "%s", conn->node_role); + snprintf(record.node_id, sizeof(record.node_id), "%s", conn->node_id); + record.message_type = msg->type; + record.message_id = msg->id; + snprintf(record.from, sizeof(record.from), "%s", msg->from); + snprintf(record.to, sizeof(record.to), "%s", msg->to); + snprintf(record.file_name, sizeof(record.file_name), "%s", msg->file_name); + record.body_size = (int) msg->body_len; + snprintf(record.phase, sizeof(record.phase), "%s", "background"); + record.read_index = 0; + snprintf(record.event_name, sizeof(record.event_name), "%s", event->event_name); + record.ts_unix_nano = event->ts_unix_nano; + record.ee_info = event->ee_info; + record.ee_data = event->ee_data; + record.expected_tx_id = tx_id; + record.selected_for_latency = selected; + tx_timestamp_debug_log(conn->debug_logger, &record); +} + +static void *udp_errqueue_thread_main(void *arg) { + udp_conn_t *conn = (udp_conn_t *) arg; + uint8_t control[512]; + struct msghdr msg; + struct iovec iov; + uint8_t dummy; + + while (!conn->closed) { + ssize_t rc; + omni_tx_timestamp_event_t event; + udp_pending_tx_t *prev = NULL; + udp_pending_tx_t *cur = NULL; + memset(&msg, 0, sizeof(msg)); + memset(control, 0, sizeof(control)); + dummy = 0; + iov.iov_base = &dummy; + iov.iov_len = sizeof(dummy); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + rc = recvmsg(conn->fd, &msg, MSG_ERRQUEUE | MSG_DONTWAIT); + if (rc < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { + usleep(10000); + continue; + } + if (conn->closed) { + return NULL; + } + usleep(10000); + continue; + } + if (linux_timestamping_parse_tx_timestamp(&msg, &event) != 0) { + continue; + } + pthread_mutex_lock(&conn->pending_mu); + cur = NULL; + prev = NULL; + { + udp_pending_tx_t **head = &conn->pending_head; + udp_pending_tx_t *iter = *head; + while (iter != NULL) { + if (iter->tx_id == event.ee_data) { + cur = iter; + break; + } + prev = iter; + iter = iter->next; + } + if (cur != NULL) { + if (strcmp(event.event_name, EVENT_A_TX_SCHED) == 0 && !cur->saw_sched) { + cur->saw_sched = 1; + latencylog_log_message_event_at(conn->logger, conn->node_role, conn->node_id, EVENT_A_TX_SCHED, event.ts_unix_nano, &cur->msg); + } else if (strcmp(event.event_name, EVENT_A_TX_SOFTWARE) == 0 && !cur->saw_software) { + cur->saw_software = 1; + latencylog_log_message_event_at(conn->logger, conn->node_role, conn->node_id, EVENT_A_TX_SOFTWARE, event.ts_unix_nano, &cur->msg); + } + udp_debug_log_errqueue_event(conn, &cur->msg, &event, cur->tx_id, 1); + if (cur->saw_sched && cur->saw_software) { + if (prev == NULL) { + *head = cur->next; + } else { + prev->next = cur->next; + } + protocol_message_clear(&cur->msg); + free(cur); + } + } + } + pthread_mutex_unlock(&conn->pending_mu); + } + return NULL; +} + +static int udp_conn_start_errqueue(udp_conn_t *conn) { + if (!conn->timestamping_enabled) { + return 0; + } + if (pthread_create(&conn->errqueue_thread, NULL, udp_errqueue_thread_main, conn) != 0) { + return -1; + } + conn->errqueue_thread_started = 1; + return 0; +} + +udp_conn_t *udp_conn_dial(const char *server_addr, const char *bind_ip, const char *bind_device, int enable_timestamping, latency_logger_t *logger, const char *node_role, const char *node_id, tx_timestamp_debug_logger_t *debug_logger) { + struct sockaddr_storage remote_addr; + struct sockaddr_storage local_addr; + socklen_t remote_len; + socklen_t local_len; + int family; + int fd; + udp_conn_t *conn; + + if (omni_parse_sockaddr(server_addr, 0, &remote_addr, &remote_len, &family) != 0) { + return NULL; + } + fd = udp_open_socket_for_addr((struct sockaddr *) &remote_addr, remote_len, bind_device != NULL && bind_device[0] != '\0', bind_device); + if (fd < 0) { + return NULL; + } + if (bind_ip != NULL && bind_ip[0] != '\0') { + if (udp_resolve_ip_only(bind_ip, family, &local_addr, &local_len) != 0 || bind(fd, (struct sockaddr *) &local_addr, local_len) != 0) { + close(fd); + return NULL; + } + } + if (connect(fd, (struct sockaddr *) &remote_addr, remote_len) != 0) { + close(fd); + return NULL; + } + if (enable_timestamping && linux_timestamping_enable_udp_socket(fd, 1) != 0) { + close(fd); + return NULL; + } + conn = udp_conn_alloc(fd, 1, enable_timestamping, logger, node_role, node_id, debug_logger); + if (conn == NULL) { + close(fd); + return NULL; + } + if (udp_conn_start_errqueue(conn) != 0) { + udp_conn_free(conn); + return NULL; + } + return conn; +} + +udp_conn_t *udp_conn_bind(const char *listen_addr, const char *bind_device, int enable_timestamping, latency_logger_t *logger, const char *node_role, const char *node_id, tx_timestamp_debug_logger_t *debug_logger) { + struct sockaddr_storage local_addr; + socklen_t local_len; + int family; + int fd; + udp_conn_t *conn; + if (omni_parse_sockaddr(listen_addr, 1, &local_addr, &local_len, &family) != 0) { + return NULL; + } + fd = udp_open_socket_for_addr((struct sockaddr *) &local_addr, local_len, bind_device != NULL && bind_device[0] != '\0', bind_device); + if (fd < 0) { + return NULL; + } + if (bind(fd, (struct sockaddr *) &local_addr, local_len) != 0) { + close(fd); + return NULL; + } + if (enable_timestamping && linux_timestamping_enable_udp_socket(fd, 1) != 0) { + close(fd); + return NULL; + } + conn = udp_conn_alloc(fd, 0, enable_timestamping, logger, node_role, node_id, debug_logger); + if (conn == NULL) { + close(fd); + return NULL; + } + if (udp_conn_start_errqueue(conn) != 0) { + udp_conn_free(conn); + return NULL; + } + return conn; +} + +static int udp_conn_send_inner(udp_conn_t *conn, const message_t *msg, const struct sockaddr *addr, socklen_t addr_len) { + uint8_t *payload = NULL; + size_t payload_len = 0; + ssize_t rc; + udp_pending_tx_t *pending = NULL; + uint32_t tx_id; + + if (protocol_encode_message_datagram(msg, &payload, &payload_len) != 0) { + return -1; + } + pthread_mutex_lock(&conn->write_mu); + latencylog_log_message_event(conn->logger, conn->node_role, conn->node_id, EVENT_SEND_HANDOFF_BEGIN, msg); + tx_id = conn->next_tx_id++; + if (conn->timestamping_enabled) { + pending = (udp_pending_tx_t *) calloc(1, sizeof(*pending)); + if (pending == NULL || protocol_message_copy(&pending->msg, msg) != 0) { + free(payload); + pthread_mutex_unlock(&conn->write_mu); + free(pending); + return -1; + } + pending->tx_id = tx_id; + pending->bytes_written = (int) payload_len; + pthread_mutex_lock(&conn->pending_mu); + pending->next = conn->pending_head; + conn->pending_head = pending; + pthread_mutex_unlock(&conn->pending_mu); + udp_debug_log_send_chunk(conn, msg, (int) payload_len, tx_id); + } + if (addr != NULL) { + rc = sendto(conn->fd, payload, payload_len, 0, addr, addr_len); + } else { + rc = send(conn->fd, payload, payload_len, 0); + } + free(payload); + if (rc < 0 || (size_t) rc != payload_len) { + if (pending != NULL) { + udp_pending_tx_t *prev = NULL; + udp_pending_tx_t *cur; + pthread_mutex_lock(&conn->pending_mu); + for (cur = conn->pending_head; cur != NULL; cur = cur->next) { + if (cur == pending) { + if (prev == NULL) { + conn->pending_head = cur->next; + } else { + prev->next = cur->next; + } + break; + } + prev = cur; + } + pthread_mutex_unlock(&conn->pending_mu); + protocol_message_clear(&pending->msg); + free(pending); + } + pthread_mutex_unlock(&conn->write_mu); + return -1; + } + latencylog_log_message_event(conn->logger, conn->node_role, conn->node_id, EVENT_SEND_HANDOFF_END, msg); + pthread_mutex_unlock(&conn->write_mu); + return 0; +} + +int udp_conn_send(udp_conn_t *conn, const message_t *msg) { + if (conn == NULL || !conn->connected) { + errno = ENOTCONN; + return -1; + } + return udp_conn_send_inner(conn, msg, NULL, 0); +} + +int udp_conn_send_to(udp_conn_t *conn, const message_t *msg, const struct sockaddr *addr, socklen_t addr_len) { + if (conn == NULL || addr == NULL) { + errno = EINVAL; + return -1; + } + return udp_conn_send_inner(conn, msg, addr, addr_len); +} + +int udp_conn_receive(udp_conn_t *conn, message_t *out_msg, struct sockaddr_storage *addr, socklen_t *addr_len) { + uint8_t control[512]; + struct sockaddr_storage source; + struct iovec iov; + struct msghdr msg; + ssize_t n; + int64_t rx_ts = 0; + char err[128]; + + if (conn == NULL || out_msg == NULL) { + errno = EINVAL; + return -1; + } + memset(&msg, 0, sizeof(msg)); + memset(&source, 0, sizeof(source)); + iov.iov_base = conn->recv_buffer; + iov.iov_len = conn->recv_buffer_cap; + msg.msg_name = &source; + msg.msg_namelen = sizeof(source); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + if (conn->timestamping_enabled) { + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + } + n = recvmsg(conn->fd, &msg, 0); + if (n < 0) { + return -1; + } + if (conn->timestamping_enabled) { + rx_ts = linux_timestamping_parse_rx_timestamp(&msg); + } + if (protocol_decode_message_datagram(conn->recv_buffer, (size_t) n, out_msg, err, sizeof(err)) != 0) { + errno = EPROTO; + return -1; + } + if (addr != NULL && addr_len != NULL) { + omni_clone_sockaddr((struct sockaddr *) &source, msg.msg_namelen, addr, addr_len); + } + if (rx_ts > 0) { + latencylog_log_message_event_at(conn->logger, conn->node_role, conn->node_id, EVENT_B_RX_SOFTWARE, rx_ts, out_msg); + } + return 0; +} + +int udp_conn_fd(const udp_conn_t *conn) { + return conn == NULL ? -1 : conn->fd; +} + +int udp_conn_local_addr(const udp_conn_t *conn, struct sockaddr_storage *addr, socklen_t *addr_len) { + socklen_t len = sizeof(*addr); + if (conn == NULL || addr == NULL || addr_len == NULL) { + errno = EINVAL; + return -1; + } + if (getsockname(conn->fd, (struct sockaddr *) addr, &len) != 0) { + return -1; + } + *addr_len = len; + return 0; +} + +int udp_conn_close(udp_conn_t *conn) { + if (conn == NULL) { + return 0; + } + if (!conn->closed) { + conn->closed = 1; + close(conn->fd); + if (conn->errqueue_thread_started) { + pthread_join(conn->errqueue_thread, NULL); + conn->errqueue_thread_started = 0; + } + } + return 0; +} + +void udp_conn_free(udp_conn_t *conn) { + if (conn == NULL) { + return; + } + udp_conn_close(conn); + udp_pending_destroy(conn->pending_head); + free(conn->recv_buffer); + pthread_mutex_destroy(&conn->write_mu); + pthread_mutex_destroy(&conn->pending_mu); + free(conn); +} diff --git a/c/src/tx_timestamp_debug.c b/c/src/tx_timestamp_debug.c new file mode 100644 index 0000000..77f1bef --- /dev/null +++ b/c/src/tx_timestamp_debug.c @@ -0,0 +1,108 @@ +#include "tx_timestamp_debug.h" + +tx_timestamp_debug_logger_t *tx_timestamp_debug_open_jsonl(const char *path) { + tx_timestamp_debug_logger_t *logger; + FILE *file; + if (path == NULL || path[0] == '\0') { + return NULL; + } + if (omni_ensure_parent_dir(path) != 0) { + return NULL; + } + file = fopen(path, "ab"); + if (file == NULL) { + return NULL; + } + logger = (tx_timestamp_debug_logger_t *) calloc(1, sizeof(*logger)); + if (logger == NULL) { + fclose(file); + return NULL; + } + omni_file_logger_init(&logger->file_logger, file); + logger->enabled = 1; + return logger; +} + +void tx_timestamp_debug_close(tx_timestamp_debug_logger_t *logger) { + if (logger == NULL) { + return; + } + if (logger->file_logger.file != NULL) { + fclose(logger->file_logger.file); + } + omni_file_logger_destroy(&logger->file_logger); + free(logger); +} + +int tx_timestamp_debug_log(tx_timestamp_debug_logger_t *logger, const tx_timestamp_debug_record_t *record) { + char *line; + char *node_role; + char *node_id; + char *from; + char *to; + char *file_name; + char *phase; + char *event_name; + + if (logger == NULL || record == NULL || !logger->enabled) { + return 0; + } + node_role = omni_json_escape(record->node_role); + node_id = omni_json_escape(record->node_id); + from = omni_json_escape(record->from); + to = omni_json_escape(record->to); + file_name = omni_json_escape(record->file_name); + phase = omni_json_escape(record->phase); + event_name = omni_json_escape(record->event_name); + if (node_role == NULL || node_id == NULL || from == NULL || to == NULL || file_name == NULL || phase == NULL || event_name == NULL) { + free(node_role); + free(node_id); + free(from); + free(to); + free(file_name); + free(phase); + free(event_name); + return -1; + } + line = omni_strdup_printf( + "{\"record_type\":\"%s\",\"node_role\":\"%s\",\"node_id\":\"%s\",\"message_type\":\"%s\",\"message_id\":%" PRIu64 ",\"from\":\"%s\",\"to\":\"%s\",\"file_name\":\"%s\",\"body_size\":%d,\"phase\":\"%s\",\"send_call_index\":%d,\"frame_offset_start\":%d,\"frame_offset_end\":%d,\"bytes_written\":%d,\"expected_tx_id\":%u,\"read_index\":%d,\"event_name\":\"%s\",\"ts_unix_nano\":%" PRId64 ",\"ee_info\":%u,\"ee_data\":%u,\"matched_send_call_index\":%d,\"selected_for_latency\":%d}", + record->record_type, + node_role, + node_id, + protocol_message_type_name(record->message_type), + record->message_id, + from, + to, + file_name, + record->body_size, + phase, + record->send_call_index, + record->frame_offset_start, + record->frame_offset_end, + record->bytes_written, + record->expected_tx_id, + record->read_index, + event_name, + record->ts_unix_nano, + record->ee_info, + record->ee_data, + record->matched_send_call_index, + record->selected_for_latency + ); + free(node_role); + free(node_id); + free(from); + free(to); + free(file_name); + free(phase); + free(event_name); + if (line == NULL) { + return -1; + } + if (omni_file_logger_write_line(&logger->file_logger, line) != 0) { + free(line); + return -1; + } + free(line); + return 0; +} diff --git a/c/third_party/cjson/cJSON.c b/c/third_party/cjson/cJSON.c new file mode 100644 index 0000000..702ea61 --- /dev/null +++ b/c/third_party/cjson/cJSON.c @@ -0,0 +1,3302 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +/* cJSON */ +/* JSON parser in C. */ + +/* disable warnings about old C89 functions in MSVC */ +#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) +#define _CRT_SECURE_NO_DEPRECATE +#endif + +#ifdef __GNUC__ +#pragma GCC visibility push(default) +#endif +#if defined(_MSC_VER) +#pragma warning(push) +/* disable warning about single line comments in system headers */ +#pragma warning(disable : 4001) +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_LOCALES +#include +#endif + +#if defined(_MSC_VER) +#pragma warning(pop) +#endif +#ifdef __GNUC__ +#pragma GCC visibility pop +#endif + +#include "cJSON.h" + +/* define our own boolean type */ +#ifdef true +#undef true +#endif +#define true ((cJSON_bool)1) + +#ifdef false +#undef false +#endif +#define false ((cJSON_bool)0) + +/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */ +#ifndef isinf +#define isinf(d) (isnan((d - d)) && !isnan(d)) +#endif +#ifndef isnan +#define isnan(d) (d != d) +#endif + +#ifndef NAN +#ifdef _WIN32 +#define NAN sqrt(-1.0) +#else +#define NAN 0.0 / 0.0 +#endif +#endif + +typedef struct +{ + const unsigned char *json; + size_t position; +} error; +static error global_error = {NULL, 0}; + +CJSON_PUBLIC(const char *) +cJSON_GetErrorPtr(void) +{ + return (const char *)(global_error.json + global_error.position); +} + +CJSON_PUBLIC(char *) +cJSON_GetStringValue(const cJSON *const item) +{ + if (!cJSON_IsString(item)) + { + return NULL; + } + + return item->valuestring; +} + +CJSON_PUBLIC(double) +cJSON_GetNumberValue(const cJSON *const item) +{ + if (!cJSON_IsNumber(item)) + { + return (double)NAN; + } + + return item->valuedouble; +} + +/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */ +#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 19) +#error cJSON.h and cJSON.c have different versions. Make sure that both have the same. +#endif + +CJSON_PUBLIC(const char *) +cJSON_Version(void) +{ + static char version[15]; + sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); + + return version; +} + +/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */ +static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2) +{ + if ((string1 == NULL) || (string2 == NULL)) + { + return 1; + } + + if (string1 == string2) + { + return 0; + } + + for (; tolower(*string1) == tolower(*string2); (void)string1++, string2++) + { + if (*string1 == '\0') + { + return 0; + } + } + + return tolower(*string1) - tolower(*string2); +} + +typedef struct internal_hooks +{ + void *(CJSON_CDECL *allocate)(size_t size); + void(CJSON_CDECL *deallocate)(void *pointer); + void *(CJSON_CDECL *reallocate)(void *pointer, size_t size); +} internal_hooks; + +#if defined(_MSC_VER) +/* work around MSVC error C2322: '...' address of dllimport '...' is not static */ +static void *CJSON_CDECL internal_malloc(size_t size) +{ + return malloc(size); +} +static void CJSON_CDECL internal_free(void *pointer) +{ + free(pointer); +} +static void *CJSON_CDECL internal_realloc(void *pointer, size_t size) +{ + return realloc(pointer, size); +} +#else +#define internal_malloc malloc +#define internal_free free +#define internal_realloc realloc +#endif + +/* strlen of character literals resolved at compile time */ +#define static_strlen(string_literal) (sizeof(string_literal) - sizeof("")) + +static internal_hooks global_hooks = {internal_malloc, internal_free, internal_realloc}; + +static unsigned char *cJSON_strdup(const unsigned char *string, const internal_hooks *const hooks) +{ + size_t length = 0; + unsigned char *copy = NULL; + + if (string == NULL) + { + return NULL; + } + + length = strlen((const char *)string) + sizeof(""); + copy = (unsigned char *)hooks->allocate(length); + if (copy == NULL) + { + return NULL; + } + memcpy(copy, string, length); + + return copy; +} + +CJSON_PUBLIC(void) +cJSON_InitHooks(cJSON_Hooks *hooks) +{ + if (hooks == NULL) + { + /* Reset hooks */ + global_hooks.allocate = malloc; + global_hooks.deallocate = free; + global_hooks.reallocate = realloc; + return; + } + + global_hooks.allocate = malloc; + if (hooks->malloc_fn != NULL) + { + global_hooks.allocate = hooks->malloc_fn; + } + + global_hooks.deallocate = free; + if (hooks->free_fn != NULL) + { + global_hooks.deallocate = hooks->free_fn; + } + + /* use realloc only if both free and malloc are used */ + global_hooks.reallocate = NULL; + if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) + { + global_hooks.reallocate = realloc; + } +} + +/* Internal constructor. */ +static cJSON *cJSON_New_Item(const internal_hooks *const hooks) +{ + cJSON *node = (cJSON *)hooks->allocate(sizeof(cJSON)); + if (node) + { + memset(node, '\0', sizeof(cJSON)); + } + + return node; +} + +/* Delete a cJSON structure. */ +CJSON_PUBLIC(void) +cJSON_Delete(cJSON *item) +{ + cJSON *next = NULL; + while (item != NULL) + { + next = item->next; + if (!(item->type & cJSON_IsReference) && (item->child != NULL)) + { + cJSON_Delete(item->child); + } + if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) + { + global_hooks.deallocate(item->valuestring); + item->valuestring = NULL; + } + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + global_hooks.deallocate(item->string); + item->string = NULL; + } + global_hooks.deallocate(item); + item = next; + } +} + +/* get the decimal point character of the current locale */ +static unsigned char get_decimal_point(void) +{ +#ifdef ENABLE_LOCALES + struct lconv *lconv = localeconv(); + return (unsigned char)lconv->decimal_point[0]; +#else + return '.'; +#endif +} + +typedef struct +{ + const unsigned char *content; + size_t length; + size_t offset; + size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */ + internal_hooks hooks; +} parse_buffer; + +/* check if the given size is left to read in a given parse buffer (starting with 1) */ +#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length)) +/* check if the buffer can be accessed at the given index (starting with 0) */ +#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length)) +#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index)) +/* get a pointer to the buffer at the position */ +#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset) + +/* Parse the input text to generate a number, and populate the result into item. */ +static cJSON_bool parse_number(cJSON *const item, parse_buffer *const input_buffer) +{ + double number = 0; + unsigned char *after_end = NULL; + unsigned char *number_c_string; + unsigned char decimal_point = get_decimal_point(); + size_t i = 0; + size_t number_string_length = 0; + cJSON_bool has_decimal_point = false; + + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; + } + + /* copy the number into a temporary buffer and replace '.' with the decimal point + * of the current locale (for strtod) + * This also takes care of '\0' not necessarily being available for marking the end of the input */ + for (i = 0; can_access_at_index(input_buffer, i); i++) + { + switch (buffer_at_offset(input_buffer)[i]) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '+': + case '-': + case 'e': + case 'E': + number_string_length++; + break; + + case '.': + number_string_length++; + has_decimal_point = true; + break; + + default: + goto loop_end; + } + } +loop_end: + /* malloc for temporary buffer, add 1 for '\0' */ + number_c_string = (unsigned char *)input_buffer->hooks.allocate(number_string_length + 1); + if (number_c_string == NULL) + { + return false; /* allocation failure */ + } + + memcpy(number_c_string, buffer_at_offset(input_buffer), number_string_length); + number_c_string[number_string_length] = '\0'; + + if (has_decimal_point) + { + for (i = 0; i < number_string_length; i++) + { + if (number_c_string[i] == '.') + { + /* replace '.' with the decimal point of the current locale (for strtod) */ + number_c_string[i] = decimal_point; + } + } + } + + number = strtod((const char *)number_c_string, (char **)&after_end); + if (number_c_string == after_end) + { + /* free the temporary buffer */ + input_buffer->hooks.deallocate(number_c_string); + return false; /* parse_error */ + } + + item->valuedouble = number; + + /* use saturation in case of overflow */ + if (number >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)number; + } + + item->type = cJSON_Number; + + input_buffer->offset += (size_t)(after_end - number_c_string); + /* free the temporary buffer */ + input_buffer->hooks.deallocate(number_c_string); + return true; +} + +/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */ +CJSON_PUBLIC(double) +cJSON_SetNumberHelper(cJSON *object, double number) +{ + if (object == NULL) + { + return (double)NAN; + } + + if (number >= INT_MAX) + { + object->valueint = INT_MAX; + } + else if (number <= (double)INT_MIN) + { + object->valueint = INT_MIN; + } + else + { + object->valueint = (int)number; + } + + return object->valuedouble = number; +} + +/* Note: when passing a NULL valuestring, cJSON_SetValuestring treats this as an error and return NULL */ +CJSON_PUBLIC(char *) +cJSON_SetValuestring(cJSON *object, const char *valuestring) +{ + char *copy = NULL; + size_t v1_len; + size_t v2_len; + /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */ + if ((object == NULL) || !(object->type & cJSON_String) || (object->type & cJSON_IsReference)) + { + return NULL; + } + /* return NULL if the object is corrupted or valuestring is NULL */ + if (object->valuestring == NULL || valuestring == NULL) + { + return NULL; + } + + v1_len = strlen(valuestring); + v2_len = strlen(object->valuestring); + + if (v1_len <= v2_len) + { + /* strcpy does not handle overlapping string: [X1, X2] [Y1, Y2] => X2 < Y1 or Y2 < X1 */ + if (!(valuestring + v1_len < object->valuestring || object->valuestring + v2_len < valuestring)) + { + return NULL; + } + strcpy(object->valuestring, valuestring); + return object->valuestring; + } + copy = (char *)cJSON_strdup((const unsigned char *)valuestring, &global_hooks); + if (copy == NULL) + { + return NULL; + } + if (object->valuestring != NULL) + { + cJSON_free(object->valuestring); + } + object->valuestring = copy; + + return copy; +} + +typedef struct +{ + unsigned char *buffer; + size_t length; + size_t offset; + size_t depth; /* current nesting depth (for formatted printing) */ + cJSON_bool noalloc; + cJSON_bool format; /* is this print a formatted print */ + internal_hooks hooks; +} printbuffer; + +/* realloc printbuffer if necessary to have at least "needed" bytes more */ +static unsigned char *ensure(printbuffer *const p, size_t needed) +{ + unsigned char *newbuffer = NULL; + size_t newsize = 0; + + if ((p == NULL) || (p->buffer == NULL)) + { + return NULL; + } + + if ((p->length > 0) && (p->offset >= p->length)) + { + /* make sure that offset is valid */ + return NULL; + } + + if (needed > INT_MAX) + { + /* sizes bigger than INT_MAX are currently not supported */ + return NULL; + } + + needed += p->offset + 1; + if (needed <= p->length) + { + return p->buffer + p->offset; + } + + if (p->noalloc) + { + return NULL; + } + + /* calculate new buffer size */ + if (needed > (INT_MAX / 2)) + { + /* overflow of int, use INT_MAX if possible */ + if (needed <= INT_MAX) + { + newsize = INT_MAX; + } + else + { + return NULL; + } + } + else + { + newsize = needed * 2; + } + + if (p->hooks.reallocate != NULL) + { + /* reallocate with realloc if available */ + newbuffer = (unsigned char *)p->hooks.reallocate(p->buffer, newsize); + if (newbuffer == NULL) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + } + else + { + /* otherwise reallocate manually */ + newbuffer = (unsigned char *)p->hooks.allocate(newsize); + if (!newbuffer) + { + p->hooks.deallocate(p->buffer); + p->length = 0; + p->buffer = NULL; + + return NULL; + } + + memcpy(newbuffer, p->buffer, p->offset + 1); + p->hooks.deallocate(p->buffer); + } + p->length = newsize; + p->buffer = newbuffer; + + return newbuffer + p->offset; +} + +/* calculate the new length of the string in a printbuffer and update the offset */ +static void update_offset(printbuffer *const buffer) +{ + const unsigned char *buffer_pointer = NULL; + if ((buffer == NULL) || (buffer->buffer == NULL)) + { + return; + } + buffer_pointer = buffer->buffer + buffer->offset; + + buffer->offset += strlen((const char *)buffer_pointer); +} + +/* securely comparison of floating-point variables */ +static cJSON_bool compare_double(double a, double b) +{ + double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); + return (fabs(a - b) <= maxVal * DBL_EPSILON); +} + +/* Render the number nicely from the given item into a string. */ +static cJSON_bool print_number(const cJSON *const item, printbuffer *const output_buffer) +{ + unsigned char *output_pointer = NULL; + double d = item->valuedouble; + int length = 0; + size_t i = 0; + unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */ + unsigned char decimal_point = get_decimal_point(); + double test = 0.0; + + if (output_buffer == NULL) + { + return false; + } + + /* This checks for NaN and Infinity */ + if (isnan(d) || isinf(d)) + { + length = sprintf((char *)number_buffer, "null"); + } + else if (d == (double)item->valueint) + { + length = sprintf((char *)number_buffer, "%d", item->valueint); + } + else + { + /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ + length = sprintf((char *)number_buffer, "%1.15g", d); + + /* Check whether the original double can be recovered */ + if ((sscanf((char *)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) + { + /* If not, print with 17 decimal places of precision */ + length = sprintf((char *)number_buffer, "%1.17g", d); + } + } + + /* sprintf failed or buffer overrun occurred */ + if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) + { + return false; + } + + /* reserve appropriate space in the output */ + output_pointer = ensure(output_buffer, (size_t)length + sizeof("")); + if (output_pointer == NULL) + { + return false; + } + + /* copy the printed number to the output and replace locale + * dependent decimal point with '.' */ + for (i = 0; i < ((size_t)length); i++) + { + if (number_buffer[i] == decimal_point) + { + output_pointer[i] = '.'; + continue; + } + + output_pointer[i] = number_buffer[i]; + } + output_pointer[i] = '\0'; + + output_buffer->offset += (size_t)length; + + return true; +} + +/* parse 4 digit hexadecimal number */ +static unsigned parse_hex4(const unsigned char *const input) +{ + unsigned int h = 0; + size_t i = 0; + + for (i = 0; i < 4; i++) + { + /* parse digit */ + if ((input[i] >= '0') && (input[i] <= '9')) + { + h += (unsigned int)input[i] - '0'; + } + else if ((input[i] >= 'A') && (input[i] <= 'F')) + { + h += (unsigned int)10 + input[i] - 'A'; + } + else if ((input[i] >= 'a') && (input[i] <= 'f')) + { + h += (unsigned int)10 + input[i] - 'a'; + } + else /* invalid */ + { + return 0; + } + + if (i < 3) + { + /* shift left to make place for the next nibble */ + h = h << 4; + } + } + + return h; +} + +/* converts a UTF-16 literal to UTF-8 + * A literal can be one or two sequences of the form \uXXXX */ +static unsigned char utf16_literal_to_utf8(const unsigned char *const input_pointer, const unsigned char *const input_end, unsigned char **output_pointer) +{ + long unsigned int codepoint = 0; + unsigned int first_code = 0; + const unsigned char *first_sequence = input_pointer; + unsigned char utf8_length = 0; + unsigned char utf8_position = 0; + unsigned char sequence_length = 0; + unsigned char first_byte_mark = 0; + + if ((input_end - first_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + /* get the first utf16 sequence */ + first_code = parse_hex4(first_sequence + 2); + + /* check that the code is valid */ + if (((first_code >= 0xDC00) && (first_code <= 0xDFFF))) + { + goto fail; + } + + /* UTF16 surrogate pair */ + if ((first_code >= 0xD800) && (first_code <= 0xDBFF)) + { + const unsigned char *second_sequence = first_sequence + 6; + unsigned int second_code = 0; + sequence_length = 12; /* \uXXXX\uXXXX */ + + if ((input_end - second_sequence) < 6) + { + /* input ends unexpectedly */ + goto fail; + } + + if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) + { + /* missing second half of the surrogate pair */ + goto fail; + } + + /* get the second utf16 sequence */ + second_code = parse_hex4(second_sequence + 2); + /* check that the code is valid */ + if ((second_code < 0xDC00) || (second_code > 0xDFFF)) + { + /* invalid second half of the surrogate pair */ + goto fail; + } + + /* calculate the unicode codepoint from the surrogate pair */ + codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF)); + } + else + { + sequence_length = 6; /* \uXXXX */ + codepoint = first_code; + } + + /* encode as UTF-8 + * takes at maximum 4 bytes to encode: + * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ + if (codepoint < 0x80) + { + /* normal ascii, encoding 0xxxxxxx */ + utf8_length = 1; + } + else if (codepoint < 0x800) + { + /* two bytes, encoding 110xxxxx 10xxxxxx */ + utf8_length = 2; + first_byte_mark = 0xC0; /* 11000000 */ + } + else if (codepoint < 0x10000) + { + /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */ + utf8_length = 3; + first_byte_mark = 0xE0; /* 11100000 */ + } + else if (codepoint <= 0x10FFFF) + { + /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */ + utf8_length = 4; + first_byte_mark = 0xF0; /* 11110000 */ + } + else + { + /* invalid unicode codepoint */ + goto fail; + } + + /* encode as utf8 */ + for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) + { + /* 10xxxxxx */ + (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF); + codepoint >>= 6; + } + /* encode first byte */ + if (utf8_length > 1) + { + (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF); + } + else + { + (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F); + } + + *output_pointer += utf8_length; + + return sequence_length; + +fail: + return 0; +} + +/* Parse the input text into an unescaped cinput, and populate item. */ +static cJSON_bool parse_string(cJSON *const item, parse_buffer *const input_buffer) +{ + const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1; + const unsigned char *input_end = buffer_at_offset(input_buffer) + 1; + unsigned char *output_pointer = NULL; + unsigned char *output = NULL; + + /* not a string */ + if (buffer_at_offset(input_buffer)[0] != '\"') + { + goto fail; + } + + { + /* calculate approximate size of the output (overestimate) */ + size_t allocation_length = 0; + size_t skipped_bytes = 0; + while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"')) + { + /* is escape sequence */ + if (input_end[0] == '\\') + { + if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) + { + /* prevent buffer overflow when last input character is a backslash */ + goto fail; + } + skipped_bytes++; + input_end++; + } + input_end++; + } + if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"')) + { + goto fail; /* string ended unexpectedly */ + } + + /* This is at most how much we need for the output */ + allocation_length = (size_t)(input_end - buffer_at_offset(input_buffer)) - skipped_bytes; + output = (unsigned char *)input_buffer->hooks.allocate(allocation_length + sizeof("")); + if (output == NULL) + { + goto fail; /* allocation failure */ + } + } + + output_pointer = output; + /* loop through the string literal */ + while (input_pointer < input_end) + { + if (*input_pointer != '\\') + { + *output_pointer++ = *input_pointer++; + } + /* escape sequence */ + else + { + unsigned char sequence_length = 2; + if ((input_end - input_pointer) < 1) + { + goto fail; + } + + switch (input_pointer[1]) + { + case 'b': + *output_pointer++ = '\b'; + break; + case 'f': + *output_pointer++ = '\f'; + break; + case 'n': + *output_pointer++ = '\n'; + break; + case 'r': + *output_pointer++ = '\r'; + break; + case 't': + *output_pointer++ = '\t'; + break; + case '\"': + case '\\': + case '/': + *output_pointer++ = input_pointer[1]; + break; + + /* UTF-16 literal */ + case 'u': + sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer); + if (sequence_length == 0) + { + /* failed to convert UTF16-literal to UTF-8 */ + goto fail; + } + break; + + default: + goto fail; + } + input_pointer += sequence_length; + } + } + + /* zero terminate the output */ + *output_pointer = '\0'; + + item->type = cJSON_String; + item->valuestring = (char *)output; + + input_buffer->offset = (size_t)(input_end - input_buffer->content); + input_buffer->offset++; + + return true; + +fail: + if (output != NULL) + { + input_buffer->hooks.deallocate(output); + output = NULL; + } + + if (input_pointer != NULL) + { + input_buffer->offset = (size_t)(input_pointer - input_buffer->content); + } + + return false; +} + +/* Render the cstring provided to an escaped version that can be printed. */ +static cJSON_bool print_string_ptr(const unsigned char *const input, printbuffer *const output_buffer) +{ + const unsigned char *input_pointer = NULL; + unsigned char *output = NULL; + unsigned char *output_pointer = NULL; + size_t output_length = 0; + /* numbers of additional characters needed for escaping */ + size_t escape_characters = 0; + + if (output_buffer == NULL) + { + return false; + } + + /* empty string */ + if (input == NULL) + { + output = ensure(output_buffer, sizeof("\"\"")); + if (output == NULL) + { + return false; + } + strcpy((char *)output, "\"\""); + + return true; + } + + /* set "flag" to 1 if something needs to be escaped */ + for (input_pointer = input; *input_pointer; input_pointer++) + { + switch (*input_pointer) + { + case '\"': + case '\\': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + /* one character escape sequence */ + escape_characters++; + break; + default: + if (*input_pointer < 32) + { + /* UTF-16 escape sequence uXXXX */ + escape_characters += 5; + } + break; + } + } + output_length = (size_t)(input_pointer - input) + escape_characters; + + output = ensure(output_buffer, output_length + sizeof("\"\"")); + if (output == NULL) + { + return false; + } + + /* no characters have to be escaped */ + if (escape_characters == 0) + { + output[0] = '\"'; + memcpy(output + 1, input, output_length); + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; + } + + output[0] = '\"'; + output_pointer = output + 1; + /* copy the string */ + for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) + { + if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) + { + /* normal character, copy */ + *output_pointer = *input_pointer; + } + else + { + /* character needs to be escaped */ + *output_pointer++ = '\\'; + switch (*input_pointer) + { + case '\\': + *output_pointer = '\\'; + break; + case '\"': + *output_pointer = '\"'; + break; + case '\b': + *output_pointer = 'b'; + break; + case '\f': + *output_pointer = 'f'; + break; + case '\n': + *output_pointer = 'n'; + break; + case '\r': + *output_pointer = 'r'; + break; + case '\t': + *output_pointer = 't'; + break; + default: + /* escape and print as unicode codepoint */ + sprintf((char *)output_pointer, "u%04x", *input_pointer); + output_pointer += 4; + break; + } + } + } + output[output_length + 1] = '\"'; + output[output_length + 2] = '\0'; + + return true; +} + +/* Invoke print_string_ptr (which is useful) on an item. */ +static cJSON_bool print_string(const cJSON *const item, printbuffer *const p) +{ + return print_string_ptr((unsigned char *)item->valuestring, p); +} + +/* Predeclare these prototypes. */ +static cJSON_bool parse_value(cJSON *const item, parse_buffer *const input_buffer); +static cJSON_bool print_value(const cJSON *const item, printbuffer *const output_buffer); +static cJSON_bool parse_array(cJSON *const item, parse_buffer *const input_buffer); +static cJSON_bool print_array(const cJSON *const item, printbuffer *const output_buffer); +static cJSON_bool parse_object(cJSON *const item, parse_buffer *const input_buffer); +static cJSON_bool print_object(const cJSON *const item, printbuffer *const output_buffer); + +/* Utility to jump whitespace and cr/lf */ +static parse_buffer *buffer_skip_whitespace(parse_buffer *const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL)) + { + return NULL; + } + + if (cannot_access_at_index(buffer, 0)) + { + return buffer; + } + + while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) + { + buffer->offset++; + } + + if (buffer->offset == buffer->length) + { + buffer->offset--; + } + + return buffer; +} + +/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */ +static parse_buffer *skip_utf8_bom(parse_buffer *const buffer) +{ + if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) + { + return NULL; + } + + if (can_access_at_index(buffer, 4) && (strncmp((const char *)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) + { + buffer->offset += 3; + } + + return buffer; +} + +CJSON_PUBLIC(cJSON *) +cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + size_t buffer_length; + + if (NULL == value) + { + return NULL; + } + + /* Adding null character size due to require_null_terminated. */ + buffer_length = strlen(value) + sizeof(""); + + return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated); +} + +/* Parse an object - create a new root, and populate. */ +CJSON_PUBLIC(cJSON *) +cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated) +{ + parse_buffer buffer = {0, 0, 0, 0, {0, 0, 0}}; + cJSON *item = NULL; + + /* reset error position */ + global_error.json = NULL; + global_error.position = 0; + + if (value == NULL || 0 == buffer_length) + { + goto fail; + } + + buffer.content = (const unsigned char *)value; + buffer.length = buffer_length; + buffer.offset = 0; + buffer.hooks = global_hooks; + + item = cJSON_New_Item(&global_hooks); + if (item == NULL) /* memory fail */ + { + goto fail; + } + + if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) + { + /* parse failure. ep is set. */ + goto fail; + } + + /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ + if (require_null_terminated) + { + buffer_skip_whitespace(&buffer); + if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') + { + goto fail; + } + } + if (return_parse_end) + { + *return_parse_end = (const char *)buffer_at_offset(&buffer); + } + + return item; + +fail: + if (item != NULL) + { + cJSON_Delete(item); + } + + if (value != NULL) + { + error local_error; + local_error.json = (const unsigned char *)value; + local_error.position = 0; + + if (buffer.offset < buffer.length) + { + local_error.position = buffer.offset; + } + else if (buffer.length > 0) + { + local_error.position = buffer.length - 1; + } + + if (return_parse_end != NULL) + { + *return_parse_end = (const char *)local_error.json + local_error.position; + } + + global_error = local_error; + } + + return NULL; +} + +/* Default options for cJSON_Parse */ +CJSON_PUBLIC(cJSON *) +cJSON_Parse(const char *value) +{ + return cJSON_ParseWithOpts(value, 0, 0); +} + +CJSON_PUBLIC(cJSON *) +cJSON_ParseWithLength(const char *value, size_t buffer_length) +{ + return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0); +} + +#define cjson_min(a, b) (((a) < (b)) ? (a) : (b)) + +static unsigned char *print(const cJSON *const item, cJSON_bool format, const internal_hooks *const hooks) +{ + static const size_t default_buffer_size = 256; + printbuffer buffer[1]; + unsigned char *printed = NULL; + + memset(buffer, 0, sizeof(buffer)); + + /* create buffer */ + buffer->buffer = (unsigned char *)hooks->allocate(default_buffer_size); + buffer->length = default_buffer_size; + buffer->format = format; + buffer->hooks = *hooks; + if (buffer->buffer == NULL) + { + goto fail; + } + + /* print the value */ + if (!print_value(item, buffer)) + { + goto fail; + } + update_offset(buffer); + + /* check if reallocate is available */ + if (hooks->reallocate != NULL) + { + printed = (unsigned char *)hooks->reallocate(buffer->buffer, buffer->offset + 1); + if (printed == NULL) + { + goto fail; + } + buffer->buffer = NULL; + } + else /* otherwise copy the JSON over to a new buffer */ + { + printed = (unsigned char *)hooks->allocate(buffer->offset + 1); + if (printed == NULL) + { + goto fail; + } + memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1)); + printed[buffer->offset] = '\0'; /* just to be sure */ + + /* free the buffer */ + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + return printed; + +fail: + if (buffer->buffer != NULL) + { + hooks->deallocate(buffer->buffer); + buffer->buffer = NULL; + } + + if (printed != NULL) + { + hooks->deallocate(printed); + printed = NULL; + } + + return NULL; +} + +/* Render a cJSON item/entity/structure to text. */ +CJSON_PUBLIC(char *) +cJSON_Print(const cJSON *item) +{ + return (char *)print(item, true, &global_hooks); +} + +CJSON_PUBLIC(char *) +cJSON_PrintUnformatted(const cJSON *item) +{ + return (char *)print(item, false, &global_hooks); +} + +CJSON_PUBLIC(char *) +cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt) +{ + printbuffer p = {0, 0, 0, 0, 0, 0, {0, 0, 0}}; + + if (prebuffer < 0) + { + return NULL; + } + + p.buffer = (unsigned char *)global_hooks.allocate((size_t)prebuffer); + if (!p.buffer) + { + return NULL; + } + + p.length = (size_t)prebuffer; + p.offset = 0; + p.noalloc = false; + p.format = fmt; + p.hooks = global_hooks; + + if (!print_value(item, &p)) + { + global_hooks.deallocate(p.buffer); + p.buffer = NULL; + return NULL; + } + + return (char *)p.buffer; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format) +{ + printbuffer p = {0, 0, 0, 0, 0, 0, {0, 0, 0}}; + + if ((length < 0) || (buffer == NULL)) + { + return false; + } + + p.buffer = (unsigned char *)buffer; + p.length = (size_t)length; + p.offset = 0; + p.noalloc = true; + p.format = format; + p.hooks = global_hooks; + + return print_value(item, &p); +} + +/* Parser core - when encountering text, process appropriately. */ +static cJSON_bool parse_value(cJSON *const item, parse_buffer *const input_buffer) +{ + if ((input_buffer == NULL) || (input_buffer->content == NULL)) + { + return false; /* no input */ + } + + /* parse the different types of values */ + /* null */ + if (can_read(input_buffer, 4) && (strncmp((const char *)buffer_at_offset(input_buffer), "null", 4) == 0)) + { + item->type = cJSON_NULL; + input_buffer->offset += 4; + return true; + } + /* false */ + if (can_read(input_buffer, 5) && (strncmp((const char *)buffer_at_offset(input_buffer), "false", 5) == 0)) + { + item->type = cJSON_False; + input_buffer->offset += 5; + return true; + } + /* true */ + if (can_read(input_buffer, 4) && (strncmp((const char *)buffer_at_offset(input_buffer), "true", 4) == 0)) + { + item->type = cJSON_True; + item->valueint = 1; + input_buffer->offset += 4; + return true; + } + /* string */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) + { + return parse_string(item, input_buffer); + } + /* number */ + if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9')))) + { + return parse_number(item, input_buffer); + } + /* array */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) + { + return parse_array(item, input_buffer); + } + /* object */ + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) + { + return parse_object(item, input_buffer); + } + + return false; +} + +/* Render a value to text. */ +static cJSON_bool print_value(const cJSON *const item, printbuffer *const output_buffer) +{ + unsigned char *output = NULL; + + if ((item == NULL) || (output_buffer == NULL)) + { + return false; + } + + switch ((item->type) & 0xFF) + { + case cJSON_NULL: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char *)output, "null"); + return true; + + case cJSON_False: + output = ensure(output_buffer, 6); + if (output == NULL) + { + return false; + } + strcpy((char *)output, "false"); + return true; + + case cJSON_True: + output = ensure(output_buffer, 5); + if (output == NULL) + { + return false; + } + strcpy((char *)output, "true"); + return true; + + case cJSON_Number: + return print_number(item, output_buffer); + + case cJSON_Raw: + { + size_t raw_length = 0; + if (item->valuestring == NULL) + { + return false; + } + + raw_length = strlen(item->valuestring) + sizeof(""); + output = ensure(output_buffer, raw_length); + if (output == NULL) + { + return false; + } + memcpy(output, item->valuestring, raw_length); + return true; + } + + case cJSON_String: + return print_string(item, output_buffer); + + case cJSON_Array: + return print_array(item, output_buffer); + + case cJSON_Object: + return print_object(item, output_buffer); + + default: + return false; + } +} + +/* Build an array from input text. */ +static cJSON_bool parse_array(cJSON *const item, parse_buffer *const input_buffer) +{ + cJSON *head = NULL; /* head of the linked list */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (buffer_at_offset(input_buffer)[0] != '[') + { + /* not an array */ + goto fail; + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) + { + /* empty array */ + goto success; + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + /* parse next value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') + { + goto fail; /* expected end of array */ + } + +success: + input_buffer->depth--; + + if (head != NULL) + { + head->prev = current_item; + } + + item->type = cJSON_Array; + item->child = head; + + input_buffer->offset++; + + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an array to text */ +static cJSON_bool print_array(const cJSON *const item, printbuffer *const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_element = item->child; + + if (output_buffer == NULL) + { + return false; + } + + if (output_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* nesting is too deep */ + } + + /* Compose the output array. */ + /* opening square bracket */ + output_pointer = ensure(output_buffer, 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer = '['; + output_buffer->offset++; + output_buffer->depth++; + + while (current_element != NULL) + { + if (!print_value(current_element, output_buffer)) + { + return false; + } + update_offset(output_buffer); + if (current_element->next) + { + length = (size_t)(output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ','; + if (output_buffer->format) + { + *output_pointer++ = ' '; + } + *output_pointer = '\0'; + output_buffer->offset += length; + } + current_element = current_element->next; + } + + output_pointer = ensure(output_buffer, 2); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ']'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Build an object from the text. */ +static cJSON_bool parse_object(cJSON *const item, parse_buffer *const input_buffer) +{ + cJSON *head = NULL; /* linked list head */ + cJSON *current_item = NULL; + + if (input_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* to deeply nested */ + } + input_buffer->depth++; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) + { + goto fail; /* not an object */ + } + + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) + { + goto success; /* empty object */ + } + + /* check if we skipped to the end of the buffer */ + if (cannot_access_at_index(input_buffer, 0)) + { + input_buffer->offset--; + goto fail; + } + + /* step back to character in front of the first element */ + input_buffer->offset--; + /* loop through the comma separated array elements */ + do + { + /* allocate next item */ + cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); + if (new_item == NULL) + { + goto fail; /* allocation failure */ + } + + /* attach next item to list */ + if (head == NULL) + { + /* start the linked list */ + current_item = head = new_item; + } + else + { + /* add to the end and advance */ + current_item->next = new_item; + new_item->prev = current_item; + current_item = new_item; + } + + if (cannot_access_at_index(input_buffer, 1)) + { + goto fail; /* nothing comes after the comma */ + } + + /* parse the name of the child */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_string(current_item, input_buffer)) + { + goto fail; /* failed to parse name */ + } + buffer_skip_whitespace(input_buffer); + + /* swap valuestring and string, because we parsed the name */ + current_item->string = current_item->valuestring; + current_item->valuestring = NULL; + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) + { + goto fail; /* invalid object */ + } + + /* parse the value */ + input_buffer->offset++; + buffer_skip_whitespace(input_buffer); + if (!parse_value(current_item, input_buffer)) + { + goto fail; /* failed to parse value */ + } + buffer_skip_whitespace(input_buffer); + } while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); + + if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) + { + goto fail; /* expected end of object */ + } + +success: + input_buffer->depth--; + + if (head != NULL) + { + head->prev = current_item; + } + + item->type = cJSON_Object; + item->child = head; + + input_buffer->offset++; + return true; + +fail: + if (head != NULL) + { + cJSON_Delete(head); + } + + return false; +} + +/* Render an object to text. */ +static cJSON_bool print_object(const cJSON *const item, printbuffer *const output_buffer) +{ + unsigned char *output_pointer = NULL; + size_t length = 0; + cJSON *current_item = item->child; + + if (output_buffer == NULL) + { + return false; + } + + if (output_buffer->depth >= CJSON_NESTING_LIMIT) + { + return false; /* nesting is too deep */ + } + + /* Compose the output: */ + length = (size_t)(output_buffer->format ? 2 : 1); /* fmt: {\n */ + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + + *output_pointer++ = '{'; + output_buffer->depth++; + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + output_buffer->offset += length; + + while (current_item) + { + if (output_buffer->format) + { + size_t i; + output_pointer = ensure(output_buffer, output_buffer->depth); + if (output_pointer == NULL) + { + return false; + } + for (i = 0; i < output_buffer->depth; i++) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += output_buffer->depth; + } + + /* print key */ + if (!print_string_ptr((unsigned char *)current_item->string, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + length = (size_t)(output_buffer->format ? 2 : 1); + output_pointer = ensure(output_buffer, length); + if (output_pointer == NULL) + { + return false; + } + *output_pointer++ = ':'; + if (output_buffer->format) + { + *output_pointer++ = '\t'; + } + output_buffer->offset += length; + + /* print value */ + if (!print_value(current_item, output_buffer)) + { + return false; + } + update_offset(output_buffer); + + /* print comma if not last */ + length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0)); + output_pointer = ensure(output_buffer, length + 1); + if (output_pointer == NULL) + { + return false; + } + if (current_item->next) + { + *output_pointer++ = ','; + } + + if (output_buffer->format) + { + *output_pointer++ = '\n'; + } + *output_pointer = '\0'; + output_buffer->offset += length; + + current_item = current_item->next; + } + + output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2); + if (output_pointer == NULL) + { + return false; + } + if (output_buffer->format) + { + size_t i; + for (i = 0; i < (output_buffer->depth - 1); i++) + { + *output_pointer++ = '\t'; + } + } + *output_pointer++ = '}'; + *output_pointer = '\0'; + output_buffer->depth--; + + return true; +} + +/* Get Array size/item / object item. */ +CJSON_PUBLIC(int) +cJSON_GetArraySize(const cJSON *array) +{ + cJSON *child = NULL; + size_t size = 0; + + if (array == NULL) + { + return 0; + } + + child = array->child; + + while (child != NULL) + { + size++; + child = child->next; + } + + /* FIXME: Can overflow here. Cannot be fixed without breaking the API */ + + return (int)size; +} + +static cJSON *get_array_item(const cJSON *array, size_t index) +{ + cJSON *current_child = NULL; + + if (array == NULL) + { + return NULL; + } + + current_child = array->child; + while ((current_child != NULL) && (index > 0)) + { + index--; + current_child = current_child->next; + } + + return current_child; +} + +CJSON_PUBLIC(cJSON *) +cJSON_GetArrayItem(const cJSON *array, int index) +{ + if (index < 0) + { + return NULL; + } + + return get_array_item(array, (size_t)index); +} + +static cJSON *get_object_item(const cJSON *const object, const char *const name, const cJSON_bool case_sensitive) +{ + cJSON *current_element = NULL; + + if ((object == NULL) || (name == NULL)) + { + return NULL; + } + + current_element = object->child; + if (case_sensitive) + { + while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) + { + current_element = current_element->next; + } + } + else + { + while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char *)name, (const unsigned char *)(current_element->string)) != 0)) + { + current_element = current_element->next; + } + } + + if ((current_element == NULL) || (current_element->string == NULL)) + { + return NULL; + } + + return current_element; +} + +CJSON_PUBLIC(cJSON *) +cJSON_GetObjectItem(const cJSON *const object, const char *const string) +{ + return get_object_item(object, string, false); +} + +CJSON_PUBLIC(cJSON *) +cJSON_GetObjectItemCaseSensitive(const cJSON *const object, const char *const string) +{ + return get_object_item(object, string, true); +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_HasObjectItem(const cJSON *object, const char *string) +{ + return cJSON_GetObjectItem(object, string) ? 1 : 0; +} + +/* Utility for array list handling. */ +static void suffix_object(cJSON *prev, cJSON *item) +{ + prev->next = item; + item->prev = prev; +} + +/* Utility for handling references. */ +static cJSON *create_reference(const cJSON *item, const internal_hooks *const hooks) +{ + cJSON *reference = NULL; + if (item == NULL) + { + return NULL; + } + + reference = cJSON_New_Item(hooks); + if (reference == NULL) + { + return NULL; + } + + memcpy(reference, item, sizeof(cJSON)); + reference->string = NULL; + reference->type |= cJSON_IsReference; + reference->next = reference->prev = NULL; + return reference; +} + +static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) +{ + cJSON *child = NULL; + + if ((item == NULL) || (array == NULL) || (array == item)) + { + return false; + } + + child = array->child; + /* + * To find the last item in array quickly, we use prev in array + */ + if (child == NULL) + { + /* list is empty, start new one */ + array->child = item; + item->prev = item; + item->next = NULL; + } + else + { + /* append to the end */ + if (child->prev) + { + suffix_object(child->prev, item); + array->child->prev = item; + } + } + + return true; +} + +/* Add item to array/object. */ +CJSON_PUBLIC(cJSON_bool) +cJSON_AddItemToArray(cJSON *array, cJSON *item) +{ + return add_item_to_array(array, item); +} + +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) +#pragma GCC diagnostic push +#endif +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif +/* helper function to cast away const */ +static void *cast_away_const(const void *string) +{ + return (void *)string; +} +#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) +#pragma GCC diagnostic pop +#endif + +static cJSON_bool add_item_to_object(cJSON *const object, const char *const string, cJSON *const item, const internal_hooks *const hooks, const cJSON_bool constant_key) +{ + char *new_key = NULL; + int new_type = cJSON_Invalid; + + if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) + { + return false; + } + + if (constant_key) + { + new_key = (char *)cast_away_const(string); + new_type = item->type | cJSON_StringIsConst; + } + else + { + new_key = (char *)cJSON_strdup((const unsigned char *)string, hooks); + if (new_key == NULL) + { + return false; + } + + new_type = item->type & ~cJSON_StringIsConst; + } + + if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) + { + hooks->deallocate(item->string); + } + + item->string = new_key; + item->type = new_type; + + return add_item_to_array(object, item); +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, false); +} + +/* Add an item to an object with constant string as key */ +CJSON_PUBLIC(cJSON_bool) +cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item) +{ + return add_item_to_object(object, string, item, &global_hooks, true); +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) +{ + if (array == NULL) + { + return false; + } + + return add_item_to_array(array, create_reference(item, &global_hooks)); +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item) +{ + if ((object == NULL) || (string == NULL)) + { + return false; + } + + return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false); +} + +CJSON_PUBLIC(cJSON *) +cJSON_AddNullToObject(cJSON *const object, const char *const name) +{ + cJSON *null = cJSON_CreateNull(); + if (add_item_to_object(object, name, null, &global_hooks, false)) + { + return null; + } + + cJSON_Delete(null); + return NULL; +} + +CJSON_PUBLIC(cJSON *) +cJSON_AddTrueToObject(cJSON *const object, const char *const name) +{ + cJSON *true_item = cJSON_CreateTrue(); + if (add_item_to_object(object, name, true_item, &global_hooks, false)) + { + return true_item; + } + + cJSON_Delete(true_item); + return NULL; +} + +CJSON_PUBLIC(cJSON *) +cJSON_AddFalseToObject(cJSON *const object, const char *const name) +{ + cJSON *false_item = cJSON_CreateFalse(); + if (add_item_to_object(object, name, false_item, &global_hooks, false)) + { + return false_item; + } + + cJSON_Delete(false_item); + return NULL; +} + +CJSON_PUBLIC(cJSON *) +cJSON_AddBoolToObject(cJSON *const object, const char *const name, const cJSON_bool boolean) +{ + cJSON *bool_item = cJSON_CreateBool(boolean); + if (add_item_to_object(object, name, bool_item, &global_hooks, false)) + { + return bool_item; + } + + cJSON_Delete(bool_item); + return NULL; +} + +CJSON_PUBLIC(cJSON *) +cJSON_AddNumberToObject(cJSON *const object, const char *const name, const double number) +{ + cJSON *number_item = cJSON_CreateNumber(number); + if (add_item_to_object(object, name, number_item, &global_hooks, false)) + { + return number_item; + } + + cJSON_Delete(number_item); + return NULL; +} + +CJSON_PUBLIC(cJSON *) +cJSON_AddStringToObject(cJSON *const object, const char *const name, const char *const string) +{ + cJSON *string_item = cJSON_CreateString(string); + if (add_item_to_object(object, name, string_item, &global_hooks, false)) + { + return string_item; + } + + cJSON_Delete(string_item); + return NULL; +} + +CJSON_PUBLIC(cJSON *) +cJSON_AddRawToObject(cJSON *const object, const char *const name, const char *const raw) +{ + cJSON *raw_item = cJSON_CreateRaw(raw); + if (add_item_to_object(object, name, raw_item, &global_hooks, false)) + { + return raw_item; + } + + cJSON_Delete(raw_item); + return NULL; +} + +CJSON_PUBLIC(cJSON *) +cJSON_AddObjectToObject(cJSON *const object, const char *const name) +{ + cJSON *object_item = cJSON_CreateObject(); + if (add_item_to_object(object, name, object_item, &global_hooks, false)) + { + return object_item; + } + + cJSON_Delete(object_item); + return NULL; +} + +CJSON_PUBLIC(cJSON *) +cJSON_AddArrayToObject(cJSON *const object, const char *const name) +{ + cJSON *array = cJSON_CreateArray(); + if (add_item_to_object(object, name, array, &global_hooks, false)) + { + return array; + } + + cJSON_Delete(array); + return NULL; +} + +CJSON_PUBLIC(cJSON *) +cJSON_DetachItemViaPointer(cJSON *parent, cJSON *const item) +{ + if ((parent == NULL) || (item == NULL) || (item != parent->child && item->prev == NULL)) + { + return NULL; + } + + if (item != parent->child) + { + /* not the first element */ + item->prev->next = item->next; + } + if (item->next != NULL) + { + /* not the last element */ + item->next->prev = item->prev; + } + + if (item == parent->child) + { + /* first element */ + parent->child = item->next; + } + else if (item->next == NULL) + { + /* last element */ + parent->child->prev = item->prev; + } + + /* make sure the detached item doesn't point anywhere anymore */ + item->prev = NULL; + item->next = NULL; + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_DetachItemFromArray(cJSON *array, int which) +{ + if (which < 0) + { + return NULL; + } + + return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which)); +} + +CJSON_PUBLIC(void) +cJSON_DeleteItemFromArray(cJSON *array, int which) +{ + cJSON_Delete(cJSON_DetachItemFromArray(array, which)); +} + +CJSON_PUBLIC(cJSON *) +cJSON_DetachItemFromObject(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItem(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(cJSON *) +cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string); + + return cJSON_DetachItemViaPointer(object, to_detach); +} + +CJSON_PUBLIC(void) +cJSON_DeleteItemFromObject(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObject(object, string)); +} + +CJSON_PUBLIC(void) +cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string) +{ + cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string)); +} + +/* Replace array/object items with new ones. */ +CJSON_PUBLIC(cJSON_bool) +cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem) +{ + cJSON *after_inserted = NULL; + + if (which < 0 || newitem == NULL) + { + return false; + } + + after_inserted = get_array_item(array, (size_t)which); + if (after_inserted == NULL) + { + return add_item_to_array(array, newitem); + } + + if (after_inserted != array->child && after_inserted->prev == NULL) + { + /* return false if after_inserted is a corrupted array item */ + return false; + } + + newitem->next = after_inserted; + newitem->prev = after_inserted->prev; + after_inserted->prev = newitem; + if (after_inserted == array->child) + { + array->child = newitem; + } + else + { + newitem->prev->next = newitem; + } + return true; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_ReplaceItemViaPointer(cJSON *const parent, cJSON *const item, cJSON *replacement) +{ + if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL)) + { + return false; + } + + if (replacement == item) + { + return true; + } + + replacement->next = item->next; + replacement->prev = item->prev; + + if (replacement->next != NULL) + { + replacement->next->prev = replacement; + } + if (parent->child == item) + { + if (parent->child->prev == parent->child) + { + replacement->prev = replacement; + } + parent->child = replacement; + } + else + { /* + * To find the last item in array quickly, we use prev in array. + * We can't modify the last item's next pointer where this item was the parent's child + */ + if (replacement->prev != NULL) + { + replacement->prev->next = replacement; + } + if (replacement->next == NULL) + { + parent->child->prev = replacement; + } + } + + item->next = NULL; + item->prev = NULL; + cJSON_Delete(item); + + return true; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem) +{ + if (which < 0) + { + return false; + } + + return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem); +} + +static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive) +{ + if ((replacement == NULL) || (string == NULL)) + { + return false; + } + + /* replace the name in the replacement */ + if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) + { + cJSON_free(replacement->string); + } + replacement->string = (char *)cJSON_strdup((const unsigned char *)string, &global_hooks); + if (replacement->string == NULL) + { + return false; + } + + replacement->type &= ~cJSON_StringIsConst; + + return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement); +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, false); +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) +{ + return replace_item_in_object(object, string, newitem, true); +} + +/* Create basic types: */ +CJSON_PUBLIC(cJSON *) +cJSON_CreateNull(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_NULL; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateTrue(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_True; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateFalse(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateBool(cJSON_bool boolean) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = boolean ? cJSON_True : cJSON_False; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateNumber(double num) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Number; + item->valuedouble = num; + + /* use saturation in case of overflow */ + if (num >= INT_MAX) + { + item->valueint = INT_MAX; + } + else if (num <= (double)INT_MIN) + { + item->valueint = INT_MIN; + } + else + { + item->valueint = (int)num; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateString(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_String; + item->valuestring = (char *)cJSON_strdup((const unsigned char *)string, &global_hooks); + if (!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateStringReference(const char *string) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) + { + item->type = cJSON_String | cJSON_IsReference; + item->valuestring = (char *)cast_away_const(string); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateObjectReference(const cJSON *child) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) + { + item->type = cJSON_Object | cJSON_IsReference; + item->child = (cJSON *)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateArrayReference(const cJSON *child) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item != NULL) + { + item->type = cJSON_Array | cJSON_IsReference; + item->child = (cJSON *)cast_away_const(child); + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateRaw(const char *raw) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Raw; + item->valuestring = (char *)cJSON_strdup((const unsigned char *)raw, &global_hooks); + if (!item->valuestring) + { + cJSON_Delete(item); + return NULL; + } + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateArray(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Array; + } + + return item; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateObject(void) +{ + cJSON *item = cJSON_New_Item(&global_hooks); + if (item) + { + item->type = cJSON_Object; + } + + return item; +} + +/* Create Arrays: */ +CJSON_PUBLIC(cJSON *) +cJSON_CreateIntArray(const int *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if (!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) + { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateFloatArray(const float *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber((double)numbers[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if (!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) + { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateDoubleArray(const double *numbers, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (numbers == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateNumber(numbers[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if (!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) + { + a->child->prev = n; + } + + return a; +} + +CJSON_PUBLIC(cJSON *) +cJSON_CreateStringArray(const char *const *strings, int count) +{ + size_t i = 0; + cJSON *n = NULL; + cJSON *p = NULL; + cJSON *a = NULL; + + if ((count < 0) || (strings == NULL)) + { + return NULL; + } + + a = cJSON_CreateArray(); + + for (i = 0; a && (i < (size_t)count); i++) + { + n = cJSON_CreateString(strings[i]); + if (!n) + { + cJSON_Delete(a); + return NULL; + } + if (!i) + { + a->child = n; + } + else + { + suffix_object(p, n); + } + p = n; + } + + if (a && a->child) + { + a->child->prev = n; + } + + return a; +} + +/* Duplication */ +cJSON *cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse); + +CJSON_PUBLIC(cJSON *) +cJSON_Duplicate(const cJSON *item, cJSON_bool recurse) +{ + return cJSON_Duplicate_rec(item, 0, recurse); +} + +cJSON *cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse) +{ + cJSON *newitem = NULL; + cJSON *child = NULL; + cJSON *next = NULL; + cJSON *newchild = NULL; + + /* Bail on bad ptr */ + if (!item) + { + goto fail; + } + /* Create new item */ + newitem = cJSON_New_Item(&global_hooks); + if (!newitem) + { + goto fail; + } + /* Copy over all vars */ + newitem->type = item->type & (~cJSON_IsReference); + newitem->valueint = item->valueint; + newitem->valuedouble = item->valuedouble; + if (item->valuestring) + { + newitem->valuestring = (char *)cJSON_strdup((unsigned char *)item->valuestring, &global_hooks); + if (!newitem->valuestring) + { + goto fail; + } + } + if (item->string) + { + newitem->string = (item->type & cJSON_StringIsConst) ? item->string : (char *)cJSON_strdup((unsigned char *)item->string, &global_hooks); + if (!newitem->string) + { + goto fail; + } + } + /* If non-recursive, then we're done! */ + if (!recurse) + { + return newitem; + } + /* Walk the ->next chain for the child. */ + child = item->child; + while (child != NULL) + { + if (depth >= CJSON_CIRCULAR_LIMIT) + { + goto fail; + } + newchild = cJSON_Duplicate_rec(child, depth + 1, true); /* Duplicate (with recurse) each item in the ->next chain */ + if (!newchild) + { + goto fail; + } + if (next != NULL) + { + /* If newitem->child already set, then crosswire ->prev and ->next and move on */ + next->next = newchild; + newchild->prev = next; + next = newchild; + } + else + { + /* Set newitem->child and move to it */ + newitem->child = newchild; + next = newchild; + } + child = child->next; + } + if (newitem && newitem->child) + { + newitem->child->prev = newchild; + } + + return newitem; + +fail: + if (newitem != NULL) + { + cJSON_Delete(newitem); + } + + return NULL; +} + +static void skip_oneline_comment(char **input) +{ + *input += static_strlen("//"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if ((*input)[0] == '\n') + { + *input += static_strlen("\n"); + return; + } + } +} + +static void skip_multiline_comment(char **input) +{ + *input += static_strlen("/*"); + + for (; (*input)[0] != '\0'; ++(*input)) + { + if (((*input)[0] == '*') && ((*input)[1] == '/')) + { + *input += static_strlen("*/"); + return; + } + } +} + +static void minify_string(char **input, char **output) +{ + (*output)[0] = (*input)[0]; + *input += static_strlen("\""); + *output += static_strlen("\""); + + for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) + { + (*output)[0] = (*input)[0]; + + if ((*input)[0] == '\"') + { + (*output)[0] = '\"'; + *input += static_strlen("\""); + *output += static_strlen("\""); + return; + } + else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) + { + (*output)[1] = (*input)[1]; + *input += static_strlen("\""); + *output += static_strlen("\""); + } + } +} + +CJSON_PUBLIC(void) +cJSON_Minify(char *json) +{ + char *into = json; + + if (json == NULL) + { + return; + } + + while (json[0] != '\0') + { + switch (json[0]) + { + case ' ': + case '\t': + case '\r': + case '\n': + json++; + break; + + case '/': + if (json[1] == '/') + { + skip_oneline_comment(&json); + } + else if (json[1] == '*') + { + skip_multiline_comment(&json); + } + else + { + json++; + } + break; + + case '\"': + minify_string(&json, (char **)&into); + break; + + default: + into[0] = json[0]; + json++; + into++; + } + } + + /* and null-terminate. */ + *into = '\0'; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_IsInvalid(const cJSON *const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Invalid; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_IsFalse(const cJSON *const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_False; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_IsTrue(const cJSON *const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xff) == cJSON_True; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_IsBool(const cJSON *const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & (cJSON_True | cJSON_False)) != 0; +} +CJSON_PUBLIC(cJSON_bool) +cJSON_IsNull(const cJSON *const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_NULL; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_IsNumber(const cJSON *const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Number; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_IsString(const cJSON *const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_String; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_IsArray(const cJSON *const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Array; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_IsObject(const cJSON *const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Object; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_IsRaw(const cJSON *const item) +{ + if (item == NULL) + { + return false; + } + + return (item->type & 0xFF) == cJSON_Raw; +} + +CJSON_PUBLIC(cJSON_bool) +cJSON_Compare(const cJSON *const a, const cJSON *const b, const cJSON_bool case_sensitive) +{ + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) + { + return false; + } + + /* check if type is valid */ + switch (a->type & 0xFF) + { + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + case cJSON_Number: + case cJSON_String: + case cJSON_Raw: + case cJSON_Array: + case cJSON_Object: + break; + + default: + return false; + } + + /* identical objects are equal */ + if (a == b) + { + return true; + } + + switch (a->type & 0xFF) + { + /* in these cases and equal type is enough */ + case cJSON_False: + case cJSON_True: + case cJSON_NULL: + return true; + + case cJSON_Number: + if (compare_double(a->valuedouble, b->valuedouble)) + { + return true; + } + return false; + + case cJSON_String: + case cJSON_Raw: + if ((a->valuestring == NULL) || (b->valuestring == NULL)) + { + return false; + } + if (strcmp(a->valuestring, b->valuestring) == 0) + { + return true; + } + + return false; + + case cJSON_Array: + { + cJSON *a_element = a->child; + cJSON *b_element = b->child; + + for (; (a_element != NULL) && (b_element != NULL);) + { + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + + a_element = a_element->next; + b_element = b_element->next; + } + + /* one of the arrays is longer than the other */ + if (a_element != b_element) + { + return false; + } + + return true; + } + + case cJSON_Object: + { + cJSON *a_element = NULL; + cJSON *b_element = NULL; + cJSON_ArrayForEach(a_element, a) + { + /* TODO This has O(n^2) runtime, which is horrible! */ + b_element = get_object_item(b, a_element->string, case_sensitive); + if (b_element == NULL) + { + return false; + } + + if (!cJSON_Compare(a_element, b_element, case_sensitive)) + { + return false; + } + } + + /* doing this twice, once on a and b to prevent true comparison if a subset of b + * TODO: Do this the proper way, this is just a fix for now */ + cJSON_ArrayForEach(b_element, b) + { + a_element = get_object_item(a, b_element->string, case_sensitive); + if (a_element == NULL) + { + return false; + } + + if (!cJSON_Compare(b_element, a_element, case_sensitive)) + { + return false; + } + } + + return true; + } + + default: + return false; + } +} + +CJSON_PUBLIC(void *) +cJSON_malloc(size_t size) +{ + return global_hooks.allocate(size); +} + +CJSON_PUBLIC(void) +cJSON_free(void *object) +{ + global_hooks.deallocate(object); + object = NULL; +} \ No newline at end of file diff --git a/c/third_party/cjson/cJSON.h b/c/third_party/cjson/cJSON.h new file mode 100644 index 0000000..c760c95 --- /dev/null +++ b/c/third_party/cjson/cJSON.h @@ -0,0 +1,381 @@ +/* + Copyright (c) 2009-2017 Dave Gamble and cJSON contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +#ifndef cJSON__h +#define cJSON__h + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32)) +#define __WINDOWS__ +#endif + +#ifdef __WINDOWS__ + + /* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options: + + CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols + CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default) + CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol + + For *nix builds that support visibility attribute, you can define similar behavior by + + setting default visibility to hidden by adding + -fvisibility=hidden (for gcc) + or + -xldscope=hidden (for sun cc) + to CFLAGS + + then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does + + */ + +#define CJSON_CDECL __cdecl +#define CJSON_STDCALL __stdcall + +/* export symbols by default, this is necessary for copy pasting the C and header file */ +#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_EXPORT_SYMBOLS +#endif + +#if defined(CJSON_HIDE_SYMBOLS) +#define CJSON_PUBLIC(type) type CJSON_STDCALL +#elif defined(CJSON_EXPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL +#elif defined(CJSON_IMPORT_SYMBOLS) +#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL +#endif +#else /* !__WINDOWS__ */ +#define CJSON_CDECL +#define CJSON_STDCALL + +#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined(__SUNPRO_C)) && defined(CJSON_API_VISIBILITY) +#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type +#else +#define CJSON_PUBLIC(type) type +#endif +#endif + +/* project version */ +#define CJSON_VERSION_MAJOR 1 +#define CJSON_VERSION_MINOR 7 +#define CJSON_VERSION_PATCH 19 + +#include + +/* cJSON Types: */ +#define cJSON_Invalid (0) +#define cJSON_False (1 << 0) +#define cJSON_True (1 << 1) +#define cJSON_NULL (1 << 2) +#define cJSON_Number (1 << 3) +#define cJSON_String (1 << 4) +#define cJSON_Array (1 << 5) +#define cJSON_Object (1 << 6) +#define cJSON_Raw (1 << 7) /* raw json */ + +#define cJSON_IsReference 256 +#define cJSON_StringIsConst 512 + + /* The cJSON structure: */ + typedef struct cJSON + { + /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ + struct cJSON *next; + struct cJSON *prev; + /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ + struct cJSON *child; + + /* The type of the item, as above. */ + int type; + + /* The item's string, if type==cJSON_String and type == cJSON_Raw */ + char *valuestring; + /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ + int valueint; + /* The item's number, if type==cJSON_Number */ + double valuedouble; + + /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ + char *string; + } cJSON; + + typedef struct cJSON_Hooks + { + /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */ + void *(CJSON_CDECL *malloc_fn)(size_t sz); + void(CJSON_CDECL *free_fn)(void *ptr); + } cJSON_Hooks; + + typedef int cJSON_bool; + +/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_NESTING_LIMIT +#define CJSON_NESTING_LIMIT 1000 +#endif + +/* Limits the length of circular references can be before cJSON rejects to parse them. + * This is to prevent stack overflows. */ +#ifndef CJSON_CIRCULAR_LIMIT +#define CJSON_CIRCULAR_LIMIT 10000 +#endif + + /* returns the version of cJSON as a string */ + CJSON_PUBLIC(const char *) + cJSON_Version(void); + + /* Supply malloc, realloc and free functions to cJSON */ + CJSON_PUBLIC(void) + cJSON_InitHooks(cJSON_Hooks *hooks); + + /* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ + /* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ + CJSON_PUBLIC(cJSON *) + cJSON_Parse(const char *value); + CJSON_PUBLIC(cJSON *) + cJSON_ParseWithLength(const char *value, size_t buffer_length); + /* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ + /* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ + CJSON_PUBLIC(cJSON *) + cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); + CJSON_PUBLIC(cJSON *) + cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); + + /* Render a cJSON entity to text for transfer/storage. */ + CJSON_PUBLIC(char *) + cJSON_Print(const cJSON *item); + /* Render a cJSON entity to text for transfer/storage without any formatting. */ + CJSON_PUBLIC(char *) + cJSON_PrintUnformatted(const cJSON *item); + /* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */ + CJSON_PUBLIC(char *) + cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt); + /* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */ + /* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */ + CJSON_PUBLIC(cJSON_bool) + cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format); + /* Delete a cJSON entity and all subentities. */ + CJSON_PUBLIC(void) + cJSON_Delete(cJSON *item); + + /* Returns the number of items in an array (or object). */ + CJSON_PUBLIC(int) + cJSON_GetArraySize(const cJSON *array); + /* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */ + CJSON_PUBLIC(cJSON *) + cJSON_GetArrayItem(const cJSON *array, int index); + /* Get item "string" from object. Case insensitive. */ + CJSON_PUBLIC(cJSON *) + cJSON_GetObjectItem(const cJSON *const object, const char *const string); + CJSON_PUBLIC(cJSON *) + cJSON_GetObjectItemCaseSensitive(const cJSON *const object, const char *const string); + CJSON_PUBLIC(cJSON_bool) + cJSON_HasObjectItem(const cJSON *object, const char *string); + /* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ + CJSON_PUBLIC(const char *) + cJSON_GetErrorPtr(void); + + /* Check item type and return its value */ + CJSON_PUBLIC(char *) + cJSON_GetStringValue(const cJSON *const item); + CJSON_PUBLIC(double) + cJSON_GetNumberValue(const cJSON *const item); + + /* These functions check the type of an item */ + CJSON_PUBLIC(cJSON_bool) + cJSON_IsInvalid(const cJSON *const item); + CJSON_PUBLIC(cJSON_bool) + cJSON_IsFalse(const cJSON *const item); + CJSON_PUBLIC(cJSON_bool) + cJSON_IsTrue(const cJSON *const item); + CJSON_PUBLIC(cJSON_bool) + cJSON_IsBool(const cJSON *const item); + CJSON_PUBLIC(cJSON_bool) + cJSON_IsNull(const cJSON *const item); + CJSON_PUBLIC(cJSON_bool) + cJSON_IsNumber(const cJSON *const item); + CJSON_PUBLIC(cJSON_bool) + cJSON_IsString(const cJSON *const item); + CJSON_PUBLIC(cJSON_bool) + cJSON_IsArray(const cJSON *const item); + CJSON_PUBLIC(cJSON_bool) + cJSON_IsObject(const cJSON *const item); + CJSON_PUBLIC(cJSON_bool) + cJSON_IsRaw(const cJSON *const item); + + /* These calls create a cJSON item of the appropriate type. */ + CJSON_PUBLIC(cJSON *) + cJSON_CreateNull(void); + CJSON_PUBLIC(cJSON *) + cJSON_CreateTrue(void); + CJSON_PUBLIC(cJSON *) + cJSON_CreateFalse(void); + CJSON_PUBLIC(cJSON *) + cJSON_CreateBool(cJSON_bool boolean); + CJSON_PUBLIC(cJSON *) + cJSON_CreateNumber(double num); + CJSON_PUBLIC(cJSON *) + cJSON_CreateString(const char *string); + /* raw json */ + CJSON_PUBLIC(cJSON *) + cJSON_CreateRaw(const char *raw); + CJSON_PUBLIC(cJSON *) + cJSON_CreateArray(void); + CJSON_PUBLIC(cJSON *) + cJSON_CreateObject(void); + + /* Create a string where valuestring references a string so + * it will not be freed by cJSON_Delete */ + CJSON_PUBLIC(cJSON *) + cJSON_CreateStringReference(const char *string); + /* Create an object/array that only references it's elements so + * they will not be freed by cJSON_Delete */ + CJSON_PUBLIC(cJSON *) + cJSON_CreateObjectReference(const cJSON *child); + CJSON_PUBLIC(cJSON *) + cJSON_CreateArrayReference(const cJSON *child); + + /* These utilities create an Array of count items. + * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/ + CJSON_PUBLIC(cJSON *) + cJSON_CreateIntArray(const int *numbers, int count); + CJSON_PUBLIC(cJSON *) + cJSON_CreateFloatArray(const float *numbers, int count); + CJSON_PUBLIC(cJSON *) + cJSON_CreateDoubleArray(const double *numbers, int count); + CJSON_PUBLIC(cJSON *) + cJSON_CreateStringArray(const char *const *strings, int count); + + /* Append item to the specified array/object. */ + CJSON_PUBLIC(cJSON_bool) + cJSON_AddItemToArray(cJSON *array, cJSON *item); + CJSON_PUBLIC(cJSON_bool) + cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); + /* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object. + * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before + * writing to `item->string` */ + CJSON_PUBLIC(cJSON_bool) + cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item); + /* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ + CJSON_PUBLIC(cJSON_bool) + cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); + CJSON_PUBLIC(cJSON_bool) + cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item); + + /* Remove/Detach items from Arrays/Objects. */ + CJSON_PUBLIC(cJSON *) + cJSON_DetachItemViaPointer(cJSON *parent, cJSON *const item); + CJSON_PUBLIC(cJSON *) + cJSON_DetachItemFromArray(cJSON *array, int which); + CJSON_PUBLIC(void) + cJSON_DeleteItemFromArray(cJSON *array, int which); + CJSON_PUBLIC(cJSON *) + cJSON_DetachItemFromObject(cJSON *object, const char *string); + CJSON_PUBLIC(cJSON *) + cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string); + CJSON_PUBLIC(void) + cJSON_DeleteItemFromObject(cJSON *object, const char *string); + CJSON_PUBLIC(void) + cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string); + + /* Update array items. */ + CJSON_PUBLIC(cJSON_bool) + cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */ + CJSON_PUBLIC(cJSON_bool) + cJSON_ReplaceItemViaPointer(cJSON *const parent, cJSON *const item, cJSON *replacement); + CJSON_PUBLIC(cJSON_bool) + cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem); + CJSON_PUBLIC(cJSON_bool) + cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem); + CJSON_PUBLIC(cJSON_bool) + cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem); + + /* Duplicate a cJSON item */ + CJSON_PUBLIC(cJSON *) + cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); + /* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will + * need to be released. With recurse!=0, it will duplicate any children connected to the item. + * The item->next and ->prev pointers are always zero on return from Duplicate. */ + /* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. + * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ + CJSON_PUBLIC(cJSON_bool) + cJSON_Compare(const cJSON *const a, const cJSON *const b, const cJSON_bool case_sensitive); + + /* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. + * The input pointer json cannot point to a read-only address area, such as a string constant, + * but should point to a readable and writable address area. */ + CJSON_PUBLIC(void) + cJSON_Minify(char *json); + + /* Helper functions for creating and adding items to an object at the same time. + * They return the added item or NULL on failure. */ + CJSON_PUBLIC(cJSON *) + cJSON_AddNullToObject(cJSON *const object, const char *const name); + CJSON_PUBLIC(cJSON *) + cJSON_AddTrueToObject(cJSON *const object, const char *const name); + CJSON_PUBLIC(cJSON *) + cJSON_AddFalseToObject(cJSON *const object, const char *const name); + CJSON_PUBLIC(cJSON *) + cJSON_AddBoolToObject(cJSON *const object, const char *const name, const cJSON_bool boolean); + CJSON_PUBLIC(cJSON *) + cJSON_AddNumberToObject(cJSON *const object, const char *const name, const double number); + CJSON_PUBLIC(cJSON *) + cJSON_AddStringToObject(cJSON *const object, const char *const name, const char *const string); + CJSON_PUBLIC(cJSON *) + cJSON_AddRawToObject(cJSON *const object, const char *const name, const char *const raw); + CJSON_PUBLIC(cJSON *) + cJSON_AddObjectToObject(cJSON *const object, const char *const name); + CJSON_PUBLIC(cJSON *) + cJSON_AddArrayToObject(cJSON *const object, const char *const name); + +/* When assigning an integer value, it needs to be propagated to valuedouble too. */ +#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number)) + /* helper for the cJSON_SetNumberValue macro */ + CJSON_PUBLIC(double) + cJSON_SetNumberHelper(cJSON *object, double number); +#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number)) + /* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */ + CJSON_PUBLIC(char *) + cJSON_SetValuestring(cJSON *object, const char *valuestring); + +/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/ +#define cJSON_SetBoolValue(object, boolValue) ( \ + (object != NULL && ((object)->type & (cJSON_False | cJSON_True))) ? (object)->type = ((object)->type & (~(cJSON_False | cJSON_True))) | ((boolValue) ? cJSON_True : cJSON_False) : cJSON_Invalid) + +/* Macro for iterating over an array or object */ +#define cJSON_ArrayForEach(element, array) for (element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) + + /* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ + CJSON_PUBLIC(void *) + cJSON_malloc(size_t size); + CJSON_PUBLIC(void) + cJSON_free(void *object); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/c/third_party/kcp/ikcp.c b/c/third_party/kcp/ikcp.c new file mode 100644 index 0000000..b6176b9 --- /dev/null +++ b/c/third_party/kcp/ikcp.c @@ -0,0 +1,1460 @@ +//===================================================================== +// +// KCP - A Better ARQ Protocol Implementation +// skywind3000 (at) gmail.com, 2010-2011 +// +// Features: +// + Average RTT reduce 30% - 40% vs traditional ARQ like tcp. +// + Maximum RTT reduce three times vs tcp. +// + Lightweight, distributed as a single source file. +// +//===================================================================== +#include "ikcp.h" + +#include +#include +#include +#include +#include + +#define IKCP_FASTACK_CONSERVE + +//===================================================================== +// KCP BASIC +//===================================================================== +const IUINT32 IKCP_RTO_NDL = 30; // no delay min rto +const IUINT32 IKCP_RTO_MIN = 100; // normal min rto +const IUINT32 IKCP_RTO_DEF = 200; +const IUINT32 IKCP_RTO_MAX = 60000; +const IUINT32 IKCP_CMD_PUSH = 81; // cmd: push data +const IUINT32 IKCP_CMD_ACK = 82; // cmd: ack +const IUINT32 IKCP_CMD_WASK = 83; // cmd: window probe (ask) +const IUINT32 IKCP_CMD_WINS = 84; // cmd: window size (tell) +const IUINT32 IKCP_ASK_SEND = 1; // need to send IKCP_CMD_WASK +const IUINT32 IKCP_ASK_TELL = 2; // need to send IKCP_CMD_WINS +const IUINT32 IKCP_WND_SND = 32; +const IUINT32 IKCP_WND_RCV = 128; // must >= max fragment size +const IUINT32 IKCP_MTU_DEF = 1400; +const IUINT32 IKCP_ACK_FAST = 3; +const IUINT32 IKCP_INTERVAL = 100; +const IUINT32 IKCP_OVERHEAD = 24; +const IUINT32 IKCP_DEADLINK = 20; +const IUINT32 IKCP_THRESH_INIT = 2; +const IUINT32 IKCP_THRESH_MIN = 2; +const IUINT32 IKCP_PROBE_INIT = 7000; // 7 secs to probe window size +const IUINT32 IKCP_PROBE_LIMIT = 120000; // up to 120 secs to probe window +const IUINT32 IKCP_FASTACK_LIMIT = 5; // max times to trigger fastack + +//--------------------------------------------------------------------- +// encode / decode +//--------------------------------------------------------------------- + +/* encode 8 bits unsigned int */ +static inline char *ikcp_encode8u(char *p, unsigned char c) +{ + *(unsigned char *)p++ = c; + return p; +} + +/* decode 8 bits unsigned int */ +static inline const char *ikcp_decode8u(const char *p, unsigned char *c) +{ + *c = *(unsigned char *)p++; + return p; +} + +/* encode 16 bits unsigned int (lsb) */ +static inline char *ikcp_encode16u(char *p, unsigned short w) +{ +#if IWORDS_BIG_ENDIAN || IWORDS_MUST_ALIGN + *(unsigned char *)(p + 0) = (w & 255); + *(unsigned char *)(p + 1) = (w >> 8); +#else + memcpy(p, &w, 2); +#endif + p += 2; + return p; +} + +/* decode 16 bits unsigned int (lsb) */ +static inline const char *ikcp_decode16u(const char *p, unsigned short *w) +{ +#if IWORDS_BIG_ENDIAN || IWORDS_MUST_ALIGN + *w = *(const unsigned char *)(p + 1); + *w = *(const unsigned char *)(p + 0) + (*w << 8); +#else + memcpy(w, p, 2); +#endif + p += 2; + return p; +} + +/* encode 32 bits unsigned int (lsb) */ +static inline char *ikcp_encode32u(char *p, IUINT32 l) +{ +#if IWORDS_BIG_ENDIAN || IWORDS_MUST_ALIGN + *(unsigned char *)(p + 0) = (unsigned char)((l >> 0) & 0xff); + *(unsigned char *)(p + 1) = (unsigned char)((l >> 8) & 0xff); + *(unsigned char *)(p + 2) = (unsigned char)((l >> 16) & 0xff); + *(unsigned char *)(p + 3) = (unsigned char)((l >> 24) & 0xff); +#else + memcpy(p, &l, 4); +#endif + p += 4; + return p; +} + +/* decode 32 bits unsigned int (lsb) */ +static inline const char *ikcp_decode32u(const char *p, IUINT32 *l) +{ +#if IWORDS_BIG_ENDIAN || IWORDS_MUST_ALIGN + *l = *(const unsigned char *)(p + 3); + *l = *(const unsigned char *)(p + 2) + (*l << 8); + *l = *(const unsigned char *)(p + 1) + (*l << 8); + *l = *(const unsigned char *)(p + 0) + (*l << 8); +#else + memcpy(l, p, 4); +#endif + p += 4; + return p; +} + +static inline IUINT32 _imin_(IUINT32 a, IUINT32 b) +{ + return a <= b ? a : b; +} + +static inline IUINT32 _imax_(IUINT32 a, IUINT32 b) +{ + return a >= b ? a : b; +} + +static inline IUINT32 _ibound_(IUINT32 lower, IUINT32 middle, IUINT32 upper) +{ + return _imin_(_imax_(lower, middle), upper); +} + +static inline long _itimediff(IUINT32 later, IUINT32 earlier) +{ + return ((IINT32)(later - earlier)); +} + +//--------------------------------------------------------------------- +// manage segment +//--------------------------------------------------------------------- +typedef struct IKCPSEG IKCPSEG; + +static void *(*ikcp_malloc_hook)(size_t) = NULL; +static void (*ikcp_free_hook)(void *) = NULL; + +// internal malloc +static void *ikcp_malloc(size_t size) +{ + if (ikcp_malloc_hook) + return ikcp_malloc_hook(size); + return malloc(size); +} + +// internal free +static void ikcp_free(void *ptr) +{ + if (ikcp_free_hook) + { + ikcp_free_hook(ptr); + } + else + { + free(ptr); + } +} + +// redefine allocator +void ikcp_allocator(void *(*new_malloc)(size_t), void (*new_free)(void *)) +{ + ikcp_malloc_hook = new_malloc; + ikcp_free_hook = new_free; +} + +// allocate a new kcp segment +static IKCPSEG *ikcp_segment_new(ikcpcb *kcp, int size) +{ + return (IKCPSEG *)ikcp_malloc(sizeof(IKCPSEG) + size); +} + +// delete a segment +static void ikcp_segment_delete(ikcpcb *kcp, IKCPSEG *seg) +{ + ikcp_free(seg); +} + +// write log +void ikcp_log(ikcpcb *kcp, int mask, const char *fmt, ...) +{ + char buffer[1024]; + va_list argptr; + if ((mask & kcp->logmask) == 0 || kcp->writelog == 0) + return; + va_start(argptr, fmt); + vsprintf(buffer, fmt, argptr); + va_end(argptr); + kcp->writelog(buffer, kcp, kcp->user); +} + +// check log mask +static int ikcp_canlog(const ikcpcb *kcp, int mask) +{ + if ((mask & kcp->logmask) == 0 || kcp->writelog == NULL) + return 0; + return 1; +} + +// output segment +static int ikcp_output(ikcpcb *kcp, const void *data, int size) +{ + assert(kcp); + assert(kcp->output); + if (ikcp_canlog(kcp, IKCP_LOG_OUTPUT)) + { + ikcp_log(kcp, IKCP_LOG_OUTPUT, "[RO] %ld bytes", (long)size); + } + if (size == 0) + return 0; + return kcp->output((const char *)data, size, kcp, kcp->user); +} + +// output queue +void ikcp_qprint(const char *name, const struct IQUEUEHEAD *head) +{ +#if 0 + const struct IQUEUEHEAD *p; + printf("<%s>: [", name); + for (p = head->next; p != head; p = p->next) { + const IKCPSEG *seg = iqueue_entry(p, const IKCPSEG, node); + printf("(%lu %d)", (unsigned long)seg->sn, (int)(seg->ts % 10000)); + if (p->next != head) printf(","); + } + printf("]\n"); +#endif +} + +//--------------------------------------------------------------------- +// create a new kcpcb +//--------------------------------------------------------------------- +ikcpcb *ikcp_create(IUINT32 conv, void *user) +{ + ikcpcb *kcp = (ikcpcb *)ikcp_malloc(sizeof(struct IKCPCB)); + if (kcp == NULL) + return NULL; + kcp->conv = conv; + kcp->user = user; + kcp->snd_una = 0; + kcp->snd_nxt = 0; + kcp->rcv_nxt = 0; + kcp->ts_recent = 0; + kcp->ts_lastack = 0; + kcp->ts_probe = 0; + kcp->probe_wait = 0; + kcp->snd_wnd = IKCP_WND_SND; + kcp->rcv_wnd = IKCP_WND_RCV; + kcp->rmt_wnd = IKCP_WND_RCV; + kcp->cwnd = 0; + kcp->incr = 0; + kcp->probe = 0; + kcp->mtu = IKCP_MTU_DEF; + kcp->mss = kcp->mtu - IKCP_OVERHEAD; + kcp->stream = 0; + + kcp->buffer = (char *)ikcp_malloc((kcp->mtu + IKCP_OVERHEAD) * 3); + if (kcp->buffer == NULL) + { + ikcp_free(kcp); + return NULL; + } + + iqueue_init(&kcp->snd_queue); + iqueue_init(&kcp->rcv_queue); + iqueue_init(&kcp->snd_buf); + iqueue_init(&kcp->rcv_buf); + kcp->nrcv_buf = 0; + kcp->nsnd_buf = 0; + kcp->nrcv_que = 0; + kcp->nsnd_que = 0; + kcp->state = 0; + kcp->acklist = NULL; + kcp->ackblock = 0; + kcp->ackcount = 0; + kcp->rx_srtt = 0; + kcp->rx_rttval = 0; + kcp->rx_rto = IKCP_RTO_DEF; + kcp->rx_minrto = IKCP_RTO_MIN; + kcp->current = 0; + kcp->interval = IKCP_INTERVAL; + kcp->ts_flush = IKCP_INTERVAL; + kcp->nodelay = 0; + kcp->updated = 0; + kcp->logmask = 0; + kcp->ssthresh = IKCP_THRESH_INIT; + kcp->fastresend = 0; + kcp->fastlimit = IKCP_FASTACK_LIMIT; + kcp->nocwnd = 0; + kcp->xmit = 0; + kcp->dead_link = IKCP_DEADLINK; + kcp->output = NULL; + kcp->writelog = NULL; + + return kcp; +} + +//--------------------------------------------------------------------- +// release a new kcpcb +//--------------------------------------------------------------------- +void ikcp_release(ikcpcb *kcp) +{ + assert(kcp); + if (kcp) + { + IKCPSEG *seg; + while (!iqueue_is_empty(&kcp->snd_buf)) + { + seg = iqueue_entry(kcp->snd_buf.next, IKCPSEG, node); + iqueue_del(&seg->node); + ikcp_segment_delete(kcp, seg); + } + while (!iqueue_is_empty(&kcp->rcv_buf)) + { + seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node); + iqueue_del(&seg->node); + ikcp_segment_delete(kcp, seg); + } + while (!iqueue_is_empty(&kcp->snd_queue)) + { + seg = iqueue_entry(kcp->snd_queue.next, IKCPSEG, node); + iqueue_del(&seg->node); + ikcp_segment_delete(kcp, seg); + } + while (!iqueue_is_empty(&kcp->rcv_queue)) + { + seg = iqueue_entry(kcp->rcv_queue.next, IKCPSEG, node); + iqueue_del(&seg->node); + ikcp_segment_delete(kcp, seg); + } + if (kcp->buffer) + { + ikcp_free(kcp->buffer); + } + if (kcp->acklist) + { + ikcp_free(kcp->acklist); + } + + kcp->nrcv_buf = 0; + kcp->nsnd_buf = 0; + kcp->nrcv_que = 0; + kcp->nsnd_que = 0; + kcp->ackcount = 0; + kcp->buffer = NULL; + kcp->acklist = NULL; + ikcp_free(kcp); + } +} + +//--------------------------------------------------------------------- +// set output callback, which will be invoked by kcp +//--------------------------------------------------------------------- +void ikcp_setoutput(ikcpcb *kcp, int (*output)(const char *buf, int len, + ikcpcb *kcp, void *user)) +{ + kcp->output = output; +} + +//--------------------------------------------------------------------- +// user/upper level recv: returns size, returns below zero for EAGAIN +//--------------------------------------------------------------------- +int ikcp_recv(ikcpcb *kcp, char *buffer, int len) +{ + struct IQUEUEHEAD *p; + int ispeek = (len < 0) ? 1 : 0; + int peeksize; + int recover = 0; + IKCPSEG *seg; + assert(kcp); + + if (iqueue_is_empty(&kcp->rcv_queue)) + return -1; + + if (len < 0) + len = -len; + + peeksize = ikcp_peeksize(kcp); + + if (peeksize < 0) + return -2; + + if (peeksize > len) + return -3; + + if (kcp->nrcv_que >= kcp->rcv_wnd) + recover = 1; + + // merge fragment + for (len = 0, p = kcp->rcv_queue.next; p != &kcp->rcv_queue;) + { + int fragment; + seg = iqueue_entry(p, IKCPSEG, node); + p = p->next; + + if (buffer) + { + memcpy(buffer, seg->data, seg->len); + buffer += seg->len; + } + + len += seg->len; + fragment = seg->frg; + + if (ikcp_canlog(kcp, IKCP_LOG_RECV)) + { + ikcp_log(kcp, IKCP_LOG_RECV, "recv sn=%lu", (unsigned long)seg->sn); + } + + if (ispeek == 0) + { + iqueue_del(&seg->node); + ikcp_segment_delete(kcp, seg); + kcp->nrcv_que--; + } + + if (fragment == 0) + break; + } + + assert(len == peeksize); + + // move available data from rcv_buf -> rcv_queue + while (!iqueue_is_empty(&kcp->rcv_buf)) + { + seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node); + if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) + { + iqueue_del(&seg->node); + kcp->nrcv_buf--; + iqueue_add_tail(&seg->node, &kcp->rcv_queue); + kcp->nrcv_que++; + kcp->rcv_nxt++; + } + else + { + break; + } + } + + // fast recover + if (kcp->nrcv_que < kcp->rcv_wnd && recover) + { + // ready to send back IKCP_CMD_WINS in ikcp_flush + // tell remote my window size + kcp->probe |= IKCP_ASK_TELL; + } + + return len; +} + +//--------------------------------------------------------------------- +// peek data size +//--------------------------------------------------------------------- +int ikcp_peeksize(const ikcpcb *kcp) +{ + struct IQUEUEHEAD *p; + IKCPSEG *seg; + int length = 0; + + assert(kcp); + + if (iqueue_is_empty(&kcp->rcv_queue)) + return -1; + + seg = iqueue_entry(kcp->rcv_queue.next, IKCPSEG, node); + if (seg->frg == 0) + return seg->len; + + if (kcp->nrcv_que < seg->frg + 1) + return -1; + + for (p = kcp->rcv_queue.next; p != &kcp->rcv_queue; p = p->next) + { + seg = iqueue_entry(p, IKCPSEG, node); + length += seg->len; + if (seg->frg == 0) + break; + } + + return length; +} + +//--------------------------------------------------------------------- +// user/upper level send, returns below zero for error +//--------------------------------------------------------------------- +int ikcp_send(ikcpcb *kcp, const char *buffer, int len) +{ + IKCPSEG *seg; + int count, i; + int sent = 0; + + assert(kcp->mss > 0); + if (len < 0) + return -1; + + // append to previous segment in streaming mode (if possible) + if (kcp->stream != 0) + { + if (!iqueue_is_empty(&kcp->snd_queue)) + { + IKCPSEG *old = iqueue_entry(kcp->snd_queue.prev, IKCPSEG, node); + if (old->len < kcp->mss) + { + int capacity = kcp->mss - old->len; + int extend = (len < capacity) ? len : capacity; + seg = ikcp_segment_new(kcp, old->len + extend); + assert(seg); + if (seg == NULL) + { + return -2; + } + iqueue_add_tail(&seg->node, &kcp->snd_queue); + memcpy(seg->data, old->data, old->len); + if (buffer) + { + memcpy(seg->data + old->len, buffer, extend); + buffer += extend; + } + seg->len = old->len + extend; + seg->frg = 0; + len -= extend; + iqueue_del_init(&old->node); + ikcp_segment_delete(kcp, old); + sent = extend; + } + } + if (len <= 0) + { + return sent; + } + } + + if (len <= (int)kcp->mss) + count = 1; + else + count = (len + kcp->mss - 1) / kcp->mss; + + if (count >= (int)IKCP_WND_RCV) + { + if (kcp->stream != 0 && sent > 0) + return sent; + return -2; + } + + if (count == 0) + count = 1; + + // fragment + for (i = 0; i < count; i++) + { + int size = len > (int)kcp->mss ? (int)kcp->mss : len; + seg = ikcp_segment_new(kcp, size); + assert(seg); + if (seg == NULL) + { + return -2; + } + if (buffer && len > 0) + { + memcpy(seg->data, buffer, size); + } + seg->len = size; + seg->frg = (kcp->stream == 0) ? (count - i - 1) : 0; + iqueue_init(&seg->node); + iqueue_add_tail(&seg->node, &kcp->snd_queue); + kcp->nsnd_que++; + if (buffer) + { + buffer += size; + } + len -= size; + sent += size; + } + + return sent; +} + +//--------------------------------------------------------------------- +// parse ack +//--------------------------------------------------------------------- +static void ikcp_update_ack(ikcpcb *kcp, IINT32 rtt) +{ + IINT32 rto = 0; + if (kcp->rx_srtt == 0) + { + kcp->rx_srtt = rtt; + kcp->rx_rttval = rtt / 2; + } + else + { + long delta = rtt - kcp->rx_srtt; + if (delta < 0) + delta = -delta; + kcp->rx_rttval = (3 * kcp->rx_rttval + delta) / 4; + kcp->rx_srtt = (7 * kcp->rx_srtt + rtt) / 8; + if (kcp->rx_srtt < 1) + kcp->rx_srtt = 1; + } + rto = kcp->rx_srtt + _imax_(kcp->interval, 4 * kcp->rx_rttval); + kcp->rx_rto = _ibound_(kcp->rx_minrto, rto, IKCP_RTO_MAX); +} + +static void ikcp_shrink_buf(ikcpcb *kcp) +{ + struct IQUEUEHEAD *p = kcp->snd_buf.next; + if (p != &kcp->snd_buf) + { + IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node); + kcp->snd_una = seg->sn; + } + else + { + kcp->snd_una = kcp->snd_nxt; + } +} + +static void ikcp_parse_ack(ikcpcb *kcp, IUINT32 sn) +{ + struct IQUEUEHEAD *p, *next; + + if (_itimediff(sn, kcp->snd_una) < 0 || _itimediff(sn, kcp->snd_nxt) >= 0) + return; + + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) + { + IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node); + next = p->next; + if (sn == seg->sn) + { + iqueue_del(p); + ikcp_segment_delete(kcp, seg); + kcp->nsnd_buf--; + break; + } + if (_itimediff(sn, seg->sn) < 0) + { + break; + } + } +} + +static void ikcp_parse_una(ikcpcb *kcp, IUINT32 una) +{ + struct IQUEUEHEAD *p, *next; + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) + { + IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node); + next = p->next; + if (_itimediff(una, seg->sn) > 0) + { + iqueue_del(p); + ikcp_segment_delete(kcp, seg); + kcp->nsnd_buf--; + } + else + { + break; + } + } +} + +static void ikcp_parse_fastack(ikcpcb *kcp, IUINT32 sn, IUINT32 ts) +{ + struct IQUEUEHEAD *p, *next; + + if (_itimediff(sn, kcp->snd_una) < 0 || _itimediff(sn, kcp->snd_nxt) >= 0) + return; + + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = next) + { + IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node); + next = p->next; + if (_itimediff(sn, seg->sn) < 0) + { + break; + } + else if (sn != seg->sn) + { +#ifndef IKCP_FASTACK_CONSERVE + seg->fastack++; +#else + if (_itimediff(ts, seg->ts) >= 0) + seg->fastack++; +#endif + } + } +} + +//--------------------------------------------------------------------- +// ack append +//--------------------------------------------------------------------- +static void ikcp_ack_push(ikcpcb *kcp, IUINT32 sn, IUINT32 ts) +{ + IUINT32 newsize = kcp->ackcount + 1; + IUINT32 *ptr; + + if (newsize > kcp->ackblock) + { + IUINT32 *acklist; + IUINT32 newblock; + + for (newblock = 8; newblock < newsize; newblock <<= 1) + ; + acklist = (IUINT32 *)ikcp_malloc(newblock * sizeof(IUINT32) * 2); + + if (acklist == NULL) + { + assert(acklist != NULL); + abort(); + } + + if (kcp->acklist != NULL) + { + IUINT32 x; + for (x = 0; x < kcp->ackcount; x++) + { + acklist[x * 2 + 0] = kcp->acklist[x * 2 + 0]; + acklist[x * 2 + 1] = kcp->acklist[x * 2 + 1]; + } + ikcp_free(kcp->acklist); + } + + kcp->acklist = acklist; + kcp->ackblock = newblock; + } + + ptr = &kcp->acklist[kcp->ackcount * 2]; + ptr[0] = sn; + ptr[1] = ts; + kcp->ackcount++; +} + +static void ikcp_ack_get(const ikcpcb *kcp, int p, IUINT32 *sn, IUINT32 *ts) +{ + if (sn) + sn[0] = kcp->acklist[p * 2 + 0]; + if (ts) + ts[0] = kcp->acklist[p * 2 + 1]; +} + +//--------------------------------------------------------------------- +// parse data +//--------------------------------------------------------------------- +void ikcp_parse_data(ikcpcb *kcp, IKCPSEG *newseg) +{ + struct IQUEUEHEAD *p, *prev; + IUINT32 sn = newseg->sn; + int repeat = 0; + + if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) >= 0 || + _itimediff(sn, kcp->rcv_nxt) < 0) + { + ikcp_segment_delete(kcp, newseg); + return; + } + + for (p = kcp->rcv_buf.prev; p != &kcp->rcv_buf; p = prev) + { + IKCPSEG *seg = iqueue_entry(p, IKCPSEG, node); + prev = p->prev; + if (seg->sn == sn) + { + repeat = 1; + break; + } + if (_itimediff(sn, seg->sn) > 0) + { + break; + } + } + + if (repeat == 0) + { + iqueue_init(&newseg->node); + iqueue_add(&newseg->node, p); + kcp->nrcv_buf++; + } + else + { + ikcp_segment_delete(kcp, newseg); + } + +#if 0 + ikcp_qprint("rcvbuf", &kcp->rcv_buf); + printf("rcv_nxt=%lu\n", kcp->rcv_nxt); +#endif + + // move available data from rcv_buf -> rcv_queue + while (!iqueue_is_empty(&kcp->rcv_buf)) + { + IKCPSEG *seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node); + if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) + { + iqueue_del(&seg->node); + kcp->nrcv_buf--; + iqueue_add_tail(&seg->node, &kcp->rcv_queue); + kcp->nrcv_que++; + kcp->rcv_nxt++; + } + else + { + break; + } + } + +#if 0 + ikcp_qprint("queue", &kcp->rcv_queue); + printf("rcv_nxt=%lu\n", kcp->rcv_nxt); +#endif + +#if 1 +// printf("snd(buf=%d, queue=%d)\n", kcp->nsnd_buf, kcp->nsnd_que); +// printf("rcv(buf=%d, queue=%d)\n", kcp->nrcv_buf, kcp->nrcv_que); +#endif +} + +//--------------------------------------------------------------------- +// input data +//--------------------------------------------------------------------- +int ikcp_input(ikcpcb *kcp, const char *data, long size) +{ + IUINT32 prev_una = kcp->snd_una; + IUINT32 maxack = 0, latest_ts = 0; + int flag = 0; + + if (ikcp_canlog(kcp, IKCP_LOG_INPUT)) + { + ikcp_log(kcp, IKCP_LOG_INPUT, "[RI] %d bytes", (int)size); + } + + if (data == NULL || (int)size < (int)IKCP_OVERHEAD) + return -1; + + while (1) + { + IUINT32 ts, sn, len, una, conv; + IUINT16 wnd; + IUINT8 cmd, frg; + IKCPSEG *seg; + + if (size < (int)IKCP_OVERHEAD) + break; + + data = ikcp_decode32u(data, &conv); + if (conv != kcp->conv) + return -1; + + data = ikcp_decode8u(data, &cmd); + data = ikcp_decode8u(data, &frg); + data = ikcp_decode16u(data, &wnd); + data = ikcp_decode32u(data, &ts); + data = ikcp_decode32u(data, &sn); + data = ikcp_decode32u(data, &una); + data = ikcp_decode32u(data, &len); + + size -= IKCP_OVERHEAD; + + if ((long)size < (long)len || (int)len < 0) + return -2; + + if (cmd != IKCP_CMD_PUSH && cmd != IKCP_CMD_ACK && + cmd != IKCP_CMD_WASK && cmd != IKCP_CMD_WINS) + return -3; + + kcp->rmt_wnd = wnd; + ikcp_parse_una(kcp, una); + ikcp_shrink_buf(kcp); + + if (cmd == IKCP_CMD_ACK) + { + if (_itimediff(kcp->current, ts) >= 0) + { + ikcp_update_ack(kcp, _itimediff(kcp->current, ts)); + } + ikcp_parse_ack(kcp, sn); + ikcp_shrink_buf(kcp); + if (flag == 0) + { + flag = 1; + maxack = sn; + latest_ts = ts; + } + else + { + if (_itimediff(sn, maxack) > 0) + { +#ifndef IKCP_FASTACK_CONSERVE + maxack = sn; + latest_ts = ts; +#else + if (_itimediff(ts, latest_ts) > 0) + { + maxack = sn; + latest_ts = ts; + } +#endif + } + } + if (ikcp_canlog(kcp, IKCP_LOG_IN_ACK)) + { + ikcp_log(kcp, IKCP_LOG_IN_ACK, + "input ack: sn=%lu rtt=%ld rto=%ld", (unsigned long)sn, + (long)_itimediff(kcp->current, ts), + (long)kcp->rx_rto); + } + } + else if (cmd == IKCP_CMD_PUSH) + { + if (ikcp_canlog(kcp, IKCP_LOG_IN_DATA)) + { + ikcp_log(kcp, IKCP_LOG_IN_DATA, + "input psh: sn=%lu ts=%lu", (unsigned long)sn, (unsigned long)ts); + } + if (_itimediff(sn, kcp->rcv_nxt + kcp->rcv_wnd) < 0) + { + ikcp_ack_push(kcp, sn, ts); + if (_itimediff(sn, kcp->rcv_nxt) >= 0) + { + seg = ikcp_segment_new(kcp, len); + seg->conv = conv; + seg->cmd = cmd; + seg->frg = frg; + seg->wnd = wnd; + seg->ts = ts; + seg->sn = sn; + seg->una = una; + seg->len = len; + + if (len > 0) + { + memcpy(seg->data, data, len); + } + + ikcp_parse_data(kcp, seg); + } + } + } + else if (cmd == IKCP_CMD_WASK) + { + // ready to send back IKCP_CMD_WINS in ikcp_flush + // tell remote my window size + kcp->probe |= IKCP_ASK_TELL; + if (ikcp_canlog(kcp, IKCP_LOG_IN_PROBE)) + { + ikcp_log(kcp, IKCP_LOG_IN_PROBE, "input probe"); + } + } + else if (cmd == IKCP_CMD_WINS) + { + // do nothing + if (ikcp_canlog(kcp, IKCP_LOG_IN_WINS)) + { + ikcp_log(kcp, IKCP_LOG_IN_WINS, + "input wins: %lu", (unsigned long)(wnd)); + } + } + else + { + return -3; + } + + data += len; + size -= len; + } + + if (flag != 0) + { + ikcp_parse_fastack(kcp, maxack, latest_ts); + } + + if (_itimediff(kcp->snd_una, prev_una) > 0) + { + if (kcp->cwnd < kcp->rmt_wnd) + { + IUINT32 mss = kcp->mss; + if (kcp->cwnd < kcp->ssthresh) + { + kcp->cwnd++; + kcp->incr += mss; + } + else + { + if (kcp->incr < mss) + kcp->incr = mss; + kcp->incr += (mss * mss) / kcp->incr + (mss / 16); + if ((kcp->cwnd + 1) * mss <= kcp->incr) + { +#if 1 + kcp->cwnd = (kcp->incr + mss - 1) / ((mss > 0) ? mss : 1); +#else + kcp->cwnd++; +#endif + } + } + if (kcp->cwnd > kcp->rmt_wnd) + { + kcp->cwnd = kcp->rmt_wnd; + kcp->incr = kcp->rmt_wnd * mss; + } + } + } + + return 0; +} + +//--------------------------------------------------------------------- +// ikcp_encode_seg +//--------------------------------------------------------------------- +static char *ikcp_encode_seg(char *ptr, const IKCPSEG *seg) +{ + ptr = ikcp_encode32u(ptr, seg->conv); + ptr = ikcp_encode8u(ptr, (IUINT8)seg->cmd); + ptr = ikcp_encode8u(ptr, (IUINT8)seg->frg); + ptr = ikcp_encode16u(ptr, (IUINT16)seg->wnd); + ptr = ikcp_encode32u(ptr, seg->ts); + ptr = ikcp_encode32u(ptr, seg->sn); + ptr = ikcp_encode32u(ptr, seg->una); + ptr = ikcp_encode32u(ptr, seg->len); + return ptr; +} + +static int ikcp_wnd_unused(const ikcpcb *kcp) +{ + if (kcp->nrcv_que < kcp->rcv_wnd) + { + return kcp->rcv_wnd - kcp->nrcv_que; + } + return 0; +} + +//--------------------------------------------------------------------- +// ikcp_flush +//--------------------------------------------------------------------- +void ikcp_flush(ikcpcb *kcp) +{ + IUINT32 current = kcp->current; + char *buffer = kcp->buffer; + char *ptr = buffer; + int count, size, i; + IUINT32 resent, cwnd; + IUINT32 rtomin; + struct IQUEUEHEAD *p; + int change = 0; + int lost = 0; + IKCPSEG seg; + + // 'ikcp_update' haven't been called. + if (kcp->updated == 0) + return; + + seg.conv = kcp->conv; + seg.cmd = IKCP_CMD_ACK; + seg.frg = 0; + seg.wnd = ikcp_wnd_unused(kcp); + seg.una = kcp->rcv_nxt; + seg.len = 0; + seg.sn = 0; + seg.ts = 0; + + // flush acknowledges + count = kcp->ackcount; + for (i = 0; i < count; i++) + { + size = (int)(ptr - buffer); + if (size + (int)IKCP_OVERHEAD > (int)kcp->mtu) + { + ikcp_output(kcp, buffer, size); + ptr = buffer; + } + ikcp_ack_get(kcp, i, &seg.sn, &seg.ts); + ptr = ikcp_encode_seg(ptr, &seg); + } + + kcp->ackcount = 0; + + // probe window size (if remote window size equals zero) + if (kcp->rmt_wnd == 0) + { + if (kcp->probe_wait == 0) + { + kcp->probe_wait = IKCP_PROBE_INIT; + kcp->ts_probe = kcp->current + kcp->probe_wait; + } + else + { + if (_itimediff(kcp->current, kcp->ts_probe) >= 0) + { + if (kcp->probe_wait < IKCP_PROBE_INIT) + kcp->probe_wait = IKCP_PROBE_INIT; + kcp->probe_wait += kcp->probe_wait / 2; + if (kcp->probe_wait > IKCP_PROBE_LIMIT) + kcp->probe_wait = IKCP_PROBE_LIMIT; + kcp->ts_probe = kcp->current + kcp->probe_wait; + kcp->probe |= IKCP_ASK_SEND; + } + } + } + else + { + kcp->ts_probe = 0; + kcp->probe_wait = 0; + } + + // flush window probing commands + if (kcp->probe & IKCP_ASK_SEND) + { + seg.cmd = IKCP_CMD_WASK; + size = (int)(ptr - buffer); + if (size + (int)IKCP_OVERHEAD > (int)kcp->mtu) + { + ikcp_output(kcp, buffer, size); + ptr = buffer; + } + ptr = ikcp_encode_seg(ptr, &seg); + } + + // flush window probing commands + if (kcp->probe & IKCP_ASK_TELL) + { + seg.cmd = IKCP_CMD_WINS; + size = (int)(ptr - buffer); + if (size + (int)IKCP_OVERHEAD > (int)kcp->mtu) + { + ikcp_output(kcp, buffer, size); + ptr = buffer; + } + ptr = ikcp_encode_seg(ptr, &seg); + } + + kcp->probe = 0; + + // calculate window size + cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd); + if (kcp->nocwnd == 0) + cwnd = _imin_(kcp->cwnd, cwnd); + + // move data from snd_queue to snd_buf + while (_itimediff(kcp->snd_nxt, kcp->snd_una + cwnd) < 0) + { + IKCPSEG *newseg; + if (iqueue_is_empty(&kcp->snd_queue)) + break; + + newseg = iqueue_entry(kcp->snd_queue.next, IKCPSEG, node); + + iqueue_del(&newseg->node); + iqueue_add_tail(&newseg->node, &kcp->snd_buf); + kcp->nsnd_que--; + kcp->nsnd_buf++; + + newseg->conv = kcp->conv; + newseg->cmd = IKCP_CMD_PUSH; + newseg->wnd = seg.wnd; + newseg->ts = current; + newseg->sn = kcp->snd_nxt++; + newseg->una = kcp->rcv_nxt; + newseg->resendts = current; + newseg->rto = kcp->rx_rto; + newseg->fastack = 0; + newseg->xmit = 0; + } + + // calculate resent + resent = (kcp->fastresend > 0) ? (IUINT32)kcp->fastresend : 0xffffffff; + rtomin = (kcp->nodelay == 0) ? (kcp->rx_rto >> 3) : 0; + + // flush data segments + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) + { + IKCPSEG *segment = iqueue_entry(p, IKCPSEG, node); + int needsend = 0; + if (segment->xmit == 0) + { + needsend = 1; + segment->xmit++; + segment->rto = kcp->rx_rto; + segment->resendts = current + segment->rto + rtomin; + } + else if (_itimediff(current, segment->resendts) >= 0) + { + needsend = 1; + segment->xmit++; + kcp->xmit++; + if (kcp->nodelay == 0) + { + segment->rto += _imax_(segment->rto, (IUINT32)kcp->rx_rto); + } + else + { + IINT32 step = (kcp->nodelay < 2) ? ((IINT32)(segment->rto)) : kcp->rx_rto; + segment->rto += step / 2; + } + segment->resendts = current + segment->rto; + lost = 1; + } + else if (segment->fastack >= resent) + { + if ((int)segment->xmit <= kcp->fastlimit || + kcp->fastlimit <= 0) + { + needsend = 1; + segment->xmit++; + segment->fastack = 0; + segment->resendts = current + segment->rto; + change++; + } + } + + if (needsend) + { + int need; + segment->ts = current; + segment->wnd = seg.wnd; + segment->una = kcp->rcv_nxt; + + size = (int)(ptr - buffer); + need = IKCP_OVERHEAD + segment->len; + + if (size + need > (int)kcp->mtu) + { + ikcp_output(kcp, buffer, size); + ptr = buffer; + } + + ptr = ikcp_encode_seg(ptr, segment); + + if (segment->len > 0) + { + memcpy(ptr, segment->data, segment->len); + ptr += segment->len; + } + + if (segment->xmit >= kcp->dead_link) + { + kcp->state = (IUINT32)-1; + } + } + } + + // flash remain segments + size = (int)(ptr - buffer); + if (size > 0) + { + ikcp_output(kcp, buffer, size); + } + + // update ssthresh + if (change) + { + IUINT32 inflight = kcp->snd_nxt - kcp->snd_una; + kcp->ssthresh = inflight / 2; + if (kcp->ssthresh < IKCP_THRESH_MIN) + kcp->ssthresh = IKCP_THRESH_MIN; + kcp->cwnd = kcp->ssthresh + resent; + kcp->incr = kcp->cwnd * kcp->mss; + } + + if (lost) + { + kcp->ssthresh = cwnd / 2; + if (kcp->ssthresh < IKCP_THRESH_MIN) + kcp->ssthresh = IKCP_THRESH_MIN; + kcp->cwnd = 1; + kcp->incr = kcp->mss; + } + + if (kcp->cwnd < 1) + { + kcp->cwnd = 1; + kcp->incr = kcp->mss; + } +} + +//--------------------------------------------------------------------- +// update state (call it repeatedly, every 10ms-100ms), or you can ask +// ikcp_check when to call it again (without ikcp_input/_send calling). +// 'current' - current timestamp in millisec. +//--------------------------------------------------------------------- +void ikcp_update(ikcpcb *kcp, IUINT32 current) +{ + IINT32 slap; + + kcp->current = current; + + if (kcp->updated == 0) + { + kcp->updated = 1; + kcp->ts_flush = kcp->current; + } + + slap = _itimediff(kcp->current, kcp->ts_flush); + + if (slap >= 10000 || slap < -10000) + { + kcp->ts_flush = kcp->current; + slap = 0; + } + + if (slap >= 0) + { + kcp->ts_flush += kcp->interval; + if (_itimediff(kcp->current, kcp->ts_flush) >= 0) + { + kcp->ts_flush = kcp->current + kcp->interval; + } + ikcp_flush(kcp); + } +} + +//--------------------------------------------------------------------- +// Determine when should you invoke ikcp_update: +// returns when you should invoke ikcp_update in millisec, if there +// is no ikcp_input/_send calling. you can call ikcp_update in that +// time, instead of call update repeatly. +// Important to reduce unnacessary ikcp_update invoking. use it to +// schedule ikcp_update (eg. implementing an epoll-like mechanism, +// or optimize ikcp_update when handling massive kcp connections) +//--------------------------------------------------------------------- +IUINT32 ikcp_check(const ikcpcb *kcp, IUINT32 current) +{ + IUINT32 ts_flush = kcp->ts_flush; + IINT32 tm_flush = 0x7fffffff; + IINT32 tm_packet = 0x7fffffff; + IUINT32 minimal = 0; + struct IQUEUEHEAD *p; + + if (kcp->updated == 0) + { + return current; + } + + if (_itimediff(current, ts_flush) >= 10000 || + _itimediff(current, ts_flush) < -10000) + { + ts_flush = current; + } + + if (_itimediff(current, ts_flush) >= 0) + { + return current; + } + + tm_flush = _itimediff(ts_flush, current); + + for (p = kcp->snd_buf.next; p != &kcp->snd_buf; p = p->next) + { + const IKCPSEG *seg = iqueue_entry(p, const IKCPSEG, node); + IINT32 diff = _itimediff(seg->resendts, current); + if (diff <= 0) + { + return current; + } + if (diff < tm_packet) + tm_packet = diff; + } + + minimal = (IUINT32)(tm_packet < tm_flush ? tm_packet : tm_flush); + if (minimal >= kcp->interval) + minimal = kcp->interval; + + return current + minimal; +} + +int ikcp_setmtu(ikcpcb *kcp, int mtu) +{ + char *buffer; + if (mtu < 50 || mtu < (int)IKCP_OVERHEAD) + return -1; + buffer = (char *)ikcp_malloc((mtu + IKCP_OVERHEAD) * 3); + if (buffer == NULL) + return -2; + kcp->mtu = mtu; + kcp->mss = kcp->mtu - IKCP_OVERHEAD; + ikcp_free(kcp->buffer); + kcp->buffer = buffer; + return 0; +} + +int ikcp_interval(ikcpcb *kcp, int interval) +{ + if (interval > 5000) + interval = 5000; + else if (interval < 10) + interval = 10; + kcp->interval = interval; + return 0; +} + +int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc) +{ + if (nodelay >= 0) + { + kcp->nodelay = nodelay; + if (nodelay) + { + kcp->rx_minrto = IKCP_RTO_NDL; + } + else + { + kcp->rx_minrto = IKCP_RTO_MIN; + } + } + if (interval >= 0) + { + if (interval > 5000) + interval = 5000; + else if (interval < 10) + interval = 10; + kcp->interval = interval; + } + if (resend >= 0) + { + kcp->fastresend = resend; + } + if (nc >= 0) + { + kcp->nocwnd = nc; + } + return 0; +} + +int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd) +{ + if (kcp) + { + if (sndwnd > 0) + { + kcp->snd_wnd = sndwnd; + } + if (rcvwnd > 0) + { // must >= max fragment size + kcp->rcv_wnd = _imax_(rcvwnd, IKCP_WND_RCV); + } + } + return 0; +} + +int ikcp_waitsnd(const ikcpcb *kcp) +{ + return kcp->nsnd_buf + kcp->nsnd_que; +} + +// read conv +IUINT32 ikcp_getconv(const void *ptr) +{ + IUINT32 conv; + ikcp_decode32u((const char *)ptr, &conv); + return conv; +} diff --git a/c/third_party/kcp/ikcp.h b/c/third_party/kcp/ikcp.h new file mode 100644 index 0000000..5630269 --- /dev/null +++ b/c/third_party/kcp/ikcp.h @@ -0,0 +1,418 @@ +//===================================================================== +// +// KCP - A Better ARQ Protocol Implementation +// skywind3000 (at) gmail.com, 2010-2011 +// +// Features: +// + Average RTT reduce 30% - 40% vs traditional ARQ like tcp. +// + Maximum RTT reduce three times vs tcp. +// + Lightweight, distributed as a single source file. +// +//===================================================================== +#ifndef __IKCP_H__ +#define __IKCP_H__ + +#include +#include +#include + +//===================================================================== +// 32BIT INTEGER DEFINITION +//===================================================================== +#ifndef __INTEGER_32_BITS__ +#define __INTEGER_32_BITS__ +#if defined(_WIN64) || defined(WIN64) || defined(__amd64__) || \ + defined(__x86_64) || defined(__x86_64__) || defined(_M_IA64) || \ + defined(_M_AMD64) +typedef unsigned int ISTDUINT32; +typedef int ISTDINT32; +#elif defined(_WIN32) || defined(WIN32) || defined(__i386__) || \ + defined(__i386) || defined(_M_X86) +typedef unsigned long ISTDUINT32; +typedef long ISTDINT32; +#elif defined(__MACOS__) +typedef UInt32 ISTDUINT32; +typedef SInt32 ISTDINT32; +#elif defined(__APPLE__) && defined(__MACH__) +#include +typedef u_int32_t ISTDUINT32; +typedef int32_t ISTDINT32; +#elif defined(__BEOS__) +#include +typedef u_int32_t ISTDUINT32; +typedef int32_t ISTDINT32; +#elif (defined(_MSC_VER) || defined(__BORLANDC__)) && (!defined(__MSDOS__)) +typedef unsigned __int32 ISTDUINT32; +typedef __int32 ISTDINT32; +#elif defined(__GNUC__) +#include +typedef uint32_t ISTDUINT32; +typedef int32_t ISTDINT32; +#else +typedef unsigned long ISTDUINT32; +typedef long ISTDINT32; +#endif +#endif + +//===================================================================== +// Integer Definition +//===================================================================== +#ifndef __IINT8_DEFINED +#define __IINT8_DEFINED +typedef char IINT8; +#endif + +#ifndef __IUINT8_DEFINED +#define __IUINT8_DEFINED +typedef unsigned char IUINT8; +#endif + +#ifndef __IUINT16_DEFINED +#define __IUINT16_DEFINED +typedef unsigned short IUINT16; +#endif + +#ifndef __IINT16_DEFINED +#define __IINT16_DEFINED +typedef short IINT16; +#endif + +#ifndef __IINT32_DEFINED +#define __IINT32_DEFINED +typedef ISTDINT32 IINT32; +#endif + +#ifndef __IUINT32_DEFINED +#define __IUINT32_DEFINED +typedef ISTDUINT32 IUINT32; +#endif + +#ifndef __IINT64_DEFINED +#define __IINT64_DEFINED +#if defined(_MSC_VER) || defined(__BORLANDC__) +typedef __int64 IINT64; +#else +typedef long long IINT64; +#endif +#endif + +#ifndef __IUINT64_DEFINED +#define __IUINT64_DEFINED +#if defined(_MSC_VER) || defined(__BORLANDC__) +typedef unsigned __int64 IUINT64; +#else +typedef unsigned long long IUINT64; +#endif +#endif + +#ifndef INLINE +#if defined(__GNUC__) + +#if (__GNUC__ > 3) || ((__GNUC__ == 3) && (__GNUC_MINOR__ >= 1)) +#define INLINE __inline__ __attribute__((always_inline)) +#else +#define INLINE __inline__ +#endif + +#elif (defined(_MSC_VER) || defined(__BORLANDC__) || defined(__WATCOMC__)) +#define INLINE __inline +#else +#define INLINE +#endif +#endif + +#if (!defined(__cplusplus)) && (!defined(inline)) +#define inline INLINE +#endif + +//===================================================================== +// QUEUE DEFINITION +//===================================================================== +#ifndef __IQUEUE_DEF__ +#define __IQUEUE_DEF__ + +struct IQUEUEHEAD +{ + struct IQUEUEHEAD *next, *prev; +}; + +typedef struct IQUEUEHEAD iqueue_head; + +//--------------------------------------------------------------------- +// queue init +//--------------------------------------------------------------------- +#define IQUEUE_HEAD_INIT(name) {&(name), &(name)} +#define IQUEUE_HEAD(name) \ + struct IQUEUEHEAD name = IQUEUE_HEAD_INIT(name) + +#define IQUEUE_INIT(ptr) ( \ + (ptr)->next = (ptr), (ptr)->prev = (ptr)) + +#define IOFFSETOF(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) + +#define ICONTAINEROF(ptr, type, member) ( \ + (type *)(((char *)((type *)ptr)) - IOFFSETOF(type, member))) + +#define IQUEUE_ENTRY(ptr, type, member) ICONTAINEROF(ptr, type, member) + +//--------------------------------------------------------------------- +// queue operation +//--------------------------------------------------------------------- +#define IQUEUE_ADD(node, head) ( \ + (node)->prev = (head), (node)->next = (head)->next, \ + (head)->next->prev = (node), (head)->next = (node)) + +#define IQUEUE_ADD_TAIL(node, head) ( \ + (node)->prev = (head)->prev, (node)->next = (head), \ + (head)->prev->next = (node), (head)->prev = (node)) + +#define IQUEUE_DEL_BETWEEN(p, n) ((n)->prev = (p), (p)->next = (n)) + +#define IQUEUE_DEL(entry) ( \ + (entry)->next->prev = (entry)->prev, \ + (entry)->prev->next = (entry)->next, \ + (entry)->next = 0, (entry)->prev = 0) + +#define IQUEUE_DEL_INIT(entry) \ + do \ + { \ + IQUEUE_DEL(entry); \ + IQUEUE_INIT(entry); \ + } while (0) + +#define IQUEUE_IS_EMPTY(entry) ((entry) == (entry)->next) + +#define iqueue_init IQUEUE_INIT +#define iqueue_entry IQUEUE_ENTRY +#define iqueue_add IQUEUE_ADD +#define iqueue_add_tail IQUEUE_ADD_TAIL +#define iqueue_del IQUEUE_DEL +#define iqueue_del_init IQUEUE_DEL_INIT +#define iqueue_is_empty IQUEUE_IS_EMPTY + +#define IQUEUE_FOREACH(iterator, head, TYPE, MEMBER) \ + for ((iterator) = iqueue_entry((head)->next, TYPE, MEMBER); \ + &((iterator)->MEMBER) != (head); \ + (iterator) = iqueue_entry((iterator)->MEMBER.next, TYPE, MEMBER)) + +#define iqueue_foreach(iterator, head, TYPE, MEMBER) \ + IQUEUE_FOREACH(iterator, head, TYPE, MEMBER) + +#define iqueue_foreach_entry(pos, head) \ + for ((pos) = (head)->next; (pos) != (head); (pos) = (pos)->next) + +#define __iqueue_splice(list, head) \ + do \ + { \ + iqueue_head *first = (list)->next, *last = (list)->prev; \ + iqueue_head *at = (head)->next; \ + (first)->prev = (head), (head)->next = (first); \ + (last)->next = (at), (at)->prev = (last); \ + } while (0) + +#define iqueue_splice(list, head) \ + do \ + { \ + if (!iqueue_is_empty(list)) \ + __iqueue_splice(list, head); \ + } while (0) + +#define iqueue_splice_init(list, head) \ + do \ + { \ + iqueue_splice(list, head); \ + iqueue_init(list); \ + } while (0) + +#ifdef _MSC_VER +#pragma warning(disable : 4311) +#pragma warning(disable : 4312) +#pragma warning(disable : 4996) +#endif + +#endif + +//--------------------------------------------------------------------- +// BYTE ORDER & ALIGNMENT +//--------------------------------------------------------------------- +#ifndef IWORDS_BIG_ENDIAN +#ifdef _BIG_ENDIAN_ +#if _BIG_ENDIAN_ +#define IWORDS_BIG_ENDIAN 1 +#endif +#endif +#ifndef IWORDS_BIG_ENDIAN +#if defined(__hppa__) || \ + defined(__m68k__) || defined(mc68000) || defined(_M_M68K) || \ + (defined(__MIPS__) && defined(__MIPSEB__)) || \ + defined(__ppc__) || defined(__POWERPC__) || defined(_M_PPC) || \ + defined(__sparc__) || defined(__powerpc__) || \ + defined(__mc68000__) || defined(__s390x__) || defined(__s390__) +#define IWORDS_BIG_ENDIAN 1 +#endif +#endif +#ifndef IWORDS_BIG_ENDIAN +#define IWORDS_BIG_ENDIAN 0 +#endif +#endif + +#ifndef IWORDS_MUST_ALIGN +#if defined(__i386__) || defined(__i386) || defined(_i386_) +#define IWORDS_MUST_ALIGN 0 +#elif defined(_M_IX86) || defined(_X86_) || defined(__x86_64__) +#define IWORDS_MUST_ALIGN 0 +#elif defined(__amd64) || defined(__amd64__) +#define IWORDS_MUST_ALIGN 0 +#else +#define IWORDS_MUST_ALIGN 1 +#endif +#endif + +//===================================================================== +// SEGMENT +//===================================================================== +struct IKCPSEG +{ + struct IQUEUEHEAD node; + IUINT32 conv; + IUINT32 cmd; + IUINT32 frg; + IUINT32 wnd; + IUINT32 ts; + IUINT32 sn; + IUINT32 una; + IUINT32 len; + IUINT32 resendts; + IUINT32 rto; + IUINT32 fastack; + IUINT32 xmit; + char data[1]; +}; + +//--------------------------------------------------------------------- +// IKCPCB +//--------------------------------------------------------------------- +struct IKCPCB +{ + IUINT32 conv, mtu, mss, state; + IUINT32 snd_una, snd_nxt, rcv_nxt; + IUINT32 ts_recent, ts_lastack, ssthresh; + IINT32 rx_rttval, rx_srtt, rx_rto, rx_minrto; + IUINT32 snd_wnd, rcv_wnd, rmt_wnd, cwnd, probe; + IUINT32 current, interval, ts_flush, xmit; + IUINT32 nrcv_buf, nsnd_buf; + IUINT32 nrcv_que, nsnd_que; + IUINT32 nodelay, updated; + IUINT32 ts_probe, probe_wait; + IUINT32 dead_link, incr; + struct IQUEUEHEAD snd_queue; + struct IQUEUEHEAD rcv_queue; + struct IQUEUEHEAD snd_buf; + struct IQUEUEHEAD rcv_buf; + IUINT32 *acklist; + IUINT32 ackcount; + IUINT32 ackblock; + void *user; + char *buffer; + int fastresend; + int fastlimit; + int nocwnd, stream; + int logmask; + int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user); + void (*writelog)(const char *log, struct IKCPCB *kcp, void *user); +}; + +typedef struct IKCPCB ikcpcb; + +#define IKCP_LOG_OUTPUT 1 +#define IKCP_LOG_INPUT 2 +#define IKCP_LOG_SEND 4 +#define IKCP_LOG_RECV 8 +#define IKCP_LOG_IN_DATA 16 +#define IKCP_LOG_IN_ACK 32 +#define IKCP_LOG_IN_PROBE 64 +#define IKCP_LOG_IN_WINS 128 +#define IKCP_LOG_OUT_DATA 256 +#define IKCP_LOG_OUT_ACK 512 +#define IKCP_LOG_OUT_PROBE 1024 +#define IKCP_LOG_OUT_WINS 2048 + +#ifdef __cplusplus +extern "C" +{ +#endif + + //--------------------------------------------------------------------- + // interface + //--------------------------------------------------------------------- + + // create a new kcp control object, 'conv' must equal in two endpoint + // from the same connection. 'user' will be passed to the output callback + // output callback can be setup like this: 'kcp->output = my_udp_output' + ikcpcb *ikcp_create(IUINT32 conv, void *user); + + // release kcp control object + void ikcp_release(ikcpcb *kcp); + + // set output callback, which will be invoked by kcp + void ikcp_setoutput(ikcpcb *kcp, int (*output)(const char *buf, int len, + ikcpcb *kcp, void *user)); + + // user/upper level recv: returns size, returns below zero for EAGAIN + int ikcp_recv(ikcpcb *kcp, char *buffer, int len); + + // user/upper level send, returns below zero for error + int ikcp_send(ikcpcb *kcp, const char *buffer, int len); + + // update state (call it repeatedly, every 10ms-100ms), or you can ask + // ikcp_check when to call it again (without ikcp_input/_send calling). + // 'current' - current timestamp in millisec. + void ikcp_update(ikcpcb *kcp, IUINT32 current); + + // Determine when should you invoke ikcp_update: + // returns when you should invoke ikcp_update in millisec, if there + // is no ikcp_input/_send calling. you can call ikcp_update in that + // time, instead of call update repeatly. + // Important to reduce unnacessary ikcp_update invoking. use it to + // schedule ikcp_update (eg. implementing an epoll-like mechanism, + // or optimize ikcp_update when handling massive kcp connections) + IUINT32 ikcp_check(const ikcpcb *kcp, IUINT32 current); + + // when you received a low level packet (eg. UDP packet), call it + int ikcp_input(ikcpcb *kcp, const char *data, long size); + + // flush pending data + void ikcp_flush(ikcpcb *kcp); + + // check the size of next message in the recv queue + int ikcp_peeksize(const ikcpcb *kcp); + + // change MTU size, default is 1400 + int ikcp_setmtu(ikcpcb *kcp, int mtu); + + // set maximum window size: sndwnd=32, rcvwnd=32 by default + int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd); + + // get how many packet is waiting to be sent + int ikcp_waitsnd(const ikcpcb *kcp); + + // fastest: ikcp_nodelay(kcp, 1, 20, 2, 1) + // nodelay: 0:disable(default), 1:enable + // interval: internal update timer interval in millisec, default is 100ms + // resend: 0:disable fast resend(default), 1:enable fast resend + // nc: 0:normal congestion control(default), 1:disable congestion control + int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc); + + void ikcp_log(ikcpcb *kcp, int mask, const char *fmt, ...); + + // setup allocator + void ikcp_allocator(void *(*new_malloc)(size_t), void (*new_free)(void *)); + + // read conv + IUINT32 ikcp_getconv(const void *ptr); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/change_to_c.md b/change_to_c.md new file mode 100644 index 0000000..675c8a4 --- /dev/null +++ b/change_to_c.md @@ -0,0 +1,465 @@ +OmniSocketGo -> OmniSocketC 转换计划 + + Context + + 将现有的 Go 语言实现的 UDP/KCP 传输层项目 (OmniSocketGo) 转换为纯 C 语言项目,运行在 Linux 系统上。 + + 原项目架构:A(Jetson) <-> C(relay cloud) <-> D(hub cloud) <-> B(host) + - B <-> D:KCP 链路 + - D <-> C:UDP relay 转发 + - C <-> A:KCP 链路(A 通过 relay C 连接到 hub D) + - 最终目的:B 和 A 之间双向传输数据 + + 转换要求: + - 只保留 UDP 和 KCP,不需要 TCP + - 不需要写测试 + - 完整实现协议层、传输层、日志事件系统 + - Linux only + + 项目位置 + + OmniSocketGo/c/ — 作为当前 Go 项目的子目录 + + 项目结构 + + c/ + ├── Makefile + ├── README.md + ├── include/ + │ ├── protocol.h # 协议消息定义 + 编解码 + │ ├── transport_kcp.h # KCP 连接封装 + │ ├── transport_udp.h # UDP 连接封装(含 Linux timestamping) + │ ├── linux_timestamping.h # Linux SO_TIMESTAMPING 底层实现 + │ ├── kcp_packet_debug.h # KCP packet-level kernel timestamp debug logger + │ ├── kcp_session_stats.h # KCP session stats (RTO/SRTT) logger + │ ├── tx_timestamp_debug.h # TX errqueue timestamp debug logger + │ ├── server_kcp_hub.h # KCP Hub (D 节点) + │ ├── server_udp_relay.h # UDP Relay (C 节点) + │ ├── peer_kcp_client.h # KCP Peer Client (A/B 节点) + │ ├── latencylog.h # 延迟日志事件系统 + │ ├── interactive.h # 交互式命令行 + │ └── cJSON.h # JSON 库 (第三方轻量级) + ├── src/ + │ ├── protocol.c + │ ├── transport_kcp.c + │ ├── transport_udp.c + │ ├── linux_timestamping.c + │ ├── kcp_packet_debug.c + │ ├── kcp_session_stats.c + │ ├── tx_timestamp_debug.c + │ ├── server_kcp_hub.c + │ ├── server_udp_relay.c + │ ├── peer_kcp_client.c + │ ├── latencylog.c + │ ├── interactive.c + │ └── cJSON.c + ├── cmd/ + │ ├── kcpserver.c # 主程序: KCP Hub 或 UDP Relay + │ ├── kcppeer.c # 主程序: KCP Peer (A/B) + │ └── kcpping.c # 主程序: KCP Ping 工具 + └── third_party/ + └── kcp/ + ├── ikcp.h # KCP 协议核心实现 (github.com/skywind3000/kcp) + └── ikcp.c + + 依赖说明 + + - KCP: 使用 skywind3000/kcp 的原始 C 实现 (ikcp.h/ikcp.c),替代 Go 的 xtaci/kcp-go/v5 + - JSON: 使用 cJSON (DaveGamble/cJSON) 替代 Go 的 encoding/json + - 线程: 使用 pthread 替代 Go goroutine + - 同步: 使用 pthread_mutex/pthread_rwlock 替代 Go sync.Mutex/sync.RWMutex + + 模块实现计划 + + 1. 第三方库集成 + + - 下载 ikcp.h/ikcp.c (skywind3000/kcp) + - 下载 cJSON.h/cJSON.c (DaveGamble/cJSON) + + 2. protocol.h / protocol.c + + 对应 Go: cmd/internal/protocol/message.go + codec.go + + // 消息类型 + typedef enum { + MSG_TYPE_TEXT = 0, + MSG_TYPE_FILE = 1, + MSG_TYPE_REGISTER = 2, + MSG_TYPE_ERROR = 3, + } message_type_t; + + // 消息结构 + typedef struct { + message_type_t type; + uint64_t id; + char from[64]; + char to[64]; + char file_name[256]; + uint8_t *body; + int body_len; + } message_t; + + #define MAX_FRAME_SIZE (8 * 1024 * 1024) + #define SERVER_PEER_ID "server" + + 核心函数: + - int protocol_encode_message(const message_t *msg, uint8_t **out, int *out_len) — 编码消息为 [4B headerLen][header JSON][body] + - int protocol_decode_message(const uint8_t *data, int data_len, message_t *msg) — 解码 + - int protocol_write_frame(int fd, const uint8_t *payload, int payload_len) — 写带长度前缀的帧 (用于 KCP stream) + - int protocol_read_frame(int fd, uint8_t **payload, int *payload_len) — 读帧 + - int protocol_write_message(int fd, const message_t *msg) — 完整编码+写帧 + - int protocol_read_message(int fd, message_t *msg) — 读帧+解码 + - int protocol_validate_message(const message_t *msg) — 校验 + - void message_free(message_t *msg) — 释放 body 内存 + + 注意: KCP session 在 stream 模式下行为类似 TCP,需要 [4B frameLen] 前缀来分帧。 + + 3. latencylog.h / latencylog.c + + 对应 Go: cmd/internal/latencylog/logger.go + + // 事件名常量 + #define EVENT_A_APP_PREP_BEGIN "A_APP_PREP_BEGIN" + #define EVENT_SEND_HANDOFF_BEGIN "send_handoff_begin" + #define EVENT_SEND_HANDOFF_END "send_handoff_end" + #define EVENT_B_APP_RECV "B_APP_RECV" + #define EVENT_B_PERSIST_BEGIN "B_PERSIST_BEGIN" + #define EVENT_B_PERSIST_END "B_PERSIST_END" + // ... 其他事件 + + typedef struct { + int64_t ts_unix_nano; + char node_role[16]; + char node_id[64]; + char event[32]; + message_type_t message_type; + uint64_t message_id; + char from[64]; + char to[64]; + char file_name[256]; + int body_size; + } latency_event_t; + + typedef struct latency_logger latency_logger_t; + + 核心函数: + - latency_logger_t *latencylog_new_jsonl(const char *path) — 创建 JSONL 文件日志器 + - void latencylog_log_event(latency_logger_t *logger, const latency_event_t *event) — 写事件 + - void latencylog_log_message_event(latency_logger_t *logger, const char *node_role, const char *node_id, const char *event_name, const message_t + *msg) — 为业务消息记事件 + - void latencylog_close(latency_logger_t *logger) — 关闭 + - int latencylog_is_business_message(const message_t *msg) — 判断是否业务消息 + + 4. transport_kcp.h / transport_kcp.c + + 对应 Go: cmd/internal/transport/kcp.go + kcp_packet_conn.go + + KCP 连接封装,底层用 raw ikcp + UDP socket: + + typedef struct kcp_conn { + ikcpcb *kcp; + int udp_fd; + struct sockaddr_in remote_addr; + pthread_mutex_t write_mu; + pthread_t recv_thread; // 底层 UDP -> ikcp_input 的线程 + latency_logger_t *logger; + char node_role[16]; + char node_id[64]; + int closed; + } kcp_conn_t; + + 核心函数: + - kcp_conn_t *kcp_conn_dial(const char *server_addr, const char *bind_ip, const char *bind_device) — 客户端拨号 + - kcp_conn_t *kcp_conn_accept(int udp_fd, struct sockaddr_in *remote, uint32_t conv) — 服务端接受 + - int kcp_conn_send(kcp_conn_t *conn, const message_t *msg) — 发送消息 + - int kcp_conn_receive(kcp_conn_t *conn, message_t *msg) — 接收消息 + - void kcp_conn_close(kcp_conn_t *conn) — 关闭 + + KCP 配置参数(与 Go 版一致): + #define KCP_NODELAY 1 + #define KCP_INTERVAL 10 + #define KCP_RESEND 2 + #define KCP_NC 1 + #define KCP_WND_SIZE 256 + #define KCP_MTU 1400 + + KCP 底层架构说明: + Go 版使用 kcp-go 库,该库内部维护了一个 Listener 来多路复用一个 UDP socket 上的多个 KCP session(通过 conv ID 区分)。在 C 中需要自行实现: + - 服务端:一个 UDP socket 监听,一个接收线程读取所有 UDP 包,根据 conv ID 分发到对应的 ikcpcb + - 客户端:一个 UDP socket,一个 ikcpcb,一个后台线程负责 UDP recv -> ikcp_input + + 5. transport_udp.h / transport_udp.c + + 对应 Go: cmd/internal/transport/udp.go + udp_linux.go + + typedef struct udp_conn { + int fd; + struct sockaddr_in peer_addr; + syscall_rawconn_t raw; // syscall.RawConn 等价 + int linux_timestamping_enabled; + latency_logger_t *logger; + tx_timestamp_debug_logger_t *tx_debug_logger; + uint32_t tx_packet_seq; + // pending TX records for errqueue correlation + struct udp_tx_pending *pending_tx; + char node_role[16]; + char node_id[64]; + pthread_mutex_t write_mu; + } udp_conn_t; + + 完整实现 Linux SO_TIMESTAMPING: + - TX: SOF_TIMESTAMPING_TX_SCHED + SOF_TIMESTAMPING_TX_SOFTWARE + OPT_ID + - RX: SOF_TIMESTAMPING_RX_SOFTWARE + - errqueue 采集: recvmsg(MSG_ERRQUEUE) 读取 SCM_TIMESTAMPING 控制消息 + - TX timestamp debug logger: 记录 send_chunk / errqueue_event 到 JSONL + - 对应 Go 文件: udp_linux.go, tx_timestamp_debug.go + + 同时为 KCP packet conn 实现类似的 timestamping: + - 对应 Go 文件: kcp_packet_conn_linux.go, kcp_packet_debug.go + - KCP 底层 UDP 包的 TX/RX kernel timestamp 记录 + + KCP session stats 完整实现: + - session-level: conv, RTO, SRTT, SRTTVar 周期采样 + - 对应 Go 文件: kcp_session_stats.go + + 6. server_kcp_hub.h / server_kcp_hub.c + + 对应 Go: cmd/internal/server/kcp_hub.go + + typedef struct { + pthread_rwlock_t lock; + // peer_id -> kcp_conn_t* 的哈希表 + struct peer_entry *peers; // 简单链表或哈希表 + int peer_count; + latency_logger_t *logger; + // relay 相关 + int relay_udp_fd; + struct sockaddr_in relay_peer_addr; + int relay_peer_known; + } kcp_hub_t; + + 核心函数: + - kcp_hub_t *kcp_hub_new(latency_logger_t *logger) — 创建 hub + - int kcp_hub_serve_session(kcp_hub_t *hub, kcp_conn_t *conn) — 处理新会话(注册 + 转发循环) + - void kcp_hub_set_relay(kcp_hub_t *hub, int udp_fd, struct sockaddr_in *peer_addr) — 配置 relay + - int kcp_hub_serve_relay(kcp_hub_t *hub) — relay 接收循环 + - void kcp_hub_free(kcp_hub_t *hub) — 释放 + + 服务端 KCP listener 实现: + - 主 UDP socket 监听 + - 收到新 conv ID 时创建新 ikcpcb + - 用 pthread 为每个 session 创建处理线程 + + 7. server_udp_relay.h / server_udp_relay.c + + 对应 Go: cmd/internal/server/udp_relay.go + + typedef struct { + int downstream_fd; // 监听端 + int upstream_fd; // 连接到 hub D 的 UDP + struct sockaddr_in upstream_addr; + struct sockaddr_in client_addr; + int client_known; + pthread_mutex_t lock; + } udp_relay_t; + + 核心函数: + - udp_relay_t *udp_relay_new(int listen_fd, struct sockaddr_in *upstream_addr) — 创建 + - int udp_relay_serve(udp_relay_t *relay) — 双向转发循环(两个线程) + - void udp_relay_close(udp_relay_t *relay) — 关闭 + + 8. peer_kcp_client.h / peer_kcp_client.c + + 对应 Go: cmd/internal/peer/kcp_client.go + persist.go + + typedef struct { + char id[64]; + kcp_conn_t *conn; + latency_logger_t *logger; + uint64_t next_msg_id; // atomic + pthread_mutex_t id_mu; + } kcp_client_t; + + 核心函数: + - kcp_client_t *kcp_client_dial(const char *server_addr, const char *peer_id, ...) — 连接并注册 + - int kcp_client_send_text(kcp_client_t *c, const char *to, const char *text) — 发文本 + - int kcp_client_send_file(kcp_client_t *c, const char *to, const char *path) — 发文件 + - int kcp_client_receive(kcp_client_t *c, message_t *msg) — 接收 + - int kcp_client_persist_message(kcp_client_t *c, const message_t *msg, const char *inbox_dir) — 持久化 + - void kcp_client_close(kcp_client_t *c) — 关闭 + + 9. interactive.h / interactive.c + + 对应 Go: cmd/kcppeer/interactive.go + + 交互式命令行 REPL: + - help / text / file / quit + - int run_interactive_shell(kcp_client_t *client) — 运行交互循环 + + 10. cmd/kcpserver.c + + 对应 Go: cmd/kcpserver/main.go + + 用法: + kcpserver -listen 0.0.0.0:10909 # hub 模式 + kcpserver -mode relay -listen 0.0.0.0:10909 -relay-remote 172.21.32.15:10909 # relay 模式 + + - 解析命令行参数 (getopt) + - hub 模式:创建 KCP listener -> 接受连接 -> kcp_hub_serve_session + - relay 模式:创建 UDP relay -> udp_relay_serve + + 11. cmd/kcppeer.c + + 对应 Go: cmd/kcppeer/main.go + + 用法: + kcppeer -id peer-a -server 172.21.32.15:10909 -relay-via 106.55.173.235:10909 -inbox-dir inbox/a + kcppeer -id peer-b -server 81.70.156.140:10909 -inbox-dir inbox/b + + - 连接到 KCP server + - 启动接收线程 + - 运行交互式 shell 或单次发送 + + 12. cmd/kcpping.c + + 对应 Go: cmd/kcpping/main.go + platform_linux.go + + KCP ping 工具: + - ping 模式: 发 JSON payload, 计算 RTT + - echo 模式: 回弹文本消息 + - 统计: min/avg/max/p50/p95/p99/stddev + + KCP session 多路复用实现(核心难点) + + Go 版的 kcp-go 库在一个 UDP socket 上透明地多路复用多个 KCP session。C 版需要手动实现: + + typedef struct kcp_listener { + int udp_fd; + pthread_t recv_thread; + pthread_mutex_t sessions_lock; + // conv -> kcp_session 的哈希表 + struct kcp_session_entry *sessions; + // 新会话通知队列 + kcp_conn_t **accept_queue; + int accept_queue_head, accept_queue_tail, accept_queue_cap; + pthread_mutex_t accept_lock; + pthread_cond_t accept_cond; + } kcp_listener_t; + + - kcp_listener_t *kcp_listen(const char *addr, const char *bind_device) — 创建 listener + - kcp_conn_t *kcp_accept(kcp_listener_t *listener) — 阻塞等待新会话 + - 内部 recv_thread 循环读 UDP 包,解析前 4 字节 conv ID,分发到对应 ikcpcb + - 未知 conv ID 时创建新 session 并放入 accept_queue + + 编译 + + CC = gcc + CFLAGS = -Wall -Wextra -O2 -pthread -D_GNU_SOURCE + LDFLAGS = -lpthread + + SRCS = src/protocol.c src/transport_kcp.c src/transport_udp.c \ + src/server_kcp_hub.c src/server_udp_relay.c \ + src/peer_kcp_client.c src/latencylog.c src/interactive.c \ + src/cJSON.c third_party/kcp/ikcp.c + + all: kcpserver kcppeer kcpping + + kcpserver: cmd/kcpserver.c $(SRCS) + $(CC) $(CFLAGS) -Iinclude -Ithird_party/kcp -o $@ $^ $(LDFLAGS + + kcppeer: cmd/kcppeer.c $(SRCS) + $(CC) $(CFLAGS) -Iinclude -Ithird_party/kcp -o $@ $^ $(LDFLAGS + + kcpping: cmd/kcpping.c $(SRCS) + $(CC) $(CFLAGS) -Iinclude -Ithird_party/kcp -o $@ $^ $(LDFLAGS + + 验证方法 + + 1. 编译: make all 无错误无警告 + 2. 单机测试: + - 启动 hub: ./kcpserver -listen 0.0.0.0:10909 + - 启动 peer-a: ./kcppeer -id peer-a -server 127.0.0.1:10909 -inbox-dir inbox/a + - 启动 peer-b: ./kcppeer -id peer-b -server 127.0.0.1:10909 -inbox-dir inbox/b + - peer-b shell 中: text peer-a hello + - 验证 peer-a 收到消息并落盘到 inbox/a/ + 3. 跨机器 relay 测试: + - D 机器: ./kcpserver -listen 0.0.0.0:10909 + - C 机器: ./kcpserver -mode relay -listen 0.0.0.0:10909 -relay-remote :10909 + - A 机器: ./kcppeer -id peer-a -server :10909 -relay-via :10909 -inbox-dir inbox/a + - B 机器: ./kcppeer -id peer-b -server :10909 -inbox-dir inbox/b + 4. kcpping 测试: + - echo 端: ./kcpping -id peer-a -server :10909 -echo + - ping 端: ./kcpping -id peer-b -server :10909 -to peer-a -count 20 -interval 100 + + 实现顺序 + + 1. 集成第三方库 (ikcp, cJSON) + 2. protocol 模块 (消息编解码) + 3. latencylog 模块 (日志事件) + 4. transport_kcp 模块 (KCP 连接 + listener 多路复用) + 5. transport_udp 模块 (UDP 连接,简化 timestamping) + 6. server_udp_relay 模块 (C 节点 relay) + 7. server_kcp_hub 模块 (D 节点 hub) + 8. peer_kcp_client 模块 (A/B 节点 peer + persist) + 9. interactive 模块 (交互 shell) + 10. cmd/kcpserver.c 主程序 + 11. cmd/kcppeer.c 主程序 + 12. cmd/kcpping.c 主程序 + 13. Makefile + README + 14. 编译测试 + + 简化决策 + + - 不实现 TCP 传输: 去除 transport/tcp.go, server/hub.go(TCP版), peer/client.go(TCP版) 等 TCP 相关代码 + - 不写测试: 去除所有 _test.go 对应的测试代码 + - 完整实现 Linux timestamping: 完整移植 SO_TIMESTAMPING 的 TX/RX timestamp 采集,包括 errqueue TX sched/software timestamp 和 RX software + timestamp,以及对应的 debug logger (KCPPacketDebugLogger, TXTimestampDebugLogger) + - 完整实现 KCP session stats: 包括 session-level RTO/SRTT 采样和 JSONL 记录 + - 不实现 latency summary/chart: 不实现 latencysummary 工具和 HTML chart 生成(这是离线分析工具,不属于核心传输功能) + - peer 哈希表: 使用简单链表实现,hub 连接数不多时性能足够 + + +# OmniSocketGo -> OmniSocketC 全量 UDP/KCP 迁移计划 + +## Summary +- 在仓库新增 `c/` 子项目,作为 Linux-only、C11、`make` 驱动的独立实现;现有 Go 项目保留不动,作为行为对照。 +- 迁移范围按“全量 Go 对齐,但去掉 TCP 和离线 summary/chart”执行:保留 UDP/KCP 协议、纯 UDP 程序族、KCP 程序族、运行时 JSONL 日志、Linux timestamping、KCP packet debug、KCP session stats、以及 KCP hub-to-hub 内部 relay 能力。 +- 你当前草案需要修正的关键点有 5 个:`protocol_*frame(int fd, ...)` 不适合 KCP;KCP 必须补齐 `ikcp_update/check` 调度与 conv 多路复用;纯 UDP 程序族不能省略;`latencysummary`/HTML chart 本次不迁移;Makefile 需要修正链接目标并统一输出到 `c/bin/`。 + +## Public Interfaces +- 新增二进制:`kcpserver`、`kcppeer`、`kcpping`、`udpserver`、`udppeer`、`udpping`、`udprelay`。 +- `kcpserver` 保留当前 Go 旗标语义:`-mode=hub|relay`、`-listen`、`-bind-device`、`-relay-remote`、deprecated relay aliases、`-latency-log`、`-kcp-ts-debug-log`、`-kcp-session-stats-log`、`-kcp-session-stats-interval`。 +- `kcppeer` 保留当前 Go 旗标语义:`-id`、`-server`、`-relay-via`、`-to`、`-text`、`-file`、`-bind-ip`、`-bind-device`、`-inbox-dir`、`-interactive`、`-latency-log`、`-kcp-ts-debug-log`、`-kcp-session-stats-log`、`-kcp-session-stats-interval`。 +- `kcpping`、`udpserver`、`udppeer`、`udpping`、`udprelay` 的参数与输出行为对齐当前 Go 入口;`udpserver` 默认不开 Linux timestamping,只有设置 `-tx-ts-debug-log` 时才启用。 +- 协议层改为内存接口,不再设计 fd 风格 API:`message_t`、datagram 编解码、stream frame 编解码、增量 frame feed。 +- 运行时日志层保留当前 JSON 字段和事件名;server/hub 继续作为 black-box relay,不新增端到端业务事件。 +- 内部网络 API 包括:`udp_conn_t`、`kcp_conn_t`、`kcp_listener_t`、`udp_hub_t`、`kcp_hub_t`、`udp_relay_t`、`udp_client_t`、`kcp_client_t`;KCP hub-to-hub relay 只做库级能力,不新增额外 CLI。 + +## Implementation Changes +- 目录固定为 `c/include`、`c/src`、`c/cmd`、`c/third_party/{ikcp,cjson}`、`c/bin`、`c/README.md`、`c/Makefile`。 +- 第三方依赖直接 vendoring 到仓库:`ikcp` 用于 KCP 核心,`cJSON` 同时用于协议头、ping payload、运行时日志。 +- 协议规则完全保留:`text/file/register/error`、`ServerPeerID`、`8 MiB` 限制、UTF-8 校验、`file_name` 约束、`register/error` 来源与目标约束。 +- 线上 wire format 完全保留:UDP datagram 为 `[4B headerLen][header JSON][body]`;KCP stream 为 `[4B frameLen][4B headerLen][header JSON][body]`。 +- inbox 持久化完全保留:文本追加写 `messages.log` JSONL;文件落盘为 `--`。 +- UDP 传输层实现 connected/unconnected 两种发送模式,保留 register/forward 消息收发、Linux SO_TIMESTAMPING、TX errqueue 关联、JSONL debug 记录。 +- KCP 客户端连接采用“一连接一 UDP socket + 一 `ikcpcb` + 一接收线程 + 一 update 线程 + 一阻塞接收缓冲区/条件变量”模型。 +- KCP 服务端监听采用“单 listener UDP socket + 单 listener RX 线程 + conv->session 表 + accept 队列”模型;每个 session 拥有自己的 `ikcpcb`、update 线程、接收缓冲区和关闭状态,发送通过 listener 共享 socket 和写锁完成。 +- `kcpserver` 的 relay 模式保持为原始 UDP 端口转发,不解码协议;`udprelay` 同样保持透明字节转发。 +- 纯 UDP hub、KCP hub、双 peer、双 ping 工具、两套 interactive shell 全部对齐现有 Go 行为。 +- KCP hub 保留“先本地投递,再尝试 relay”的策略;未知目标、重复注册、已注册 peer 再发 `register/error`、过大 relay 消息等错误路径全部保留。 +- Linux 观测能力完整迁移:业务事件 JSONL、UDP TX debug、KCP packet debug、KCP session/process stats;不迁移 `latencysummary` 与 HTML chart。 + +## Acceptance +- 在 Linux 上执行 `make` 能无缺失符号地构建 7 个二进制,并输出到 `c/bin/`。 +- 纯 UDP 冒烟通过:`udpserver` + 两个 `udppeer` 可双向收发文本和文件,`udpping` 的 echo/ping 正常。 +- 单 hub KCP 冒烟通过:`kcpserver` + 两个 `kcppeer` 可双向收发文本和文件,`kcpping` 的 echo/ping 正常。 +- README 目标拓扑通过:D 跑 `kcpserver -mode=hub`,C 跑 `kcpserver -mode=relay`,A 用 `-relay-via C` 连 D,B 直连 D,A/B 双向传输正常。 +- 全量 Go 对齐场景通过:两个 KCP hub 通过内部 raw UDP relay API 互通,跨 hub 文本、文件、错误回送行为与当前 Go 一致。 +- 负路径通过:重复注册被拒、未注册 UDP sender 被拒、未知目标返回 `error`、已注册 peer 发送 `register/error` 被拒、oversize relayed message 在实际 `WriteTo` 前被拒、`bind-ip`/`bind-device` 非法值在启动时失败。 +- 打开任一日志旗标后,生成的 JSONL 记录字段名、事件名、时间戳语义与现有运行时日志一致,并在 Linux 支持的情况下出现非零 kernel timestamps。 + +## Assumptions +- 默认编译器为 `gcc`/`clang`,编译参数基线为 `-std=c11 -Wall -Wextra -O2 -pthread -D_GNU_SOURCE`。 +- 本次不迁移任何 Go 测试文件,也不为 C 版编写自动化测试;验证仅靠 Linux 构建和手工场景回归。 +- 本次不迁移 TCP 入口,也不迁移 `latencysummary`/HTML chart。 +- hub-to-hub relay 在 C 版中实现为内部库能力,保持与当前 Go 仓库一致的范围,不额外扩展新的公共命令。