linux socket recv函数如何判断收完一包_Linux学习笔记-IV

dec0ef4716abfef827acb7221c1499fa.png
此篇文章主要是根据Linux视频教程与网上的相关教程所制作的笔记,为书籍UNP做一个补充,使读者更容易理解书籍内容。

读书笔记请参考此篇文章:

  • APUE-1-文件IO
  • APUE-2-文件和目录
  • APUE-3-I/O库与数据文件和信息
  • APUE-4-进程基础
  • APUE-5-信号与线程
  • APUE-6-进程通信
  • Linux学习笔记-I
  • Linux学习笔记-II -Linux学习笔记-III

[TOC]


epoll多路I/O模型

gdb调试程序

gcc test.c -g (-lpthread)
gdb ./a.our

epoll

三个函数:

该函数生成一个epoll专用的文件描述符

// 创建树的根节点
int epoll_create(int size);
// size:epoll上能关注的最大描述符数
// 监听的epfd挂到epoll树上
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 参数:
// epoll_create生成的epoll专用描述符
// op:
// EPOLL_CTL_ADD - 注册
// RPOLL_CTL_MOD - 修改
// RPOLL_CTL_DEL - 删除
// fd:关联的文件描述符
// event:告诉内核要监听什么事件
struct epoll_event {
    unit32_t events;
    epoll_data_t data;
};
// events:
// EPOLLIN - 读
// EPOLLOUT - 写
// EPOLLERR - 异常
// 等待I/O事件发生,可以设置阻塞的函数
int epoll_wait(
    int epfd,
    struct epoll_event *events, // 数组
    int maxevents,
    int timeout
);
// 对应select和poll函数
// epfd:要检测的句柄
// events:用于回传待处理的数组
// maxevents:告诉内核这个events的大小
// timeout:超时时间
//          -1:永久阻塞
//          0 :立即返回
//          >0:指定的时间数

epoll示例代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>

int main(int argc, const char* argv[])
{
    if(argc < 2)
    {
        printf("eg: ./a.out portn");
        exit(1);
    }
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    int port = atoi(argv[1]);

    // 创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in 
    memset(&serv_addr, 0, serv_len);
    serv_addr.sin_family = AF_INET;                   // 地址族 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 监听本机所有的IP
    serv_addr.sin_port = htons(port);            // 设置端口 
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......n");

    struct sockaddr_in client_addr;
    socklen_t cli_len = sizeof(client_addr);

    // 创建epoll树根节点
    int epfd = epoll_create(2000);
    // 初始化epoll树
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event all[2000];
    while(1)
    {
        // 使用epoll通知内核fd 文件IO检测
        int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);

        // 遍历all数组中的前ret个元素
        for(int i=0; i<ret; ++i)
        {
            int fd = all[i].data.fd;
            // 判断是否有新连接
            if(fd == lfd)
            {
                // 接受连接请求
                int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
                if(cfd == -1)
                {
                    perror("accept error");
                    exit(1);
                }
                // 将新得到的cfd挂到树上
                struct epoll_event temp;
                temp.events = EPOLLIN;
                temp.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);

                // 打印客户端信息
                char ip[64] = {0};
                printf("New Client IP: %s, Port: %dn",
                    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
                    ntohs(client_addr.sin_port));

            }
            else
            {
                // 处理已经连接的客户端发送过来的数据
                if(!all[i].events & EPOLLIN) 
                {
                    continue;
                }

                // 读数据
                char buf[1024] = {0};
                int len = recv(fd, buf, sizeof(buf), 0);
                if(len == -1)
                {
                    perror("recv error");
                    exit(1);
                }
                else if(len == 0)
                {
                    printf("client disconnected ....n");
                    // fd从epoll树上删除
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    if(ret == -1)
                    {
                        perror("epoll_ctl - del error");
                        exit(1);
                    }
                    close(fd);

                }
                else
                {
                    printf(" recv buf: %sn", buf);
                    write(STDOUT_FILENO, buf, len);
                    write(fd, buf, len);
                }
            }
        }
    }

    close(lfd);
    return 0;
}

epoll三种工作模式

水平触发模式(默认工作模式)

根据读数据解释:只要fd对应的缓冲区有数据,epoll_wait需要返回,返回的次数与发送数据的次数没有关系。

边沿触发模式 - ET

