websocket: support CURLOPT_READFUNCTION

Add support for CURLOPT_READFUNCTION with WebSocket urls when *not* in
connect-only mode, e.g. when using curl_multi_perform.

Install the callback function and set CURLOPT_UPLOAD. Return
CURL_READFUNC_PAUSE when having nothing more to send and unpause the
transfer when more data is ready.

This will send the read bytes in a WebSocket BINARY frame.

Add support for this mode in the pytest "ws_data" client and have all
tests run in 'curl_ws_send/recv' and 'peform' mode as well.

Add `curl_ws_start_frame()`. Document, cover in libcurl-ws.md and
explain the READFUNCTION mode for websockets.

Add example `websocket-updown` for this.

Closes #17683
This commit is contained in:
Stefan Eissing 2025-07-08 09:15:43 +02:00 committed by Daniel Stenberg
parent 756c0718c2
commit 37cecfc7b9
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
17 changed files with 878 additions and 156 deletions

View file

@ -19,6 +19,7 @@ ephiperfifo
evhiperfifo
externalsocket
fileupload
ftp-delete
ftp-wildcard
ftpget
ftpgetinfo
@ -129,4 +130,5 @@ urlapi
usercertinmem
websocket
websocket-cb
websocket-updown
xmlstream

View file

@ -136,7 +136,8 @@ check_PROGRAMS = \
url2file \
urlapi \
websocket \
websocket-cb
websocket-cb \
websocket-updown
# These examples require external dependencies that may not be commonly
# available on POSIX systems, so do not bother attempting to compile them here.

View file

@ -0,0 +1,125 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
/* <DESC>
* WebSocket download-only using write callback
* </DESC>
*/
#include <stdio.h>
#include <string.h>
#include <curl/curl.h>
static size_t writecb(char *b, size_t size, size_t nitems, void *p)
{
CURL *easy = p;
size_t i;
unsigned int blen = (unsigned int)(nitems * size);
const struct curl_ws_frame *frame = curl_ws_meta(easy);
fprintf(stderr, "Type: %s\n", frame->flags & CURLWS_BINARY ?
"binary" : "text");
if(frame->flags & CURLWS_BINARY) {
fprintf(stderr, "Bytes: %u", blen);
for(i = 0; i < nitems; i++)
fprintf(stderr, "%02x ", (unsigned char)b[i]);
fprintf(stderr, "\n");
}
else
fprintf(stderr, "Text: %.*s\n", (int)blen, b);
return nitems;
}
struct read_ctx {
CURL *easy;
char buf[1024];
size_t blen;
size_t nsent;
};
static size_t readcb(char *buf, size_t nitems, size_t buflen, void *p)
{
struct read_ctx *ctx = p;
size_t len = nitems * buflen;
size_t left = ctx->blen - ctx->nsent;
CURLcode result;
if(!ctx->nsent) {
/* On first call, set the FRAME information to be used (it defaults
* to CURLWS_BINARY otherwise). */
result = curl_ws_start_frame(ctx->easy, CURLWS_TEXT,
(curl_off_t)ctx->blen);
if(result) {
fprintf(stderr, "error staring frame: %d\n", result);
return CURL_READFUNC_ABORT;
}
}
fprintf(stderr, "read(len=%d, left=%d)\n", (int)len, (int)left);
if(left) {
if(left < len)
len = left;
memcpy(buf, ctx->buf + ctx->nsent, len);
ctx->nsent += len;
return len;
}
return 0;
}
int main(int argc, const char *argv[])
{
CURL *easy;
struct read_ctx rctx;
CURLcode res;
const char *payload = "Hello, friend!";
memset(&rctx, 0, sizeof(rctx));
easy = curl_easy_init();
if(!easy)
return 1;
if(argc == 2)
curl_easy_setopt(easy, CURLOPT_URL, argv[1]);
else
curl_easy_setopt(easy, CURLOPT_URL, "wss://example.com");
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, writecb);
curl_easy_setopt(easy, CURLOPT_WRITEDATA, easy);
curl_easy_setopt(easy, CURLOPT_READFUNCTION, readcb);
/* tell curl that we want to send the payload */
rctx.easy = easy;
rctx.blen = strlen(payload);
memcpy(rctx.buf, payload, rctx.blen);
curl_easy_setopt(easy, CURLOPT_READDATA, &rctx);
curl_easy_setopt(easy, CURLOPT_UPLOAD, 1L);
/* Perform the request, res gets the return code */
res = curl_easy_perform(easy);
/* Check for errors */
if(res != CURLE_OK)
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
/* always cleanup */
curl_easy_cleanup(easy);
return 0;
}

View file

@ -112,6 +112,7 @@ man_MANS = \
curl_ws_meta.3 \
curl_ws_recv.3 \
curl_ws_send.3 \
curl_ws_start_frame.3 \
libcurl-easy.3 \
libcurl-env-dbg.3 \
libcurl-env.3 \

View file

@ -9,6 +9,7 @@ See-also:
- curl_easy_perform (3)
- curl_easy_setopt (3)
- curl_ws_recv (3)
- curl_ws_start_frame (3)
- libcurl-ws (3)
Protocol:
- WS

View file

