吐槽

最近看unp,看了都快二十章了,谁料unp和ldd差不多,整本书从到到尾一根筋的就优化一个服务器程序,从TCP到UDP再到SCTP,从ipv4到ipv6,从迭代到并发,从select到poll再到epoll,从进程到线程,从阻塞到非阻塞,从异步到信号驱动,从广播到多播,不一而足,醒人耳目,真不愧是本神书..可是我却根本没动过手,唯一能做的就是盯着不到80行的程序揣摩Stevens先生的奥义,都不知道学的到底怎么样..这几天真的看不下去了,于是读了读ping和tracert,proxy的源码,写个简简单单的web服务器好了.记下备忘...

一点知识

HTTP

客户端(浏览器)与Web服务器之间的交互主要包含客户端的请求和服务器的应答.请求和应答的格式在HTTP协议中有相应的定义.其中,GET应该是使用最广的方法,即客户端请求某个文件,服务器做出响应.为了知道GET请求具体的格式,我们可以用telnet测试:

arthur@arthur-desktop:~$ telnet godorz.cn 80

然后敲入 GET /in.html HTTP/1.0 ,两个回车,得到响应如下:

这里只发送了一个请求,却接收到了多行返回.细节如下:

1.HTTP请求: GET

telnet创建了一个socket并调用connect来连接到Web服务器,服务器接受请求,并创建一个基于socket的从客户端终端到服务器的数据通道.

一个HTTP请求包含了3个字符串,第一个字符串是命令,第二个是参数,第三个是所使用的协议版本号..在该例中,GET为命令,以/in.html作是参数,HTTP/1.0为版本号.

2.HTTP应答: OK

服务器读取请求并返回响应,应答分为头部和内容两个部分.其中,头部以状态行起始,此例中为 HTTP/1.1 200 OK, 状态行第一个字符串为协议版本(HTTP/1.1),第二个串为返回码(200,如果文件不存在,则返回码为404),文本解释为OK..头部的其他部分为附加信息,包括服务器名,应答时间,数据类型以及连接类型等.最后,应答的其余部分为文件内容..

文件操作

文件属性

一旦得到pathname,那么我们就可以使用stat函数返回与此文件有关的信息结构.翻一下APUE,可以知道stat所含内容主要有:

  • mode_t st_mode; /* 文件类型和许可权限 */
  • uid_t st_uid; /* 用户所有者的ID */
  • gid_t st_gid; /* 所属组的ID */
  • off_t st_size; /* 所占字节数 */
  • time_t st_mtime; /* 文件最后修改时间 */

其中,需要注意的是st_mtime是time_t类型,我们可以用ctime将其转为字符串.

还有一个需要注意的是st_mode,它是一个16位的二进制数,文件类型和许可权限被编码在st_mode中..如下所示:

解码比较复杂,简单的说,就是用一系列的掩码来把st_mode的值转为ls -l要显示的字符串.原理是,文件类型在st_mode第一个字节的前四位,所以我们可以通过掩码来将其余部分置0,从而得到类型的值..至于许可权限,它在st_mode的最低9位,同样可以用掩码得到.一个很简单的例子为:

if(S_ISDIR(mode))  str[0] = 'd';    /* 是否为目录?  */

其中,S_ISDIR为宏命令,终端man stat可以查到.

最后,怎样将用户/组 ID转换成字符串呢? 答案是,用getpwuid得到完整的用户列表,用getgrgid得到组列表,然后查询用户名/组名.

Web服务器

前面的知识储备已经足够编写一个最简单的Web服务器了,它只支持GET方法,接受请求行,跳过其余参数,然后处理请求和发送应答..

程序的构造如图,在一系列初始化后,主进程掉入死循环,阻塞等待客户端连接请求,一旦有connect,主进程将fork一个子进程处理请求,而主进程自身继续等待客户端连接请求..

上面的都没什么难的,unp第5章几乎就把实现给出来了..需要注意的是,为了杀死僵尸进程,我们可以使用waitpid等待结束的子进程(捕捉信号SIGCHLD)..如果accept()函数阻塞等待客户机调用connect()建立连接时,进程恰好捕捉到信号,那么accept在返回"-1"的同时将变量errno的值设置为EINTR.这和accept()函数执行失败是有区别的(仅返回-1,errno值不变).

还有一个需要注意的是,为了列出一个目录下的所有文件或者子目录,我们常常这么写:

while((direntp = readdir(dirPtr)) != NULL)
dosomething(direntp->d_name, ...);

这里的direntp->d_name仅仅是当前目录下的文件名/子目录名,它并不是绝对地址,所以为了stat此文件,我们需要将它转为绝对地址,即  sprintf(realFilename, "%s/%s", dirname, direntp->d_name);..

最后,当客户端请求一个文件时,最简单的做法当然是将文件所有内容一次性的往客户端连接描述字里写,但这又引出了一个问题,假如文件教大,那么不管是服务器还是客户端,都会出现内存消耗过多的局面,而且下载速度过慢..一个比较好的解决方法是,使用多线程下载..

下面写写我理解的多线程下载.

客户端发送GET请求时,可以使用"Range: bytes=r1 - r2"参数,服务器将只传送指定文件中从第r1个字节到r2个字节之间的内容.因此,在实现一个下载函数时,我们可以将文件均分为m段,然后pthread_create多个线程,分别请求各段,在各线程都完成下载后,在本地将其拼接成一个完整的文件..多线程的下载还能带来一个好处,当网络连接出现问题时,我们可以将各线程的下载进度保存到文件中,作为断点信息,线程重启后从断点处重新开始下载.这样,我们就可以实现断点传送的特性了..

最后,一个简单web服务器运行如下,没有实现多线程下载...........:(

已详细注释的源代码在这..欢迎交流.