客户端给server发送数据,客户端每一次发送数据,server的epoll_wait返回一次。不在乎是否读完数据。epoll_wait调用次数越多,系统开销越大。如果使用while(recv());则数据读完之后recv会阻塞。所以要设置成非阻塞,即设置fd为非阻塞模式。也即使用边沿非阻塞出发。

边沿非阻塞触发

如何设置非阻塞:

open():设置flags,必须O_WDRW | O_NONBLOCK,适用于终端文件/dev/tty

fcntl:设置文件描述符为非阻塞。

int flag = fcntl(fd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);

将缓冲区的数据全部读出:while(recv()>0);读取完成之后返回值为0。

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

int main(int argc, const char* argv[])
{
    if(argc < 2)
    {
        printf("eg: ./a.out portn");
        exit(1);
    }
    struct sockaddr_in serv_addr;
    socklen_t serv_len = sizeof(serv_addr);
    int port = atoi(argv[1]);

    // 创建套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 初始化服务器 sockaddr_in 
    memset(&serv_addr, 0, serv_len);
    serv_addr.sin_family = AF_INET;                   // 地址族 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 监听本机所有的IP
    serv_addr.sin_port = htons(port);            // 设置端口 
    // 绑定IP和端口
    bind(lfd, (struct sockaddr*)&serv_addr, serv_len);

    // 设置同时监听的最大个数
    listen(lfd, 36);
    printf("Start accept ......n");

    struct sockaddr_in client_addr;
    socklen_t cli_len = sizeof(client_addr);

    // 创建epoll树根节点
    int epfd = epoll_create(2000);
    // 初始化epoll树
    struct epoll_event ev;

    // 设置边沿触发
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event all[2000];
    while(1)
    {
        // 使用epoll通知内核fd 文件IO检测
        int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
        printf("================== epoll_wait =============n");

        // 遍历all数组中的前ret个元素
        for(int i=0; i<ret; ++i)
        {
            int fd = all[i].data.fd;
            // 判断是否有新连接
            if(fd == lfd)
            {
                // 接受连接请求
                int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
                if(cfd == -1)
                {
                    perror("accept error");
                    exit(1);
                }
                // 设置文件cfd为非阻塞模式
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                // 将新得到的cfd挂到树上
                struct epoll_event temp;
                // 设置边沿触发
                temp.events = EPOLLIN | EPOLLET;
                temp.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);

                // 打印客户端信息
                char ip[64] = {0};
                printf("New Client IP: %s, Port: %dn",
                    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
                    ntohs(client_addr.sin_port));

            }
            else
            {
                // 处理已经连接的客户端发送过来的数据
                if(!all[i].events & EPOLLIN) 
                {
                    continue;
                }

                // 读数据
                char buf[5] = {0};
                int len;
                // 循环读数据
                while( (len = recv(fd, buf, sizeof(buf), 0)) > 0 )
                {
                    // 数据打印到终端
                    write(STDOUT_FILENO, buf, len);
                    // 发送给客户端
                    send(fd, buf, len, 0);
                }
                if(len == 0)
                {
                    printf("客户端断开了连接n");
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    if(ret == -1)
                    {
                        perror("epoll_ctl - del error");
                        exit(1);
                    }
                    close(fd);
                }
                else if(len == -1)
                {
                    if(errno == EAGAIN)
                    {
                        printf("缓冲区数据已经读完n");
                    }
                    else
                    {
                        printf("recv error----n");
                        exit(1);
                    }
                }
#if 0
                if(len == -1)
                {
                    perror("recv error");
                    exit(1);
                }
                else if(len == 0)
                {
                    printf("client disconnected ....n");
                    // fd从epoll树上删除
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    if(ret == -1)
                    {
                        perror("epoll_ctl - del error");
                        exit(1);
                    }
                    close(fd);

                }
                else
                {
                    // printf(" recv buf: %sn", buf);
                    write(STDOUT_FILENO, buf, len);
                    write(fd, buf, len);
                }
#endif
            }
        }
    }

    close(lfd);
    return 0;
}

UDP通信

