动手用c写一个HTTP服务器

动手用c写一个HTTP服务器

源码地址https://github.com/zhuangqh/a...html

看到像这个给tinyhttpd写README的仓库都有1k star的时候,我真的好气?,因此我也写一个用c写HTTP静态文件服务器的教程,并且性能更好。git

c socket编程面向的是传输层。咱们在这一层上来收发HTTP报文。github

HTTP请求报文格式以下:编程

clipboard.png

因为咱们是静态文件服务器,因此有效的请求报文是 GET url 的格式。咱们只要解析这个url,而后发送对应的文件就OK了。这个是基本的思路。数组

函数包装

我仿照UNP中对函数进行包装的方式。对基础函数进行包装,在代码中只使用包装过的函数。
UNIX函数大多会将函数的调用状态做为返回值。如Socket函数,若是返回值小于零,则是调用出错,这种状况咱们直接结束进程并报错。浏览器

int
Socket(int family, int type, int protocol)
{
    int        n;

    if ( (n = socket(family, type, protocol)) < 0)
        err_sys("socket error");
    return(n);
}

监听并处理请求

clipboard.png

服务器启动并监听的流程是这样的:首先调用socket()建立一个服务端的套接字,而后使用bind()将套接字绑定在一个指定的端口上。调用listen()将套接字从CLOSED状态转换到LISTEN状态。而accept会返回已链接队列的对头。咱们对accept返回的描述符的读写就是对客户端的收发操做。服务器

这篇教程选用的并发模型是线程池,每一个线程分别accept的形式。并发

ReqHandler这个struct存放的是对客户端描述符的处理函数和在pthread_t数组的下标。下标用于后面的pthread_createsocket

主进程在建立完线程后任务就完成了,因此它一直阻塞等待就好。函数

tptr = Calloc(THREAD_NUM, sizeof(pthread_t));

// tptr是一个pthread_t的数组。在启动的时候可给出线程池线程的数量,不指明则使用默认值8。
ReqHandler rh;
rh.handler = accept_request;
for (int i = 0; i < nthreads; i++) {
rh.index = i;
thread_make(rh);
}

for ( ; ; )
pause();    // everything done by threads

在线程建立时,将ReqHandler里的请求处理函数传递给它

Pthread_create(&tptr[rh.index], NULL, &thread_main, (void *) (rh.handler));

在各个线程中分别accept,这个有个问题,他们不该该同时accept。因此咱们在进入accept这个函数前加上互斥锁。

Pthread_mutex_lock(&mmlock);
connfd = Accept(listenfd, cliaddr, &clilen);
Pthread_mutex_unlock(&mmlock);

处理请求

在服务器搭起来以后,咱们就能够干正事了。accept_request这个函数解析出HTTP的请求方法和URL并做出响应。

咱们知道HTTP的每一行是以/r/n结束,那么getline该怎么作呢?一个字符一个字符地读,并逐一判断是否为/r/n序列的方法显然比较慢。因此咱们作一个本身的缓冲区。预先在客户端描述符connfd读入多个字符。再在缓冲区里一个字符一个字符地判断。缓冲区读完后,再读一次connfd。这样能大大减小读取connfd的次数。

请求方法错误处理

获取到HTTP的请求方法后,若是方法不是GET,咱们直接返回501错误。说明这个方法咱们尚未实现。

可能有人会对下面这种写法感到疑惑。其实编译器在作预处理的时候会把连着的字符串合并的。因此下面这种写法跟写在一对双引号里是同样的。

void
unimplemented(int sockfd)
{
  char msg[] =
  "HTTP/1.1 501 Method Not Implemented\r\n"
  SERVER_STRING
  "Content-Type: text/plain\r\n"
  "\r\n"
  "method not implemented\r\n";

  Write(sockfd, msg, strlen(msg));
}

请求文件不存在处理

而后判断URL的文件是否存在。这里咱们多作了一步处理,若是URL是以/结尾的,浏览器会自动给它加上index.html,因此咱们也按照这个来。

咱们用open的形式打开文件,而不是标准IO的fopen。open能拿到该文件的描述符。这在咱们下一步传输文件时比较方便。若是文件不存在,直接返回404。

void
serve_file(int sockfd, const char *filepath)
{
  int filefd = open(filepath, O_RDONLY); // open file for read

  if (filefd == -1) {
    not_found(sockfd);
  } else {
    set_header(sockfd, filepath);
    send_file(sockfd, filefd);
    Close(filefd);
  }
}

传输文件

向客户端发送文件,还得设置好响应报文中Content-Type的值,告诉对方这是一个什么文件。这里咱们须要一张表,根据文件的后缀名查询Content-Type。天然是使用Hash表,冲突用链表的形式解决。具体请看源码。

传输文件时,就是一对read write,UNIX一切皆文件的优雅就此体现。

void
send_file(int sockfd, int filefd)
{
  char buf[MAXLINE];
  int cnt = 0;

  while ((cnt = read(filefd, buf, MAXLINE)) > 0) {
    Write(sockfd, buf, cnt);
  }
}

具体代码请查看文章开头的github地址,我把要点和总体思路讲完,剩下就是看代码了?

相关文章
相关标签/搜索