免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
123下一页
最近访问板块 发新帖
查看: 22467 | 回复: 20
打印 上一主题 下一主题

[C] 以请求首页为例分析lighttpd处理HTTP请求的流程 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2010-01-06 17:43 |只看该作者 |倒序浏览
这篇小文不适合没读过lighttpd代码的人,文章里也不会讲网络编程内容。只适合读完converse版主那篇文章并且想知道lighttpd下一个流程的读者。由于水平有限,文章里肯定会有错误,还请大家指正……

在开始分析代码以前,先简单地说一下环境,lighttpd代码版本号为1.4.22,不是最新的1.5.0。lighttpd只配置了一个index.html,其它的什么都没有。至于index.html里就写了一个HELLO。lighttpd所在机器IP为192.168.0.137,端口配置为9090,并没有使用默认的80。

发送GET / HTTP/1.1\r\nHost: 192.168.0.139:9090\r\n\r\n请求到lighttpd的时候,lighttpd会返回首页内容,我在根目录下创建了index.html,所以在这里lighttpd默认的首页名字为index.html,这个可以在配置文件中查到。默认的配置如下,一共有四个文件可供选择。
  # files to check for if .../ is requested
  index-file.names            = ( "index.php", "index.html",
                                  "index.htm", "default.htm" )

另外还需要提到的是lighttpd加载的模块,在配置文件中我打开了mod_access,mod_cgi,mod_accesslog,而lighttpd在启动加载模块的时候会加载三个默认模块,这是一个隐晦的动作。mod_indexfile,mod_dirlisting,mod_staticfile。这样的话,最终lighttpd将加载五个模块:mod_access mod_indexfile,mod_cgi,mod_dirlisting,mod_staticfile。

前面的网络主体结构部分这里就不讲了,请大家去看lenky0401还有converse版主的贴子,里面已经讲得很详细了。这篇小文准备直接从connection_state_machine讲起。由于lighttpd1.4.22源码里有许多函数动辙上千行,肯定是没办法贴出整个函数的,只能把大致轮廓抠出来了。而且lighttpd1.4.22源里有不少case语句写得很长,但就是不加括号……1.4.22中的状态机状态没有1.5.0中多,而且这里也只涉及到 CON_STATE_REQUEST_END, CON_STATE_HANDLE_REQUEST,CON_STATE_RESPONSE_START, CON_STATE_WRITE。CON_STATE_REQUEST_START处理的是网络读取事务,这里不讲。

论坛徽章:
0
2 [报告]
发表于 2010-01-06 17:43 |只看该作者
1、CON_STATE_REQUEST_END,从字面意思就可以看出来,这代表请求完成了,该做回应了。

  1. 1395     case CON_STATE_REQUEST_END: /* transient */
  2. 1396       if (srv->srvconf.log_state_handling) {
  3. 1397         log_error_write(srv, __FILE__, __LINE__, "sds",
  4. 1398             "state for fd", con->fd, connection_get_state(con->state));
  5. 1399       }
  6. 1400
  7. 1401       if (http_request_parse(srv, con)) {
  8. 1402         /* we have to read some data from the POST request */
  9. 1403
  10. 1404         connection_set_state(srv, con, CON_STATE_READ_POST);
  11. 1405
  12. 1406         break;
  13. 1407       }
  14. 1408
  15. 1409       connection_set_state(srv, con, CON_STATE_HANDLE_REQUEST);
  16. 1410
  17. 1411       break;
复制代码


这段代码比较简短,http_request_parse解析HTTP请求,然后将状态置为CON_STATE_HANDLE_REQUEST。http_request_parse函数位于request.c当中。这个函数比较长,有接近一千行的规模。-_-! 这个函数由两个循环构成。

