linux 多路復用io,select 和 epoll 函數(shù)的tcp應用場景總結

以下內(nèi)容部分經(jīng)過驗證,可能有些地方需要讀者按照例子自己驗證

一、linux 網(wǎng)絡io相關函數(shù)

linux 關于io(包括網(wǎng)絡io)的操作,都抽象成是面向文件的模型。

1、文件描述符

linux下文件描述符是int的整數(shù)值,前兩個被系統(tǒng)占用(終端,和異常)。
關于網(wǎng)絡套接字的io,服務端和客戶端,第一步都是調(diào)用socket函數(shù)創(chuàng)建 ‘套接字’,返回一個文件‘描述符fd’。也可以理解為創(chuàng)建了一個文件。
服務端的這個‘文件描述符’,用于接收連接請求,通過accept函數(shù)。
客戶端的這個‘文件描述符’ ,用于客戶端向服務端發(fā)送和接收數(shù)據(jù),通過read/recv和write/send函數(shù) 。

(1)、創(chuàng)建網(wǎng)絡傳輸根文件描述符:socket函數(shù)

int socket(int domain, int type, int protocol); 返回的描述符,默認是阻塞模式,若type參數(shù)傳入 (xxx |SOCK_NONBLOCK) 就是非阻塞的,也可以用文件描述符通過fcntl() 函數(shù)或 ioctl() 函數(shù),將套接字設置成非阻塞的。

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

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

(2)、服務端開啟監(jiān)聽函數(shù): listen('文件描述符', 'tcp 連接隊列長度');

關于 listen 函數(shù)的最后一個參數(shù),不同的linux版本,代表不同的含義。在Linux內(nèi)核2.2之后,listen的最后一個參數(shù)(socket backlog)的形為是指等待accept的完全建立(tcp狀態(tài)ESTABLISHED )的套接字的隊列長度,不包括不完全(tcp 狀態(tài) SYN RECEIVED )連接請求的數(shù)量。 不完全連接的長度可以使用系統(tǒng)參數(shù) /proc/sys/net/ipv4/tcp_max_syn_backlog設置。

(3)、服務端接受連接: accept函數(shù)

accept('socket 函數(shù)返回的描述符', '出參: 客戶端ip地址', '地址長度'),返回值:服務器端為客戶端創(chuàng)建的文件描述符。服務端向客戶端發(fā)送和接受數(shù)據(jù)是使用。是否為阻塞的和'socket 函數(shù)返回的描述符'一致,也可以用accept4函數(shù)返回的文件描述符為非阻塞的。或通過fcntl() 函數(shù)設置為非阻塞的。

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

(4)、客戶端主動連接服務端: connect 函數(shù)

次函數(shù)會經(jīng)過tcp三次握手,若sockfd文件操作符,非阻塞方式不等三次握手完成的終止態(tài)ESTABLISHED,connect 的函數(shù)就會返回小于0的值。

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

2、socket讀寫操作

(1)、讀操作

linux 為讀操作提供了:read(文件描述符, 緩沖, 緩沖長度)、recv(文件描述符, 緩沖, 緩沖長度,讀取類型) 函數(shù)。
read函數(shù):如果入?yún)ⅰ募枋龇?是阻塞的,即為阻塞模式讀取。如果入?yún)ⅰ募枋龇?是非阻塞的,就以非阻塞方式讀取。
recv函數(shù):同read函數(shù)類似,區(qū)別是‘讀取類型’ 參數(shù)。決定是否為阻塞讀取

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

入?yún)ⅲ?br> MSG_DONTWAIT:這個標志將單個IO操作設為非堵塞方式,而不需要在套接字上打開非堵塞的標志,執(zhí)行IO操作。然后關閉非堵塞的標志。
MSG_WAITALL:這個標志告訴內(nèi)核在沒有讀到請求的字節(jié)數(shù)之前不使讀操作返回。
注意:
使用MSG_WAITALL時,’文件描述符‘ 必須處于阻塞模式下,否則不起作用。所MSG_WAITALL不能和MSG_NONBLOCK同時使用。

