服务器并发处理客户端有两种常见的方式:
1. fork方式
每连上一个客户端就fork一个进程,由子进程和客户端通信。
2. IO复用方式
IO复用也被称为事件驱动,包括使用select,poll,epoll等函数。
我们分别来介绍一下。
fork方式
使用fork方式需要注意几点:
1. 创建新进程后,子进程会继承父进程所有已打开的文件描述符,所以子进程需要关闭不使用的文件描述符。
2. 子进程退出后,会向父进程发送SIGCHLD信号,如果父进程未处理,子进程会变成僵尸进程。
来重点看一下这个问题。
服务器主要代码如下:
-
int main(void)
-
{
-
int listenfd;
-
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
-
ERR_EXIT("socket error");
-
-
struct sockaddr_in servaddr;
-
memset(&servaddr, 0, sizeof(servaddr));
-
servaddr.sin_family = AF_INET;
-
servaddr.sin_port = htons(5188);
-
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
-
-
int on = 1;
-
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
-
ERR_EXIT("setsockopt error");
-
-
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
-
ERR_EXIT("bind error");
-
-
if (listen(listenfd, 10) < 0)
-
ERR_EXIT("listen error");
-
-
struct sockaddr_in peeraddr;
-
socklen_t peerlen = sizeof(peeraddr);
-
int conn;
-
-
pid_t pid;
-
-
while (1) {
-
if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
-
ERR_EXIT("accept error");
-
printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
-
-
pid = fork();
-
if (pid == -1)
-
ERR_EXIT("fork error");
-
if (pid == 0) {
-
printf("client close \n");
-
close(listenfd);
-
do_service(conn);
-
exit(EXIT_SUCCESS);
-
} else {
-
close(conn);
-
}
-
}
-
-
return 0;
-
}
-
-
void do_service(int conn)
-
{
-
char recvbuf[1024];
-
int size = 1024;
-
int total = 0;
-
while (1) {
-
memset(recvbuf, 0, size);
-
int ret = read(conn, recvbuf, size);
-
if (ret == 0) {
-
printf("client close\n");
-
break;
-
} else if (ret == -1)
-
ERR_EXIT("read error");
-
printf("len = %d\n", ret);
-
total += ret;
-
if (total >= size)
-
break;
-
}
-
write(conn, "ack\n", 4);
-
}
运行服务器和客户端,客户端退出后,运行:
-
# ps -aux|grep fork_server
-
root 16664 0.0 0.0 4192 356 pts/7 S+ 15:20 0:00 ./fork_server
-
root 16666 0.0 0.0 0 0 pts/7 Z+ 15:20 0:00 [fork_server] <defunct>
-
root 16675 0.0 0.0 11740 940 pts/8 S+ 15:21 0:00 grep --color=auto fork_server
我们发现,有一个fork_server进程变成了僵尸进程(ps命令僵尸进程的标志为Z)。
这是因为,子进程退出时会向父进程发送SIGCHLD信号,父进程默认情况下不会处理该信号,子进程就成了僵尸进程。
要解决这个问题,最简单的方法是忽略SIGCHLD信号。
-
signal(SIGCHLD, SIG_IGN);
把这句加到main()函数开头,再次运行程序,查看状态:
-
# ps -aux|grep fork_server
-
root 16698 0.0 0.0 4196 352 pts/7 S+ 15:22 0:00 ./fork_server
-
root 16704 0.0 0.0 11740 940 pts/8 S+ 15:22 0:00 grep --color=auto fork_server
这次不存在僵尸进程了。
如果不想忽略该信号,则需要在信号处理函数中调用wait()之类的函数给子进程“收尸”。注意,信号不能排队,如果多个子进程同时退出,父进程只会收到一个SIGCHLD信号。而wait()函数只能等待一个子进程退出,这就会造成有些进程无法被“收尸”。所以必须使用waitpid()函数循环等待子进程,并判断返回值来决定是否处理完全部子进程。示例代码如下:
-
void handler(int sig)
-
{
-
while (waitpid(-1, NULL, WNOHANG) > 0);
-
}
IO复用方式
我们以select()函数为例。
-
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
先简单介绍一下函数参数,参数中,readfds,writefds,errorfds分别为需要监听读、写、错误写状态的描述符集合,maxfd设置为集合中描述符最大值加1。timeout为超时时间,设置NULL表示阻塞,设置0表示立刻返回,其他值就是等待对应的时间,没有任何监听状态改变则返回。
使用该函数时,把需要监听的socket描述符添加到集合中,一旦集合中有描述符发生变化,select就会返回,集合中未发生改变的描述符清0。上层需要查询描述符集合,值为1的描述符就是状态发生改变的描述符。
代码如下:
-
int main(void)
-
{
-
int listenfd;
-
if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
-
ERR_EXIT("socket error");
-
-
struct sockaddr_in servaddr;
-
memset(&servaddr, 0, sizeof(servaddr));
-
servaddr.sin_family = AF_INET;
-
servaddr.sin_port = htons(5188);
-
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
-
-
int on = 1;
-
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
-
ERR_EXIT("setsockopt error");
-
-
if (bind(listenfd, (struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
-
ERR_EXIT("bind error");
-
-
if (listen(listenfd, SOMAXCONN) < 0)
-
ERR_EXIT("listen error");
-
-
struct sockaddr_in peeraddr;
-
socklen_t peerlen = sizeof(peeraddr);
-
-
int conn;
-
int i;
-
int client[FD_SETSIZE];
-
int maxi = 0;
-
for (i = 0; i < FD_SETSIZE; i++)
-
client[i] = -1;
-
-
int nready;
-
int maxfd = listenfd;
-
fd_set rset;
-
fd_set allset;
-
FD_ZERO(&rset);
-
FD_ZERO(&allset);
-
FD_SET(listenfd, &allset);
-
-
while (1) {
-
rset = allset;
-
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
-
if (nready == -1) {
-
if (errno == EINTR)
-
continue;
-
ERR_EXIT("select error");
-
}
-
-
if (nready == 0)
-
continue;
-
-
if (FD_ISSET(listenfd, &rset)) {
-
conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);
-
if (conn == -1)
-
ERR_EXIT("accept error");
-
-
for (i = 0; i < FD_SETSIZE; i++) {
-
if (client[i] < 0) {
-
client[i] = conn;
-
if (i > maxi)
-
maxi = i;
-
break;
-
}
-
}
-
-
if (i == FD_SETSIZE) {
-
fprintf(stderr, "too many clients\n");
-
exit(EXIT_FAILURE);
-
}
-
-
printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
-
ntohs(peeraddr.sin_port));
-
-
FD_SET(conn, &allset);
-
if (conn > maxfd)
-
maxfd = conn;
-
-
if (--nready <= 0)
-
continue;
-
}
-
-
for (i = 0; i <= maxi; i++) {
-
conn = client[i];
-
if (conn == -1)
-
continue;
-
-
if (FD_ISSET(conn, &rset)) {
-
printf("client close \n");
-
do_service(conn);
-
close(conn);
-
FD_CLR(conn, &allset);
-
client[i] = -1;
-
-
if (--nready <= 0)
-
break;
-
}
-
}
-
}
-
-
return 0;
-
}
-
-
void do_service(int conn)
-
{
-
char recvbuf[1024];
-
int size = 1024;
-
int total = 0;
-
while (1) {
-
memset(recvbuf, 0, size);
-
int ret = read(conn, recvbuf, size);
-
if (ret == 0) {
-
printf("client close\n");
-
break;
-
} else if (ret == -1)
-
ERR_EXIT("read error");
-
printf("len = %d\n", ret);
-
total += ret;
-
if (total >= size)
-
break;
-
}
-
write(conn, "ack\n", 4);
-
}
使用select需要注意一些问题。
1. select返回后,还是不能使用阻塞IO操作。man 2 select 能看到这样一段话:
Under Linux, select() may report a socket file descriptor as "ready for reading", while nevertheless a subsequent read blocks. This could for example happen when data has arrived but upon examination has wrong checksum and is discarded. There may be other circumstances in which a file descriptor is spuriously reported as ready. Thus it may be safer to use O_NONBLOCK on sockets that should not block.
也就是说,select返回并不一定代表能读写socket,所以socket还是需要设置为非阻塞的。
2. select的最大并发数受以下影响:
a. 一个进程能打开的最大文件数,可以通过ulimit -n查看;
b. 一个系统能打开的最大文件数,可以通过cat /proc/sys/fs/file-max 查看;
c. FD_SETSIZE(fd_set)的限制,需要重新编译内核 ,但是会导致内核效率减小。poll和epoll没有该限制。
文中涉及到的代码可以在下载执行。
参考资料:
http://blog.csdn.net/jnu_simba/article/details/9034407
阅读(3667) | 评论(0) | 转发(0) |