UNIX网络编程3 使用select()

select函数必需的头文件:<sys/select.h>和<sys/time.h>。这个函数允许进程指示内核等待多个事件(任何描述符字准备好读或写或有异常条件待处理)中的任一个发生,并仅在事件发生或经过某指定的时间后才唤醒进程。所以调用select的进程会在这个函数阻塞,等待事件发生或者超时。

函数原型:select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
传递给select函数的参数会告诉内核:我们所关心的文件描述符;对每个描述符我们所关心的状态;我们要等待多长时间。
从select返回后,内核告诉我们:对我们的要求,已经准备好的描述符个数。

参数说明:
第一个参数int型,指定select中监视的文件句柄数。
三个fd_set参数是类似的,分别代表监视的读、写、异常文件句柄集合。对应的监视集合中有对应的事件发生,则select返回正数,若没有事件发生并超时,select返回0,若发生错误则返回0。struct fd_set是一个存放文件描述符的集合,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄建立联系,但建立联系的工作要由我们手动完成,FD_SET(int fd, fd_set *fdset)建立文件句柄fd与fdset的联系,同理有FD_CLR、FD_ISSET还有FD_ZERO。
timeout参数指定超时时间,若传入NULL(即不传入时间结构),select就是阻塞状态直到有事件返回;若设置为0秒0毫秒(不是NULL),select就是非阻塞函数,不管文件描述符是否有变化都立即返回(non-blocking);若时间大于0,则select在timeout时间内阻塞,期间有事件到来就返回,否则在超时后返回。

简单的使用流程:
1)套接字句柄-socket描述符
假设服务端有一个listenfd,用于侦听客户端连接
int listenfd = socket(...);
然后bind和listen
2)将描述符加入到读(写、异常)监视集合中
FD_ZERO(&rdfds);
FD_SET(listenfd, &rdfds);
3)设置超时
struct timeval tv;
tv.tv_sec = ...;
tv.tv_usec = ...;
4)调用select
一般情况下只用到读事件
ret = select(maxfd+1, &rdfds, NULL, NULL, &tv);

if (ret < 0)
     perror("select error");
else if (ret == 0)
     printf("超时\n");
else
{
     //如果listenfd这个被监视的描述符变成可读
     if (FD_ISSET(listenfd, &rdfds))
     {
          //说明有新的client连接,将新的连接的socket描述符加入到读监视集合中
          connfd = accept(...);
          FD_SET(connfd, &allset);
          //还需要保存这个客户端socket到client队列中,用于后续的通信
     }
     //遍历client队列,查看是哪个socket读就绪
     for(...)
     {
          ...
          FD_ISSET(..)
          //如果某个客户端socket退出,则清除对它的监视
          FD_CLR(...) 
     }
}

服务端使用select的源程序:
//:selectServer.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>

#define BUF_LEN 1024
#define SERV_PORT 6000
#define FD_SIZE 100
#define MAX_BACK 100

