linux 多路复用io,select 和 epoll 函数的tcp应用场景总结

以下内容部分经过验证,可能有些地方需要读者按照例子自己验证

一、linux 网络io相关函数

linux 关于io(包括网络io)的操作,都抽象成是面向文件的模型。

1、文件描述符

linux下文件描述符是int的整数值,前两个被系统占用(终端,和异常)。
关于网络套接字的io,服务端和客户端,第一步都是调用socket函数创建 ‘套接字’,返回一个文件‘描述符fd’。也可以理解为创建了一个文件。
服务端的这个‘文件描述符’,用于接收连接请求,通过accept函数。
客户端的这个‘文件描述符’ ,用于客户端向服务端发送和接收数据,通过read/recv和write/send函数 。

(1)、创建网络传输根文件描述符:socket函数

int socket(int domain, int type, int protocol); 返回的描述符,默认是阻塞模式,若type参数传入 (xxx |SOCK_NONBLOCK) 就是非阻塞的,也可以用文件描述符通过fcntl() 函数或 ioctl() 函数,将套接字设置成非阻塞的。

举例1:
socket(AF_INET,SOCK_STREAM|SOCK_NONBLOCK,0)

举例2:
int flags = fcntl('文件描述符', F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl('文件描述符', F_SETFL, flags);

(2)、服务端开启监听函数: listen('文件描述符', 'tcp 连接队列长度');

关于 listen 函数的最后一个参数,不同的linux版本,代表不同的含义。在Linux内核2.2之后,listen的最后一个参数(socket backlog)的形为是指等待accept的完全建立(tcp状态ESTABLISHED )的套接字的队列长度,不包括不完全(tcp 状态 SYN RECEIVED )连接请求的数量。 不完全连接的长度可以使用系统参数 /proc/sys/net/ipv4/tcp_max_syn_backlog设置。

(3)、服务端接受连接: accept函数

accept('socket 函数返回的描述符', '出参: 客户端ip地址', '地址长度'),返回值:服务器端为客户端创建的文件描述符。服务端向客户端发送和接受数据是使用。是否为阻塞的和'socket 函数返回的描述符'一致,也可以用accept4函数返回的文件描述符为非阻塞的。或通过fcntl() 函数设置为非阻塞的。

举例1:
int accept4(sockfd,(struct sockaddr*)&cli,&len,SOCK_NONBLOCK);

(4)、客户端主动连接服务端: connect 函数

次函数会经过tcp三次握手,若sockfd文件操作符,非阻塞方式不等三次握手完成的终止态ESTABLISHED,connect 的函数就会返回小于0的值。

   if((connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)))<0){
        printf("tcp 三次握手未完成,或错误无!");
        //exit(1);
   }

2、socket读写操作

(1)、读操作

linux 为读操作提供了:read(文件描述符, 缓冲, 缓冲长度)、recv(文件描述符, 缓冲, 缓冲长度,读取类型) 函数。
read函数:如果入参‘文件描述符’ 是阻塞的,即为阻塞模式读取。如果入参‘文件描述符’ 是非阻塞的,就以非阻塞方式读取。
recv函数:同read函数类似,区别是‘读取类型’ 参数。决定是否为阻塞读取

 举例1:
 recv(sockfd, buff, buff_size,MSG_DONTWAIT);  

入参:
MSG_DONTWAIT:这个标志将单个IO操作设为非堵塞方式,而不需要在套接字上打开非堵塞的标志,执行IO操作。然后关闭非堵塞的标志。
MSG_WAITALL:这个标志告诉内核在没有读到请求的字节数之前不使读操作返回。
注意:
使用MSG_WAITALL时,’文件描述符‘ 必须处于阻塞模式下,否则不起作用。所MSG_WAITALL不能和MSG_NONBLOCK同时使用。

返回值: 成功执行时,返回接收到的字节数。另一端已关闭则返回0。失败返回-1,errno被设为以下的某个值
EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时EBADF:sock不是有效的描述词
ECONNREFUSE:远程主机阻绝网络连接
EFAULT:内存空间访问出错
EINTR:操作被信号中断
EINVAL:参数无效
ENOMEM:内存不足
ENOTCONN:与面向连接关联的套接字尚未被连接上
ENOTSOCK:sock索引的不是套接字