返回值: 成功執(zhí)行時,返回接收到的字節(jié)數(shù)。另一端已關閉則返回0。失敗返回-1,errno被設為以下的某個值
EAGAIN:套接字已標記為非阻塞,而接收操作被阻塞或者接收超時EBADF:sock不是有效的描述詞
ECONNREFUSE:遠程主機阻絕網(wǎng)絡連接
EFAULT:內(nèi)存空間訪問出錯
EINTR:操作被信號中斷
EINVAL:參數(shù)無效
ENOMEM:內(nèi)存不足
ENOTCONN:與面向連接關聯(lián)的套接字尚未被連接上
ENOTSOCK:sock索引的不是套接字

(2)、linux 為寫操作提供了:write、send函數(shù)
(3) 讀寫函數(shù)總結:
  • 1> send 函數(shù):并不是往網(wǎng)絡上發(fā)送數(shù)據(jù),而是將應用層發(fā)送緩沖區(qū)的數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)(網(wǎng)卡緩沖區(qū))中去,至于什么時候數(shù)據(jù)會從網(wǎng)卡緩沖區(qū)中真正地發(fā)到網(wǎng)絡中去要根據(jù) TCP/IP 協(xié)議棧的行為來確定,這種行為涉及到一個叫 nagel 算法和 TCP_NODELAY 的 socket 選項。

  • 2> recv 函數(shù):不是從網(wǎng)絡上收取數(shù)據(jù),而只是將內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到應用程序的緩沖區(qū)中,拷貝完成以后會將內(nèi)核緩沖區(qū)中該部分數(shù)據(jù)移除。

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

  1. 建立連接
    阻塞方式下,客戶端 connect首先發(fā)送SYN請求到服務器,當客戶端收到服務器返回的SYN的確認時,則connect返回,否則的話一直阻塞。
    非阻塞方式,connect將啟用TCP協(xié)議的三次握手,但是connect函數(shù)并不等待連接建立好才返回,而是立即返回,返回的錯誤碼為EINPROGRESS,表示正在進行某種過程。
  2. 接收連接
    阻塞模式下調(diào)用accept()函數(shù),而且沒有新連接時,進程會進入睡眠狀態(tài),直到有可用的連接,才返回。
    非阻塞模式下調(diào)用accept()函數(shù)立即返回,有連接返回客戶端套接字描述符,沒有新連接時,將返回EWOULDBLOCK錯誤碼,表示本來應該阻塞。
  3. 讀操作
    阻塞模式下調(diào)用read(),recv()等讀套接字函數(shù)會一直阻塞住,直到有數(shù)據(jù)到來才返回。當socket緩沖區(qū)中的數(shù)據(jù)量小于期望讀取的數(shù)據(jù)量時,返回實際讀取的字節(jié)數(shù)。當sockt的接收緩沖區(qū)中的數(shù)據(jù)大于期望讀取的字節(jié)數(shù)時,讀取期望讀取的字節(jié)數(shù),返回實際讀取的長度。
    對于非阻塞socket而言,socket的接收緩沖區(qū)中有沒有數(shù)據(jù),read調(diào)用都會立刻返回。接收緩沖區(qū)中有數(shù)據(jù)時,與阻塞socket有數(shù)據(jù)的情況是一樣的,如果接收緩沖區(qū)中沒有數(shù)據(jù),則返回錯誤號為EWOULDBLOCK,表示該操作本來應該阻塞的,但是由于本socket為非阻塞的socket,因此立刻返回。遇到這樣的情況,可以在下次接著去嘗試讀取。如果返回值是其它負值,則表明讀取錯誤。
  4. 寫操作
    對于阻塞Socket而言,如果發(fā)送緩沖區(qū)沒有空間或者空間不足的話,write操作會直接阻塞住,如果有足夠空間,則拷貝所有數(shù)據(jù)到發(fā)送緩沖區(qū),然后返回.
    對于寫操作write,原理和read是類似的,非阻塞socket在發(fā)送緩沖區(qū)沒有空間時會直接返回錯誤號EWOULDBLOCK,表示沒有空間可寫數(shù)據(jù),如果錯誤號是別的值,則表明發(fā)送失敗。如果發(fā)送緩沖區(qū)中有足夠空間或者是不足以拷貝所有待發(fā)送數(shù)據(jù)的空間的話,則拷貝前面N個能夠容納的數(shù)據(jù),返回實際拷貝的字節(jié)數(shù)。
    尤其注意非阻塞的socket,在建立連接時要兼容處理返回EINPROGRESS情況,在接收連接、讀操作、寫操作時要兼容處理返回EWOULDBLOCK錯誤碼的情況。

