计算机网络开发(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 ,然后把这个交给内核 ,由内核对 这些文件描述符进行检测 (我们不需要知道他是怎么检测的,反正他就是知道了)。最后内核返回有变动的文件描述符的数量,和文件描述符列表。我们再从这之中遍历。去读取

cpp 复制代码
#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操作该实例 (解决了总是在内核态和用户态拷贝转化内的资源浪费)
  • 结构体中有红黑树双链表,分别用来存储需要检测的文件描述符和**存储已经发生改变的文件描述符
cpp 复制代码
#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是一个结构体,用于存放文件描述符的信息,看可以重用。

如果有多个监听文件例如读和写分开监听,那就分开设置。这里就设置了监听读

cpp 复制代码
服务端:
#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;

相关推荐
zzy20887402716 分钟前
网络初级复习作业
网络
渗透测试老鸟-九青10 分钟前
我与红队:一场网络安全实战的较量与成长
运维·服务器·网络·经验分享·安全·web安全·代码审计
黑风风27 分钟前
详解了解websocket协议
网络·websocket·网络协议
黑客-秋凌29 分钟前
网络安全基础与应用习题 网络安全基础答案
网络·安全·web安全
2022计科一班唐文1 小时前
靶场练习ing
网络·渗透
Albert XUU1 小时前
nettrace rtt分析器
linux·运维·网络·网络协议·网络安全·腾讯云·运维开发
桃酥4031 小时前
17、UDP怎么实现可靠传输【中高频】
网络·网络协议·udp
做我想做2 小时前
虚拟机 CentOS 9 网络配置
linux·网络·centos
东阳马生架构2 小时前
Netty基础—2.网络编程基础三
网络·netty
_丿丨丨_2 小时前
Django下防御Race Condition
网络·后端·python·django