用 epoll 写一个socket双工通信

时间:2022-08-01 16:14:55

用 epoll 写一个socket双工通信

NIO 说明

常用的方式有 select、poll、epoll。

select是使用的一种类似位操作的方式,获取到了事件响应就将对应位置为1。缺点就是首先受限于这个标识符长度,因此能监听的数量有限,其次就是每次遍历都需要遍历每一位。

poll 改进了select,不是使用位操作,解决了数量有限的问题,但是依旧遍历需要遍历。

epoll 是使用类似链表的数据结构解决数量有限的问题的,然后是利用回调函数,避免了遍历。

epoll 使用

用 epoll 写一个socket双工通信

主要用到的几个函数:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

分别说明:

epoll_create 创建一个 epoll 套接字,之后所有的要监听的套接字都会add到这个套接字上,参数size只需要是一个大于0的整数即可。

epoll_ctl 的op参数标志执行一些操作 EPOLL_CTL_ADD | EPOLL_CTL_MOD | EPOLL_CTL_DEL,分别是增删改套接字,epdf参数就是epoll_create 创建的套接字,然后 fd是对应的文件描述符,event是事件描述。(有一点我不明白的是,event 结构体中其实已经包含了 fd参数,不知道为什么这个地方还需要重新传入)

epoll_wait 监听事件。events 参数用于暂时存放那些响应的事件,timeout代表接受到了消息之后多久返回,-1代表无限等待。

写一个socket通信程序

在计算机网络课程上可能我们第一次接触的程序就是一个简单的socket C/S 程序,但是那个程序只能是 客户端请求->服务端响应的方式,这里我们写一个服务端也能主动发送消息的程序。

主要区别就是在于使用epoll同时监听stdin和socket连接套接字。

server

#include <sys/epoll.h>
#include <fcntl.h>
#include <netinet/in.h>    // for sockaddr_in
#include <sys/types.h>    // for socket
#include <sys/socket.h>    // for socket
#include <stdio.h>        // for printf
#include <stdlib.h>        // for exit
#include <string.h>        // for bzero
#include <arpa/inet.h>     // inet_ntoa

#define LISTENQ 20
#define MAXLINE 10000
#define SERV_PORT 6666

int main(int argc, char **argv)
{
    int                 listenfd, connfd;
    struct sockaddr_in  cliaddr, servaddr;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(SERV_PORT);

    bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr));

    listen(listenfd, LISTENQ);

    int addrSize = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &addrSize);
    printf("connected from:%s, port:%d \n\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

    close(listenfd);    /* close listening socket */

    int epollFd;

    epollFd = epoll_create(256);

    struct epoll_event ev,events[20];

    ev.data.fd=connfd;
    ev.events=EPOLLIN|EPOLLET;

    epoll_ctl(epollFd,EPOLL_CTL_ADD,connfd,&ev);

    // stdin = 0
    ev.data.fd=0;
    ev.events=EPOLLIN;
    epoll_ctl(epollFd,EPOLL_CTL_ADD,0,&ev);

    char    sendline[MAXLINE], recvline[MAXLINE];
    while(1){
        int nfds=epoll_wait(epollFd,events,20,0);
        for (int i = 0; i < nfds; ++i){
            if(events[i].data.fd==connfd){
                int n = read(connfd, recvline, MAXLINE); // TODO: add n == 0
                fputs(recvline, stdout);
            }else if(events[i].data.fd==0){
                fgets(sendline, MAXLINE, stdin);
                send(connfd, sendline, strlen(sendline), 0);
            }
        }
    }

    close(connfd);
    close(epollFd);
    return 0;
}

client

#include <sys/epoll.h>
#include <fcntl.h>
#include <netinet/in.h>    // for sockaddr_in
#include <sys/types.h>    // for socket
#include <sys/socket.h>    // for socket
#include <stdio.h>        // for printf
#include <stdlib.h>        // for exit
#include <string.h>        // for bzero
#include <arpa/inet.h>     // inet_ntoa
#include <unistd.h>

#define SERV_PORT 6666
#define MAXLINE 10000
struct sockaddr_in  servaddr, servaddr1;


int
main(int argc, char **argv)
{
    int                 connfd;

    if (argc != 2){
        printf("usage: tcpcli <IPaddress>");
        exit(0);
    }

    connfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    connect(connfd, (struct sockaddr*) &servaddr, sizeof(servaddr));


    int epollFd;

    epollFd = epoll_create(256);

    struct epoll_event ev,events[20];

    ev.data.fd=connfd;
    ev.events=EPOLLIN|EPOLLET;

    epoll_ctl(epollFd,EPOLL_CTL_ADD,connfd,&ev);
    // stdin = 0
    ev.data.fd=0;
    ev.events=EPOLLIN;
    epoll_ctl(epollFd,EPOLL_CTL_ADD,0,&ev);

    char    sendline[MAXLINE], recvline[MAXLINE];
    while(1){
        int nfds=epoll_wait(epollFd,events,20,0);
        for (int i = 0; i < nfds; ++i){
            if(events[i].data.fd==connfd){
                int n = read(connfd, recvline, MAXLINE);
                fputs(recvline, stdout);
            }else if(events[i].data.fd==0){
                fgets(sendline, MAXLINE, stdin);
                send(connfd, sendline, strlen(sendline), 0);
            }
        }
    }

    close(connfd);
    close(epollFd);
    return 0;
}

执行

gcc tcpserv.c -o tcpserv
gcc tcpcli.c -o tcpcli
./tcpserv
...

# 另开一个窗口
./tcpcli 127.0.0.1

就可以在两边都进行输入了。

PS:

关于epoll事件定义

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

实际使用中只用到了EPOLLIN...... 以后再学习其他的吧。