源码见:
这里我们按照时间线性过程来描述整个代码过程,并在每一块提出一个问题,来解答为什么要这么选择。
一、建立Listen监听
我们启动服务器程序,进入main函数
则我们先建立了服务器的监听,便于与即将来到的请求启动TCP连接。 建立监听的函数为startup
int startup(u_short *port)
{
int httpd = 0;
struct sockaddr_in name;
//建立一个socket描述符
httpd = socket(PF_INET, SOCK_STREAM, 0);
if (httpd == -1) //这种出错一般在于描述符过多,或其他。
error_die("socket");
memset(&name, 0, sizeof(name)); //对name结构体清零(也可用bzero等函数)
//设置服务器的监听元素: socket族,端口, 可接受的地址
name.sin_family = AF_INET;
name.sin_port = htons(*port); //主机序转网络序的函数
name.sin_addr.s_addr = htonl(INADDR_ANY);//可接受任何地址的访问
//将描述符和这个socket结构体进行绑定
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
error_die("bind");
//若port之前为0,则bind后,开启的端口为随机端口
//这时候我们需要获取该端口是多少
if (*port == 0) {
socklen_t namelen = sizeof(name);
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
//建立监听,最大连接数为5.
if (listen(httpd, 5) < 0)
error_die("listen");
return(httpd);
}
注意,这个函数的输入参数为port, 当port为非0整数时, 则指代开启port端口用作连接。
若port为0,则为随机端口。
问题: 为什么要设置随机端口? 一直固定1个端口不好么,或者我们自己输入一个端口?
答: 一是我们无法判断自己输入的端口是否被占用或者正确(即并非公用端口)
二是当我们重启服务器,或者关闭服务器再打开时, 不需要去进行MSL等待。(即虽然服务器程序关闭,连接终止,但是端口会等待2MSL的时间)
二、连接建立和处理(主要在main函数中)
监听建立后, 服务器便进入阻塞。 一旦有客户连接到来时,才会解除阻塞。
当我们在浏览器上输入 服务器IP:端口/url 时,则会发起TCP连接。
此时服务器需要处理这个连接,用一个线程处理即可, 主程序继续进行监听。
int main(void)
{
int server_sock = -1;
u_short port = 54834; //port为0时,最终开启的端口为随机端口
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){
//等待连接建立,并返回1个客户端描述符,且客户端信息存入client_name。
client_sock = accept(server_sock,
(struct sockaddr *)&client_name,
&client_name_len);
if (client_sock == -1)
error_die("accept");
//新建一个线程,处理这个客户的连接(非持久连接)
if (pthread_create(&newthread , NULL, accept_request, (void *)&client_sock) != 0)
perror("pthread_create");
}
close(server_sock);
return(0);
}
问题:为什么不用多进程,而是用多线程????
答:因为HTTP是 请求-响应模式的,
即1次请求,只有 1次响应(这个是HTTP非持续连接的特征, HTTP的持续连接这里暂时不需要实现)
因此每次响应不需要太多复杂逻辑的处理, 时间也不会太长, 没有必要使用资源开销巨大的多进程模型。
此时我们启用了线程,并进行了1个accept_request的处理。
三、请求行处理
我们先读取出请求方法
再读取出url路径
再判断究竟是直接返回一个静态文件即可,还是返回一个需要经过cgi脚本处理的数据。
void *accept_request(void *client1){
int client = *(int *)client1;
char buf[1024];
int 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));
i = 0; j = 0;
//找出请求方法(Get或者POST)
while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
{
method[i] = buf[j];
i++; j++;
}
method[i] = '\0';
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);
return NULL;
}
//如果是POST,肯定要调用cgi脚本获得响应数据
if (strcasecmp(method, "POST") == 0){
printf("This is Post!");
cgi = 1;
}
//找出url
i = 0;
while (ISspace(buf[j]) && (j < sizeof(buf)))
j++;
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
{
url[i] = buf[j];
i++; j++;
}
url[i] = '\0'; //字符串结束符不要忘记加。
//如果是get, 且get的url中有?,则?后的是
if (strcasecmp(method, "GET") == 0)
{
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
if (*query_string == '?')
{
cgi = 1;
*query_string = '\0';
query_string++;
}
}
//这里先默认路径为htdoc中的文件,故添加上htdoc
sprintf(path, "htdocs%s", url);
//如果最后一个是'\',说明没输入文件名,故默认为首页index.html
if (path[strlen(path) - 1] == '/')
strcat(path, "index.html");
//判断一下目录下是否存在该文件
if (stat(path, &st) == -1) {
//如果不存在,需要把剩余的内容都读完,这个操作在后面会非常常见。
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
printf("path = -1, discrad headrs: %s\n",buf);
not_found(client);
}
else
{
//如果访问的是目录,也认为访问的是首页index.html
if ((st.st_mode & S_IFMT) == S_IFDIR)
strcat(path, "/index.html");
//如果访问的文件是可执行文件,则认为调用的cgi处理脚本,而非静态文件。
if ((st.st_mode & S_IXUSR) ||
(st.st_mode & S_IXGRP) ||
(st.st_mode & S_IXOTH) ){
cgi = 1;
}
//根据cgi的值,来判断要传输静态文件数据, 还是做cgi脚本的处理
if (!cgi)
serve_file(client, path);
else
execute_cgi(client, path, method, query_string);
}
close(client);
return NULL;
}
问题:POST和GET的区别是什么?
可以从字面上去理解, GET就是单纯获取数据,POST则常常会伴有对后端数据的修改。
故POST一定会有cgi的处理, 而GET则是可能有,可能没有。
如果不需要cgi处理,则我们进入:
四、静态文件传输
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);
}
void headers(int client, const char *filename)
{
char buf[1024];
(void)filename; /* could use filename to determine file type */
strcpy(buf, "HTTP/1.1 200 OK\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, "\r\n");
send(client, buf, strlen(buf), 0);
}
//传输静态文件数据
//注意这里只能传文本类型,故采用的行读取
/**********************************************************************/
void cat(int client, FILE *resource)
{
char buf[1024];
fgets(buf, sizeof(buf), resource);
while (!feof(resource))
{
send(client, buf, strlen(buf), 0);
fgets(buf, sizeof(buf), resource);
}
}
问题:五、CGI处理,响应数据
这里首先获取一些POST可能需要的头信息(例如长度)
或者GET中的query_string
以形成需要传输给cgi的输入参数。
接着把这些参数传入给cgi,获取cgi的返回,并把数据发回给客户端。
/**********************************************************************/
/* 如何处理脚本,并发回响应?
/**********************************************************************/
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];
pid_t pid;
int status;
int i;
char c;
int numchars = 1;
int content_length = -1;
buf[0] = 'A'; buf[1] = '\0';
//如果是GET请求,则请求头暂时没有用。我们舍弃掉。
if (strcasecmp(method, "GET") == 0)
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
{
numchars = get_line(client, buf, sizeof(buf));
//printf("%s\n",buf);
}
else /* POST */
{
//如果是POST请求,我们需要知道content-Lenth的内容
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]));
numchars = get_line(client, buf, sizeof(buf));
}
if (content_length == -1) {
bad_request(client);
return;
}
}
//这个ok响应必须添加
//其他响应头 通过cgi脚本去发出。
sprintf(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
//建立2个管道
if (pipe(cgi_output) < 0) {
cannot_execute(client);
return;
}
if (pipe(cgi_input) < 0) {
cannot_execute(client);
return;
}
//建立子进程
if ( (pid = fork()) < 0 ) {
cannot_execute(client);
return;
}
//子进程负责将 请求参数输入到cgi脚本中,并返回处理后的数据
if (pid == 0)
{
char meth_env[255];
char query_env[255];
char length_env[255];
dup2(cgi_output[1], 1); //在子进程中,把out1描述符重定向为标准输出
dup2(cgi_input[0], 0); //把in0描述符重定向为标准输入
close(cgi_output[0]); //关闭无用的2个方向。
close(cgi_input[1]);
//设置cgi脚本所需要的环境变量
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else { /* POST */
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
//调用execl执行
execl(path, path,NULL);
exit(0);
} else {
/* 父进程,负责把参数传入子进程
把子进程中cgi处理得到的数据发给浏览器*/
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);
write(cgi_input[1], &c, 1);
}
while (read(cgi_output[0], &c, 1) > 0)
send(client, &c, 1, 0);
close(cgi_output[0]);
close(cgi_input[1]);
waitpid(pid, &status, 0);
}
}
问题:为什么父子进程要使用管道来做输入输出? 父进程直接获得子进程的cgi输出不可以吗
管道的特点是,一旦有数据传入,就会立刻传出,并常常被我们设立为单向管道。
这里如果不用管道,那么当子进程用excl调用cgi时,所获得的输出将会不知所向, 正常情况下是输出给标准输出(描述符为1),但我们还需要传给客户端, 而传给客户端这个工作,就交给父进程来执行, 所以建立了2个管道,即
父进程 从管道A推参数, 推到子进程的输入中
子进程用cgi处理数据,并把print的数据,从管道B,推回父进程
父进程从管道B获取数据,发回到客户端中。