server.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, const char* argv[])
{
    // 创建套接字
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd == -1)
    {
        perror("socket error");
        exit(1);
    }

    // fd绑定本地的IP和端口
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(8765);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    int ret = bind(fd, (struct sockaddr*)&serv, sizeof(serv));
    if(ret == -1)
    {
        perror("bind error");
        exit(1);
    }

    struct sockaddr_in client;
    socklen_t cli_len = sizeof(client);
    // 通信
    char buf[1024] = {0};
    while(1)
    {
        int recvlen = recvfrom(fd, buf, sizeof(buf), 0, 
                               (struct sockaddr*)&client, &cli_len);
        if(recvlen == -1)
        {
            perror("recvform error");
            exit(1);
        }

        printf("recv buf: %sn", buf);
        char ip[64] = {0};
        printf("New Client IP: %s, Port: %dn",
            inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, sizeof(ip)),
            ntohs(client.sin_port));

        // 给客户端发送数据
        sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&client, sizeof(client));
    }

    close(fd);

    return 0;
}

client.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, const char* argv[])
{
    // create socket
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd == -1)
    {
        perror("socket error");
        exit(1);
    }

    // 初始化服务器的IP和端口
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(8765);
    inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);

    // 通信
    while(1)
    {
        char buf[1024] = {0};
        fgets(buf, sizeof(buf), stdin);
        // 数据的发送 - server - IP port
        sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&serv, sizeof(serv));

        // 等待服务器发送数据过来
        recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        printf("recv buf: %sn", buf);
    }

    close(fd);

    return 0;
}

TCP/UDP使用场景

TCP使用场景:

  • 对数据安全性要求高的时候:登录数据的传输、文件传输
  • http协议:传输层协议tcp

UDP使用场景:

  • 效率高,实时性要求比较高,视频通话,共享屏幕,通话
  • 有实力的大公司,先使用UDP,在应用层自定义协议,做数据校验。

心跳包

判断客户端和服务器是否处于连接状态。

  • 心跳机制:不会携带大量的数据,每隔一定时间服务器至客户端/客户端至服务器发送一个数据包;
  • 心跳包:一个应用层协议;
  • 判断网络是否断开:有多个连接的心跳包没有收到/回复,关闭通信的套接字;
  • 重连:重新初始套接字,继续发送心跳包;

乒乓包:

  • 比心跳包携带的数据更多;
  • 除了知道连接是否存在以外,还能获取一些信息;

广播与组播

广播源码

server.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, const char* argv[])
{
    // 创建套接字
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd == -1)
    {
        perror("socket error");
        exit(1);
    }

    // 绑定server的iP和端口
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family  = AF_INET;
    serv.sin_port = htons(8787);    // server端口
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    int ret = bind(fd, (struct sockaddr*)&serv, sizeof(serv));
    if(ret == -1)
    {
        perror("bind error");
        exit(1);
    }

    // 初始化客户端地址信息
    struct sockaddr_in client;
    memset(&client, 0, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_port = htons(6767);  // 客户端要绑定的端口
    // 使用广播地址给客户端发数据
    inet_pton(AF_INET, "192.168.123.255", &client.sin_addr.s_addr);

    // 给服务器开放广播权限
    int flag = 1;
    setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag));

    // 通信
    while(1)
    {
        // 一直给客户端发数据
        static int num = 0;
        char buf[1024] = {0};
        sprintf(buf, "hello, udp == %dn", num++);
        int ret = sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&client, sizeof(client));
        if(ret == -1)
        {
            perror("sendto error");
            break;
        }    
        printf("server == send buf: %sn", buf);
        sleep(1);
    }
    close(fd);
    return 0;
}

client.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>

int main(int argc, const char* argv[])
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd == -1)
    {
        perror("socket error");
        exit(1);
    }

    // 绑定iP和端口
    struct sockaddr_in client;
    memset(&client, 0, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_port = htons(6767);  
    inet_pton(AF_INET, "0.0.0.0", &client.sin_addr.s_addr);
    int ret  = bind(fd, (struct sockaddr*)&client, sizeof(client));
    if(ret == -1)
    {
        perror("bind error");
        exit(1);
    }

    // 接收数据
    while(1)
    {
        char buf[1024] = {0};
        int len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        if(len == -1)
        {
            perror("recvfrom error");
            break;
        }

        printf("client == recv buf: %sn", buf);
    }

    close(fd);

    return 0;
}

组播

组播适用于局域网或广域网(Internet)。广播只适用于局域网。

server.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <net/if.h>

