Implement HTTP client in consistent style with server.c
Refactored HTTP client implementation to follow the same coding patterns and style as server.c: - Added static helper functions (http_client_conn_init, http_client_conn_free, check_response_buffer, request_builder_to_conn) - Implemented buffer limit setters (http_client_set_input_limit, http_client_set_output_limit) - Fully implemented request builder API (url, header, body, send) - Implemented event processing with state machine approach - Added connection reuse support via http_free_response - Used consistent naming, formatting, and error handling patterns - Renamed builder_to_conn to request_builder_to_conn to avoid symbol conflicts with server.c during amalgamation The implementation follows the same architecture as the server: - Connection state machine for managing request/response lifecycle - ByteQueue for input/output buffering - Ready queue for completed responses - Generation counters for builder validation - Socket manager integration for async I/O
This commit is contained in:
+338
-40
@@ -1,19 +1,31 @@
|
||||
|
||||
void http_client_conn_init(HTTP_ClientConn *conn)
|
||||
static void http_client_conn_init(HTTP_ClientConn *conn,
|
||||
SocketHandle handle, uint32_t input_buffer_limit,
|
||||
uint32_t output_buffer_limit)
|
||||
{
|
||||
// TODO
|
||||
conn->state = HTTP_CLIENT_CONN_WAIT_LINE;
|
||||
conn->handle = handle;
|
||||
conn->gen = 0;
|
||||
byte_queue_init(&conn->input, input_buffer_limit);
|
||||
byte_queue_init(&conn->output, output_buffer_limit);
|
||||
}
|
||||
|
||||
void http_client_conn_free(HTTP_ClientConn *conn)
|
||||
static void http_client_conn_free(HTTP_ClientConn *conn)
|
||||
{
|
||||
// TODO
|
||||
byte_queue_free(&conn->output);
|
||||
byte_queue_free(&conn->input);
|
||||
}
|
||||
|
||||
int http_client_init(HTTP_Client *client)
|
||||
{
|
||||
client->input_buffer_limit = 1<<20;
|
||||
client->output_buffer_limit = 1<<20;
|
||||
|
||||
client->num_conns = 0;
|
||||
for (int i = 0; i < HTTP_CLIENT_CAPACITY; i++)
|
||||
for (int i = 0; i < HTTP_CLIENT_CAPACITY; i++) {
|
||||
client->conns[i].state = HTTP_CLIENT_CONN_FREE;
|
||||
client->conns[i].gen = 0;
|
||||
}
|
||||
|
||||
client->num_ready = 0;
|
||||
client->ready_head = 0;
|
||||
@@ -38,6 +50,16 @@ void http_client_free(HTTP_Client *client)
|
||||
}
|
||||
}
|
||||
|
||||
void http_client_set_input_limit(HTTP_Client *client, uint32_t limit)
|
||||
{
|
||||
client->input_buffer_limit = limit;
|
||||
}
|
||||
|
||||
void http_client_set_output_limit(HTTP_Client *client, uint32_t limit)
|
||||
{
|
||||
client->output_buffer_limit = limit;
|
||||
}
|
||||
|
||||
int http_client_wakeup(HTTP_Client *client)
|
||||
{
|
||||
if (socket_manager_wakeup(&client->sockets) < 0)
|
||||
@@ -45,35 +67,6 @@ int http_client_wakeup(HTTP_Client *client)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int http_client_get_builder(HTTP_Client *client,
|
||||
HTTP_Response *response, HTTP_RequestBuilder *builder)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
void http_request_builder_url(HTTP_RequestBuilder builder,
|
||||
HTTP_String url)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
void http_request_builder_header(HTTP_RequestBuilder builder,
|
||||
HTTP_String str)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
void http_request_builder_body(HTTP_RequestBuilder builder,
|
||||
HTTP_String str)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
int http_request_builder_send(HTTP_RequestBuilder builder)
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
int http_client_register_events(HTTP_Client *client,
|
||||
EventRegister *reg)
|
||||
{
|
||||
@@ -82,25 +75,319 @@ int http_client_register_events(HTTP_Client *client,
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get a connection pointer from a request builder.
|
||||
// If the builder is invalid, returns NULL.
|
||||
static HTTP_ClientConn*
|
||||
request_builder_to_conn(HTTP_RequestBuilder builder)
|
||||
{
|
||||
HTTP_Client *client = builder.client;
|
||||
if (client == NULL)
|
||||
return NULL;
|
||||
|
||||
if (builder.index >= HTTP_CLIENT_CAPACITY)
|
||||
return NULL;
|
||||
|
||||
HTTP_ClientConn *conn = &client->conns[builder.index];
|
||||
if (builder.gen != conn->gen)
|
||||
return NULL;
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
int http_client_get_builder(HTTP_Client *client,
|
||||
HTTP_Response *response, HTTP_RequestBuilder *builder)
|
||||
{
|
||||
HTTP_ClientConn *conn = NULL;
|
||||
|
||||
if (response != NULL && response->context != NULL) {
|
||||
// Reuse the connection from the previous response
|
||||
conn = (HTTP_ClientConn*) response->context;
|
||||
|
||||
// Mark the response as freed
|
||||
response->context = NULL;
|
||||
|
||||
// Reset the connection for a new request
|
||||
byte_queue_read_ack(&conn->input, byte_queue_read_buf(&conn->input).len);
|
||||
byte_queue_read_ack(&conn->output, byte_queue_read_buf(&conn->output).len);
|
||||
conn->state = HTTP_CLIENT_CONN_WAIT_LINE;
|
||||
|
||||
} else {
|
||||
// Find a free connection slot
|
||||
if (client->num_conns == HTTP_CLIENT_CAPACITY)
|
||||
return -1;
|
||||
|
||||
int i = 0;
|
||||
while (client->conns[i].state != HTTP_CLIENT_CONN_FREE) {
|
||||
i++;
|
||||
if (i >= HTTP_CLIENT_CAPACITY)
|
||||
return -1;
|
||||
}
|
||||
|
||||
conn = &client->conns[i];
|
||||
conn->state = HTTP_CLIENT_CONN_WAIT_LINE;
|
||||
conn->gen = 0;
|
||||
conn->handle = SOCKET_HANDLE_INVALID;
|
||||
conn->client = client;
|
||||
byte_queue_init(&conn->input, client->input_buffer_limit);
|
||||
byte_queue_init(&conn->output, client->output_buffer_limit);
|
||||
client->num_conns++;
|
||||
}
|
||||
|
||||
*builder = (HTTP_RequestBuilder) {
|
||||
client,
|
||||
conn - client->conns,
|
||||
conn->gen
|
||||
};
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void http_request_builder_url(HTTP_RequestBuilder builder,
|
||||
HTTP_String url)
|
||||
{
|
||||
HTTP_ClientConn *conn = request_builder_to_conn(builder);
|
||||
if (conn == NULL)
|
||||
return;
|
||||
|
||||
if (conn->state != HTTP_CLIENT_CONN_WAIT_LINE)
|
||||
return;
|
||||
|
||||
// Parse the URL to extract components
|
||||
HTTP_URL parsed_url;
|
||||
if (http_parse_url(url.ptr, url.len, &parsed_url) != 1)
|
||||
return;
|
||||
|
||||
// Store parsed URL for connection establishment
|
||||
conn->url = parsed_url;
|
||||
|
||||
// Determine HTTP method (default to GET)
|
||||
HTTP_String method = HTTP_STR("GET");
|
||||
|
||||
// Build request line: METHOD path HTTP/1.1\r\n
|
||||
byte_queue_write_fmt(&conn->output, "%.*s %.*s HTTP/1.1\r\n",
|
||||
method.len, method.ptr,
|
||||
parsed_url.path.len, parsed_url.path.ptr);
|
||||
|
||||
// Add Host header automatically
|
||||
byte_queue_write_fmt(&conn->output, "Host: %.*s",
|
||||
parsed_url.authority.host.text.len,
|
||||
parsed_url.authority.host.text.ptr);
|
||||
|
||||
if (parsed_url.authority.port > 0) {
|
||||
byte_queue_write_fmt(&conn->output, ":%d", parsed_url.authority.port);
|
||||
}
|
||||
byte_queue_write(&conn->output, "\r\n", 2);
|
||||
|
||||
conn->state = HTTP_CLIENT_CONN_WAIT_HEADER;
|
||||
}
|
||||
|
||||
void http_request_builder_header(HTTP_RequestBuilder builder,
|
||||
HTTP_String str)
|
||||
{
|
||||
HTTP_ClientConn *conn = request_builder_to_conn(builder);
|
||||
if (conn == NULL)
|
||||
return;
|
||||
|
||||
if (conn->state != HTTP_CLIENT_CONN_WAIT_HEADER)
|
||||
return;
|
||||
|
||||
// Validate header: must contain a colon and no control characters
|
||||
bool has_colon = false;
|
||||
for (int i = 0; i < str.len; i++) {
|
||||
char c = str.ptr[i];
|
||||
if (c == ':')
|
||||
has_colon = true;
|
||||
// Reject control characters (especially \r and \n)
|
||||
if (c < 0x20 && c != '\t')
|
||||
return;
|
||||
}
|
||||
if (!has_colon)
|
||||
return;
|
||||
|
||||
byte_queue_write(&conn->output, str.ptr, str.len);
|
||||
byte_queue_write(&conn->output, "\r\n", 2);
|
||||
}
|
||||
|
||||
void http_request_builder_body(HTTP_RequestBuilder builder,
|
||||
HTTP_String str)
|
||||
{
|
||||
HTTP_ClientConn *conn = request_builder_to_conn(builder);
|
||||
if (conn == NULL)
|
||||
return;
|
||||
|
||||
// Transition from WAIT_HEADER to WAIT_BODY if needed
|
||||
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
|
||||
// End headers section
|
||||
byte_queue_write(&conn->output, "\r\n", 2);
|
||||
conn->state = HTTP_CLIENT_CONN_WAIT_BODY;
|
||||
}
|
||||
|
||||
if (conn->state != HTTP_CLIENT_CONN_WAIT_BODY)
|
||||
return;
|
||||
|
||||
byte_queue_write(&conn->output, str.ptr, str.len);
|
||||
}
|
||||
|
||||
int http_request_builder_send(HTTP_RequestBuilder builder)
|
||||
{
|
||||
HTTP_ClientConn *conn = request_builder_to_conn(builder);
|
||||
if (conn == NULL)
|
||||
return -1;
|
||||
|
||||
// Finalize the request
|
||||
if (conn->state == HTTP_CLIENT_CONN_WAIT_HEADER) {
|
||||
// No body, just end headers
|
||||
byte_queue_write(&conn->output, "\r\n", 2);
|
||||
}
|
||||
|
||||
// Establish connection if not already connected
|
||||
if (conn->handle == SOCKET_HANDLE_INVALID) {
|
||||
|
||||
// Determine if connection should be secure
|
||||
bool secure = false;
|
||||
if (conn->url.scheme.len == 5 &&
|
||||
strncmp(conn->url.scheme.ptr, "https", 5) == 0) {
|
||||
secure = true;
|
||||
}
|
||||
|
||||
// Prepare connection target
|
||||
ConnectTarget target;
|
||||
target.port = conn->url.authority.port;
|
||||
if (target.port <= 0)
|
||||
target.port = secure ? 443 : 80;
|
||||
|
||||
// Set up target based on host type
|
||||
if (conn->url.authority.host.mode == HTTP_HOST_MODE_NAME) {
|
||||
target.type = CONNECT_TARGET_NAME;
|
||||
target.name = (String) {
|
||||
conn->url.authority.host.name.ptr,
|
||||
conn->url.authority.host.name.len
|
||||
};
|
||||
} else if (conn->url.authority.host.mode == HTTP_HOST_MODE_IPV4) {
|
||||
target.type = CONNECT_TARGET_IPV4;
|
||||
target.ipv4 = (IPv4) { conn->url.authority.host.ipv4.data };
|
||||
} else if (conn->url.authority.host.mode == HTTP_HOST_MODE_IPV6) {
|
||||
target.type = CONNECT_TARGET_IPV6;
|
||||
for (int i = 0; i < 8; i++)
|
||||
target.ipv6.data[i] = conn->url.authority.host.ipv6.data[i];
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (socket_connect(&conn->client->sockets, 1, &target, secure, conn) < 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
conn->state = HTTP_CLIENT_CONN_FLUSHING;
|
||||
conn->gen++;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Look at the input buffer to see if a complete response
|
||||
// was buffered. If it was, change the connection's status
|
||||
// to COMPLETE and push it to the ready queue.
|
||||
static void
|
||||
check_response_buffer(HTTP_Client *client, HTTP_ClientConn *conn)
|
||||
{
|
||||
assert(conn->state == HTTP_CLIENT_CONN_BUFFERING);
|
||||
|
||||
ByteView src = byte_queue_read_buf(&conn->input);
|
||||
int ret = http_parse_response(src.ptr, src.len, &conn->response);
|
||||
|
||||
if (ret < 0) {
|
||||
// Invalid response
|
||||
byte_queue_read_ack(&conn->input, 0);
|
||||
socket_close(&client->sockets, conn->handle);
|
||||
|
||||
} else if (ret == 0) {
|
||||
// Still waiting
|
||||
byte_queue_read_ack(&conn->input, 0);
|
||||
|
||||
// If the queue reached its limit and we still didn't receive
|
||||
// a complete response, abort the exchange.
|
||||
if (byte_queue_full(&conn->input))
|
||||
socket_close(&client->sockets, conn->handle);
|
||||
|
||||
} else {
|
||||
// Ready
|
||||
assert(ret == 1);
|
||||
|
||||
conn->state = HTTP_CLIENT_CONN_COMPLETE;
|
||||
conn->response.context = conn;
|
||||
|
||||
// Push to the ready queue
|
||||
assert(client->num_ready < HTTP_CLIENT_CAPACITY);
|
||||
int tail = (client->ready_head + client->num_ready) % HTTP_CLIENT_CAPACITY;
|
||||
client->ready[tail] = conn - client->conns;
|
||||
client->num_ready++;
|
||||
}
|
||||
}
|
||||
|
||||
int http_client_process_events(HTTP_Client *client,
|
||||
EventRegister *reg)
|
||||
{
|
||||
SocketEvent events[HTTP_CLIENT_CAPACITY];
|
||||
int num_events = socket_manager_translate_events(
|
||||
&client->sockets, events, reg);
|
||||
if (num_events < 0)
|
||||
return -1;
|
||||
|
||||
for (int i = 0; i < num_events; i++) {
|
||||
|
||||
HTTP_ClientConn *conn = events[i].user;
|
||||
|
||||
if (events[i].type == SOCKET_EVENT_DISCONNECT) {
|
||||
|
||||
// TODO
|
||||
if (conn != NULL) {
|
||||
http_client_conn_free(conn);
|
||||
conn->state = HTTP_CLIENT_CONN_FREE;
|
||||
client->num_conns--;
|
||||
}
|
||||
|
||||
} else if (events[i].type == SOCKET_EVENT_READY) {
|
||||
|
||||
// TODO
|
||||
}
|
||||
if (conn == NULL)
|
||||
continue;
|
||||
|
||||
// TODO
|
||||
// Store the handle if this is a new connection
|
||||
if (conn->handle == SOCKET_HANDLE_INVALID)
|
||||
conn->handle = events[i].handle;
|
||||
|
||||
if (conn->state == HTTP_CLIENT_CONN_FLUSHING) {
|
||||
|
||||
// Send request data
|
||||
int num = 0;
|
||||
ByteView src = byte_queue_read_buf(&conn->output);
|
||||
if (src.len)
|
||||
num = socket_send(&client->sockets, conn->handle, src.ptr, src.len);
|
||||
byte_queue_read_ack(&conn->output, num);
|
||||
|
||||
if (byte_queue_error(&conn->output)) {
|
||||
socket_close(&client->sockets, conn->handle);
|
||||
} else if (byte_queue_empty(&conn->output)) {
|
||||
// Request fully sent, now wait for response
|
||||
conn->state = HTTP_CLIENT_CONN_BUFFERING;
|
||||
}
|
||||
|
||||
} else if (conn->state == HTTP_CLIENT_CONN_BUFFERING) {
|
||||
|
||||
// Receive response data
|
||||
int min_recv = 1<<10;
|
||||
byte_queue_write_setmincap(&conn->input, min_recv);
|
||||
|
||||
int num = 0;
|
||||
ByteView dst = byte_queue_write_buf(&conn->input);
|
||||
if (dst.len)
|
||||
num = socket_recv(&client->sockets, conn->handle, dst.ptr, dst.len);
|
||||
byte_queue_write_ack(&conn->input, num);
|
||||
|
||||
if (byte_queue_error(&conn->input))
|
||||
socket_close(&client->sockets, conn->handle);
|
||||
else
|
||||
check_response_buffer(client, conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -112,7 +399,7 @@ bool http_client_next_response(HTTP_Client *client,
|
||||
if (client->num_ready == 0)
|
||||
return false;
|
||||
|
||||
HTTP_ClientConn *conn = &client->conns[client->ready_head];
|
||||
HTTP_ClientConn *conn = &client->conns[client->ready[client->ready_head]];
|
||||
client->ready_head = (client->ready_head + 1) % HTTP_CLIENT_CAPACITY;
|
||||
client->num_ready--;
|
||||
|
||||
@@ -123,5 +410,16 @@ bool http_client_next_response(HTTP_Client *client,
|
||||
|
||||
void http_free_response(HTTP_Response *res)
|
||||
{
|
||||
// TODO
|
||||
if (res == NULL || res->context == NULL)
|
||||
return;
|
||||
|
||||
HTTP_ClientConn *conn = (HTTP_ClientConn*) res->context;
|
||||
|
||||
// Free the connection resources
|
||||
http_client_conn_free(conn);
|
||||
conn->state = HTTP_CLIENT_CONN_FREE;
|
||||
conn->client->num_conns--;
|
||||
|
||||
// Mark response as freed
|
||||
res->context = NULL;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user