源码来自[https://github.com/EZLippi/Tinyhttpd/blob/master/httpd.c]
线程的解释:[http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html]
http请求的解释:https://blog.youkuaiyun.com/weixin_44347636/article/details/98078723
client端、server端、web app之间的联系:https://www.cnblogs.com/hwlong/p/9076764.html
网络协议:描述报文内容详细语义的协议
通用网关接口cgi:server端与web app之间遵守的协议
什么是pipe:https://blog.youkuaiyun.com/qq_42914528/article/details/82023408
client端发送的http请求报文的内容格式:
文章目录
main函数
int main(void)
{
int server_sock = -1;
u_short port = 4000;
int client_sock = -1;
struct sockaddr_in client_name;
socklen_t client_name_len = sizeof(client_name);
pthread_t newthread;
server_sock = startup(&port);
printf("httpd running on port %d\n", port);
while (1)
{
client_sock = accept(server_sock,
(struct sockaddr *)&client_name,
&client_name_len);
if (client_sock == -1)
error_die("accept");
/* accept_request(&client_sock); */
/**********************************************************************/
/*int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);
返回值:若成功则返回0,否则返回出错编号
参数:
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数func的地址。
最后一个参数是运行函数func的参数。*/
if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
perror("pthread_create");
/**********************************************************************/
}
close(server_sock);
return(0);
}
startup函数:
/**********************************************************************/
/*startup函数启动 侦听(listen)指定端口(port)上的 Web 连接 的过程*/
/*如果端口为 0,则动态分配端口并修改原始端口变量以反映实际端口。*/
/*参数:指向包含要连接的端口的变量的指针*/
/*返回值:套接字socket*/
/**********************************************************************/
int startup(u_short *port)
{
int httpd = 0;
int on = 1;
struct sockaddr_in name;
/**********************************************************************/
/* int socket(int af, int type, int protocol);
/*af:地址描述*/
/*PF_INET:IPv4协议*/
/*type:新套接口的通信类型描述。
/*SOCKET_STREAM:双向可靠数据流,对应TCP*/
/*protocol:套接口所用的协议。如调用者不想指定,可用0指定,表示缺省。*/
httpd = socket(PF_INET, SOCK_STREAM, 0);
/*创建一个IPv4、TCP协议套接口httpd*/
if (httpd == -1)
error_die("socket");/*socket创建失败*/
/**********************************************************************/
/* void *memset(void *s, int ch, size_t n);*/
/*函数解释:
/*将s中当前位置后面的n个字节用 ch 替换并返回 s */
/*(typedef unsigned int size_t )*/
memset(&name, 0, sizeof(name));
/*将结构体name的全部内容,即地址族、端口、地址置0*/
name.sin_family = AF_INET;
/*设置地址族*/
name.sin_port = htons(*port);
/*设置端口*/
name.sin_addr.s_addr = htonl(INADDR_ANY);
/*设置地址*/
/**********************************************************************/
/*int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
/*函数解释:设置套接口sockfd的各种属性
/*sockfd:标识一个套接口的描述字。
/*level:选项定义的层次。支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
/*如果想要在套接字级别上设置选项,就必须把level设置为SOL_SOCKET。
/*optname:需设置的选项。
/*SO_REUSEADDR:端口释放后可立即被再次使用
/*optval:指针,指向存放选项待设置的新值的缓冲区。
optlen:optval缓冲区长度。*/
if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)
{
error_die("setsockopt failed");
}
/*将套接口httpd设置成SO_REUSEADDR属性*/
/**********************************************************************/
/*server端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来*/
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
error_die("bind");
/*将套接字http和包含IP地址和端口的结构体name绑定*/
/**********************************************************************/
/*
网络字节顺序,NBO是网络数据在传输中的规定的数据格式,从高到低位顺序存储,即数据低位存储在高地址,数据高位存储在低地址;即“大端模式”。网络字节顺序可以避免不同主机字节顺序的差异。
主机字节顺序,HBO则与机器CPU相关,数据的存储顺序由CPU决定。
在C/C++写网络程序的时候,往往会遇到字节的网络顺序和主机顺序的问题。这是就可能用到htons(), ntohl(), ntohs(),htons()这4个函数,也就是网络字节顺序n与本地字节顺序h之间的转换函数:
htonl()--"Host to Network Long"
ntohl()--"Network to Host Long"
htons()--"Host to Network Short"
ntohs()--"Network to Host Short"
int getsockname(int sockfd, struct sockaddr *localaddr,socklen_t *addrlen);
参数:
sockfd:需要获取名称的套接字。
localaddr:存放所获取套接字名称的缓冲区。
addrlen:作为入口参数,name指向空间的最大长度。作为出口参数,name的实际长度*/
if (*port == 0) /*若端口为0,则动态分配端口*/
{
socklen_t namelen = sizeof(name);
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
error_die("getsockname");
/*获取一个套接口的本地名字*/
*port = ntohs(name.sin_port);
}
/**********************************************************************/
/*int listen(SOCKET sock, int backlog); //Windows
/*sock :需要进入监听状态的套接字
/*backlog :请求的等待队列最大长度
/*通过 listen() 函数可以让套接字进入被动监听状态。当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求*/
if (listen(httpd, 5) < 0)
error_die("listen");
/*缓冲区的请求数最大为5*/
return(httpd);
/**************************************************************************/ }
/*当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求
/*int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
/*accept() 返回一个新的套接字来和客户端通信,sock为server端的套接字,addr 保存了client端的IP地址和端口号*/
accept_request函数:
/**********************************************************************/
/* A request has caused a call to accept() on the server port to
* 返回值:Process the request appropriately.
* 参数: 已经连接到client端的socket */
/**********************************************************************/
void accept_request(void *arg)
{
/**************************************************************************/
/*intptr_t是为了跨平台,其长度总是所在平台的位数,所以用来存放地址。*/
/*size_t是为了方便系统之间的移植而定义的,它是一个无符号整型,在32位系统上定义为:unsigned int;在64位系统上定义为unsigned long。*/
/*以下是client端的http请求报文所包含的内容:*/
/*结构体stat记录文件的各类参数*/
/*method:请求方法*/
/*url:*/
int client = (intptr_t)arg;
char buf[1024]; /*接收server端发送的数据的缓存区*/
size_t numchars;
char method[255];
char url[255];
char path[512];
size_t i, j;
struct stat st;
int cgi = 0; /* becomes true if server decides this is a CGI
* program */
char *query_string = NULL;
numchars = get_line(client, buf, sizeof(buf));
/*将client端套接口收到的数据的第一行经过“转换成换行”处理后,存放在buf中*/
/**************************************************************************/
/*ISspace():判断字符是否为空白字符' '
/*若返回0,则表示字符不是空白的;否则为空白字符
/*ISspace()表示若存在空白字符,则执行操作*/
/*提取buf中的method:*/
i = 0; j = 0;
while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
/*若遇到空格,则停止提取操作*/
/*将buf收到的文本中的method提取出来*/
{
method[i] = buf[i];
i++;
}
j=i;
method[i] = '\0';
/**************************************************************************/
/*strcasecmp()函数:判断字符串是否相等(忽略大小写)*/
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
/*当method既不是GET也不是POST时,执行unimplemented函数*/
{
unimplemented(client);
return;
}
if (strcasecmp(method, "POST") == 0)
/*当method是POST时*/
cgi = 1;
/**************************************************************************/
/*提取buf中的url:*/
i = 0;
while (ISspace(buf[j]) && (j < numchars))
/*找到buf[j]的第一个非空字符*/
/*上面的操作找到method后,通过该循环操作使得index移动至url的开头,为提取url做准备*/
j++;
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
{
url[i] = buf[j];
i++; j++;
/*提取url*/
}
url[i] = '\0';
/**************************************************************************/
/*提取url中的query,用于给cgi传递参数*/
/*若请求为GET,则提取query
/*url的组成:
/*[]表示该部分可省略
/*protocol :// hostname[:port] / path / [;parameters][?query]#fragment
/*hostname之后的部分就是url*/
if (strcasecmp(method, "GET") == 0)
{
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
/*将index移动到url的字符'?'位置*/
query_string++;
if (*query_string == '?')
{
cgi = 1;
*query_string = '\0';
query_string++;
}
}
/**************************************************************************/
/*sprintf:格式化输出字符*/
/* /htdocs 目录是用于存放用户网站程序文件的默认站点目录 */
/*index.html是主页的html文件*/
sprintf(path, "htdocs%s", url);
/*在url前面加上"htdocs",并将拼接后的url赋给path*/
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");
/*若url末尾是/,则将index.html拼在紧接path的后面变成/index.html */
/**************************************************************************/
/*文件权限定义:
/*#define S_IFMT 0170000 type of file 文件类型掩码
/*#define S_IFDIR 0040000 directory 目录文件
/*S_IXUSR 所有者拥有执行权限
/*S_IXGRP 群组拥有执行权限
/*S_IXOTH 其他用户拥有执行权限*/
if (stat(path, &st) == -1)
/*若在path路径下文件信息获取失败,则:*/
{
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
not_found(client);
}
else
{
if ((st.st_mode & S_IFMT) == S_IFDIR)
strcat(path, "/index.html");
/*若文件类型是目录文件,则path末尾添加/index.html */
if ((st.st_mode & S_IXUSR) ||
(st.st_mode & S_IXGRP) ||
(st.st_mode & S_IXOTH) )
cgi = 1;
/*若对文件的操作权限满足上述任意一种执行权限,则cgi=1*/
/**************************************************************************/
if (!cgi)
serve_file(client, path);
else
/*若cgi=1,则执行execute_cgi函数*/
execute_cgi(client, path, method, query_string);
}
/**************************************************************************/
close(client);
}
get_line函数:
/**************************************************************************/
/* 从套接字获取一行,无论该行以换行符、回车符还是 CRLF 组合结束。
/* 终止使用空字符读取的字符串。
/* 如果在缓冲区结束之前未找到正行指示器,则字符串将终止为 null。
/* 如果读取上述三行终止符中的任何一个,则字符串的最后一个字符将是换行符,字符串将以空字符终止。
* 参数:套接字描述符
* 用于将数据保存在缓冲区
* 缓冲区大小
* 返回:存储的字节数(不包括空) *
/*读取套接字的一行,把回车\r换行\n等情况都统一为换行符\n结束。
/*目的是使得windows的文件和unix的数据都能以同一方式打开,即结尾都是换行符
/**************************************************************************/
int get_line(int sock, char *buf, int size)
{
int i = 0;
char c = '\0';
int n;
while ((i < size - 1) && (c != '\n'))
{
/**************************************************************************/
/*int recv( SOCKET s,char* buf,int len,int flags);
不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。
该函数的第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。
返回:缓存区的字节数*/
n = recv(sock, &c, 1, 0);
/*在这里的作用是:
/*client端的1字节缓冲区接收server端发送的数据*/
/* 若要DEBUG,此处应添加下语句来检验数据是否正确接收
/* printf("%02X\n", c); */
/**************************************************************************/
/*在Windows中:
/*'\n' 换行,换到当前位置的下一行,而不会回到行首
/*'\r' 回车,回到当前行的行首,而不会换到下一行,如果接着输出的话,本行以前的内容会被逐一覆盖;*/
if (n > 0)
{
if (c == '\r')/*出现回车符号时,可能将要换行*/
{
n = recv(sock, &c, 1, MSG_PEEK);
/*MSG_PEEK表示只查看数据,不取走数据*/
/*0表示取走数据*/
/* 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);
}
unimplemented函数:
/**********************************************************************/
/* 告知client that the requested web method has not been
* 实现.
* 参数: client端套接字 */
/**********************************************************************/
void unimplemented(int client)
{
char buf[1024];
sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</TITLE></HEAD>\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}
/**********************************************************************/
serve_file函数:
/**********************************************************************/
/* Send a regular file to the client. Use headers, and report
* errors to client if they occur.
* Parameters: a pointer to a file structure produced from the socket
* file descriptor
* the name of the file to serve */
/**********************************************************************/
void serve_file(int client, const char *filename)
{
FILE *resource = NULL;
int numchars = 1;
char buf[1024];
buf[0] = 'A'; buf[1] = '\0';
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
resource = fopen(filename, "r");
if (resource == NULL)
not_found(client);
else
{
headers(client, filename);
cat(client, resource);
}
fclose(resource);
}
execute_cgi函数:
/**********************************************************************/
/* 执行 CGI 脚本。 将会根据需要设置环境变量。
* 参量: client socket descriptor
* path to the CGI script */
/**********************************************************************/
void execute_cgi(int client, const char *path,
const char *method, const char *query_string)
{
char buf[1024];
int cgi_output[2];
int cgi_input[2];
/*这两个都是为了pipe的创建,其中[0]代表pipe读的端口,[1]代表pipe写的端口*/
/*pipe是半双工,读了的数据就不会继续存在于pipe内*/
pid_t pid;
/*pid用于储存进程号*/
int status;
int i;
char c;
int numchars = 1;
int content_length = -1;
/**********************************************************************/
/*?*/
buf[0] = 'A'; buf[1] = '\0';
if (strcasecmp(method, "GET") == 0)
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
else if (strcasecmp(method, "POST") == 0) /*POST*/
{
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > 0) && strcmp("\n", buf))
{
buf[15] = '\0';
if (strcasecmp(buf, "Content-Length:") == 0)
content_length = atoi(&(buf[16]));
/*将字符串里的数字提取出来并转换成整形,找到content_length*/
numchars = get_line(client, buf, sizeof(buf));
}
if (content_length == -1) {
bad_request(client);
return;
}
}
else/*HEAD or other*/
{
}
/**********************************************************************/
if (pipe(cgi_output) < 0)
/*若成功创建pipe,则返回0,否则返回-1*/
{
cannot_execute(client);
return;
}
if (pipe(cgi_input) < 0) {
cannot_execute(client);
return;
}
if ( (pid = fork()) < 0 )
/*若fork返回-1,则未能创建进程*/
{
cannot_execute(client);
return;
}
sprintf(buf, "HTTP/1.0 200 OK\r\n");
/*若成功接收,则将HTTP/1.0 200 OK\r\n附在原内容后面*/
/*HTTP/1.0是版本号,200是状态码*/
send(client, buf, strlen(buf), 0);
/*发送响应报文的状态行给套接字client_sock*/
/**********************************************************************/
/*fork函数子进程返回0,父进程返回子进程的pid。*/
if (pid == 0) /* child: CGI script */
{
char meth_env[255];
char query_env[255];
char length_env[255];
/*close(fd):
/*关闭事先已经打开的文件描述符*/
/*cgi数据的具体流向:
/*输入数据:(cgi_input[1]-->STDIN(cgi_input[0]))
/*响应数据:(STDOUT(cgi_output[1])--> cgi_output[0])*/
dup2(cgi_output[1], STDOUT);
/*使得cgi_output[1]与STDOUT建立映射关系,指向同一个文件表项*/
dup2(cgi_input[0], STDIN);
/*使得cgi_input[0]与STDIN建立映射关系,指向同一个文件表项*/
close(cgi_output[0]);
/*关闭cgi_output读的端口*/
close(cgi_input[1]);
/*关闭cgi_input写的端口*/
/**********************************************************************/
/*putenv(const char)
/*按照char的内容配置对应的环境变量*/
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
/*将环境变量REQUEST_METHOD配置为 已提取的method对应的请求方法*/
if (strcasecmp(method, "GET") == 0) {
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
/*将环境变量QUERY_STRING配置为已提取的query_string对应的请求方法*/
}
else { /* POST */
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
/*将环境变量CONTENT_LENGTH配置为已提取的content_length对应的字节长度*/
}
execl(path, NULL);
/*执行path所代表的文件路径*/
exit(0);
/**********************************************************************/
} else { /* parent */
close(cgi_output[1]);
close(cgi_input[0]);
if (strcasecmp(method, "POST") == 0)
for (i = 0; i < content_length; i++) {
recv(client, &c, 1, 0);
/*将套接字client要发送的文本长度的内容存在c内*/
write(cgi_input[1], &c, 1);
/*将c内,直到内容为空*/
}
while (read(cgi_output[0], &c, 1) > 0)
/*将cgi_output[0]收到的一字节内容存在c内,直到内容为空*/
send(client, &c, 1, 0);
/*将c存的一字节内容发送到套接字client*/
close(cgi_output[0]);
close(cgi_input[1]);
waitpid(pid, &status, 0);/*等待子进程结束*/
}
}