在上一篇中,我们编写了一个简单的TCP服务器/客户端程序,初步探讨了一些问题,本文将进一步优化该程序,使我们的程序更加健壮。
问题提出
我们的服务器阻塞于accept时,如果被信号中断了,将会返回一个错误,有些内核会自动重启被中断的系统调用,但为了可移植性,我们必须对慢系统调用返回EINTR有所准备。
服务器关闭后,客户端由于阻塞在read上,不能及时收到服务器关闭的通知.
close存在两点限制(代码注释中将详细说明)
改进后的版本
/*
* tcp_server.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <linux/in.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#define PORT 8888
#define LISTENQ 2
#define BUFSIZE 1024
typedef void (*sighandler_t)(int);
/*向信号signum注册一个void (*sighandler_t)(int)类型
*的函数,函数句柄为handler
*使用typedef简化
*/
sighandler_t signal(int signum, sighandler_t handler);
/*处理子进程的终止信号,防止僵死进程*/
void sig_chld(int sign);
int
main (int argc, char **argv)
{
struct sockaddr_in cliaddr, servaddr;
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
signal(SIGCHLD, sig_chld);
if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) //监听套接字
{
printf("socket error\n");
return -1;
}
bzero(&servaddr, sizeof(servaddr)); //清零
servaddr.sin_family = AF_INET;
/*地址和端口都要转成网络字节序*/
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //本地地址
servaddr.sin_port = htons(PORT);
/*绑定地址结构到套接字描述符*/
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0)
{
printf("bind error\n");
return -1;
}
/*监听, LISTENQ为监听队列的长度*/
if (listen(listenfd, LISTENQ) < 0)
{
printf("listen error\n");
return -1;
}
/*主循环*/
for( ; ; )
{
clilen = sizeof(cliaddr);
/*接收客户端连接*/
if ( (connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &clilen)) < 0 )
{
/*accept属于慢系统调用,即它可能永远阻塞;
*比如,如果没有客户连接到服务器上,那accept将不会返回。
*当阻塞于某个慢系统调用的进程捕捉了某个信号,
*且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。
*在这里,我们重启被中断的系统调用。
*/
if (errno == EINTR)
continue;
else
{
printf("accept error\n");
return -1;
}
}
/*fork子进程来处理请求*/
if ( (childpid = fork()) == 0 )
{
close(listenfd); //子进程关闭监听套接字
process_conn_server(connfd);
exit(0);
}
close(connfd); //父进程关闭连接套接字
}
}
void
sig_chld(int sign)
{
pid_t pid;
int stat;
/*注意信号是不排队的,使用wait的话,同时多个信号只处理一个.
*使用waitpid来取得所有已终止子进程的状态;
*指定WNOHANG,告诉waitpid在有未终止的子进程运行时不要阻塞;
*不能在循环中调用wait,因为它会阻塞.
*/
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
/*
* tcp_client.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <linux/in.h>
#include <string.h>
#include <signal.h>
#define PORT 8888
#define BUFSIZE 1024
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
void sig_pipe(int sign);
int
main (int argc, char **argv)
{
struct sockaddr_in servaddr;
int clifd;
signal(SIGPIPE, sig_pipe);
if (argc != 2)
{
printf("usage: client server_addr\n");
exit(1);
}
/*设置服务器地址*/
bzero(&servaddr, sizeof(servaddr)); //清零
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //本地地址
servaddr.sin_port = htons(PORT);
if ((clifd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("socket error\n");
return -1;
}
/* 将用户输入的字符串类型的IP地址转为整型,
* p: presentation 表达格式 ASCII串
* n: numeric 数值格式 二进制
* 第二个参数为字符串指针, 第三个参数为指向结构struct in_addr的指针
*/
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
if (connect(clifd, (struct sockaddr*) &servaddr, sizeof(struct sockaddr)) < 0)
{
printf("connect error\n");
return -1;
}
process_conn_client(clifd);
close(clifd);
}
/*当服务器一端关闭时,客户端如果继续向服务器发送数据,将会
*收到一个SIGPIPE信号,我们在这里捕捉该信号.
*/
void sig_pipe(int sign)
{
printf("Catch a SIGPIPE signal\n");
}
/*
* tcp_process.c
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#define max(a,b) a>b?a:b
void process_conn_server(int fd)
{
ssize_t size = 0;
char buffer[1024];
for (;;)
{
size = read(fd, buffer, 1024);
if (size == 0)
return ;
sprintf(buffer, "%d bytes altogether\n", size);
write(fd, buffer, strlen(buffer)+1);
}
}
/*上一版的问题:服务器关闭后,客户端由于阻塞在read上,
*不能及时收到服务器关闭的通知.
*本版本改为阻塞于select调用,等待标准输入可读或者套接字可读.
*套接字上的三个处理条件:
*1.对端发送数据,套接字可读,read返回大于0的值;
*2.对端发送FIN(对端进程终止),套接字可读,read返回0(EOF);
*3.对端发送RST(对端主机崩溃并重启),套接字可读,read返回-1,
* errno中含有确切的错误码.
*
*本版本使用shutdown主要原因如下:
*1.close在描述符引用计数为0时才关闭套接字,
* 而shutdown立即激发TCP的正常连接终止序列.
*2.close终止读写两个方向的数据传送。有时我们需要告知对端
* 我们已经完成了数据发送,即使对端仍有数据要发送给我们.
* 这时可以使用shutdown实现半关闭.
*/
void process_conn_client(int fd)
{
ssize_t size = 0;
char buffer[1024];
int maxfdp, stdineof;
fd_set rset;
stdineof = 0; //用来标识标准输入的结束
FD_ZERO(&rset);
for (;;)
{
if (stdineof == 0) //标识为0才select标准输入的可读性
FD_SET(0, &rset);
FD_SET(fd, &rset); //套接字
maxfdp = max(0, fd) + 1;
select(maxfdp, &rset, NULL, NULL, NULL);
if (FD_ISSET(fd, &rset)) //套接字可读
{
if ( (size = read(fd, buffer, 1024)) == 0 ) //从服务器读数据
{
if (stdineof == 1)
return; //正常结束
else
{
printf("server terminated prematurely\n");
return;
}
}
write(1, buffer, size); //输出到标准输出
}
if (FD_ISSET(0, &rset)) //标准输入可读
{
/*从标准输入读数据*/
if ( (size = read(0, buffer, 1024)) == 0 )
{
//读到EOF, 调用shutdown来发送FIN
stdineof = 1;
shutdown(fd, SHUT_WR);
FD_CLR(0, &rset);
continue; //继续循环,此时可以接收数据,不可发送数据
}
write(fd, buffer, size); //写数据给服务器
}
}
}
一些其他需要考虑的问题
服务器主机崩溃,此时如果客户端向主机发送数据,将得不到响应,在不断重传到最后不得不放弃之后才得到错误消息(比如9分钟)。
解决方案:设置超时, or 使用SO_KEEPALIVE套接字选项。accept返回前连接中止。假设已经完成三次握手,客户TCP却发送了一个RST,在服务器端看了,就在该连接已由TCP排队,等着服务器进程调用accept时RST到达。稍后,服务器进程调用accept。
服务器主机崩溃后重启。