【Linux网络】select{理解认识select/select与多线程多进程/认识select函数/使用select开发并发聊天服务器}

0.理解/认识

多路转接是人们在不断探索IO模型路上的一大亮点,select的设计比较早,所以有很多缺点,但仍然有很大的学习意义,简单学一下逻辑,重点学习思想:之前学的read/write接口都是既完成等待又完成拷贝即完成了IO的全过程,由于IO的效率主要由等待时长决定,故人们发明新模式:让一个人去等待一个/多个事件的就绪,当事件就绪后,再调用read/write接口,此时read/write直接可以进行读写拷贝。通过在单进程内同时监听多个事件的就绪状态。

回顾回调函数

回调函数(Callback Function)是一种特殊的函数,它作为参数传递给另一个函数(通常是异步操作或需要在特定条件下执行的函数),并在某个时刻(如操作完成、条件满足时)被调用。这种机制允许程序的结构更加模块化,同时也支持更灵活的编程范式,如事件驱动编程和异步编程。

回调函数的基本特点:

作为参数传递:一个函数可以作为参数传递给另一个函数
在特定条件下执行:回调函数的执行不是立即的,而是在满足某种条件或某个操作完成后才执行。
支持异步操作:在异步编程中,回调函数常用于处理异步操作的结果。

举例

假设我们有一个需要下载文件的功能,并且我们希望在文件下载完成后执行一些操作(比如显示下载完成的消息)。这里,我们可以使用回调函数来实现这一需求。

// 假设的下载函数,它接受一个URL和一个回调函数作为参数  
function downloadFile(url, callback) {  
    // 这里仅作为示例,实际中下载文件会更复杂  
    // 假设这是一个异步操作,我们使用setTimeout来模拟  
    setTimeout(() => {  
        console.log('文件下载完成');  
        // 调用回调函数  
        callback();  
    }, 2000); // 假设下载需要2秒  
}  
  
// 回调函数,当文件下载完成后执行  
function onDownloadComplete() {  
    console.log('下载完成的消息已显示');  
}  
  
// 调用下载函数,并传入回调函数  
downloadFile('http://example.com/file.zip', onDownloadComplete);

在这个例子中,downloadFile 函数负责模拟文件的下载过程,并在下载完成后通过调用 callback 函数来通知调用者。onDownloadComplete 函数作为回调函数被传递给 downloadFile,并在下载完成后被调用,以执行一些后续操作(如显示消息)。

当然,在C++中,回调函数的概念与在其他编程语言中类似,但实现方式可能略有不同,特别是当涉及到类和成员函数时。以下是一个简单的C++例子,展示了如何使用全局函数作为回调函数,以及如何使用类的成员函数(注意,成员函数作为回调函数时需要特别处理,因为成员函数需要一个指向对象的指针来调用)。

使用全局函数作为回调函数

首先,我们定义一个接受回调函数作为参数的函数,并在这个函数内部调用这个回调函数。

cpp
#include <iostream>  
  
// 定义一个回调函数类型  
typedef void (*Callback)();  
  
// 声明一个接受回调函数的函数  
void doSomething(Callback cb) {  
    // 执行一些操作...  
    std::cout << "Doing something..." << std::endl;  
  
    // 调用回调函数  
    cb();  
}  
  
// 一个全局函数,将作为回调函数  
void myCallback() {  
    std::cout << "Callback function called!" << std::endl;  
}  
  
int main() {  
    // 调用doSomething,传入myCallback作为回调  
    doSomething(myCallback);  
  
    return 0;  
}

使用类的成员函数作为回调函数(需要特别处理)

当使用类的成员函数作为回调时,由于成员函数需要一个对象实例来调用,因此我们不能直接将成员函数地址赋给函数指针类型的参数。一个常见的解决方案是使用std::function和std::bind(或C++11中的lambda表达式)。

cpp
#include <iostream>  
#include <functional>  
  
class MyClass {  
public:  
    void myMemberCallback() {  
        std::cout << "Member callback function called!" << std::endl;  
    }  
};  
  
// 修改doSomething以接受std::function  
void doSomething(std::function<void()> cb) {  
    std::cout << "Doing something..." << std::endl;  
    cb();  
}  
  
