多路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) ) 返回值: 成功: 返回客户的 通信套接字 失败:错误码 |

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

相关推荐
天空'之城1 小时前
C 语言工业级通用组件手写 03:双向链表
c语言·嵌入式开发·双向链表
xiaoye-duck1 小时前
《Linux系统编程》Linux 系统多线程(一):线程概念(从虚拟地址空间与分页机制到优缺点解析)
linux·线程
国服第二切图仔1 小时前
05-构建与特性开关
linux·服务器·ubuntu
FREEDOM_X1 小时前
嵌入式——定时器工作原理
linux·c语言·单片机·嵌入式硬件·ubuntu
2401_827501281 小时前
51单片机(四)DS18B20 数字温度传感器
c语言·51单片机
Thecozzy2 小时前
Skill和MCP和Subagent 的选择
linux·人工智能·ubuntu
Acrellea2 小时前
固态变压器(SST)热管理破局:安科瑞交直流专属测温方案护航无人值守运维
运维·网络
振浩微433射频芯片2 小时前
5个IO控制20个LED?查理复用实战拆解
网络·单片机·物联网·学习·智能家居
rcms152702692182 小时前
IS200TREAH2A 终端板
网络