代码改变世界

Linux TCP server系列(2)-简单优化服务器和客户端程序

2011-09-15 16:48  Aga.J  阅读(2309)  评论(0编辑  收藏  举报

目标:
 在上个server中考虑更多细节问题,完善server。


思路:
 (1)服务器
父进程使用fork派生子进程后,如果子进程运行结束,那么该进程不会立刻被销毁,而会进入“僵尸状态”,仍然维护着自身的信息,这时候如果服务器父进程不加以处理,那么很快就会消耗完系统的内存空间,所以父进程需要监听子进程SIGCHLD信号,并做出处理以销毁残留信息,这里可以使用wait或者waitpid来实现。
我们在父进程调用listen之后,注册监听信号和信号处理函数signal(SIGCHLD, sig_child);
信号处理函数实现如下:
void sig_child(int signo)         //父进程对子进程结束的信号处理
{
 pid_t pid;
 int   stat;

 while( (pid=waitpid(-1,&stat,WNOHANG))>0)
 printf("child %d terminated\n",pid);

 return;
}
这样一来我们就可以应付子进程退出后的残留信息问题。注意这里使用waitpid,其
中使用WNOHANG参数来防止父进程阻塞在wait上。


(2)客户端
   客户端需要考虑下面几种边界情况,根据自身需求做出调整:
   a)客户端连接成功后,某个时刻向服务器发送信息,接着再调用read从服务器接收信息,如果在发送信息之前,服务器子进程关闭了,那么它会发送FIN给客户端TCP,客户端TCP则以ACK响应,然后客户端向服务器发送消息时,服务器就会以RST响应,如果这时候客户端继续调用read,由于客户端TCP已经从服务器接收FIN,所以read会返回0,这时候客户根本不知道之前所发送的东西没有到达服务器,所以在read返回0时要做恰当提示和处理。
   b)如果客户端不理会read的错误,继续发送信息给服务器,那么当一个进程向接收了RST的套接口进行写时,内核会给它发送一个SIGPIPE信号,该信号会终止进程,所以我们也需要自己捕获该信号并做处理。
   c)在read之前,服务器主机崩溃,它没有回传FIN,这样read会阻塞很长时间才放弃连接,这也是需要考虑的。
   考虑到客户端既需要从用户接收输入,又需要从服务器接收输入,其中一方在使用时会阻塞另一方,影响客户体验,所以这里使用select来做这两个描述符的监听。后文会将select使用到服务器上,提高服务器的性能。
   select的主要用法:
    1 在fd_set中设置被监听的描述符(输入描述符集,输出描述符集,异常输出描述符集)
    2 调用select函数开始监听(可以自行配置超时时间)
    3 如果监听到有数据流动,则使用FD_ISSET判断发生在哪个描述符,并做处理。
   代码如下:
 FD_ZERO(&rset);
 for(;;)
 {
  FD_SET(fileno(fp),&rset);
  FD_SET(sockfd,&rset);
  maxfdp1=std::max( fileno(fp),sockfd) +1;
  select(maxfdp1,&rset,NULL,NULL,NULL);            //使用select使得客户端不需要阻塞在标准IO 或者 TCP read的其中一个上

  if( FD_ISSET(sockfd,&rset))
  {
   if(read(sockfd,recvline,sizeof(recvline))==0)
   //那边发来需要有换行符表示串尾,不然这边要求接受的字节数还不到,或者还没到尾部,所以没有输出
   {
    printf("read error\n");
    //handle error;
    return;
   }
  // printf("readed:%d,str:%d",cr,strlen(recvline));
  //      如果tcp缓冲区内有多于预定义取的字节,则会自动调用read再取,直到结尾或者遇到换行符
   fputs(recvline,stdout);       //标准库函数fputs向标准IO上写一行
   bzero(&recvline,strlen(recvline));
  }
  if( FD_ISSET(fileno(fp),&rset))
  {
   if(fgets(sendline,sizeof(sendline),fp)==NULL)    //标准库函数fgets从标准IO上获取一行
   //fgets会加上换行符,如果有
    return;
   write(sockfd,sendline,strlen(sendline)-1);  
   //减一去除换行符
   bzero(&sendline,strlen(sendline));
  }
 }


实现:

server.cpp
  1 #include<sys/types.h>
