您的位置:首页 > 理论基础 > 计算机网络

UNIX网络编程笔记(4):简单的回射程序

2015-12-02 15:50 417 查看
上一讲中我们通过调用fork函数实现了一个简单的并发时间获取服务器。这是一个简单的并发服务器框架,然而这里使用这个框架实现一个简单的回射服务器会出现一个问题,这个问题就是僵尸子进程。

1、回射程序

下图是这个回射程序的主要结构:



客户向服务器发送一行文字,服务器回射回来,然后客户再输出到屏幕上。这里的fgets、fputs、read、write函数都是标准库函数或系统调用,唯一要说明的就是这个readline函数,这个函数读取文件中的一行,调用read系统调用:

int readline(int fd,void *vptr,size_t maxlen)
{
ssize_t n,rc;
char c,*ptr;
ptr=vptr;
for(n=1;n<maxlen;n++)
{
again:
if((rc=read(fd,&c,1))==1)
{
*ptr++=c;
if(c=='\n')
break;
}
else if(rc==0)
{
*ptr=0;
return (n-1);
}
else
{
if(errno==EINTR)
goto again;
return (-1);
}
}
*ptr=0;
return(n);
}
这里的服务器程序还是遵循上一讲中的流程,不同的是处理请求的过程,服务器处理请求时调用一个函数str_echo,返回客户写入的一行,代码如下:

#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#define MAXLINE 1024
void str_echo(int sockfd);
int main(int argc,char *argv[])
{
int listenfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
time_t ticks;
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
{
printf("socket error\n");
return 0;
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(5000);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
printf("bind error\n");
return 0;
}
if(listen(listenfd,5)<0)
{
printf("listen error\n");
return 0;
}
int connfd;
socklen_t len;
struct sockaddr_in cliaddr;
pid_t pid;
for(;;)
{
len=sizeof(cliaddr);
if((connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&len))<0)
{
printf("accept error\n");
return 0;
}
if((pid=fork())==0)
{
if(close(listenfd)<0)
{
printf("close listenfd error\n");
return 0;
}
printf("[PID]%ld Receive a connection from:%s.%d\n",(long)getpid(),inet_ntop(AF_INET,&cliaddr.sin_addr,buff,sizeof(buff)),ntohs(cliaddr.sin_port));
str_echo(connfd);
if(close(connfd)<0)
{
printf("close child connfd error\n");
return 0;
}
return 0;
}
if(close(connfd)<0)
{
printf("close parent connfd error\n");
return 0;
}
}
}

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)
{
printf("read error\n");
return;
}
}
客户端程序也大体相同,首先从标准输入中读取一行,写入套接字发送给服务器,接收到套接字后将套接字的内容输出到标准输出上,代码如下:

#include <sys/socket.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 1024
void str_cli(FILE *fp,int sockfd);
int readline(int fd,void *vptr,size_t maxlen);
int main(int argc,char *argv[])
{
int sockfd;
char recvline[MAXLINE];
if(argc!=2||strcmp(argv[1],"--help")==0)
{
printf("Usage:%s <IPaddress>\n",argv[0]);
return 0;
}

if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)
{
printf("socket error\n");
return 0;
}
struct sockaddr_in servaddr;
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(5000);
if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<=0)
{
printf("inet_pton error for %s\n",argv[1]);
return 0;
}
if(connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
{
printf("connect error\n");
return 0;
}
str_cli(stdin,sockfd);
return 0;
}
void str_cli(FILE *fp,int sockfd)
{
char sendline[MAXLINE],recvline[MAXLINE];
printf("puts:");
while(fgets(sendline,MAXLINE,fp))
{
write(sockfd,sendline,strlen(sendline));
if(readline(sockfd,recvline,MAXLINE)==0)
{
printf("str_cli:server terminated prematurely\n");
return;
}
printf("gets:");
fputs(recvline,stdout);
printf("puts:");
}
}
int readline(int fd,void *vptr,size_t maxlen) { ssize_t n,rc; char c,*ptr; ptr=vptr; for(n=1;n<maxlen;n++) { again: if((rc=read(fd,&c,1))==1) { *ptr++=c; if(c=='\n') break; } else if(rc==0) { *ptr=0; return (n-1); } else { if(errno==EINTR) goto again; return (-1); } } *ptr=0; return(n); }
下面是运行结果:

(1)启动服务器:



(2)客户发起请求并输入文字:



服务器成功返回并输出到标准输出上。

(3)服务器端:



2、程序执行时发生了什么?

1、启动时

(1)服务器启动后阻塞在accept函数上,等待客户连接。客户发起连接后accept函数返回,服务器进入子进程调用str_echo函数,函数调用read系统调用,并阻塞在这里,因为客户还没有输入文字;

(2)客户建立连接后,调用str_cli函数,函数调用fgets并阻塞,等待用户输入;

(3)与此同时,服务器父进程关闭连接描述符connfd,重新阻塞在accept函数上,等待下一个客户连接;

以上就是连接建立后发生的事情。

2、执行时

(1)执行时用户在客户端输入文字,fgets函数返回,str_cli函数将输入写入套接字,发送给服务器子进程,然后调用readline,readline函数调用read,并阻塞于read,因为客户等待服务器的应答;

(2)服务器子进程调用read读取客户输入,写入套接字发送给客户作为应答并继续阻塞在read函数上;

(3)客户调用readline函数读取服务器子进程应答并将数据写入标准输出;

(4)循环进行直到客户得到用户的EOF输入;

3、终止时

(1)用户在客户输入EOF字符,fgets返回NULL指针,于是str_cli函数返回;

(2)str_cli函数返回到main函数,main然后通过return终止;

(3)进程终止后关闭所有打开的描述符,因此客户打开的套接字由内核关闭,导致客户TCP发送一个FIN给服务器,服务器以ACK响应,这就是TCP终止的前半部分;

(4)当服务器接收到FIN时,服务器子进程阻塞于readline函数,readline函数返回0,导致str_echo函数返回到main;

(5)服务器子进程通过return终止;

(6)服务器子进程打开的所有描述符关闭。由子进程来关闭已连接套接字会引发TCP连接终止序列的最后两个分节:一个从服务器到客户的FIN和客户到服务器的ACK。至此,连接完全终止;

3、有什么问题?

上面的进程终止还有一部分内容,就是服务器子进程终止时,会给父进程发送一个SIGCHLD信号。这里我们没有捕获信号,而该信号的默认行为是被忽略。由于父进程中没有处理该信号,子进程于是进入僵死状态,即僵尸进程。可以使用ps命令查看:



可以看到,进程号是4228的进程是僵尸进程,而在我们的服务器运行时,客户连接的子进程ID正好是4228。

僵尸进程必须处理。下一讲来用信号处理函数处理僵尸进程。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: