• socket编程之select(),poll(),epoll()


    socket编程,通信

          client端  socket() ----->connect() ------->recv() -----> close();

          server端 socket() ----->bind()  ------> listen() ---->accept() ------>send() ------->close();

    1> socket(int family,int type,int protocol);

    family ->协议类型

    AF_INET    -------------->  IPV4

    AF_INET6  ---------------> IPV6

    AF_ROUTE ---------------> 路由套接口

    type->是指套接字类型

    SOCK_STREAM  -------------------->  字节流套接字   TCP

    SOCK_DGRAM   -------------------->  数据报套接字  UDP

    SOCK_RAW       -------------------->  原始套接字

    函数调用成功返回小的非负整数,文件描述符类型,否则返回-1表示调用失败

    2.connect(int sockfd,struct sockaddr*addr,socklen_t addrlen)

    sockfd 是从socket的返回值;

    addr是指向服务器的套接字的地址结构的指针

    addrlen是该套接字的地址结构的大小

    1 struct sockaddr_in server;
    2 bzero(&server,sizeof(server));
    3 server.sin_family = AF_INET;
    4 server.sin_port = htonl(1234);
    5 server.sin_addr.s_addr = inte_addr("127.0.0.1");
    6 if(connect(sockfd,(struct sockaddr *)&server,sizeof(server)) == -1)
    7 {
    8     //强制性地址转换,转为通用套接字结构
    9 }

    3>bind(int sockfd,struct sockaddr *server,socklen_len addrlen);

    sockfd->socket的返回值

    server->表示指向于特定协议的地址结构的指针

    addrlen->表示该套接字的地址结构的长度

    1 struct sockaddr_in server;
    2 bzero(&server,sizeof(server));
    3 server.sin_family = AF_INET;
    4 server.sin_port = htonl(1234);
    5 server.sin_addr.s_addr = htonl(INADDR_ANY);
    6 if(bind(sockfd,(struct sockaddr *)&server,sizeof(server)) == -1)
    7 {
    8     ///sockaddr 强制转换
    9 }

     4>listen(int sockfd,int backlog)

    sockfd --->socket的返回值

    backlog --->规定了请求返回队列的最大连接数,它对队列中等待服务请求的数目进行限制,如果一个服务请求到来时,输入队列已满,该套接字将拒绝连接请求。

    listen(sockfd,5);

    对于一个监听套接字,内核要维护两个队列:未完成连接队列和已完成连接队列

    未完成连接队列:为每个请求建立连接的SYN分节设一个条目,服务器正等待完成三次握手,当前的套接字处在SYN_RECD状态

    已完成连接队列:为每个完成TCP三次握手的客户端开设一个条目,当前的套接字状态时ESTABLISHED。

    5>accept(int sockfd,struct sockaddr *client,socklen_t *addrlen)

     sockfd 是socket()返回的套接字描述符,在调用listen的时候此套接字变成了监听套接字

    client 是返回对方的套接字和地址结构

    addrlen 是对应结构的长度

    accetp()函数返回,已连接套接字描述符。

      注:

        一个服务器只能有一个监听套接字,而且会一直存在,直到服务器关闭,而已连接套接字描述符是内核为每个被接受的客户都创建一个,当服务器完成与客户的数据传输时,要关闭连接套接字,所以监听套接字接受客户的链接请求,已连接套接字描述符负责对应客户进行数据传送。

    1 int listenfd,connfd;
    2 struct sockaddr_in client;
    3 socklen_t addrlen;
    4 addrlen = sizeof(client);
    5 connfd = accept(listenfd,(struct sockaddr *)&client,&addrlen);
    6 if(connfd == -1)
    7 {
    8     
    9 }

     原型如下:

    1 int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, const struct timeval *timeout);


    各个参数含义如下:

    • int maxfdp:最大描述符值 + 1
    • fd_set *readfds:对可读感兴趣的描述符集
    • fd_set *writefds:对可写感兴趣的描述符集
    • fd_set *errorfds:对出错感兴趣的描述符集
    • struct timeval *timeout:超时时间(注意:对于linux系统,此参数没有const限制,每次select调用完毕timeout的值都被修改为剩余时间,而unix系统则不会改变timeout值)

    select函数会在发生以下情况时返回:

    1. readfds集合中有描述符可读
    2. writefds集合中有描述符可写
    3. errorfds集合中有描述符遇到错误条件
    4. 指定的超时时间timeout到了

    当select返回时,描述符集合将被修改以指示哪些个描述符正处于可读、可写或有错误状态。可以用FD_ISSET宏对描述符进行测试以找到状态变化的描述符。如果select因为超时而返回的话,所有的描述符集合都将被清空。
    select函数返回状态发生变化的描述符总数。返回0意味着超时。失败则返回-1并设置errno。可能出现的错误有:EBADF(无效描述符)、EINTR(因终端而返回)、EINVAL(nfds或timeout取值错误)。
    设置描述符集合通常用如下几个宏定义:

    1 FD_ZERO(fd_set *fdset);                /* 将所有位设为0   */
    2 FD_SET(int fd, fd_set *fdset);         /* 将fd位设为1     */
    3 FD_CLR(int fd, fd_set *fdset);         /* 将fd位设为0     */
    4 int FD_ISSET(int fd, fd_set *fdset);   /* 检测fd位是否为1 */

    如:

    1 fd_set  set;
    2 FD_ZERO(&rset);                        /* i初始化rset */
    3 FD_SET(1, &rset);                      /* 将fd = 1 的描述符设为1  */
    4 FD_SET(4, &rset);                      /* 将fd = 4 的描述符设为1   */
    5 FD_SET(5, &rset);                      /* 将fd = 5 的描述符设为1  */

    当select返回的时候,rset位都将被置0,除了那些有变化的fd位。
    当发生如下情况时认为是可读的:

    1. socket的receive buffer中的字节数大于socket的receive buffer的low-water mark属性值。(low-water mark值类似于分水岭,当receive buffer中的字节数小于low-water mark值的时候,认为socket还不可读,只有当receive buffer中的字节数达到一定量的时候才认为socket可读)
    2. 连接半关闭(读关闭,即收到对端发来的FIN包)
    3. 发生变化的描述符是被动套接字,而连接的三路握手完成的数量大于0,即有新的TCP连接建立
    4. 描述符发生错误,如果调用read系统调用读套接字的话会返回-1。

    当发生如下情况时认为是可写的:

    1. socket的send buffer中的字节数大于socket的send buffer的low-water mark属性值以及socket已经连接或者不需要连接(如UDP)。
    2. 写半连接关闭,调用write函数将产生SIGPIPE
    3. 描述符发生错误,如果调用write系统调用写套接字的话会返回-1。

    注意:
    select默认能处理的描述符数量是有上限的,为FD_SETSIZE的大小。
    对于timeout参数,如果置为NULL,则表示wait forever;若timeout->tv_sec = timeout->tv_usec = 0,则表示do not wait at all;否则指定等待时间。
    如果使用select处理多个套接字,那么需要使用一个数组(也可以是其他结构)来记录各个描述符的状态。而使用poll则不需要,下面看poll函数。

    (二)poll()函数

    原型如下:

    1 int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);


    各参数含义如下:

    • struct pollfd *fdarray:一个结构体,用来保存各个描述符的相关状态。
    • unsigned long nfds:fdarray数组的大小,即里面包含有效成员的数量。
    • int timeout:设定的超时时间。(以毫秒为单位)

    poll函数返回值及含义如下:

    • -1:有错误产生
    • 0:超时时间到,而且没有描述符有状态变化
    • >0:有状态变化的描述符个数

    着重讲fdarray数组,因为这是它和select()函数主要的不同的地方:
    pollfd的结构如下:

    1 struct pollfd {
    2      int fd;                  /* 测试描述符*/
    3      short events;      /* 测试条件*/
    4     short revents;     /* 测试结果 */
    5 };

    其实poll()和select()函数要处理的问题是相同的,只不过是不同组织在几乎相同时刻同时推出的,因此才同时保留了下来。select()函数把可读描述符、可写描述符、错误描述符分在了三个集合里,这三个集合都是用bit位来标记一个描述符,一旦有若干个描述符状态发生变化,那么它将被置位,而其他没有发生变化的描述符的bit位将被clear,也就是说select()的readset、writeset、errorset是一个value-result类型,通过它们传值,而也通过它们返回结果。这样的一个坏处是每次重新select 的时候对集合必须重新赋值。而poll()函数则与select()采用的方式不同,它通过一个结构数组保存各个描述符的状态,每个结构体第一项fd代表描述符,第二项代表要监听的事件,也就是感兴趣的事件,而第三项代表poll()返回时描述符的返回状态。合法状态如下:

    • POLLIN:                有普通数据或者优先数据可读
    • POLLRDNORM:    有普通数据可读
    • POLLRDBAND:    有优先数据可读
    • POLLPRI:              有紧急数据可读
    • POLLOUT:            有普通数据可写
    • POLLWRNORM:   有普通数据可写
    • POLLWRBAND:    有紧急数据可写
    • POLLERR:            有错误发生
    • POLLHUP:            有描述符挂起事件发生
    • POLLNVAL:          描述符非法

    对于POLLIN | POLLPRI等价与select()的可读事件;POLLOUT | POLLWRBAND等价与select()的可写事件;POLLIN 等价与POLLRDNORM | POLLRDBAND,而POLLOUT等价于POLLWRBAND。如果你对一个描述符的可读事件和可写事件以及错误等事件均感兴趣那么你应该都进行相应的设置。
    对于timeout的设置如下:

        INFTIME:wait forever

        0    :不等待,轮训

        >0    :等待特定的时间。

    (三)epoll() 函数

        epoll()函数是在LINUX2.6中被提到的。

        epoll()函数有三个接口

        

    1 #include <sys/epoll.h>
    2 int epoll_create(int size);
    3 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    4 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

    epoll_create()函数中size告诉内核监听数目有多少。

    epoll_ctl()表第一个函数是epoll_create()的返回值,op表示要进行的操作,fd表示需要监听的fd,event告诉内核需要监听什么事

    op有三种宏定义:

    EPOLL_CTL_ADD://注册新的fd到epfd中;
    EPOLL_CTL_MOD://修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL://从epfd中删除一个fd;

    struct epoll_event *event函数的结构体特征是

    1 struct epoll_event
    2 {
    3     __uint32_t events;/* epoll事件 */
    4     epoll_data_t data;/*用户数据变量 */
    5 };

    其中epoll_event结构体中events事件:

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

    epfd:是epoll_create的返回值

    events:是从内核得到的事件集合

    maxevents:告诉内核events的最大数目

    timeout:表示时间(0表示不等待,-1不确定,有的说法是阻塞)

    epoll的工作模式:

    epoll对文件描述符的操作有两种操作:LT(水平触发)&ET(边缘触发)

      水平触发:当epoll_wait检测到描述符事件发生并将事件通知应用程序,应用程序可以不立刻处理事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

      边缘触发:当epoll_wait检测到描述符事件发生并将时间通知应用程序,应用程序必须立刻处理事件。如果不处理下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

      边缘触发事件在很大程度上减少了epoll事件被重复触发的次数,因此效率被水平触发高。epoll在边缘触发的时候,必须使用非阻塞接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把多个文件描述符的任务饿死。

  • 相关阅读:
    连接池的配置与维护
    对面向对象的理解
    ActiveMQ重试机制
    activemq持久化的几种方式详解
    zookeeper写数据流程
    G1垃圾回收器
    REDIS哨兵模式和集群模式
    REDIS复制
    REDIS参数配置和运行状态
    slow-log 和bin-log相关参数介绍
  • 原文地址:https://www.cnblogs.com/chenyang920/p/5469587.html
Copyright © 2020-2023  润新知