int main(int argc, char **argv)
{
     int listenfd, connfd, sockfd, maxfd, maxi, i;
     int nready, client[FD_SIZE]; //接收select返回值,保存客户端套接字
     int lens;
     size_t n; //read字节数
     //fd_set的数据结构,实际上是一long类型的数组,
     //每一个数组元素都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系
     fd_set rset, allset;
     char buf[BUF_LEN];
     socklen_t clilen;
     struct sockaddr_in servaddr, cliaddr;

     if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
     {
          printf("Create socket Error: %d\n", errno);
          exit(EXIT_FAILURE);
     }
     //bzero是传统BSD函数,属于POSIX标准,使用头文件string.h,bzero无返回值,推荐使用memset替代bzero
     bzero(&servaddr, sizeof(servaddr));
     servaddr.sin_family = AF_INET;
     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
     servaddr.sin_port = htons(SERV_PORT);

     if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) == -1)
     {
          printf("Bind Error: %d\n", errno);
          exit(EXIT_FAILURE);
     }

     if (listen(listenfd, MAX_BACK) == -1)
     {
          printf("Listen Error: %d\n", errno);
          exit(EXIT_FAILURE);
     }

     maxfd = listenfd;
     maxi = -1;
     for(i = 0; i < FD_SIZE; i ++)
     {
          client[i] = -1;
     }
     FD_ZERO(&allset);
     //把服务端侦听的listenfd放到allset中
     FD_SET(listenfd, &allset);

     while(1)
     {
          rset = allset;
          //select(最大句柄数+1, 监视的文件句柄集合, 写, 异常, 超时)
          if ((nready = select(maxfd + 1, &rset, NULL, NULL, NULL)) == -1)
          {
               printf("Select Error: %d\n", errno);
               exit(EXIT_FAILURE);
          }
          if (nready <= 0)
          {
               continue;
          }

          //如果是侦听的listenfd就绪,则说明是有新的连接进入
          if(FD_ISSET(listenfd, &rset))
          {
               clilen = sizeof(cliaddr);
               printf("Start doing...\n");
               //新连接
               if ((     ) == -1)
               {
                    printf("Accept Error: %d\n", errno);
                    continue;
               }
               for (i = 0; i < FD_SIZE; i ++)
               {
                    //保存这个新的client fd
                    if (client[i] < 0)
                    {
                         client[i] = connfd;
                         break;
                    }
               }
               if (i == FD_SIZE)
               {
                    printf("To many clients...Reject\n");
                    close(connfd);
                    continue;
               }
               FD_SET(connfd, &allset);
               if (connfd > maxfd)
               {
                    maxfd = connfd;
               }
               if (i > maxi)
               {
                    maxi = i;
               }
          }

          //否则轮训一遍,看看是哪个客户端socket的读就绪
          for (i = 0; i <= maxi; i ++)
          {
               if ((sockfd = client[i]) > 0)
               {
                    if (FD_ISSET(sockfd, &rset))
                    {
                         memset(buf, 0, sizeof(buf));
                         n = read(sockfd, buf, BUF_LEN);
                         if (n < 0)
                         {
                              printf("Error!\n");
                              close(sockfd);
                              FD_CLR(sockfd, &allset);
                              client[i] = -1;
                              continue;
                         }
                         if (n == 0)
                         {
                              printf("No data\n");
                              //close(sockfd);
                              //FD_CLR(sockfd, &allset);
                              //client[i] = -1;
                              continue;
                         }

                         printf("Server Recv: %s\n", buf);
                         if (strcmp(buf, "q") == 0)
                         {
                              close(sockfd);
                              FD_CLR(sockfd, &allset);
                              client[i] = -1;
                              continue;
                         }
                         printf("Server send: %s\n", buf);
                         write(sockfd, buf, n);
                    }
               }
          }
     }
     return 0;
}

简单的客户端程序:
//:selectClient.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define BUF_LEN 1024
#define SERVER_PORT 6000

int main(int argc, char const **argv)
{
     int connfd;
     int message_len;
     struct sockaddr_in remote_addr;
     char buf[BUF_LEN];
     memset(&remote_addr, 0, sizeof(remote_addr));
     memset(buf, 0, BUF_LEN);
     remote_addr.sin_family = AF_INET;
     remote_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
     remote_addr.sin_port = htons(SERVER_PORT);
     connfd = socket(AF_INET, SOCK_STREAM, 0);
     if (connfd < 0)
     {
          //perror和strerror都是C语言提供的库函数,用于获取与errno相关的错误信息
          //不同的是perror向stderr输出结果,strerror(errno)向stdout输出结果
          perror("socket error!");
          return -1;
     }
     if (connect(connfd, (struct sockaddr *)&remote_addr, sizeof(struct sockaddr)) < 0)
     {
          perror("connect error!");
          return -1;
     }
     printf("Connected! You can send message.\n");
     while(1)
     {
          gets(buf);
          if (strcmp(buf, "q") == 0)
          {
               write(connfd, buf, message_len);
               printf("Exit.\n");
               break;
          }
          if ((message_len = strlen(buf)) > 0)
          {
               write(connfd, buf, message_len);
               memset(buf, 0, BUF_LEN);
               message_len = read(connfd, buf, BUF_LEN);
               if (message_len > 0)
               {
                    printf("Receive from server: %s\n", buf);
               }
          }
     }
     //客户端主动断开连接
     close(connfd);
     return 0;
}