int main(int argc, const char* argv[])
{
    // 创建套接字
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd == -1)
    {
        perror("socket error");
        exit(1);
    }

    // 绑定server的iP和端口
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family  = AF_INET;
    serv.sin_port = htons(8787);    // server端口
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    int ret = bind(fd, (struct sockaddr*)&serv, sizeof(serv));
    if(ret == -1)
    {
        perror("bind error");
        exit(1);
    }

    // 初始化客户端地址信息
    struct sockaddr_in client;
    memset(&client, 0, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_port = htons(6767);  // 客户端要绑定的端口
    // 使用组播地址给客户端发数据
    inet_pton(AF_INET, "239.0.0.10", &client.sin_addr.s_addr);

    // 给服务器开放组播权限
    struct ip_mreqn flag;
    // init flag
    inet_pton(AF_INET, "239.0.0.10", &flag.imr_multiaddr.s_addr);   // 组播地址
    inet_pton(AF_INET, "0.0.0.0", &flag.imr_address.s_addr);    // 本地IP
    flag.imr_ifindex = if_nametoindex("ens33");
    setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &flag, sizeof(flag));

    // 通信
    while(1)
    {
        // 一直给客户端发数据
        static int num = 0;
        char buf[1024] = {0};
        sprintf(buf, "hello, udp == %dn", num++);
        int ret = sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&client, sizeof(client));
        if(ret == -1)
        {
            perror("sendto error");
            break;
        }
        printf("server == send buf: %sn", buf);
        sleep(1);
    }

    close(fd);
    return 0;
}

client.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
#include <net/if.h>

int main(int argc, const char* argv[])
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(fd == -1)
    {
        perror("socket error");
        exit(1);
    }

    // 绑定iP和端口
    struct sockaddr_in client;
    memset(&client, 0, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_port = htons(6767); // ........ 
    inet_pton(AF_INET, "0.0.0.0", &client.sin_addr.s_addr);
    int ret  = bind(fd, (struct sockaddr*)&client, sizeof(client));
    if(ret == -1)
    {
        perror("bind error");
        exit(1);
    }

    // 加入到组播地址
    struct ip_mreqn fl;
    inet_pton(AF_INET, "239.0.0.10", &fl.imr_multiaddr.s_addr);
    inet_pton(AF_INET, "0.0.0.0", &fl.imr_address.s_addr);
    fl.imr_ifindex = if_nametoindex("ens33");
    setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &fl, sizeof(fl));

    // 接收数据
    while(1)
    {
        char buf[1024] = {0};
        int len = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        if(len == -1)
        {
            perror("recvfrom error");
            break;
        }
        printf("client == recv buf: %sn", buf);
    }
    close(fd);
    return 0;
}

epoll反应堆模型

此篇博客不错。

epoll模型(epoll+ET+非阻塞+自定义结构体)是Libevent库核心思想。

epoll中的数据存在共享内存区,所以不需要像select和poll一样,需要做一个从用户区到内核区的切换与拷贝。

epoll模型和epoll接口最本质的区别在于epoll模型中,传入联合体的是一个自定义结构体指针,该结构体的基本结构至少包括:

struct my_events {  
    int        m_fd;                             //监听的文件描述符
    void       *m_arg;                           //泛型参数
    void       (*call_back)(void *arg);          //回调函数
    /*
     *  你可以在此处封装更多的数据内容
     *  例如用户缓冲区、节点状态、节点上树时间等等
     */
};
/*
 * 注意:用户需要自行开辟空间存放my_events类型的数组,并在每次上树前用epoll_data_t里的  
 *      ptr指向一个my_events元素。
 */

根据该模型,我们在程序中可以让所有的事件都拥有自己的处理函数,你只需要使用ptr传入即可。epoll_wait()返回后,epoll模型将直接调用事件中对应的回调函数,就像这样:

while(1) {
    /* 监听红黑树, 1秒没事件满足则返回0 */ 
    int n_ready = epoll_wait(ep_fd, events, MAX_EVENTS, 1000);
    if (n_ready > 0) {
        for (i=0; i<n_ready; i++) 
            events[i].data.ptr->call_back(/* void *arg */);
    }
    else
    /*  
(3) 这里可以做很多很多其他的工作,例如定时清除没读完的不要的数据也可以做点和数据库有关的设置玩大点你在这里搞搞分布式的代码也可以
     */
}

