This patch adds chunked encoding for IIS responses - i.e. Transfer-Encoding: chunked responses for HTTP/1.1 clients - allowing IIS to maintain persistent connections to HTTP/1.1 clients through the ISAPI redirector.

IIS hands a lot of the responsibility for HTTP protocol handling to the ISAPI extension - Apache is sooo much nicer to deal with. IIS will keep connections alive if there is a Content-Length specified in the request, but won't do transfer encoding for requests that don't like Apache does.

Contents of the patch:
- Added 'enable_chunked_encoding' param (bool) to enable chunked encoding (default is disabled) - Modified isapi_private_data_t to track whether chunked encoding is enabled per request
- Modified start_response to:
- Detect HTTP 1.1 clients and enable chunked encoding for the response (if the Tomcat response hasn't already precluded that) - Determine whether IIS should be told to keep the connection alive (IIS makes us manage this as well if we want to have the ability to keep connections)
   - Set the Transfer-Encoding: chunked header when used
- Use the HSE_SEND_HEADER_EX_INFO/ServerSupportFunction API to allow us to tell IIS to keep the connections open
- Factored the WriteClient calls into a new function (for the new write())
- Added support in write() for writing a chunked encoded response chunk
- Added a flush hook for IIS (with flush_packets false) to allow the chunked encoded terminator (the 0 chunk) to be written on response completion

I'd consider the chunked encoding experimental at the moment - we're beta testing at the moment - which is why it's disabled by default. We've run this against fairly complex web applications, with a mix of jsp, htm, xml, Content-Length, no Content-Length and some Connection: close behaviour, IE, Firefox etc. All seems to work fine at the moment - chunked encoding isn't that complex - though IE does get confused if there are non-chunk-encoded files in the cache. The only reported failure is when using Fiddler with an HTTP/1.0 client, though I suspect this is Fiddlers problem.
Straight HTTP/1.0 from IE works fine.

It'd be great to get this into the codebase and have people beta test it to see if if any problems show up. Any problems are likely to be the result of incorrect negotiation of keep-alive or incorrect detection of responses from Tomcat that preclude using chunked encoding.

cheers
tim
Index: jk/native/common/jk_util.c
===================================================================
RCS file: /home/cvspublic/jakarta-tomcat-connectors/jk/native/common/jk_util.c,v
retrieving revision 1.69
diff -u -r1.69 jk_util.c
--- jk/native/common/jk_util.c  15 May 2005 15:22:05 -0000      1.69
+++ jk/native/common/jk_util.c  9 Jun 2005 03:13:30 -0000
@@ -1230,6 +1230,8 @@
     s->num_attributes = 0;
     s->jvm_route = NULL;
     s->retries = JK_RETRIES;
+    s->flush = NULL;
+    s->flush_packets = JK_FALSE;
 }
 
 #ifdef _MT_CODE_PTHREAD
Index: jk/native/iis/jk_isapi_plugin.c
===================================================================
RCS file: 
/home/cvspublic/jakarta-tomcat-connectors/jk/native/iis/jk_isapi_plugin.c,v
retrieving revision 1.49
diff -u -r1.49 jk_isapi_plugin.c
--- jk/native/iis/jk_isapi_plugin.c     18 May 2005 18:04:53 -0000      1.49
+++ jk/native/iis/jk_isapi_plugin.c     9 Jun 2005 04:38:46 -0000
@@ -20,6 +20,7 @@
  * Author:      Larry Isaacs <[EMAIL PROTECTED]>                           *
  * Author:      Ignacio J. Ortega <[EMAIL PROTECTED]>                       *
  * Author:      Mladen Turk <[EMAIL PROTECTED]>                             *
+ * Author:      Tim Whittington <[EMAIL PROTECTED]           *
  * Version:     $Revision: 1.49 $                                          *
  ***************************************************************************/
 
@@ -69,6 +70,15 @@
 #define URI_SELECT_UNPARSED_VERB    ("unparsed")
 #define URI_SELECT_ESCAPED_VERB     ("escaped")
 