第一循环的作用是获取HTTP协议版本号,HTTP请求类型,请求的URI,并且会检查HTTP版本是否合法,URI是否含有非法字符。处理的流程是,寻找第一行中的空格,如果找到第一个空格,那么空格前的字符串为请求类型,记下当前位置。寻找第一行中的第二个空格,如果找到,那么空格前从上一个位置起的字符串为URI,这代表要请求的资源。如果在第一行中找到第三个空格,这代表出错了。-_-!!!

  1. 496     case ' ':
  2. 497       switch(request_line_stage) {
  3. 498       case 0:
  4. 499         /* GET|POST|... */
  5. 500         method = con->parse_request->ptr + first;
  6. 501         first = i + 1;
  7. 502         break;
  8. 503       case 1:
  9. 504         /* /foobar/... */
  10. 505         uri = con->parse_request->ptr + first;
  11. 506         first = i + 1;
  12. 507         break;
  13. 508       default:
  14. 509         /* ERROR, one space to much */
  15. 510         con->http_status = 400;
  16. 511         con->response.keep_alive = 0;
  17. 512         con->keep_alive = 0;
  18. 513
  19. 514         if (srv->srvconf.log_request_header_on_error) {
  20. 515           log_error_write(srv, __FILE__, __LINE__, "s", "overlong request line -> 400");
  21. 516           log_error_write(srv, __FILE__, __LINE__, "Sb",
  22. 517               "request-header:\n",
  23. 518               con->request.request);
  24. 519         }
  25. 520         return 0;
  26. 521       }
  27. 522
  28. 523       request_line_stage++; // 注意这一行
  29. 524       break;
复制代码