普通多路IO转接服务器:

  1. 创建树的根节点
  2. 在树上添加需要监听的节点
  3. 监听读事件
  4. 有返回
  5. 通信
  6. epoll_wait

epoll反应堆模型:

  1. 创建树的根节点
  2. 在树上添加需要监听的节点
  3. 监听读事件
  4. 有返回
  5. 通信(接收数据)
  6. 将这个fd从树上删除
  7. 监听写事件
  8. 写操作
  9. fd从树上摘下来
  10. 监听fd的读事件
  11. epoll_wait

(1)如此频繁的增加删除不是浪费CPU资源吗? 答:对于同一个socket而言,完成收发至少占用两个树上的位置。而交替只需要一个。任何一种设计方式都会有浪费CPU资源的时候,关键看你浪费得值不值,此处的耗费能否换来更大的收益才是衡量是否浪费的标准。和第二个问题综合来看,这里不算浪费

(2)为什么要可读以后设置可写,然后一直交替? 答:服务器的基本工作无非数据的收发,epoll反应堆模型准从TCP模式,一问一答。服务器收到了数据,再给与回复,是目前绝大多数服务器的情况。 (3)服务器能收到数据并不是一定能写数据? 假设一:服务器接收到客户端数据,刚好此时客户端的接收滑动窗口满,我们假设不进行可写事件设置,并且客户端是有意让自己的接收滑动窗口满的情况(黑客)。那么,当前服务器将随客户端的状态一直阻塞在可写事件,除非你自己在写数据时设置非阻塞+错误处理 假设二:客户端在发送完数据后突然由于异常原因停止,这将导致一个FIN发送至服务器,如果服务器不设置可写事件监听,那么在接收数据后写入数据会引发异常SIGPIPE,最终服务器进程终止。


Libevent

  1. 时间的底层处理框架
  2. 消息循环
  3. 创建事件:不带缓冲区(event);带缓冲区(bufferevent)
  4. 资源释放

靠事件驱动。可以去官网下载libevent源码包,通过源码安装。

  1. ./configure用于检测安装环境;生成makefile文件;
  2. make用于编译源代码,生成一些静态、动态库与可执行文件;
  3. sudo make install:将数据拷贝到对应的目录,如果目录不存在,创建该目录。

使用模板

  1. 创建一个事件处理框架
  2. 创建一个事件
  3. 事件添加到事件处理框架上
  4. 开始事件循环
  5. 释放资源

使用libevent函数之前需要分配一个或多个event_base结构体。每个event_base结构体持有一个时间集合,可以检测以确定哪个事件是激活的。

  • 相当于epoll红黑树的树根
  • 底座
  • 抽象层,完成对event_base的封装
  • 每个event_base都有一种用于检测哪种时间已经就绪的方法或者说后端。
struct eventop {
    const char *name;
    void *(*init)(struct event_base *); // 初始化
    int (*add)(void * struct event *);  // 注册事件
    int (*del)(void * struct event *);  // 删除事件
}

创建event_base:struct event_base* event_base_new(void);失败就返回NULL。

释放event_base:event_base_free(struct event_base* base);

循环监听base对应的时间,等待条件满足(启动事件循环):event_base_dispatch();

查看event_base封装的后端:

const char **event_get_supported_methods(void);
const char *event_base_get_method(const struct event_base *base);

event_base和fork

  • 子进程创建成功之后,父进程可以继续使用event_base
  • 子进程中需要继续使用event_base需要重新进程初始化:int event_reinit(struct event_base *base)
#define EV_TIMEOUT  0x01      // 废弃
#define EV_READ     0x02
#define EV_WRITE    0x04
#define EV_SIGNAL   0x08
#define EV_PERSIST  0x10    // 持续触发 
#define EV_ET       0x20    // 边沿模式
typedef void (*event_callback_fn)(evutil_socket_t, short, void *); 
struct event *event_new(
    struct event_base *base,
    evutil_socket_t fd,   // 文件描述符 - int
    short what,
    event_callback_fn cb, // 事件的处理动作
    void *arg 
);

调用event_new()函数之后, 新事件处于已初始化和非未决状态

释放事件void event_free(struct event *event);