int main() {  
    MyClass obj;  
  
    // 使用std::bind将成员函数和对象实例绑定到一个可调用对象  
    auto boundMemberFunc = std::bind(&MyClass::myMemberCallback, &obj);  
  
    // 或者,使用lambda表达式(更简洁)  
    auto lambdaMemberFunc = [&]() { obj.myMemberCallback(); };  
  
    // 调用doSomething,传入成员函数作为回调(通过std::bind或lambda)  
    // doSomething(boundMemberFunc); // 使用std::bind  
    doSomething(lambdaMemberFunc); // 使用lambda表达式  
  
    return 0;  
}

在这个例子中,我们修改了doSomething函数,使其接受一个std::function<void()>类型的参数,这样它就可以接受任何可调用对象作为回调,包括函数指针、成员函数(通过std::bind绑定)以及lambda表达式。在main函数中,我们展示了如何使用std::bind和lambda表达式来将类的成员函数作为回调传递给doSomething函数。

select/poll+read与直接使用 read 的效率差异

非阻塞和并发处理:

使用 select/poll 可以实现非阻塞或伪阻塞的I/O操作。这意味着程序可以在等待I/O操作完成的同时执行其他任务,从而提高了程序的并发性和响应性。
直接使用 read 在没有数据可读时会阻塞程序,这可能导致程序在等待I/O操作完成时无法执行其他任务。

减少系统调用次数:

当处理多个文件描述符时,select/poll 允许程序等待多个文件描述符中的任何一个变为就绪状态,而不是对每个文件描述符单独调用 read 并可能因无数据可读而阻塞。这减少了系统调用的次数,从而可能提高性能。
如果不使用 select/poll,程序可能需要对每个文件描述符进行轮询,不断调用 read 直到有数据可读,这会导致大量的系统调用和CPU浪费。

资源利用率:

使用 select/poll 可以更有效地利用系统资源,因为它们允许程序在等待I/O操作时执行其他任务。
直接使用 read 可能会导致CPU时间被浪费在等待I/O操作上,尤其是在处理多个连接时。

注意事项

select 有文件描述符数量限制(通常是1024),而 poll 没有这个限制,但 poll 的效率在文件描述符数量很大时可能会下降。
对于大量文件描述符的情况,可以考虑使用 epoll(Linux特有),它提供了更高的效率和更好的可扩展性。
在某些情况下,使用非阻塞I/O和轮询(如使用 read 和忙等待)可能是可接受的,但这通常不是最佳实践,因为它可能导致CPU资源的高消耗。
综上所述,select/poll 相比于直接使用 read 在处理多个文件描述符时提供了更高的效率和更好的并发处理能力。然而,选择哪种方法取决于具体的应用场景和需求。

1.认识select

系统提供select函数来实现多路复用输入/输出模型.
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

select 是 Linux 系统编程中用于 I/O 多路复用的一个系统调用。它允许一个进程监视多个文件描述符(file descriptors),以等待一个或多个文件描述符变得“就绪”(ready),即可以进行 I/O 操作(如读、写或异常)。这使得单个进程能够有效地管理多个 I/O 通道,提高了程序的响应性和效率

基本用法

select 的原型定义在 <sys/select.h> 头文件中,其基本语法如下:

c
#include <sys/select.h>  
#include <sys/time.h>  
#include <unistd.h>  
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

nfds:是一个整数值,指定了被监听的文件描述符集(fd_set)中最大文件描述符值加1。这并非指 select 实际监视的文件描述符数量,而是为了效率而做的一个限制:select通过扫描内核中fd_array,遍历所有文件描述符,以此来获得fd的状态,有nfds,select在调用时就可以不把所有fd遍历完,遍历到nfds即可。
readfds:指向一个 fd_set 结构体,该结构体中包含了需要监视的、希望进行读操作的文件描述符。
writefds:指向一个 fd_set 结构体,该结构体中包含了需要监视的、希望进行写操作的文件描述符。如果不需要监视写操作,可以设为 NULL。
exceptfds:指向一个 fd_set 结构体,该结构体中包含了需要监视的、希望进行异常条件捕获的文件描述符。如果不需要监视异常条件,可以设为 NULL。
timeout:是一个指向 timeval 结构体的指针,它指定了 select 调用等待的最长时间。如果 timeout 为 NULL,select 将无限期地等待,直到一个文件描述符就绪(变为阻塞式)。如果 timeout 的时间值为零,select 将立即返回,这可以用于轮询(非阻塞轮询)。

返回值