下一步是找到表示行结束的\r\n。

  1. switch(*cur) {
  2. 324     case '\r':
  3. 325       if (con->parse_request->ptr[i+1] == '\n') {
  4. 330         /* \r\n -> \0\0 */
  5. 331         con->parse_request->ptr[i] = '\0';
  6. 332         con->parse_request->ptr[i+1] = '\0';
  7. ....
  8. 349
  9. 350         proto = con->parse_request->ptr + first;
  10. 351
  11. 352         *(uri - 1) = '\0';  // 确定URI
  12. 353         *(proto - 1) = '\0'; // 确定协议
  13. ...
  14. 371         con->request.http_method = r; // 确定请求类型,为GET
  15. ...
  16. 487         buffer_copy_string_buffer(con->request.orig_uri, con->request.uri);
  17. 488
  18. 489         con->http_status = 0;
  19. 490
  20. 491         i++;
  21. 492         line++;  // 注意这一行,如果执行到这里,下轮for循环就退出
  22. 493         first = i+1;
  23. 494       }
  24. 495       break;
复制代码


第二个循环的作用是取HTTP头,同时还检查是否有非法字符。处理的流程是,先寻找:号,这个是分隔符,用来分隔头名称和值。代码中分别使用key和value来表示,另外使用is_key这个变量来表示是否已经找到了key。代码分为if(is_key) ... else 两块。is_key的初始值为1,所以代码一开始进入的是if(is_key)的部分,一直到下面的代码。

  1. 558       case ':':
  2. 559         is_key = 0;
  3. 560
  4. 561         value = cur + 1;
  5. 562
  6. 563         if (is_ws_after_key == 0) {
  7. 564           key_len = i - first;
  8. 565         }
  9. 566         is_ws_after_key = 0;
  10. 567
  11. 568         break;
复制代码


如果找到了:号,下一次循环进入的就是else部分了。这里说的只是正常情况,如果出错了或者有异常的话,就退出了。再看看else部分里的代码。

  1. 760             int s_len;
  2. 761             key = con->parse_request->ptr + first; // 取得key
  3. 762
  4. 763             s_len = cur - value;
  5. 764
  6. 765             /* strip trailing white-spaces */
  7. 766             for (; s_len > 0 &&
  8. 767                 (value[s_len - 1] == ' ' ||
  9. 768                  value[s_len - 1] == '\t'); s_len--);
  10. 769
  11. 770             value[s_len] = '\0'; // 取得value
  12. 771
复制代码


如果一行结束了,就把key和value存起来。

  1. 772             if (s_len > 0) {
  2. 773               int cmp = 0;
  3. 774               if (NULL == (ds = (data_string *)array_get_unused_element(con->request.headers, TYPE_STRING))) {
  4. 775                 ds = data_string_init();
  5. 776               }
  6. 777               buffer_copy_string_len(ds->key, key, key_len);
  7. 778               buffer_copy_string_len(ds->value, value, s_len);

  8. 把取到的HTTP头加入队列中。然后再对几个头做一些处理,"Connection", "Content-Length","Content-Type","Expect","Host","If-Modified-Since"等等……最后做一下初始化,进入下一轮循环。
  9. [code]
  10. 996           i++;
  11. 997           first = i+1;
  12. 998           is_key = 1;  // 注意这一行,恢复原始值
  13. 999           value = 0;
复制代码


循环结束以后,所有的HTTP头都读出来了。最后还要做一些后期工作,这里不详细讲了。到这里为止,这个复杂的函数就讲完了。接着上面的讲,现在设置状态为CON_STATE_HANDLE_REQUEST,要开始处理请求了。

[ 本帖最后由 李营长 于 2010-1-6 17:45 编辑 ]

论坛徽章:
0
3 [报告]
发表于 2010-01-06 17:47 |只看该作者
2、CON_STATE_HANDLE_REQUEST,处理HTTP请求
处理HTTP请求只调用一个函数http_response_prepare,当然了,这个函数也是不短,有500行的样子,复杂度没有上面的解析函数高,但是与其它源文件的关联度却要大得多。

  1. 1427       switch (r = http_response_prepare(srv, con)) {
复制代码


如果http_response_prepare函数调用成功了,就把HTTP状态设置为200,即OK状态。然后把状态机的状态改为CON_STATE_RESPONSE_START,开始回应。

  1. 1465         if (con->http_status == 0) con->http_status = 200;
  2. 1466
  3. 1467         /* we have something to send, go on */
  4. 1468         connection_set_state(srv, con, CON_STATE_RESPONSE_START);
复制代码


下面开始讲解http_response_prepare这个函数,这个函数位于response.c文件当中。这个函数前面有许多准备工作还有许多检查,我没有认真跟进去看过,这个函数还调了许多插件函数,我没有一一跟进去了,所以很多内容都略过了。我是从这个函数看起的。

  1. 645     switch(r = plugins_call_handle_subrequest_start(srv, con)) {
复制代码


plugins_call_handle_subrequest_start这个函数调用插件中的handle_subrequest_start函数指针,真正做了事情的是前面提到的mod_indexfile模块还有mod_staticfile模块,mod_indexfile模块中在mod_indexfile_plugin_init函数中将handle_subrequest_start 函数指针指向了
mod_indexfile_subrequest函数。

  1. 214   p->handle_subrequest_start = mod_indexfile_subrequest;
复制代码


那这个函数到底做了什么呢?它把配置文件中保存的四个首页名字取出来,一个个加到doc root后面,然后看看这个文件是否存在,如果存在就完成任务。

  1. 155   /* indexfile */
  2. 156   for (k = 0; k < p->conf.indexfiles->used; k++) {
  3. 157     data_string *ds = (data_string *)p->conf.indexfiles->data[k]; // 取出首页名
  4. 158
  5. 159     if (ds->value && ds->value->ptr[0] == '/') {
  6. 160       /* if the index-file starts with a prefix as use this file as
  7. 161        * index-generator */
  8. 162       buffer_copy_string_buffer(p->tmp_buf, con->physical.doc_root);
  9. 163     } else {
  10. 164       buffer_copy_string_buffer(p->tmp_buf, con->physical.path);
  11. 165     }
  12. 166     buffer_append_string_buffer(p->tmp_buf, ds->value); // 附加
  13. 167
  14. 168     if (HANDLER_ERROR == stat_cache_get_entry(srv, con, p->tmp_buf, &sce)) { // 检查
复制代码


下面将这个完整的文件名保存到phsical.path当中。

  1. 194     /* rewrite uri.path to the real path (/ -> /index.php) */
  2. 195     buffer_append_string_buffer(con->uri.path, ds->value);
  3. 196     buffer_copy_string_buffer(con->physical.path, p->tmp_buf);

  4. mod_staticfile模块中的mod_staticfile_plugin_init函数中将handle_subrequest_start 指针指向mod_staticfile_subrequest函数。
  5. [code]
  6. 540   p->handle_subrequest_start = mod_staticfile_subrequest;
复制代码


这个函数又做了什么呢?它的任务是填好回应包的HTTP头,并且把index.html添加到回应队列当中去。这个函数还涉及到cache,cache部分我一无所知,没有看过,就无能为力了。catche完以后,开始填HTTP头,包括以下几个部分:"Content-Type","Accept-Ranges","ETag","Last-Modified",最后将文件附加到回应队列里去。

  1. 522   /* we add it here for all requests
  2. 523    * the HEAD request will drop it afterwards again
  3. 524    */
  4. 525   http_chunk_append_file(srv, con, con->physical.path, 0, sce->st.st_size);
复制代码


在http_chunk_append_file函数中,会调用

  1. 65   chunkqueue_append_file(cq, fn, offset, len);
复制代码


而chunkqueue_append_file这个函数中会附加一个类型为FILE_CHUNK;的chunk结构到回应队列里。

  1. 168   c = chunkqueue_get_unused_chunk(cq);
  2. 169
  3. 170   c->type = FILE_CHUNK; // 注意这里
  4. 171
  5. 172   buffer_copy_string_buffer(c->file.name, fn);
  6. 173   c->file.start = offset;
  7. 174   c->file.length = len;
  8. 175   c->offset = 0;
  9. 176
  10. 177   chunkqueue_append_chunk(cq, c);
复制代码


这里为什么要对FILE_CHUNK特别说明一下,因为在发送数据的时候,如果是FILE_CHUNK这个类型会调一个单独的函数,这个等后面再详细讲了。

在调完 plugins_call_handle_subrequest以后,http_response_prepare函数就成功返回了。接着上面的讲,这个时候该改状态机的状态了。状态机的状态被改为CON_STATE_RESPONSE_START。

论坛徽章:
0
4 [报告]
发表于 2010-01-06 17:47 |只看该作者
营长研究怎么样了?

论坛徽章:
0
5 [报告]
发表于 2010-01-06 17:48 |只看该作者
3、CON_STATE_RESPONSE_START,到现在为止,已经准备好了要发送的HTTP头还有index.html文件内容,是否可以发送了呢?答案是否定的。还需要做一些其它工作才能准备好。

  1. 1496       /*
  2. 1497        * the decision is done
  3. 1498        * - create the HTTP-Response-Header
  4. 1499        *
  5. 1500        */
  6. 1501
  7. 1502       if (srv->srvconf.log_state_handling) {
  8. 1503         log_error_write(srv, __FILE__, __LINE__, "sds",
  9. 1504             "state for fd", con->fd, connection_get_state(con->state));
  10. 1505       }
  11. 1506
  12. 1507       if (-1 == connection_handle_write_prepare(srv, con)) {
  13. 1508         connection_set_state(srv, con, CON_STATE_ERROR);
  14. 1509
  15. 1510         break;
  16. 1511       }
  17. 1512
  18. 1513       connection_set_state(srv, con, CON_STATE_WRITE);
复制代码

connection_handle_write_prepare函数主要是写一下"Content-Length"这个HTTP头,然后调用一个函数

  1. 590   http_response_write_header(srv, con);
复制代码

其它的工作都在http_response_write_header函数当中完成,http_response_write_header函数位于response.c当中。这个函数写的是前面一些东西,其实就是HTTP/1.1 200 OK这一行。然后再检查几个HTTP头,如果没有的话就添加一份。

  1. 35   b = chunkqueue_get_prepend_buffer(con->write_queue);
  2. 36
  3. 37   if (con->request.http_version == HTTP_VERSION_1_1) {
  4. 38     buffer_copy_string_len(b, CONST_STR_LEN("HTTP/1.1 ")); // HTTP/1.1
  5. 39   } else {
  6. 40     buffer_copy_string_len(b, CONST_STR_LEN("HTTP/1.0 "));
  7. 41   }
  8. 42   buffer_append_long(b, con->http_status);        // 200
  9. 43   buffer_append_string_len(b, CONST_STR_LEN(" "));
  10. 44   buffer_append_string(b, get_http_status_name(con->http_status)); // OK
复制代码

下面开始添加前面遗漏的HTTP头,Date还有Server。

  1. 86   if (!have_date) {
  2. 87     /* HTTP/1.1 requires a Date: header */
  3. 88     buffer_append_string_len(b, CONST_STR_LEN("\r\nDate: "));
  4. 89
  5. 90     /* cache the generated timestamp */
  6. 91     if (srv->cur_ts != srv->last_generated_date_ts) {
  7. 92       buffer_prepare_copy(srv->ts_date_str, 255);
  8. 93
  9. 94       strftime(srv->ts_date_str->ptr, srv->ts_date_str->size - 1,
  10. 95          "%a, %d %b %Y %H:%M:%S GMT", gmtime(&(srv->cur_ts)));
  11. 96
  12. 97       srv->ts_date_str->used = strlen(srv->ts_date_str->ptr) + 1;
  13. 98
  14. 99       srv->last_generated_date_ts = srv->cur_ts;
  15. 100     }
  16. 101

  17. 105   if (!have_server) {
  18. 106     if (buffer_is_empty(con->conf.server_tag)) {
  19. 107       buffer_append_string_len(b, CONST_STR_LEN("\r\nServer: " PACKAGE_NAME "/" PACKAGE_VERSION));
  20. 108     } else if (con->conf.server_tag->used > 1) {
  21. 109       buffer_append_string_len(b, CONST_STR_LEN("\r\nServer: "));
  22. 110       buffer_append_string_encoded(b, CONST_BUF_LEN(con->conf.server_tag), ENCODING_HTTP_HEADER);
  23. 111     }
  24. 112   }
  25. 113
  26. 114   buffer_append_string_len(b, CONST_STR_LEN("\r\n\r\n"));
  27. 115
复制代码

好了,到此为止,所有的数据都已经准备齐全了,可以发送了。终于可以把状态设成CON_STATE_WRITE了。

论坛徽章:
0
6 [报告]
发表于 2010-01-06 17:49 |只看该作者
4、
CON_STATE_WRITE,这个地方只有一点需要讲一下,那就是发送文件的时候lighttpd做了一个单独处理。
connection_handle_write->network_write_chunkqueue-> srv->network_backend_write,好了, srv->network_backend_write这个地方就是我要讲一下的。这又是一个绕来绕去的过程,确实比较痛苦……首先要从network.c说起……network.c中有一个函数名为network_init,这个函数是在server.c当中被main函数调用的。在network_init函数当中,有一个名为network_backends的数组

  1. 474   struct nb_map {
  2. 475     network_backend_t nb;
  3. 476     const char *name;
  4. 477   } network_backends[] = {
  5. 478     /* lowest id wins */
  6. 479 #if defined USE_LINUX_SENDFILE
  7. 480     { NETWORK_BACKEND_LINUX_SENDFILE,       "linux-sendfile" },
  8. 481 #endif
  9. 482 #if defined USE_FREEBSD_SENDFILE
  10. 483     { NETWORK_BACKEND_FREEBSD_SENDFILE,     "freebsd-sendfile" },
  11. 484 #endif
  12. 485 #if defined USE_SOLARIS_SENDFILEV
  13. 486     { NETWORK_BACKEND_SOLARIS_SENDFILEV,  "solaris-sendfilev" },
  14. 487 #endif
  15. 488 #if defined USE_WRITEV
  16. 489     { NETWORK_BACKEND_WRITEV,   "writev" },
  17. 490 #endif
  18. 491     { NETWORK_BACKEND_WRITE,    "write" },
  19. 492     { NETWORK_BACKEND_UNSET,          NULL }
  20. 493   };
复制代码

因为我用的是debian,所以是480行那一项了。这一项位于数组0的位置。再往下看

  1. 510   /* get a usefull default */
  2. 511   backend = network_backends[0].nb;
复制代码

backend赋值了,赋成了NETWORK_BACKEND_LINUX_SENDFILE。这还没完,还有下文,请不要着急-_-!!!
正主终于上场了……


  1. 533   switch(backend) {
  2. 534   case NETWORK_BACKEND_WRITE:
  3. 535     srv->network_backend_write = network_write_chunkqueue_write;
  4. 536     break;
  5. 537 #ifdef USE_WRITEV
  6. 538   case NETWORK_BACKEND_WRITEV:
  7. 539     srv->network_backend_write = network_write_chunkqueue_writev;
  8. 540     break;
  9. 541 #endif
  10. 542 #ifdef USE_LINUX_SENDFILE
  11. 543   case NETWORK_BACKEND_LINUX_SENDFILE:
  12. 544     srv->network_backend_write = network_write_chunkqueue_linuxsendfile; // 请注意
  13. 545     break;
  14. 546 #endif
  15. 547 #ifdef USE_FREEBSD_SENDFILE
  16. 548   case NETWORK_BACKEND_FREEBSD_SENDFILE:
  17. 549     srv->network_backend_write = network_write_chunkqueue_freebsdsendfile;
  18. 550     break;
  19. 551 #endif
  20. 552 #ifdef USE_SOLARIS_SENDFILEV
  21. 553   case NETWORK_BACKEND_SOLARIS_SENDFILEV:
  22. 554     srv->network_backend_write = network_write_chunkqueue_solarissendfilev;
  23. 555     break;
  24. 556 #endif
  25. 557   default:
  26. 558     return -1;
  27. 559   }

复制代码

也就是说前面看到的那个network_backend_write函数其实就是network_write_chunkqueue_linuxsendfile; 这个函数位于linux_network_sendfile.c文件里面。在发送内容里,它会判断chunk类型,因为这里只针对index.html这个FILE_CHUNK类型,所以贴一下FILE_CHUNK部分的代码。

  1. 129     case FILE_CHUNK: {
  2. 130       ssize_t r;
  3. 131       off_t offset;
  4. 132       size_t toSend;
  5. 133       stat_cache_entry *sce = NULL;
  6. 134
  7. 135       offset = c->file.start + c->offset;
  8. 136       /* limit the toSend to 2^31-1 bytes in a chunk */
  9. 137       toSend = c->file.length - c->offset > ((1 << 30) - 1) ?
  10. 138         ((1 << 30) - 1) : c->file.length - c->offset;
  11. 139
  12. 140       /* open file if not already opened */
  13. 141       if (-1 == c->file.fd) {
  14. 142         if (-1 == (c->file.fd = open(c->file.name->ptr, O_RDONLY))) { // 请注意
  15. 143           log_error_write(srv, __FILE__, __LINE__, "ss", "open failed: ", strerror(errno));
  16. 144
  17. 145           return -1;
  18. 146         }
  19. 147 #ifdef FD_CLOEXEC
  20. 148         fcntl(c->file.fd, F_SETFD, FD_CLOEXEC);
  21. 149 #endif
  22. 150 #ifdef HAVE_POSIX_FADVISE
  23. 151         /* tell the kernel that we want to stream the file */
  24. 152         if (-1 == posix_fadvise(c->file.fd, 0, 0, POSIX_FADV_SEQUENTIAL)) {
  25. 153           if (ENOSYS != errno) {
  26. 154             log_error_write(srv, __FILE__, __LINE__, "ssd",
  27. 155               "posix_fadvise failed:", strerror(errno), c->file.fd);
  28. 156           }
  29. 157         }
  30. 158 #endif
  31. 159       }
  32. 160
  33. 161       if (-1 == (r = sendfile(fd, c->file.fd, &offset, toSend))) { // 请注意
复制代码

由此可见,最终调的是sendfile来发送文件内容。

到这里为止,终于把流程讲完了。

论坛徽章:
0
7 [报告]
发表于 2010-01-06 17:50 |只看该作者
原帖由 5毛党党员 于 2010-1-6 17:47 发表
营长研究怎么样了?

研究得想吐

论坛徽章:
0
8 [报告]
发表于 2010-01-06 20:20 |只看该作者
支持!

论坛徽章:
0
9 [报告]
发表于 2010-01-07 09:58 |只看该作者
原帖由 李营长 于 2010-1-6 17:50 发表

研究得想吐

看c语言代码有时确实是很恶心,有些东西想跳过却不知道跳到哪里为止。

论坛徽章:
0
10 [报告]
发表于 2010-01-08 08:15 |只看该作者
支持,看代码必须动态地看。静态是很难看下去的。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP