tiny web服务器源码分析

正如csapp书中所记,在短短250行代码中,它结合了许多我们已经学习到的思想,如进程控制,unix I/O,套接字接口和HTTP。虽然它缺乏一个实际服务器所具备的功能性,健壮性和安全性,但是它足够用来为实际的web浏览器提供静态和动态的内容。我们鼓励你研究它,并且自己实现它,将一个实际的浏览器指向你自己的服务器,看着它显示一个复杂的带有文本和图片的web页面,真是非常令人兴奋。接下来就看我们能从这之中发掘出什么来。

头文件及声明

#ifndef __CSAPP_H__
#define __CSAPP_H__
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <setjmp.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <math.h>
#include <semaphore.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/* 默认的文件访问权限为 DEF_MODE & ~DEF_UMASK */
#define DEF_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define DEF_UMASK S_IWGRP|S_IWOTH
typedef struct sockaddr SA;
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd; /* 内部缓存区的描述符 */
int rio_cnt; /* 内部缓存区剩下还未读的字节数 */
char *rio_bufptr; /* 指向内部缓存区中下一个未读字节 */
char rio_buf[RIO_BUFSIZE]; /* 内部缓存区 */
} rio_t;
extern char **environ;
#define MAXLINE 8192 /* 每行最大字符数 */
#define MAXBUF 8192 /* I/O缓存区的最大容量 */
#define LISTENQ 1024 /* 监听的第二个参数 */
/* helper functions */
ssize_t rio_writen(int fd,void *usrbuf,size_t n);
void rio_readinitb(rio_t *rp,int fd);
ssize_t rio_readlineb(rio_t *rp,void *usrbuf,size_t maxlen);
int open_clientfd(char *hostname, int portno);
int open_listenfd(int portno);
#endif
void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri,char *filename,char *cgiargs);
void serve_static(int fd,char *filename,int filesize);
void get_filetype(char *filename,char *filetype);
void serve_dynamic(int fd,char *filename,char *cgiargs);
void clienterror(int fd,char *cause,char *errum,char *shorting,char *longmsg);

主函数

int main(int argc, char **argv)
{
int listenfd,connfd, port, clientlen;
struct sockaddr_in clientaddr; /* Check command line args */
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
port = atoi(argv[1]); //从命令行参数中提取端口号
listenfd = Open_listenfd(port); //打开监听套接字
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); //line:netp:tiny:accept
doit(connfd); //line:netp:tiny:doit
Close(connfd); //line:netp:tiny:close
}
}

TINY是一个迭代服务器,监听在命令行中传递来的端口上的连接请求。再通过调用open_listenfd()函数打开一个监听套接字以后,tiny执行典型的无限服务器循环,不断的接受连接请求,执行事务,并关闭连接它的那一端。

其中:

Open_listenfd(int port),是将socket,bind,listen函数结合的一个函数,

这也看作是是一个服务器初始化的过程,其主要步骤如下:

1.listenfd = socket(AF_INET,SOCK_STREAM,0),创建一个套接字。

2.setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(const void *)&optval,sizeof(int)).设置套接字的属性使它能够在计算机重启的时候可以再次使用套接字的端口和IP

3.bind(listenfd,(SA *)&serveraddr,sizeof(serveraddr)),将监听套接字与服务器套接字地址联系起来。

4.listen(listenfd,LISTENQ),将listenfd套接字从主动套接字转化为监听套接字。

doit函数