select 返回监视的文件描述符集中就绪的文件描述符数量。如果超时,返回0。如果发生错误,返回-1,并设置 errno 以指示错误原因。

缺点

尽管 select 是 I/O 多路复用的一个有用工具,但它也有几个显著的缺点:

文件描述符数量限制:select 能够监视的文件描述符数量受限于 FD_SETSIZE,这通常是1024。对于需要处理大量连接的应用来说,这是一个严重的限制。
效率问题:select 会修改传入的 fd_set 集合,这使得在调用之间保存和恢复文件描述符集合变得复杂和低效。
数据拷贝:select 需要将文件描述符集合从用户空间拷贝到内核空间,然后再从内核空间拷贝回用户空间,这增加了开销。
因此,对于需要高性能和高并发性的应用,更现代的 I/O 多路复用机制,如 poll 和 epoll(特别是 epoll),通常是更好的选择。

select/多线程(Multi-threading)/多进程(Multi-processing)

select、多线程(Multi-threading)和多进程(Multi-processing)是编程中常用的几种并发处理机制,它们各自在不同的场景下有着广泛的应用。下面我将简要叙述这三种机制的基本概念、应用场景及优缺点。

  1. select
    select 是一种 I/O 多路复用技术,它允许单个进程同时监视多个文件描述符(file descriptors),以等待一个或多个文件描述符上的事件(如可读、可写或异常)。当其中一个文件描述符准备就绪时,select 会返回,然后进程可以处理该事件。select 最初是为 Unix 系统设计的,后来被许多其他操作系统支持。

应用场景:

服务器需要同时处理多个客户端连接时,可以使用 select 来监听多个套接字(sockets)。
当需要同时监视多个文件或设备的I/O事件时。
优缺点:

优点:实现简单,跨平台性好,能够在单个进程中处理多个I/O事件。
缺点:select 处理的文件描述符数量有限制(通常是1024),且随着监视的文件描述符数量增加,性能会显著下降(因为 select 需要遍历所有文件描述符)。
2. 多线程(Multi-threading)
多线程是指在单个进程中创建多个线程,这些线程共享进程的内存空间(包括代码段、数据段、堆等),但每个线程拥有独立的栈空间。线程是CPU调度的基本单位,它们可以并发执行,提高了程序的执行效率。

应用场景:

需要并行处理多个任务时,如同时处理多个用户请求。
需要提高程序响应速度时,如GUI程序中的事件处理。
优缺点:

优点:线程间共享内存,通信方便,切换开销较小。
缺点:线程同步和互斥控制复杂,容易引入死锁等问题;多线程编程的复杂度和出错率高于单线程编程。
3. 多进程(Multi-processing)
多进程是指操作系统中同时运行多个进程,每个进程拥有独立的内存空间和系统资源。进程间通信(IPC)需要通过操作系统提供的机制进行,如管道、消息队列、共享内存等。

应用场景:

需要高可靠性和稳定性的服务,如Web服务器。
需要隔离性较高的任务,如不同用户的请求处理。
优缺点:

优点:进程间相互独立,一个进程的崩溃不会影响其他进程;便于管理复杂的并发操作。
缺点:进程间通信开销较大,切换开销也较大;创建和销毁进程的开销比线程大。
综上所述,select、多线程和多进程各有其适用场景和优缺点,在实际应用中应根据具体需求和环境选择合适的并发处理机制。

select函数

在这里插入图片描述

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数nfds:需要监视的最大的文件描述符值+1;
rdset,wrset,exset:需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
参数timeout为结构timeval,用来设置select()的等待时间

参数timeout取值:

NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
0:仅检测描述符集合的状态,立即返回,并不等待外部事件的发生。
特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回

详细介绍

select 函数是 Linux 系统编程中用于 I/O 多路复用的一个重要系统调用。它允许程序同时监视多个文件描述符,以等待一个或多个文件描述符的状态发生变化(如可读、可写或出现异常)。以下是 select 函数参数使用的详细介绍:

select 函数原型

c
#include <sys/select.h>  
#include <sys/time.h>  
#include <unistd.h>  
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数详解

