Skip to content

Commit 9f83c6a

Browse files
committed
mqtt: qos2
1 parent a07e882 commit 9f83c6a

12 files changed

Lines changed: 691 additions & 6 deletions

File tree

include/libwebsockets/lws-callbacks.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -885,20 +885,26 @@ enum lws_callback_reasons {
885885
* Return nonzero to close the wsi.
886886
*/
887887

888-
LWS_CALLBACK_HTTP_INTERCEPTOR_CHECK = 213,
888+
LWS_CALLBACK_MQTT_QOS2_RX_COMPLETE = 213,
889+
/**< When a QoS2 message has fully completed the transaction (PUBREL
890+
* received, PUBCOMP sent), this callback is generated.
891+
* `in` will point to the `uint16_t` packet ID that completed.
892+
*/
893+
894+
LWS_CALLBACK_HTTP_INTERCEPTOR_CHECK = 214,
889895
/**< A mount has a interceptor_path enabled, this callback asks the
890896
* protocol bound to that mount if it is OK for this request to
891897
* proceed. If returning 0, the request proceeds to the original
892898
* mount. If nonzero, the request is diverted to the interceptor_path
893899
* mount.
894900
*/
895901

896-
LWS_CALLBACK_GET_PSS_SIZE = 214,
902+
LWS_CALLBACK_GET_PSS_SIZE = 215,
897903
/**< Called when a protocol wants to specify its PSS size at runtime.
898904
* If the protocol structure has per_session_data_size == 0, lws will
899905
* call this to get the size to allocate for the session. */
900906

901-
LWS_CALLBACK_DHT_VERB_DISPATCH = 215,
907+
LWS_CALLBACK_DHT_VERB_DISPATCH = 216,
902908
/**< Sent to the user protocol handler callback when a DHT message
903909
* carrying a registered verb has been matched by lws-dht.
904910
* `in` is a pointer to `struct lws_dht_verb_dispatch_args` containing

include/libwebsockets/lws-mqtt.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ typedef struct lws_mqtt_str_st lws_mqtt_str_t;
7373
typedef enum {
7474
QOS0,
7575
QOS1,
76-
QOS2, /* not supported */
76+
QOS2,
7777
RESERVED_QOS_LEVEL,
7878
FAILURE_QOS_LEVEL = 0x80
7979
} lws_mqtt_qos_levels_t;
@@ -88,6 +88,11 @@ typedef union {
8888
uint8_t bits;
8989
} lws_mqtt_fixed_hdr_t;
9090

91+
typedef struct lws_mqtt_qos2_state_ops {
92+
int (*rx_add)(struct lws *wsi, const char *client_id, uint16_t pkt_id);
93+
int (*rx_remove)(struct lws *wsi, const char *client_id, uint16_t pkt_id);
94+
} lws_mqtt_qos2_state_ops_t;
95+
9196
/*
9297
* MQTT connection parameters, passed into struct
9398
* lws_client_connect_info to establish a connection using
@@ -122,6 +127,7 @@ typedef struct lws_mqtt_client_connect_param_s {
122127
parameters */
123128
const char *username;
124129
const char *password;
130+
const lws_mqtt_qos2_state_ops_t *qos2_state_ops;
125131
uint8_t aws_iot;
126132
} lws_mqtt_client_connect_param_t;
127133

@@ -383,4 +389,16 @@ LWS_VISIBLE LWS_EXTERN int LWS_WARN_UNUSED_RESULT
383389
lws_mqtt_client_send_unsubcribe(struct lws *wsi,
384390
const lws_mqtt_subscribe_param_t *unsub);
385391

392+
/**
393+
* lws_mqtt_client_qos2_rx_add() - inject a saved QoS2 packet ID into the rx list
394+
*
395+
* \param wsi: the mqtt child wsi
396+
* \param pkt_id: the packet ID to inject
397+
*
398+
* This allows the application to repopulate the unacknowledged QoS2 receives
399+
* from persistent storage when a session is resumed.
400+
*/
401+
LWS_VISIBLE LWS_EXTERN int
402+
lws_mqtt_client_qos2_rx_add(struct lws *wsi, uint16_t pkt_id);
403+
386404
#endif /* _LWS_MQTT_H */

