C++项目实践smallchat/select

111 阅读13分钟

C/C++工程实践训练营 第02-03课

C/C++ 开发编码时的防御和习惯优于调试和生产问题排查。

项目

如何学好C/C++?

工作三年:

  • 学习C/C++基本语法

  • 学习操作系统常用的API函数以及其背后的操作系统原理

    • Windows程序设计(第五版)+ Windows核心编程
    • Linux系统编程
std::thread t(threadfunc)

windows----调用函数----CreateThread
linux-----调用函数-----pthread_create

t.join();

windows----调用函数----WaitForSingleObject
linux-----调用函数-----pthread_join

std::fstream
fopen
fread
fwrite

windows----调用函数----
CreateFile
ReadFile
WriteFile
linux-----调用函数-----
open
read
write

std::mutex
std::shared_mutex
std::condition_variable
std::semaphore

工作五年:

  • 掌握一些常见的设计模式、设计思想(包括C++常用的设计模式)
  • 整个架构(三层结构)

smallchat项目的组织结构

服务端 smallchat-server

数组越界问题

char currentDir[256];
getcwd(currentDir);

currentDir[255] = 0;  //截断
std::string strCurrentDir(currentDir);

string(const char* s,size_t n);
string(size_t n,char c);


char c = -1;
size_t len = 65;

std::string str(c, len);//构造函数问题
//A
  • 程序结构

    //初始化
    //一个while循环,在while循环不断检测包括监听fd在内的读事件,如果是监听fd,则接受连接,并将接受的fd挂到select函数,如果是普通fd,处理读事件(收数据)
    

    一个连接 - fd - client 对象

    管理所有 clients 的对象 —— struct chatState *Chat

客户端 smallchat-client

程序结构

//初始化(选择一个本机可用ip地址,并通过用户传进来的端口号建立连接)
//使用一个while循环,将connectfd和stdin-fd挂载到select函数上,判断读事件,如果是connectfd的读事件,则从服务端收取数据,如果是stdin-fd读事件,则从控制台收集数据。

好的思路:把stdin-fd也挂载到select函数上,实现了单线程无阻塞处理网络事件和控制台输入。

知识点:select函数的用法

select只是IO复用的一种方式,其他的还有:poll,epoll等。

select()函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种。

我们使用select来监视文件描述符时,要向内核传递的信息包括: 1、我们要监视的文件描述符个数 2、每个文件描述符,我们可以监视它的一种或多种状态,包括:可读,可写,发生异常三种。 3、要等待的时间,监视是一个过程,我们希望内核监视多长时间,然后返回给我们监视结果呢? 4、监视结果包括:准备好了的文件描述符个数,对于读,写,异常,分别是哪儿个文件描述符准备好了。


/* According to POSIX.1-2001 */
#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>



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


void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);


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

select函数的第一个参数

nfds:是一个整数值, 表示集合中所有文件描述符的范围,即所有文件描述符的最大值+1。在windows中不需要管这个。 待测试的描述集总是从0, 1, 2, …开始的,所以要加1。

select函数的第二三四参数

**fd_set:**一个文件描述符集合保存在fd_set变量中,可读,可写,异常这三个描述符集合需要使用三个变量来保存,分别是 readfds,writefds,exceptfds。我们可以认为一个fd_set变量是由很多个二进制构成的数组,每一位表示一个文件描述符是否需要监视。

对于fd_set类型的变量,我们只能使用相关的函数来操作。

void FD_CLR(int fd, fd_set *set);//清除某一个被监视的文件描述符。
int  FD_ISSET(int fd, fd_set *set);//测试一个文件描述符是否是集合中的一员
void FD_SET(int fd, fd_set *set);//添加一个文件描述符,将set中的某一位设置成1;
void FD_ZERO(fd_set *set);//清空集合中的文件描述符,将每一位都设置为0;

使用案例:

fd_set readfds;
int fd;
FD_ZERO(&readfds)//新定义的变量要清空一下。相当于初始化。
FD_SET(fd,&readfds);//把文件描述符fd加入到readfds中。
//select 返回
if(FD_ISSET(fd,&readset))//判断是否成功监视
{
    //dosomething
}