nfds
含义:指定被监听的文件描述符集合中最大文件描述符值加1。这并不是说 select 实际监视了这么多文件描述符,而是用于内核检查的有效范围的上限。
使用注意:在调用 select 前,需要确定并传递最大的文件描述符值加1,以确保所有感兴趣的文件描述符都被包含在检查范围内。
readfds
类型:指向 fd_set 的指针
含义:指向一个文件描述符集合,该集合包含了需要监视的可读文件描述符。如果不关心读事件,可以传递 NULL。
使用方式:
使用 FD_ZERO 宏初始化 fd_set 变量。
使用 FD_SET 宏将需要监视的文件描述符添加到 fd_set 中。
调用 select 后,可以通过 FD_ISSET 宏检查哪些文件描述符变得可读。
writefds
类型:指向 fd_set 的指针
含义:指向一个文件描述符集合,该集合包含了需要监视的可写文件描述符。如果不关心写事件,可以传递 NULL。
使用方式与 readfds 类似。
exceptfds
类型:指向 fd_set 的指针
含义:指向一个文件描述符集合,该集合包含了需要监视的异常文件描述符。如果不关心异常事件,可以传递 NULL。
使用方式与 readfds 类似。
timeout
类型:指向 timeval 结构体的指针
含义:指定 select 调用等待的最长时间。如果设置为 NULL,则 select 将无限期地等待,直到一个文件描述符就绪。如果设置为 0,则 select 将立即返回,这可以用于轮询。如果设置为非零值,则 select 将在指定的时间后返回,无论是否有文件描述符就绪。

timeval 结构体定义:

c
struct timeval {  
    long tv_sec;   /* 秒 */  
    long tv_usec;  /* 微秒 */  
};

在这里插入图片描述

使用注意:在调用 select 之前,需要设置 timeval 结构体的 tv_sec 和 tv_usec 成员,以指定超时时间。

返回值

成功:返回就绪的文件描述符数量。如果 readfds、writefds 或 exceptfds 中有文件描述符就绪,则返回这些集合中就绪的文件描述符总数
超时:如果指定的超时时间到达而没有任何文件描述符就绪,则返回 0。
错误:如果发生错误,则返回 -1,并设置 errno 以指示错误原因。此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。

错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足

fd_set
只想关心x的读/写:把x只设置进rfds/wfds;
既想关心x的读又想关心写:都设置
先关心读后关心写:先设置进rfds再设置进wfds。
在这里插入图片描述

这个结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符.提供了一组操作fd_set的接口, 来比较方便的操作位图.

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

理解select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。
注意:没有事件发生的fd=5被清空。

socket就绪条件

读就绪

socket内核中, 接收缓冲区中的字节数大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
监听的socket上有新的连接请求;
socket上有未处理的错误;

写就绪

socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小)大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
socket使用非阻塞connect连接成功或失败之后;
socket上有未读取的错误;

异常就绪(选学)

socket上收到带外数据. 关于带外数据, 和TCP紧急模式相关(回忆TCP协议头中, 有一个紧急指针的字段).

select的特点

可监控的文件描述符个数取决与sizeof(fd_set)的值. 有的系统sizeof(fd_set)=512,每bit表示一个文件描述符,则支持的最大文件描述符是512*8=4096.
将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,
一是用于select 返回后,array作为源数据和fd_set进行FD_ISSET判断。
二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得
fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数

select缺点

每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
select支持的文件描述符数量太小.

只检测标准输入

#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int main()
{
    fd_set read_fds;
    FD_ZERO(&read_fds);
    FD_SET(0, &read_fds);
    for (;;)
    {
        printf("> ");
        fflush(stdout);
        struct timeval timeout = {0, 0};
        // int ret = select(1, &read_fds, NULL, NULL, NULL);//阻塞式
        int ret = select(1, &read_fds, NULL, NULL, &timeout);//轮询
        if (ret < 0)
        {
            perror("select");
            continue;
        }
        if (FD_ISSET(0, &read_fds))
        {
            char buf[1024] = {0};
            read(0, buf, sizeof(buf) - 1);
            printf("input: %s", buf);
        }
        else
        {
            printf("error! invaild fd\n");
            continue;
        }
        FD_ZERO(&read_fds);
        FD_SET(0, &read_fds);
    }
    return 0;
}

select的后四个参数都是输入输出型参数即输入型:需要创建变量传入;输出型:函数执行完后会对此变量作修改,用户可以打印查看函数执行对变量带来的改变。

fd_set* read_fds

输入:内核需要关心此集合中fd们的读事件
输出时:在需要关心的fd们中有哪些fd上事件就绪了

总结