2 #include<sys/socket.h>
3 #include<strings.h>
4 #include<arpa/inet.h>
5 #include<unistd.h>
6 #include<stdlib.h>
7 #include<stdio.h>
8 #include<string.h>
9 #include<errno.h>
10 #include<signal.h>
11 #include<sys/wait.h>
12 #include<pthread.h>
13
14 #define LISTEN_PORT 84
15
16 void str_echo(int sockfd); // 服务器收到客户端的消息后的响应
17
18 void sig_child(int signo); //父进程对子进程结束的信号处理
19
20 int main(int argc, char **argv)
21 {
22 int listenfd, connfd;
23 pid_t childpid;
24 socklen_t chilen;
25
26 struct sockaddr_in chiaddr,servaddr;
27
28 listenfd=socket(AF_INET,SOCK_STREAM,0);
29 if(listenfd==-1)
30 {
31 printf("socket established error: %s\n",(char*)strerror(errno)); //后面需要采用日志到方式来记录
32 //socket创建失败后可以让用户选择重新连接
33 }
34
35 bzero(&servaddr,sizeof(servaddr));
36 servaddr.sin_family=AF_INET;
37 servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
38 servaddr.sin_port=htons(LISTEN_PORT);
39
40 int bindc=bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
41 if(bindc==-1)
42 {
43 printf("bind error: %s\n",strerror(errno));
44 //绑定失败,错误提示
45 }
46
47 listen(listenfd,SOMAXCONN); //limit是SOMAXCONN
48
49 signal(SIGCHLD,sig_child); //子进程退出的信号处理
50 for(;;)
51 {
52 chilen=sizeof(chiaddr);
53
54 connfd=accept(listenfd,(struct sockaddr*)&chiaddr,&chilen);
55
56 if(connfd==-1)
57 printf("accept client error: %s\n",strerror(errno));
58 else
59 printf("client connected\n");
60
61 if((childpid=fork())==0)
62 {
63 close(listenfd); //关闭没用的继承资源
64 printf("client from %s\n",inet_ntoa(chiaddr.sin_addr));
65 str_echo(connfd);
66 exit(0);
67 //子进程结束,会成为僵尸进程,所以注册SIGCHLD来让父亲进程处理僵尸进程的遗留数据
68 }
69 else if (childpid<0)
70 printf("fork error: %s\n",strerror(errno));
71 //注意父子进程的执行顺序无法确定。
72 close(connfd);
73 }
74 }
75
76
77 void str_echo(int sockfd) // 服务器收到客户端的消息后的响应
78 {
79 ssize_t n;
80 char line[512];
81
82 printf("ready to read\n");
83
84 while( (n=read(sockfd,line,512))>0 )
85 {
86 if(n>0)
87 {
88 line[n]='\0';
89 printf("Client Diary: %s\n",line);
90
91 //写回客户端提示信息
92 char msgBack[512];
93 snprintf(msgBack,sizeof(msgBack),"recv: %s\n",line);
94 write(sockfd,msgBack,strlen(msgBack));
95 bzero(&line,sizeof(line));
96 }
97 else
98 {
99 break;
100 }
101 }
102
103 printf("end read\n");
104 }
105
106 void sig_child(int signo) //父进程对子进程结束的信号处理
107 {
108 pid_t pid;
109 int stat;
110
111 while( (pid=waitpid(-1,&stat,WNOHANG))>0)
112 printf("child %d terminated\n",pid);
113
114 return;
115 }
client.cpp
 1 #include<sys/types.h>
2 #include<stdlib.h>
3 #include<stdio.h>
4 #include<unistd.h>
5 #include<sys/socket.h>
6 #include<strings.h>
7 #include<string.h>
8 #include<arpa/inet.h>
9 #include<errno.h>
10 #include<stdio.h>
11
12 #include<algorithm>
13
14 #define SERVER_PORT 84
15
16 void str_cli(char *data,int sockfd);
17
18 int main(int argc, char **argv)
19 {
20 int sockfd;
21 struct sockaddr_in servaddr;
22
23 //tcpcli <ipaddress> <data>
24 if(argc!=3)
25 return -1;
26
27 sockfd=socket(AF_INET,SOCK_STREAM,0);
28 if(sockfd==-1)
29 {
30 printf("socket established error: %s\n",(char*)strerror(errno));
31 }
32
33 bzero(&servaddr,sizeof(servaddr));
34 servaddr.sin_family=AF_INET;
35 servaddr.sin_port=htons(SERVER_PORT);
36 inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
37
38 printf("client try to connect\n");
39 int conRes=connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
40 if(conRes==-1)
41 {
42 printf("connect error: %s\n",strerror(errno));
43 }
44
45 str_cli(argv[2],sockfd);
46
47 exit(0);
48 }
49
50 void str_cli(char *data,int sockfd)
51 {
52 int n=0;
53 char recv[512];
54
55 FILE* fp=stdin;
56
57 int maxfdp1;
58 fd_set rset;
59 char sendline[512],recvline[512];
60
61 FD_ZERO(&rset);
62 for(;;)
63 {
64 FD_SET(fileno(fp),&rset);
65 FD_SET(sockfd,&rset);
66 maxfdp1=std::max( fileno(fp),sockfd) +1;
67 select(maxfdp1,&rset,NULL,NULL,NULL); //使用select使得客户端不需要注释在标准IO 或者 TCP read的其中一个上
68
69 if( FD_ISSET(sockfd,&rset))
70 {
71 if(read(sockfd,recvline,sizeof(recvline))==0)
72 //那边发来需要有换行符表示串尾,不然这边要求接受的字节数还不到,或者还没到尾部,所以没有输出
73 {
74 printf("read error\n");
75 //handle error;
76 return;
77 }
78 // printf("readed:%d,str:%d",cr,strlen(recvline));
79 // 如果tcp缓冲区内有多于预定义取的字节,则会自动调用read再取,直到结尾或者遇到换行符
80 fputs(recvline,stdout); //标准库函数fputs向标准IO上写一行
81 bzero(&recvline,strlen(recvline));
82 }
83 if( FD_ISSET(fileno(fp),&rset))
84 {
85 if(fgets(sendline,sizeof(sendline),fp)==NULL) //标准库函数fgets从标准IO上获取一行
86 //fgets会加上换行符,如果有
87 return;
88 write(sockfd,sendline,strlen(sendline)-1);
89 //减一去除换行符
90 bzero(&sendline,strlen(sendline));
91 }
92 }
93
94 }

分析:

  服务器增加了对子进程退出时的信号处理,防止子进程数增多后带来的资源消耗,客户端使用select避免阻塞在标准IO和socket IO上。