设置未决事件:构造事件之后,在将其添加到event_base之前实际上是不能对其做任何操作的。使用event_add()将事件添加到event_base, 非未决事件-未决事件.

int event_add (
    struct event *ev,
    const struct timeval *tv 
);

tv:NULL: 事件被触发, 对应的回调被调用;tv={0,100},如果设置的时间。在改时间段内检测的事件没被触发, 时间到达之后,回调函数还是会被调用。函数调用成功返回0,失败返回-1。

设置非未决:int event_del(struct event *ev);

对已经初始化的事件调用event_del()将使其成为非未决和非激活的。如果事件不是未决的或者激活的,调用将没有效果。成功时 函数返回 0,失败时返回-1。

非未决:没有资格被处理 未决:有资格,但是没有被处理

内部事件状态转换

  1. 创建新事件:event_new()非未决状态,通过event_add转至2;
  2. 未决状态,进入事件循环event_base_dispatch(base);并且对应的事件被触发,转至3;
  3. 激活,callback(由dispatch调用相关的callback),转至4;
  4. 处理,需要继续处理就转至5;
  5. 非未决。event_add或者使用EV_PERSIST让其变为持续触发,转至2;

读写有名管道

read_fifo.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <event2/event.h>

// 对操作处理函数
void read_cb(evutil_socket_t fd, short what, void *arg)
{
    // 读管道
    char buf[1024] = {0};
    int len = read(fd, buf, sizeof(buf));
    printf("data len = %d, buf = %sn", len, buf);
    printf("read event: %s", what & EV_READ ? "Yes" : "No");
}

// 读管道
int main(int argc, const char* argv[])
{
    unlink("myfifo");
    //创建有名管道
    mkfifo("myfifo", 0664);
    // open file
    int fd = open("myfifo", O_RDONLY | O_NONBLOCK);
    if(fd == -1)
    {
        perror("open error");
        exit(1);
    }
    // 读管道
    struct event_base* base = NULL;
    base = event_base_new();
    // 创建事件
    struct event* ev = NULL;
    ev = event_new(base, fd, EV_READ | EV_PERSIST, read_cb, NULL);
    // 添加事件
    event_add(ev, NULL);
    // 事件循环
    event_base_dispatch(base);
    // 释放资源
    event_free(ev);
    event_base_free(base);
    close(fd);

    return 0;
}

write_fifo.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <event2/event.h>

// 对操作处理函数
void write_cb(evutil_socket_t fd, short what, void *arg)
{
    // write管道
    char buf[1024] = {0};
    static int num = 0;
    sprintf(buf, "hello, world == %dn", num++);
    write(fd, buf, strlen(buf)+1);
}

// 写管道
int main(int argc, const char* argv[])
{
    // open file
    int fd = open("myfifo", O_WRONLY | O_NONBLOCK);
    if(fd == -1)
    {
        perror("open error");
        exit(1);
    }
    // 写管道
    struct event_base* base = NULL;
    base = event_base_new();
    // 创建事件
    struct event* ev = NULL;
    // 检测的写缓冲区是否有空间写
    ev = event_new(base, fd, EV_WRITE , write_cb, NULL);
    // 添加事件
    event_add(ev, NULL);
    // 事件循环
    event_base_dispatch(base);
    // 释放资源
    event_free(ev);
    event_base_free(base);
    close(fd);

    return 0;
}

数据缓冲区-Bufferevent

是libevent为IO缓冲区操作提供的一种通用机制。

bufferevent由一个底层的传输端口(如套接字),一个读取缓冲区和一个写入缓冲区组成。

与通常的事件在底层传输端口已经就绪,可以读取或者写入的时候执行回调不同的是,bufferevent在读取或者写入了足够量的数据之后调用用户提供的回调。

回调-缓冲区对应的操作(缓冲区内部实现为队列):

每个bufferevent有两个数据相关的回调:

  • 一个读取回调:从底层传输端口读取了任意量的数据之后会调用读取回调(默认)
  • 一个写入回调:输出缓冲区中足够量的数据被清空到底层传输端口后写入回调会被调用(默认)

创建基于套接字的bufferevent。可以使用bufferevent_socket_new()创建基于套接字的bufferevent。

struct bufferevent *bufferevent_socket_new(
    struct event_base *base, 
    evutil_socket_t fd,
    enum bufferevent_options options
);