多路转接属于 IO 复用方式的一种。系统提供 select() 函数来实现多路复用输入/输出模型。select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。

select 只负责等待,而且一次可以等待多个文件描述符。nfds 表示 select 等待的多个文件描述符的最大值+1,例如需要 select 等待的 fd 有 1、2、3、4、5,那么 nfds 这个参数就是 6.

返回值如果大于0,代表有 n 个 fd 就绪了;如果返回值等于 0,代表超时,表示没有错误,也没有 fd 就绪;如果小于 0,表示等待出错。

参数 struct timeval,在 Linux 中有对应的接口可以获取时间,例如 gettimeofday() 可以获取特定时区下的特定时间。
struct timeval:其中 tv_sec 表示时间戳,以秒为单位;tv_usec 以微秒为单位。 struct timeval 表示给 select 设置等待方式,设为 struct timeval timeout = {5, 0} 表示每隔 5 秒timeout 一次,在这 5 秒期间,没有任何一个文件描述符就绪,select 就会直接返回,然后再重新进入,设置 5 秒的时候,就重复刚才的工作。如果在等待 5 秒期间有文件描述符就绪了,那么就会立即返回。设为 {0, 0} 代表立马返回,非阻塞的一种。-1 表示阻塞等待。这个参数是一个输入输出型参数。例如设置每隔 5 秒 timeout 一次,可是刚过去 2 秒就有文件描述符就绪了,此时 timeout 输出时就变成了 3 秒。
第二、三、四个参数都是同一个类型 fd_set,fd_set 是内核提供的一种数据类型,它是位图。目前关心的 fd 上面的读写事件,要么特定的 fd 上读事件就绪,要么特定的 fd 上写事件就绪,要么特定的 fd 上有异常事件。所以对于任何一个文件描述符,如果只准它关心一种事件,那么就是这三种的其中一种。如果关心特定一个 fd 上读事件就绪,就让 select 来通知我们,我们就应该把文件描述符设置进第二个参数中。如果我们关心写事件就绪,就把文件描述符设置进第三个参数中。既关心读又关心写,我们可以同时设置进第二和第三个参数中。
readfds : 它是输入时,表示的是,用户告诉内核,我给你的一个或者多个 fd,你要帮我关心 fd 上面的读事件,如果事件就绪了,你就要告诉我!当它是输出时,也就是返回时,内核告诉用户,你让我关心的多个 fd 中,有哪些已经就绪了,你赶紧读取吧!其中这个位图传入的时候,比特位的位置,就表示文件描述符编号,比特位的内容,0 或者 1,就表示是否需要内核关心! 当有 fd 就绪时,操作系统就直接修改该位图中的内容,将已经就绪的 fd 在该位图的位置不变,也就是还是 1,将没有就绪的位置清0,也就是返回输出的时候,0 还是 1,表示哪些用户关心的 fd 上面的读事件已经就绪了!所以 fd_set 是一张位图,是为了让用户和内核传递 fd 是否就绪的信息的!
注定了使用 select 的时候,会有大量的位图操作,操作系统提供了一系列的位图操作接口。fd_set 是一个位图,并且是一个具体的类型,fd_set 有具体的大小,只要有实际的大小,那么 fd_set 就一定有它位图中比特位的个数,也就是说 select 等待多个文件描述符一定是有上限的!

优点

select 是一种多路转接的方案,在单进程的同时也能处理多用户的请求。select 一次可以等待多个文件描述符。IO 等于等待+拷贝,select 可以知道多个文件描述符上的 IO 事件是否就绪,也就是把所有的等待时间重叠起来。如果有任何一个事件就绪,就可以知道这个事件就绪,然后把事件派发上来,让上层进行处理,要么是获取新连接,要么是读写数据。

缺点

select 能够等待的 fd 是有上限的
输入输出型参数比较多,数据拷贝的频率比较高
输入输出型参数比较多,每次都要对关心的 fd 进行事件重置,也就是需要大量的循环
用户层使用第三方数组管理用户的 fd,用户层需要很多次遍历;内核中检测 fd 事件就绪,也要遍历

2.select下聊天服务器

封装套接字接口

#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

enum
{
    SocketErr = 2,
    BindErr,
    ListenErr,
};

const int g_backlog = 10;

