tiny web服务器源码分析
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服务器源码分析的更多相关文章
- 【TencentOS tiny】深度源码分析(4)——消息队列
消息队列 在前一篇文章中[TencentOS tiny学习]源码分析(3)--队列 我们描述了TencentOS tiny的队列实现,同时也点出了TencentOS tiny的队列是依赖于消息队列的, ...
- [1]传奇3服务器源码分析一 LoginGate
服务端下载地址: 点击这里 网上基本上都有分析该源码的分析详解,如:请点击该链接,但容易晕,而且也不全!所以才有了本文! 一.首先来看服务端的LoginGate源码 先来张图比较让人容易理解
- 【TencentOS tiny】深度源码分析(2)——调度器
温馨提示:本文不描述与浮点相关的寄存器的内容,如需了解自行查阅(毕竟我自己也不懂) 调度器的基本概念 TencentOS tiny中提供的任务调度器是基于优先级的全抢占式调度,在系统运行过程中,当有比 ...
- trinitycore 魔兽服务器源码分析(三) 多线程相关
先看LockedQueue.h template <class T, typename StorageType = std::deque<T> >class LockedQue ...
- trinitycore 魔兽服务器源码分析(二) 网络
书接上文 继续分析Socket.h SocketMgr.h template<class T>class Socket : public std::enable_shared_from_t ...
- trinitycore 魔兽服务器源码分析(一) 网络
trinitycore是游戏服务器的开源代码 许多玩家使用魔兽的数据来进行测试 ,使用它来假设魔兽私服. 官方网址 https://www.trinitycore.org/ 类似的还有mangos ...
- python之epoll服务器源码分析
#!/usr/bin/env python # -*- coding: utf8 -*- import socket, select EOL1 = b'/r/n' EOL2 = b'/r/n/r/n' ...
- 【TencentOS tiny】深度源码分析(1)——task
任务的基本概念 从系统的角度看,任务是竞争系统资源的最小运行单元.TencentOS tiny是一个支持多任务的操作系统,任务可以使用或等待CPU.使用内存空间等系统资源,并独立于其它任务运行,理论上 ...
- 【TencentOS tiny】深度源码分析(3)——队列
队列基本概念 队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间.中断和任务间传递消息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时, ...
随机推荐
- [极客-Linux] 04 Linux命令
创建用户: useradd 装文件: CentOS: (软件管家yum) rpm -i xxx.rpm Ubuntu: (apt-get) dpkg -i xxx.deb nohup: no ...
- Kali下进行局域网断网攻击
今天我就来演示一下在kali下的局域网断网攻击,即ARP地址欺骗,下图所要用到的arp地址欺骗状态图: 则: 第一步:假设主机A访问某网站,那么要告知某网站我的IP和MAC地址,但这是以广播的方式告知 ...
- netty websocket
1 全局保存websocket的通道 NettyConfig.java public class NettyConfig { public static ChannelGroup group = n ...
- 二维码内嵌LOGO
package Test; import java.awt.BasicStroke; import java.awt.Graphics; import java.awt.Graphics2D; imp ...
- Dubbo源码分析:Dubbo协议解码
Dubbo协议解码时序图
- select下拉选中显示对应的div隐藏不相关的div
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- @NotNull,@NotEmpty,@NotBlank区别
示例结果: // null String name = null; @NotNull: false @NotEmpty: false @NotBlank: false // 空字符串 String n ...
- (Python) SOAP Web Service (HTTP POST)
功能很强大,可惜只有试用 https://www.example-code.com/python/soap_web_service.asp
- linux共享文件 - samba 服务器
1.Samba 服务器 客户端 yum 安装: # yum install samba samba-client -y 2.samba 配置文件配置 /etc/samba/smb.conf [glo ...
- C静态库和动态库的制作
(一)静态库就是把一些*.o的文件集合起来:以*.a结尾打包:做成的lib库文件:专门放到lib目录下 静态库的制作: 1.制作*.a文件 把之前src/mymath.c 的源文件 编译成单独的o文件 ...