Linux程序设计--socket编程的IO多路复用

完整代码:
github:https://github.com/liudong-ch/c-socket
gitee:https://gitee.com/liudong_ch/c-socket

IO多路复用的系统函数

select函数原型

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

函数监视文件描述符集合,有可读、可写、异常的fd就返回,没有就阻塞。

参数:

  • nfds:要监视的最大文件描述符+1。内核为每个进程分配一组文件描述符,0是标准输入,1标准输出,2标注错误。select会监听[0, nfds)的文件描述符。默认最大支持监视1024个fd。
  • readfds、writefds、exceptfds:要监视的读、写、异常文件描述符的集合。函数返回时,会将可读、可写、异常的文件描述符对应的位置为1。传一个数组的地址,不使用可以设置为NULL。
  • timeout:超时时间,如果超时函数会返回。

返回值:返回可以操作的文件描述符数量。

fd_set 文件描述符集合类型,操作系统提供相关的集合操作函数

#include <sys/select.h>

void FD_CLR(int fd, fd_set *set); // 清除 set 中的某一位 fd
int  FD_ISSET(int fd, fd_set *set); // 如果 fd 在 set 中,返回非 0 值,否则返回 0 值
void FD_SET(int fd, fd_set *set); // 开启 set 中的 fd
void FD_ZERO(fd_set *set); // 将 set 的所有位都设置为 0

poll函数原型

#include <poll.h>
struct pollfd
{
 int fd; /* 文件描述符 */
 short events; /* 等待的事件 */
 short revents; /* 实际发生了的事件,由内核返回 */
} ;
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数:

  • fds:pollfd对象数组,对象包含fd,监视的事件,fd发生的事件。事件有下面的类型:
    在这里插入图片描述
  • nfds:pollfd数组的大小。
  • timeout:超时时间。

返回值:可操作的fd的数量。

epoll函数原型

int epoll_create(int size)

创建一个epoll对象,size是监视fd的数量,好像现在没有了。返回epoll对象的文件描述符。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

控制函数,设置epoll对象要做的操作。
参数:

  • epfd:epoll_create返回的fd。
  • op:要做的操作。系统定义有三个操作:
    EPOLL_CTL_ADD:注册新的fd到epfd中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从epfd中删除一个fd;
  • fd:epoll要监视的fd。
  • events:要监听的事件类型,结构如下:
typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };

events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

等待监视的事件发生,没有事件就阻塞。返回IO准备好的fd数量。
参数:

  • epfd:epoll对象fd。
  • events:结构同上,有可操作的fd,内核会将数据写入该数组中并返回。
  • maxevents:events数组的长度。
  • timeout:超时时间。

epoll事件有两种触发模式,水平触发(LT 默认方式)、边缘触发(ET)。
LT:每次wait(),内核会返回已经就绪的fd。如果就绪fd没有进行IO处理,下次wait()仍会通知。select、poll就是采用这种方式。容错性大,就算没有立即处理fd,wait()也会通知所有就绪fd。
ET:高速模式。每次wait(),内核返回已经就绪的fd。无论fd是否被处理,下次wait()都不会通知。提高了效率,容错小。就绪的fd,wait()只通知一次。对于没有及时处理的fd,必须在使用epoll_ctl()添加监听事件。

socket的IO多路复用是做什么的

socket程序一般为下面的流程,伪代码:

listenfd = socket();   // 打开一个网络通信端口
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
	connfd = accept(listenfd);  // 阻塞建立连接
	while(is_close(connfd)) {   // 判断客户端是否关闭连接
		int n = read(connfd, buf);  // 阻塞读数据
		res = handle_data(buf);  // 处理客户端的数据
		write(connfd, res);  // 将结果写入客户端的连接中
	}
	close(connfd);     // 关闭连接
}

在几个步骤中:accept()、read() 会默认会阻塞。阻塞能节省cpu计算资源。

  • accept() 阻塞等待有新的连接建立。
  • read() 阻塞等待IO完成。此处IO会将网卡的数据拷贝到内核缓存中,在将内核缓存的数据拷贝到用户态缓存中。在IO没有完成所有的数据拷贝之前,程序会一直阻塞,这就是常见的阻塞IO

该流程每次只能服务一个客户端
第一个过程:当一个客户端建立连接后,服务端会阻塞等待接收客户端的数据。发送数据受到网速、数据大小、客户端业务什么时候发送等因素影响,可能等待很长时间。
第二个过程:读到客户端的数据后,处理数据。处理逻辑跟具体业务有关,时间可能很长。