readfds: 监视文件描述符的一个集合,我们监视其中的文件描述符是不是可读,或者更准确的说,读取是不是不阻塞了。 writefds: 监视文件描述符的一个集合,我们监视其中的文件描述符是不是可写,或者更准确的说,写入是不是不阻塞了。 exceptfds: 用来监视发生错误异常文件

select函数的第五个参数

timeout

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

timeout表示select返回之前的时间上限。 如果timeout==NULL,无期限等待下去,这个等待可以被一个信号中断,只有当一个描述符准备好,或者捕获到一个信号时函数才会返回。如果是捕获到信号,select返回-1,并将变量errno设置成EINTR。

如果timeout->tv_sec= =0 && timeout->= =tv_sec0 ,不等待直接返回,加入的描述符都会被测试,并且返回满足要求的描述符个数,这种方法通过轮询,无阻塞地获得了多个文件描述符状态。

如果timeout->tv_sec!=0 || timeout->tv_sec!=0 ,等待指定的时间。当有描述符复合条件或者超过超时时间的话,函数返回。等待总是会被信号中断。

select函数的返回值

返回值 成功时:返回三中描述符集合中”准备好了“的文件描述符数量。 超时:返回0 错误:返回-1,并设置 errno

EBADF:集合中包含无效的文件描述符。(文件描述符已经关闭了,或者文件描述符上已经有错误了)。 EINTR:捕获到一个信号。 EINVAL:nfds是负的或者timeout中包含的值无效。 ENOMEM:无法为内部表分配内存。

  • FD_ZERO、FD_SET_FD_ISSET

    这是几个常用的宏

总结

select()可以同时监视多个描述符,如果他们没有活动,则正确地将进程置于休眠状态。Unix程序员们经常要处理多个文件描述符的I/O,他们的数据流可能是间歇性的。如果只创建read或者write会导致程序阻塞。

在我们使用select的时候,需要注意:

1、我们应该总是设置timeout=0,因为如果没有可用的数据,程序在运行时间里将无视可做。依赖超时的代码通常是不可移植,并且很难调试。

2、nfds的值一要准备且适当。

3、如果在调用完select之后,你不想检查结果,也不想做出适当的响应,那么文件描述符不需要添加到集合中。

4、select返回后,所有的文件描述符都应该被检查,看看他们是否准备好了。

5、read,recv,write,send,这几个函数不一定读/写你所请求的全部数据。如果他们读/写全部数据,是因为低流量负载和快速流。情况并非重视如此,应该处理你的函数仅管理发送或接收单个字节的情况。

6、除非你真的确信你有少量的数据要处理,否则不要一次只读一个字节,当你每次都能缓冲的时候,尽可能多的读取数据是非常低效的。

7、read,recv,write,send和select都会有返回-1的情况,并set errno的值。这些errno必须被恰当的处理。如果你的程序不会接收到任何信号,那么errno永远都不会等于EINTR,如果你的程序并不会设置非阻塞IO,那么errno就不会等于EAGAIN。

8、调用read,recv,write,send,不要使buffer的长度为0;

9、如果read,recv,write,send调用失败,并且返回的errno不是7中说的那两种情况,或者返回0,意思是“end-of-file”,这种情况下我们不应再将文件描述符传递给select。

10、每次调用select之前,timeout都用重新设置。

11、由于select()修改其文件描述符集,如果调用在循环中使用,则必须在每次调用之前重新初始化这些集。

大多数的操作系统都支持select。相比于试图用线程,进程,IPCS,信号,内存共享等方式来解决问题,select函数更有效且轻松。系统调用poll和select相似,在监视稀疏文件集合的时候更加有效。poll现在也在被广泛的使用,但没有select简便。linux专用的epoll在监视大量数据时比select和poll更加有效。

案例

案例1

