This guide explains how to use MSH3 as an HTTP/3 client.
Here's a step-by-step guide to using MSH3 as an HTTP/3 client:
- Initialize the MSH3 API
- Create a configuration with appropriate settings
- Create a connection to the server
- Create a request and send headers and data
- Process response headers and data in the callbacks
- Clean up resources
#include "msh3.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Callback for request events
MSH3_STATUS
RequestCallback(
MSH3_REQUEST* Request,
void* Context,
MSH3_REQUEST_EVENT* Event
)
{
switch (Event->Type) {
case MSH3_REQUEST_EVENT_HEADER_RECEIVED:
printf("Header: %.*s: %.*s\n",
(int)Event->HEADER_RECEIVED.Header->NameLength,
Event->HEADER_RECEIVED.Header->Name,
(int)Event->HEADER_RECEIVED.Header->ValueLength,
Event->HEADER_RECEIVED.Header->Value);
break;
case MSH3_REQUEST_EVENT_DATA_RECEIVED:
printf("Received %u bytes of data\n", Event->DATA_RECEIVED.Length);
printf("%.*s", (int)Event->DATA_RECEIVED.Length,
(const char*)Event->DATA_RECEIVED.Data);
// Indicate that we've processed the data
MsH3RequestCompleteReceive(Request, Event->DATA_RECEIVED.Length);
break;
case MSH3_REQUEST_EVENT_PEER_SEND_SHUTDOWN:
printf("Server completed sending data\n");
break;
case MSH3_REQUEST_EVENT_SEND_COMPLETE:
printf("Request sent completely\n");
break;
case MSH3_REQUEST_EVENT_SHUTDOWN_COMPLETE:
printf("Request shutdown complete\n");
break;
}
return MSH3_STATUS_SUCCESS;
}
// Callback for connection events
MSH3_STATUS
ConnectionCallback(
MSH3_CONNECTION* Connection,
void* Context,
MSH3_CONNECTION_EVENT* Event
)
{
switch (Event->Type) {
case MSH3_CONNECTION_EVENT_CONNECTED:
printf("Connection established\n");
// Create a request when connected
const char* host = (const char*)Context;
MSH3_REQUEST* Request = MsH3RequestOpen(
Connection,
RequestCallback,
NULL,
MSH3_REQUEST_FLAG_NONE);
if (Request) {
// Setup HTTP headers
const MSH3_HEADER Headers[] = {
{ ":method", 7, "GET", 3 },
{ ":path", 5, "/", 1 },
{ ":scheme", 7, "https", 5 },
{ ":authority", 10, host, strlen(host) },
{ "user-agent", 10, "msh3-client", 11 }
};
const size_t HeadersCount = sizeof(Headers) / sizeof(MSH3_HEADER);
// Send the request
MsH3RequestSend(
Request,
MSH3_REQUEST_SEND_FLAG_FIN, // No more data to send
Headers,
HeadersCount,
NULL, // No body data
0,
NULL);
} else {
printf("Failed to create request\n");
}
break;
case MSH3_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
printf("Connection shutdown by transport, status=0x%x, error=0x%llx\n",
Event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status,
(unsigned long long)Event->SHUTDOWN_INITIATED_BY_TRANSPORT.ErrorCode);
break;
case MSH3_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
printf("Connection shutdown by peer, error=0x%llx\n",
(unsigned long long)Event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode);
break;
case MSH3_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
printf("Connection shutdown complete\n");
break;
}
return MSH3_STATUS_SUCCESS;
}
int main(int argc, char** argv) {
if (argc < 2) {
printf("Usage: %s <hostname> [port]\n", argv[0]);
return 1;
}
const char* hostname = argv[1];
uint16_t port = (argc > 2) ? (uint16_t)atoi(argv[2]) : 443;
// Initialize MSH3 API
MSH3_API* api = MsH3ApiOpen();
if (!api) {
printf("Failed to initialize MSH3 API\n");
return 1;
}
// Create configuration
MSH3_SETTINGS settings = {0};
settings.IsSetFlags = 0;
settings.IsSet.IdleTimeoutMs = 1;
settings.IdleTimeoutMs = 30000; // 30 seconds
MSH3_CONFIGURATION* config =
MsH3ConfigurationOpen(api, &settings, sizeof(settings));
if (!config) {
printf("Failed to create configuration\n");
MsH3ApiClose(api);
return 1;
}
// Create connection
MSH3_CONNECTION* connection =
MsH3ConnectionOpen(api, ConnectionCallback, (void*)hostname);
if (!connection) {
printf("Failed to create connection\n");
MsH3ConfigurationClose(config);
MsH3ApiClose(api);
return 1;
}
// Configure the connection
MSH3_STATUS status = MsH3ConnectionSetConfiguration(connection, config);
if (MSH3_FAILED(status)) {
printf("Failed to set connection configuration, status=0x%x\n", status);
MsH3ConnectionClose(connection);
MsH3ConfigurationClose(config);
MsH3ApiClose(api);
return 1;
}
// Set up the server address
MSH3_ADDR serverAddr = {0};
serverAddr.Ipv4.sin_family = AF_INET;
// Resolve hostname and set IP address (simplified here)
inet_pton(AF_INET, "198.51.100.1", &serverAddr.Ipv4.sin_addr); // Example IP
MSH3_SET_PORT(&serverAddr, port);
// Start the connection
status = MsH3ConnectionStart(connection, config, hostname, &serverAddr);
if (MSH3_FAILED(status)) {
printf("Failed to start connection, status=0x%x\n", status);
MsH3ConnectionClose(connection);
MsH3ConfigurationClose(config);
MsH3ApiClose(api);
return 1;
}
// In a real application, you'd wait for events using an event loop
// For this example, we'll just wait for user input before cleaning up
printf("Press Enter to terminate...\n");
getchar();
// Clean up
MsH3ConnectionClose(connection);
MsH3ConfigurationClose(config);
MsH3ApiClose(api);
return 0;
}// Set up client authentication with a certificate
MSH3_CERTIFICATE_FILE certFile = {
.PrivateKeyFile = "client.key",
.CertificateFile = "client.crt"
};
MSH3_CREDENTIAL_CONFIG credConfig = {
.Type = MSH3_CREDENTIAL_TYPE_CERTIFICATE_FILE,
.Flags = MSH3_CREDENTIAL_FLAG_CLIENT,
.CertificateFile = &certFile
};
status = MsH3ConfigurationLoadCredential(config, &credConfig);
if (MSH3_FAILED(status)) {
printf("Failed to load credentials, status=0x%x\n", status);
return 1;
}const MSH3_HEADER Headers[] = {
{ ":method", 7, "POST", 4 },
{ ":path", 5, "/api/data", 9 },
{ ":scheme", 7, "https", 5 },
{ ":authority", 10, host, strlen(host) },
{ "content-type", 12, "application/json", 16 }
};
const size_t HeadersCount = sizeof(Headers) / sizeof(MSH3_HEADER);
const char* body = "{\"key\":\"value\"}";
uint32_t bodyLength = (uint32_t)strlen(body);
// Send request with body
MsH3RequestSend(
Request,
MSH3_REQUEST_SEND_FLAG_FIN, // No more data to send after this
Headers,
HeadersCount,
body, // Request body
bodyLength,
NULL);// Send headers first
MsH3RequestSend(
Request,
MSH3_REQUEST_SEND_FLAG_NONE, // Not the end of the request
Headers,
HeadersCount,
NULL, // No body data yet
0,
NULL);
// Then send the first part of the body
const char* bodyPart1 = "First part of the data";
MsH3RequestSend(
Request,
MSH3_REQUEST_SEND_FLAG_DELAY_SEND, // More data coming
NULL, // No headers this time
0,
bodyPart1,
(uint32_t)strlen(bodyPart1),
NULL);
// Send the last part of the body and finish the request
const char* bodyPart2 = "Last part of the data";
MsH3RequestSend(
Request,
MSH3_REQUEST_SEND_FLAG_FIN, // End of request
NULL,
0,
bodyPart2,
(uint32_t)strlen(bodyPart2),
NULL);For applications that make many requests with similar headers, enabling dynamic QPACK can improve compression efficiency and reduce bandwidth usage. This feature is available when MSH3_API_ENABLE_PREVIEW_FEATURES is defined.
#ifdef MSH3_API_ENABLE_PREVIEW_FEATURES
// Create configuration with dynamic QPACK enabled
MSH3_SETTINGS settings = {0};
settings.IsSet.IdleTimeoutMs = 1;
settings.IdleTimeoutMs = 30000; // 30 seconds
settings.IsSet.DynamicQPackEnabled = 1;
settings.DynamicQPackEnabled = 1; // Enable dynamic QPACK compression
MSH3_CONFIGURATION* config =
MsH3ConfigurationOpen(api, &settings, sizeof(settings));
#endifWith dynamic QPACK enabled:
- MSH3 will use a dynamic table with 4096 bytes capacity
- Up to 100 streams can be blocked waiting for dynamic table updates
- Headers that are repeated across requests will be compressed more efficiently
- The first few requests may have slightly higher latency as the dynamic table is built
This feature is particularly beneficial for:
- Applications making many requests to the same server
- REST APIs with consistent header patterns
- Scenarios where bandwidth efficiency is critical
- Static QPACK (default): Lower latency per request, consistent performance, simpler implementation
- Dynamic QPACK: Better compression for repeated headers, but may introduce some latency for initial requests as the dynamic table is populated
Choose dynamic QPACK when you expect to make multiple requests with similar headers and bandwidth efficiency is more important than minimal per-request latency.