两个过程都可能需要处理较长时间,在这段时间其他客户端建立的连接不能被accept()取出(服务端不能开始处理连接,能建立连接)。其他客户端需要等待前一个客户端完成这两个过程,才能被服务端服务。过程一是等待,服务端处于空闲状态,可以利用起来服务其他客户端。过程二是处理数据,服务器忙碌,但是可以考虑利用cpu多核并发处理其他客户端请求。

处理服务端能处理多个客户端的请求。
处理方式一
使用非阻塞的IO,将read() 设置成非阻塞的读。有数据就处理,没有数据的循环read()。
使用下面函数设置非阻塞的IO:

fcntl(connfd, F_SETFL, O_NONBLOCK);

这样处理会导致cpu的“忙等待”,服务端一直执行循环。并且还是只能处理一个客户端的数据。

处理方式二
使用多进程(或多线程)。有新连接accept(),创建一个新进程,让新进程处理这个连接。主进程继续接收新连接。新进程read()客户端的数据,处理数据,write()数据。

这种方式利用多进程方式可以处理多个客户端的请求。避免服务端等待客户端发送数据的空闲。多线程可以并发处理。
这样会有一个问题是:每个客户端创建一个进程,客户端特别多,会创建过多的进程,占用资源多,并且进程切换也有代价。还可能超过系统进程数量限制。

处理方式三
IO多路复用。就是多个IO操作复用同一个进程。常见的IO多路复用有select、poll、epoll。
思路:监听多个文件描述符fd。没有事件发生,函数会阻塞。如果有可读、可写等事件,函数会返回对应的可读或可写的fd。然后进行读、写fd,此时的fd数据一定是已经准备好的,read()函数不会阻塞。
具体代码下面实现。

IO多路复用解决了read()的阻塞,又不会造成循环“忙等待”问题。
但是,服务端在处理数据阶段仍然不能accept()取出新连接进行处理

对于数据处理阶段,一般由应用程序处理,操作系统好像没有提供相关的方法。
应用程序会开启新线程做数据处理,主线程继续执行IO多路复用监听fd的可读、可写等事件。另一种实现思路跟IO多路复用类似,也是使用一个新线程处理多个客户端的请求,根据客户端的标识符将结果写入对应的客户端链接中。

客户端实现

客户端代码相同,实现的功能:循环 接收用户输入数据发送给服务端,然后等待接收服务端返回数据。exit退出客户端输入。核心代码段:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int start(char *ip, int port) {
    int c_sock ;
    struct sockaddr_in s_add;

    c_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (c_sock == -1) {
        printf("socket 创建失败\n");
        exit(EXIT_FAILURE);
    }

    memset(&s_add, 0, sizeof(s_add));
    s_add.sin_family = AF_INET;
    s_add.sin_addr.s_addr = inet_addr(ip);
    s_add.sin_port = htons(port);

    if(connect(c_sock, (struct sockaddr *)&s_add, sizeof(s_add)) == -1) {
        printf("连接失败\n");
        exit(EXIT_FAILURE);
    }
    printf("连接成功\n");

    while(1) {
        char msg[2*1024];
        scanf("%s", &msg[0]);
        int msg_len = strlen(msg);
        int recv_size;

        if (strcmp(msg, "exit") == 0) {
            break;
        }
        if (send(c_sock, msg, msg_len, 0) == -1) {
            printf("发送失败...\n");
            continue;
        }
        printf("发送成功\n");
        if (strcmp(msg, "end") == 0) {
            break;
        }

        recv_size = recv(c_sock, msg, 2*1024, 0);
        if (recv_size == -1) {
            printf("接收失败...\n");
            continue;
        }
        if (recv_size == 0) {
            printf("连接断开...\n");
            break;
        }

        msg[recv_size] = '\0';
        printf("接收成功:%s\n", msg);
    }
    close(c_sock);
    return 0;
}

socket 多进程服务端

代码逻辑:

  • socket – bind – listen ;基本操作
  • 循环接受(accept)新的连接。接受到新连接,创建新的子进程处理连接。父进程继续accept新连接。
  • 子进程循环接收数据,
  • 处理数据
  • 将处理完的结果发送给客户端。
  • 连接断开时,子进程结束。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>

void handle_client(int c_socket);