参考资料:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unix网络编程中,select、poll和epoll都是用于多路复用的I/O模型,可以同时监视多个文件描述符的可读、可写和异常事件。它们之间的主要区别如下: 1. select的文件描述符集合是使用数组来表示,而poll使用链表,epoll使用红黑树,因此在文件描述符数量较大的情况下,select和poll的效率会随着文件描述符数量的增加而降低,而epoll则不会有太大的性能影响。 2. select和poll每次调用时都需要将文件描述符集合从用户态拷贝到内核态,而epoll只需要在第一次调用时将文件描述符集合拷贝到内核态,之后只需要在需要修改时再次拷贝,因此epoll的性能更优。 3. select和poll对于同一个文件描述符,如果既可以读也可以写,需要分别在读集合和写集合中进行标记,而epoll则可以在注册时指定感兴趣的事件类型,不需要分别标记。 下面是一个使用select、poll和epoll进行通信的demo,该程序可以监听本地端口,并接受客户端的连接请求,然后将客户端发送的数据原样返回。 ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/select.h> #include <sys/poll.h> #include <sys/epoll.h> #define PORT 8888 #define MAX_EVENTS 10 #define BUF_SIZE 1024 int main(int argc, char *argv[]) { int listen_fd, conn_fd; struct sockaddr_in serv_addr, cli_addr; socklen_t cli_len; char buf[BUF_SIZE]; int i, n; // 创建监听socket listen_fd = socket(AF_INET, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); exit(EXIT_FAILURE); } // 绑定地址和端口 memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(PORT); if (bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("bind"); exit(EXIT_FAILURE); } // 监听端口 if (listen(listen_fd, 10) < 0) { perror("listen"); exit(EXIT_FAILURE); } // 使用select进行通信 // fd_set read_fds; // FD_ZERO(&read_fds); // FD_SET(listen_fd, &read_fds); // while (1) { // fd_set tmp_fds = read_fds; // if (select(FD_SETSIZE, &tmp_fds, NULL, NULL, NULL) < 0) { // perror("select"); // exit(EXIT_FAILURE); // } // for (i = 0; i < FD_SETSIZE; i++) { // if (FD_ISSET(i, &tmp_fds)) { // if (i == listen_fd) { // // 有新连接 // cli_len = sizeof(cli_addr); // conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &cli_len); // if (conn_fd < 0) { // perror("accept"); // exit(EXIT_FAILURE); // } // printf("New connection from %s:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); // FD_SET(conn_fd, &read_fds); // } else { // // 有数据可读 // n = read(i, buf, BUF_SIZE); // if (n <= 0) { // // 连接关闭 // close(i); // FD_CLR(i, &read_fds); // printf("Connection closed\n"); // } else { // // 发送数据 // write(i, buf, n); // } // } // } // } // } // 使用poll进行通信 // struct pollfd fds[MAX_EVENTS]; // fds[0].fd = listen_fd; // fds[0].events = POLLIN; // while (1) { // if (poll(fds, MAX_EVENTS, -1) < 0) { // perror("poll"); // exit(EXIT_FAILURE); // } // for (i = 0; i < MAX_EVENTS; i++) { // if (fds[i].revents & POLLIN) { // if (i == 0) { // // 有新连接 // cli_len = sizeof(cli_addr); // conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &cli_len); // if (conn_fd < 0) { // perror("accept"); // exit(EXIT_FAILURE); // } // printf("New connection from %s:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); // fds[i].fd = conn_fd; // fds[i].events = POLLIN; // } else { // // 有数据可读 // n = read(fds[i].fd, buf, BUF_SIZE); // if (n <= 0) { // // 连接关闭 // close(fds[i].fd); // printf("Connection closed\n"); // fds[i].fd = -1; // } else { // // 发送数据 // write(fds[i].fd, buf, n); // } // } // } // } // } // 使用epoll进行通信 int epfd, nfds; struct epoll_event ev, events[MAX_EVENTS]; epfd = epoll_create(MAX_EVENTS); ev.events = EPOLLIN; ev.data.fd = listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev); while (1) { nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); for (i = 0; i < nfds; i++) { if (events[i].data.fd == listen_fd) { // 有新连接 cli_len = sizeof(cli_addr); conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &cli_len); if (conn_fd < 0) { perror("accept"); exit(EXIT_FAILURE); } printf("New connection from %s:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); ev.events = EPOLLIN; ev.data.fd = conn_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev); } else { // 有数据可读 n = read(events[i].data.fd, buf, BUF_SIZE); if (n <= 0) { // 连接关闭 close(events[i].data.fd); printf("Connection closed\n"); epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL); } else { // 发送数据 write(events[i].data.fd, buf, n); } } } } return 0; } ``` 其中,使用注释掉的代码分别实现了使用select、poll和epoll进行通信的功能。可以通过注释掉相应的代码来切换使用不同的I/O模型。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值