Socket编程实践(8) --Select-I/O复用

五种I/O模型介绍

(1)堵塞I/O[默认]

当上层应用App调用recv系统调用时,假设对等方没有发送数据(Linux内核缓冲区中没有数据),上层应用Application1将堵塞;当对等方发送了数据,Linux内核recv端缓冲区数据到达,内核会把数据copy给用户空间。

然后上层应用App解除堵塞,运行下一步操作。

(2)非堵塞I/O[少用]

上层应用App将套接字设置成非堵塞模式, 然后循环调用recv函数。接受数据。

若缓冲区没有数据。上层应用不会堵塞。recv返回值为-1,错误码是EWOULDBLOCK。

上层应用程序不断轮询有没有数据到来。造成上层应用忙等待。大量消耗CPU。

因此非堵塞模式非常少直接用。

应用范围小,一般和IO复用配合使用。

(3)I/O多路复用[重点]

上层应用App调用select等其它IO复用系统调用(该机制由Linux内核支持。避免了App忙等待),进行轮询文件描写叙述符的状态变化; 当select管理的文件描写叙述符没有数据(或者状态没有变化时),上层应用也会堵塞。

优点是:select机制能够管理多个文件描写叙述符; 能够将select看成一个管理者。用select来管理多个IO, 一旦检測到的一个IO或者多个IO,有我们感兴事件发生时,select函数将返回。返回值为检測到的事件个数。进而能够利用select相关API函数,操作详细事件。

select函数能够设置等待时间。避免了上层应用App长期僵死。

和堵塞IO模型相比,select I/O复用模型相当于提前堵塞了。

等到有数据到来时,再调用recv就不会发生堵塞。

(4)信号驱动I/O[并不经常使用]

上层应用App建立SIGIO信号处理程序。当缓冲区有数据到来,内核会发送信号告诉上层应用App; 当上层应用App接收到信号后,调用recv函数,因缓冲区有数据,recv函数一般不会堵塞。

这样的用于模型用的比較少,属于典型的“拉模式(上层应用被动的去Linux内核空间中拉数据)”。即:上层应用App,须要调用recv函数把数据拉进来,会有时间延迟,我们无法避免在延迟时,又有新的信号的产生。

(5)异步I/O[并不经常使用]

上层应用App调用aio_read函数,同一时候提交一个应用层的缓冲区buf;调用完毕后。不会堵塞。

上层应用程序App能够继续其它任务; 当TCP/IP协议缓冲区有数据时,Linux主动的把内核数据copy到用户空间。然后再给上层应用App发送信号;告诉App数据到来,须要处理!

异步IO属于典型的“推模式”, 是效率最高的一种模式。上层应用程序App有异步处理的能力(在Linux内核的支持下,处理其它任务的同一时候。也可支持IO通讯, 与Windows平台下的完毕port作用相似IOCP)。

Select-I/O复用

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

select实现的是一个管理者的功能: 用select来管理多个IO, 一旦当中的一个IO或者多个IO检測到我们所感兴趣的事件, select就返回, 返回值就是检測到的事件个数, 而且由第2~4个參数返回那些IO发送了事件, 这样我们就能够遍历这些事件, 进而处理这些事件;

參数:

nfds: is the highest-numbered file descriptor in any of the three sets,plus 1[读,写,异常集合中的最大文件描写叙述符+1].

fd_set[四个宏用来对fd_set进行操作]

FD_CLR(int fd, fd_set *set);

FD_ISSET(int fd, fd_set *set);

FD_SET(int fd, fd_set *set);

FD_ZERO(fd_set *set);

timeout[从调用開始到select返回前,会经历的最大等待时间, 注意此处是指的是相对时间]

//timeval结构:
struct timeval
{
    long    tv_sec;	    /* seconds */
    long	tv_usec;    /* microseconds */
};
//一些调用使用3个空的set, n为0, 一个非空的timeout来达到较为精确的sleep.

Linux中, select函数改变了timeout值。用来指示还剩下的时间,但非常多实现并不改timeout。

为了较好的可移植性,timeout在循环中一般常被又一次赋初值。

Timeout取值:

timeout== NULL

无限等待,被信号打断时返回-1, errno 设置成 EINTR

timeout->tv_sec == 0 && tvptr->tv_usec == 0

不等待马上返回

timeout->tv_sec != 0 || tvptr->tv_usec != 0

等待特定时间长度, 超时返回0

返回值:

On success, select() and pselect() return the number of  file  descriptors  contained  in

the  three  returned descriptor sets (that is, the total number of bits that are  set  in

readfds,  writefds,  exceptfds) which  may  be  zero if the timeout expires before anything

interesting happens.  On error, -1 is returned, and errno is set appropriately; the sets