class Sock
{
private:
    int _sockfd;

public:
    Sock()
    {
    }
    ~Sock()
    {
    }

public:
    void Socket()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
            exit(SocketErr);
        }
    }

    void Bind(uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
            exit(BindErr);
        }
    }

    void Listen()
    {
        if (listen(_sockfd, g_backlog) < 0)
        {
            lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
            exit(ListenErr);
        }
    }

    int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int newfd = accept(_sockfd, (struct sockaddr *)&peer, &len);
        if (newfd < 0)
        {
            lg(Warning, "accept error, %s: %d", strerror(errno), errno);
            return -1;
        }

        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));

        *clientip = ipstr;
        *clientport = ntohs(peer.sin_port);

        return newfd;
    }

    bool Connect(const std::string &ip, const uint16_t &port)
    {
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));

        int n = connect(_sockfd, (struct sockaddr *)&peer, sizeof(peer));
        if (n == -1)
        {
            std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
            return false;
        }
        return true;
    }

    void CloseFd()
    {
        close(_sockfd);
    }

    int getSocketFd()
    {
        return _sockfd;
    }
};

Makefile

selectServer:Main.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f selectServer

主函数

#include "SelectServer.hpp"
#include <memory>

int main()
{
    //1024位图比特位个数最多 
    // std::cout <<"fd_set bits num : " << sizeof(fd_set) * 8 << std::endl;

    std::unique_ptr<SelectServer> svr(new SelectServer());
    svr->Init();
    svr->Start();

    return 0;
}

日志服务

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
private:
    int printMethod;
    std::string path;

public:
    Log()
    {
        printMethod = Screen;
        path = "./";
    }

    void Enable(int method)
    {
        printMethod = method;
    }

    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    /*
     void logmessage(int level, const char *format, ...)
        {
            time_t t = time(nullptr);
            struct tm *ctime = localtime(&t);
            char leftbuffer[SIZE];
            snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                     ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                     ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

            va_list s;
            va_start(s, format);
            char rightbuffer[SIZE];
            vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
            va_end(s);

            // 格式:默认部分+自定义部分
            char logtxt[SIZE * 2];
            snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

            // printf("%s", logtxt);
            printLog(level, logtxt);
        }
    */

   // lg(Warning, "accept error, %s: %d", strerror(errno), errno);
    void operator()(int level, const char *msg_format, ...)
    {
        time_t timestamp = time(nullptr);
        struct tm *ctime = localtime(&timestamp);
        //level 年月日
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        //自定义msg
        va_list arg_list;//存储可变参数列表信息
        va_start(arg_list, msg_format);//初始化 使其指向函数参数列表中format参数之后的第一个可变参数
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), msg_format, arg_list);
        va_end(arg_list);//清理va_list变量

        // 格式:默认部分+自定义部分
        char log_content[SIZE * 2];
        snprintf(log_content, sizeof(log_content), "%s %s", leftbuffer, rightbuffer);

        // printf("%s", logtxt); // 暂时打印
        printLog(level, log_content);
    }

    void printLog(int level, const std::string &log_content)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << log_content << std::endl;
            break;
        case Onefile:
            printOneFile(LogFile, log_content);
            break;
        case Classfile:
            printClassFile(level, log_content);
            break;
        default:
            break;
        }
    }

    void printOneFile(const std::string &log_filename, const std::string &log_content)
    {
        //path = "./"; #define LogFile "log.txt"
        std::string _logFilename = path + log_filename;
        int fd = open(_logFilename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
        if (fd < 0)
            return;
        write(fd, log_content.c_str(), log_content.size());
        close(fd);
    }

    void printClassFile(int level, const std::string &log_content)
    {
        //#define LogFile "log.txt"
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level); // "log.txt.Debug"
        printOneFile(filename, log_content);
    }

    ~Log()
    {
    }
};

Log lg;

/*
int sum(int n, ...)
{
    va_list s; // char*
    va_start(s, n);

    int sum = 0;
    while(n)
    {
        sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
        n--;
    }

    va_end(s); //s = NULL
    return sum;
}
*/

聊天服务器

#pragma once

#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"

using namespace std;

static const uint16_t defaultport = 8888;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultFd = -1;

