这一次的Socket系列准备讲Web服务器。就是编写一个简单的Web服务器,具体怎么做呢?我也不是很清楚流程,所以我找来了一个开源的小的Web服务器--tinyhttpd。这个服务器才500多行的代码,使用C语言。这一小节就不讲别的内容了。就对这个程序进行一些注释和讲解了。

  主函数:

 int main(void)
{
int server_sock = -;
u_short port = ;
int client_sock = -;
struct sockaddr_in client_name;
int client_name_len = sizeof(client_name);
pthread_t newthread; server_sock = startup(&port);//Web服务器打开指定端口
printf("httpd running on port %d\n", port); while ()
{
client_sock = accept(server_sock,(struct sockaddr *)&client_name,&client_name_len);
if (client_sock == -)
error_die("accept");
if (pthread_create(&newthread , NULL, accept_request, client_sock) != )
perror("pthread_create");
}
close(server_sock);
return();
}

  从主函数我们可以知道,这个服务器是对于每一个客户端的连接都采用一个线程对其处理。上面对应的startup函数是对指定的端口进行socket的创建,绑定,监听。

  startup函数:

 int startup(u_short *port)
{
int httpd = ;
struct sockaddr_in name; httpd = socket(PF_INET, SOCK_STREAM, );
if (httpd == -)
error_die("socket");
memset(&name, , sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < )
error_die("bind");
if (*port == ) /* if dynamically allocating a port */
{
int namelen = sizeof(name);
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
if (listen(httpd, ) < )
error_die("listen");
return(httpd);
}

  对于上面的getsockname函数是,如果传进来的port为0,那么就前面的bind就会失败,所以要使用getsockname函数来获取一个当前可用的可连接的Socket套接字的名字。此时返回的端口就是随机的。

  接下来就是一个对每个客户端连接的处理函数

  accept_request函数

 void accept_request(int client)
{
char buf[];
int numchars;
char method[];
char url[];
char path[];
size_t i, j;
struct stat st;
int cgi = ; /* becomes true if server decides this is a CGI
* program */
char *query_string = NULL; numchars = get_line(client, buf, sizeof(buf));//获取第一行客户端的请求 GET / HTTP/1.1 类似这样的
i = ; j = ;
while (!ISspace(buf[j]) && (i < sizeof(method) - ))//获取第一个单词,一般为GET或POST 两种请求方法
{
method[i] = buf[j];
i++; j++;
}
method[i] = '\0'; if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))//如果不是GET或POST方法的,那么就回复一个不支持的请求方法页面。话说如果自己写服务器可以加自己的请求方法。不过有个问题就是浏览器是没有的?怎么办,看来还要自己弄个小的浏览器
{
unimplemented(client);
return;
} if (strcasecmp(method, "POST") == )//如果是使用POST方法,那么就一定是cgi程序
cgi = ; i = ;
while (ISspace(buf[j]) && (j < sizeof(buf)))//取出空格
j++;
// GET / HTTP/1.1 接下来是取第二个字符串,第二个串是此次请求的页面地址
while (!ISspace(buf[j]) && (i < sizeof(url) - ) && (j < sizeof(buf)))
{
url[i] = buf[j];
i++; j++;
}
url[i] = '\0'; if (strcasecmp(method, "GET") == )//如果是GET方法,GET方法和POST方法是有点区别的,GET方法是通过URL请求来传递用户的数据,将表单等各个字段名称与内容,以成对的字符串进行连接来传递参数的。
//例如 http://www.baidu.com/s?wd=cnblogs 这个URL就是使用百度搜索cnblogs的URL地址,baidu搜索怎么知道我在输入框中输入的是什么数据?就是通过这样的一个参数来告诉它的。一般参数都是在?(问号)后面的。
{
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))//一直读,直到遇到问号
query_string++;
if (*query_string == '?')//如果有问号,就表示可能要调用cgi程序了,不是简单的静态HTML页面的。
{
cgi = ;
*query_string = '\0';
query_string++;
}
} sprintf(path, "htdocs%s", url);//这个是web服务器的主目录
if (path[strlen(path) - ] == '/')
strcat(path, "index.html");//如果输入的网址没有指定网页,那么默认使用index.html这个页面
if (stat(path, &st) == -) {//根据文件名,获取该文件的文件信息,如果为-1,表示获取该文件的文件信息失败,可能的问题是没有该文件,或是权限什么的问题,具体失败的原因可以查看errno
while ((numchars > ) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
not_found(client);//返回一个not found 404的页面了
}
else
{
if ((st.st_mode & S_IFMT) == S_IFDIR)//如果该文件名对应的是一个目录,那么就访问该目录下的默认主页index.html,这里如果是jsp,就是index.jsp什么的。
strcat(path, "/index.html");
if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))//判断该文件的执行权限问题
cgi = ;
if (!cgi)//如果不是cgi程序,而是一个简单的静态页面
serve_file(client, path);
else//一个cgi程序
execute_cgi(client, path, method, query_string);
} close(client);
}

  关于GET和POST的区别,可以参考别的博客,这里就不详解了。指说一个我们处理是要注意的问题,那就是GET方法的参数是在URL地址中。而Post 方法通过 HTTP post 机制,将表单内各字段名称与其内容放置在 HTML 表头(header)内一起传送给服务器端交由 action 属性能所指的程序处理,该程序会通过标准输入(stdin)方式,将表单的数据读出并加以处理。说的有点抽象,还是上几张图片比较容易看吧。

  这一张是get方法的(使用百度搜索功能,搜索的关键字是使用get方法提交)

  这一张是post方法的(使用一个游戏的登录界面,该登录界面的帐号和密码的提交方式是使用POST方式)

  可以看到,在Hypertext Transfer Protocol后面有个Line-based text data。可以看到有个这样的字符串,username=...&passwd=...&serverid=...居然明文传输,这个游戏太不厚道了,伐开心了,我一直不知道。我们可以看到上面的Content-Length:53 就表示在\r\n\r\n后面会有接着的53个字符要接收。这个看起来是不是跟应答信息很像啊。

  提示:通过get方法提交数据,可能会带来安全性的问题。比如一个登陆页面。当通过get方法提交数据时,用户名和密码将出现在URL上。
  1.登陆页面可以被浏览器缓存;
  2.其他人可以访问客户的这台机器。
  那么,别人即可以从浏览器的历史记录中,读取到此客户的账号和密码。所以,在某些情况下,get方法会带来严重的安全性问题。所以建议在Form中,建议使用post方法。

  好,我们继续讲解其他的函数了。

  serve_file函数,就是对一个简单的HTML静态页面进行返回

 void serve_file(int client, const char *filename)
{
FILE *resource = NULL;
int numchars = ;
char buf[]; buf[] = 'A'; buf[] = '\0';
while ((numchars > ) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));//从上面的图我们可以看到还有一些请求信息如Connection,Cache-Control,User-Agent,Accept等等的信息,这些在这个简单Web服务器中就忽略了。如果要增加功能,就可以使用这些信息。如最简单的判断使用的浏览器类型,操作系统等。 resource = fopen(filename, "r");//根据GET 后面的文件名,将文件打开。
if (resource == NULL)
not_found(client);
else
{
headers(client, filename);//发送一个应答头信息
cat(client, resource);//逐字符发送
}
fclose(resource);
}

  cat函数,这个就不用讲了。就是一个发送send

 void cat(int client, FILE *resource)
{
char buf[]; fgets(buf, sizeof(buf), resource);
while (!feof(resource))
{
send(client, buf, strlen(buf), );
fgets(buf, sizeof(buf), resource);
}
}

  还有一个关键的函数,execute_cgi这个函数,用来执行cgi程序的。

 void execute_cgi(int client, const char *path, const char *method, const char *query_string)
{
char buf[];
int cgi_output[];
int cgi_input[];
pid_t pid;
int status;
int i;
char c;
int numchars = ;
int content_length = -; buf[] = 'A'; buf[] = '\0';
if (strcasecmp(method, "GET") == )//同什么的serve_file函数,对那些请求头进行忽略
{
while ((numchars > ) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
}
else /* POST方法 */
{
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > ) && strcmp("\n", buf))//这里同样是忽略请求头
{
buf[] = '\0';
if (strcasecmp(buf, "Content-Length:") == )//但是考虑到在请求头后面还有信息要读,而信息的大小就在这里。这个Content-Length后面,也就是上面截图是所说的。看了这个代码是不是对刚才说的有了更深的理解了
content_length = atoi(&(buf[]));//获取后面字符的个数
numchars = get_line(client, buf, sizeof(buf));
}
//注意到了这里后Post请求头后面的附带信息还没有读出来,要在下面才读取。
if (content_length == -) {
bad_request(client);
return;
}
} sprintf(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), ); if (pipe(cgi_output) < ) {//创建管道,方便程序或进程之间的数据通信
cannot_execute(client);
return;
}
if (pipe(cgi_input) < ) {
cannot_execute(client);
return;
}
//子进程中,用刚才初始化的管道替换掉标准输入标准输出,将请求参数加到环境变量中,调用execl执行cgi程序获得输出。
if ( (pid = fork()) < ) {
cannot_execute(client);
return;
}
if (pid == ) /* child: CGI script */
{
char meth_env[];
char query_env[];
char length_env[]; dup2(cgi_output[], );//将文件描述符为1(stdout)的句柄复制到output中
dup2(cgi_input[], );//将文件描述符为0(stdin)的句柄复制到input中
close(cgi_output[]);//关闭output的读端
close(cgi_input[]);//关闭input的写端
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);//putenv保存到环境变量中
if (strcasecmp(method, "GET") == ) {
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(path, path, NULL);//保存在环境变量中的数据,还有parent进行的write到cgi_input[1]中的数据,都是存在的,可以在cgi程序本身中进行判断。看起来有点复杂,我到时候实现就实现个简单的吧。
exit();
} else { /* parent */
close(cgi_output[]);//关闭output的写端
close(cgi_input[]);//关闭input的读端
if (strcasecmp(method, "POST") == )//Post方式,读取后面还没有读的附带信息
for (i = ; i < content_length; i++) {
recv(client, &c, , );
write(cgi_input[], &c, );//读取到的信息一个一个字符写到input的写端
}
while (read(cgi_output[], &c, ) > )//循环读取output的读端,然后发送个客户端,注意这里接收的是cgi程序的输出(也就是打印在stdin上的数据)
send(client, &c, , ); close(cgi_output[]);
close(cgi_input[]);
waitpid(pid, &status, );//等待子进程结束
}
}

  上面第72行处,原来的代码就是那样,但据说好像是错的。应该是:execl(path,参数列表,NULL);而参数列表对于get方法就是query_string,而对于post方法就没有参数,它的参数是在父进程中第80行处通过stdin进行输入,所以cgi程序要手动从控制台stdin读取数据。现在重要的函数都基本完了,接下来就是几个应答信息头。

  400 Bad Request

 void bad_request(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
send(client, buf, sizeof(buf), );
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, sizeof(buf), );
sprintf(buf, "\r\n");
send(client, buf, sizeof(buf), );
sprintf(buf, "<P>Your browser sent a bad request, ");
send(client, buf, sizeof(buf), );
sprintf(buf, "such as a POST without a Content-Length.\r\n");
send(client, buf, sizeof(buf), );
}

  500 Internal Server Error

 void cannot_execute(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
send(client, buf, strlen(buf), );
}

  200 OK

 void headers(int client, const char *filename)
{
char buf[];
(void)filename; /* could use filename to determine file type */ strcpy(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), );
strcpy(buf, SERVER_STRING);
send(client, buf, strlen(buf), );
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), );
strcpy(buf, "\r\n");
send(client, buf, strlen(buf), );
}

  404 Not Found

 void not_found(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), );
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "your request because the resource specified\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "is unavailable or nonexistent.\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), );
}

  501 Method Not Implemented

 void unimplemented(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), );
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "</TITLE></HEAD>\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), );
}

  这个简单的服务器目前应该是不支持图片声音等非文本信息(到我自己写时,不知道能不能实现)。总的来说,这次对整个HTTP协议的处理过程,还有Web服务器的内部实现简单的进行了解。接下来的几个小节,我就自己参考这个程序,自己写一个。当然代码肯定没有这个程序那么简练。不过如果可以实现,还是不错的。到时候对我开发web服务器是遇到的问题再进行讲解。

  参考资料:

  GET,POST的区别  http://blog.sina.com.cn/s/blog_50e4caf701009eys.html

  什么是CGI           http://www.doc88.com/p-173100939493.html

  本文地址: http://www.cnblogs.com/wunaozai/p/3926033.html