void doit(int fd)
{
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio; /* Read request line and headers */
Rio_readinitb(&rio, fd);
Rio_readlineb(&rio, buf, MAXLINE); //line:netp:doit:readrequest
sscanf(buf, "%s %s %s", method, uri, version); //line:netp:doit:parserequest
if (strcasecmp(method, "GET")) { //line:netp:doit:beginrequesterr
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
} //line:netp:doit:endrequesterr
read_requesthdrs(&rio); //line:netp:doit:readrequesthdrs /* Parse URI from GET request */
is_static = parse_uri(uri, filename, cgiargs); //line:netp:doit:staticcheck
if (stat(filename, &sbuf) < 0) { //line:netp:doit:beginnotfound
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
} //line:netp:doit:endnotfound if (is_static) { /* Serve static content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { //line:netp:doit:readable
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
serve_static(fd, filename, sbuf.st_size); //line:netp:doit:servestatic
}
else { /* Serve dynamic content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { //line:netp:doit:executable
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs); //line:netp:doit:servedynamic
}
}

doit函数用来处理一个HTTP请求,读取请求后,首先tiny只支持get方法,如果客户端以其他方法请求,则返回错误,然后,解析uri,解析为文件路径和一个CGI参数字符串,然后再按请求为动态内容或静态内容分别处理。

细节:

用RIO包健壮的读写,因为打开的文件类型为网络套接字,那么内部缓冲约束和较长的网络延迟会造成read和write返回不足值。而RIO会处理这样的不足值。

 Rio_readinitb(&rio, fd);   //将文件描述符和内部缓冲区相联系。
Rio_readlineb(&rio, buf, MAXLINE); //从内部缓存区读出一个文本行至buf中,以null字符来结束这个文本行。当然,每行最大的字符数量不能超过MAXLINE。

HTTP请求

一个HTTP请求:一个请求行(request line) 后面跟随0个或多个请求报头(request header), 再跟随一个空的文本行来终止报头

请求行:<method> <uri> <version>

HTTP支持许多方法,包括 GET,POST,PUT,DELETE,OPTIONS,HEAD,TRACE。

URI是相应URL的后缀,包括文件名和可选参数

version 字段表示该请求所遵循的HTTP版本

请求报头:<header name> : <header data> 为服务器提供了额外的信息,例如浏览器的版本类型

HTTP 1.1中 一个IP地址的服务器可以是 多宿主主机,例如 www.host1.com www.host2.com 可以存在于同一服务器上。

HTTP 1.1 中必须有 host 请求报头,如 host:www.google.com:80 如果没有这个host请求报头,每个主机名都只有唯一IP,IP地址很快将用尽。

read_requsethdrs函数

void read_requesthdrs(rio_t *rp)
{
char buf[MAXLINE]; Rio_readlineb(rp, buf, MAXLINE);
while(strcmp(buf, "\r\n")) { //line:netp:readhdrs:checkterm
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}

Tiny不使用请求报头中的任何信息,仅仅调用 read_requesthdrs函数来读取并忽略这些报头。

parse_uri函数

int parse_uri(char *uri, char *filename, char *cgiargs)
{
char *ptr; if (!strstr(uri, "cgi-bin")) { /* Static content */ //line:netp:parseuri:isstatic
strcpy(cgiargs, ""); //line:netp:parseuri:clearcgi
strcpy(filename, "."); //line:netp:parseuri:beginconvert1
strcat(filename, uri); //line:netp:parseuri:endconvert1
if (uri[strlen(uri)-1] == '/') //line:netp:parseuri:slashcheck
strcat(filename, "home.html"); //line:netp:parseuri:appenddefault
return 1;
}
else { /* Dynamic content */ //line:netp:parseuri:isdynamic
ptr = index(uri, '?'); //line:netp:parseuri:beginextract
if (ptr) {
strcpy(cgiargs, ptr+1);
*ptr = '\0';
}
else
strcpy(cgiargs, ""); //line:netp:parseuri:endextract
strcpy(filename, "."); //line:netp:parseuri:beginconvert2
strcat(filename, uri); //line:netp:parseuri:endconvert2
return 0;
}
}

uri解析函数,tiny默认静态内容的主目录就是它的当前目录,而可执行文件的主目录./cgi_bin.任何包含字符串cgi-bin的url都会被认为表示为对动态内容的请求。默认的静态文件名为 ./home.html.

clienterror函数

