TinyHttpd 源码剖析

HTTP

HTTP是封装在socket上面的,之前一直都是只接触web,用chrome看看network的各种信息
比如header或者response什么的,之前学过linux下的socket协议,想看看是到底怎么封装的
就去下了tinyhttp的源码

tinyhttpd版本

编译

我的环境是ubuntu 14.04 TLS,然后gcc版本是自己带的好像

修改代码里的内容
33行改为

void *accept_request(void *);

所以下面的实现也要修改下:

void *accept_request(void *tclient)
{
 int client = *(int *)tclient;

这个函数中两处return返回值改为

return NULL;

两行的变量类型改为socklen_t

socklen_t namelen = sizeof(name); // line 438

socklen_t client_name_len = sizeof(client_name); // line 483

main函数里修改pthread_create参数 和port
我开的是8080端口

u_short port = 8080;

if (pthread_create(&newthread , NULL, accept_request, (void*)&client_sock) != 0)

修改Makefile文件中的内容
gcc -W -Wall -o httpd httpd.c -lpthread

先不用管simpleclinet,然后就可以make编译了

其实主要是两个函数在作用catheaders
cat主要用来发送数据,用fgets每次从对应的文件读一个1024长度的buffer,然后send出去直到eof
headers很有意思,是发送http头,来看一下代码

void headers(int client, const char *filename)
{
 char buf[1024];
 (void)filename;  /* could use filename to determine file type */

 strcpy(buf, "HTTP/1.0 200 OK\r\n");
 send(client, buf, strlen(buf), 0);
 strcpy(buf, SERVER_STRING);
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "Content-Type: text/html\r\n");
 send(client, buf, strlen(buf), 0);
 strcpy(buf, "\r\n");
 send(client, buf, strlen(buf), 0);
}

其中那个SERVER_STRING是

#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"

基本上是http头的一些简易信息,然后method是包含在accept的请求的内容里面的
可以自己加一些输出信息,把接受到的请求收到,然后根据http协议处理

这是我从打开服务器到接受到GET请求和CGI的POST请求后的输出,输出都是自己加的

httpd running on port 8080
GET / HTTP/1.1

HTTP/1.0 200 OK
Server: jdbhttpd/0.1.0
Content-Type: text/html


GET / HTTP/1.1

HTTP/1.0 200 OK
Server: jdbhttpd/0.1.0
Content-Type: text/html

POST /color.cgi HTTP/1.1

有GET,请求方法,在最前面,所以先读method,然后是path,最后是http1.1是使用的协议

格式如下

METHOD PATH PROTOCOL

把三个分开,path就是你请求的url,method是数据传送的方式,protocol标记你用的哪个版本的http协议,中级用空格隔开

后面是服务端返回的response:
第一行有服务器http协议,状态码,和状态码代表的含义
第二行是Server的信息
第三行是Content-Type,text/html表示传输的html

返回的页面有个表单,我发了个POST请求,它调用的是它自己的.cgi文件,perl写的
你可以看到POST请求的信息,大概就有点清楚了

它的一些函数的实现

int get_line(int, char *, int)

我们来讨论一下get_line吧,get_line是用recv一次读一个字符然后读成一个buffer,在recv的第四个参数选择了MSG_PEEK这个值,它的含义是从recv到的buffer里取,但是不清空,就是它会一直在一个buffer取内容,取到空为止。
http协议要求\r\n作为分隔符,所以当你读到这两个字符代表一行内容已经结束,然后你可以再对内容处理,当然可能会有一些多余的空格需要你自己清理掉

int get_line(int sock, char *buf, int size)
{
 int i = 0;
 char c = '\0';
 int n;

 while ((i < size - 1) && (c != '\n'))
 {
  n = recv(sock, &c, 1, 0);
  /* DEBUG printf("%02X\n", c); */
  if (n > 0)
  {
   if (c == '\r')
   {
    n = recv(sock, &c, 1, MSG_PEEK);
    /* DEBUG printf("%02X\n", c); */
    if ((n > 0) && (c == '\n'))
     recv(sock, &c, 1, 0);
    else
     c = '\n';
   }
   buf[i] = c;
   i++;
  }
  else
   c = '\n';
 }
 buf[i] = '\0';

 return(i);
}

void serve_file(int, const char *)

文件读写和发送

void accept_request(void )

解析 HTTP 请求主要是依靠它,比如GET / HTTP/1.1这样的一行请求内容
那么用空格分割 method,urlpath 和 protocol,所以就要用空格分开

先读 method

while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
 {
  method[i] = buf[j];
  i++; j++;
 }
 method[i] = '\0';

然后是 urlpath

i = 0;
 while (ISspace(buf[j]) && (j < sizeof(buf)))
  j++;
 while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
 {
  url[i] = buf[j];
  i++; j++;
 }
 url[i] = '\0';

由于是比较简易的 server,protocol 那个数据我们就忽略了

cat

用 fgets 把文件内容读进 buffer,send 给客户端

HTTP 状态码

我们来看看400 bad request的实现

void bad_request(int client)
{
 char buf[1024];

 sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
 send(client, buf, sizeof(buf), 0);
 sprintf(buf, "Content-type: text/html\r\n");
 send(client, buf, sizeof(buf), 0);
 sprintf(buf, "\r\n");
 send(client, buf, sizeof(buf), 0);
 sprintf(buf, "<P>Your browser sent a bad request, ");
 send(client, buf, sizeof(buf), 0);
 sprintf(buf, "such as a POST without a Content-Length.\r\n");
 send(client, buf, sizeof(buf), 0);
}

看 buf 里的内容

首先第一行

protocol: HTTP/1.0
status code: 400
status message: BAD REQUEST

第二行

Content-Type: text/html

这行是为了告诉浏览器请按照 html 解析服务端发送的内容

第三行

一个<p>标签,里面有内容,告诉 client 它发送了一个坏请求,这句会显示在 html页面 上

为了方便快速重启

需要添加 socket 设置SO_REUSEADDR

可以在 startup 里添加

 if (setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof(int)) < 0)
    error_die("setsockopt(SO_REUSEADDR) failed");

短期重启就不会提示 bind addr in use 的 error

启动之后

之所以修改 accept_request 是因为函数是要传给 pthread_create,创建一个新线程,
你可以在 accept_request 最开头打印一句话,然后开启 server,不断刷新根目录,你就会看到 console 里不断打印你加的那句话,就是因为创建了一个线程,为了 http 快速处理请求而不是卡住

想自己写一个 http server ?

请参考我的另一篇博文「写一个简易的 HTTP Server」