Socket网络编程--简单Web服务器(1)的更多相关文章

  1. Socket网络编程--简单Web服务器(6)

    本来是想实现ssl连接的,但是弄了好久都不成功,就索性不做了,等以后有能力再做了.所以这一小节就是本次的最后一节了.就简单的说几个注意点. 1.加个配置文件 使用单例模式,使用一个类,该类保存一些信息 ...

  2. Socket网络编程--简单Web服务器(2)

    上一小节通过阅读开源的Web服务器--tinyhttpd.大概知道了一次交互的请求信息和应答信息的具体过程.接下来我就自己简单的实现一个Web服务器. 下面这个程序只是实现一个简单的框架出来.这次先实 ...

  3. Socket网络编程--简单Web服务器(3)

    上一小节已经实现了浏览器发送请求,然后服务器给出应答信息,然后浏览器显示出服务器发送过来的网页.一切看起来都是那么的美好.这一小节就准备实现可以根据地址栏url的不同来返回指定的网页.目前还不考虑带参 ...

  4. Socket网络编程--简单Web服务器(4)

    上一小节已经实现了对图片的传输,接下来就是判断文件是否为js,css,png等格式.我们增加一个函数用于判断格式 int WebServer::get_filetype(char *type,char ...

  5. Socket网络编程--简单Web服务器(5)

    这一小节我们将实现服务器对get和post的请求进行对cgi程序的调用.对于web服务器以前的章节已经实现了对get和post请求的调用接口,接下来给出对应接口的实现. int WebServer:: ...

  6. C++ socket 网络编程 简单聊天室

    操作系统里的进程通讯方式有6种:(有名/匿名)管道.信号.消息队列.信号量.内存(最快).套接字(最常用),这里我们来介绍用socket来实现进程通讯. 1.简单实现一个单向发送与接收 这是套接字的工 ...

  7. C#中使用Socket实现简单Web服务器

    上一篇博客中介绍了怎样使用socket访问web服务器.关键有两个: 熟悉Socket编程: 熟悉HTTP协议. 上一篇主要是通过socket来模拟浏览器向(任何)Web服务器发送(HTTP)请求,重 ...

  8. Java Web 基础(一) 基于TCP的Socket网络编程

    一.Socket简单介绍 Socket通信作为Java网络通讯的基础内容,集中了异常.I/O流模式等众多知识点.学习Socket通信,既能够了解真正的网络通讯原理,也能够增强对I/O流模式的理解. 1 ...

  9. python之Socket网络编程

    什么是网络? 网络是由节点和连线构成,表示诸多对象及其相互联系.在数学上,网络是一种图,一般认为专指加权图.网络除了数学定义外,还有具体的物理含义,即网络是从某种相同类型的实际问题中抽象出来的模型.在 ...

随机推荐

  1. 想要进步,就要阅读大神的博客,再推荐一波springmvc映射路径之url的action请求

    http://www.cnblogs.com/liukemng/p/3726897.html

  2. 《深入探索Androdi热修复技术原理(阿里巴巴)》--读书笔记

    No1: Hybrid就是原生和Html5混合开发app No2: 插件化方法Altas或者DroidPlugin No3: 热修复技术可以把更新补丁上传到云端,此时APP就可以直接从云端下拉补丁直接 ...

  3. macos 下安装virtualenv,virtualenvwrapper,然后在pycharm中正常配置方法日志

    1.安装virtualenv或virtualenvwrapper pip install virtualenv pip install virtualenvwraper 注意pip的版本号(查看 pi ...

  4. IPython3 notebook 成功配置Python2和Python3内核(Kernel)

    1.首先通过python3的pip3安装ipython sudo pip3 install ipython 2.安装python 内核 python2: sudo pip2 install ipyke ...

  5. 洛谷 P1141【BFS】+记忆化搜索+染色

    题目链接:https://www.luogu.org/problemnew/show/P1141 题目描述 有一个仅由数字 0 与 1 组成的n×n 格迷宫.若你位于一格0上,那么你可以移动到相邻 4 ...

  6. 为什么要使用getter/setter

    变量私有化的好处 1. 在setter中可以加入合法性检查,比如设置颜色的函数中,对于RGB颜色要判断其值在0~255之间. 2. 更新与被设置变量相关的其它变量的值,比如在一个潜水艇模拟系统中,改变 ...

  7. 2827: 千山鸟飞绝 非旋treap

    国际惯例的题面:看起来很不可做的样子,我们先来整理一下题意吧.就是,维护每个点曾经拥有过的最大的两个属性值,支持把点的位置移动.我们用map对每个位置进行离散化,对每个位置建立一个平衡树.为了方便分离 ...

  8. BZOJ.2229.[ZJOI2011]最小割(最小割树)

    题目链接 题意:给定一张无向图,求任意两点之间的最小割. 在所有点中任选两个点作为源点\(S\).汇点\(T\),求它们之间的最小割\(ans\),并把原图分成两个点集\(S',T'\),用\(ans ...

  9. BZOJ.4543.[POI2014]Hotel加强版(长链剖分 树形DP)

    题目链接 弱化版:https://www.cnblogs.com/SovietPower/p/8663817.html. 令\(f[x][i]\)表示\(x\)的子树中深度为\(i\)的点的个数,\( ...

  10. Scrapy爬虫框架的安装

    Scrapy框架是我在Windows中遇到的最难安装的第三方库,一直不想写这篇博客,但碰巧今天重装了系统,这正好是个机会? 1.安装pywin32:https://sourceforge.net/pr ...