and  timeout  become  undefined, so do not rely on their contents after an error.

假设成功。返回全部sets中描写叙述符的个数;假设超时。返回0;假设出错,返回-1.


读, 写, 异常事件发生条件


可读:


可写:


异常:


套接口缓冲区有数据可读(连接的对等方发送数据过来, 填充了本地套接口缓冲区, 所以导致套接口缓冲区有数据可读);


套接口发送缓冲区有空间容纳数据(由于大部分时间发送缓冲区是未满的, 因此我们一般不关心这个事件);


套接口存在带外数据;


连接的读一半(对端)关闭(对方调用了close), 即接收到FIN段, 读操作将返回0;


连接的写一半关闭. 即收到RST段之后, 再次调用write操作;


假设是监听套接口, 已完毕队列不为空时;


套接口发生了一个错误待处理, 错误能够通过getsockopt指定SO_ERROR选项来获取;


套接口发生了一个错误等待处理, 错误能够通过getsockopt指定SO_ERROR选项来获取;

/**演示样例1: 用select来改进echo回声服务器的client端的echoClient函数
使得能够在单进程的情况下同一时候监听多个文件描写叙述符;
**/
void echoClient(int sockfd)
{
    char buf[512];
    fd_set rset;
    //确保标准输入不会被重定向
    int fd_stdin = fileno(stdin);
    int maxfd = fd_stdin > sockfd ?

fd_stdin : sockfd;
    while (true)
    {
        FD_ZERO(&rset);
        FD_SET(fd_stdin, &rset);
        FD_SET(sockfd, &rset);
        int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nReady == -1)
            err_exit("select error");
        else if (nReady == 0)
            continue;

        /** nReady > 0: 检測到了可读事件 **/

        if (FD_ISSET(fd_stdin, &rset))
        {
            memset(buf, 0, sizeof(buf));
            if (fgets(buf, sizeof(buf), stdin) == NULL)
                break;
            if (writen(sockfd, buf, strlen(buf)) == -1)
                err_exit("write socket error");
        }
        if (FD_ISSET(sockfd, &rset))
        {
            memset(buf, 0, sizeof(buf));
            int readBytes = readline(sockfd, buf, sizeof(buf));
            if (readBytes == 0)
            {
                cerr << "server connect closed..." << endl;
                exit(EXIT_FAILURE);
            }
            else if (readBytes == -1)
                err_exit("read-line socket error");

            cout << buf;
        }
    }
}
/**演示样例2: 用select来改进echo回声服务器的server端的接受连接与处理连接部分的代码:
使得能够在单进程的情况下处理多客户连接, 对于单核的CPU来说, 单进程使用select处理连接与监听套接字其效率不一定就会比多进程/多线程性能差;

**/
    struct sockaddr_in clientAddr;
    socklen_t addrLen;
    int maxfd = listenfd;
    fd_set rset;
    fd_set allset;
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    //用于保存已连接的客户端套接字
    int client[FD_SETSIZE];
    for (int i = 0; i < FD_SETSIZE; ++i)
        client[i] = -1;
    int maxi = 0;   //用于保存最大的不空暇的位置, 用于select返回之后遍历数组

    while (true)
    {
        rset = allset;
        int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nReady == -1)
        {
            if (errno == EINTR)
                continue;
            err_exit("select error");
        }
        //nReady == 0表示超时, 可是此处是一定不会发生的
        else if (nReady == 0)
            continue;

        if (FD_ISSET(listenfd, &rset))
        {
            addrLen = sizeof(clientAddr);
            int connfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);
            if (connfd == -1)
                err_exit("accept error");

            int i;
            for (i = 0; i < FD_SETSIZE; ++i)
            {
                if (client[i] < 0)
                {
                    client[i] = connfd;
                    if (i > maxi)
                        maxi = i;
                    break;
                }
            }
            if (i == FD_SETSIZE)
            {
                cerr << "too many clients" << endl;
                exit(EXIT_FAILURE);
            }
            //打印客户IP地址与port号
            cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)
                 << ", " << ntohs(clientAddr.sin_port) << endl;
            //将连接套接口放入allset, 并更新maxfd
            FD_SET(connfd, &allset);
            if (connfd > maxfd)
                maxfd = connfd;

            if (--nReady <= 0)
                continue;
        }

        /**假设是已连接套接口发生了可读事件**/
        for (int i = 0; i <= maxi; ++i)
            if ((client[i] != -1) && FD_ISSET(client[i], &rset))
            {
                char buf[512] = {0};
                int readBytes = readline(client[i], buf, sizeof(buf));
                if (readBytes == -1)
                    err_exit("readline error");
                else if (readBytes == 0)
                {
                    cerr << "client connect closed..." << endl;
                    FD_CLR(client[i], &allset);
                    close(client[i]);
                    client[i] = -1;
                }
                //注意此处: Server从Client获取数据之后并没有马上回射回去,
                //        而是等待四秒钟之后再进行回射
                sleep(4);
                cout << buf;
                if (writen(client[i], buf, readBytes) == -1)
                    err_exit("writen error");

                if (--nReady <= 0)
                    break;
            }
    }