void clienterror(int fd, char *cause, char *errnum,
char *shortmsg, char *longmsg)
{
char buf[MAXLINE], body[MAXBUF]; /* Build the HTTP response body */
sprintf(body, "<html><title>Tiny Error</title>");
sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body); /* Print the HTTP response */
sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: text/html\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
Rio_writen(fd, buf, strlen(buf));
Rio_writen(fd, body, strlen(body));
}

clienterror,是向客户端发送一个HTTP响应,在响应行中包含相应的状态码和状态消息,响应主体中包含一个HTML文件,向浏览器的用户解释这个错误。

细节

HTTP响应

一个HTTP响应:一个响应行(response line) 后面跟随0个或多个响应报头(response header),再跟随一个空的文本行来终止报头,最后跟随一个响应主体(response body)

响应行:<version> <status code> <status message>

status code 是一个三位的正整数

serve_static函数

void serve_static(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF]; /* Send response headers to client */
get_filetype(filename, filetype); //line:netp:servestatic:getfiletype
sprintf(buf, "HTTP/1.0 200 OK\r\n"); //line:netp:servestatic:beginserve
sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);
Rio_writen(fd, buf, strlen(buf)); //line:netp:servestatic:endserve /* Send response body to client */
srcfd = Open(filename, O_RDONLY, 0); //line:netp:servestatic:open
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);//line:netp:servestatic:mmap
Close(srcfd); //line:netp:servestatic:close
Rio_writen(fd, srcp, filesize); //line:netp:servestatic:write
Munmap(srcp, filesize); //line:netp:servestatic:munmap
}
void get_filetype(char *filename, char *filetype)
{
if (strstr(filename, ".html"))
strcpy(filetype, "text/html");
else if (strstr(filename, ".gif"))
strcpy(filetype, "image/gif");
else if (strstr(filename, ".jpg"))
strcpy(filetype, "image/jpeg");
else
strcpy(filetype, "text/plain");
}

serve_static函数发送一个HTTP响应,其主体包括一个本地文件内容。首先,我们通过检查文件名的后缀来判断文件类型,并且发送一个响应行和响应报头给客户端,注意:用一个空行来终止报头。

serve_dynamic函数

void serve_dynamic(int fd, char *filename, char *cgiargs)
{
char buf[MAXLINE], *emptylist[] = { NULL }; /* Return first part of HTTP response */
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf)); if (Fork() == 0) { /* child */ //line:netp:servedynamic:fork
/* Real server would set all CGI vars here */
setenv("QUERY_STRING", cgiargs, 1); //line:netp:servedynamic:setenv
Dup2(fd, STDOUT_FILENO); /* Redirect stdout to client */ //line:netp:servedynamic:dup2
Execve(filename, emptylist, environ); /* Run CGI program */ //line:netp:servedynamic:execve
}
Wait(NULL); /* Parent waits for and reaps child */ //line:netp:servedynamic:wait
}

Tiny通过派生一个子进程并在子进程的上下文中运行一个cgi程序(可执行文件),来提供各种类型的动态内容。

setenv("QUERY_STRING",cgiargs,1) :设置QUERY_STRING环境变量。

dup2(fd,STDOUT_FILENO):重定向它的标准输出到已连接描述符。此时,任何写到标准输出的东西都直接写到客户端。

execve(filename,emptylist,environ) :加载运行cgi程序。

源码来源:

http://csapp.cs.cmu.edu/public/ics2/code/netp/tiny/tiny.c

配置

http://blog.sina.com.cn/s/blog_3e250da301019xne.html