+#define ENABLE_CHUNKED_ENCODING_TAG ("enable_chunked_encoding")
+
+/* Headers used in negotiating chunked encoding */
+#define TRANSFER_ENCODING_HEADER_COMPLETE ("Transfer-Encoding: chunked")
+#define TRANSFER_ENCODING_HEADER_NAME ("Transfer-Encoding")
+#define CONTENT_LENGTH_HEADER_NAME ("Content-Length")
+#define CONNECTION_HEADER_NAME ("Connection")
+#define CONNECTION_CLOSE_VALUE ("Close")
+
 #define BAD_REQUEST     -1
 #define BAD_PATH        -2
 #define MAX_SERVERNAME  128
@@ -115,6 +125,9 @@
         (place) = def;                                      \
   } } while(0)
 
+/* HTTP protocol CRLF */
+static char CRLF[3] = { (char)13, (char)10, '\0' };
+
 static char ini_file_name[MAX_PATH];
 static int using_ini_file = JK_FALSE;
 static int is_inited = JK_FALSE;
@@ -133,6 +146,8 @@
 static int log_level = JK_LOG_EMERG_LEVEL;
 static char worker_file[MAX_PATH * 2];
 static char worker_mount_file[MAX_PATH * 2] = {0};
+/* Whether chunked encoding has been enabled in the settings */
+static int chunked_encoding_enabled = JK_FALSE;
 
 #define URI_SELECT_OPT_PARSED       0
 #define URI_SELECT_OPT_UNPARSED     1
@@ -150,6 +165,7 @@
     int request_started;
     unsigned int bytes_read_so_far;
     LPEXTENSION_CONTROL_BLOCK lpEcb;
+    int chunk_content; /* Whether we're responding with Transfer-Encoding: 
chunked content */
 };
 
 typedef struct isapi_log_data_t isapi_log_data_t;
@@ -170,6 +186,8 @@
 
 static int JK_METHOD write(jk_ws_service_t *s, const void *b, unsigned int l);
 
+static int JK_METHOD flush_response(jk_ws_service_t *s);
+
 static int init_ws_service(isapi_private_data_t * private_data,
                            jk_ws_service_t *s, char **worker_name);
 