(2)、linux 为写操作提供了:write、send函数
(3) 读写函数总结:
  • 1> send 函数:并不是往网络上发送数据,而是将应用层发送缓冲区的数据拷贝到内核缓冲区(网卡缓冲区)中去,至于什么时候数据会从网卡缓冲区中真正地发到网络中去要根据 TCP/IP 协议栈的行为来确定,这种行为涉及到一个叫 nagel 算法和 TCP_NODELAY 的 socket 选项。

  • 2> recv 函数:不是从网络上收取数据,而只是将内核缓冲区中的数据拷贝到应用程序的缓冲区中,拷贝完成以后会将内核缓冲区中该部分数据移除。

二、linux socket阻塞和非阻塞有哪些不同(引用)

  1. 建立连接
    阻塞方式下,客户端 connect首先发送SYN请求到服务器,当客户端收到服务器返回的SYN的确认时,则connect返回,否则的话一直阻塞。
    非阻塞方式,connect将启用TCP协议的三次握手,但是connect函数并不等待连接建立好才返回,而是立即返回,返回的错误码为EINPROGRESS,表示正在进行某种过程。
  2. 接收连接
    阻塞模式下调用accept()函数,而且没有新连接时,进程会进入睡眠状态,直到有可用的连接,才返回。
    非阻塞模式下调用accept()函数立即返回,有连接返回客户端套接字描述符,没有新连接时,将返回EWOULDBLOCK错误码,表示本来应该阻塞。
  3. 读操作
    阻塞模式下调用read(),recv()等读套接字函数会一直阻塞住,直到有数据到来才返回。当socket缓冲区中的数据量小于期望读取的数据量时,返回实际读取的字节数。当sockt的接收缓冲区中的数据大于期望读取的字节数时,读取期望读取的字节数,返回实际读取的长度。
    对于非阻塞socket而言,socket的接收缓冲区中有没有数据,read调用都会立刻返回。接收缓冲区中有数据时,与阻塞socket有数据的情况是一样的,如果接收缓冲区中没有数据,则返回错误号为EWOULDBLOCK,表示该操作本来应该阻塞的,但是由于本socket为非阻塞的socket,因此立刻返回。遇到这样的情况,可以在下次接着去尝试读取。如果返回值是其它负值,则表明读取错误。
  4. 写操作
    对于阻塞Socket而言,如果发送缓冲区没有空间或者空间不足的话,write操作会直接阻塞住,如果有足够空间,则拷贝所有数据到发送缓冲区,然后返回.
    对于写操作write,原理和read是类似的,非阻塞socket在发送缓冲区没有空间时会直接返回错误号EWOULDBLOCK,表示没有空间可写数据,如果错误号是别的值,则表明发送失败。如果发送缓冲区中有足够空间或者是不足以拷贝所有待发送数据的空间的话,则拷贝前面N个能够容纳的数据,返回实际拷贝的字节数。
    尤其注意非阻塞的socket,在建立连接时要兼容处理返回EINPROGRESS情况,在接收连接、读操作、写操作时要兼容处理返回EWOULDBLOCK错误码的情况。

三、linux select 方法实现的服务端