下面是"man select "帮助文档中案例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    fd_set rfds;//定义一个能保存文件描述符集合的变量
    struct timeval tv;//定义超时时间
    int retval;//保存返回值

    /* Watch stdin (fd 0) to see when it has input. */
    /* 监测标准输入流(fd=0)看什么时候又输入*/
    FD_ZERO(&rfds);//初始化集合
    FD_SET(0, &rfds);//把文件描述符0加入到监测集合中。

    /* Wait up to five seconds. */
    /* 设置超时时间为5s */
    tv.tv_sec = 5;
    tv.tv_usec = 0;

    /*调用select函数,将文件描述符集合设置成读取监测 */
    retval = select(1, &rfds, NULL, NULL, &tv);
    /* Don't rely on the value of tv now! */
    /* 这时候的tv值是不可依赖的 */

    /*根据返回值类型判断select函数 */
    if (retval == -1)
        perror("select()");
    else if (retval)
        printf("Data is available now.\n");
    /* FD_ISSET(0, &rfds) will be true. */
    /* 因为值增加了一个fd,如果返回值>0,则说明fd=0在集合中。*/
    else
        printf("No data within five seconds.\n");

    exit(EXIT_SUCCESS);
}


案例2

下面是"man select_tut "帮助文档中案例:

这个例子更好的说明了select函数的作用,这是一个TCP转发相关的程序,从一个端口转发到另一个端口

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

static int forward_port;

#undef max
#define max(x,y) ((x) > (y) ? (x) : (y))

static int listen_socket(int listen_port)
{
    struct sockaddr_in a;
    int s;
    int yes;

    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        return -1;
    }
    yes = 1;
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
                   (char *) &yes, sizeof(yes)) == -1) {
        perror("setsockopt");
        close(s);
        return -1;
    }
    memset(&a, 0, sizeof(a));
    a.sin_port = htons(listen_port);
    a.sin_family = AF_INET;
    if (bind(s, (struct sockaddr *) &a, sizeof(a)) == -1) {
        perror("bind");
        close(s);
        return -1;
    }
    printf("accepting connections on port %d\n", listen_port);
    listen(s, 10);
    return s;
}

static int connect_socket(int connect_port, char *address)
{
    struct sockaddr_in a;
    int s;

    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        close(s);
        return -1;
    }

    memset(&a, 0, sizeof(a));
    a.sin_port = htons(connect_port);
    a.sin_family = AF_INET;

    if (!inet_aton(address, (struct in_addr *) &a.sin_addr.s_addr)) {
        perror("bad IP address format");
        close(s);
        return -1;
    }

    if (connect(s, (struct sockaddr *) &a, sizeof(a)) == -1) {
        perror("connect()");
        shutdown(s, SHUT_RDWR);
        close(s);
        return -1;
    }
    return s;
}

#define SHUT_FD1 do {                                \
                            if (fd1 >= 0) {                 \
                                shutdown(fd1, SHUT_RDWR);   \
                                close(fd1);                 \
                                fd1 = -1;                   \
                            }                               \
                        } while (0)

#define SHUT_FD2 do {                                \
                            if (fd2 >= 0) {                 \
                                shutdown(fd2, SHUT_RDWR);   \
                                close(fd2);                 \
                                fd2 = -1;                   \
                            }                               \
                        } while (0)

#define BUF_SIZE 1024