int multi_process(unsigned int port) {
    int s_socket, c_socket;
    struct sockaddr_in server_add, client_add2;
    int s_add_len = sizeof(server_add);
    socklen_t c_add_len = sizeof(client_add2);
    pid_t pid, pp;

    s_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (s_socket == -1) {
        printf("socket 创建失败\n");
        exit(EXIT_FAILURE);
    }

    memset(&server_add, 0, s_add_len);
    server_add.sin_family = AF_INET;
    server_add.sin_addr.s_addr = htonl(INADDR_ANY);
    server_add.sin_port = htons(port);

    int r = bind(s_socket, (struct sockaddr *)&server_add, s_add_len);
    if (r == -1) {
        printf("绑定socket失败--%s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }

    r = listen(s_socket, 20);
    if (r == -1) {
        printf("监听失败\n");
        exit(EXIT_FAILURE);
    }
    printf("服务正在运行...\n");

    while(1) {
        struct sockaddr_in client_add;
        c_socket = accept(s_socket, (struct sockaddr *)&client_add, &c_add_len);
        if (c_socket == -1) {
            printf("接受连接失败\n");
            continue;
        }
        // 创建进程,会复制父进程的所有代码和文件描述符,
        // 子进程的代码执行和父进程完全一致。子进程中 pid返回 0
        pid = fork();
        if (pid == -1) {
            printf("创建进程失败\n");
            continue;
        }
        if (pid == 0) {  // 子进程 处理逻辑
            close(s_socket);
            printf("正在准备接收数据\n");
            handle_client(c_socket);
            break;
        } else {  // 父进程 处理逻辑
            close(c_socket);
        }
    }
    return 0;
}
// 连接处理函数
void handle_client(int c_socket) {
    char buffer[5*1024];
    int recv_size;
    char msg[2*2014];

    struct sockaddr_in client_add;
    int c_add_len = sizeof(client_add);
    getpeername(c_socket, (struct sockaddr *)&client_add, &c_add_len);
    char cli_ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client_add.sin_addr, cli_ip, INET_ADDRSTRLEN);
    while (1) {
        recv_size = recv(c_socket, buffer, 5*1024, 0);
        if (recv_size <= 0) {
            printf("断开连接...主机地址:%s:%d\n", cli_ip, ntohs(client_add.sin_port));
            break;
        }
        buffer[recv_size] = '\0';
        if (strcmp(buffer, "end") == 0) {
            printf("断开连接...主机地址:%s:%d\n", cli_ip, ntohs(client_add.sin_port));
            close(c_socket);
            break;
        }

        printf("正在处理...主机地址:%s:%d\n数据:%s\n", cli_ip, ntohs(client_add.sin_port), buffer);
        sleep(5);
        printf("正在返回数据...主机地址:%s:%d\n", cli_ip, ntohs(client_add.sin_port));
        sleep(1);
        int msg_size = sprintf(msg, "收到数据,长度:%d", recv_size);

        send(c_socket, msg, msg_size, 0);
    }
    close(c_socket);
}

select 多路复用

poll 多路复用跟select的使用类似。

代码逻辑:

  • socket – bind – listen ;基本操作 与上面相同,省略。
  • 把监听新连接的socket放入集合。
  • 循环 select监听文件描述符集合。
  • 有可读写的socket时,遍历文件描述符集合。
  • 如果文件描述符等于监听socket,说明有新连接,accept新连接并添加到文件描述符集合。
  • 否则有客户端的数据到达,接收数据
  • 处理数据
  • 发送结果给客户端
  • 连接断开还需要把socket从集合中删除。

核心代码:只展示接受新连接、接收和发送数据部分。

fd_set readfds, testfds;
FD_ZERO(&readfds);
FD_SET(s_socket, &readfds);
while(1) {
	int fd;
	
	testfds = readfds;
	int ret = select(FD_SETSIZE, &testfds, (fd_set *)0, (fd_set *)0, (struct timeval *)0);
	if (ret < 1) {
	    printf("select 失败\n");
	    exit(EXIT_FAILURE);
	}
	
	for (fd = 0; fd < FD_SETSIZE; fd++) {
	    if (FD_ISSET(fd, &testfds)) {
	        if (fd == s_socket) {   //服务监听的socket,有新连接
	            struct sockaddr_in client_add;
	            c_socket_fd = accept(s_socket, (struct sockaddr *)&client_add, &c_add_len);
	            if (c_socket_fd == -1) {
	                printf("接受连接失败\n");
	                continue;
	            }
	            FD_SET(c_socket_fd, &readfds);
	            
	            printf("连接成功...主机地址:%s:%d\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port));
	        } else {    //客户端的socket有数据到达
	            struct sockaddr_in client_add;
	            int c_add_len = sizeof(client_add);
	            getpeername(fd, (struct sockaddr *)&client_add, &c_add_len);
	            recv_size = recv(fd, buffer, 5*1024, 0);
	            if (recv_size <= 0) {
	                FD_CLR(fd, &readfds);
	                printf("断开连接...主机地址:%s:%d\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port));
	                break;
	            }
	            buffer[recv_size] = '\0';
	            if (strcmp(buffer, "end") == 0) {
	                FD_CLR(fd, &readfds);
	                printf("断开连接...主机地址:%s:%d\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port));
	                close(fd);
	                break;
	            }
	
	            printf("正在处理...主机地址:%s:%d\n数据:%s\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port), buffer);
	            sleep(5);
	            printf("正在返回数据...主机地址:%s:%d\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port));
	            sleep(1);
	            int msg_size = sprintf(msg, "收到数据,长度:%d", recv_size);
	
	            send(fd, msg, msg_size, 0);
	        }
	    }
	} 
}

epoll 多路复用

代码逻辑:

  • socket – bind – listen ;基本操作 与上面相同,省略。
  • 创建epoll
  • 为监听连接的socket添加epoll的监听事件类型
  • 循环等待epoll监听事件发生
  • 有事件发生,遍历所有发生的事件
  • 如果事件是连接断开,删除该事件对应的fd的epoll的监听事件类型,关闭fd
  • 如果事件是新连接到达,accept新连接,添加连接fd的epoll监听事件类型
  • 如果事件是客户端数据到达,读数据
  • 处理数据
  • 返回数据给客户端
struct epoll_event event, evlist[512];
// 创建epoll
if ((epollfd = epoll_create(512)) < 0) {
    printf("epoll creat 失败\n");
    exit(EXIT_FAILURE);
}

event.events = EPOLLIN | EPOLLET | EPOLLHUP;
event.data.fd = s_socket;
// 添加epoll的fd的事件监听类型
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, s_socket, &event) < 0) {
    printf("epoll ctl 失败\n");
    exit(EXIT_FAILURE);
}

char buffer[5*1024];
int recv_size;
char msg[2*2014];
printf("服务正在运行...\n");

while(1) {
    // 等待监听的事件发生
    events = epoll_wait(epollfd, evlist, 512, -1);
    if (events <= 0) {
        printf("epoll wait 失败\n");
        continue;
    }

    int i;
    for (i = 0; i < events; i++) {
        // 连接断开、报错
        if (evlist[i].events & EPOLLHUP || evlist[i].events & EPOLLERR) {
            struct sockaddr_in client_add;
            int c_add_len = sizeof(client_add);
            getpeername(evlist[i].data.fd, (struct sockaddr *)&client_add, &c_add_len);
            printf("断开连接...主机地址:%s:%d\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port));
			// 删除fd的事件监听,关闭fd
            epoll_ctl(epollfd, EPOLL_CTL_DEL, evlist[i].data.fd, NULL);
            close(evlist[i].data.fd);
            continue;
        } else if (evlist[i].data.fd == s_socket) {  // 新连接到达
            struct sockaddr_in client_add;
            c_socket_fd = accept(s_socket, (struct sockaddr *)&client_add, &c_add_len);
            if (c_socket_fd == -1) {
                printf("接受连接失败\n");
                continue;
            }

            event.data.fd = c_socket_fd;
            event.events = EPOLLIN | EPOLLET | EPOLLHUP;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, c_socket_fd, &event) < 0) {
                printf("epoll ctl 失败\n");
                close(c_socket_fd);
                continue;
            }

            printf("连接成功...主机地址:%s:%d\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port));
        } else if (evlist[i].events & EPOLLIN) {  // 客户端,连接有可读事件
            struct sockaddr_in client_add;
            int c_add_len = sizeof(client_add);
            getpeername(evlist->data.fd, (struct sockaddr *)&client_add, &c_add_len);

            recv_size = recv(evlist->data.fd, buffer, 5*1024, 0); // 读数据
            if (recv_size <= 0) {  // 删除fd的事件监听,关闭fd
                epoll_ctl(epollfd, EPOLL_CTL_DEL, evlist[i].data.fd, NULL);
                close(evlist[i].data.fd);
                printf("断开连接...主机地址:%s:%d\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port));
                break;
            }

            buffer[recv_size] = '\0';
            if (strcmp(buffer, "end") == 0) {
                epoll_ctl(epollfd, EPOLL_CTL_DEL, evlist[i].data.fd, NULL);
                close(evlist[i].data.fd);
                printf("断开连接...主机地址:%s:%d\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port));
                break;
            }

            printf("正在处理...主机地址:%s:%d\n数据:%s\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port), buffer);
            sleep(5);
            printf("正在返回数据...主机地址:%s:%d\n", inet_ntoa(client_add.sin_addr), ntohs(client_add.sin_port));
            sleep(1);
            int msg_size = sprintf(msg, "收到数据,长度:%d", recv_size);

            send(evlist[i].data.fd, msg, msg_size, 0);  // 发送数据
        }
    }  
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值