options: BEV_OPT_CLOSE_ON_FREE

  • 释放bufferevent时关闭底层传输端口。这将关闭底层套接字,释放底层bufferevent等。
  • struct bufferevent也是一个event
  • 成功时函数返回一个bufferevent,失败则返回NULL。

在bufferevent上启动链接

int bufferevent_socket_connect (
    struct bufferevent *bev,
    struct sockaddr *address, // server ip和port
    int addrlen
);

address和addrlen参数跟标准调用connect()的参数相同。如果还没有为bufferevent设置套接字,调用函数将为其分配一个新的流套接字,并且设置为非阻塞的;

如果已经为bufferevent设置套接字,调用bufferevent_socket_connect()将告知libevent套接字还未连接,直到连接成功之前不应该对其进行读取或者写入操作。

连接完成之前可以向输出缓冲区添加数据。

释放bufferevent操作

void bufferevent_free(struct bufferevent *bev); 这个函数释放bufferevent。

bufferevent读写缓冲区回调操作

typedef void (*bufferevent_data_cb)(
    struct bufferevent *bev,
    void *ctx
);
typedef void (*bufferevent_event_cb)(
    struct bufferevent *bev,
    short events,
    void *ctx
);

events参数:

EV_EVENT_READING:读取操作时发生某事件,具体是哪种事件请看其他标志。 BEV_EVENT_WRITING:写入操作时发生某事件,具体是哪种事件请看其他标志。 BEV_EVENT_ERROR:操作时发生错误。关于错误的更多信息,请调用EVUTIL_SOCKET_ERROR()。 BEV_EVENT_TIMEOUT:发生超时。 BEV_EVENT_EOF:遇到文件结束指示。 BEV_EVENT_CONNECTED:请求的连接过程已经完成。

实现客户端的时候可以判断:

void bufferevent_setcb(
    struct bufferevent *bufev,
    bufferevent_data_cb readcb,
    // 在读回调中读数据 bufferevent_read() 
    bufferevent_data_cb writecb, // NULL
    bufferevent_event_cb eventcb,// NULL
    void *cbarg
);

禁用、启用缓冲区

禁用之后, 对应的回调就不会被调用了

void bufferevent_enable(
    struct bufferevent *bufev,
    short events 
);

void bufferevent_disable(
    struct bufferevent *bufev,
    short events
);

short bufferevent_get_enabled(
    struct bufferevent *bufev 
);

可以启用或者禁用bufferevent上的EV_READ、EV_WRITE或者EV_READ|EV_WRITE事件。没有启用读取或者写入事件时,bufferevent 将不会试图进行数据读取或者写入。

操作bufferevent中的数据

向bufferevent的输出缓冲区添加数据:

int bufferevent_write (
    struct bufferevent *bufev,
    const void *data,
    size_t size 
);

从bufferevent的输入缓冲区移除数据:

size_t bufferevent_read (
    struct bufferevent *bufev,
    void *data,
    size_t size 
);

创建监听器-evconnlistener

typedef void (*evconnlistener_cb) (
    struct evconnlistener *listener, // 用于通信的文件描述符
    evutil_socket_t sock, // 客户端的IP和端口信息
    struct sockaddr *addr,
    int len,              // 外部传进来的数据
    void *ptr
);

struct evconnlistener * evconnlistener_new(
    struct event_base *base,
    evconnlistener_cb cb,
    void *ptr,
    unsigned flags,
    int backlog,
    evutil_socket_t fd 
);

struct evconnlistener *evconnlistener_new_bind (
    struct event_base *base,
    evconnlistener_cb cb, //接受连接之后,用户要做的操作
    void *ptr, // 给回调传参
    // 参数flags:LEV_OPT_CLOSE_ON_FREE、LEV_OPT_REUSEABLE
    unsigned flags, 
    int backlog,// -1: 使用默认的最大值
    const struct sockaddr *sa,
    //服务器的IP和端口信息
    int socklen
);

两个evconnlistener_new*()函数都分配和返回一个新连接监听器对象。连接监听器使用event_base来得知什么时候在给定的监听套接字上有新的TCP连接。新连接到达时,监听 器调用你给出的回调函数。

void evconnlistener_free(struct evconnlistener *lev);

启用和禁用evconnlistener

int evconnlistener_disable(struct evconnlistener *lev);
int evconnlistener_enable(struct evconnlistener *lev)

