一、 一个简单TCP回射服务端程序
#include "unp.h" #define MAXLINE 1024 #define PORT 13 #define CONMAX 5 void err_sys(const char* s) { fprintf(stderr, "%s\n",s); exit(1); } void str_echo(int connfd) { int nbyte; char buff[MAXLINE+1]; again: while(nbyte=read(connfd,buff, MAXLINE)>0) write(connfd, buff, nbyte); if(nbyte<0&& errno=EINTR)//被中断,重启。后面详见 goto again; else if(nbyte<0) err_sys("read error"); } int main(int argc, char** argv) { int listenfd, connfd; struct sockaddr_in servaddr; listenfd=socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.port=htona(PORT); servaddr.sin_addr.s_addr=htonl(INADDR_ANY); bind(listenfd,(struct sockaddr*) &servaddr, sizeof(servaddr)); listen(listenfd, CONMAX); pid_t childpid; while(true) { connfd=accept(listenfd, (struct sockaddr*) NULL, NULL); if((childpid=fork())==0){ close(listenfd); str_echo(connfd); exit(0); } close(connfd); } } // socket bind listen connect accept涉及socket的这些函数正常情况下都返回0或者具体描述符,在错误情况下都返回-1,错误被记录在errno。 //这里就不做错误检查了。
1. 这个回射程序很简单,就是客户端给服务器字符串,然后服务端接受并返回给客户端。和前面的程序相比较,不同的是这里是多进程程序,调用fork来创建子进程。子进程处理函数为str_echo
二、一个简单TCP回射客户端程序
#include "unp.h" #define MAXCON 50 #define MAXLINE 1024 #define PORT 13 void err_sys(const char* s) { fprintf(stderr, "%s\n",s); exit(1); } void str_cli(int sockfd, FILE* fd) { char recvmsg[MAXLINE], sendmsg[MAXLINE]; int n; while(fgets(sendmsg,MAXLINE,fd)!=NULL)//fgets函数会自动在尾部加'\0' { write(sockfd, sendmsg,strlen(sendmsg)); if(read(sockfd,recvmsg, MAXLINE)<=0) //注意此时read返回0,不是正常终止,原因后面说 err_sys("readerror"); fputs(recvmsg, stdout); } } int main(int argc, char** argv) { int sockfd; struct sockaddr_in servaddr; char buff[MAXLINE+1]; if(argc!=2) err_sys("input error"); if((sockfd=sock(AF_INET,SOCK_STREAM, 0))<0) err_sys("socket error"); servaddr.sin_fimly=AF_INET; servaddr.sin_port=htons(PORT); if(inet_pton(AF_INET,argv[1], &servaddr.sin_addr)<=0) err_sys("ipaddress error"); if(connect(sockfd,(struct sockaddr *)servaddr, sizeof(servaddr))<0) err_sys("connect error"); str_cli(sockfd,stdin); exit(0); }
1. 这个客户端程序很简单,就是从标准输入读入一行数据,然后发送给服务器。然后从服务器读取一行数据,显示在标准输出上。
三、正常启动
1. 首先服务器端启动,然后阻塞在accept函数。
2. 客户端启动,connect连接。然后客户端调用str_cli函数,阻塞在fgets函数上,等待用户输入数据。
3. 服务器端accept接受连接,fork一个子进程,调用str_echo函数,然后子进程阻塞在read函数上,而服务器端的主进程又会阻塞在accept函数,等待其他用户连接。
至此我们三个进程都在阻塞。
四、正常终止
1. 当客户端输入EOF(Ctrl-D)时,我们来终止程序,此时客户端fgets返回NULL,然后函数str_cli返回,则客户端exit退出,于是客户端会关闭打开的所有描述符,于是向服务器端发送一个FIN。
2. 当服务器端接受到这个FIN时,read函数返回0,向客户端发送ACK确认。然后 str_echo函数终止,然后子进程exit终止,这会关闭其打开的描述符,所以会向客户端发送一个FIN。
3. 然后客户端收到FIN并确认ACK,则至此客户端进入TIME_WAIT状态,服务器端继续监听其他客户。连接终止。
注意:Unix子进程终止时,会给父进程发送一个SIGCHLD信号,在上例中我们没有处理该信号,默认是忽略信号。既然父进程未加处理,则子进程进入僵死状态。
注意僵死状态说明该进程并没有完全地终止,没有释放所有资源。下面我们要说一下如何让子进程完全终止消失。
五、 信号处理
1. 信号:就是告知某个进程发生了某个事件的通知。一般信号都是异步的,即进程事先不知道信号在何时发生。
2. 信号可以是:
(1) 一个进程给另一个进程发送信号。
(2) 内核给一个进程发送信号。
上面提到的SIGCHLD信号就是内核在一个进程终止时,给它的父进程发送的信号。
3. 我们对一个信号的处理有三种方式:设置如何处理信号使用sigaction函数来设置。
(1) 我们提供一个信号处理函数,只要特定的信号发生,它就会被调用。这种行为称为捕获信号。有两种信号不能被捕获:SIGKILL和SIGSTOP
这个信号处理函数的原型:
void handler (int signo); 即无返回值,且参数是一个整型。
(2) 我们可以把某个信号的处理设定为SIG_IGN忽略它。SIG_KILL和SIG_STOP这两种信号不能被忽略。注意此时我们调用sigaction函数来设置忽略,则此进程就会被交给系统init去回收,不会存在僵死状态。
子进程进入僵死状态的原因是父进程未加任何处理。我们可以调用sigaction函数设置为SIG_IGN,则就不会产生僵死状态了。
(3) 我们也可以把某个信号的处理设定为SIG_DFL,来启动它的默认处置。有的信号默认处置是忽略,有的信号默认是终止进程。
4. sigaction函数
我们调用sigaction函数来设置对某个信号的处理。但是sigaction函数比较复杂,而signal函数比较简单:
void (* Sigfunc)(int); Sigfunc signal(int sig, Sigfunc fun);
Sig为信号类型,如:SIGCHLD,fun为自定义的信号处理函数或SIG_IGN/SIG_DFL。然后函数返回该信号以前的信号处理函数指针。
但是POSIX规定要使用sigaction函数来设置信号。因为sigaction规定了一些有关信号处理的其他操作,而且signal函数在不同的系统之间的行为可能有所差别。有关如何使用sigaction详见P104,这里就不多说了。
5. POSIX保证某个信号在被处理函数处理期间,该信号是阻塞的。即如果此时,其他进程产生该信号,则该信号不会被提交。
注意:POSIX默认的是该信号在被处理函数处理处理期间,其他信号是不会被阻塞的,我们可以调用sigaction函数设置这个默认项。
且Unix信号是不排队的。即如果某个信号正在被处理,此时其他进程产生了多个该信号,则当这个信号被处理结束后,只有会产生一次信号提交。
六、 处理SIGCHLD信号
1. Unix设置僵死状态是为了维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括进程pid,终止状态,资源利用信息等。
如果一个进程终止,如果该进程有僵死子进程,则僵死子进程的父进程的id都会被设为1,则交给init处理,来回收所有的僵死子进程。
注意只有fork子进程时,才有可能产生僵死进程。所以当我们fork子进程时,一定要处理SIGCHLD信号。
2. 在上述程序中,我们显然不需要保留僵死进程,则我们想要为SIGCHLD信号设置一个我们自定义的信号处理函数。则我们在主进程中加入代码:
signal(SIGCHLD, handler); --注意这段代码一定要在第一个fork之前,且只要执行一次,所以在while循环之前加入此代码。
且必须在处理函数中调用wait/waitpid函数。
例如我们写一个处理函数:
void hander(int signo) { pid_t pid; int stat; pid=wait(&stat);//此时的pid就是子进程的id return ; }
3. 注意此时,看看程序,当一个子进程终止,向父进程递交SIGCHLD信号,此时父进程还正在阻塞accept。注意:此时父进程的accept会被中断,转而执行信号处理函数。即accept函数的优先级比较低。此时accept会发生错误,并且errno被设为EINTR。我们可以使用sigaction函数来设置处理该信号之后,会重启被中断的函数。
所以我们可以把上述的服务器端程序中的accept函数改为如下:
while(true) { if((connfd=accept(listenfd,(struct sockaddr*) NULL, NULL))<0) { if(errno==EINTR) continue; else err_sys("accept error"); }
注意此时accept可以重启的,read,write都是可以重启的。但是connect是不可以重启的,所以如果connect被中断,我们将调用select来等待连接完成。
七、 wait和waitpid函数
#include<sys/wait.h> pid_t wait(int* statloc); pid_t waitpid(pid_t pid, int* statloc, int option);
1. 可以看到waitpid提供了更多的选项,可以指定进程id以处理特定的子进程。-1表示处理第一个终止子进程。statloc和wait一样。option提供一些附加选项,其中最常用的就是WNOHANG,它告知waitpid在尚有未终止的子进程时,不要阻塞。
2. 我们建议使用waitpid,而不使用wait。原因:
考虑如下场景,当多个客户端连接上服务器端时,如果某个时刻,这多个客户端几乎同时终止。这样也就是多个子进程几乎同时终止,则同时给父进程发送多个SIGCHLD信号,而上文提到,Unix信号是不排队的,所以造成了不能完全处理多个子进程,导致存在多个僵死进程。
解决办法:使用waitpid
void hander(int signo) { pid_t pid; int stat; while((pid=waitpid(-1,&stat, WNOHANG))>0); return; }
此时我们使用WNOHANG来告知waitpid在尚有未终止的子进程时,不阻塞。而wait是阻塞的。
所以这样即使多个SIGCHLD信号几乎同时来到,waitpid不会阻塞,这样就能保证多个进程被正确处理,避免僵死进程的存在。
八、服务器进程终止
当我们正常的启动上述客户端和服务器端进程后,当某个时刻,服务器端的进程终止/崩溃了(可能是服务器端执行关机了,注意不是服务器主机崩溃)。
1. 这时,服务器端关闭打开的描述符,向客户端发送FIN。
2. 客户端响应ACK(注意一般响应ACK是内核自动完成的),但是此时我们看看客户端程序,其还阻塞在fgets上面,这就有问题了。客户端等待用户输入数据,当客户端输入数据后,阻塞在read上面,而服务器端接收到这个数据后发送一个RST,由于套接字接收缓冲区中已有一个第1步服务器发送FIN,所以read会去读这个FIN,然后导致read返回0,然后输出错误。导致程序终止。
3. 我们不希望出现这种情况,而是希望当服务器端程序终止,客户端这边应该立即知道,并且终止客户端。
4. 出现上述错误的原因在于,当FIN分组到达后,客户端阻塞在fgets上。客户此时有两个描述符,套接字和用户输入。它不能单纯的阻塞在某一个描述符上,所以后面我们提到使用select,poll等IO复用技术来解决这个问题。
插入的知识:
1. 发送/响应SYN,ACK,FIN,RST等分组是系统内核自动完成的,无需我们关心,即即使某个套接字描述符已经关闭,内核也可以发送RST,ACK等分组。
2. 套接字接收缓冲区的作用:当内核接收到对端发送的消息时,就会把这些消息放入套接字接收缓冲区中,如果此时进程正在处理其他程序,而不是阻塞在read上,等到进程处理完其他的程序后,调用read,这时read就会读出接收缓冲区内的消息。
九、 SIGPIPE信号
当一个进程向某个已收到RST分组的套接字描述符进行写操作时,内核就会向该进程发送SIGPIPE信号。
情景模拟:假如某个时刻服务器进程崩溃,客户端并不是知道,连续进行两次写操作,则第一次写操作,服务器端发送RST,第二次写操作就会发生SIGPIPE信号。
不管进程对该信号是处理还是忽略,第二个写操作都会返回EPIPE错误。
十、 服务器主机崩溃
注意这里不是主机关机。
则服务端不会发送任何东西,而客户端不知情,所以当客户端继续发送数据时,得不到任何东西,则它就会重传以希望得到ACK,大约过了9分钟左右,客户端才会发送重传。Read函数返回一个超时的错误。
十一、 服务器主机崩溃后重启
当服务器端崩溃后,9分钟内重启,此时服务器端接收到消息,而然服务器此时丢失了TCP连接的信息,所以它会向客户端发送RST分组。
客户端接收RST,然后read函数返回错误。
十二、服务器主机关机和服务器进程崩溃的效果是一样的。
十三、客户端、服务器端传送数据类型
我们可以在write函数中,发送二进制结构,如:结构体对象。
struct { int i; long k; }msg; write(sockfd,&msg, sizeof(msg));
但是很不提倡在socket传输中,有这样的二进制结构,因为如果客户端和服务器端某一方不支持这种数据结构,或字节序不一样,支持的long的字节不一样,这都会引起异常的现象。
所以在传输socket时,应该把所有的二进制结构转换为字符型文本串,即char数组的形式。再进行传输是明智的。