class SelectServer
{
private:
    Sock _listenSock;
    uint16_t _port;
    int fd_array[fd_num_max]; // 用户维护的辅助数组
    // int wfd_array[fd_num_max];
public:
    SelectServer(uint16_t port = defaultport)
        : _port(port)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            fd_array[i] = defaultFd;
            // std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
        }
    }
    bool Init()
    {
        _listenSock.Socket();
        _listenSock.Bind(_port);
        _listenSock.Listen();

        return true;
    }
    void Accepter()
    {
        // 连接事件就绪 直接调用accpet获取连接套接字 之后服务器可以通过该套接字和客户端通信
        std::string clientip;
        uint16_t clientport = 0;
        int sock = _listenSock.Accept(&clientip, &clientport); // 不会阻塞在这里 因为该函数是在连接事件就绪后调用的
        if (sock < 0)
            return;
        lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);

        // ssize_t n =read(sock,buffer,1024); 不可以直接读取 因为连接事件是就绪了 但是读事件不确定
        //需要先将连接套接字加入fd_array 交给select 当select通知读事件就绪再调用read
        //将连接套接字加入fd_array前需要判断fd_array余量情况
        int pos = 1;
        for (pos = 1; pos < fd_num_max; pos++)
        {
            if (fd_array[pos] != defaultFd)
                continue;
            else
                break;
        }
        //fd_array满了
        if (pos == fd_num_max)
        {
            lg(Warning, "The fd that the server can handle is full, close %d now!", sock);
            close(sock);
        }
        else
        {
            fd_array[pos] = sock;
            PrintFd();
        }
    }
    void Recver(int fd, int pos)
    {
        // demo
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
        if (n > 0)
        {
            buffer[n] = 0;
            cout << "get a messge: " << buffer << endl;
        }
        else if (n == 0)
        {
            lg(Info, "client quit, me too, close fd is : %d", fd);
            close(fd);
            fd_array[pos] = defaultFd; // 这里本质是从select中移除
        }
        else
        {
            lg(Warning, "recv error: fd is : %d", fd);
            close(fd);
            fd_array[pos] = defaultFd; // 这里本质是从select中移除
        }
    }
    void Dispatcher(fd_set &rfds)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            int fd = fd_array[i];

            if (fd == defaultFd)
                continue;
            if (FD_ISSET(fd, &rfds))
            {
                if (fd == _listenSock.getSocketFd())
                    Accepter(); // 连接管理器
                else           
                    Recver(fd, i); // 读事件管理器
            }
        }
    }

    void Start()
    {
        int listensock = _listenSock.getSocketFd();
        fd_array[0] = listensock;
        for (;;)
        {
            // rfds: 输入输出型参数。 1111 1111 -> 0000 0000
            fd_set rfds;
            FD_ZERO(&rfds);

            // 把使用中的fd设置进rfds供select使用 并 获取最大fd值
            int maxfd = fd_array[0];
            for (int i = 0; i < fd_num_max; i++)
            {
                if (fd_array[i] == defaultFd)
                    continue;
                FD_SET(fd_array[i], &rfds);
                if (maxfd < fd_array[i])
                {
                    maxfd = fd_array[i];
                    lg(Info, "max fd update, max fd is: %d", maxfd);
                }
            }

            // 不能直接accept 需要检测并获取listensock上面的事件 新连接到来==》读事件就绪
            // accept的本质是获取listensock上的连接事件[三次握手完成后 客户端请求连接进入全连接队列 等待上层accept取走]
            // 目的是通过select检测多个fd的事件 如果主动accept就无法达成这样的目的

            // struct timeval timeout = {1, 0}; // 输入输出 要进行周期的重复设置
            struct timeval timeout = {0, 0};//null/0:阻塞等待; {0, 0}:轮询; 其余:指定时长

            // 如果事件就绪 上层不处理 select会一直通知
            // select通知事件就绪了 accept/read/write就可以直接完成对应操作 不用再等待
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
            switch (n)
            {
            case 0:
                cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
                break;
            case -1:
                cerr << "select error" << endl;
                break;
            default:
                // 有事件就绪了!
                cout << "get a new link!!!!!" << endl;
                Dispatcher(rfds); // 就绪的事件:连接到来了(调用accept) or 数据到来了(调用read/write)
                break;
            }
        }
    }

    // 显示有哪些fd在使用
    void PrintFd()
    {
        cout << "online fd list: ";
        for (int i = 0; i < fd_num_max; i++)
        {
            if (fd_array[i] == defaultFd)
                continue;
            cout << fd_array[i] << " ";
        }
        cout << endl;
    }

    ~SelectServer()
    {
        _listenSock.CloseFd();
    }
};
  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿猿收手吧!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值