lib/roles/mqtt/client/client-mqtt.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ lws_create_client_mqtt_object(const struct lws_client_connect_info *i,
131131
lws_free((void *)cp->client_id);
132132

133133
c->keep_alive_secs = cp->keep_alive;
134+
c->qos2_state_ops = cp->qos2_state_ops;
134135
c->aws_iot = cp->aws_iot;
135136

136137
if (cp->will_param.topic &&

lib/roles/mqtt/mqtt.c

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,32 @@ _lws_mqtt_rx_parser(struct lws *wsi, lws_mqtt_parser_t *par,
929929
buf += 2;
930930
len -= 2;
931931
wsi->mqtt->peer_ack_pkt_id = par->cpkt_id;
932+
wsi->mqtt->qos2_duplicate = 0;
933+
934+
if (pub->qos == QOS2) {
935+
lws_mqtt_qos2_rx_t *rx;
936+
937+
lws_start_foreach_dll(struct lws_dll2 *, p, wsi->mqtt->qos2_rx_list.head) {
938+
rx = lws_container_of(p, lws_mqtt_qos2_rx_t, list);
939+
if (rx->packet_id == par->cpkt_id) {
940+
wsi->mqtt->qos2_duplicate = 1;
941+
break;
942+
}
943+
} lws_end_foreach_dll(p);
944+
945+
if (!wsi->mqtt->qos2_duplicate) {
946+
rx = lws_malloc(sizeof(*rx), "qos2 rx");
947+
if (rx) {
948+
const char *cid = wsi->mqtt->client.id ? (const char *)wsi->mqtt->client.id->buf : "unknown";
949+
rx->packet_id = par->cpkt_id;
950+
lws_dll2_add_tail(&rx->list, &wsi->mqtt->qos2_rx_list);
951+
if (wsi->mqtt->client.qos2_state_ops &&
952+
wsi->mqtt->client.qos2_state_ops->rx_add)
953+
wsi->mqtt->client.qos2_state_ops->rx_add(wsi, cid, par->cpkt_id);
954+
}
955+
}
956+
}
957+
932958
lwsl_debug("%s: Packet ID %d\n",
933959
__func__, (int)par->cpkt_id);
934960
par->state = LMQCPP_PAYLOAD;
@@ -1471,11 +1497,40 @@ _lws_mqtt_rx_parser(struct lws *wsi, lws_mqtt_parser_t *par,
14711497
break;
14721498

14731499
case LMQCP_PUBREL:
1474-
lwsl_err("%s: cmd_completion: PUBREL\n",
1500+
{
1501+
lws_mqtt_qos2_rx_t *rx;
1502+
1503+
lwsl_info("%s: cmd_completion: PUBREL\n",
14751504
__func__);
1505+
1506+
lws_start_foreach_dll_safe(struct lws_dll2 *, p, tp, wsi->mqtt->qos2_rx_list.head) {
1507+
rx = lws_container_of(p, lws_mqtt_qos2_rx_t, list);
1508+
if (rx->packet_id == par->cpkt_id) {
1509+
const char *cid = wsi->mqtt->client.id ? (const char *)wsi->mqtt->client.id->buf : "unknown";
1510+
lws_dll2_remove(&rx->list);
1511+
lws_free(rx);
1512+
if (wsi->mqtt->client.qos2_state_ops &&
1513+
wsi->mqtt->client.qos2_state_ops->rx_remove)
1514+
wsi->mqtt->client.qos2_state_ops->rx_remove(wsi, cid, par->cpkt_id);
1515+
break;
1516+
}
1517+
} lws_end_foreach_dll_safe(p, tp);
1518+
1519+
lws_start_foreach_ll(struct lws *, w,
1520+
wsi->mux.child_list) {
1521+
uint16_t pid = par->cpkt_id;
1522+
if (w->a.protocol->callback(w,
1523+
LWS_CALLBACK_MQTT_QOS2_RX_COMPLETE,
1524+
w->user_space,
1525+
(void *)&pid, 0)) {
1526+
return 1;
1527+
}
1528+
} lws_end_foreach_ll(w, mux.sibling_list);
1529+
14761530
wsi->mqtt->send_pubcomp = 1;
14771531
lws_callback_on_writable(wsi);
14781532
break;
1533+
}
14791534

14801535
case LMQCP_PUBACK:
14811536
lwsl_info("%s: cmd_completion: PUBACK\n",
@@ -1671,7 +1726,8 @@ _lws_mqtt_rx_parser(struct lws *wsi, lws_mqtt_parser_t *par,
16711726

16721727
lws_start_foreach_ll(struct lws *, w,
16731728
wsi->mux.child_list) {
1674-
if (lws_mqtt_find_sub(w->mqtt,
1729+
if (!wsi->mqtt->qos2_duplicate &&
1730+
lws_mqtt_find_sub(w->mqtt,
16751731
pub->topic))
16761732
if (w->a.protocol->callback(
16771733
w, (enum lws_callback_reasons)n,
@@ -2531,3 +2587,22 @@ lws_wsi_mqtt_adopt(struct lws *parent_wsi, struct lws *wsi)
25312587
return NULL;
25322588
}
25332589

2590+
int
2591+
lws_mqtt_client_qos2_rx_add(struct lws *wsi, uint16_t pkt_id)
2592+
{
2593+
struct lws *nwsi = lws_get_network_wsi(wsi);
2594+
lws_mqtt_qos2_rx_t *rx;
2595+
2596+
if (!nwsi || !nwsi->mqtt)
2597+
return 1;
2598+
2599+
rx = lws_malloc(sizeof(*rx), "qos2 rx");
2600+
if (!rx)
2601+
return 1;
2602+
2603+
rx->packet_id = pkt_id;
2604+
lws_dll2_add_tail(&rx->list, &nwsi->mqtt->qos2_rx_list);
2605+
2606+
return 0;
2607+
}
2608+

lib/roles/mqtt/ops-mqtt.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,17 @@ rops_close_role_mqtt(struct lws_context_per_thread *pt, struct lws *wsi)
485485
s = s1;
486486
}
487487

488+
/* clean up QoS2 rx list */
489+
{
490+
lws_mqtt_qos2_rx_t *rx;
491+
492+
lws_start_foreach_dll_safe(struct lws_dll2 *, p, tp, wsi->mqtt->qos2_rx_list.head) {
493+
rx = lws_container_of(p, lws_mqtt_qos2_rx_t, list);
494+
lws_dll2_remove(&rx->list);
495+
lws_free(rx);
496+
} lws_end_foreach_dll_safe(p, tp);
497+
}
498+
488499
lws_mqtt_publish_param_t *pub =
489500
(lws_mqtt_publish_param_t *)
490501
wsi->mqtt->rx_cpkt_param;

lib/roles/mqtt/private-lib-roles-mqtt.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,11 @@ typedef struct lws_mqtt_subs {
327327
char topic[];
328328
} lws_mqtt_subs_t;
329329

330+
typedef struct lws_mqtt_qos2_rx {
331+
struct lws_dll2 list;
332+
uint16_t packet_id;
333+
} lws_mqtt_qos2_rx_t;
334+
330335
typedef struct lws_mqtts {
331336
lws_mqtt_parser_t par;
332337
lwsgs_mqtt_states_t estate;
@@ -348,6 +353,7 @@ typedef struct lws_mqttc {
348353
} will;
349354
uint16_t keep_alive_secs;
350355
uint16_t conn_flags;
356+
const lws_mqtt_qos2_state_ops_t *qos2_state_ops;
351357
uint8_t aws_iot;
352358
} lws_mqttc_t;
353359

@@ -358,6 +364,7 @@ struct _lws_mqtt_related {
358364
lws_sorted_usec_list_t sul_unsuback_wait; /* unsuback wait TO */
359365
lws_sorted_usec_list_t sul_qos2_pubrec_wait; /* QoS2 pubrec wait TO */
360366
lws_sorted_usec_list_t sul_shadow_wait; /* Device Shadow wait TO */
367+
struct lws_dll2_owner qos2_rx_list;
361368
struct lws *wsi; /**< so sul can use lws_container_of */
362369
lws_mqtt_subs_t *subs_head; /**< Linked-list of heap-allocated subscription objects */
363370
void *rx_cpkt_param;
@@ -382,6 +389,8 @@ struct _lws_mqtt_related {
382389
uint8_t unacked_publish:1;
383390
uint8_t unacked_pubrel:1;
384391

392+
uint8_t qos2_duplicate:1;
393+
385394
uint8_t done_subscribe:1;
386395
uint8_t done_birth:1;
387396
uint8_t inside_shadow:1;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
project(lws-minimal-mqtt-client-qos2 C)
2+
cmake_minimum_required(VERSION 3.10)
3+
include(CheckCSourceCompiles)
4+
include(LwsCheckRequirements)
5+
6+
set(SAMP lws-minimal-mqtt-client-qos2)
7+
set(SRCS minimal-mqtt-client-qos2.c)
8+
9+
set(requirements 1)
10+
require_lws_config(LWS_ROLE_MQTT 1 requirements)
11+
require_lws_config(LWS_WITH_CLIENT 1 requirements)
12+
require_lws_config(LWS_WITH_SYS_STATE 1 requirements)
13+
14+
if (requirements)
15+
add_executable(${SAMP} ${SRCS})
16+
17+
if (websockets_shared)
18+
target_link_libraries(${SAMP} websockets_shared ${LIBWEBSOCKETS_DEP_LIBS})
19+
add_dependencies(${SAMP} websockets_shared)
20+
else()
21+
target_link_libraries(${SAMP} websockets ${LIBWEBSOCKETS_DEP_LIBS})
22+
endif()
23+
24+
#
25+
# requires mosquitto running locally
26+
#
27+
add_test(NAME mqtt-client-qos2 COMMAND lws-minimal-mqtt-client-qos2 -f)
28+
set_tests_properties(mqtt-client-qos2 PROPERTIES
29+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/minimal-examples-lowlevel/mqtt-client/minimal-mqtt-client-qos2
30+
TIMEOUT 20)
31+
endif()
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# lws-minimal-mqtt-client-qos2
2+
3+
This is a minimal example demonstrating MQTT QoS 2 functionality with deduplication and session restoration capabilities.
4+
5+
The client connects to a local Mosquitto broker, subscribes to a topic (`test/topic0`), and publishes a QoS 2 message to that same topic. It tracks the `packet_id` of unacknowledged QoS 2 receives using the operations API.
6+
7+
## Build
8+
9+
```bash
10+
$ cmake . && make
11+
```
12+
13+
## Usage
14+
15+
This example requires a local `mosquitto` broker to run against.
16+
17+
### 1. Standard Testing
18+
Run the client normally to see the full QoS 2 handshake without any interruptions:
19+
20+
```bash
21+
$ ./lws-minimal-mqtt-client-qos2
22+
```
23+
24+
The client will successfully connect, subscribe, publish a message, receive it, and log the payload, followed by the normal completion.
25+
26+
### 2. Fault Injection Mode (Session Resumption)
27+
Run the client with the `-f` flag to simulate a dropped connection during the QoS 2 handshake:
28+
29+
```bash
30+
$ ./lws-minimal-mqtt-client-qos2 -f
31+
```
32+
33+
**Sequence of Events:**
34+
1. The client receives the QoS 2 `PUBLISH` from the broker.
35+
2. The custom `my_rx_add` callback fires, saving the unacknowledged `packet_id` to the simulated state store.
36+
3. The client immediately drops its connection (forceful simulation).
37+
4. The built-in retry policy triggers a reconnection.
38+
5. On connection establishment, the client injects the stashed `packet_id` back into the library using `lws_mqtt_client_qos2_rx_add`.
39+
6. Mosquitto resends the `PUBLISH` with `DUP=1`.
40+
7. `libwebsockets` recognizes the duplicate, drops the payload to maintain the *Exactly-Once* guarantee, and seamlessly resumes the handshake (`PUBREC` -> `PUBREL` -> `PUBCOMP`).
41+
42+
## Commandline Options
43+
44+
- `-d <log level>`: Set the logging level (e.g., `-d 1039`)
45+
- `-s`: Use TLS / HTTPS
46+
- `-f`: Simulate connection drop for testing QoS 2 restoration

0 commit comments

Comments
 (0)