完整源码请參照:

http://download.csdn.net/detail/hanqing280441589/8486517

时间: 2024-11-09 01:16:15

Socket编程实践(8) --Select-I/O复用的相关文章

Socket编程实践(11) --Select I/O复用

Select函数 Man-Page /* According to POSIX.1-2001 */ #include <sys/select.h> /* According to earlier standards */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds,

Socket编程实践(10) --select的限制与poll的使用

select的限制 用select实现的并发服务器,能达到的并发数一般受两方面限制: 1)一个进程能打开的最大文件描述符限制.这可以通过调整内核参数.可以通过ulimit -n(number)来调整或者使用setrlimit函数设置,但一个系统所能打开的最大数也是有限的,跟内存大小有关,可以通过cat /proc/sys/fs/file-max 查看 /**示例: getrlimit/setrlimit获取/设置进程打开文件数目**/ int main() { struct rlimit rl;

Socket编程实践(12) --select实现超时I/O API[比较难于理解/代码较多]

read_timeout函数封装 //读超时函数,但不包含读操作 int read_timeout(int fd, long waitSec) { int returnValue = 0; if (waitSec > 0) { fd_set readSet; FD_ZERO(&readSet); //清零 FD_SET(fd,&readSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_

Linux下的socket编程实践(三)端口复用和 P2P多进程服务器

Socket端口复用 先说为什么要使用socket端口复用?如果你遇到过这样的问题:server程序重启之后,无法连接,需要过一段时间才能连接上? 1.一个监听(listen)server已经启动 2.当有client有连接请求的时候,server产生一个子进程去处理该client的事物. 3.server主进程终止了,但是子进程还在占用该连接处理client的事情.虽然子进程终止了,但是由于子进程没有终止,该socket的引用计数不会为0,所以该socket不会被关闭. 4.server程序重

Socket编程实践(6) --TCPNotes服务器

僵尸进程过程 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中加入 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法.解决僵尸进程 signal(SIGCHLD,onSignalCatch); void onSignalCatch(int signalNumber) { wait(NULL); } 3) 假设多个客户端同一时候关闭, 问题描写叙述如以下两幅图所看到的: watermark/2/text/aHR0cDovL2Jsb2cuY

Socket编程实践(6) --TCP服务端注意事项

僵尸进程处理 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中添加 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法,解决僵尸进程 signal(SIGCHLD,onSignalCatch); void onSignalCatch(int signalNumber) { wait(NULL); } 3) 如果多个客户端同时关闭, 问题描述如下面两幅图所示: /** client端实现的测试代码**/ int main() { int s

Socket编程实践(19) --Socket API封装(2)

注:这一片博客与下一篇博客<Socket编程实践(20)>合为一篇,由于代码较多,所以分为两篇,本篇为上篇,主要讲解前一篇讲解的Socket类的增强,下一篇主要讲解怎样使用这个增强版的Socket类(ServerSocket/ClientSocket类的实现与使用)! 思想来源: 1)http://www.cnblogs.com/-Lei/archive/2012/09/04/2670964.html 2)http://blog.csdn.net/column/details/linux66.

Socket编程实践(20) --Socket API封装(3)

注:这一片博客与上一篇博客<Socket编程实践(19)>合为一篇,由于代码较多,所以分为两篇,本篇为下篇,这一篇主要讲解怎样使用上一篇开发的增强版的Socket类(ServerSocket/ClientSocket类的实现与使用)! 思想来源: 1)http://www.cnblogs.com/-Lei/archive/2012/09/04/2670964.html 2)http://blog.csdn.net/column/details/linux66.html 3)http://blo

Socket编程实践(6) --TCP粘包原因与解决

流协议与粘包 粘包的表现 Host A 发送数据给 Host B; 而Host B 接收数据的方式不确定 粘包产生的原因 说明 TCP 字节流,无边界 对等方,一次读操作,不能保证完全把消息读完 UDP 数据报,有边界 对方接受数据包的个数是不确定的 产生粘包问题的原因分析 1.SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区.接受缓冲区) 2.tcp传送的端 mss大小限制 3.链路层也有MTU大小限制,如果数据包大于>MTU要在IP层进行分片,导致消息分割. 4.tcp的流量控制和拥塞控