说明:只供学习交流,转载请注明出处
使用套接字实现通信的实例中,服务器端在调用recv函数或recvfrom函数接收客户端发送来的消息或在调用accept函数时,都将处于阻塞状态。当进程处于阻塞状态时,程序将停止运行,这将限制程序的处理能力和功能。
Linux系统提供了fcntl函数来实现将套接字端口设置为非阻塞状态的功能,使用该函数设置套接字为非阻塞的代码如下:
……
sock = sock(PF_INET, SOCK_STREAM, 0);
fcntl(sock, F_SETFL, O_NONBLOCK);
……
这样程序就不会停止在accept函数或recv函数了。但是,却带来了一个新的问题,如何保证能够收到到来的信息?在这一情况下,只能不断地查询套接字是否收到了信息。而程序无法在监听同时,处理其他客户机来进行连接。其代码如下:
……
sock = sock(PF_INET, SOCK_STREAM, 0);
fcntl(sock, F_SETFL, O_NONBLOCK);
……
while ( 1 )
{
……
int num = read(sock, buf, len);
……
}
……
这种查询会极大地占用CPU的时间,更好的办法是使用select函数。select函数提供了实现多路复用的功能。例如,使用select函数可以在监听其他客户机连接请求的同时,保存与已经建立连接的客户机之间的通信。select函数的具体信息如下表所示:
头文件 | #include <sys/select.h> | ||
函数原型 | int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds struct timeval *timeout) | ||
返回值 | 成功 | 失败 | 是否设置errno |
存在3种情况,具体参考该函数的说明信息 | -1 | 是 |
说明:select函数充许程序同时对多个文件描述符进行监视。参数nfds为要监视的3个文件描述符最大的文件描述符的数值加1。参数readfds、writefds和exceptfds为要监视的读、写和异常条件的文件描述符集合。可以使用FD_SET函数将某个文件描述符加入该集合,FD_SET函数的具体信息如下:
void FD_SET(int fd, fd_set *set);
当select函数发现有文件描述符准备好之后,将返回准备好的文件描述符的数量。这时候并不知道哪个文件描述符集合中哪个文件描述符已经准备好了,FD_ISSET将获得准备好的文件描述符信息。该函数的具体信息如下:
int FD_ISSET(int fd, fd_set *set);
除了FD_SET函数和FD_ISSET函数,还有FD_ZERO函数和FD_CLR函数。FD_ZERO函数用于清除文件描述符集,而FD_CLR函数用于将fd文件描述符从文件描述符集中移除。这两个函数的定义如下:
void FD_ZERO(fd_set *set);
void FD_CLR(int fd, fd_set *set);
参数timeout为select函数等待时间。timeout为指向timeval结构体的指针。timeval结构体定义如下:
struct timeval
{
long tv_sec; //秒
long tv_usec; //毫秒
};
当timeout为NULL时,表示select函数将无限期等待,除非捕获信息中断这一等待过程。select函数的返回值有3中情况,如下表:
返回值 | 说明 |
>0 | 返回准备好的文件描述符 |
0 | 到达timeout中的等待时间,没有文件描述符准备好 |
-1 | select函数被信号等特殊情况中断 |
错误信息:
EBADF:文件描述符集中存在非法的文件描述符。
EINTR:捕获到信号。
EINVAL:nfds参数为负值或timeout参数非法。
ENOMEM:内存不足。
一个简单的例子:
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
fd_set rfds;
struct timeval tv;
int retval;
FD_ZERO(&rfds);
FD_SET(0, &rfds);
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);
if (retval == -1)
{
perror("select()");
}
else if (retval)
{
printf("Data is available now.\n");
}
else
{
printf("No data within five seconds.\n");
}
return (0);
}
运行结果:
[root@localhost test]# ./select
No data within five seconds.
[root@localhost test]#
网络的多路I/O复用实现实例:
通过使用select函数,避免了程序在接收客户机发送来的消息时陷入阻塞状态。如果存在多个套接字,可以将其加入文件描述符集中,这样就可以同时对多个套接字通信进行监视。
这里只是给出使用select监视套接字集的一个简单的框架,因此只是对一个套接字进行了监视。具体代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int srv_sock;
socklen_t clt_len;
struct sockaddr_in srv_addr;
struct sockaddr_in clt_addr;
struct timeval wait_time;
int port;
int ret;
fd_set read_fds;
int num;
char recv_buf[1024] = {'\0'};
if (argc != 2)
{
printf("Usage: %s port_name\n", argv[0]);
return (1);
}
port = atoi(argv[1]);
srv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (srv_sock < 0)
{
perror("Cannot create socket");
return (1);
}
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
srv_addr.sin_port = htons(port);
ret = bind(srv_sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
if (ret < 0)
{
perror("Cannot bind the socket");
return (1);
}
//在while循环中调用select函数,每次等待3秒,如果在这段时间内
//没有数据到达服务器的套接字端口,将继续处于循环中
//当数据到达时,将输出收到的内容
while ( 1 )
{
wait_time.tv_sec = 3;
wait_time.tv_usec = 0;
FD_ZERO(&read_fds);
FD_SET(srv_sock, &read_fds);
num = select(srv_sock+1, &read_fds, NULL, NULL, &wait_time);
if (num < 0)
{
perror("Select fail");
continue;
}
if (FD_ISSET(srv_sock, &read_fds))
{
int n = recvfrom(srv_sock, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr*)&clt_addr, &clt_len);
if (n < 0)
{
perror("Cannot receive client message");
close(srv_sock);
return (1);
}
printf("server receive : %s\n", recv_buf);
memset(recv_buf, 0, sizeof(recv_buf));
}
printf("waiting...\n");
}
return (0);
}