@@ -469,8 +487,6 @@
                                     const char *const *header_values,
                                     unsigned int num_of_headers)
 {
-    static char crlf[3] = { (char)13, (char)10, '\0' };
-
     JK_TRACE_ENTER(logger);
     if (status < 100 || status > 1000) {
         jk_log(logger, JK_LOG_ERROR,
@@ -482,10 +498,13 @@
 
     if (s && s->ws_private) {
         isapi_private_data_t *p = s->ws_private;
+        int keep_alive = JK_FALSE;         /* Whether the downstream or us can 
supply content length */
+        
         if (!p->request_started) {
-            size_t len_of_status;
+            HSE_SEND_HEADER_EX_INFO send_header_ex_info;
             char *status_str;
             char *headers_str;
+            int keep_alive = JK_FALSE;     /* Whether the downstream or us can 
supply content length */
 
             p->request_started = JK_TRUE;
 
@@ -497,46 +516,113 @@
             }
             status_str = (char *)_alloca((6 + strlen(reason)) * sizeof(char));
             sprintf(status_str, "%d %s", status, reason);
-            len_of_status = strlen(status_str);
 
             /*
              * Create response headers string
              */
             if (num_of_headers) {
+                int chunked_ok = JK_FALSE;         /* Whether the downstream 
response allows chunking */
+                int http11_request = JK_FALSE; /* Whether the client is 
HTTP/1.1 */
+
                 size_t i, len_of_headers;
+
+                /* Check if we've got an HTTP/1.1 response */
+                jk_log(logger, JK_LOG_DEBUG, 
+                    "jk_ws_service_t::start_response, Request uses protocol 
%s\n", s->protocol);
+
+                if (strcasecmp(s->protocol, "HTTP/1.1")==0) {
+                    http11_request = JK_TRUE;
+                    chunked_ok = JK_TRUE;      /* Chunking only on HTTP/1.1 */
+                }
+
                 for (i = 0, len_of_headers = 0; i < num_of_headers; i++) {
                     len_of_headers += strlen(header_names[i]);
                     len_of_headers += strlen(header_values[i]);
                     len_of_headers += 4;        /* extra for colon, space and 
crlf */
                 }
 
+                /* Provide room in the buffer for the Transfer-Encoding header 
if we use it. */
+                len_of_headers += strlen(TRANSFER_ENCODING_HEADER_COMPLETE) + 
2;
+
                 len_of_headers += 3;    /* crlf and terminating null char */
                 headers_str = (char *)_alloca(len_of_headers * sizeof(char));
                 headers_str[0] = '\0';
 
                 for (i = 0; i < num_of_headers; i++) {
+                    /* Check the downstream response to see whether 
+                    it's appropriate the chunk the response content
+                    and whether it supports keeping the connection open */
+                    if(strcasecmp(TRANSFER_ENCODING_HEADER_NAME, 
header_names[i])==0) {
+                        keep_alive = http11_request;
+                        chunked_ok = JK_FALSE;
+                    }
+                    else if(strcasecmp(CONTENT_LENGTH_HEADER_NAME, 
header_names[i])==0) {
+                        keep_alive = http11_request;
+                        chunked_ok = JK_FALSE;
+                    } 
+                    else if((strcasecmp(CONNECTION_HEADER_NAME, 
header_names[i])==0)
+                            && (strcasecmp(CONNECTION_CLOSE_VALUE, 
header_values[i])==0)) {
+                        keep_alive = JK_FALSE;
+                        chunked_ok = JK_FALSE;
+                    }
+
                     strcat(headers_str, header_names[i]);
                     strcat(headers_str, ": ");
                     strcat(headers_str, header_values[i]);
-                    strcat(headers_str, crlf);
+                    strcat(headers_str, CRLF);
                 }
-                strcat(headers_str, crlf);
+
+                /* Check if we can send chunked content */
+                if (chunked_encoding_enabled && chunked_ok) {
+                    jk_log(logger, JK_LOG_DEBUG, 
+                        "jk_ws_service_t::start_response, using 
Transfer-Encoding: chunked\n");
+
+                    /** We will supply the transfer-encoding to allow IIS to 
keep the connection open */
+                    keep_alive = JK_TRUE;
+
+                    p->chunk_content = JK_TRUE;
+                    /* Indicate to the client that the content will be chunked
+                    - We've already reserved space for this */
+                    strcat(headers_str, TRANSFER_ENCODING_HEADER_COMPLETE);
+                    strcat(headers_str, CRLF);
+                } 
+                else {
+                    p->chunk_content = JK_FALSE;
+                }
+
+                strcat(headers_str, CRLF);
             }
             else {
-                headers_str = crlf;
+                headers_str = CRLF;
             }
 
-            if (!p->lpEcb->ServerSupportFunction(p->lpEcb->ConnID,
-                                                 HSE_REQ_SEND_RESPONSE_HEADER,
-                                                 status_str,
-                                                 (LPDWORD) &len_of_status,
-                                                 (LPDWORD) headers_str)) {
-                jk_log(logger, JK_LOG_ERROR,
-                       "HSE_REQ_SEND_RESPONSE_HEADER failed");
+            /** Fill in the response */
+            send_header_ex_info.pszStatus = status_str;
+            send_header_ex_info.pszHeader = headers_str;
+            send_header_ex_info.cchStatus = strlen(status_str);
+            send_header_ex_info.cchHeader = strlen(headers_str);
+
+            /* 
+             * Using the extended form of the API means we have to get this 
right,
+             * i.e. IIS won't keep connections open if there's a 
Content-Length and close them if there isn't.
+             */
+            jk_log(logger, JK_LOG_DEBUG, 
+                "jk_ws_service_t::start_response, keep_alive = %d\n", 
keep_alive);
+
+            send_header_ex_info.fKeepConn = keep_alive;
+
+            if (!p->lpEcb->ServerSupportFunction(p->lpEcb->ConnID, 
+                                                 
HSE_REQ_SEND_RESPONSE_HEADER_EX,
+                                                 &send_header_ex_info,
+                                                 NULL,
+                                                 NULL)) {
+                jk_log(logger, JK_LOG_ERROR, 
+                    "HSE_REQ_SEND_RESPONSE_HEADER failed");
                 JK_TRACE_EXIT(logger);
                 return JK_FALSE;
-            }
+            }       
         }
+
         JK_TRACE_EXIT(logger);
         return JK_TRUE;
 
@@ -602,6 +688,38 @@
     return JK_FALSE;
 }
 
+/*
+ * Writes a buffer to the ISAPI response.
+ */
+static int isapi_write_client(isapi_private_data_t *p, const char *buf, 
unsigned write_length)
+{
+    unsigned written = 0;           
+    DWORD try_to_write = 0;
+
+    JK_TRACE_ENTER(logger);
+
+    while (written < write_length) {
+        try_to_write = (write_length - written);
+        if (!p->lpEcb->WriteClient(p->lpEcb->ConnID, 
+                                   buf + written, &try_to_write, 0)) {
+            jk_log(logger, JK_LOG_ERROR,
+                    "WriteClient failed with %08x", GetLastError());
+            JK_TRACE_EXIT(logger);
+            return JK_FALSE;
+        }
+        written += try_to_write;
+    }
+    JK_TRACE_EXIT(logger);
+    return JK_TRUE;
+}
+
+/* 
+ * Write content to the response.
+ * If chunked encoding has been enabled and the client supports it 
+ *(and it's appropriate for the response), then this will write a
+ * single "Transfer-Encoding: chunked" chunk
+ * 
+ */
 static int JK_METHOD write(jk_ws_service_t *s, const void *b, unsigned int l)
 {
     JK_TRACE_ENTER(logger);
@@ -610,29 +728,87 @@
         isapi_private_data_t *p = s->ws_private;
 
         if (l) {
-            unsigned int written = 0;
             char *buf = (char *)b;
 
             if (!p->request_started) {
                 start_response(s, 200, NULL, NULL, NULL, 0);
             }
 
-            while (written < l) {
-                DWORD try_to_write = l - written;
-                if (!p->lpEcb->WriteClient(p->lpEcb->ConnID,
-                                           buf + written, &try_to_write, 0)) {
-                    jk_log(logger, JK_LOG_ERROR,
-                           "WriteClient failed with %08x", GetLastError());
+            /* Chunk header */
+            if (p->chunk_content) {
+                char chunk_header[sizeof(unsigned)*2+3]; /* Hex of chunk 
length + CRLF + term. */
+                jk_log(logger, JK_LOG_DEBUG, 
+                    "Using chunked encoding - writing chunk header\n");
+
+                /* Construct chunk header : HEX CRLF*/
+                sprintf(chunk_header, "%X%s", l, CRLF);
+
+                if (!isapi_write_client(p, chunk_header, 
strlen(chunk_header))) {
+                    jk_log(logger, JK_LOG_ERROR, 
+                        "WriteClient for chunk header failed\n");
+                    JK_TRACE_EXIT(logger);
+                    return JK_FALSE;
+                }
+            }
+
+            /* Write chunk body */
+            if (!isapi_write_client(p, buf, l)) {
+                jk_log(logger, JK_LOG_ERROR, 
+                    "WriteClient for body chunk failed\n");
+                JK_TRACE_EXIT(logger);
+                return JK_FALSE;
+            }
+
+            /* Write chunk trailer */
+            if (p->chunk_content) {
+                jk_log(logger, JK_LOG_DEBUG, 
+                    "Using chunked encoding - writing chunk trailer\n");
+
+                if (!isapi_write_client(p, CRLF, strlen(CRLF))) {
+                    jk_log(logger, JK_LOG_ERROR, 
+                        "WriteClient for chunk trailer failed\n");
                     JK_TRACE_EXIT(logger);
                     return JK_FALSE;
                 }
-                written += try_to_write;
             }
         }
 
         JK_TRACE_EXIT(logger);
         return JK_TRUE;
+    }
+    
+    JK_LOG_NULL_PARAMS(logger);
+    JK_TRACE_EXIT(logger);
+    return JK_FALSE;
+}
+
+/**
+ * In the case of a Transfer-Encoding: chunked response, this will write the 
terminator chunk.
+ */
+static int JK_METHOD flush_response(jk_ws_service_t *s)
+{
+    JK_TRACE_ENTER(logger);
 
+    if (s && s->ws_private) {
+        isapi_private_data_t *p = s->ws_private;
+
+        /* Write last chunk + terminator */
+        if (p->chunk_content) {
+            static char CHUNKED_ENCODING_TRAILER[6] = { '0', (char)13, 
(char)10, (char)13, (char)10, '\0' };
+
+            jk_log(logger, JK_LOG_DEBUG, 
+                "Terminating chunk encoded response.\n");
+
+            if (!isapi_write_client(p, CHUNKED_ENCODING_TRAILER, 
strlen(CHUNKED_ENCODING_TRAILER))) {
+                jk_log(logger, JK_LOG_ERROR, 
+                    "WriteClient for chunk response terminator failed\n");
+                JK_TRACE_EXIT(logger);
+                return JK_FALSE;
+            }
+        }
+        
+        JK_TRACE_EXIT(logger);
+        return JK_TRUE;
     }
 
     JK_LOG_NULL_PARAMS(logger);
@@ -992,6 +1168,7 @@
         private_data.request_started = JK_FALSE;
         private_data.bytes_read_so_far = 0;
         private_data.lpEcb = lpEcb;
+        private_data.chunk_content = JK_FALSE;
 
         s.ws_private = &private_data;
         s.pool = &private_data.p;
@@ -1139,6 +1316,7 @@
         jk_log(logger, JK_LOG_DEBUG, "Using worker mount file %s.",
                worker_mount_file);
         jk_log(logger, JK_LOG_DEBUG, "Using uri select %d.", 
uri_select_option);
+        jk_log(logger, JK_LOG_DEBUG, "Using chunked encoding? %d.\n", 
chunked_encoding_enabled);
     }
     if (uri_worker_map_alloc(&uw_map, NULL, logger)) {
         rc = JK_FALSE;
@@ -1256,6 +1434,7 @@
                 ok = JK_FALSE;
             }
         }
+        chunked_encoding_enabled = jk_map_get_bool(map, 
ENABLE_CHUNKED_ENCODING_TAG, JK_FALSE);
 
     }
     else {
@@ -1323,6 +1502,15 @@
             }
         }
 
+        if (get_registry_config_parameter(hkey,
+                                          ENABLE_CHUNKED_ENCODING_TAG, 
+                                          tmpbuf, sizeof(tmpbuf))) {
+            if (strcasecmp(tmpbuf, "true") == 0 ||
+                *tmpbuf == 'Y' || *tmpbuf == 'y' || *tmpbuf == '1') {
+                    chunked_encoding_enabled = JK_TRUE;
+            }
+        }
+
         RegCloseKey(hkey);
     }
     return ok;
@@ -1356,7 +1544,10 @@
     s->start_response = start_response;
     s->read = read;
     s->write = write;
-    s->flush = NULL;
+
+    /* We want to flush at end of content to terminate chunked encoding */
+    s->flush = flush_response;
+    s->flush_packets = JK_FALSE;    
 
     /* Clear RECO status */
     s->reco_status = RECO_NONE;
---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to