分类: 2010 May

杀死习惯

今天面试腾讯后台开发,漏一道小题目..简历项目上写了简单的web服务器,于是和蔼可亲又高又帅的面试官问了一个问题,子进程进行了什么操作?我一听就懵了,这不是自证的吗,该做的不该做的不是一幕了然吗?根本抓不到所问的知识点,搞得都不知道怎么回答了,貌似面试官也不知道该怎么提示我,只得听我胡吹一通..最后,在我讲到子进程结束时调用exit(0);才想起来,原来面试官想考的是内存的清理回收,可惜当我意识到这一点时,已经浪费了不少时间了..聊完内存释放,突然又想到,套接口描述字的关闭又忘记说了,这才是一个无比重要的亮点啊!

无法展示自己的亮点真的是挺悲剧的一件事,究其原因,无外乎是因为对于某些习以为常的东西没有放在心上,以致于毫无道理地认为它原本就该那样子了,得之于形,背后的精髓却被忽略了.习惯是一种很可怕的力量,比如说这么一道题目,求出10亿个数中最大的10000个..我一直以为最大堆已经是最优的算法了,前几天才知道,原来还有一个O(n)的算法..这个例子或许不够深刻,但某天突然发现一直信以为真的知识居然是错误的时候,真的有一种价值观坍塌的感觉..再举一个面试官今天问到的题目为例,在vim中怎样快速对齐代码..这又是一个水题,选中按<键就OK了,可悲剧同样发生了,我在桌上一边画着小于号,却把它说成是方向键了..面试结束走出门口时真觉丢尽了脸面..这还不算,记得有一次,我坐在公园里绿油油的草地上,看见一个公告牌,对着里面的英语念了几乎有快10遍,一直到走的时候,才发现,原来我一直在念着,”please don’t step over the grass.”..

或许最后一个例子算不上是习惯的缘故吧,可能我当时太入迷了..正如今天和面试官聊的很起劲一样,面试结束了,一起走到门口,我们还小聊了一会,直到面试官帮我打开了门…

习惯是得杀死的.

简单的web服务器

吐槽

最近看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服务器运行如下,没有实现多线程下载………..:(

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

Read The Fucking Manual

今天不小心在freenode上发个问题,牛人一句”Read The Fucking Manual!”顶了回来,只得乖乖RTFM..一直就听说man命令很强大,却仅仅用了其中一丁点功能,有点暴殄天物的感觉.

下面以编写who命令为例,记一下man的用法,做个备忘.

首先需要知道who命令可以干些什么:

arthur@arthur-desktop:~$ who

who命令输出了一堆东西,根据输出大概可以猜到who的作用了,显示关于当前在本地系统上的所有用户的信息..接下来看一下who的手册:

arthur@arthur-desktop:~$ man who

又是一个把终端淹没的信息流,仔细看下,其中的NAME部分包含命令的名字以及命令的简短说明.DESCRIPTION部分是关于命令功能的详细阐述.在DESCRITION的最后,我们看见了这么一条信息:

…If FILE is not specified, use /var/run/utmp.  /var/log/wtmp as FILE  is common….

上述描述说明,已登录的用户信息是存放在/var/run/utmp中的,who通过该文件获得具体信息.不妨看看这个文件的结构信息,这可以用man -k来实现,它根据关键字在联机文档中搜索:

arthur@arthur-desktop:~$ man -k tmp

输出很多,其中有这么一条是值得我们注意的:

arthur@arthur-desktop:~$ man -k utmp
utmp (5)             – login records

其中,5是小节编号,说明该文档是在第5节,继续查看utmp,注意别把小节编号漏了:

arthur@arthur-desktop:~$ man 5 utmp

这次终于搜索得差不多了,第5节中utmp的帮助文档写得足够充分,值得注意的是SYNOPSIS,它注明了utmp的头文件(#include <utmp.h>),假如对其有兴趣,我们可以打开它仔细读读组织结构,文件位置可以从utmp的帮助文档FILES部分得到:

arthur@arthur-desktop:~$ more /usr/include/utmp.h

接下来重新分析who的输出,查一下资料,很容易知道who中有两件事是需要做的:

  1. 从文件中读取数据结构
  2. 将结构中的信息以合适的形式显示出来

这就有了一个问题,既然是读取文件,那么肯定需要用到read,查看其帮助文档:

arthur@arthur-desktop:~$ man read

其中,又可以看到write,open等系统调用,就这样,把who ,utmp,open,read,write等帮助文档看完,就可以自己写一个who命令了..

PS: 其实通常是这样做的,先猜测utmp需要的系统调用,然后过滤man的帮助文档.. 如man 5 utmp | grep read,不然累死了..