From fc295b79592d5fefa28fd841eae6528f54e6cbdf Mon Sep 17 00:00:00 2001 From: Andy Green Date: Mon, 23 Sep 2019 04:23:07 -0700 Subject: [PATCH] muxable client: make http support generic h1 and h2 has a bunch of code supporting autobinding outgoing client connections to be streams in, or queued as pipelined on, the same / existing single network connection, if it's to the same endpoint. Adapt this http-specific code and active connection tracking to be usable for generic muxable protocols the same way. --- include/libwebsockets/lws-client.h | 3 +- lib/abstract/transports/raw-skt.c | 10 ++ lib/core-net/private-lib-core-net.h | 9 ++ lib/core-net/vhost.c | 100 +++++++++++++++ lib/core-net/wsi.c | 57 +++++++++ lib/roles/http/client/client-handshake.c | 147 ++++------------------- lib/roles/http/client/client-http.c | 47 +------- 7 files changed, 203 insertions(+), 170 deletions(-) diff --git a/include/libwebsockets/lws-client.h b/include/libwebsockets/lws-client.h index 64b089bdc..22a4ec128 100644 --- a/include/libwebsockets/lws-client.h +++ b/include/libwebsockets/lws-client.h @@ -48,7 +48,8 @@ enum lws_client_connect_ssl_connection_flags { * HTTP/1.0: possible if Keep-Alive: yes sent by server * HTTP/1.1: always possible... uses pipelining * HTTP/2: always possible... uses parallel streams - * */ + */ + LCCSCF_MUXABLE_STREAM = (1 << 17), }; /** struct lws_client_connect_info - parameters to connect with when using diff --git a/lib/abstract/transports/raw-skt.c b/lib/abstract/transports/raw-skt.c index 7c0653e03..384807f29 100644 --- a/lib/abstract/transports/raw-skt.c +++ b/lib/abstract/transports/raw-skt.c @@ -273,6 +273,16 @@ lws_atcrs_client_conn(const lws_abs_t *abs) i.seq = abs->seq; i.opaque_user_data = abs->opaque_user_data; + /* + * the protocol itself has some natural attributes we should pass on + */ + + if (abs->ap->flags & LWS_AP_FLAG_PIPELINE_TRANSACTIONS) + i.ssl_connection |= LCCSCF_PIPELINE; + + if (abs->ap->flags & LWS_AP_FLAG_MUXABLE_STREAM) + i.ssl_connection |= LCCSCF_MUXABLE_STREAM; + priv->wsi = lws_client_connect_via_info(&i); if (!priv->wsi) return 1; diff --git a/lib/core-net/private-lib-core-net.h b/lib/core-net/private-lib-core-net.h index a15c55db6..377c5171c 100644 --- a/lib/core-net/private-lib-core-net.h +++ b/lib/core-net/private-lib-core-net.h @@ -1239,6 +1239,15 @@ lws_async_dns_deinit(lws_async_dns_t *dns); int lws_protocol_init_vhost(struct lws_vhost *vh, int *any); +int +_lws_generic_transaction_completed_active_conn(struct lws *wsi); + +#define ACTIVE_CONNS_SOLO 0 +#define ACTIVE_CONNS_MUXED 1 +#define ACTIVE_CONNS_QUEUED 2 + +int +lws_vhost_active_conns(struct lws *wsi, struct lws **nwsi); #ifdef __cplusplus }; diff --git a/lib/core-net/vhost.c b/lib/core-net/vhost.c index f41fcac18..c9cb96a5b 100644 --- a/lib/core-net/vhost.c +++ b/lib/core-net/vhost.c @@ -1363,6 +1363,7 @@ lws_context_deprecate(struct lws_context *context, lws_reload_func cb) #endif #if defined(LWS_WITH_NETWORK) + struct lws_vhost * lws_get_vhost_by_name(struct lws_context *context, const char *name) { @@ -1375,4 +1376,103 @@ lws_get_vhost_by_name(struct lws_context *context, const char *name) return NULL; } + + +#if defined(LWS_WITH_CLIENT) +/* + * This is the logic checking to see if the new connection wsi should have a + * pipelining or muxing relationship with an existing "active connection" to + * the same endpoint under the same conditions. + * + * This was originally in the client code but since the list is held on the + * vhost (to ensure the same client tls ctx is involved) it's cleaner in vhost.c + */ + +int +lws_vhost_active_conns(struct lws *wsi, struct lws **nwsi) +{ + const char *adsin; + + adsin = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_PEER_ADDRESS); + + lws_vhost_lock(wsi->vhost); /* ----------------------------------- { */ + + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, + wsi->vhost->dll_cli_active_conns_owner.head) { + struct lws *w = lws_container_of(d, struct lws, + dll_cli_active_conns); + + lwsl_debug("%s: check %s %s %d %d\n", __func__, adsin, + w->cli_hostname_copy, wsi->c_port, w->c_port); + + if (w != wsi && w->cli_hostname_copy && + !strcmp(adsin, w->cli_hostname_copy) && +#if defined(LWS_WITH_TLS) + (wsi->tls.use_ssl & LCCSCF_USE_SSL) == + (w->tls.use_ssl & LCCSCF_USE_SSL) && +#endif + wsi->c_port == w->c_port) { + + /* + * There's already an active connection. + * + * The server may have told the existing active + * connection that it doesn't support pipelining... + */ + if (w->keepalive_rejected) { + lwsl_info("defeating pipelining due to no " + "keepalive on server\n"); + goto solo; + } +#if defined (LWS_WITH_HTTP2) + /* + * h2: in usable state already: just use it without + * going through the queue + */ + if (w->client_h2_alpn && + (lwsi_state(w) == LRS_H2_WAITING_TO_SEND_HEADERS || + lwsi_state(w) == LRS_ESTABLISHED)) { + + lwsl_info("%s: just join h2 directly\n", + __func__); + + wsi->client_h2_alpn = 1; + lws_wsi_h2_adopt(w, wsi); + lws_vhost_unlock(wsi->vhost); /* } ---------- */ + + return ACTIVE_CONNS_MUXED; + } +#endif + + lwsl_info("apply %p to txn queue on %p state 0x%lx\n", + wsi, w, (unsigned long)w->wsistate); + /* + * ...let's add ourselves to his transaction queue... + * we are adding ourselves at the HEAD + */ + lws_dll2_add_head(&wsi->dll2_cli_txn_queue, + &w->dll2_cli_txn_queue_owner); + + /* + * h1: pipeline our headers out on him, + * and wait for our turn at client transaction_complete + * to take over parsing the rx. + */ + lws_vhost_unlock(wsi->vhost); /* } ---------- */ + + *nwsi = w; + + return ACTIVE_CONNS_QUEUED; + } + + } lws_end_foreach_dll_safe(d, d1); + +solo: + lws_vhost_unlock(wsi->vhost); /* } ---------------------------------- */ + + /* there is nobody already connected in the same way */ + + return ACTIVE_CONNS_SOLO; +} +#endif #endif diff --git a/lib/core-net/wsi.c b/lib/core-net/wsi.c index 3051c485c..47a4f2177 100644 --- a/lib/core-net/wsi.c +++ b/lib/core-net/wsi.c @@ -751,6 +751,63 @@ lws_get_context(const struct lws *wsi) return wsi->context; } +#if defined(LWS_WITH_CLIENT) +int +_lws_generic_transaction_completed_active_conn(struct lws *wsi) +{ + struct lws *wsi_eff = lws_client_wsi_effective(wsi); + + /* + * Are we constitutionally capable of having a queue, ie, we are on + * the "active client connections" list? + * + * If not, that's it for us. + */ + + if (lws_dll2_is_detached(&wsi->dll_cli_active_conns)) + return 0; /* no new transaction */ + + /* if this was a queued guy, close him and remove from queue */ + + if (wsi->transaction_from_pipeline_queue) { + lwsl_debug("closing queued wsi %p\n", wsi_eff); + /* so the close doesn't trigger a CCE */ + wsi_eff->already_did_cce = 1; + __lws_close_free_wsi(wsi_eff, + LWS_CLOSE_STATUS_CLIENT_TRANSACTION_DONE, + "queued client done"); + } + + /* after the first one, they can only be coming from the queue */ + wsi->transaction_from_pipeline_queue = 1; + + wsi->hdr_parsing_completed = 0; + + /* is there a new tail after removing that one? */ + wsi_eff = lws_client_wsi_effective(wsi); + + /* + * Do we have something pipelined waiting? + * it's OK if he hasn't managed to send his headers yet... he's next + * in line to do that... + */ + if (wsi_eff == wsi) { + /* + * Nothing pipelined... we should hang around a bit + * in case something turns up... + */ + lwsl_info("%s: nothing pipelined waiting\n", __func__); + lwsi_set_state(wsi, LRS_IDLING); + + lws_set_timeout(wsi, PENDING_TIMEOUT_CLIENT_CONN_IDLE, 5); + + return 0; /* no new transaction right now */ + } + + return 1; /* new transaction */ +} +#endif + LWS_VISIBLE int LWS_WARN_UNUSED_RESULT lws_raw_transaction_completed(struct lws *wsi) { diff --git a/lib/roles/http/client/client-handshake.c b/lib/roles/http/client/client-handshake.c index 77af3f11d..368a0323e 100644 --- a/lib/roles/http/client/client-handshake.c +++ b/lib/roles/http/client/client-handshake.c @@ -258,12 +258,9 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, char ni[48]; int m; -#ifdef LWS_WITH_IPV6 -#if defined(__ANDROID__) +#if defined(LWS_WITH_IPV6) && defined(__ANDROID__) ipv6only = 0; #endif -#endif - /* * async dns calls back here for everybody who cares when it gets a @@ -688,16 +685,14 @@ failed1: struct lws * lws_client_connect_2_dnsreq(struct lws *wsi) { -#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) - const char *adsin; -#endif - const char *cce = "", *meth = NULL, *ads; + const char *meth = NULL, *ads; struct addrinfo *result = NULL; #if defined(LWS_WITH_IPV6) struct sockaddr_in addr; const char *iface; #endif int n, port = 0; + struct lws *w; if (lwsi_state(wsi) == LRS_WAITING_DNS) { lwsl_notice("%s: LRS_WAITING_DNS\n", __func__); @@ -705,113 +700,37 @@ lws_client_connect_2_dnsreq(struct lws *wsi) return wsi; } -#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) - if (!wsi->http.ah && !wsi->stash) { - cce = "ah was NULL at cc2"; - lwsl_err("%s\n", cce); - goto oom4; - } - - /* we can only piggyback GET or POST */ - if (wsi->stash) meth = wsi->stash->cis[CIS_METHOD]; else meth = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_METHOD); - if (meth && strcmp(meth, "GET") && strcmp(meth, "POST")) { - lwsl_debug("%s: new conn on meth\n", __func__); - - goto create_new_conn; - } - /* we only pipeline connections that said it was okay */ if (!wsi->client_pipeline) { lwsl_debug("%s: new conn on no pipeline flag\n", __func__); - goto create_new_conn; + goto solo; } - /* - * let's take a look first and see if there are any already-active - * client connections we can piggy-back on. - */ + /* only pipeline things we associate with being a stream */ - adsin = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_PEER_ADDRESS); + if (meth && strcmp(meth, "RAW") && strcmp(meth, "GET") && + strcmp(meth, "POST")) + goto solo; - lws_vhost_lock(wsi->vhost); /* ----------------------------------- { */ + /* consult active connections to find out disposition */ - lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, lws_dll2_get_head( - &wsi->vhost->dll_cli_active_conns_owner)) { - struct lws *w = lws_container_of(d, struct lws, - dll_cli_active_conns); - -// lwsl_notice("%s: check %s %s %d %d\n", __func__, adsin, -// w->cli_hostname_copy, wsi->c_port, w->c_port); - - if (w != wsi && w->cli_hostname_copy && - !strcmp(adsin, w->cli_hostname_copy) && -#if defined(LWS_WITH_TLS) - (wsi->tls.use_ssl & LCCSCF_USE_SSL) == - (w->tls.use_ssl & LCCSCF_USE_SSL) && -#endif - wsi->c_port == w->c_port) { - - /* someone else is already connected to the right guy */ - - /* do we know for a fact pipelining won't fly? */ - if (w->keepalive_rejected) { - lwsl_info("defeating pipelining due to no " - "keepalive on server\n"); - lws_vhost_unlock(wsi->vhost); /* } ---------- */ - goto create_new_conn; - } -#if defined (LWS_WITH_HTTP2) - /* - * h2: in usable state already: just use it without - * going through the queue - */ - if (w->client_h2_alpn && - (lwsi_state(w) == LRS_H2_WAITING_TO_SEND_HEADERS || - lwsi_state(w) == LRS_ESTABLISHED)) { - - lwsl_info("%s: just join h2 directly\n", - __func__); - - wsi->client_h2_alpn = 1; - lws_wsi_h2_adopt(w, wsi); - lws_vhost_unlock(wsi->vhost); /* } ---------- */ - - return wsi; - } -#endif - - lwsl_info("apply %p to txn queue on %p state 0x%lx\n", - wsi, w, (unsigned long)w->wsistate); - /* - * ...let's add ourselves to his transaction queue... - * we are adding ourselves at the HEAD - */ - lws_dll2_add_head(&wsi->dll2_cli_txn_queue, - &w->dll2_cli_txn_queue_owner); - - /* - * h1: pipeline our headers out on him, - * and wait for our turn at client transaction_complete - * to take over parsing the rx. - */ - lws_vhost_unlock(wsi->vhost); /* } ---------- */ - return lws_client_connect_4_established(wsi, w, 0); - } - - } lws_end_foreach_dll_safe(d, d1); - - lws_vhost_unlock(wsi->vhost); /* } ---------------------------------- */ - -create_new_conn: -#endif + switch (lws_vhost_active_conns(wsi, &w)) { + case ACTIVE_CONNS_SOLO: + break; + case ACTIVE_CONNS_MUXED: + return wsi; + case ACTIVE_CONNS_QUEUED: + return lws_client_connect_4_established(wsi, w, 0); + } +solo: wsi->addrinfo_idx = 0; /* @@ -840,7 +759,8 @@ create_new_conn: * piggyback on our transaction queue */ - if (meth && (!strcmp(meth, "GET") || !strcmp(meth, "POST")) && + if (meth && (!strcmp(meth, "RAW") || !strcmp(meth, "GET") || + !strcmp(meth, "POST")) && lws_dll2_is_detached(&wsi->dll2_cli_txn_queue) && lws_dll2_is_detached(&wsi->dll_cli_active_conns)) { lws_vhost_lock(wsi->vhost); @@ -961,35 +881,12 @@ next_step: #endif return lws_client_connect_3_connect(wsi, ads, result, n, NULL); -oom4: - if (lwsi_role_client(wsi) && wsi->protocol /* && lwsi_state_est(wsi) */) - lws_inform_client_conn_fail(wsi,(void *)cce, strlen(cce)); - - /* take care that we might be inserted in fds already */ - if (wsi->position_in_fds_table != LWS_NO_FDS_POS) - goto failed1; - - /* - * We can't be an active client connection any more, if we thought - * that was what we were going to be doing. It should be if we are - * failing by oom4 path, we are still called by - * lws_client_connect_via_info() and will be returning NULL to that, - * so nobody else should have had a chance to queue on us. - */ - { - struct lws_vhost *vhost = wsi->vhost; - - lws_vhost_lock(vhost); - __lws_free_wsi(wsi); - lws_vhost_unlock(vhost); - } - - return NULL; - +#if defined(LWS_WITH_SYS_ASYNC_DNS) failed1: lws_close_free_wsi(wsi, LWS_CLOSE_STATUS_NOSTATUS, "client_connect2"); return NULL; +#endif } #if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) diff --git a/lib/roles/http/client/client-http.c b/lib/roles/http/client/client-http.c index d23474731..2b2281c7a 100644 --- a/lib/roles/http/client/client-http.c +++ b/lib/roles/http/client/client-http.c @@ -619,6 +619,7 @@ int LWS_WARN_UNUSED_RESULT lws_http_transaction_completed_client(struct lws *wsi) { struct lws *wsi_eff = lws_client_wsi_effective(wsi); + int n; lwsl_info("%s: wsi: %p, wsi_eff: %p (%s)\n", __func__, wsi, wsi_eff, wsi_eff->protocol->name); @@ -631,55 +632,13 @@ lws_http_transaction_completed_client(struct lws *wsi) return -1; } - /* - * Are we constitutionally capable of having a queue, ie, we are on - * the "active client connections" list? - * - * If not, that's it for us. - */ - - if (lws_dll2_is_detached(&wsi->dll_cli_active_conns)) - return -1; - - /* if this was a queued guy, close him and remove from queue */ - - if (wsi->transaction_from_pipeline_queue) { - lwsl_debug("closing queued wsi %p\n", wsi_eff); - /* so the close doesn't trigger a CCE */ - wsi_eff->already_did_cce = 1; - __lws_close_free_wsi(wsi_eff, - LWS_CLOSE_STATUS_CLIENT_TRANSACTION_DONE, - "queued client done"); - } + n = _lws_generic_transaction_completed_active_conn(wsi); _lws_header_table_reset(wsi->http.ah); - - /* after the first one, they can only be coming from the queue */ - wsi->transaction_from_pipeline_queue = 1; - wsi->http.rx_content_length = 0; - wsi->hdr_parsing_completed = 0; - - /* is there a new tail after removing that one? */ - wsi_eff = lws_client_wsi_effective(wsi); - - /* - * Do we have something pipelined waiting? - * it's OK if he hasn't managed to send his headers yet... he's next - * in line to do that... - */ - if (wsi_eff == wsi) { - /* - * Nothing pipelined... we should hang around a bit - * in case something turns up... - */ - lwsl_info("%s: nothing pipelined waiting\n", __func__); - lwsi_set_state(wsi, LRS_IDLING); - - lws_set_timeout(wsi, PENDING_TIMEOUT_CLIENT_CONN_IDLE, 5); + if (!n) return 0; - } /* * H1: we can serialize the queued guys into the same ah