这两个函数暂时禁止或者重新允许监听新连接。

调整evconnlistener的回调函数

void evconnlistener_set_cb(
    struct evconnlistener *lev,
    evconnlistener_cb cb,
    void *arg
);

函数调整 evconnlistener 的回调函数和其参数

bufferevent

server.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <event2/event.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>

// 读缓冲区回调
void read_cb(struct bufferevent *bev, void *arg)
{
    char buf[1024] = {0};   
    bufferevent_read(bev, buf, sizeof(buf));
    char* p = "我已经收到了你发送的数据!";
    printf("client say: %sn", p);

    // 发数据给客户端
    bufferevent_write(bev, p, strlen(p)+1);
    printf("====== send buf: %sn", p);
}

// 写缓冲区回调
void write_cb(struct bufferevent *bev, void *arg)
{
    printf("我是写缓冲区的回调函数...n"); 
}

// 事件
void event_cb(struct bufferevent *bev, short events, void *arg)
{
    if (events & BEV_EVENT_EOF)
    {
        printf("connection closedn");  
    }
    else if(events & BEV_EVENT_ERROR)   
    {
        printf("some other errorn");
    }

    bufferevent_free(bev);    
    printf("buffevent 资源已经被释放...n"); 
}



void cb_listener(
        struct evconnlistener *listener, 
        evutil_socket_t fd, 
        struct sockaddr *addr, 
        int len, void *ptr)
{
   printf("connect new clientn");

   struct event_base* base = (struct event_base*)ptr;
   // 通信操作
   // 添加新事件
   struct bufferevent *bev;
   bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);

   // 给bufferevent缓冲区设置回调
   bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
   bufferevent_enable(bev, EV_READ);
}


int main(int argc, const char* argv[])
{

    // init server 
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(9876);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);

    struct event_base* base;
    base = event_base_new();
    // 创建套接字
    // 绑定
    // 接收连接请求
    struct evconnlistener* listener;
    listener = evconnlistener_new_bind(base, cb_listener, base, 
                                  LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 
                                  36, (struct sockaddr*)&serv, sizeof(serv));

    event_base_dispatch(base);

    evconnlistener_free(listener);
    event_base_free(base);

    return 0;
}

client.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <event2/event.h>
#include <event2/bufferevent.h>


void read_cb(struct bufferevent *bev, void *arg)
{
    char buf[1024] = {0}; 
    bufferevent_read(bev, buf, sizeof(buf));
    printf("Server say: %sn", buf);
}

void write_cb(struct bufferevent *bev, void *arg)
{
    printf("I am Write_cb function....n");
}

void event_cb(struct bufferevent *bev, short events, void *arg)
{
    if (events & BEV_EVENT_EOF)
    {
        printf("connection closedn");  
    }
    else if(events & BEV_EVENT_ERROR)   
    {
        printf("some other errorn");
    }
    else if(events & BEV_EVENT_CONNECTED)
    {
        printf("成功连接到服务器, O(∩_∩)O哈哈~n");
        return;
    }

    bufferevent_free(bev);
    printf("free bufferevent...n");
}

void send_cb(evutil_socket_t fd, short what, void *arg)
{
    char buf[1024] = {0}; 
    struct bufferevent* bev = (struct bufferevent*)arg;
    printf("请输入要发送的数据: n");
    read(fd, buf, sizeof(buf));
    bufferevent_write(bev, buf, strlen(buf)+1);
}


int main(int argc, const char* argv[])
{
    struct event_base* base;
    base = event_base_new();


    struct bufferevent* bev;
    bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);

    // 连接服务器
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(9876);
    evutil_inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
    bufferevent_socket_connect(bev, (struct sockaddr*)&serv, sizeof(serv));

    // 设置回调
    bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
    bufferevent_enable(bev, EV_READ | EV_PERSIST);

    // 创建一个事件
    struct event* ev = event_new(base, STDIN_FILENO, 
                                 EV_READ | EV_PERSIST, 
                                 send_cb, bev);
    event_add(ev, NULL);

    event_base_dispatch(base);

    event_base_free(base);

    return 0;
}

makefile:

target:server.c client.c
    gcc server.c -o server -levent
    gcc client.c -o client -levent

.PHONY:clean
clean:
    -rm *.o server client -f
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值