源码地址https://github.com/zhuangqh/a...html
看到像这个给tinyhttpd写README的仓库都有1k star的时候,我真的好气?,因此我也写一个用c写HTTP静态文件服务器的教程,并且性能更好。git
c socket编程面向的是传输层。咱们在这一层上来收发HTTP报文。github
HTTP请求报文格式以下:编程
因为咱们是静态文件服务器,因此有效的请求报文是 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); }
服务器启动并监听的流程是这样的:首先调用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地址,我把要点和总体思路讲完,剩下就是看代码了?