tiny web服务器源码分析的更多相关文章

  1. 【TencentOS tiny】深度源码分析(4)——消息队列

    消息队列 在前一篇文章中[TencentOS tiny学习]源码分析(3)--队列 我们描述了TencentOS tiny的队列实现,同时也点出了TencentOS tiny的队列是依赖于消息队列的, ...

  2. [1]传奇3服务器源码分析一 LoginGate

    服务端下载地址: 点击这里 网上基本上都有分析该源码的分析详解,如:请点击该链接,但容易晕,而且也不全!所以才有了本文! 一.首先来看服务端的LoginGate源码 先来张图比较让人容易理解

  3. 【TencentOS tiny】深度源码分析(2)——调度器

    温馨提示:本文不描述与浮点相关的寄存器的内容,如需了解自行查阅(毕竟我自己也不懂) 调度器的基本概念 TencentOS tiny中提供的任务调度器是基于优先级的全抢占式调度,在系统运行过程中,当有比 ...

  4. trinitycore 魔兽服务器源码分析(三) 多线程相关

    先看LockedQueue.h template <class T, typename StorageType = std::deque<T> >class LockedQue ...

  5. trinitycore 魔兽服务器源码分析(二) 网络

    书接上文 继续分析Socket.h SocketMgr.h template<class T>class Socket : public std::enable_shared_from_t ...

  6. trinitycore 魔兽服务器源码分析(一) 网络

    trinitycore是游戏服务器的开源代码 许多玩家使用魔兽的数据来进行测试 ,使用它来假设魔兽私服. 官方网址  https://www.trinitycore.org/ 类似的还有mangos ...

  7. python之epoll服务器源码分析

    #!/usr/bin/env python # -*- coding: utf8 -*- import socket, select EOL1 = b'/r/n' EOL2 = b'/r/n/r/n' ...

  8. 【TencentOS tiny】深度源码分析(1)——task

    任务的基本概念 从系统的角度看,任务是竞争系统资源的最小运行单元.TencentOS tiny是一个支持多任务的操作系统,任务可以使用或等待CPU.使用内存空间等系统资源,并独立于其它任务运行,理论上 ...

  9. 【TencentOS tiny】深度源码分析(3)——队列

    队列基本概念 队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间.中断和任务间传递消息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时, ...

随机推荐

  1. 笔谈FFmpeg(二)

    经过前面的学习对FFmpeg的基本流程已经很熟悉了,现在到了掌握其中细节的时候了,用FFmpeg做播放器解码操作中,涉及到了一些结构体,这些结构之间到底有什么关系,它们是怎样协同工作的呢.文章 FFM ...

  2. django图片上传修改图片名称

    storage.py # 给上传的图片重命名 from django.core.files.storage import FileSystemStorage from django.http impo ...

  3. https和证书小结

    https://www.cnblogs.com/andy9468/p/10484598.html https://www.cnblogs.com/andy9468/p/10414371.html ht ...

  4. [LeetCode] 0279. Perfect Squares 完全平方数

    题目 Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9 ...

  5. Kotlin属性揭秘与延迟初始化特性

    在上一次https://www.cnblogs.com/webor2006/p/11210181.html学习了Kotlin的伴生对象,这次来学习属性相关的东东. 属性揭秘: 先声明一个属性: 没啥可 ...

  6. top命令定位CPU高占用代码

    步骤如下: 1.使用top命令定位异常进程.可以看见12836的CPU和内存占用率都非常高 2.使用top -H -p 进程号查看异常线程 3.使用printf "%x\n" 线程 ...

  7. epoll版http服务器

    epoll是事件通知方式接收数据,效率比轮询要高 代码: import socket import re import select def client_server(new_client,recv ...

  8. Õ() Big-O-notation

    Õ只是大\(O\)表示法的变种,忽略了对数因子: \[f(n) \in \tilde O(h(n))\] \[=> \exists k : f(n) \in O \!\left( h(n)\lo ...

  9. FasfDFS整合Java实现文件上传下载功能实例详解

    https://www.jb51.net/article/120675.htm 在上篇文章给大家介绍了FastDFS安装和配置整合Nginx-1.13.3的方法,大家可以点击查看下. 今天使用Java ...

  10. debug错误总结

    1, 2,就是一个大括号的问题..让你总是得不了满分..明明和别人的代码差不多. 3,就比如P1914,这种藏坑的题,或者说这一类藏坑的题. 坑是什么呢?就是位数不够往后推的时候.. 你不填坑你就得不 ...