计算机网络开发(3)——端口复用、I\O多路复用
端口复用
由于有一个MSL,所以上一秒关闭的服务器,可能之前的端口还未释放;又或者是程序突然退出系统没有释放端口,导致端口被占用。
当有新的服务想要用这个端口的时候,会出现错误:服务会出现Bind error:Address already in use
解决办法
设置套接字属性,
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- 功能:设置套接字的属性(不仅仅能设置端口复用)
- 参数
sockfd
:要操作的文件描述符level
:级别,SOL_SOCKET
(端口复用的级别)optname
:选项的名称,使用SO_REUSEADDR
(解决在TimeWait的情况)或SO_REUSEPORT
(允许多个线程、进程绑在同一个端口上)optval
:端口复用的值(整形) ,1表示可复用,0表示不可复用optlen
:optval参数的大小
netstat -参数名
a:所有的socket
p:显示正在使用socket的程序名称
n:直接使用IP地址,不通过域名服务器
I\O多路复用(IO多路转接
BIO模型
遇到read/recv/accept的时候,需要阻塞等待,直到有数据或者连接的时候才能继续往下执行
要么是单任务的时候,一个时刻只能处理一个操作,效率低;
要么是多任务的时候,虽然是多进程、多线程,一个线程或者进程对应一个任务, 但在进程线程之间的切换会消耗CPU资源。
这些的根本问题都是阻塞在那。
select
大概思想是:将要监听的文件的文件描述符表放到一个列表里面,需要监听的就置为1 ,然后把这个交给内核,由内核对这些文件描述符进行检测(我们不需要知道他是怎么检测的,反正他就是知道了)。最后内核返回有变动的文件描述符的数量,和文件描述符列表。我们再从这之中遍历。去读取
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds:委托内核检测的最大文件描述符的值 + 1(+1是因为遍历是下标从0开始,for循环<设定)
readfds:要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性一般检测读操作
对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区是一个传入传出参数
writefds:要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
exceptfds:检测发生异常的文件描述符的集合,一般不用
timeout:设置的超时时间,含义见select参数列表说明
NULL:永久阻塞,直到检测到了文件描述符有变化
tv_sec = tv_usec = 0 不阻塞
tv_sec > 0,tv_usec > 0:阻塞对应的时间
* 返回值
* -1:失败
* \>0(n):检测的集合中有n个文件描述符发生了变化
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int FD_ISSET(int fd, fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);
注意事项:因为内核返回去会改变这个文件包描述符列表,所以最好:
select
中需要的监听集合需要两个:
- 一个是用户态真正需要监听的集合
rSet
- 一个是内核态返回给用户态的修改集合
tmpSet
会存在的问题
- 每次都需要利用
FD_ISSET
轮训[0, maxfd]
之间的连接状态,如果位于中间的某一个客户端断开了连接,此时不应该再去利用FD_ISSET
轮训,造成资源浪费 - 如果在处理客户端数据时,某一次read没有对数据读完,那么造成重新进行下一次时select,获取上一次未处理完的文件描述符,从0开始遍历到maxfd,对上一次的进行再一次操作,效率十分低下
解决:
- 考虑到
select
只有1024
个最大可监听数量,可以申请等量客户端数组- 初始置为-1,当有状态改变时,置为相应文件描述符
- 此时再用
FD_ISSET
轮训时,跳过标记为-1的客户端,加快遍历速度
- 对于问题二:对读缓存区循环读,直到返回
EAGAIN
再处理数据
select缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
- fds集合不能重用,每次都需要重置
poll
解决了上面缺点的三四条
epoll
- 直接在内核态创建
eventpoll实例
(结构体),通过epoll
提供的API操作该实例 (解决了总是在内核态和用户态拷贝转化内的资源浪费) - 结构体中有
红黑树
和双链表
,分别用来存储需要检测的文件描述符和**存储已经发生改变的文件描述符
#include <sys/epoll.h>
// 创建一个新的epoll实例
// 在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)
int epoll_create(int size);
// 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
// 检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_create(int size);
- 功能:创建一个新的epoll实例
- 参数:
size
,目前没有意义了(之前底层实现是哈希表,现在是红黑树),随便写一个数,必须大于0 - 返回值
- -1:失败
- >0:操作
epoll实例
的文件描述符 epfd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 功能:对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
- 参数:
epfd
:epoll实例对应的文件描述符op
:要进行什么操作- 添加:
EPOLL_CTL_ADD
- 删除:
EPOLL_CTL_DEL
- 修改:
EPOLL_CTL_MOD
- 添加:
fd
:要检测的文件描述符event
:检测文件描述符什么事情,通过设置epoll_event.events
,常见操作- 读事件:
EPOLLIN
- 写事件:
EPOLLOUT
- 错误事件:
EPOLLERR
- 设置边沿触发:
EPOLLET
(默认水平触发)
- 读事件:
- 返回值:成功0,失败-1
-
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 功能:检测哪些文件描述符发生了改变
- 参数:
epfd
:epoll实例对应的文件描述符events
:传出参数,这个结构体保存了发生了变化的文件描述符的信息maxevents
:第二个参数结构体数组的大小timeout
:阻塞时长- 0:不阻塞
- -1:阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
- >0:具体的阻塞时长(ms)
- 返回值:
- > 0:成功,返回发送变化的文件描述符的个数
- -1:失败
实现代码
里面的events是一个结构体,用于存放文件描述符的信息,看可以重用。
如果有多个监听文件例如读和写分开监听,那就分开设置。这里就设置了监听读
服务端:
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#define SERVERIP "127.0.0.1"
#define PORT 6789
int main()
{
// 1. 创建socket(用于监听的套接字)
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
perror("socket");
exit(-1);
}
// 2. 绑定
struct sockaddr_in server_addr;
server_addr.sin_family = PF_INET;
// 点分十进制转换为网络字节序
inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr.s_addr);
// 服务端也可以绑定0.0.0.0即任意地址
// server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
int ret = bind(listenfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1) {
perror("bind");
exit(-1);
}
// 3. 监听
ret = listen(listenfd, 8);
if (ret == -1) {
perror("listen");
exit(-1);
}
// 创建epoll实例
int epfd = epoll_create(100); //随便写一个大于0的就行
// 将监听文件描述符加入实例
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = listenfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event);
if (ret == -1) {
perror("epoll_ctl");
exit(-1);
}
// 此结构体用来保存内核态返回给用户态发生改变的文件描述符信息
struct epoll_event events[1024]; //这里设置1024个结构体,那么就是最多同时连接1024个,具体个数可以自己再调整
// 不断循环等待客户端连接
while (1) {
// 使用epoll,设置为永久阻塞,有文件描述符变化才返回
int num = epoll_wait(epfd, events, 1024, -1);
if (num == -1) {
perror("poll");
exit(-1);
} else if (num == 0) {
// 当前无文件描述符有变化,执行下一次遍历
// 在本次设置中无效(因为select被设置为永久阻塞)
continue;
} else {
// 遍历发生改变的文件描述符集合
for (int i = 0; i < num; i++) {
// 判断监听文件描述符是否发生改变(即是否有客户端连接)
int curfd = events[i].data.fd;
if (curfd == listenfd) {
// 4. 接收客户端连接
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &client_addr_len);
if (connfd == -1) {
perror("accept");
exit(-1);
}
// 输出客户端信息,IP组成至少16个字符(包含结束符)
char client_ip[16] = {0};
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip));
unsigned short client_port = ntohs(client_addr.sin_port);
printf("ip:%s, port:%d\n", client_ip, client_port);
// 将信息加入监听集合
event.events = EPOLLIN;
event.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);
} else {
// 只检测读事件
if (events[i].events & EPOLLOUT) {
continue;
}
// 接收消息
char recv_buf[1024] = {0};
ret = read(curfd, recv_buf, sizeof(recv_buf));
if (ret == -1) {
perror("read");
exit(-1);
} else if (ret > 0) {
printf("recv server data : %s\n", recv_buf);
write(curfd, recv_buf, strlen(recv_buf));
} else {
// 表示客户端断开连接
printf("client closed...\n");
close(curfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
break;
}
}
}
}
}
close(listenfd);
close(epfd);
return 0;
}
EPOLL的LT方式和ET方式
- LT(缺省模式)
- 用户不读数据,数据一直在缓冲区,epoll 会一直通知
- 用户只读了一部分数据,epoll会通知
- 缓冲区的数据读完了,不通知
- ET
- 用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
- 用户只读了一部分数据,epoll不通知
- 缓冲区的数据读完了,不通知
ET的设计方式:event.events = EPOLLIN | EPOLLET;