三、linux select 方法實現(xiàn)的服務端

//服務端程序
#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)是內(nèi)核為文件所創(chuàng)建的索引
//Linux剛啟動的時候會自動設置0是標準輸入,1是標準輸出,2是標準錯誤。
int main(int argc, char const *argv[])
{
    //判斷入?yún)?    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);
    //開啟監(jiān)聽,設置最大
    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 數(shù)組的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))
            //如果超時那么跳過循環(huán)
            continue;
        //循環(huán)所有監(jiān)聽的描述符
        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);
                    }
                    //向新文件描述符(連接)寫入數(shù)據(jù)
                    //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));
                    //從文件描述符,讀取信息, 如果沒有讀到數(shù)據(jù)說明,連接斷了
                    if(read(fd, buf , sizeof(buf)) <= 0){
                        printf("Connection closed.\n");
                        //取消文件描述符的可讀狀態(tài)
                        isConnected[fd] = 0;
                        //關閉文件描述符
                        close(fd);
                    }
                    else{
                        //打印客戶端輸入的內(nèi)容
                        printf("%s", buf);
                    }
                }
            }
        }
    }
}

四、linux epoll 方法實現(xiàn)的服務端

#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]);

    //創(chuàng)建套接字,文件描述符,接受連接的節(jié)點
    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;
    //設置監(jiān)聽本機ip
    serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
    //設置監(jiān)聽端口
    serverAddr.sin_port=htons(port);
    //綁定ip
    bind(lfd,(struct sockaddr *)&serverAddr,serverAddrlen);
    
    //監(jiān)聽 最大值128
    //在Linux內(nèi)核2.2之后,socket backlog參數(shù)(listen的最后一個參數(shù))的形為改變了,
    //現(xiàn)在它指等待accept的完全建立(ESTABLISHED 狀態(tài))的套接字的隊列長度,而不是不完全(SYN RECEIVED 狀態(tài))連接請求的數(shù)量。 
    //不完全連接的長度可以使用/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 相關代碼----------------------------------------
    //創(chuàng)建epoll樹, 初始創(chuàng)建2000個子節(jié)點, 可擴容
    int epfd = epoll_create(2000);
    //初始化,根節(jié)點 關注的事件
    struct epoll_event ev;
    //讀寫事件
    ev.events=EPOLLIN;
    ev.data.fd=lfd;
    //初始化epoll數(shù)根節(jié)點
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
    //聲明 內(nèi)核返回的,檢測到的事件數(shù)組
    struct epoll_event all[2000];
    while(1){
        //使用epoll 通知內(nèi)核 文件io檢測,第3參數(shù)為數(shù)組大小,最后一個參數(shù)-1代表阻塞
        //返回值為,有多少個元素發(fā)生了,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);
                }
                //初始化,普通節(jié)點 關注的事件
                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 {
                //處理已連接的客戶端發(fā)來的數(shù)據(jù)
                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之間的區(qū)別 , 在于第四個參數(shù)recv可以控制是否,讀取完成后是否刪除緩沖
void main(int argc, char const *argv[]){
    //判斷入?yún)?    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;
    //創(chuàng)建根文件描述符
    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);
    //創(chuàng)建連接,此處三次握手,如果socket 創(chuàng)建時,參數(shù)為非阻塞,那么此處不等三次握手完成,立即返回。
    if((connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)))<0){
        printf("tcp 三次握手未完成,或錯誤無!");
        exit(1);
    }
    //讀取數(shù)據(jù)的大小
    char buf[256];
    //從根文件描述符讀取數(shù)據(jù),最后一個參數(shù)可設置,是否阻塞模式
    //注意協(xié)議接收到的數(shù)據(jù)可能大于buf的長度,
    //所以在這種情況下要調(diào)用幾次recv函數(shù)才能把s的接收緩沖中的數(shù)據(jù)copy完。recv函數(shù)僅僅是copy數(shù)據(jù),真正的接收數(shù)據(jù)是協(xié)議來完成的
    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);
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容