Linux网络编程(4):端口复用

端口复用

端口复用最常用的用途是:

  • 防止服务器重启时之前绑定的端口还未释放
  • 程序突然退出而系统没有释放端口
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
/*
参数:
    sockfd:要操作的文件描述符
    level:级别,有不同的选项,根据不同的选项第三个参数optval也不同。端口复用选择选项SOL_SOCKET
    optname:选项的名称,也有许多选项(以下两个为复用常见选项)
        SO_REUSEADDR:允许重用本地地址,数据类型int
        So_REUSEPORT:允许重用本地端口,数据类型int
    optval:第三个参数选择的选项的值,有int型或结构体等,上面两个选项都是int型,所以:
        1:可以复用
        0:不可以复用
    optlen:optval参数的大小
返回值:
    成功 0
    失败-1
注意:端口复用,设置的时机是在服务器绑定端口之前,即先setsockopt()再bind()。
*/

常看网络相关信息的命令
netstat
参数:
-a所有的socket
-P显示正在使用socket的程序的名称
-n直接使用IP地址,而不通过域名服务器

I/O多路复用(I/O多路转接)

I/O多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现I/O多路复用的系统调用主要有select,poll和epoll。
BIO、NIO的优缺点。select/poll只会告诉你有几个到了,epoll不仅会告诉你有几个到了,还会告诉你谁到了

select

  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
  2. 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行IO操作时,该函数才返回。
    a.这个函数是阻塞
    b.函数对文件描述符的检测的操作是由内核完成的
  3. 在返回时,它会告诉进程有多少(哪些)描述符要进行IO操作。
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/*
参数:
    nfds:委托内核检测的最大文件描述符的值 + 1
    readfds:要检测的读的文件描述符的集合,对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区。是一个传入传出参数
    writefds:要检测的写的文件描述符的集合,委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
    exceptfds:检测发生异常的文件描述符的集合。一般不用,传入NULL
    timeout:设置的超时时间
        struct timeval 
        {
            long tv_sec;    /* seconds */
            long tv_usec;    /* microseconds */
        };
        timeout = NULL:永久阻塞,直到检测到了文件描述符有变化
        tv_sec = 0,tv_usec = 0:不阻塞
        tv_sec、tv_usec不都为0:阻塞对应的时间
返回值:
    -1:调用失败
    0:过了超时时间也没有检测到发生变化的文件描述符(timeout不能是NULL)
    n(n > 0):检测的集合中有n个文件描述符发生了变化

*/

void FD_CLR(int fd, fd_set *set);    //将参数文件描述符fd对应的标志位设置为0
int FD_ISSET(int fd, fd_set *set);   //判断fd对应的标志位是0还是1,是0就返回0,是1就返回1
void FD_SET(int fd, fd_set *set);    //将参数文件描述符fd对应的标志位设置为1
void FD_ZERO(fd_set *set);           //将set中所有标志位(1024bit)全部初始化为0

读流程:

  1. 创建文件描述符集合set并初始化(FD_ZERO)
  2. 将要检测的文件描述符通过函数FD_SET加入集合
  3. 调用select函数,把set的地址传入,当检测的文件描述符中的一个或者多个进行IO操作时,该函数返回。
  4. 遍历set,找到标志位为1的文件描述符(FD_ISSET),即有数据到达的文件描述符,读数据
  5. 断开连接的文件描述符用FD_CLR函数清除
    关于集合的说明:
  6. 传入select之前的set里面的1和0代表是否检测,调用select函数后会把set从用户态拷贝进内核态
  7. 检测时文件描述符从小到大遍历。当前标志位如果为0,代表不检测,则跳过到下一位;如果标志位为1,则检测是否有数据到达,如果当前文件描述符没有数据到达则把set里的标志位置为0,有数据则置为1。这样遍历一遍后set里的1和0就代表是否有数据到达,然后再拷贝到用户态,这时的set与传入select之前的set里面的1和0所代表的含义就不同了。

select的缺点:

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024
  4. fds集合不能重用,每次都需要重置

poll

#include <poll.h>
struct pollfd 
{
    int fd;          /*委托内核检测的文件描述符*/
    short events;    /*委托内核检测文件描述符的什么事件*/
    short revents;   /*文件描述符实际发生的事件*/
};

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/*
参数:
    fds:struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
    nfds:第一个参数数组中最后一个有效元素的下标 + 1,注意是下标,不是文件描述符本身
    timeout:阻塞时长
        0:不阻塞
        -1:阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
        >0:阻塞的时长
返回值:
    -1:调用失败
    0:过了阻塞时长也没有检测到发生变化的文件描述符(timeout不能是-1)
    n(n > 0):检测的集合中有n个文件描述符发生了变化
*/

events和revents表格,可同时有多个常值,中间用 | 隔开

事件 常值 作为events的值 作为revents的值 说明
读事件 POLLIN 普通或优先带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级数据可读
写事件 POLLOUT 普通或优先带数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
错误事件 POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述不是打开的文件

注意:

  1. poll解决了select中“支持的文件描述符数量太小了”、“fds集合不能重用,每次都需要重置”的问题,其他问题还是没有解决。
  2. 读事件中,poll函数返回后,在判断当前文件描述符是否有数据到达时用:POLLIN & pollfd[i].revents 。写事件一样

epoll

#include <sys/epoll.h>


//创建一个新的epoll实例。在内核中创建了一个数据,这个数据中有两个比较重要的成员,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。
int epoll_create(int size);
/*
参数:
    size:目前没有意义了。可以随便写一个数,但必须大于0
返回值:
    -1:失败
    >0:文件描述符,操作epoll实例的
*/

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 */
};
/*
常见的epoll检测事件:
    - EPOLLIN  
    - EPOLLOUT
    - EPOLLERR
    - EPOLLET    :设置为ET工作模式
*/


//对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
参数:
    epfd:epoll实例对应的文件描述符
    op:要进行什么操作
        EPOLL_CTL_ADD:添加
        EPOLL_CTL_MOD:修改
        EPOLL_CTL_DEL:删除
    fd:要检测的文件描述符
    event:检测文件描述符什么事情
*/


//检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/*
参数:
    epfd:epoll实例对应的文件描述符
    events:传出参数,保存了发送了变化的文件描述符的信息
    maxevents:最多监听多少个事件,它必须大于0。
    timeout:阻塞时间
        0:不阻塞
        -1:阻塞,直到检测到fd数据发生变化,解除阻塞
        > 0:阻塞的时长(毫秒)
返回值:
    成功,返回发送变化的文件描述符的个数 >0
    失败 -1
*/

epoll的工作模式:

  • LT模式(水平触发)
    假设委托内核检测读事件 -> 检测fd的读缓冲区
      读缓冲区有数据 -> epoll检测到了会给用户通知
        a.用户不读数据,数据一直在缓冲区,epoll 会一直通知
        b.用户只读了一部分数据,epoll会通知
        C.缓冲区的数据读完了,不通知

LT (level - triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

  • ET模式(边沿触发)
    假设委托内核检测读事件 -> 检测fd的读缓冲区
      读缓冲区有数据 -> epoll检测到了会给用户通知
        a.用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
        b.用户只读了一部分数据,epoll不通知
        C.缓冲区的数据读完了,不通知

ET (edge - triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。

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

注意:ET模式下,注意read函数返回-1时的错误号“EAGIN”,需要特殊处理

posted @ 2022-09-23 22:08  小肉包i  阅读(799)  评论(0)    收藏  举报