@ -0,0 +1,143 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Title: curl_ws_start_frame
Section: 3
Source: libcurl
See-also:
- curl_easy_getinfo (3)
- curl_easy_perform (3)
- curl_easy_setopt (3)
- curl_ws_recv (3)
- libcurl-ws (3)
Protocol:
- WS
Added-in: 8.16.0
---
# NAME
curl_ws_start_frame - start a new WebSocket frame
# SYNOPSIS
~~~c
#include <curl/curl.h>
CURLcode curl_ws_start_frame(CURL *curl,
unsigned int flags,
curl_off_t frame_len);
~~~
# DESCRIPTION
Add the WebSocket frame header for the given flags and length to
the transfers send buffer for WebSocket encoded data. Intended for
use in a CURLOPT_READFUNCTION(3) callback.
When using a CURLOPT_READFUNCTION(3) in a WebSocket transfer, any
data returned by that function is sent as a *CURLWS_BINARY* frame
with the length being the amount of data read.
To send larger frames or frames of a different type, call
curl_ws_start_frame() from within the read function and then return
the data belonging to the frame.
The function fails, if a previous frame has not been completely
read yet. Also it fails in *CURLWS_RAW_MODE*.
# FLAGS
Supports all flags documented in curl_ws_meta(3).
# %PROTOCOLS%
# EXAMPLE
~~~c
#include <string.h> /* for strlen */
struct read_ctx {
CURL *easy;
char *message;
size_t msg_len;
size_t nsent;
};
static size_t readcb(char *buf, size_t nitems, size_t buflen, void *p)
{
struct read_ctx *ctx = p;
size_t len = nitems * buflen;
size_t left = ctx->msg_len - ctx->nsent;
CURLcode result;
if(!ctx->nsent) {
/* Want to send TEXT frame. */
result = curl_ws_start_frame(ctx->easy, CURLWS_TEXT,
(curl_off_t)ctx->msg_len);
if(result) {
fprintf(stderr, "error staring frame: %d\n", result);
return CURL_READFUNC_ABORT;
}
}
if(left) {
if(left < len)
len = left;
memcpy(buf, ctx->message + ctx->nsent, len);
ctx->nsent += len;
return len;
}
return 0;
}
int main(void)
{
CURL *easy;
struct read_ctx rctx;
CURLcode res;
easy = curl_easy_init();
if(!easy)
return 1;
curl_easy_setopt(easy, CURLOPT_URL, "wss://example.com");
curl_easy_setopt(easy, CURLOPT_READFUNCTION, readcb);
/* tell curl that we want to send the payload */
memset(&rctx, 0, sizeof(rctx));
rctx.easy = easy;
rctx.message = "Hello, friend!";
rctx.msg_len = strlen(rctx.message);
curl_easy_setopt(easy, CURLOPT_READDATA, &rctx);
curl_easy_setopt(easy, CURLOPT_UPLOAD, 1L);
/* Perform the request, res gets the return code */
res = curl_easy_perform(easy);
/* Check for errors */
if(res != CURLE_OK)
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
/* always cleanup */
curl_easy_cleanup(easy);
return 0;
}
~~~
# %AVAILABILITY%
# RETURN VALUE
This function returns a CURLcode indicating success or error.
CURLE_OK (0) means everything was OK, non-zero means an error occurred, see
libcurl-errors(3). If CURLOPT_ERRORBUFFER(3) was set with curl_easy_setopt(3)
there can be an error message stored in the error buffer when non-zero is
returned.
Instead of blocking, the function returns **CURLE_AGAIN**. The correct
behavior is then to wait for the socket to signal readability before calling
this function again.
Any other non-zero return value indicates an error. See the libcurl-errors(3)
man page for the full list with descriptions.

View file

@ -97,14 +97,17 @@ Because of the many different ways WebSocket can be used, which is much more
flexible than limited to plain downloads or uploads, libcurl offers two
different API models to use it:
1. CURLOPT_WRITEFUNCTION model:
1. CURLOPT_WRITEFUNCTION/CURLOPT_READFUNCTION model:
Using a write callback with CURLOPT_WRITEFUNCTION(3) much like other
downloads for when the traffic is download oriented.
Using a read callback with CURLOPT_READFUNCTION(3) much like other
uploads for sending WebSocket frames to the server.
2. CURLOPT_CONNECT_ONLY model:
Using curl_ws_recv(3) and curl_ws_send(3) functions.
## CURLOPT_WRITEFUNCTION MODEL
## CURLOPT_WRITEFUNCTION/CURLOPT_READFUNCTION MODEL
CURLOPT_CONNECT_ONLY(3) must be unset or **0L** for this model to take effect.
@ -114,6 +117,21 @@ callback configured in CURLOPT_WRITEFUNCTION(3), whenever an incoming chunk
of WebSocket data is received. The callback is handed a pointer to the payload
data as an argument and can call curl_ws_meta(3) to get relevant metadata.
With libcurl 8.16.0 or later, sending of WebSocket frames via a
CURLOPT_READFUNCTION(3) is supported. To use that on such a connection,
register a callback via CURLOPT_READFUNCTION(3) and set CURLOPT_UPLOAD(3)
as well. Once, the WebSocket connection is established, your callback is
invoked to get data to send. That data is sent in a *CURLWS_BINARY* frame with
length of exactly the data returned.
To send other frame types or longer frames, use curl_ws_start_frame(3)
in the read callback. See the *websocket-updown* example.
When using curl_multi_perform(3) to drive transfers, more possibilities
exist. The CURLOPT_READFUNCTION(3) may return *CURL_READFUNC_PAUSE* when
it has no more data to send. Calling curl_easy_pause(3) afterwards
resumes the upload and the read callback is invoked again.
## CURLOPT_CONNECT_ONLY MODEL
CURLOPT_CONNECT_ONLY(3) must be **2L** for this model to take effect.

View file

@ -39,6 +39,7 @@ Available bits in the bitmask
## CURLWS_RAW_MODE (1)
Deliver "raw" WebSocket traffic to the CURLOPT_WRITEFUNCTION(3)
callback. Read "raw" WebSocket traffic from the CURLOPT_READFUNCTION(3)
callback.
In raw mode, libcurl does not handle pings or any other frame for the