int main(int argc, char *argv[])
{
    int h;
    int fd1 = -1, fd2 = -1;
    char buf1[BUF_SIZE], buf2[BUF_SIZE];
    int buf1_avail, buf1_written;
    int buf2_avail, buf2_written;

    //我们希望调用主函数的时候,要指明,本地端口,发送端口,还有发送的ip地址
    if (argc != 4) {
        fprintf(stderr, "Usage\n\tfwd <listen-port> "
                "<forward-to-port> <forward-to-ip-address>\n");
        exit(EXIT_FAILURE);
    }

    // 忽略SIGPIPE这个信号,这个信号常出现在网络编程中,访问一个已经关闭的文件描述符时候出现。
    signal(SIGPIPE, SIG_IGN);

    //确定发送端口
    forward_port = atoi(argv[2]);

    //监听本地端口
    h = listen_socket(atoi(argv[1]));
    if (h == -1)
        exit(EXIT_FAILURE);

    for (;;) {
        int r, nfds = 0;
        fd_set rd, wr, er;

        FD_ZERO(&rd);
        FD_ZERO(&wr);
        FD_ZERO(&er);
        FD_SET(h, &rd);
        // 获取nfds的值。并把fd1,fd2分别加入到,可读,可写,异常监视集合中去。
        nfds = max(nfds, h);
        if (fd1 > 0 && buf1_avail < BUF_SIZE) {
            FD_SET(fd1, &rd);
            nfds = max(nfds, fd1);
        }
        if (fd2 > 0 && buf2_avail < BUF_SIZE) {
            FD_SET(fd2, &rd);
            nfds = max(nfds, fd2);
        }
        if (fd1 > 0 && buf2_avail - buf2_written > 0) {
            FD_SET(fd1, &wr);
            nfds = max(nfds, fd1);
        }
        if (fd2 > 0 && buf1_avail - buf1_written > 0) {
            FD_SET(fd2, &wr);
            nfds = max(nfds, fd2);
        }
        if (fd1 > 0) {
            FD_SET(fd1, &er);
            nfds = max(nfds, fd1);
        }
        if (fd2 > 0) {
            FD_SET(fd2, &er);
            nfds = max(nfds, fd2);
        }
        
        //开始监视
        r = select(nfds + 1, &rd, &wr, &er, NULL);

        if (r == -1 && errno == EINTR)
            continue;

        if (r == -1) {
            perror("select()");
            exit(EXIT_FAILURE);
        }

        if (FD_ISSET(h, &rd)) {
            unsigned int l;
            struct sockaddr_in client_address;

            memset(&client_address, 0, l = sizeof(client_address));
            r = accept(h, (struct sockaddr *) &client_address, &l);
            if (r == -1) {
                perror("accept()");
            } else {
                SHUT_FD1;
                SHUT_FD2;
                buf1_avail = buf1_written = 0;
                buf2_avail = buf2_written = 0;
                fd1 = r;
                fd2 = connect_socket(forward_port, argv[3]);
                if (fd2 == -1)
                    SHUT_FD1;
                else
                    printf("connect from %s\n",
                           inet_ntoa(client_address.sin_addr));
            }
        }

        /* NB: read oob data before normal reads */

        if (fd1 > 0)
            if (FD_ISSET(fd1, &er)) {
                char c;

                r = recv(fd1, &c, 1, MSG_OOB);
                if (r < 1)
                    SHUT_FD1;
                else
                    send(fd2, &c, 1, MSG_OOB);
            }
        if (fd2 > 0)
            if (FD_ISSET(fd2, &er)) {
                char c;

                r = recv(fd2, &c, 1, MSG_OOB);
                if (r < 1)
                    SHUT_FD2;
                else
                    send(fd1, &c, 1, MSG_OOB);
            }
        if (fd1 > 0)
            if (FD_ISSET(fd1, &rd)) {
                r = read(fd1, buf1 + buf1_avail,
                         BUF_SIZE - buf1_avail);
                if (r < 1)
                    SHUT_FD1;
                else
                    buf1_avail += r;
            }
        if (fd2 > 0)
            if (FD_ISSET(fd2, &rd)) {
                r = read(fd2, buf2 + buf2_avail,
                         BUF_SIZE - buf2_avail);
                if (r < 1)
                    SHUT_FD2;
                else
                    buf2_avail += r;
            }
        if (fd1 > 0)
            if (FD_ISSET(fd1, &wr)) {
                r = write(fd1, buf2 + buf2_written,
                          buf2_avail - buf2_written);
                if (r < 1)
                    SHUT_FD1;
                else
                    buf2_written += r;
            }
        if (fd2 > 0)
            if (FD_ISSET(fd2, &wr)) {
                r = write(fd2, buf1 + buf1_written,
                          buf1_avail - buf1_written);
                if (r < 1)
                    SHUT_FD2;
                else
                    buf1_written += r;
            }

        /* check if write data has caught read data */

        if (buf1_written == buf1_avail)
            buf1_written = buf1_avail = 0;
        if (buf2_written == buf2_avail)
            buf2_written = buf2_avail = 0;

        /* one side has closed the connection, keep
                  writing to the other side until empty */

        if (fd1 < 0 && buf1_avail - buf1_written == 0)
            SHUT_FD2;
        if (fd2 < 0 && buf2_avail - buf2_written == 0)
            SHUT_FD1;
    }
    exit(EXIT_SUCCESS);
}