多路IO复用[select版TCP服务器与poll版TCP服务器]与常用网络编程函数详解

目录

select()接口

函数的原型:

返回值:

附带操作fd_set的四个宏

select版TCP版服务器代码示例

poll()接口

函数原型:

返回值:

poll版TCP版服务器代码示例

网络编程常用函数

socket()

bind()

inet_addr()

[htons() htonl()](#htons() htonl())

listen()

accept()


为每个客户端都开一个线程虽然能够提高实时性, 但是非常浪费资源并且具备一定风险!

所以使用多路 IO 复用:
IO复用(Input/Output Multiplexing)是一种高效的技术,用于在单个线程中管理多个输入/输出通道。它允许程序同时监听多个IO事件(如网络套接字、文件描述符等),并在有事件发生时进行处理,而无需为每个通道创建独立的线程或进程。

通过IO复用,程序可以避免阻塞在单一IO操作上,从而提升系统的并发性能

select()接口

函数的原型:

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| int select ( int nfds **, //**所有被监控的 fd 中, 最大值 + 1 fd_set * readfds , // 可读文件描述符集合 fd_set * writefds , // 可写文件描述符集合 fd_set * exceptfds , // 异常事件文件描述符集合 struct timeval * timeout // 超时时间,填NULL表示无限阻塞,直到有事件发生 ); |

返回值:

|--------------------------------------|
| 成功,返回所有集合中就绪 fd 的总数 超时,返回 0 失败,返回 -1 |

附带操作fd_set的四个宏

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| void FD_SET**(int fd,fd_set *set); // 往集合里面添加一个文件描述符 void FD_CLR(int fd,fd_set*set); //从集合里面 删除一个指定的文件描述符 int FD_ISSET(int fd,**fd_set ***set); //查看集合里面是否存在这个文件描述符 void FD_ZERO(**fd_set *set); // 清空整个集合,使用前调用 |

select版TCP版服务器代码示例

复制代码
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include "stdio.h"
#include "stdint.h"
#include "stdlib.h"
#include "string.h"
#include "fcntl.h"
#include "sys/socket.h"
#include "netinet/in.h"
#include <arpa/inet.h>
#include <sys/stat.h>

int main(void)
{
// 1.创建套接字
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == socket_fd)
    {
        perror("socket_fd");
        return -1;
    }
// 2.设置端口复用
    int opt = 1;
    setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    setsockopt(socket_fd, SOL_SOCKET, 15, &opt, sizeof(opt));
// 3.绑定IP和端口号
    struct sockaddr_in addr = {0};
    addr.sin_addr.s_addr = 0;   // 万用IP
    addr.sin_family = AF_INET;   // IPV4
    addr.sin_port = htons(6000);  // 端口号
    int result = bind(socket_fd, (struct sockaddr*)&addr, sizeof addr);
    if(result < 0)   // 成功result= 0,失败result = -1
    {
        perror("bind:");
        close(socket_fd);
        return -1;
    }
// 4.设置监听数
    listen(socket_fd, 16);
    printf("服务器已开启\r\n");
//------------------------------------------------------------------
// 1. 创建一个读表
    int max_fd = 0;
    max_fd = socket_fd;  // 目前服务器套接字为最大的描述符
    fd_set readfds, backupsfds;   // 读表和备份
// 2. 清空读表
    FD_ZERO(&readfds);
// 3. 把服务器套接字添加进读表
    FD_SET(socket_fd, &readfds);
// 4. 备份读表
    backupsfds = readfds;
// 5. 循环:阻塞检测
    while(1)
    {
        readfds = backupsfds;
        result = select(max_fd+1, &readfds, NULL, NULL, NULL);
        if(result >= 1)  // 至少有一个监测的文件发生了事件
        {
            for(int i = socket_fd; i <= max_fd; i++) // 遍历读表
            {
                if(FD_ISSET(i, &readfds)) // 找到出现事件的文件
                {
                    if(i == socket_fd)  // 服务器套接字出现事件
                    {
                        // 接受客户端的连接
                        int client_fd = accept(socket_fd, NULL, NULL);
                        // 将连接的客户端的套接字描述符加入读表
                        FD_SET(client_fd, &backupsfds);
                        max_fd = max_fd < client_fd ? client_fd : max_fd;
                        printf("客户端%d连接!\n", client_fd);
                    }
                    else   // 客户套接字事件
                    {
                        char buf[1024] = {0};
                        int len = read(i, buf, sizeof buf);
                        if(0 == len)  // 客户端离线
                        {
                            close(i);
                            FD_CLR(i, &backupsfds);
                            if(max_fd == i) max_fd--;
                            printf("客户端%d离线...\n", i);
                            continue; // 
                        }
                        printf("客户端%d->%s\n", i , buf);
                    }
                }
            }
        }
    }
    return 0;
}

poll()接口

函数原型:

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| #include <poll.h> struct pollfd { int fd; /* 要监控的文件描述符 */ short events; /* 输入参数:需要监测的事件 */ short revents; /* 输出参数:内核返回的、实际发生的事件 */ }; int poll(struct pollfd *fds, // 指向pollfd结构体数组的数组名          nfds_t nfds, // 第一个参数数组的元素个数,即要监控多少个描述符          int timeout // 等待的超时时间,单位是ms, 填-1为死等 ); |

返回值:

|-----------------------------------|
| 成功:返回有事件发生的文件描述符个数 超时:返回0 失败:返回-1 |

poll版TCP版服务器代码示例

复制代码
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include "stdio.h"
#include "stdint.h"
#include "stdlib.h"
#include "string.h"
#include "fcntl.h"
#include "sys/socket.h"
#include "netinet/in.h"
#include <arpa/inet.h>
#include <sys/stat.h>
#include <poll.h>

#define SERVER_IP    "192.168.61.130"
#define SERVER_PORT  60000

int main(void)
{
// 1. 创建服务器套接字
    int socket_fd = socket(AF_INET, SOCK_STREAM,  0);
    if(socket_fd < 0) 
    {
        perror("socket_fd");
        return -1;
    }
// 2. 绑定地址信息
    struct sockaddr_in addr = {0};
    addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    addr.sin_port = htons(SERVER_PORT);
    addr.sin_family = AF_INET;
    int result = bind(socket_fd, (struct sockaddr*)&addr, sizeof addr);
    if(result != 0)
    {
        perror("bind");
        return -1;
    }
// 3. 监听
    listen(socket_fd, 10); 
    printf("服务器已开启 IP->%s  PORT->%d\n", SERVER_IP, SERVER_PORT);
//--------------------------------------------------------------------
// 1. 创建一个poll需要的监测结构体数组
    struct pollfd fd_array[10] = {0};
    uint8_t fd_array_itemCount = 0;  // 数组中成员的总数
// 2. 添加服务器套接字描述符至数组
    fd_array[0].fd = socket_fd;
    fd_array[0].events = POLLIN; // 有数据可读(有数据输入)
    fd_array[0].revents = 0; // 实际上发生的事件类型
    fd_array_itemCount++;
// 3.轮训监测
    while(1)
    {
        result = poll(fd_array, fd_array_itemCount, -1);
        if(result > 0)  // 监测的文件有事件发生
        {
            for(int i = 0; i < fd_array_itemCount; i++)
            {
                // 找到触发事件的文件
                if(fd_array[i].events == fd_array[i].revents)
                {
                    if(fd_array[i].fd == socket_fd) // 如果是服务器
                    {
                        // 接受客户端的连接
                        int client_fd = accept(socket_fd, NULL, NULL);
                        // 添加客户端套接字描述符至结构体数组
                        fd_array[fd_array_itemCount].fd = client_fd;
                        fd_array[fd_array_itemCount].events = POLLIN;
                        fd_array[fd_array_itemCount].revents = 0;
                        fd_array_itemCount++;
                        printf("客户端%d连接成功..\n", client_fd);
                    }
                    else  // 客户端
                    {
                        char buf[1024] = {0};
                        int len = read(fd_array[i].fd, buf, sizeof buf);
                        if(len == 0)  // 客户端离线
                        {
                            printf("客户端%d离线...\n", fd_array[i].fd);
                            // 1. 关闭客户端套接字
                            close(fd_array[i].fd);
                            // 2. 从数组中删除客户端套接字描述符
                            for(int j = i; j < fd_array_itemCount; j++)
                            {
                                fd_array[j] = fd_array[j+1];
                            }
                            // 3. 更新数组中成员的总数
                            fd_array_itemCount--;
                            
                        }
                        else  // 正常收到消息
                        {
                            printf("收到客户端%d信息->%s\n", fd_array[i].fd, buf);
                        }
                    }
                }
            }
        }
    }
    return 0;
}

网络编程常用函数

socket()

作用:创建一个socket套接字

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| #include <sys/socket.h> int socket(         int domain, // 协议族(AF_INET 用于 IPv4,AF_INET6 用于 IPv6)         int type, // 套接字类型(SOCK_STREAM 用于 TCP,SOCK_DGRAM 用于 UDP)         int protocol // 通常填 0,让系统自动选择协议 ); 返回值: 成功返回文件描述符(>=0),失败返回 -1 |

bind()

作用 :用于绑定套接字的 IP 属性和端口号

|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| #include <sys/types.h> #include <sys/socket.h> int bind(         int sockfd,              // 已经创建但尚未绑定的套接字         const struct sockaddr *addr, // 要绑定的 IP 地址和端口号         socklen_t addrlen // addr结构体的长度,一般用sizeof() ); 返回值: 0: 绑定成功 -1:绑定失败 |

示例:

复制代码
// 2. 绑定地址信息
    struct sockaddr_in addr = {0};
    addr.sin_addr.s_addr = inet_addr("192.168.130.31);
    addr.sin_port = htons(55555);
    addr.sin_family = AF_INET;
    int result = bind(socket_fd, (struct sockaddr*)&addr, sizeof addr);
    if(result != 0)
    {
        perror("bind");
        return -1;
    }

inet_addr()

inet_addr() 是一个将点分十进制 IPv4 地址字符串(如 "192.168.1.1")转换为 32 位网络字节序整数的函数

函数原型:

|--------------------------------------------------------------------------------------------------------------|
| #include <arpa/inet.h> // Linux 下的头文件 in_addr_t inet_addr(const char *cp); 返回值: 将字符串IP装换为大端的32位数字IP |

示例:

复制代码
addr.sin_addr.s_addr = inet_addr("192.168.130.31);

htons() htonl()

16 或32 位整数从主机字节序转换为网络字节序

host to network short / long

函数原型:

|----------------------------------------------------------------------------------------------------------------------------------------------------|
| #include <arpa/inet.h> // Linux #include <winsock2.h> // Windows uint16_t htons(uint16_t hostshort); uint32_t htonl(uint32_t hostlong); |

listen()

listen() 开启监听且设置最大监听数量

函数原型:

|---------------------------------------------------------------------------------------------------------------|
| #include <sys/types.h> #include <sys/socket.h> int listen(int sockfd, int backlog); 返回值: 0:监听成功 -1:失败 |

accept()

函数功能:用于接受客户端的连接。

函数原型:

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| int accept ( int sockfd , // 服务器的套接字 struct sockaddr * addr , // 存放客户端地址信息的结构体地址 socklen_t * addrlen // 上一个参数的大小,sizeof(addr) ) 返回值: 成功: 返回客户的 通信套接字 失败:错误码 |

如有错误或不完善之处, 请不吝赐教。

相关推荐
天空'之城20 小时前
C 语言工业级通用组件手写 24:互补滤波
c语言·开发语言·互补滤波·嵌入式算法·工业级组件
杨云龙UP21 小时前
MySQL 数据库常用备份工具对比:mysqldump、mydumper 与 XtraBackup
linux·运维·服务器·数据库·mysql·dba·备份恢复
happymagic1 天前
java spring boot做的jar包程序,如何实现自动运行启动
java·运维·服务器·spring boot·jar
风曦Kisaki1 天前
Kubernetes(K8s)笔记Day06: 持久化存储(emptyDir,hostPath,NFS),PV 和 PVC,StorageClass存储类
linux·运维·笔记·云原生·容器·kubernetes
Dr.kangder1 天前
嵌入式处理器仿真技术——Hypervisor 原理与实践
服务器·嵌入式硬件·架构·嵌入式
_艾伦 耶格尔.1 天前
基础I/O
linux
abbgogo1 天前
TCP/IP、OSI 与常见网络协议知识点总结
网络·网络协议·计算机网络
星蓝_starblue1 天前
零服务器、零数据库!开源growth-board,利用GitHub自动管理刷题/学习/求职全流程
服务器·数据库·程序人生·系统架构·node.js·github·改行学it
FungLeo1 天前
Flutter 接入 Alice 调试浮窗:一个顶层 final 抢跑,把 release 网络整没了
网络·flutter
fb_123451 天前
Linux磁盘分区从入门到实操:MBR_GPT全解析+分区工具实战指南
java·linux·gpt