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
- 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
- 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行IO操作时,该函数才返回。
a.这个函数是阻塞
b.函数对文件描述符的检测的操作是由内核完成的 - 在返回时,它会告诉进程有多少(哪些)描述符要进行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
读流程:
- 创建文件描述符集合set并初始化(FD_ZERO)
- 将要检测的文件描述符通过函数FD_SET加入集合
- 调用select函数,把set的地址传入,当检测的文件描述符中的一个或者多个进行IO操作时,该函数返回。
- 遍历set,找到标志位为1的文件描述符(FD_ISSET),即有数据到达的文件描述符,读数据
- 断开连接的文件描述符用FD_CLR函数清除
关于集合的说明: - 传入select之前的set里面的1和0代表是否检测,调用select函数后会把set从用户态拷贝进内核态
- 检测时文件描述符从小到大遍历。当前标志位如果为0,代表不检测,则跳过到下一位;如果标志位为1,则检测是否有数据到达,如果当前文件描述符没有数据到达则把set里的标志位置为0,有数据则置为1。这样遍历一遍后set里的1和0就代表是否有数据到达,然后再拷贝到用户态,这时的set与传入select之前的set里面的1和0所代表的含义就不同了。
select的缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
- 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 | ✅ | 描述不是打开的文件 |
注意:
- poll解决了select中“支持的文件描述符数量太小了”、“fds集合不能重用,每次都需要重置”的问题,其他问题还是没有解决。
- 读事件中,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”,需要特殊处理
浙公网安备 33010602011771号