如果在设计服务器的时候,不注重细节处理,那么这样的服务器仅仅只能算是小孩儿的游戏,是不具有实用性的,为此单开一篇博客来记录服务器设计中的细枝末节。
#close之后会发生什么?
##问题描述
close一个TCP套接字的默认行为是把该套接字标记成已关闭。该套接字不能再由调用进程调用,即这个套接字以后就不能再作为read或write的参数。但要注意TCP将尝试---------发送已排队等待发送到对端的任何数据----------(套接字发送缓冲区里面的数据),发送完毕后发生的是正常的TCP连接终止序列。
##问题解答
至于如何设置:主要有1/SO_LINGER套接字选项来决定;2/若由close转换为shutdown则通过配合SHUT_RD和SHUT_WR来使用
#Ctrl+D就正常终止进程了吗?
##问题描述
当通过客户端输入Ctrl+D时来终止与服务器端的连接,如果处理不慎,往往会有所残留(通常对于新手会造成,客户进程已关闭,但相对应的服务器子进程没有被真正的关闭而是进入到“僵死”状态)。这样的原因是:当服务器子进程终止时,给父进程发送一个SIGCHLD信号,而对于新手的程序,很有可能会忽略这些细节。如果我们没有在代码中捕获该信号,对其进行信号处理,而该信号的默认行为就是忽略我吧,哈哈。父进程未加处理这个信号,子进程就将进入“僵死”状态。
##做一个实验
我们以大家都会遇到的第一个服务-客户程序为例。这个程序无论是会是任何网络编程书上的第一个例子:代码如下:
#include "config.h"
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while((n=read(sockfd,buf,MAXLINE))>0)
write(sockfd,buf,n);
if(n<0&& errno ==EINTR)
goto again;
else if(n<0)
{perror("str_echo : read error");
exit(1);
}
}
int main(int argc,char **argv)
{
int listenfd,connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr,servaddr;
/*step 1: set up a listen socket*/
listenfd=socket(AF_INET,SOCK_STREAM,0);
/*step 2: bind server address*/
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
/*step3: listen*/
listen(listenfd,LISTENQ);
/*step4: */
for( ; ; )
{
connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&clilen);
if((childpid=fork())==0)
{
close(listenfd);
str_echo(connfd);
close(connfd);
exit(0);
}
close(connfd);
}
}
客户端程序
#include "config.h"
ssize_t readline(int fd,char *vptr,size_t maxlen)
{
ssize_t n,rc;
char c,*ptr;
ptr=vptr;
for(n=1;n<maxlen;n++)
{
if((rc=read(fd,&c,1))==1){
*ptr++=c;
if(c=='\n')
break;
}else if(rc==0){
*ptr=0;
return(n-1);
}else
return(-1);
}
*ptr=0;
return(n);
}
void str_cli(FILE *fp,int sockfd)
{
char sendline[MAXLINE],recvline[MAXLINE];
while(fgets(sendline,MAXLINE,fp)!=NULL)
{
write(sockfd,sendline,strlen(sendline));
if(readline(sockfd,recvline,MAXLINE)==0)
{perror("str_cli:server terminated prematurely");
exit(1);
}
fputs(recvline,stdout);
}
}
int main(int argc,char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if(argc!=2)
{perror("usage:tcpcli <IPaddress>");
exit(1);
}
/*step1 */
sockfd=socket(AF_INET,SOCK_STREAM,0);
/*step2 */
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
connect(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr));
str_cli(stdin,sockfd);
exit(0);
}
头文件
#ifndef CONFIG_H_
#define CONFIG_H_
#include <sys/socket.h>// socket functions
#include <unistd.h>// fork,read,write functions
#include <netinet/in.h>//address struct
#include <sys/errno.h>
#include <stdio.h>
#include <string.h> //bzero function
#include <stdlib.h>
#define SERV_PORT 9877
#define LISTENQ 100
#define MAXLINE 100
#endif /* CONFIG_H_ */
例如上面这个程序,当我们按下ctrl+d终止之后,输入ps -a -o pid,ppid,tty,stat,args,wchan,我们可以看到当前进程的状态,状态Z代表僵死(zombie)
PID PPID TT STAT COMMAND WCHAN
72432 72361 pts/0 S+ ./server_cha5 inet_csk_accept
72469 72432 pts/0 Z+ [server_cha5] exit
72577 72433 pts/1 R+ ps -a -o pid,ppid,tty,stat, -
##解决办法
signal(SIGCHLD,sig_chld);
调用signal函数处理子进程在被关闭的时候传递给父进程的SIGCHLD的信号(其实本身已经有signal函数处理,但默认处理为忽略该信号,我们这里需要自己定义如何处理该信号)。下面为具体接收到SIGCHLD的具体处理函数。
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid=wait(&stat);
printf("child %d terminated\n",pid);
return;
}
注意点:signal函数的调用应该在listen()函数之后,fork()函数之前
我们再来看下添加完信号处理之后的运行情况:
PID PPID TT STAT COMMAND WCHAN
6395 6361 pts/1 S+ ./server_cha5 inet_csk_accept
6468 6396 pts/2 R+ ps -a -o pid,ppid,tty,stat, -
在终端CTRL+D,之后,
ps -a -o pid,ppid,tty,stat,args,wchan
查看各个进程的状态,发现客户进程和,与之相连接的服务器子进程已经正常终止,无僵尸进程,只剩下服务器父进程阻塞与accept等待客户的再一次连接。