一般认为Web服务器程序是一个长时间(后台)运行的程序(即,守护程序,daemon) -> 此类程序会被以进程的形式初始化,守护进程程序的名称通常以字母“d”结尾,如httpd。通常由客户发起请求可以简化协议和程序本身,某些复杂的网络应用需要异步回调(asynchronous callback)通信,由服务器向客户发起请求信息。
在一个多任务的电脑操作系统中,守护进程(英语:daemon,英语发音:/?di?m?n/或英语发音:/?de?m?n/)是一种在后台执行的电脑程序。此类程序会被以进程的形式初始化。守护进程程序的名称通常以字母“d”结尾:例如,syslogd就是指管理系统日志的守护进程。
通常,守护进程没有任何存在的父进程(即PPID=1),且在UNIX系统进程层级中直接位于init之下。守护进程程序通常通过如下方法使自己成为守护进程:对一个子进程运行 fork,然后使其父进程立即终止,使得这个子进程能在 init 下运行。这种方法通常被称为“脱壳”。
系统通常在启动时一同起动守护进程。守护进程为对网络请求,硬件活动等进行响应,或其他通过某些任务对其他应用程序的请求进行回应提供支持。守护进程也能够对硬件进行配置(如在某些Linux系统上的devfsd),运行计划任务(例如cron),以及运行其他任务。
在DOS环境中,此类应用程序被称为驻留程序(TSR)。在Windows系统中,由称为Windows服务的应用程序来履行守护进程的职责。
在原本的Mac OS系统中,此类应用程序被称为“extensions”。而作为Unix-like的 Mac OS X有守护进程。(在Mac OS X中也有“服务”,但他们与Windows中类似的程序在概念上完全不相同。)
书上第5页的程序,时间获取客户程序:
#include "unp.h" int main(int argc, char **argv) { int sockfd, n; char recvline[MAXLINE + 1]; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: a.out <IPaddress>"); if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) err_sys("socket error"); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(13); /* daytime server */ if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) err_quit("inet_pton error for %s", argv[1]); if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) err_sys("connect error"); while ( (n = read(sockfd, recvline, MAXLINE)) > 0) { recvline[n] = 0; /* null terminate */ if (fputs(recvline, stdout) == EOF) err_sys("fputs error"); } if (n < 0) err_sys("read error"); exit(0); }
使用的前提是需要有unp.h,配置方法摸我
主要步骤:
- 创建TCP套接字 sockfd = socket(AF_INET,SOCK_STREAM, 0);
// socket函数创建套接字,表示网际(AF_INET,IPv4的协议,如果为v6的话则是AF_INET6)字节流(SOCK_STREAM,tcp)。返回值是一个小整数(int)描述符,以后所有的函数调用都用该描述符来标识这个套接字,返回值-1表示错误
- 指定服务器的IP地址和端口号
// 先清空结构体的内容(bzero),htons(int)表示转换端口为short类型,inet_pton是inet_addr的升级版,支持IPv6,用于将点分十进制转换成二进制,如127.0.0.1 -> 0xFF000001
- 建立与服务器的连接
// 传入套接字描述符,服务器地址结构体,服务器地址结构体的长度
- 读入并输出服务器的应答
// 使用read函数来读取服务器的应答,TCP是一个没有记录边界的字节流协议(一次发送的字节数不定)。当数据量很大的时候,不能确保一次read就可以返回服务器的整个应答,因此从TCP套接字读取数据时,要写在while(>0)中,当read返回0(表明对端关闭连接)或负值(错误)的时候终止。可以看出,n表示返回的记录的长度,故recvline[n] = 0;是指添加结束符(TCP本身并不提供记录结束标志),便于输出。
- 终止程序
// exit终止程序运行,Unix在终止进程的同时,关闭该进程所有打开的描述符,TCP套接字就此被关闭
定义包裹函数(wrapper function) -> 约定首字母大写,有助于代码的简洁,可以实现错误处理。
以下用包裹函数来写一个时间获取的服务器程序:
#include "unp.h" #include <time.h> int main(int argc, char **argv) { int listenfd, connfd; struct sockaddr_in servaddr; char buff[MAXLINE]; time_t ticks; listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(13); /* daytime server */ Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); for ( ; ; ) { connfd = Accept(listenfd, (SA *) NULL, NULL); ticks = time(NULL); snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); Write(connfd, buff, strlen(buff)); Close(connfd); } }
主要步骤:
- 创建TCP套接字
- 将地址(用于给其他客户端连接的服务器端口)捆绑到套接字中
// INADDR_ANY表示任意的(但只能到达其中一个网卡)网络接口
INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。
一般情况下,如果你要建立网络服务器应用程序,则你要通知服务器操作系统:请在某地址 xxx.xxx.xxx.xxx上的某端口 yyyy上进行侦听,并且把侦听到的数据包发送给我。这个过程,你是通过bind()系统调用完成的。——也就是说,你的程序要绑定服务器的某地址,或者说:把服务器的某地址上的某端口占为已用。服务器操作系统可以给你这个指定的地址,也可以不给你。
如果你的服务器有多个网卡(每个网卡上有不同的IP地址),而你的服务(不管是在udp端口上侦听,还是在tcp端口上侦听),出于某种原因:可能是你的服务器操作系统可能随时增减IP地址,也有可能是为了省去确定服务器上有什么网络端口(网卡)的麻烦 —— 可以要在调用bind()的时候,告诉操作系统:“我需要在 yyyy 端口上侦听,所有发送到服务器的这个端口,不管是哪个网卡/哪个IP地址接收到的数据,都是我处理的。”这时候,服务器程序则在0.0.0.0这个地址上进行侦听。
- 把套接字转换成监听套接字(这个套接字是专门用于让外来连接被内核接受)
//socket、bind和listen这3个调用步骤是任何TCP服务器准备“监听描述符(listening descriptor,本例中为listenfd)"的正常步骤。LISTENQ是在unp.h中定义,它指定系统内核允许在这个监听描述符上排队的最大客户连接数。
- 接受客户连接(TCP三次握手,并返回连接服务器的该用户的新描述符),发送应答
//服务器进程在accept调用中被投入睡眠,等待某个客户连接的到达并被内核接受(这一步是“建立TCP连接”),TCP连接使用所谓的“三次握手”来建立连接,握手完毕时accept返回,返回值称为“已连接描述符(connected descriptor)”的新描述符(connfd),该描述符用于与新近连接的那个客户通信。
- 终止连接(TCP四次挥手)
//close(connfd)引发TCP连接终止序列:每隔方向上发送一个FIN,每隔FIN又由各自的对端确认。