//服务端程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>//hotn
#include <unistd.h>
#include <sys/select.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
//文件描述符(file descriptor)是内核为文件所创建的索引
//Linux刚启动的时候会自动设置0是标准输入,1是标准输出,2是标准错误。
int main(int argc, char const *argv[])
{
    //判断入参
    if (argc<2){
        printf("eg: ./a.out prot\n");
        exit(1);
    }
    //atoi 类型转换
    int port = atoi(argv[1]);
    //开启服务端 socket文件描述符,用户标记新连接的接收
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    //初始化服务器 sockaddr_in
    struct sockaddr_in serverAddr;
    socklen_t serverAddrLen= sizeof(serverAddr);
    memset(&serverAddr,0,serverAddrLen);
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddr.sin_port = htons(port);
    //将文件描述符绑定端口和地址
    bind(lfd, (struct sockaddr *)&serverAddr, serverAddrLen);
    //开启监听,设置最大
    listen(lfd, 3);
    printf("select io:Start accept .....\n");
    //客户端地址 声明
    struct sockaddr_in clientAddr;
    socklen_t clientAddrLen= sizeof(clientAddr);

    //-------------- select 相关代码----------------------------------------
    int MAX_SOCK_FD_INDEX = 12;
    //超时时间
    struct timeval timeout;
    timeout.tv_sec = 10;
    timeout.tv_usec = 1000;
    //可读取的文件描述符集合
    fd_set readFds;
    //初始文件描述符'标记'集合 is_connected 数组的index为文件描述符‘索引’
    int isConnected[MAX_SOCK_FD_INDEX];
    for(int i = 0; i < MAX_SOCK_FD_INDEX; i++)
        isConnected[i] = 0;

    while(1){
        //将读操作集合重置
        FD_ZERO(&readFds);
        //将服务端描述符,设置为可读操作
        FD_SET(lfd, &readFds);
        //将准备就绪的,文件描述,设置为可读操作
        for(int i= 0; i < MAX_SOCK_FD_INDEX; i++)
            if(isConnected[i])
                FD_SET(i, &readFds);

        if(!select(MAX_SOCK_FD_INDEX, &readFds, NULL, NULL, &timeout))
            //如果超时那么跳过循环
            continue;
        //循环所有监听的描述符
        for(int i = 0; i < MAX_SOCK_FD_INDEX; i++){
            //如果文件描述符是可读的
            if(FD_ISSET(i, &readFds)){
                int fd = i;
                //如果可读的文件描述符为 '根文件描述符' ,那么说明是-----新的连接
                if(lfd == fd){
                    int cfd = accept(lfd, (struct sockaddr *)&clientAddr, &clientAddrLen);
                    if(cfd ==-1){
                        perror("accpet error");
                        exit(0);
                    }
                    //向新文件描述符(连接)写入数据
                    //write(cfd, "hello world", sizeof("hello world"));
                    //将新文件描述符设置为 可读
                    isConnected[cfd] = 1;
                    //打印客户端地址
                    char ip[64] = {0};
                    printf("new Clinet ip %s, Port %d \n",
                        inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,ip,sizeof(ip)),
                        ntohs(clientAddr.sin_port)
                    );
                }
                //否则为 久文件描述符,那么是可读事件
                else{
                    char buf[256];
                    bzero(buf, sizeof(buf));
                    //从文件描述符,读取信息, 如果没有读到数据说明,连接断了
                    if(read(fd, buf , sizeof(buf)) <= 0){
                        printf("Connection closed.\n");
                        //取消文件描述符的可读状态
                        isConnected[fd] = 0;
                        //关闭文件描述符
                        close(fd);
                    }
                    else{
                        //打印客户端输入的内容
                        printf("%s", buf);
                    }
                }
            }
        }
    }
}

