当前位置: 首页 > article >正文

网络编程 day4

网络编程 day4

  • 10. IO多路复用
    • select
      • 超时监测
      • select实现并发服务器
    • poll
      • 特点
      • 编程步骤
      • 函数接口
      • 练习
    • epoll
      • 特点
      • 原理
      • 编程步骤
    • 总结
  • 11. 服务器模型
    • 分类
    • 循环服务器
    • 并发服务器
      • IO多路复用
      • 多线程/多进程
    • 总结

10. IO多路复用

select

超时监测

必要性

  1. 避免进程在没有数据的时候阻塞
  2. 规定时间内没有完成相应功能,就执行超时的语句
    select
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

参数:struct timeval *timeout
 NULL:一直阻塞,直到有文件描述符就绪或出错
不同时间值为0:仅仅检测文件描述符集的状态,然后立即返回
 时间值不为0:在指定时间内,如果没有事件发生,则超时返回0,并清空设置的时间值
 时间结构体

struct timeval 
{
    long tv_sec;		// 秒 
    long tv_usec;		// 微秒 = 10^-6秒
};

返回值:超时返回0
:超时检测实际上是- - 的过程,所以需要在循环中重置timeval结构体的内容

select实现并发服务器

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <dirent.h>

int main(int argc, char const *argv[])
{
    // 命令行判错
    if (argc != 2)
    {
        printf("argc err\n");
        return -1;
    }

    // 定义变量
    int fd_socket = -1;       // 连接套接字文件描述符
    struct sockaddr_in saddr; // 服务器网络信息
    struct sockaddr_in caddr; // 客户端网络信息
    int acc = -1;             // 通信套接字的文件描述符
    int len = sizeof(caddr);  // 客户端网络信息结构体大小
    char buf[128] = {};       // 存放收发的内容
    int ret = -1;             // 返回值
    fd_set fds;               // 创建表
    fd_set tempfds;           // 备份表
    int max = 0;              // 最大文件描述符
    int sel = -1;             // 超时检测
    struct timeval tm;        // 倒计时

    // 1. 创建套接字
    fd_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (fd_socket < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("fd_socket : %d\n", fd_socket);

    // 2. 指定网络信息
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 3. 绑定套接字
    if (bind(fd_socket, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    printf("bind ok\n");

    // 4. 监听套接字
    // 将主动套接字变成被动套接字
    // 队列1:未连接
    // 队列2:已连接
    if (listen(fd_socket, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen ok\n");

    // 清空
    FD_ZERO(&fds);
    FD_ZERO(&tempfds);
    // 添加关心的文件描述符
    FD_SET(fd_socket, &fds); // 连接文件描述符
    max = fd_socket;
    while (1)
    {
        // 设定超时检测计时 // 倒计时的过程是--的过程,会重新赋值,所以在循环里面每次重置时间
        tm.tv_sec = 2;
        tm.tv_usec = 0;
        // 表备份
        tempfds = fds;
        // 监听表
        sel = select(max + 1, &tempfds, NULL, NULL, &tm);
        if (sel == 0)
        {
            printf("time out ……\n");
            continue;
        }
        // 判断哪个文件描述符有动作
        if (FD_ISSET(fd_socket, &tempfds))
        {
            // 接收客户端连接请求
            acc = accept(fd_socket, (struct sockaddr *)&caddr, &len);
            if (acc < 0)
            {
                perror("accept err");
                return -1;
            }
            printf("accfd : %d\n", acc);
            printf("ip:%s\tport:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));

            // 添加到原始表
            FD_SET(acc, &fds);
            // 判断最大值和新来的文件描述符大小
            if (acc > max)
                max = acc;
        }
        for (int i = fd_socket + 1; i <= max; i++)
        {
            if (FD_ISSET(i, &tempfds))
            {
                // 接收消息
                ret = recv(i, buf, sizeof(buf), 0);
                if (ret < 0) // 接收失败
                {
                    perror("recv err");
                    return -1;
                }
                else if (ret == 0) // 客户端关闭
                {
                    printf("client exit\n");
                    close(i); // 关闭文件描述符
                    // 将文件描述符从原表中清除
                    FD_CLR(i, &fds);
                    // 判断调整最大值
                    while (!FD_ISSET(max, &fds))
                        max--;
                }
                else
                {
                    // 使用通信信息
                    printf("buf : %s\n", buf);
                }
            }
        }
        memset(buf, 0, sizeof(buf));
    }
}

poll

特点

  1. 优化了文件描述符的限制
  2. poll每次被唤醒之后,需要重新轮询,效率低,耗费CPU
  3. poll 不需要构造文件描述符的表,采用结构体数组,每次调用也要经过用户空间内拷贝到用户空间

编程步骤

  1. 创建结构体数组
  2. 将关心的文件描述符添加到结构体数组中,并赋予事件
  3. 保存数组内最后一个有效元素下标
  4. 调用poll函数,监听
  5. 判断结构体内文件描述符触发的事件
  6. 根据不同文件描述符发生的不同事件做对应的逻辑处理

函数接口

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

功能: 监听并等待文件描述符响应
参数:
struct pollfd *fds关心的文件描述符数组,数组元素个数自己定义struct pollfd fds[N]
nfds_t nfds最大文件描述符个数,last+1
int timeout超时检测,直接写时间,单位是毫秒
  -1:阻塞 0:不阻塞
返回值:有事件产生 > 0,失败返回 < 0,超时返回0
结构体:

struct pollfd
{
	int fd;	 //文件描述符
	short events;//等待的事件触发条件----POLLIN读时间触发
	short revents;	//实际发生的事件(未产生事件为0 ))
}

练习

输入键盘事件,响应键盘事件,输入鼠标事件,响应鼠标事件(两路IO)

int main(int argc, char const *argv[])
{
    // 变量
    struct pollfd fds[2]; // 创建结构体数组
    int fd_mouse = -1;    // 鼠标文件描述符
    int last = -1;        // 数组内最后一个有效元素的下标
    char buf[128] = {};
    int rpo = -1;

    // 打开鼠标文件
    fd_mouse = open("/dev/input/mouse0", O_RDONLY);
    if (fd_mouse < 0)
    {
        perror("open mouse err");
        return -1;
    }

    // 将关心的文件描述符和对应事件添加到结构体数组中
    fds[0].fd = 0; // 键盘
    fds[0].events = POLLIN;
    fds[1].fd = fd_mouse; // 鼠标
    fds[1].events = POLLIN;

    // 保存数组内最后一个元素的下标
    last = 1;

    while (1)
    {
        // poll函数监听
        rpo = poll(fds, last + 1, 1000); // 直接写时间,单位是毫秒
        if(rpo == 0)
        {
            printf("time out ……\n");
        }

        // 判断文件描述符触发事件
        if (fds[0].revents == POLLIN)
        {
            // 对应处理
            fgets(buf, sizeof(buf), stdin);
            printf("keyboard:%s\n", buf);
        }
        if (fds[1].revents == POLLIN)
        {
            read(fd_mouse, buf, sizeof(buf));
            printf("mouse:%s\n", buf);
        }
        memset(buf, 0, sizeof(buf));
    }
    return 0;
}

epoll

特点

  1. 监听的文件描述符没有限制
  2. 属于异步IO, epoll 有事件唤醒后,发生事件的文件描述符会主动调用 callback 拿到对应的文件描述符,不需要轮询,效率高
  3. epoll不需要构造表,只需要用用户空间复制一次到内核空间
  1. epoll支持的文件描述符上限是系统能打开的最大文件数目,1GB上限是10万左右
  2. 每个文件描述符都有一个callback函数,只有产生事件的fd才会主动调用callback,不需要轮询
  3. epoll处理百万级的并发

原理

  1. 红黑树和就绪链表在内核空间创建
  2. epoll_ctl将文件描述符上树
  3. 发生事件时,对应的文件描述符调用callback,将文件描述符和事件放到链表中
  4. epoll_wait将文件描述符返回到用户空间

编程步骤

  1. 创建红黑树和就绪链表——epoll_create
  2. 将关心的文件描述符和事件上树——epool_ctl
  3. 阻塞等待事件发生,一旦产生事件,则进行处理——epll_wait
  4. 根据准备好的文件描述符做对应的逻辑处理

总结

selectpollepoll
监听个数一个文件描述符表最多监听1024个文件描述符结构体数组定义多大就监听多少百万级
唤醒每次唤醒都需要轮询每次唤醒都需要轮询红黑树的callback自动回调,不需要轮询
效率文件描述符越多,轮询越多,效率越低文件描述符越多,轮询越多,效率越低不需要轮询,效率高
结构文件描述符(位表)结构体数组红黑树和就绪链表

11. 服务器模型

分类

  1. 循环服务器
  2. 并发服务器

循环服务器

一个服务器同时只能处理一个客户端请求

socket();
// 指定网络信息
bind();
listen();
while(1)
{
	accept();
	while(1)
	{
		recv()/send();
	}
	close(acceptfd);
}
close(sockfd);

并发服务器

一个服务器在同时可以处理多个客户端请求

IO多路复用

select poll epoll

多线程/多进程

每多一个客户端就多创建一个线程/进程通信
子线程/进程:与客户端通信
主线程/进程:等待下一个客户端连接
多线程

#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>

// 变量定义
int fd_socket = -1;       // 连接套接字文件描述符
struct sockaddr_in saddr; // 服务器网络信息
struct sockaddr_in caddr; // 客户端网络信息
int acc = -1;             // 通信套接字的文件描述符
int len = sizeof(caddr);  // 客户端网络信息结构体大小
char buf[128] = {};       // 存放收发的内容
pthread_t tid = 0;      // 线程id

void *pthreadsocket(void *arg)
{
    int ret = -1;
    int accfd =*(int *)arg;
    while (1)
    {
        ret = recv(accfd, buf, sizeof(buf), 0);
        if (ret < 0)
        {
            perror("recv err");
            pthread_exit(NULL);
        }
        if (ret == 0)
        {
            printf("client exit\n");
            close(accfd);
            pthread_exit(NULL);
        }
        printf("buf:%s\n", buf);
    }
}

int main(int argc, char const *argv[])
{
    // 1. 创建套接字
    fd_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (fd_socket < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("fd_socket : %d\n", fd_socket);

    // 2. 指定网络信息
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 3. 绑定套接字
    if (bind(fd_socket, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    printf("bind ok\n");

    // 4. 监听套接字
    // 将主动套接字变成被动套接字
    // 队列1:未连接
    // 队列2:已连接
    if (listen(fd_socket, 6) < 0)
    {
        perror("listen err");
        return -1;
    }
    printf("listen ok\n");

    while (1)
    {
        // 接收客户端连接请求
        acc = accept(fd_socket, (struct sockaddr *)&caddr, &len);
        if (acc < 0)
        {
            perror("accept err");
            return -1;
        }
        printf("accfd : %d\n", acc);
        printf("ip:%s\tport:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
        pthread_create(&tid, NULL, pthreadsocket, &acc);
        pthread_detach(tid);
    }

    return 0;
}

总结

IO多路复用多线程多进程
优点节省资源
系统开销小
性能高
资源开销小服务器稳定
资源独立
安全性高
缺点代码复杂安全性差资源开销大

http://www.kler.cn/a/576422.html

相关文章:

  • 使用开源OPUS-MT模型进行文本翻译(python)
  • 针对Ollama进行DeepSeek本地部署存在的安全风险,使用nginx进行反向代理配置是一种有效的解决方案
  • 开发环境搭建-07.后端环境搭建-前后端联调-Nginx反向代理和负载均衡配置
  • 微软发布Dragon Copilot,打造医疗行业首款AI语音助手
  • 深度学习代码解读——自用
  • Qt调试功能使用方法
  • bash: uwsgi: 未找到命令
  • 基于Python+openGauss实现(图形界面)多功能本地视频播放系统
  • 使用 Apache POI 实现 Excel 单元格合并
  • uniapp 安卓app图片回显,默认不支持http图片地址,上传图片和回显图片
  • 腾讯 TDF 即将开源 Kuikly 跨端框架,Kotlin 支持全平台
  • 人工智能与深度学习的应用案例:从技术原理到实践创新
  • 紫光无人机AI飞控平台2.0——航线管理模块
  • ⭐算法OJ⭐N-皇后问题【回溯剪枝】(C++实现)N-Queens
  • 不小心更改了/etc权限为777导致sudo,ssh等软件都无法使用
  • Vue基础之Element-ui
  • 2025-03-07 学习记录--C/C++-PTA 习题8-5 使用函数实现字符串部分复制
  • 【如何删除在 Linux 系统中的删除乱码文件】
  • SpringSecurity认证授权完整流程
  • 【JAVA架构师成长之路】【Redis】第14集:Redis缓存穿透原理、规避、解决方案