四、linux epoll 方法实现的服务端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>//hotn
#include <unistd.h>
#include <sys/epoll.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
    if (argc<2) {
        printf("eg: ./a.out prot\n");
        exit(1);
    }
    //atoi 类型转换
    int port = atoi(argv[1]);

    //创建套接字,文件描述符,接受连接的节点
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    //服务端地址
    struct sockaddr_in serverAddr;
    socklen_t serverAddrlen= sizeof(serverAddr);
    //初始化服务器 sockaddr_in
    memset(&serverAddr,0,serv_len);
    //地址族
    serverAddr.sin_family=AF_INET;
    //设置监听本机ip
    serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
    //设置监听端口
    serverAddr.sin_port=htons(port);
    //绑定ip
    bind(lfd,(struct sockaddr *)&serverAddr,serverAddrlen);
    
    //监听 最大值128
    //在Linux内核2.2之后,socket backlog参数(listen的最后一个参数)的形为改变了,
    //现在它指等待accept的完全建立(ESTABLISHED 状态)的套接字的队列长度,而不是不完全(SYN RECEIVED 状态)连接请求的数量。 
    //不完全连接的长度可以使用/proc/sys/net/ipv4/tcp_max_syn_backlog设置。
    listen(lfd,128);
    printf("epoll io:Start accept .....\n");
    //客户端地址 声明
    struct sockaddr_in clientAddr;
    socklen_t clientAddrLen= sizeof(clientAddr);

    //-------------- epoll 相关代码----------------------------------------
    //创建epoll树, 初始创建2000个子节点, 可扩容
    int epfd = epoll_create(2000);
    //初始化,根节点 关注的事件
    struct epoll_event ev;
    //读写事件
    ev.events=EPOLLIN;
    ev.data.fd=lfd;
    //初始化epoll数根节点
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
    //声明 内核返回的,检测到的事件数组
    struct epoll_event all[2000];
    while(1){
        //使用epoll 通知内核 文件io检测,第3参数为数组大小,最后一个参数-1代表阻塞
        //返回值为,有多少个元素发生了,io事件
        int ret = epoll_wait(epfd,all,sizeof(all)/sizeof(0),-1);
        for (int i = 0; i < ret; ++i) {
            int fd=all[i].data.fd;
            if(fd == lfd){//新连接
                //接受连接请求
                int cfd=accept(lfd,(struct sockaddr *)&clientAddr, &clientAddrLen);
                if(cfd ==-1){
                    perror("accpet error");
                    exit(0);
                }
                //初始化,普通节点 关注的事件
                struct epoll_event tmp;
                //读写事件
                tmp.events=EPOLLIN;
                tmp.data.fd=cfd;
                //将新得到的cfd挂在树上
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&tmp);
                //打印客户端信息
                char ip[64] = {0};
                printf("new Clinet ip %s, Port %d \n",
                    inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,ip,sizeof(ip)),
                    ntohs(clientAddr.sin_port)
                );
            }else {
                //处理已连接的客户端发来的数据
                char buf[1024] ={0};
                int len = recv(fd, buf, sizeof(buf), 0);
                if(len == -1){
                    perror("recv error");
                }else if(len == 0){
                    printf("clent close");
                    close(fd);
                    //将fd从树上删除
                    epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
                }else{
                    printf("recv buf %s\n", buf);
                    //将 buf 回写客户端
                    write(fd,buf,len);
                }
            }
        }
    }
    close(lfd);
    return 0;
}

五、linux 客户端


//客户端程序
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <ctype.h>
// socket编程中write、read和send、recv之间的区别 , 在于第四个参数recv可以控制是否,读取完成后是否删除缓冲
void main(int argc, char const *argv[]){
    //判断入参
    if (argc<2 || argc>3){
        printf("eg: ./a.out [ip] prot\n");
        exit(1);
    }
    const char *ip="127.0.0.1";
    int port;
    if (argc==3){
        ip = argv[1];
        port = atoi(argv[2]);
    }else{
        port = atoi(argv[1]);
    }

    //连接地址
    struct sockaddr_in addr;
    //创建根文件描述符
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    bzero(&addr,sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip);
    //创建连接,此处三次握手,如果socket 创建时,参数为非阻塞,那么此处不等三次握手完成,立即返回。
    if((connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)))<0){
        printf("tcp 三次握手未完成,或错误无!");
        exit(1);
    }
    //读取数据的大小
    char buf[256];
    //从根文件描述符读取数据,最后一个参数可设置,是否阻塞模式
    //注意协议接收到的数据可能大于buf的长度,
    //所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的
    recv(sockfd, buf, sizeof(buf), 10);
    printf("%s\n",buf);
    printf("please in put information:\n");
    while(1){
        bzero(buf,sizeof(buf));
/*      printf("%s\n","非阻塞 读取");
        int flags;
        //使用非阻塞io
        if(flags = fcntl(STDIN_FILENO, F_GETFL, 0) < 0)
        {
            perror("fcntl");
            return -1;
        }
        flags |= O_NONBLOCK;
        if(fcntl(STDIN_FILENO, F_SETFL, flags) < 0)
        {
            perror("fcntl");
            return -1;
        }*/

        //键盘读入.默认为阻塞
        read(STDIN_FILENO, buf, sizeof(buf));
        //写入到 根文件描述符
        if(send(sockfd, buf, sizeof(buf), 0) < 0){
            perror("send error!");
            exit(1);
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容