Linux C++

1.Linux环境配置

1.安装C和C++的编译器

bash 复制代码
yum -y install gcc*	// centos7

2.升级编译器

  • 升级软件包:

    bash 复制代码
    yum -y install centos-release-scl devtoolset-8-gcc*
  • 启用软件包:

    bash 复制代码
    echo "source /opt/rh/devtoolset-8/enable" >>/etc/profile
    # 每次启动shell的时候,会执行/etc/profile脚本

    或者:

    bash 复制代码
    mv /usr/bin/gcc /usr/bin/gcc-4.8.5
    ln -s /opt/rh/devtoolset-8/root/bin/gcc /usr/bin/gcc
    mv /usr/bin/g++ /usr/bin/g++-4.8.5
    ln -s /opt/rh/devtoolset-8/root/bin/g++ /usr/bin/g++

3.安装库函数的帮助文档

bash 复制代码
yum -y install man-pages
  • 帮助文档的使用

    bash 复制代码
    man 级别 命令或者函数
    • 显示帮助的界面可以用vi的命令,q退出
    • man的级别:
      1. 用户命令
      2. 系统接口
      3. 库函数
      4. 特殊文件,比如设备文件
      5. 文件
      6. 游戏
      7. 系统的软件包
      8. 系统管理命令
      9. 内核

4.编译

bash 复制代码
gcc/g++ 选项 源代码文件1 源代码文件2 源代码文件n
  • 常用选项:
    • -o 指定输出的文件名,这个名称不能和源文件同名。如果不给出这个选项,则生成可执行文件a.out
    • -g 如果想对源代码进行调试,必须加入这个选项
    • -On 在编译、链接过程中进行优化处理,生成的可执行程序效率将更高
    • -c 只编译,不链接成为可执行文件,通常用于把源文件编译成静态库或动态库
    • -std=c++11 支持C++11标准
    • 优化选项:
      • -O0 不做任何优化,这是默认的编译选项
      • -O或者-O1 对程序做部分编译优化,对于大函数,优化编译占用稍微多的时间和相当大的内存。使用本项优化,编译器会尝试减小生成代码的尺寸,以及缩短执行时间,但并不执行需要占用大量编译时间的优化
      • -O2 这是推荐的优化等级。与O1比较而言,O2优化增加了编译时间的基础上,提高了生成代码的执行效率
      • -O3 这是最高最危险的优化等级。用这个选项会延长编译代码的时间,并且在使用gcc4.x的系统里不应全局启用。自从3.x版本以来gcc的行为已经有了极大地改变。在3.x,-O3生成的代码也只是比-O2快一点点而已,而gcc4.x中还未必更快。用-O3来编译所有的软件包将产生更大体积更耗内存的二进制文件,大大增加编译失败的机会或不可预知的程序行为(包括错误)。这样做将得不偿失,记住过犹不及。在gcc 4.x.中使用-O3是不推荐的
      • 如果使用了优化选项:
        1. 编译的时间将会更长
        2. 目标程序不可调试
        3. 有效果,但是不可能显著提升程序的性能

2.静态库和动态库

  • 在实际开发中,我们把通用的函数和类分文件编写,称之为库。在其它的程序中,可以使用库中的函数和类
  • 一般来说,通用的函数和类不提供源代码文件(安全性、商业机密),而是编译成二进制文件
  • 库的二进制文件有两种:静态库和动态库

1.静态库

  1. 制作静态库

    bash 复制代码
    g++ -c -o lib 库名.a 源代码文件清单
  2. 使用静态库

    • 不规范的做法:

      bash 复制代码
      g++ 选项 源代码文件名清单 静态库文件名
    • 规范的做法:

      bash 复制代码
      g++ 选项 源代码文件名清单 -l 库名 -L 库文件所在的目录名
  3. 静态库的概念

    • 程序在编译时会把库文件的二进制代码链接到目标程序中,这种方式称为静态链接。

      如果多个程序中用到了同一静态库中的函数或类,就会存在多份拷贝。

  4. 静态库的特点

    • 静态库的链接是在编译时期完成的,执行的时候代码加载速度快。
    • 目标程序的可执行文件比较大,浪费空间。
    • 程序的更新和发布不方便,如果某一个静态库更新了,所有使用它的程序都需要重新编译。

2.动态库

  1. 制作动态库

    bash 复制代码
    g++ -fPIC -shared -o lib 库名.so 源代码文件清单
  2. 使用动态库

    • 不规范的做法:

      bash 复制代码
      g++ 选项 源代码文件名清单 动态库文件名
    • 规范的做法:

      bash 复制代码
      g++ 选项 源代码文件名清单 -l 库名 -L 库文件所在的目录名
    • 运行可执行程序的时候,需要提前设置LD_LIBRARY_PATH环境变量。

  3. 动态库的概念

    • 程序在编译时不会把库文件的二进制代码链接到目标程序中,而是在运行时候才被载入。

      如果多个进程中用到了同一动态库中的函数或类,那么在内存中只有一份,避免了空间浪费问题。

  4. 动态库的特点

    • 程序在运行的过程中,需要用到动态库的时候才把动态库的二进制代码载入内存。
    • 可以实现进程之间的代码共享,因此动态库也称为共享库。
    • 程序升级比较简单,不需要重新编译程序,只需要更新动态库就行了。
  • 如果动态库和静态库同时存在,编译器将优先使用动态库。

3.main函数的参数

1.main函数的参数

  • main函数有三个参数,argcargvenvp,它的标准写法如下:

    c++ 复制代码
    int main(int agrc, char *argv[], char *envp[])
    {
        return 0;
    }
  • argc 存放了程序参数的个数,包括程序本身。

  • argv 字符串的数组,存放了每个参数的值,包括程序本身。

  • envp 字符串的数组,存放了环境变量,数组的最后一个元素是空。

  • 在程序中,如果不关心main()函数的参数,可以省略不写。

2.操作环境变量

c++ 复制代码
int setenv(const char *name, const char *value, int overwrite);
  • name 环境变量名。

  • value 环境变量的值。

  • overwrite 0-如果环境如果环境不存在,增加新的环境变量,如果环境变量已存在,不替换其值;非0-如果环境不存在,增加新的环境变量,如果环境变量已存在,替换其值

    返回值:0-成功;-1-失败(失败的情况极少见)

    注意:此函数设置的环境变量只对本进程有效,不会影响shell的环境变量。如果在运行程序时执行了setenv()函数,进程终止后再次运行该程序,上次的设置是无效的。

c++ 复制代码
char* getenv(const char *name);

3.示例

c++ 复制代码
#include <iostream>
#include <cstdlib>

int main(int argc, char *argv[], char *envp[]) {
    // 检查参数数量是否正确
    if (argc != 3) {
        std::cout << "Usage: ./demo <arg1> <arg2>" << std::endl;
        return -1;
    }

    // 显示命令行参数
    std::cout << "Command line arguments:" << std::endl;
    for (int i = 0; i < argc; ++i) {
        std::cout << "argv[" << i << "] = " << argv[i] << std::endl;
    }

    // 显示环境变量
    std::cout << "\nEnvironment variables:" << std::endl;
    for (int i = 0; envp[i] != nullptr; ++i) {
        std::cout << "envp[" << i << "] = " << envp[i] << std::endl;
    }

    // 设置环境变量AA
    setenv("AA", "aaaa", 1);

    // 显示环境变量AA的值
    std::cout << "\nEnvironment variable AA=" << getenv("AA") << std::endl;

    return 0;
}

4.gdb的常用命令

  • 如果程序有问题,不要问别人为什么会这样,而是立即动手调试。

1.安装gdb

bash 复制代码
yum -y install gdb

2.gdb常用命令

  • 如果希望程序可调试,编译时需要加-g选项,并且,不能使用-O的优化选项。

    bash 复制代码
    gdb 目标程序
    命令 简写 命令说明
    set args 设置程序的运行参数。例如:./demo 张三 李四 我是王五 设置参数的方法:set args 张三 李四 我是王五
    break b 设置断点,b 20 表示在第20行设置断点,可以设置多个断点。
    run r 开始运行程序,程序运行到断点的位置会停下来,如果没有遇到断点,程序一直运行下去。
    next n 执行当前语句,如果该语句为函数调用,不会进入函数内部。相当于VS的F10
    step s 执行当前语句,如果该语句为函数调用,则进入函数内部。详单与VS的F11;注意,如果函数是库函数或第三方提供的函数,用s也是进不去的,因为没有源代码,如果是自定义的函数,只要有源码就可以进去。
    print p 显示变量或表达式的值,如果p后面是表达式,会执行这个表达式。
    continue c 继续运行程序,遇到下一个断点停止,如果没有遇到断点,程序将一直运行。相当于VS的F5
    set var 设置变量的值。假设程序中定义了两个变量:int i; char name[10]; set var i = 10把i的值设置为10; set var name = "张三"。
    quit q 退出gdb
    • 注意:在gdb中,用上下光标键可以选择执行的gdb命令。

3.gdb调试core文件

  • 如果程序在运行的过程中发生了内存泄漏,会被内核强行终止,提示"段错误(吐核)",内存的状态将保存在core文件中,方便程序员进一步分析。

  • Linux缺省不会生成core文件,需要修改系统参数。

    调试core文件的步骤如下:

    1. ulimit -a查看当前用户的资源限制参数;
    2. ulimit -c unlimitedcore file size改为unlimited
    3. 运行程序,产生core文件;
    4. 运行gdb 程序名 core文件名
    5. 在gdb中,用bt查看函数调用栈。

4.gdb调试正在运行中的程序

bash 复制代码
gdb 程序名 -p 进程编号

5.Linux的时间操作

  • UNIX操作系统根据计算机产生的年代把1970年1月1日作为UNIX的纪元时间,1970年1月1日是时间的中间点,将从1970年1月1日起经过的秒数用一个整数存放。

1.time_t别名

  • time_t用于表示时间类型,它是一个long类型的别名,在<time.h>文件中定义,表示从1970年1月1日0时0分0秒到现在的秒数。

    c++ 复制代码
    typedef long time_t;

2.time()库函数

  • time()库函数用于获取操作系统的当前时间。

  • 包含头文件:<time.h>

  • 声明:

    c++ 复制代码
    time_t time(time_t *tloc);

    有两种调用方法:

    c++ 复制代码
    time_t now = time(0);	// 将空地址传递给time()函数,并将time()返回值赋给变量now

    或者:

    c++ 复制代码
    time_t now;	time(&now);	// 将变量now的地址作为参数传递给time()函数

3.tm结构体

  • time_t是一个长整数,不符合人类的使用习惯,需要转换成tm结构体,tm结构体在<time.h>中声明,如下:

    c++ 复制代码
    struct tm
    {
      int tm_sec;			/* 秒.	[0-60] */
      int tm_min;			/* 分.	[0-59] */
      int tm_hour;			/* 时.	[0-23] */
      int tm_mday;			/* 日期.		[1-31] */
      int tm_mon;			/* 月份.	[0-11] */
      int tm_year;			/* 年份	- 1900.  */
      int tm_wday;			/* 星期.	[0-6] */
      int tm_yday;			/* 从每年的1月1日开始算起的天数.[0-365]	*/
      int tm_isdst;			/* 夏令时标识符.		[-1/0/1]*/
    };

4.localtime()库函数

  • localtime()函数用于把time_t表示的时间转换为tm结构体表示的时间。

  • localtime()函数不是线程安全的,localtime_r()是线程安全的。

  • 包含头文件:<time.h>

  • 函数声明:

    c++ 复制代码
    extern struct tm *localtime (const time_t *__timer) __THROW;
    extern struct tm *localtime_r (const time_t *__restrict __timer, struct tm *__restrict __tp) __THROW;
  • 示例:

    c++ 复制代码
    #include <iostream>
    #include <time.h>
    #include <cstring>
    
    int main()
    {
        time_t now = time(0); // 获取当前时间,存放在now中。
    
        std::cout << "now = " << now << std::endl; // 显示当前时间,1970年1月1日到现在的秒数。
    
        tm tmnow;
        localtime_r(&now, &tmnow); // 把整数的时间转换成tm结构体。
    
        // 根据tm结构体拼接成习惯的字符串格式。
        std::string stime = std::to_string(tmnow.tm_year + 1900) + "-" +
                            std::to_string(tmnow.tm_mon + 1) + "-" +
                            std::to_string(tmnow.tm_mday) + " " +
                            std::to_string(tmnow.tm_hour) + ":" +
                            std::to_string(tmnow.tm_min) + ":" +
                            std::to_string(tmnow.tm_sec);
    
        std::cout << "stime = " << stime << std::endl;
    
        return 0;
    }

5.mktime()库函数

  • mktime()函数的功能与localtime()函数相反,用于把tm结构体时间转换为time_t时间。

  • 包含头文件:<time.h>

  • 函数声明:

    c++ 复制代码
    extern time_t mktime (struct tm *__tp) __THROW;
    • 该函数主要用于时间的运算,例如:把 2024-01-01 00:00:00加30分钟。

    • 思路:

      1. 解析字符串格式的时间,转换成tm结构体;
      2. mktime()函数把tm结构体转换成time_t时间;
      3. time_t时间加30*60秒;
      4. localtime_r()函数把time_t时间转换成tm结构体;
      5. tm结构体转换成字符串。
    • 示例:

      c++ 复制代码
      #include <iostream>
      #include <time.h>
      #include <cstring>
      
      int main()
      {
          // 初始时间字符串
          const char *initial_time_str = "2024-01-01 00:00:00";
      
          // 解析时间字符串
          struct tm tm_time;
          memset(&tm_time, 0, sizeof(tm_time));
          if (strptime(initial_time_str, "%Y-%m-%d %H:%M:%S", &tm_time) == nullptr)
          {
              std::cerr << "Failed to parse time string" << std::endl;
              return -1;
          }
      
          // 转换 tm 结构体到 time_t
          time_t time = mktime(&tm_time);
          if (time == -1)
          {
              std::cerr << "Failed to convert to time_t" << std::endl;
              return -1;
          }
      
          // 增加 30 分钟(1800 秒)
          time += 30 * 60;
      
          // 转换 time_t 到 tm 结构体
          struct tm new_tm_time;
          localtime_r(&time, &new_tm_time);
      
          // 转换 tm 结构体到字符串
          char new_time_str[20];
          strftime(new_time_str, sizeof(new_time_str), "%Y-%m-%d %H:%M:%S", &new_tm_time);
      
          // 输出结果
          std::cout << "Initial time: " << initial_time_str << std::endl;
          std::cout << "New time: " << new_time_str << std::endl;
      
          return 0;
      }

6.gettimeofday()库函数

  • 用于获取1970年1月1日到现在的秒和当前秒中已逝去的微秒数,可以用于程序的计时。

  • 包含头文件:<sys/time.h>

  • 函数声明:

    c++ 复制代码
    typedef struct timezone *__restrict __timezone_ptr_t;
    extern int gettimeofday (struct timeval *__restrict __tv, __timezone_ptr_t __tz) __THROW __nonnull ((1));
    
    struct timeval
    {
    	__time_t tv_sec;		/* 秒.  */
    	__suseconds_t tv_usec;	/* 微秒.  */
    };
    
    struct timezone
    {
    	int tz_minuteswest;		/* 格林威治以西几分钟.  */
    	int tz_dsttime;		/* 如果DST生效,则非零.  */
    };
  • 示例:

    c++ 复制代码
    #include <iostream>
    #include <sys/time.h>
    
    int main()
    {
        timeval start, end;
    
        gettimeofday(&start, 0); // 计时开始。
    
        for (int i = 0; i < 1000000000; i++)
            ;
    
        gettimeofday(&end, 0); // 计时结束。
    
        // 计算消耗的时长。
        timeval tv;
        tv.tv_usec = end.tv_usec - start.tv_usec;
        tv.tv_sec = end.tv_sec - start.tv_sec;
        if (tv.tv_usec < 0)
        {
            tv.tv_usec = 1000000 - tv.tv_usec;
            tv.tv_sec--;
        }
    
        std::cout << "耗时: " << tv.tv_sec << " 秒和 " << tv.tv_usec << " 微秒。" << std::endl;
    
        return 0;
    }

7.程序睡眠

  • 如果需要把程序挂起一段时间,可以使用sleep()usleep()两个库函数。

  • 包含头文件:<unistd.h>

  • 函数声明:

    c++ 复制代码
    extern unsigned int sleep (unsigned int __seconds);
    extern int usleep (__useconds_t __useconds);

6.Linux的目录操作

1.几个简单的目录操作函数

1.获取当前工作目录

  • 包含头文件:<unistd.h>

    c++ 复制代码
    extern char *getcwd (char *__buf, size_t __size) __THROW __wur;
    extern char *get_current_dir_name (void) __THROW;
  • 示例:

    c++ 复制代码
    #include <iostream>
    #include <unistd.h>
    
    int main()
    {
        char path1[256]; // linux系统目录的最大长度是255。
        getcwd(path1, 256);
        std::cout << "path1 = " << path1 << std::endl;
    
        char *path2 = get_current_dir_name();
        std::cout << "path2 = " << path2 << std::endl;
        free(path2); // 注意释放内存
    
        return 0;
    }

2.切换工作目录

  • 包含头文件:<unistd.h>

    c++ 复制代码
    extern int chdir (const char *__path) __THROW __nonnull ((1)) __wur;
  • 返回值:0-成功;其他-失败(目录不存在或没有权限)

3.创建目录

  • 包含头文件:<sys/stat.h>

    c++ 复制代码
    extern int mkdir (const char *__path, __mode_t __mode) __THROW __nonnull ((1));
  • __path:目录名

  • __mode:访问权限,如0755,不要省略前置的0

  • 返回值:0-成功;其他-失败(上级目录不存在或没有权限)

4.删除目录

  • 包含头文件:<unistd.h>

    c++ 复制代码
    extern int rmdir (const char *__path) __THROW __nonnull ((1));
  • __path:目录名

  • 返回值:0-成功;其他-失败(上级目录不存在或没有权限)

2.获取目录中文件的列表

  • 文件存放在目录中,在处理文件之前,必须先知道目录中有哪些文件,所以要获取目录中文件的列表。
  1. 包含头文件

    c++ 复制代码
    #include <dirent.h>
  2. 相关的库函数

    • 步骤一:用opendir()函数打开目录。

      c++ 复制代码
      extern DIR *opendir (const char *__name) __nonnull ((1));
    • 成功-返回目录的地址,失败-返回空地址

    • 步骤二:用readdir()函数循环的读取目录。

      c++ 复制代码
      extern struct dirent *readdir (DIR *__dirp) __nonnull ((1));
    • 成功-返回struct dirent结构体的地址,失败-返回空地址。

    • 步骤三:用closerdir()关闭目录

      c++ 复制代码
      extern int closedir (DIR *__dirp) __nonnull ((1));
  3. 数据结构

    • 目录指针:

      c++ 复制代码
      Dir *目录指针变量名;
    • 每次调用readdir(),函数返回struct dirent的地址,存放了本次读取到的内容。

      c++ 复制代码
      typdef unsigned long __ino_t;
      typdef long __off_t;
      
      struct dirent
      {
      	__ino_t d_ino;				// 索引节点号
      	__off_t d_off;				// 在目录文件中的偏移
      	unsigned short int d_reclen; // 文件名长度
      	unsigned char d_type;		// 文件类型
          char d_name[256];			// 文件名,最长255字符,不能包含<limits.h>头文件
      };
    • 重点关注结构体的d_named_type成员。

    • d_name:文件名或目录名。

    • d_type:文件的类型,有多种取值,最重要的是8和4,8-常规文件(A regular file);4-子目录(A directory),其它的暂时不关心。注意,d_name的数据类型是字符,不可直接显示。

    • 示例:

      c++ 复制代码
      #include <iostream>
      #include <dirent.h>
      
      int main(int argc, char *argv[])
      {
          if (argc != 2)
          {
              std::cout << "using ./test 目录名\n";
              return -1;
          }
      
          DIR *dir; // 定义目录指针。
      
          // 打开目录。
          if ((dir = opendir(argv[1])) == nullptr)
              return -1;
      
          // 用于存放从目录中读取到的内容。
          struct dirent *stdinfo = nullptr;
      
          while (1)
          {
              // 读取一项内容并显示出来。
              if ((stdinfo = readdir(dir)) == nullptr)
                  break;
      
              std::cout << "文件名 = " << stdinfo->d_name << " 文件类型 = " << (int)stdinfo->d_type << std::endl;
          }
      
          closedir(dir); // 关闭目录指针。
      }

7.Linux的系统错误

  • 在C++程序中,如果调用了库函数,可以通过函数的返回值判断调用是否成功。其实,还有一个整型的全局变量errno,存放了函数调用过程中产生的错误代码。

    如果调用库函数失败,可以通过errno的值来查找原因,这也是调试程序的一个重要方法。errno<errno.h>中声明。

    配合strerror()perror()两个库函数,可以查看出错的详细信息。

1.strerror()库函数

  • strerror()<string.h>中声明,用于获取错误代码对应的详细信息。

    c++ 复制代码
    extern char *strerror (int __errnum) __THROW;	// 非线程安全
    extern char *strerror_r (int __errnum, char *__buf, size_t __buflen) __THROW __nonnull ((2)) __wur;	// 线程安全
  • gcc8.3.1一共有133个错误代码

  • 示例(查看所有错误代码):

    c++ 复制代码
    #include <iostream>
    #include <cstring>
    
    int main(int argc, char *argv[])
    {
        for(int i = 0; i < 150; i++)
        {
            std::cout << i << ":" << strerror(i) << std::endl;
        }
    
        return 0;
    }

2.perror()库函数

  • perror()<stdio.h>中声明,用于在控制台显示最近一次系统错误的详细信息,在实际开发中,服务程序在后台运行,通过控制台显示错误信息意义不大。(对调试程序略有帮助)

    c++ 复制代码
    extern void perror (const char *__s);

3.注意事项

  1. 调用库函数失败不一定会设置errno

    并不是全部的库函数在调用失败时都会设置errno的值,以man手册为准(一般来说,不属于系统调用的函数不会设置errno,属于系统调用的函数才会设置errno)。

  2. errno不能作为调用库函数失败的标志

    errno的值只有在库函数调用发生错误时才会被设置,当库函数调用成功时,errno的值不会被修改,不会主动的置为0

    在实际开发中,判断函数执行是否成功还得靠函数的返回值,只有在返回值是失败的情况下,才需要关注errno的值。

8.目录和文件的更多操作

1.access()库函数

  • access()函数用于判断当前用户对目录或文件的存取权限。

  • 包含头文件:

    c++ 复制代码
    #include <unistd.h>
  • 函数声明:

    c++ 复制代码
    extern int access (const char *__name, int __type) __THROW __nonnull ((1));
  • 参数说明:

    __name:目录或文件名

    __type:需要判断的存取权限,在头文件<unistd.h>中的预定如下:

    c++ 复制代码
    /* 第二个参数要访问的值.
       这些可以放在一起.  */
    #define	R_OK	4		/* 测试读权限.  */
    #define	W_OK	2		/* 测试写权限.  */
    #define	X_OK	1		/* 测试执行权限.  */
    #define	F_OK	0		/* 是否存在.  */
  • 返回值:

    __name满足__mode权限返回0,不满足返回-1,error被设置。

    在实际开发中,access()函数主要用于判断目录或文件是否存在。

2.stat()库函数

  1. stat结构体

    c++ 复制代码
    typedef unsigned long __dev_t;
    typedef unsigned long __ino_t;
    typedef unsigned long __nlink_t;
    typedef unsigned int __mode_t;
    typedef unsigned int __uid_t;
    typedef unsigned int __gid_t;
    typedef unsigned long __dev_t;
    typedef long __blksize_t;
    typedef long __blkcnt_t;
    typedef long __time_t;
    typedef long __syscall_slong_t;
    
    struct timespec
    {
        __time_t tv_sec;		/* 秒.  */
    	__syscall_slong_t tv_nsec;	/* 纳秒.  */
    };
    
    struct stat
    {
    __dev_t st_dev;		/* 设备.  */
    __ino_t st_ino;		/* 文件序号.	*/
    __nlink_t st_nlink;		/* 链接数.  */
    __mode_t st_mode;		/* 文件模式.  */
    __uid_t st_uid;		/* 文件所有者的用户ID.	*/
    __gid_t st_gid;		/* 文件组所属组ID.*/
    int __pad0;
    __dev_t st_rdev;		/* 设备号,如果是设备.  */
    __blksize_t st_blksize;	/* I/O的最佳块大小.  */
    __blkcnt_t st_blocks;		/* 分配的512字节块. */
    struct timespec st_atim;		/* 最后一次访问时间.  */
    struct timespec st_mtim;		/* 最后一次修改时间.  */
    struct timespec st_ctim;		/* 最后一次状态更改的时间.  */
    # define st_atime st_atim.tv_sec	/* 向后兼容性.  */
    # define st_mtime st_mtim.tv_sec
    # define st_ctime st_ctim.tv_sec
    __syscall_slong_t __unused[3];
    };
    • struct stat结构体的成员变量比较多,重点关注st_modest_sizest_mtime成员。注意:st_mtime是一个整数表示的时间,需要程序员自己写代码转换格式。

    • st_mode成员的取值很多,用以下两个宏来判断:

      c++ 复制代码
      #define	__S_ISTYPE(mode, mask)	(((mode) & __S_IFMT) == (mask))
      #define	S_ISREG(mode)	 __S_ISTYPE((mode), __S_IFREG)
      #define	S_ISDIR(mode)	 __S_ISTYPE((mode), __S_IFDIR)
      
      S_ISREG(st_mode)	// 是否为普通文件,如果是,返回真
      S_ISDIR(st_mode)	// 是否为目录,如果是,返回真
  2. stat()库函数

    • 包含头文件:

      c++ 复制代码
      #include <sys/stat.h>
    • 函数声明:

      c++ 复制代码
      /* 获取file的文件属性并将它们放在BUF中.  */
      extern int stat (const char *__restrict __file, struct stat *__restrict __buf) __THROW __nonnull ((1, 2));
    • stat()函数获取__file)参数指定目录或文件的详细信息,保存到__buf结构体中。

    • 返回值:0-成功,-1-失败,errno被设置。

    • 示例:

      c++ 复制代码
      #include <iostream>
      #include <unistd.h>
      #include <cstring>
      #include <sys/stat.h>
      
      int main(int argc, char *argv[])
      {
          if (argc != 2)
          {
              std::cout << "using: ./test 文件或目录名\n";
              return -1;
          }
      
          struct stat st; // 存放目录或文件详细信息的结构体。
      
          // 获取目录或文件的详细信息
          if (stat(argv[1], &st) != 0)
          {
              std::cout << "stat(" << argv[1] << "):" << strerror(errno) << std::endl;
              return -1;
          }
      
          if (S_ISREG(st.st_mode))
              std::cout << argv[1] << " 是一个文件(" << "mtime = " << st.st_mtime << ", size = " << st.st_size << ")\n";
          if (S_ISDIR(st.st_mode))
              std::cout << argv[1] << " 是一个目录(" << "mtime = " << st.st_mtime << ", size = " << st.st_size << ")\n";
      
          return 0;
      }

3.utime()库函数

  • utime()函数用于修改目录或文件的时间。

  • 包含头文件:

    c++ 复制代码
    #include <sys/types.h>
    #include <utime.h>
  • 函数声明:

    c++ 复制代码
    /* 将FILE的访问和修改次数设置为中给出的次数*FILE_TIMES。
       如果FILE_TIMES为NULL,则设置为当前时间.  */
    extern int utime (const char *__file, const struct utimbuf *__file_times) __THROW __nonnull ((1));
    • utime()函数用来修改参数__filest_atimest_time。如果参数__file_times为空地址,则设置为当前时间。结构utimbuf声明如下:

      c++ 复制代码
      typedef long __time_t;
      
      /* 描述文件时间的结构.  */
      struct utimbuf
      {
      	__time_t actime;		/* 访问时间.  */
      	__time_t modtime;		/* 修改时间.  */
      };
  • 返回值:0-成功,-1-失败,errno被设置。

4.rename()库函数

  • rename()函数用于重命名目录或文件,相当于操作系统的mv命令。

  • 包含头文件:

    c++ 复制代码
    #include <stdio.h>
  • 函数声明:

    c++ 复制代码
    extern int rename (const char *__old, const char *__new) __THROW;
  • 参数说明:

    __old:源目录或文件名。

    __new:目标目录或文件名。

    返回值:0-成功,-1-失败,errno被设置。

5.remove()库函数

  • remove()函数用于删除目录或文件,相当于操作系统的rm命令。

  • 包含头文件:

    c++ 复制代码
    #include <stdio.h>
  • 函数声明:

    c++ 复制代码
    /* 删除目录/文件.  */
    extern int remove (const char *__filename) __THROW;
  • 参数说明:

    __filename待删除的目录或文件名。

    返回值:0-成功,-1-失败,errno被设置。

9.Linux的信号

1.信号的基本概念

  • 信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是,不能给进程传递任何数据。

  • 信号产生的原因有很多,在shell中,可以用killkillall命令发送信号:

    bash 复制代码
    kill -信号的类型 进程编号
    killall -信号的类型 进程名
  • 查看系统定义的信号列表:

    bash 复制代码
    kill -l

2.信号的类型

信号名 信号值 默认处理动作 发出信号的原因
SIGHUP 1 A 终端挂起或者控制进程终止
SIGINT 2 A 键盘终端 ctrl+c
SIGQUIT 3 C 键盘的退出键按下
SIGILL 4 C 非法指令
SIGTRAP 5 C 跟踪断点
SIGABRT 6 C 由abort(3)发出的退出指令
SIGBUS 7 C 总线错误(例如内存对齐错误)
SIGFPE 8 C 浮点异常
SIGKILL 9 AEF 采用 kill -9 进程编号 强制杀死程序
SIGUSR1 10 A 用户自定义信号 1
SIGSEGV 11 CEF 无效的内存引用(数组越界、操作空指针和野指针等)
SIGUSR2 12 A 用户自定义信号 2
SIGPIPE 13 A 管道破裂,写一个没有读端口的管道
SIGALRM 14 A 由闹钟alarm()函数发出的信号
SIGTERM 15 A 采用 kill 进程编号 或 killall 程序名 通知程序
SIGSTKFLT 16 A 栈故障(不常被使用)
SIGCHLD 17 B 子进程结束信号
SIGCOUT 18 C 进程继续(曾被停止的进程)
SIGSTOP 19 DEF 终止进程
SIGSTP 20 D 控制终端(tty)上按下停止键
SIGTTIN 21 D 后台进程企图从控制终端读
SIGTTOU 22 D 后台进程企图从控制终端写
SIGURG 23 B 套接字上有紧急数据到达
SIGXCPU 24 C 超过CPU时间限制
SIGXFSZ 25 C 超过文件大小限制
SIGVTALRM 26 A 虚拟时钟信号,由setitimer()产生
SIGPROF 27 A 统计时钟信号,由setitimer()产生
SIGWINCH 28 B 终端窗口大小改变
SIGIO 29 B 文件描述符上可以进行I/O操作
SIGPWR 30 A 电源故障(不常被使用)
SIGSYS 31 C 非法系统调用
SIGRTMIN 34 A 实时信号,用户自定义
SIGRTMAX 64 A 实时信号,用户自定义
其它 <=64 A 自定义信号
  • 默认处理动作:
    1. A (Abort): 终止进程。
    2. B (Ignore): 忽略信号,将该信号丢弃,不做处理。
    3. C (Core): 产生核心转储文件(内核映像转储core dump), 终止进程。
    4. D (Stop): 停止进程,进入停止状态的程序还能重新继续,一般是在调试的过程中。
    5. E (Continue): 信号不能被捕获,继续执行进程。
    6. F (Force): 信号不能被忽略,强制终止进程。

3.信号的处理

  • 进程对信号的处理方法有三种:

    1. 对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程。
    2. 设置信号的处理函数,收到信号后,由该函数来处理。
    3. 忽略某个信号,对该信号不做任何处理,就像未发生过一样。
  • signal()函数可以设置程序对信号的处理方式。

  • 包含头文件:

    c++ 复制代码
    #include <signal.h>
  • 函数声明:

    c++ 复制代码
    typedef void (*__sighandler_t)(int);
    
    extern __sighandler_t signal (int __sig, __sighandler_t __handler) __THROW;
  • 参数说明:

    __sig:信号的编号(信号的值)。

    __handler:信号的处理方式,有三种情况:

    1. SIG_DFL:恢复参数__sig信号的处理方法为默认行为。
    2. 一个自定义的处理信号的函数,函数的形参是信号的编号。
    3. SIG_IGN:忽略参数__sig所指的信号。

4.信号的作用

  • 服务程序运行在后台,如果想让中止它,杀掉不是个好办法,因为进程被杀的时候,是突然死亡,没有安排善后工作。
  • 如果向服务程序发送一个信号,服务程序收到信号后,调用一个函数,在函数中编写善后的代码,程序就可以有计划的退出。
  • 如果向服务程序发送0的信号,可以检测程序是否存活。

5.信号的应用示例

c++ 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void EXIT(int sig)
{
    std::cout << "收到了信号:" << sig << std::endl;
    std::cout << "正在释放资源,程序将退出......\n";

    // 以下是释放资源的代码。
    std::cout << "程序退出。\n";
    exit(0); // 进程退出。
}

int main(int argc, char *argv[])
{
    // 忽略全部的信号,防止程序被信号异常中止。
    for (int ii = 1; ii <= 64; ii++)
        signal(ii, SIG_IGN);

    // 如果收到 2 和 15 的信号(ctrl+c 和 kill、killall),本程序将主动退出。
    signal(2, EXIT);
    signal(15, EXIT);

    while (true)
    {
        std::cout << "执行了一次任务。\n";
        sleep(1);
    }

    return 0;
}

6.发送信号

  • Linux操作系统提供了killkillall命令向进程发送信号,在程序中,可以用kill()函数向其它进程发送信号。

  • 函数声明:

    c++ 复制代码
    extern int kill (__pid_t __pid, int __sig) __THROW;
  • kill()函数将参数__sig指定的信号给参数__pid指定的进程。

  • 参数__pid有几种情况:

    1. __pid > 0 将信号传给进程号为__pid的进程。
    2. __pid = 0将信号传给和当前进程相同进程组的所有进程,常用于父进程给子进程发送信号,注意,发送信号者进程也会收到自己发出的信号。
    3. __pid = -1将信号广播传送给系统内所有的进程,例如系统关机时,会向所有的登录窗口广播关机信息。
  • __sig:准备发送的信号代码,假如其值为0则没有任何信号送出,但是系统会执行错误检查,通常会利用__sig值为零来检验某个进程是否仍在运行。

  • 返回值说明:成功执行时,返回0;失败返回-1,errno被设置。

10.进程终止

  • 8种方式可以中止进程,其中5种为正常终止,它们是:
    1. main()函数用return返回;
    2. 在任意函数中调用exit()函数;
    3. 在任意函数中调用_exit()_Exit()函数;
    4. 最后一个线程从其启动例程(线程主函数)用return返回;
    5. 在最后一个线程中调用pthread_exit()返回。
  • 异常终止有3种方式,它们是:
    1. 调用abort()函数中止;
    2. 接收到一个信号;
    3. 最后一个线程对取消请求做出响应。

1.进程终止的状态

  • main()函数中,return的返回值即终止状态,如果没有return语句或调用exit(),那么该进程的终止状态是0

  • shell中,查看进程终止的状态:

    bash 复制代码
    echo $?
  • 正常终止进程的3个函数(exit()_Exit()是由ISO C说明的,_exit()是由POSIX说明的)。

    c++ 复制代码
    extern void exit (int __status) __THROW __attribute__ ((__noreturn__));		// <stdlib.h>
    extern void _exit (int __status) __attribute__ ((__noreturn__));			// <unistd.h>
    extern void _Exit (int __status) __THROW __attribute__ ((__noreturn__));	// <stdlib.h>
  • 参数说明:

    __status也是进程终止的状态。

    如果进程被异常终止,终止状态为非0, 它们在服务程序的调度、日志和监控中常被用到。

2.资源释放的问题

  • return表示函数返回,会调用局部对象的析构函数,main()函数中的return还会调用全局对象的析构函数。
  • exit()表示终止进程,不会调用局部对象的析构函数,只调用全局对象的析构函数。
  • exit()会执行清理工作,然后退出,_exit()_Exit()直接退出,不会执行任何清理工作。

3.进程的终止函数

  • 进程可以用atexit()函数登记终止函数(最多32个),这些函数将由exit()自动调用。

  • 包含头文件:

    c++ 复制代码
    #include <stdlib.h>
  • 函数声明:

    c++ 复制代码
    /* 注册一个在调用 'exit' 时调用的函数.  */
    extern int atexit (void (*__func) (void)) __THROW __nonnull ((1));
  • exit()调用终止函数的顺序与登记时相反。

  • 使用atexit()注册一个进程终止的清理函数,用于使用exit()终止进程后自动调用清理函数。

11.调用可执行程序

  • Linux提供了system()函数和exec函数族,在C++程序中,可以执行其它的程序(二进制文件、操作系统命令或shell脚本)。

1.system()函数

  • system()函数提供了一种简单的执行程序的方法,把需要执行的程序和参数用一个字符串传给system()函数就行了。

  • 函数声明:

    c++ 复制代码
    extern int system (const char *__command) __wur;
  • system()函数的返回值比较麻烦。

    1. 如果执行的程序不存在,system()函数返回非0
    2. 如果执行程序成功,并且被执行的程序终止状态是0system()函数返回0
    3. 如果执行程序成功,并且被执行的程序终止状态不是0system()函数返回非0

2.exec函数族

  • exec函数族提供了另一种在进程中调用程序(二进制文件或shell脚本)的方法。

  • 包含头文件:

    c++ 复制代码
    #include <unistd.h>
  • exec函数族的声明如下:

    c++ 复制代码
    /* 使用从 `environ` 获取的环境变量,执行 PATH 所指向的文件,并将 PATH 之后的所有参数传递给它,直到遇到一个空指针。 */
    extern int execl (const char *__path, const char *__arg, ...) __THROW __nonnull ((1, 2));
    
    /* 执行 FILE 所指向的文件,如果 FILE 不包含斜杠(/),则在 `PATH` 环境变量中搜索 FILE,并将 FILE 之后的所有参数传递给它,直到遇到一个空指针,同时使用 `environ` 中的环境变量。 */
    extern int execlp (const char *__file, const char *__arg, ...) __THROW __nonnull ((1, 2));
    
    /* 使用从 `environ` 获取的环境变量,执行 PATH 所指向的文件,并将 PATH 之后的所有参数传递给它,直到遇到一个空指针,之后的参数为环境变量。 */
    extern int execle (const char *__path, const char *__arg, ...) __THROW __nonnull ((1, 2));
    
    /* 使用从 `environ` 获取的环境变量,执行 PATH 所指向的文件,并将 ARGV 中的参数传递给它。 */
    extern int execv (const char *__path, char *const __argv[]) __THROW __nonnull ((1, 2));
    
    /* 执行 FILE 所指向的文件,如果 FILE 不包含斜杠(/),则在 `PATH` 环境变量中搜索 FILE,并将 ARGV 中的参数传递给它,同时使用 `environ` 中的环境变量。 */
    extern int execvp (const char *__file, char *const __argv[]) __THROW __nonnull ((1, 2));
    
    /* 执行 FILE 所指向的文件,如果 FILE 不包含斜杠(/),则在 `PATH` 环境变量中搜索 FILE,并将 ARGV 中的参数传递给它,同时使用 __envp 中的环境变量。 */
    extern int execvpe (const char *__file, char *const __argv[], char *const __envp[]) __THROW __nonnull ((1, 2));
  • 注意:

    1. 如果执行程序失败则直接返回-1,失败原因存于errno中;
    2. 新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈;
    3. 如果执行成功则函数不会返回,当在主程序中成功调用exec后,被调用的程序将取代调用者程序,也就是说,exec函数之后的代码都不会被执行;
    4. 在实际开发中,最常用的是execl()execv(),其它的极少使用。
  • 示例:

    c++ 复制代码
    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
        int ret = execl("/bin/ls", "/bin/ls", "-lt", "/tmp", nullptr); // 最后一个参数 nullptr 不能省略。
        std::cout << "ret = " << ret << std::endl;
        perror("execl");
    
        /*
        char *args[10];
        args[0] = strdup("/bin/ls");
        args[1] = strdup("-lt");
        args[2] = strdup("/tmp");
        args[3] = nullptr;
    
        int ret = execv("/bin/ls", args);
        std::cout << "ret = " << ret << std::endl;
        perror("execv");
    
        // 释放动态分配的内存
        for (int i = 0; args[i] != nullptr; ++i)
        {
            free(args[i]);
        }
        */
    
        return 0;
    }

12.创建进程

1.Linux的0、1和2号进程

  • 整个Liunx系统的全部进程是一个树形结构。

    1. 0号进程(系统进程)是所有进程的祖先,它创建了1号和2号进程;
    2. 1号进程(systemd)负责执行内核的初始化工作和进行系统配置;
    3. 2号进程(kthreadd)负责所有内核线程的调度和管理。
  • pstree命令可以查看进程树(yum -y install psmisc)

    bash 复制代码
    pstree -p 进程编号

2.进程标识

  • 每个进程都有一个非负整数表示的唯一的进程ID,虽然是唯一的,但是进程ID可以复用。当一个进程终止后,其进程ID就成了复用的候选者。Linux采用延迟复用算法,让新建进程的ID不同于最近终止的进程所使用的ID。这样防止了新进程被误认为是使用了同一ID的某个已终止的进程。

  • 包含头文件:

    c++ 复制代码
    #include <sys/types.h>
    #include <unistd.h>
  • 函数声明:

    c++ 复制代码
    typedef int __pid_t;
    
    /* 获取调用进程的进程ID.  */
    extern __pid_t getpid (void) __THROW;
    
    /* 获取调用进程的父进程的进程ID.  */
    extern __pid_t getppid (void) __THROW;

3.fork()函数

  • 一个现有的进程可以调用fork()函数创建一个新的进程。

  • 包含头文件:

    c++ 复制代码
    #include <unistd.h>
  • 函数声明:

    c++ 复制代码
    typedef int __pid_t;
    
    /* 克隆调用进程,创建一个精确的副本.
       错误返回-1, 新进程返回0,
       并将新进程的进程ID赋给旧进程.  */
    extern __pid_t fork (void) __THROWNL;
  • fork()创建的新进程被称为子进程。子进程是父进程的副本,父进程和子进程都从调用fork()之后的代码开始执行。

  • fork()函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是子进程进程ID

  • 子进程获得了父进程数据空间、堆和栈的副本(注意:子进程拥有的是副本,不是和父进程共享)。

  • fork()之后,父进程和子进程的执行顺序是不确定的

  • 示例:

    c++ 复制代码
    #include <iostream>
    #include <unistd.h>
    
    int main()
    {
        int num = 0;
        std::string message = "初始化信息.";
    
        pid_t pid = fork();
    
        if (pid > 0)
        { // 父进程将执行这段代码。
            sleep(1);
            std::cout << "父进程pid: " << pid << std::endl;
            std::cout << "父进程num: " << num << ", msg: " << message << std::endl;
        }
        else
        { // 子进程将执行这段代码。
            num = 1;
            message = "子进程修改后的信息.";
            std::cout << "子进程pid: " << pid << std::endl;
            std::cout << "子进程num: " << num << ", msg: " << message << std::endl;
        }
    
        return 0;
    }

4.fork()的两种做法

  1. 父进程复制自己,然后父进程和子进程分别执行不同的代码。这种用法在网络服务程序中很常见,父进程等待客户端的连接请求,当请求到达时,父进程调用fork(),让子进程处理些请求,而父进程则继续等待下一个连接请求。
  2. 进程要执行另一个程序。这种用法在shell中很常见,子进程从fork()返回后立即调用exec
  • 示例:

    c++ 复制代码
    #include <iostream>
    #include <unistd.h>
    
    int main()
    {
        if (fork() > 0)
        { // 父进程将执行这段代码。
            while (true)
            {
                sleep(1);
                std::cout << "父进程运行中..." << std::endl;
            }
        }
        else
        { // 子进程将执行这段代码。
            sleep(10);
            std::cout << "子进程开始执行任务..." << std::endl;
            execl("/bin/ls", "/bin/ls", "-lt", "/tmp", 0);
            std::cout << "子进程执行任务结束,退出." << std::endl;
        }
    
        return 0;
    }

5.共享文件

  • fork()的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程和子进程共享同一个文件偏移量。

  • 如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么它们的输出可能会相互混合。

  • 示例:

    c++ 复制代码
    #include <iostream>
    #include <unistd.h>
    #include <fstream>
    
    int main()
    {
        std::ofstream fout;
        fout.open("/tmp/tmp.txt"); // 打开文件。
        fork();
        for (int i = 0; i < 10000000; i++) // 向文件中写入一千万行数据。
        {
            fout << "进程: " << getpid() << ", i = " << i << std::endl; // 写入的内容无所谓。
        }
    
        fout.close(); // 关闭文件。
    
        return 0;
    }

6.vfork()函数

  • vfork()函数的调用和返回值与fork()相同,但两者的语义不同。

  • vfork()函数用于创建一个新进程,而该新进程的目的是exec一个新程序,它不复制父进程的地址空间,因为子进程会立即调用exec,于是也就不会使用父进程的地址空间。如果子进程使用了父进程的地址空间,可能会带来未知的结果。

  • vfork()fork()的另一个区别是:vfork()保证子进程先运行,在子进程调用execexit()之后父进程才恢复运行。

  • 示例:

    c++ 复制代码
    #include <iostream>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main()
    {
        int x = 0;
        pid_t pid;
    
        pid = vfork();
    
        if (pid < 0)
        {
            std::cerr << "vfork()失败." << std::endl;
            return 1;
        }
        else if (pid == 0)
        {
            // 子进程
            std::cout << "子进程: x = " << x << std::endl;
            x = 1;    // 修改子进程中的变量 x
            sleep(3); // 子进程执行完毕后休息三秒再退出
            _exit(0); // 使用 _exit() 退出,避免在子进程中执行父进程的全局析构函数等
        }
        else
        {
            // 父进程
            // 等待子进程结束
            waitpid(pid, nullptr, 0);
            std::cout << "父进程: x = " << x << std::endl;
        }
    
        return 0;
    }

13.僵尸进程

  • 如果父进程子进程先退出,子进程将被1号进程托管(这也是一种让程序在后台运行的方法)。

  • 如果子进程父进程先退出,而父进程没有处理子进程退出的信息,那么,子进程将成为僵尸进程

  • 僵尸进程有什么危害?内核为每个子进程保留了一个数据结构,包括进程编号终止状态使用CPU时间等。父进程如果处理了子进程退出的信息,内核就会释放这个数据结构,父进程如果没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程的进程编号将一直被占用。系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因为没有可用的进程编号而导致系统不能产生新的进程。

  • 僵尸进程的避免:

    1. 子进程退出的时候,内核会向父进程发头SIGCHLD信号,如果父进程signal(SIGCHLD, SIG_IGN)通知内核,表示自己对子进程的退出不感兴趣,那么子进程退出后会立即释放数据结构。

    2. 父进程通过wait()/waitpid()等函数等待子进程结束,在子进程退出之前,父进程将被阻塞待。

      • 包含头文件:

        c++ 复制代码
        #include <sys/types.h>
        #include <sys/wait.h>
      • 函数声明:

        c++ 复制代码
        #define __WAIT_STATUS	void *
        typedef int __pid_t;
        // 结构体 struct rusage 在 <sys/resource.h> 内定义
        
        /* 等待一个子进程消亡. 如果有,将其状态放在 *STAT_LOC 中
           并返回其进程ID.  对于错误, 返回 (pid_t) -1.
        
           这个函数是一个消去点因此没有标记 __THROW.  */
        extern __pid_t wait (__WAIT_STATUS __stat_loc);
        
        /* 等待匹配PID的子进程消亡.
           当 PID 大于 0 时, 匹配进程号为PID的进程.
           如果 PID 为 (pid_t) -1, 匹配任何进程.
           如果 PID 为 (pid_t) 0, 则匹配任何进程与当前进程相同的进程组.
           如果 PID 小于 -1 , 匹配任何进程 进程组为PID的绝对值.
           如果在 OPTIONS 中设置了 WNOHANG 位, 则该子节点还没有死, 返回 (pid_t) 0.  
           如果成功, 返回PID并将死亡子进程的状态存储在STAT_LOC中.
           错误时返回 (pid_t) -1.  
           如果 wuntracked 位是在 OPTIONS 中设置, 停止子进程返回状态; 否则不.
        
           这个函数是一个消去点因此没有标记 __THROW.  */
        extern __pid_t waitpid (__pid_t __pid, int *__stat_loc, int __options);
        
        /* 等待子进程退出.  如果有, 将其状态放入 *STAT_LOC 和返回其进程ID.
           如果出现错误返回 (pid_t) -1.
           如果 USAGE 不是 Nil , 存储关于子进程资源使用情况的信息.
           如果在 OPTIONS 中设置了 untrace 位, 停止子进程返回状态; 否则不.  */
        extern __pid_t wait3 (__WAIT_STATUS __stat_loc, int __options,
        		      struct rusage * __usage) __THROWNL;
        
        /* PID 类似于 waitpid.  其他参数如 wait3.  */
        extern __pid_t wait4 (__pid_t __pid, __WAIT_STATUS __stat_loc, int __options,
        		      struct rusage *__usage) __THROWNL;
        
        # define WIFEXITED(status)	__WIFEXITED (__WAIT_INT (status))
        # define WTERMSIG(status)	__WTERMSIG (__WAIT_INT (status))
      • 返回值是子进程的编号。

      • __stat_loc:子进程终止的信息:

        1. 如果是正常终止,宏WIFEXITED(status)返回真,宏WEXITSTATUS(stat_loc)可获取终止状态;
        2. 如果是异常终止,宏WTERMSIG(status)可获取终止进程的信号;
        3. 如果父进程很忙,可以捕获SIGCHLD信号,在信号处理函数中调用wait()/waitpid()
      • 示例一:

        c++ 复制代码
        #include <iostream>
        #include <unistd.h>
        #include <sys/types.h>
        #include <sys/wait.h>
        
        int main()
        {
            // 创建子进程
            if (fork() > 0)
            { // 父进程的流程。
                int sts;
                pid_t pid = wait(&sts);
        
                // 输出已终止的子进程编号
                std::cout << "已终止的子进程编号是: " << pid << std::endl;
        
                // 判断子进程是否正常退出,并输出退出状态
                if (WIFEXITED(sts))
                {
                    std::cout << "子进程是正常退出的,退出状态是:" << WEXITSTATUS(sts) << std::endl;
                }
                else
                {
                    std::cout << "子进程是异常退出的,终止它的信号是:" << WTERMSIG(sts) << std::endl;
                }
            }
            else
            { // 子进程的流程。
        
                // sleep(100);
                /* 如果取消注释 sleep(100),即使子进程出现段错误并退出,
                父进程也会在等待期间一直阻塞,直到子进程结束或异常退出,或者等待时间达到 100 秒
                在这段时间内,父进程会一直等待子进程的退出状态,不会继续执行下面的代码。
                这意味着你可能会在程序中看到一段时间的停滞,直到子进程的退出状态可用或等待超时. */
        
                // 这段代码首先对一个空指针解引用,会导致段错误,然后调用 exit() 函数退出,并指定退出状态为 1
                int *p = 0;
                *p = 10;
                exit(1);
            }
        
            return 0;
        }
      • 示例二:

        c++ 复制代码
        #include <iostream>
        #include <unistd.h>
        #include <sys/types.h>
        #include <sys/wait.h>
        
        void func(int sig) // 子进程退出的信号处理函数。
        {
            int sts;
            pid_t pid = wait(&sts);
            std::cout << "已终止的子进程编号是: " << pid << std::endl;
        
            if (WIFEXITED(sts))
            {
                std::cout << "子进程是正常退出的,退出状态是: " << WEXITSTATUS(sts) << std::endl;
            }
            else
            {
                std::cout << "子进程是异常退出的,终止它的信号是: " << WTERMSIG(sts) << std::endl;
            }
        }
        
        int main()
        {
            signal(SIGCHLD, func); // 捕获子进程退出的信号。
        
            if (fork() > 0)
            { // 父进程的流程。
                while (true)
                {
                    std::cout << "父进程正在执行任务." << std::endl;
                    sleep(1);
                }
            }
            else
            { // 子进程的流程。
                sleep(5);
                // int *p = nullptr; *p=10;
                exit(1);
            }
        
            return 0;
        }
        
        /*执行流程如下:
        1. 父进程 fork 出子进程后,进入 while 循环,不断输出 "父进程正在执行任务." 的消息。
        2. 子进程执行 sleep(5) 或对空指针解引用导致段错误,然后退出。
            2.1. 如果子进程发生段错误:
                2.1.1. 子进程异常退出时,操作系统发送 SIGCHLD 信号给父进程。
                2.1.2. 父进程捕获到 SIGCHLD 信号,调用 func 函数处理。
                2.1.3. 在 func 函数中,父进程调用 wait 函数等待子进程的退出状态。
                2.1.4. 父进程输出已终止的子进程编号和子进程的退出状态,由于子进程是异常退出的,所以输出 "子进程是异常退出的,终止它的信号是: " 和相应的信号值。
            2.2. 如果子进程没有发生段错误:
                2.2.1. 子进程正常退出时,操作系统发送 SIGCHLD 信号给父进程。
                2.2.2. 父进程捕获到 SIGCHLD 信号,调用 func 函数处理。
                2.2.3. 在 func 函数中,父进程调用 wait 函数等待子进程的退出状态。
                2.2.4. 父进程输出已终止的子进程编号和子进程的退出状态,由于子进程是正常退出的,所以输出 "子进程是正常退出的,退出状态是: " 和相应的退出状态值。 */

14.多线程和信号

  • 在多进程的服务程序中,如果子进程收到退出信号,子进程自行退出,如果父进程收到退出信号,则应该先向全部的子进程发送退出信号,然后自己再退出。

  • 示例:

    c++ 复制代码
    #include <iostream>
    #include <unistd.h>
    #include <signal.h>
    
    void FatherEXIT(int sig); // 父进程的信号处理函数。
    void ChildEXIT(int sig);  // 子进程的信号处理函数。
    
    int main()
    {
        // 忽略全部的信号,不希望被打扰。
        for (int i = 1; i <= 64; i++)
            signal(ii, SIG_IGN);
    
        // 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程
        // 但请不要用 "kill -9 +进程号" 强行终止
        signal(SIGTERM, FatherEXIT);
        signal(SIGINT, FatherEXIT); // SIGTERM 15 SIGINT 2
    
        while (true)
        {
            if (fork() > 0) // 父进程的流程。
            {
                sleep(5);
                continue;
            }
            else // 子进程的流程。
            {
                // 子进程需要重新设置信号。
                signal(SIGTERM, ChildEXIT); // 子进程的退出函数与父进程不一样。
                signal(SIGINT, SIG_IGN);   // 子进程不需要捕获SIGINT信号。
    
                while (true)
                {
                    std::cout << "子进程: " << getpid() << " 正在运行中." << std::endl;
                    sleep(3);
                    continue;
                }
            }
        }
    
        return 0;
    }
    
    // 父进程的信号处理函数。
    void FatherEXIT(int sig)
    {
        // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
        signal(SIGINT, SIG_IGN);
        signal(SIGTERM, SIG_IGN);
    
        std::cout << "父进程退出, sig = " << sig << std::endl;
    
        kill(0, SIGTERM); // 向全部的子进程发送15的信号,通知它们退出。
    
        // 在这里增加释放资源的代码(全局的资源)。
    
        exit(0);
    }
    
    // 子进程的信号处理函数。
    void ChildEXIT(int sig)
    {
        // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
        signal(SIGINT, SIG_IGN);
        signal(SIGTERM, SIG_IGN);
    
        std::cout << "子进程: " << getpid() << "退出, sig = " << sig << std::endl;
    
        // 在这里增加释放资源的代码(只释放子进程的资源)。
    
        exit(0);
    }

15.共享内存

  • 多线程共享进程的地址空间,如果多个线程需要访问同一块内存,用全局变量就可以了。
  • 在多进程中,每个进程的地址空间是独立的,不共享的,如果多个进程需要访问同一块内存,不能用全局变量,只能用共享内存。
  • 共享内存(Shared Memory)允许多个进程(不要求进程之间有血缘关系)访问同一块内存空间,是多个进程之间共享和传递数据最高效的方式。进程可以将共享内存连接到它们自己的地址空间中,如果某个进程修改了共享内存中的数据,其它的进程读到的数据也会改变。
  • 共享内存没有提供锁机制,也就是说,在某一个进程对共享内存进行读/写的时候,不会阻止其它进程对它的读/写。如果要对共享内存的读/写加锁,可以使用信号量。
  • Linux中提供了一组函数用于操作共享内存。

1.shmget()函数

  • 该函数用于创建/获取共享内存。

  • 包含头文件:

    c++ 复制代码
    #include <sys/ipc.h>
    #include <sys/shm.h>
  • 函数声明:

    c++ 复制代码
    typedef int key_t;
    typedef unsigned long size_t;
    
    /* 获取共享内存段.  */
    extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW;
  • 参数说明:

    • __key:共享内存的键值,是一个整数(typedef int key_t),一般采用十六进制,例如0x5005,不同共享内存的key不能相同。

    • __size:共享内存的大小,以字节为单位。

    • __shmflg:共享内存的访问权限,与文件的权限一样,例如0666 | IPC_CREAT0666表示全部用户对它可读写,IPC_CREAT表示如果共享内存不存在,就创建它。

    • 返回值:成功返回共享内存的id(一个非负的整数),失败返回-1(系统内存不足、没有权限)。

    • 查看系统的共享内存,包括:键值(key),共享内存id(shmid),拥有者(owner),权限(perms),大小(bytes)。

      bash 复制代码
      ipcs -m
    • 手动删除共享内存。

      bash 复制代码
      ipcrm -m 共享内存id

2.shmat()函数

  • 该函数用于把共享内存连接到当前进程的地址空间。

  • 函数声明:

    c++ 复制代码
    /* 附加共享内存段.  */
    extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg) __THROW;
  • 参数说明:

    • __shmid:由shmget()函数返回的共享内存标识。
    • __shmaddr:指定共享内存连接到当前进程中的地址位置,通常填0,表示让系统来选择共享内存的地址。
    • __shmflg:标志位,通常填0
  • 调用成功时返回共享内存起始地址,失败返回(void*)-1并设置 errno 以指示错误原因。

3.shmdt()函数

  • 该函数用于将共享内存从当前进程中分离,相当于shmat()函数的反操作。

  • 函数声明:

    c++ 复制代码
    /* 分离共享内存段.  */
    extern int shmdt (const void *__shmaddr) __THROW;
  • __shmaddrshmat()函数返回的地址。

  • 调用成功返回0,失败返回-1

4.shmctl()函数

  • 该函数用于操作共享内存,最常用的操作是删除共享内存。

  • 函数声明:

    c++ 复制代码
    /* 共享内存控制操作.  */
    extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW;
  • 参数说明:

    • __shmidshmget()函数返回的共享内存id。
    • __cmd:操作共享内存的指令,如果要删除共享内存,填IPC_RMID
    • __buf:操作共享内存的数据结构的地址,如果要删除共享内存,填0
  • 调用成功返回0,失败返回-1

  • 注意:使用root创建的共享内存,不管创建的权限是什么,普通用户都无法删除。

5.示例

c++ 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

// 共享内存结构体
struct shmdata
{
    int id;            // 一个简单的整数标识
    char message[256]; // 一个消息字符串
};

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "using: ./test <id> <msg>" << std::endl;
        return -1;
    }

    // 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。
    int shmid = shmget(0x5005, sizeof(shmdata), 0640 | IPC_CREAT);
    if (shmid == -1)
    {
        perror("共享内存创建失败");
        return -1;
    }

    std::cout << "共享内存ID = " << shmid << std::endl;

    // 第2步:把共享内存连接到当前进程的地址空间。
    shmdata *ptr = (shmdata *)shmat(shmid, nullptr, 0);
    if (ptr == (void *)-1)
    {
        perror("共享内存连接失败");
        return -1;
    }

    // 第3步:使用共享内存,对共享内存进行读/写。
    std::cout << "原始数据: id = " << ptr->id << ", 消息 = " << ptr->message << std::endl;

    // 更新共享内存中的数据
    ptr->id = std::atoi(argv[1]);
    std::strncpy(ptr->message, argv[2], sizeof(ptr->message) - 1);
    ptr->message[sizeof(ptr->message) - 1] = '\0'; // 确保字符串以null结尾

    std::cout << "更新后的数据: id = " << ptr->id << ", 消息 = " << ptr->message << std::endl;

    // 第4步:把共享内存从当前进程中分离。
    if (shmdt(ptr) == -1)
    {
        perror("共享内存分离失败");
        return -1;
    }

    // 第5步:删除共享内存(如果需要删除)。
    /* if (shmctl(shmid, IPC_RMID, nullptr) == -1)
     {
         perror("共享内存删除失败");
         return -1;
     } */

    return 0;
}

16.循环队列、信号量、生产/消费者模源码

c++ 复制代码
#ifndef __PUBLIC_HH
#define __PUBLIC_HH

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/sem.h>

// 循环队列模板类。
template <class TT, int MaxLength>
class squeue
{
private:
    bool m_inited;        // 队列被初始化标志,true-已初始化;false-未初始化。
    TT m_data[MaxLength]; // 用数组存储循环队列中的元素。
    int m_head;           // 队列的头指针。
    int m_tail;           // 队列的尾指针,指向队尾元素。
    int m_length;         // 队列的实际长度。

    squeue(const squeue &) = delete;            // 禁用拷贝构造函数。
    squeue &operator=(const squeue &) = delete; // 禁用赋值函数。

public:
    squeue() { init(); } // 构造函数。

    // 循环队列的初始化操作。
    // 注意:如果用于共享内存的队列,不会调用构造函数,必须调用此函数初始化。
    void init()
    {
        if (!m_inited)
        {                                           // 循环队列的初始化只能执行一次。
            m_head = 0;                             // 头指针。
            m_tail = MaxLength - 1;                 // 为了方便写代码,初始化时,尾指针指向队列的最后一个位置。
            m_length = 0;                           // 队列的实际长度。
            std::memset(m_data, 0, sizeof(m_data)); // 数组元素清零。
            m_inited = true;
        }
    }

    // 元素入队,返回值:false-失败;true-成功。
    bool push(const TT &ee)
    {
        if (full())
        {
            std::cout << "循环队列已满,入队失败。\n";
            return false;
        }

        // 先移动队尾指针,然后再拷贝数据。
        m_tail = (m_tail + 1) % MaxLength; // 队尾指针后移。
        m_data[m_tail] = ee;
        m_length++;

        return true;
    }

    // 求循环队列的长度,返回值:>=0-队列中元素的个数。
    int size() const
    {
        return m_length;
    }

    // 判断循环队列是否为空,返回值:true-空,false-非空。
    bool empty() const
    {
        return m_length == 0;
    }

    // 判断循环队列是否已满,返回值:true-已满,false-未满。
    bool full() const
    {
        return m_length == MaxLength;
    }

    // 查看队头元素的值,元素不出队。
    TT &front()
    {
        return m_data[m_head];
    }

    // 元素出队,返回值:false-失败;true-成功。
    bool pop()
    {
        if (empty())
            return false;

        m_head = (m_head + 1) % MaxLength; // 队列头指针后移。
        m_length--;

        return true;
    }

    // 显示循环队列中全部的元素。
    // 这是一个临时的用于调试的函数,队列中元素的数据类型支持cout输出才可用。
    void printqueue() const
    {
        for (int i = 0; i < size(); i++)
        {
            std::cout << "m_data[" << (m_head + i) % MaxLength << "], value="
                      << m_data[(m_head + i) % MaxLength] << std::endl;
        }
    }
};

// 信号量类。
class csemp
{
private:
    union semun
    { // 用于信号量操作的联合体。
        int val;
        struct semid_ds *buf;
        unsigned short *arry;
    };

    int m_semid;     // 信号量id(描述符)。
    short m_sem_flg; // 信号量的标志位。

    csemp(const csemp &) = delete;            // 禁用拷贝构造函数。
    csemp &operator=(const csemp &) = delete; // 禁用赋值函数。

public:
    csemp() : m_semid(-1) {}

    // 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
    // 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
    // 如果用于生产消费者模型,value填0,sem_flg填0。
    bool init(key_t key, unsigned short value = 1, short sem_flg = SEM_UNDO);

    // 信号量的P操作,如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
    bool wait(short value = -1);

    // 信号量的V操作。
    bool post(short value = 1);

    // 获取信号量的值,成功返回信号量的值,失败返回-1。
    int getvalue() const;

    // 销毁信号量。
    bool destroy();

    ~csemp();
};

#endif // __PUBLIC_HH
c++ 复制代码
#include "public.h"

// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool csemp::init(key_t key, unsigned short value, short sem_flg)
{
    if (m_semid != -1)
        return false; // 如果已经初始化了,不必再次初始化。

    m_sem_flg = sem_flg;

    // 信号量的初始化不能直接用semget(key, 1, 0666 | IPC_CREAT)
    // 因为信号量创建后,初始值是0,如果用于互斥锁,需要把它的初始值设置为1,
    // 而获取信号量则不需要设置初始值,所以,创建信号量和获取信号量的流程不同。

    // 信号量的初始化分三个步骤:
    // 1) 获取信号量,如果成功,函数返回。
    // 2) 如果失败,则创建信号量。
    // 3) 设置信号量的初始值。

    // 获取信号量。
    if ((m_semid = semget(key, 1, 0666)) == -1)
    {
        // 如果信号量不存在,创建它。
        if (errno == ENOENT)
        {
            // 用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取。
            if ((m_semid = semget(key, 1, 0666 | IPC_CREAT | IPC_EXCL)) == -1)
            {
                if (errno == EEXIST)
                { // 如果错误代码是信号量已存在,则再次获取信号量。
                    if ((m_semid = semget(key, 1, 0666)) == -1)
                    {
                        perror("init 1 semget()");
                        return false;
                    }
                    return true;
                }
                else
                { // 如果是其它错误,返回失败。
                    perror("init 2 semget()");
                    return false;
                }
            }

            // 信号量创建成功后,还需要把它初始化成value。
            union semun sem_union;
            sem_union.val = value; // 设置信号量的初始值。
            if (semctl(m_semid, 0, SETVAL, sem_union) < 0)
            {
                perror("init semctl()");
                return false;
            }
        }
        else
        {
            perror("init 3 semget()");
            return false;
        }
    }

    return true;
}

// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool csemp::wait(short value)
{
    if (m_semid == -1)
        return false;

    struct sembuf sem_b;
    sem_b.sem_num = 0;    // 信号量编号,0代表第一个信号量。
    sem_b.sem_op = value; // P操作的value必须小于0。
    sem_b.sem_flg = m_sem_flg;
    if (semop(m_semid, &sem_b, 1) == -1)
    {
        perror("wait semop()");
        return false;
    }

    return true;
}

// 信号量的V操作(把信号量的值增加value)。
bool csemp::post(short value)
{
    if (m_semid == -1)
        return false;

    struct sembuf sem_b;
    sem_b.sem_num = 0;    // 信号量编号,0代表第一个信号量。
    sem_b.sem_op = value; // V操作的value必须大于0。
    sem_b.sem_flg = m_sem_flg;
    if (semop(m_semid, &sem_b, 1) == -1)
    {
        perror("post semop()");
        return false;
    }

    return true;
}

// 获取信号量的值,成功返回信号量的值,失败返回-1。
int csemp::getvalue() const
{
    return semctl(m_semid, 0, GETVAL);
}

// 销毁信号量。
bool csemp::destroy()
{
    if (m_semid == -1)
        return false;

    if (semctl(m_semid, 0, IPC_RMID) == -1)
    {
        perror("destroy semctl()");
        return false;
    }

    return true;
}

// 信号量析构函数。
csemp::~csemp()
{
    // 在析构函数中销毁信号量。
    destroy();
}
c++ 复制代码
// 本程序演示循环队列的使用。
#include "public.h"

int main()
{
    using ElemType = int;

    squeue<ElemType, 5> Queue;

    ElemType element; // 创建一个数据元素。

    std::cout << "元素(1、2、3)入队" << std::endl;
    element = 1;
    Queue.push(element);
    element = 2;
    Queue.push(element);
    element = 3;
    Queue.push(element);

    std::cout << "队列的长度是: " << Queue.size() << std::endl;
    Queue.printqueue();

    element = Queue.front();
    Queue.pop();
    std::cout << "出队的元素值为: " << element << std::endl;
    element = Queue.front();
    Queue.pop();
    std::cout << "出队的元素值为: " << element << std::endl;

    std::cout << "队列的长度是: " << Queue.size() << std::endl;
    Queue.printqueue();

    std::cout << "元素(11、12、13、14、15)入队." << std::endl;
    element = 11;
    Queue.push(element);
    element = 12;
    Queue.push(element);
    element = 13;
    Queue.push(element);
    element = 14;
    Queue.push(element);
    element = 15;
    Queue.push(element);

    std::cout << "队列的长度是: " << Queue.size() << std::endl;
    Queue.printqueue();

    return 0;
}
c++ 复制代码
// shared_memory_cirucularqueue.cpp,本程序演示基于共享内存的循环队列。
#include "public.h"

int main()
{
    using ElemType = int;

    // 初始化共享内存。
    int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);
    if (shmid == -1)
    {
        std::cout << "shmget(0x5005) failed." << std::endl;
        return -1;
    }

    // 把共享内存连接到当前进程的地址空间。
    squeue<ElemType, 5> *Queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);
    if (Queue == (void *)-1)
    {
        std::cout << "shmat() failed." << std::endl;
        return -1;
    }

    Queue->init(); // 初始化循环队列。

    ElemType element; // 创建一个数据元素。

    std::cout << "元素(1、2、3)入队。\n";
    element = 1;
    Queue->push(element);
    element = 2;
    Queue->push(element);
    element = 3;
    Queue->push(element);

    std::cout << "队列的长度是: " << Queue->size() << std::endl;
    Queue->printqueue();

    element = Queue->front();
    Queue->pop();
    std::cout << "出队的元素值为: " << element << std::endl;
    element = Queue->front();
    Queue->pop();
    std::cout << "出队的元素值为: " << element << std::endl;

    std::cout << "队列的长度是: " << Queue->size() << std::endl;
    Queue->printqueue();

    std::cout << "元素(11、12、13、14、15)入队." << std::endl;
    element = 11;
    Queue->push(element);
    element = 12;
    Queue->push(element);
    element = 13;
    Queue->push(element);
    element = 14;
    Queue->push(element);
    element = 15;
    Queue->push(element);

    std::cout << "队列的长度是: " << Queue->size() << std::endl;
    Queue->printqueue();

    shmdt(Queue); // 把共享内存从当前进程中分离。

    return 0;
}
c++ 复制代码
// shared_memory_lock.cpp,本程序演示用信号量给共享内存加锁。
#include "public.h"

struct PersonInfo
{                  // 人员信息结构体。
    int id;        // 编号。
    char name[32]; // 姓名。
};

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "using: ./shared_memory_lock id name" << std::endl;
        return -1;
    }

    // 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。
    int shmid = shmget(0x5005, sizeof(PersonInfo), 0640 | IPC_CREAT);
    if (shmid == -1)
    {
        std::cout << "shmget(0x5005) failed." << std::endl;
        return -1;
    }

    std::cout << "shmid = " << shmid << std::endl;

    // 第2步:把共享内存连接到当前进程的地址空间。
    PersonInfo *ptr = (PersonInfo *)shmat(shmid, 0, 0);
    if (ptr == (void *)-1)
    {
        std::cout << "shmat() failed." << std::endl;
        return -1;
    }

    // 创建、初始化二元信号量。
    csemp mutex;
    if (!mutex.init(0x5005))
    {
        std::cout << "mutex.init(0x5005) failed." << std::endl;
        ;
        return -1;
    }

    std::cout << "申请加锁..." << std::endl;
    mutex.wait(); // 申请加锁。
    std::cout << "申请加锁成功." << std::endl;

    // 第3步:使用共享内存,对共享内存进行读/写。
    std::cout << "原值: id = " << ptr->id << ", name = " << ptr->name << std::endl; // 显示共享内存中的原值。
    ptr->id = atoi(argv[1]);                                                        // 对人员信息结构体的id成员赋值。
    strcpy(ptr->name, argv[2]);                                                     // 对人员信息结构体的name成员赋值。
    std::cout << "新值: id = " << ptr->id << ", name = " << ptr->name << std::endl; // 显示共享内存中的当前值。
    sleep(10);

    mutex.post(); // 解锁。
    std::cout << "解锁." << std::endl;

    // 查看信号量:ipcs -s    // 删除信号量:ipcrm sem 信号量id
    // 查看共享内存:ipcs -m    // 删除共享内存:ipcrm -m  共享内存id

    // 第4步:把共享内存从当前进程中分离。
    shmdt(ptr);

    // 第5步:删除共享内存。
    // if (shmctl(shmid,IPC_RMID,0) == -1)
    //{
    // std::cout << "shmctl failed"; << std::endl; return -1;
    //}
}
c++ 复制代码
#include "public.h"	// 生产者 producer.cpp

int main()
{
    struct Person
    { // 生产队列的数据元素是人员信息结构体。
        int id;
        char name[31];
    };

    using ElemType = Person;

    // 初始化共享内存。
    int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);
    if (shmid == -1)
    {
        std::cout << "shmget(0x5005) failed." << std::endl;
        return -1;
    }

    // 把共享内存连接到当前进程的地址空间。
    squeue<ElemType, 5> *queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);
    if (queue == (void *)-1)
    {
        std::cout << "shmat() failed." << std::endl;
        return -1;
    }

    queue->init(); // 初始化循环队列。

    ElemType element; // 创建一个数据元素。

    csemp mutex;
    mutex.init(0x5001); // 用于给共享内存加锁。
    csemp cond;
    cond.init(0x5002, 0, 0); // 信号量的值用于表示队列中数据元素的个数。

    mutex.wait(); // 加锁。
    // 生产3个数据。
    element.id = 3;
    strncpy(element.name, "Tom", sizeof(element.name));
    queue->push(element);
    element.id = 7;
    strncpy(element.name, "Tomy", sizeof(element.name));
    queue->push(element);
    element.id = 8;
    strncpy(element.name, "Tony", sizeof(element.name));
    queue->push(element);
    mutex.post(); // 解锁。
    cond.post(3); // 实参是3,表示生产了3个数据。

    shmdt(queue); // 把共享内存从当前进程中分离。

    return 0;
}
c++ 复制代码
#include "public.h"	// 消费者 consumer.cpp

int main()
{
    struct Person
    { // 循环队列的数据元素是人员信息结构体。
        int id;
        char name[31];
    };

    using ElemType = Person;

    // 初始化共享内存。
    int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);
    if (shmid == -1)
    {
        std::cout << "shmget(0x5005) failed." << std::endl;
        return -1;
    }

    // 把共享内存连接到当前进程的地址空间。
    squeue<ElemType, 5> *queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);
    if (queue == (void *)-1)
    {
        std::cout << "shmat() failed." << std::endl;
        return -1;
    }

    queue->init(); // 初始化循环队列。

    ElemType element; // 创建一个数据元素。

    csemp mutex;
    mutex.init(0x5001); // 用于给共享内存加锁。
    csemp cond;
    cond.init(0x5002, 0, 0); // 信号量的值用于表示队列中数据元素的个数。

    while (true)
    {
        mutex.wait(); // 加锁。

        while (queue->empty())
        {                 // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if
            mutex.post(); // 解锁。
            cond.wait();  // 等待生产者的唤醒信号。
            mutex.wait(); // 加锁。
        }

        // 数据元素出队。
        element = queue->front();
        queue->pop();
        mutex.post(); // 解锁。

        // 处理出队的数据(把数据消费掉)。
        std::cout << "id = " << element.id << ", name = " << element.name << std::endl;
        usleep(100); // 假设处理数据需要时间,方便演示。
    }

    shmdt(queue); // 把共享内存从当前进程中分离。

    return 0;
}

17.第一个网络通讯程序

1.网络通讯的流程

  • 服务器端流程:
    1. 创建Socket :使用 socket() 函数创建一个套接字(socket),指定地址族和套接字类型。对于TCP通信,通常使用 AF_INETSOCK_STREAM 参数。
    2. 绑定地址和端口 :使用 bind() 函数将套接字与服务器的地址和端口绑定。需要设置套接字地址结构体 struct sockaddr_in 的成员,包括地址族、端口号和IP地址。
    3. 监听连接 :使用 listen() 函数开始监听连接请求。指定服务器可以同时处理的最大连接数,即待处理的连接请求队列长度。
    4. 接受连接请求 :使用 accept() 函数接受客户端的连接请求,创建一个新的套接字来处理与客户端之间的通信。accept() 函数会阻塞直到有新的连接请求到达。
    5. 接收数据并发送响应 :使用 recv() 函数从客户端接收数据,并使用 send() 函数向客户端发送响应。这个过程可以在一个循环中进行,直到通信结束
    6. 关闭连接 :当通信结束后,使用 close() 函数关闭连接套接字,释放资源。
  • 客户端流程:
    1. 创建Socket :使用 socket() 函数创建一个套接字(socket),指定地址族和套接字类型。对于TCP通信,通常使用 AF_INETSOCK_STREAM 参数。
    2. 连接到服务器 :使用 connect() 函数连接到服务器的套接字,指定服务器的地址和端口号。
    3. 发送请求并接收响应 :使用 send() 函数向服务器发送请求,并使用 recv() 函数从服务器接收响应。这个过程可以在一个循环中进行,直到通信结束。
    4. 关闭连接 :当通信结束后,使用 close() 函数关闭连接套接字,释放资源。

2.示例

  • 客户端

    c++ 复制代码
    #include <iostream>
    #include <cstring>
    #include <cstdlib>
    #include <unistd.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            std::cout << "using: ./socketclient <server_ip> <server_port>" << std::endl
                      << "example: ./ socketclient 192.168.101.139 5005" << std::endl;
            return -1;
        }
    
        // 创建客户端套接字
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd == -1)
        {
            perror("socket failed.");
            return -1;
        }
    
        // 获取服务器地址
        struct hostent *server_info = gethostbyname(argv[1]);
        if (server_info == nullptr)
        {
            std::cout << "Error: Failed to get server info." << std::endl;
            close(sockfd);
            return -1;
        }
    
        // 构建服务器地址结构
        struct sockaddr_in server_address;
        memset(&server_address, 0, sizeof(server_address));
        server_address.sin_family = AF_INET;
        memcpy(&server_address.sin_addr, server_info->h_addr, server_info->h_length);
        server_address.sin_port = htons(atoi(argv[2])); // 使用 atoi() 将字符串端口号转换为整数端口号
    
        // 连接服务器
        if (connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1)
        {
            perror("connect failed.");
            close(sockfd);
            return -1;
        }
    
        // 发送和接收数据
        char buffer[1024];
        for (int i = 0; i < 3; ++i)
        {
            // 发送请求报文
            sprintf(buffer, "Request #%d from client.", i + 1);
            ssize_t sent_bytes = send(sockfd, buffer, strlen(buffer), 0);
            if (sent_bytes <= 0)
            {
                perror("send failed.");
                break;
            }
            std::cout << "sent: " << buffer << std::endl;
    
            // 接收服务器响应报文
            memset(buffer, 0, sizeof(buffer));
            ssize_t recv_bytes = recv(sockfd, buffer, sizeof(buffer), 0);
            if (recv_bytes <= 0)
            {
                std::cout << "recv_bytes = " << recv_bytes << std::endl;
                break;
            }
            std::cout << "received: " << buffer << std::endl;
    
            sleep(1); // 等待1秒
        }
    
        // 关闭套接字
        close(sockfd);
        return 0;
    }
  • 服务端:

    c++ 复制代码
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <unistd.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            std::cout << "using: ./socketserver <port_number>" << std::endl;
            std::cout << "example: ./socketserver 5005." << std::endl;
            std::cout << "note: The firewall on the Linux system running the server program must open port 5005." << std::endl;
            std::cout << "if it is a cloud server, access policies on the cloud platform must also be opened." << std::endl;
            return -1;
        }
    
        // 创建服务端的socket
        int listenfd = socket(AF_INET, SOCK_STREAM, 0);
        if (listenfd == -1)
        {
            perror("socket failed.");
            return -1;
        }
    
        // 将服务端用于通信的IP和端口绑定到socket上
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port = htons(atoi(argv[1])); // 使用 atoi() 将字符串端口号转换为整数端口号
        if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
        {
            perror("bind failed.");
            close(listenfd);
            return -1;
        }
    
        // 将socket设置为可连接(监听)的状态
        if (listen(listenfd, 5) != 0)
        {
            perror("listen failed.");
            close(listenfd);
            return -1;
        }
    
        // 受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待
        int clientfd = accept(listenfd, 0, 0);
        if (clientfd == -1)
        {
            perror("accept failed.");
            close(listenfd);
            return -1;
        }
    
        std::cout << "client connected." << std::endl;
    
        // 与客户端通信,接收客户端发过来的报文后,回复ok
        char buffer[1024];
        while (true)
        {
            int iret;
            memset(buffer, 0, sizeof(buffer));
            // 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待
            // 如果客户端已断开连接,recv()函数将返回0
            if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0)
            {
                std::cout << "iret = " << iret << std::endl;
                break;
            }
            std::cout << "received: " << buffer << std::endl;
    
            strcpy(buffer, "ok"); // 生成回应报文内容
            // 向客户端发送回应报文
            if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0)
            {
                perror("send failed.");
                break;
            }
            std::cout << "sent: " << buffer << std::endl;
        }
    
        // 关闭socket,释放资源
        close(listenfd); // 关闭服务端用于监听的socket
        close(clientfd); // 关闭客户端连上来的socket
        return 0;
    }

18.基于Linux的文件操作

Linux底层文件的操作-创建文件并写入数据

c++ 复制代码
// filecw.cpp,本程序演示了Linux底层文件的操作-创建文件并写入数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    int fd; // 文件描述符

    // 打开文件,如果创建后的文件没有权限,可以手工授权 chmod 777 data.txt。
    fd = open("data.txt", O_CREAT | O_RDWR | O_TRUNC, 0666); // 添加文件权限参数0666
    if (fd == -1)
    {
        perror("open data.txt failed.");
        return -1;
    }

    printf("file descriptor fd = %d\n", fd);

    char buffer[1024];
    strcpy(buffer, "This is a sample text.\n");

    if (write(fd, buffer, strlen(buffer)) == -1)
    { // 把数据写入文件。
        perror("write failed.");
        return -1;
    }

    close(fd); // 关闭文件。
    return 0;  // 添加返回值,表示程序执行成功
}

Linux底层文件的操作-读取文件

c++ 复制代码
// fileread.cpp,本程序演示了Linux底层文件的操作-读取文件。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    int fd; // 定义一个文件描述符/文件句柄。

    fd = open("data.txt", O_RDONLY); // 打开文件。
    if (fd == -1)
    {
        perror("open data.txt failed.");
        return -1;
    }

    printf("文件描述符: fd = %d\n", fd);

    char buffer[1024];
    memset(buffer, 0, sizeof(buffer));
    if (read(fd, buffer, sizeof(buffer)) == -1) // 从文件中读取数据。
    {
        perror("write failed.");
        return -1;
    }

    printf("%s", buffer);

    close(fd); // 关闭文件。
}

19.socket()函数详解

1.什么是协议

  • 人与人沟通的方式有很多种:书信、电话、QQ、微信。如果两个人想沟通,必须先选择一种沟通的方式,如果一方使用电话,另一方也应该使用电话,而不是书信。
  • 协议是网络通讯的规则,是约定。

2.创建socket

  • 包含头文件:

    c++ 复制代码
    #include <sys/types.h>
    #include <sys/socket.h>
  • 函数声明:

    c++ 复制代码
    /* 在域 DOMAIN 中创建一个type类型的套接字, 使用协议 PROTOCOL.
       如果 PROTOCOL 为 0, 则自动选择一个.
       返回新套接字的文件描述符, 或-1表示错误.  */
    extern int socket (int __domain, int __type, int __protocol) __THROW;
  • 成功返回一个有效的socket,失败返回-1errno被设置。

  • 全部网络编程的函数,失败时基本上都是返回-1errno被设置,只要参数没填错,基本上不会失败。

  • 注意:单个进程中创建的socket数量与受系统参数open files的限制。

    • 使用以下命令查看:

      bash 复制代码
      ulimit -a

1.__domain通讯的协议家族

  • PF_INET:IPV4互联网协议族。
  • PF_INET6:IPV6互联网协议族。
  • PF_LOCAL:本地通信的协议族。
  • PF_PACKET:内核底层的协议族。
  • PF_IPX:IPX Novell协议族。
  • IPV6尚未普及,其它的不常用。

2.__type数据传输的类型

  • SOCK_STREAM:面向连接的socket
    1. 数据不会丢失;
    2. 数据的顺序不会错乱;
    3. 双向通道。
  • SOCK_DGRAM:无连接的socket
    1. 数据可能丢失;
    2. 数据的顺序可能会错乱;
    3. 传输效率更高。

3.__protocol最终使用的协议

  • 在IPv4网络协议家族中,数据传输方式为SOCK_STREAM的协议只有IPPROTO_TCP,数据传输方式为SOCK_DGRAM的协议只有IPPROTO_UDP

  • 本参数也可以填0

  • socket()函数使用实例:

    c++ 复制代码
    socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);    // 创建tcp的sock
    socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);    // 创建udp的sock

3.TCP和UDP

1.TCP和UDP的区别

  • TCP
    1. TCP面向连接,通过三次握手建立连接,四次挥手断开连接;
    2. TCP是可靠的通信方式,通过超时重传、数据校验等方式来确保数据无差错,不丢失,不重复,并且按序到达;
    3. TCP把数据当成字节流,当网络出现波动时,连接可能出现响应延迟的问题;
    4. TCP只支持点对点通信;
    5. TCP报文的首部较大,为20字节;
    6. TCP是全双工的可靠信道。
  • UDP
    1. UDP是无连接的,即发送数据之前不需要建立连接,这种方式为UDP带来了高效的传输效率,但也导致无法确保数据的发送成功;
    2. UDP以最大的速率进行传输,但不保证可靠交付,会出现丢失、重复等等问题;
    3. UDP没有拥塞控制,当网络出现拥塞时,发送方不会降低发送速率;
    4. UDP支持一对一,一对多,多对一和多对多的通信;
    5. UDP报文的首部比较小,只有8字节;
    6. UDP是不可靠信道。

2.TCP保证自身可靠的方式

  1. 数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组;
  2. 到达确认:接收端接收到分片数据时,根据分片的序号向对端回复一个确认包;
  3. 超时重发:发送方在发送分片后开始计时,若超时却没有收到对端的确认包,将会重发分片;
  4. 滑动窗口:TCP中采用滑动窗口来进行传输控制,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为0时,发送方不会再发送数据;
  5. 失序处理:TCP的接收端会把接收到的数据重新排序;
  6. 重复处理:如果传输的分片出现重复,TCP的接收端会丢弃重复的数据;
  7. 数据校验:TCP通过数据的检验和来判断数据在传输过程中是否正确。

3.UDP不可靠的原因

  • 没有上述TCP的机制,如果校验和出错,UDP会将该报文丢弃。

4.TCP和UDP使用场景

  • TCP使用场景

    • TCP实现了数据传输过程中的各种控制,适合对可靠性有要求的场景。
  • UDP使用场景

    可以容忍数据丢失的场景:

    • 视频、音频等多媒体通信(即时通信);
    • 广播信息。

5.UDP能实现可靠传输吗

  • 这是个伪命题,如果用UDP实现可靠传输,那么应用程序必须实现重传和排序等功能非常麻烦,还不如直接用TCP。谁能保证自己写的算法比写TCP协议的人更牛。

20.主机字节序与网络字节序

1.大端序/小端序

  • 如果数据类型占用的内存空间大于1字节,CPU把数据存放在内存中的方式有两种:

    • 大端序(Big Endian):低位字节存放在高位,高位字节存放在低位。
    • 小端序(Little Endia):低位字节存放在低位,高位字节存放在高位。
  • 假设从内存地址0x00000001处开始存储十六进制数0x12345678,那么:

    • Bit-endian(按原来顺序存储)

      0x00000001 0x12

      0x00000002 0x34

      0x00000003 0x56

      0x00000004 0x78

    • Little-endian(颠倒顺序储存)

      0x00000001 0x78

      0x00000002 0x56

      0x00000003 0x34

      0x00000004 0x12

  • Intel系列的CPU以小端序方式保存数据,其它型号的CPU不一定。

  • 操作文件的本质是把内存中的数据写入磁盘,在网络编程中,传输数据的本质也是把数据写入文件(socket也是文件描述符)。这样的话,字节序不同的计算机之间传输数据,可能会出现问题。

2.网络字节序

  • 为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序(大端序)。

  • C语言提供了四个库函数,用于在主机字节序和网络字节序之间转换:

    • 包含头文件:

      c++ 复制代码
      #include <apra/inet.h>
    • 函数声明:

      c++ 复制代码
      /* 在主机和网络之间进行字节顺序转换的函数.
         请注意这些函数通常使用 `unsigned long int' 或
         `unsigned short int' 值作为参数并返回它们.  但
         这是一个目光短浅的决定,因为在不同的系统上类型不同
         可能有不同的表示 但值总是相同的.  */
      extern uint32_t ntohl (uint32_t __netlong) __THROW __attribute__ ((__const__));
      extern uint16_t ntohs (uint16_t __netshort) __THROW __attribute__ ((__const__));
      extern uint32_t htonl (uint32_t __hostlong) __THROW __attribute__ ((__const__));
      extern uint16_t htons (uint16_t __hostshort) __THROW __attribute__ ((__const__))
    • 函数命名拆解:

      • h:host(主机);
      • to:转换;
      • n:network(网络);
      • s:short(2字节,16位的整数);
      • l:long(4字节,32位的整数)。

3.IP地址和通讯端口

  • 在计算机中,IPv4的地址用4字节的整数存放,通讯端口用2字节的整数(0-65535)存放。

  • 例如:192.168.190.134 3232284294 255.255.255.255

    ​ 192 168 190 134

    大端:11000000 10101000 10111110 10000110

    小段:10000110 10111110 10101000 11000000

4.如何处理大小端

  • 在网络编程中,数据收发的时候有自动转换机制,不需要手动转换,只有向sockaddr_in结体成员变量填充数据时,才需要考虑字节序的问题。

21.网络通讯的内部数据结构体

1.sockaddr结构体

  • 存放协议族、端口和地址信息,客户端和connect()函数和服务端的bind()函数需要这个结构体。

    c++ 复制代码
    typedef unsigned short sa_family_t;
    #define	__SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family
    
    /* 描述通用套接字地址的结构.  */
    struct sockaddr
    {
    	__SOCKADDR_COMMON (sa_);	/* 常用数据:地址族和长度.  */
    	char sa_data[14];		/* 地址数据.  */
    };

2.sockaddr_in结构体

  • sockaddr结构体是为了统一地址结构的表示方法,统一接口函数,但是,操作不方便,所以定义了等价的sockaddr_in结构体,它的大小与sockaddr相同,可以强制转换成sockaddr

    c++ 复制代码
    typedef unsigned short sa_family_t;
    #define	__SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family
    
    /* 网络地址.  */
    typedef uint32_t in_addr_t;
    struct in_addr
    {
    	in_addr_t s_addr;
    };
    
    #define __SOCKADDR_COMMON_SIZE	(sizeof (unsigned short int))
    
    typedef uint16_t in_port_t;
    
    /* 描述网络套接字地址的结构.  */
    struct sockaddr_in
    {
    	__SOCKADDR_COMMON (sin_);
    	in_port_t sin_port;			/* 端口号.  */
    	struct in_addr sin_addr;		/* 网络地址.  */
    
    /* 填充到 `struct sockaddr' 的大小.  */
    	unsigned char sin_zero[sizeof (struct sockaddr) -
       		__SOCKADDR_COMMON_SIZE -
       		sizeof (in_port_t) -
       		sizeof (struct in_addr)];
    };

3.gethostbyname()函数

  • 根据域名/主机名/字符串IP获取大端序IP,用于网络通讯的客户端程序中。

  • 包含头文件:

    c++ 复制代码
    #include <netdb.h>
  • 函数声明:

    c++ 复制代码
    /* 单个主机的数据库条目描述.  */
    struct hostent
    {
      	char *h_name;			/* 主机正式名.  */
      	char **h_aliases;		/* 别名列表.  */
      	int h_addrtype;		/* 主机地址类型.  */
      	int h_length;			/* 地址长度.  */
      	char **h_addr_list;		/* 来自名称服务器的地址列表.  */
    	h_addr	h_addr_list[0] /* 地址, 向后兼容.*/
    };
    
    /* 从主机数据库返回带有 NAME 的主机条目.
       这个函数是一个可能的消去点,因此不是标记为__THROW.  */
    extern struct hostent *gethostbyname (const char *__name);
  • 转换后,用以下代码把大端序的地址复制到sockaddr_in结构体的sin_addr成员结构中。

    c++ 复制代码
    memcpy(&sockaddr_in.sin_addr, hostent->h_addr, hostent->h_length);

4.字符串IP与大端序IP的转换

  • C语言提供了几个库函数,用于字符串格式的IP和大端序IP的互相转换,用于网络通讯的服务端程序中。

  • 包含头文件:

    c++ 复制代码
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
  • 函数声明:

    c++ 复制代码
    typedef unsigned int in_addr_t;
    
    /* 转换网络主机地址从数字和点符号在 CP
       转换成网络字节序的二进制数据.  */
    extern in_addr_t inet_addr (const char *__cp) __THROW;
    
    /* 转换网络主机地址从数字和点符号在 CP
       转换成二进制数据,并将结果存储在 INP 结构中.  */
    extern int inet_aton (const char *__cp, struct in_addr *__inp) __THROW;
    
    /* 将in中的Internet号码转换为ASCII表示.
       返回值指针是否指向包含字符串的内部数组.  */
    extern char *inet_ntoa (struct in_addr __in) __THROW;

5.示例

  • 基于TCP协议的客户端通信

    c++ 复制代码
    // 本程序演示了基于TCP协议的客户端通信
    
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <unistd.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            std::cout << "using: ./socket_client <服务端的IP> <服务端的端口>" << std::endl
                      << "example: ./socket_client 192.168.101.138 5005" << std::endl;
            return -1;
        }
    
        // 第1步:创建客户端的socket。
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd == -1)
        {
            perror("socket failed.");
            return -1;
        }
    
        // 第2步:向服务器发起连接请求。
        struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;            // ①协议族,固定填AF_INET。
        servaddr.sin_port = htons(atoi(argv[2])); // ②指定服务端的通信端口。
    
        struct hostent *hostent;                           // 用于存放服务端IP地址(大端序)的结构体的指针。
        if ((hostent = gethostbyname(argv[1])) == nullptr) // 把域名/主机名/字符串格式的IP转换成结构体。
        {
            std::cout << "gethostbyname failed." << std::endl;
            close(sockfd);
            return -1;
        }
        memcpy(&servaddr.sin_addr, hostent->h_addr, hostent->h_length); // ③指定服务端的IP(大端序)。
    
        // 向服务端发起连接请求。
        if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
        {
            perror("connect failed.");
            close(sockfd);
            return -1;
        }
    
        // 第3步:与服务端通讯,客户端发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。
        char buffer[1024];
        for (int i = 0; i < 10; i++) // 循环10次,与服务端进行10次通讯。
        {
            int iret;
            memset(buffer, 0, sizeof(buffer));
            sprintf(buffer, "这是第 %d 个数据包,编号: %03d.", i + 1, i + 1); // 生成请求报文内容。
            // 向服务端发送请求报文。
            if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0)
            {
                perror("send failed.");
                break;
            }
            std::cout << "发送: " << buffer << std::endl;
    
            memset(buffer, 0, sizeof(buffer));
            // 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。
            if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0)
            {
                std::cout << "iret = " << iret << std::endl;
                break;
            }
            std::cout << "接收: " << buffer << std::endl;
    
            sleep(1); // 模拟处理时间
        }
    
        // 第4步:关闭socket,释放资源。
        close(sockfd);
        return 0;
    }
  • 基于TCP协议的服务端通信

    c++ 复制代码
    // 本程序演示了基于TCP协议的服务端通信
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <unistd.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            std::cout << "using: ./socket_server <通讯端口>" << std::endl
                      << "example: ./socket_server 5005" << std::endl;
            return -1;
        }
    
        // 第1步:创建服务端的socket。
        int listenfd = socket(AF_INET, SOCK_STREAM, 0);
        if (listenfd == -1)
        {
            perror("socket failed.");
            return -1;
        }
    
        // 第2步:把服务端用于通信的IP和端口绑定到socket上。
        struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;                 // ①协议族,固定填AF_INET。
        servaddr.sin_port = htons(std::atoi(argv[1])); // ②指定服务端的通信端口。
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // ③服务端任意网卡的IP都可以用于通讯。
    
        // 绑定服务端的IP和端口。
        if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
        {
            perror("bind failed.");
            close(listenfd);
            return -1;
        }
    
        // 第3步:把socket设置为可连接(监听)的状态。
        if (listen(listenfd, 5) == -1)
        {
            perror("listen failed.");
            close(listenfd);
            return -1;
        }
    
        // 第4步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。
        int clientfd = accept(listenfd, nullptr, nullptr);
        if (clientfd == -1)
        {
            perror("accept failed.");
            close(listenfd);
            return -1;
        }
    
        std::cout << "客户端已连接." << std::endl;
    
        // 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
        char buffer[1024];
        while (true)
        {
            int iret;
            memset(buffer, 0, sizeof(buffer));
            // 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。
            // 如果客户端已断开连接,recv()函数将返回0。
            if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0)
            {
                std::cout << "iret = " << iret << std::endl;
                break;
            }
            std::cout << "接收: " << buffer << std::endl;
    
            strcpy(buffer, "ok"); // 生成回应报文内容。
            // 向客户端发送回应报文。
            if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0)
            {
                perror("send failed.");
                break;
            }
            std::cout << "发送: " << buffer << std::endl;
        }
    
        // 第6步:关闭socket,释放资源。
        close(listenfd); // 关闭服务端用于监听的socket。
        close(clientfd); // 关闭客户端连上来的socket。
    
        return 0;
    }

22.封装socket

  • 封装socket通讯的客户端

    c++ 复制代码
    // tcp_clientcpp - 基于TCP协议的客户端通信.
    
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <unistd.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    class TCPClient // TCP通讯的客户端类.
    {
    private:
        int client_fd;       // 客户端的socket,-1 表示未连接或连接已断开; >= 0 表示有效的socket.
        std::string ip;      // 服务端的IP/域名.
        unsigned short port; // 通讯端口.
    
    public:
        TCPClient() : client_fd(-1) {}
    
        // 向服务端发起连接请求,成功返回true,失败返回false.
        bool connect(const std::string &in_ip, const unsigned short in_port)
        {
            if (client_fd != -1)
                return false; // 如果socket已连接,直接返回失败.
    
            ip = in_ip;
            port = in_port; // 把服务端的IP和端口保存到成员变量中.
    
            // 第1步:创建客户端的socket.
            if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
                return false;
    
            // 第2步:向服务器发起连接请求.
            struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体.
            memset(&servaddr, 0, sizeof(servaddr));
            servaddr.sin_family = AF_INET;   // ①协议族,固定填 AF_INET.
            servaddr.sin_port = htons(port); // ②指定服务端的通信端口.
    
            struct hostent *h;                              // 用于存放服务端IP地址(大端序)的结构体的指针.
            if ((h = gethostbyname(ip.c_str())) == nullptr) // 把域名/主机名/字符串格式的IP转换成结构体.
            {
                ::close(client_fd);
                client_fd = -1;
                return false;
            }
            memcpy(&servaddr.sin_addr, h->h_addr, h->h_length); // ③指定服务端的IP(大端序).
    
            // 向服务端发起连接请求.
            if (::connect(client_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
            {
                ::close(client_fd);
                client_fd = -1;
                return false;
            }
    
            return true;
        }
    
        // 向服务端发送报文,成功返回true,失败返回false.
        bool send(const std::string &buffer) // buffer不要用const char*
        {
            if (client_fd == -1)
                return false; // 如果socket的状态是未连接,直接返回失败.
    
            if ((::send(client_fd, buffer.data(), buffer.size(), 0)) <= 0)
                return false;
    
            return true;
        }
    
        // 接收服务端的报文,成功返回true,失败返回false.
        // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度.
        bool recv(std::string &buffer, const size_t maxlen)
        {
            buffer.clear();                                              // 清空容器.
            buffer.resize(maxlen);                                       // 设置容器的大小为maxlen.
            int readn = ::recv(client_fd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存.
            if (readn <= 0)
            {
                buffer.clear();
                return false;
            }
            buffer.resize(readn); // 重置buffer的实际大小.
    
            return true;
        }
    
        // 断开与服务端的连接.
        bool close()
        {
            if (client_fd == -1)
                return false; // 如果socket的状态是未连接,直接返回失败.
    
            ::close(client_fd);
            client_fd = -1;
            return true;
        }
    
        ~TCPClient() { close(); }
    };
    
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            std::cout << "using: ./tcp_client <服务端的IP> <服务端的端口>" << std::endl
                      << "example: ./tcp_client 192.168.101.138 5005" << std::endl;
            return -1;
        }
    
        TCPClient tcpClient;
        if (tcpClient.connect(argv[1], std::atoi(argv[2])) == false) // 向服务端发起连接请求.
        {
            perror("connect failed.");
            return -1;
        }
    
        // 第3步:与服务端通讯,客户端发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文.
        std::string buffer;
        for (int i = 0; i < 10; i++) // 循环10次,与服务端进行10次通讯.
        {
            buffer = "这是第 " + std::to_string(i + 1) + " 个数据包, 编号: " + std::to_string(i + 1) + ".";
            // 向服务端发送请求报文.
            if (tcpClient.send(buffer) == false)
            {
                perror("send failed.");
                break;
            }
            std::cout << "发送: " << buffer << std::endl;
    
            // 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待.
            if (tcpClient.recv(buffer, 1024) == false)
            {
                perror("recv failed.");
                break;
            }
            std::cout << "接收: " << buffer << std::endl;
    
            sleep(1);
        }
    
        return 0;
    }
  • 基于TCP协议的服务端通信

    c++ 复制代码
    // tcp_server.cpp - 基于TCP协议的服务端通信.
    
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <unistd.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    class TCPServer // TCP通讯的服务端类.
    {
    private:
        int listen_fd;         // 监听的socket,-1表示未初始化.
        int client_fd;         // 客户端连上来的socket,-1表示客户端未连接.
        std::string client_ip; // 客户端字符串格式的IP.
        unsigned short port;   // 服务端用于通讯的端口.
    
    public:
        TCPServer() : listen_fd(-1), client_fd(-1) {}
    
        // 初始化服务端用于监听的socket.
        bool initServer(const unsigned short in_port)
        {
            // 第1步:创建服务端的socket.
            if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
                return false;
    
            port = in_port;
    
            // 第2步:把服务端用于通信的IP和端口绑定到socket上.
            struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体.
            memset(&servaddr, 0, sizeof(servaddr));
            servaddr.sin_family = AF_INET;                // ①协议族,固定填AF_INET.
            servaddr.sin_port = htons(port);              // ②指定服务端的通信端口.
            servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯.
    
            // 绑定服务端的IP和端口(为socket分配IP和端口).
            if (bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
            {
                close(listen_fd);
                listen_fd = -1;
                return false;
            }
    
            // 第3步:把socket设置为可连接(监听)的状态.
            if (listen(listen_fd, 5) == -1)
            {
                close(listen_fd);
                listen_fd = -1;
                return false;
            }
    
            return true;
        }
    
        // 受理客户端的连接(从已连接的客户端中取出一个客户端),
        // 如果没有已连接的客户端,accept()函数将阻塞等待.
        bool acceptConnection()
        {
            struct sockaddr_in caddr;          // 客户端的地址信息.
            socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小.
            if ((client_fd = ::accept(listen_fd, (struct sockaddr *)&caddr, &addrlen)) == -1)
                return false;
    
            client_ip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串.
    
            return true;
        }
    
        // 获取客户端的IP(字符串格式).
        const std::string &getClientIP() const
        {
            return client_ip;
        }
    
        // 向对端发送报文,成功返回true,失败返回false.
        bool sendMessage(const std::string &buffer)
        {
            if (client_fd == -1)
                return false;
    
            if ((::send(client_fd, buffer.data(), buffer.size(), 0)) <= 0)
                return false;
    
            return true;
        }
    
        // 接收对端的报文,成功返回true,失败返回false.
        // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度.
        bool receiveMessage(std::string &buffer, const size_t maxlen)
        {
            buffer.clear();                                              // 清空容器.
            buffer.resize(maxlen);                                       // 设置容器的大小为maxlen.
            int readn = ::recv(client_fd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存.
            if (readn <= 0)
            {
                buffer.clear();
                return false;
            }
            buffer.resize(readn); // 重置buffer的实际大小.
    
            return true;
        }
    
        // 关闭监听的socket.
        bool closeListenSocket()
        {
            if (listen_fd == -1)
                return false;
    
            ::close(listen_fd);
            listen_fd = -1;
            return true;
        }
    
        // 关闭客户端连上来的socket.
        bool closeClientSocket()
        {
            if (client_fd == -1)
                return false;
    
            ::close(client_fd);
            client_fd = -1;
            return true;
        }
    
        ~TCPServer()
        {
            closeListenSocket();
            closeClientSocket();
        }
    };
    
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            std::cout << "using: ./tcp_server <通讯端口>" << std::endl
                      << "example: ./ tcp_server 5005" << std::endl; // 端口大于1024,不与其它的重复.
            return -1;
        }
    
        TCPServer tcpServer;
        if (tcpServer.initServer(std::atoi(argv[1])) == false) // 初始化服务端用于监听的socket.
        {
            perror("initServer failed");
            return -1;
        }
    
        // 受理客户端的连接(从已连接的客户端中取出一个客户端),
        // 如果没有已连接的客户端,accept()函数将阻塞等待.
        if (tcpServer.acceptConnection() == false)
        {
            perror("acceptConnection failed.");
            return -1;
        }
        std::cout << "客户端已连接( " << tcpServer.getClientIP() << " )." << std::endl;
    
        std::string buffer;
        while (true)
        {
            // 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待.
            if (tcpServer.receiveMessage(buffer, 1024) == false)
            {
                perror("receiveMessage failed.");
                break;
            }
            std::cout << "接收: " << buffer << std::endl;
    
            buffer = "ok";
            if (tcpServer.sendMessage(buffer) == false) // 向对端发送报文.
            {
                perror("sendMessage failed.");
                break;
            }
            std::cout << "发送: " << buffer << std::endl;
        }
    
        return 0;
    }

23.多进程的网络服务端

  • 示例:

    c++ 复制代码
    // multiprocess_tcpserver.cpp - 基于TCP协议的服务端通信,支持多客户端连接.
    
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <unistd.h>
    #include <netdb.h>
    #include <signal.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    class ctcpserver // TCP通讯的服务端类.
    {
    private:
        int m_listenfd;         // 监听的socket,-1表示未初始化.
        int m_clientfd;         // 客户端连上来的socket,-1表示客户端未连接.
        std::string m_clientip; // 客户端字符串格式的IP.
        unsigned short m_port;  // 服务端用于通讯的端口.
    
    public:
        ctcpserver() : m_listenfd(-1), m_clientfd(-1) {}
    
        // 初始化服务端用于监听的socket.
        bool initserver(const unsigned short in_port)
        {
            // 第1步:创建服务端的socket.
            if ((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
                return false;
    
            m_port = in_port;
    
            // 第2步:把服务端用于通信的IP和端口绑定到socket上.
            struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体.
            memset(&servaddr, 0, sizeof(servaddr));
            servaddr.sin_family = AF_INET;                // ①协议族,固定填AF_INET.
            servaddr.sin_port = htons(m_port);            // ②指定服务端的通信端口.
            servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯.
    
            // 绑定服务端的IP和端口(为socket分配IP和端口).
            if (bind(m_listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
            {
                close(m_listenfd);
                m_listenfd = -1;
                return false;
            }
    
            // 第3步:把socket设置为可连接(监听)的状态.
            if (listen(m_listenfd, 5) == -1)
            {
                close(m_listenfd);
                m_listenfd = -1;
                return false;
            }
    
            return true;
        }
    
        // 受理客户端的连接(从已连接的客户端中取出一个客户端),
        // 如果没有已连接的客户端,accept()函数将阻塞等待.
        bool accept()
        {
            struct sockaddr_in caddr;          // 客户端的地址信息.
            socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小.
            if ((m_clientfd = ::accept(m_listenfd, (struct sockaddr *)&caddr, &addrlen)) == -1)
                return false;
    
            m_clientip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串.
    
            return true;
        }
    
        // 获取客户端的IP(字符串格式).
        const std::string &clientip() const
        {
            return m_clientip;
        }
    
        // 向对端发送报文,成功返回true,失败返回false.
        bool send(const std::string &buffer)
        {
            if (m_clientfd == -1)
                return false;
    
            if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0)
                return false;
    
            return true;
        }
    
        // 接收对端的报文,成功返回true,失败返回false.
        // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度.
        bool recv(std::string &buffer, const size_t maxlen)
        {
            buffer.clear();                                               // 清空容器.
            buffer.resize(maxlen);                                        // 设置容器的大小为maxlen.
            int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存.
            if (readn <= 0)
            {
                buffer.clear();
                return false;
            }
            buffer.resize(readn); // 重置buffer的实际大小.
    
            return true;
        }
    
        // 关闭监听的socket.
        bool closelisten()
        {
            if (m_listenfd == -1)
                return false;
    
            ::close(m_listenfd);
            m_listenfd = -1;
            return true;
        }
    
        // 关闭客户端连上来的socket.
        bool closeclient()
        {
            if (m_clientfd == -1)
                return false;
    
            ::close(m_clientfd);
            m_clientfd = -1;
            return true;
        }
    
        ~ctcpserver()
        {
            closelisten();
            closeclient();
        }
    };
    
    ctcpserver tcpserver;
    
    void FatherEXIT(int sig); // 父进程的信号处理函数.
    void ChildEXIT(int sig);  // 子进程的信号处理函数.
    
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            std::cout << "using: ./muitilprocess_tcpserver 通讯端口" << std::endl
                      << "example: ./muitilprocess_tcpserver 5005" << std::endl;
            return -1;
        }
    
        // 忽略全部的信号,不希望被打扰.顺便解决了僵尸进程的问题.
        for (int ii = 1; ii <= 64; ii++)
            signal(ii, SIG_IGN);
    
        // 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程
        // 但请不要用 "kill -9 +进程号" 强行终止
        signal(SIGTERM, FatherEXIT);
        signal(SIGINT, FatherEXIT); // SIGTERM 15 SIGINT 2
    
        if (tcpserver.initserver(atoi(argv[1])) == false) // 初始化服务端用于监听的socket.
        {
            perror("initserver failed.");
            return -1;
        }
    
        while (true)
        {
            // 受理客户端的连接(从已连接的客户端中取出一个客户端),
            // 如果没有已连接的客户端,accept()函数将阻塞等待.
            if (tcpserver.accept() == false)
            {
                perror("accept failed.");
                return -1;
            }
    
            int pid = fork();
            if (pid == -1)
            {
                perror("fork failed.");
                return -1;
            } // 系统资源不足.
            if (pid > 0)
            {                            // 父进程.
                tcpserver.closeclient(); // 父进程关闭客户端连接的socket.
                continue;                // 父进程返回到循环开始的位置,继续受理客户端的连接.
            }
    
            tcpserver.closelisten(); // 子进程关闭监听的socket.
    
            // 子进程需要重新设置信号.
            signal(SIGTERM, ChildEXIT); // 子进程的退出函数与父进程不一样.
            signal(SIGINT, SIG_IGN);    // 子进程不需要捕获SIGINT信号.
    
            // 子进程负责与客户端进行通讯.
            std::cout << "客户端已连接( " << tcpserver.clientip() << " )." << std::endl;
    
            std::string buffer;
            while (true)
            {
                // 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待.
                if (tcpserver.recv(buffer, 1024) == false)
                {
                    perror("recv()");
                    break;
                }
                std::cout << "接收: " << buffer << std::endl;
    
                buffer = "ok";
                if (tcpserver.send(buffer) == false) // 向对端发送报文.
                {
                    perror("send");
                    break;
                }
                std::cout << "发送: " << buffer << std::endl;
            }
    
            return 0; // 子进程一定要退出,否则又会回到accept()函数的位置.
        }
    }
    
    // 父进程的信号处理函数.
    void FatherEXIT(int sig)
    {
        // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断.
        signal(SIGINT, SIG_IGN);
        signal(SIGTERM, SIG_IGN);
    
        std::cout << "父进程退出,sig = " << sig << std::endl;
    
        kill(0, SIGTERM); // 向全部的子进程发送15的信号,通知它们退出.
    
        // 在这里增加释放资源的代码(全局的资源).
        tcpserver.closelisten(); // 父进程关闭监听的socket.
    
        exit(0);
    }
    
    // 子进程的信号处理函数.
    void ChildEXIT(int sig)
    {
        // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断.
        signal(SIGINT, SIG_IGN);
        signal(SIGTERM, SIG_IGN);
    
        std::cout << "子进程: " << getpid() << "退出,sig = " << sig << std::endl;
    
        // 在这里增加释放资源的代码(只释放子进程的资源).
        tcpserver.closeclient(); // 子进程关闭客户端连上来的socket.
    
        exit(0);
    }

24.实现文件传输功能

  • 实现文件传输的客户端

    c++ 复制代码
    #include <iostream>
    #include <fstream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <unistd.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    class ctcpclient // TCP通讯的客户端类.
    {
    private:
        int m_clientfd;        // 客户端的socket,-1表示未连接或连接已断开;>=0表示有效的socket.
        std::string m_ip;      // 服务端的IP/域名.
        unsigned short m_port; // 通讯端口.
    
    public:
        ctcpclient() : m_clientfd(-1) {}
    
        // 向服务端发起连接请求,成功返回true,失败返回false.
        bool connect(const std::string &in_ip, const unsigned short in_port)
        {
            if (m_clientfd != -1)
                return false; // 如果socket已连接,直接返回失败.
    
            m_ip = in_ip;
            m_port = in_port; // 把服务端的IP和端口保存到成员变量中.
    
            // 第1步:创建客户端的socket.
            if ((m_clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
                return false;
    
            // 第2步:向服务器发起连接请求.
            struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体.
            memset(&servaddr, 0, sizeof(servaddr));
            servaddr.sin_family = AF_INET;     // ①协议族,固定填AF_INET.
            servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口.
    
            struct hostent *h;                                // 用于存放服务端IP地址(大端序)的结构体的指针.
            if ((h = gethostbyname(m_ip.c_str())) == nullptr) // 把域名/主机名/字符串格式的IP转换成结构体.
            {
                ::close(m_clientfd);
                m_clientfd = -1;
                return false;
            }
            memcpy(&servaddr.sin_addr, h->h_addr, h->h_length); // ③指定服务端的IP(大端序).
    
            // 向服务端发起连接请求.
            if (::connect(m_clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
            {
                ::close(m_clientfd);
                m_clientfd = -1;
                return false;
            }
    
            return true;
        }
    
        // 向服务端发送报文(字符串),成功返回true,失败返回false.
        bool send(const std::string &buffer) // buffer不要用const char *
        {
            if (m_clientfd == -1)
                return false; // 如果socket的状态是未连接,直接返回失败.
    
            if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0)
                return false;
    
            return true;
        }
    
        // 向服务端发送报文(二进制数据),成功返回true,失败返回false.
        bool send(void *buffer, const size_t size)
        {
            if (m_clientfd == -1)
                return false; // 如果socket的状态是未连接,直接返回失败.
    
            if ((::send(m_clientfd, buffer, size, 0)) <= 0)
                return false;
    
            return true;
        }
    
        // 接收服务端的报文,成功返回true,失败返回false.
        // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度.
        bool recv(std::string &buffer, const size_t maxlen)
        {
            buffer.clear();                                               // 清空容器.
            buffer.resize(maxlen);                                        // 设置容器的大小为maxlen.
            int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存.
            if (readn <= 0)
            {
                buffer.clear();
                return false;
            }
            buffer.resize(readn); // 重置buffer的实际大小.
    
            return true;
        }
    
        // 断开与服务端的连接.
        bool close()
        {
            if (m_clientfd == -1)
                return false; // 如果socket的状态是未连接,直接返回失败.
    
            ::close(m_clientfd);
            m_clientfd = -1;
            return true;
        }
    
        // 向服务端发送文件内容.
        bool sendfile(const std::string &filename, const size_t filesize)
        {
            // 以二进制的方式打开文件.
            std::ifstream fin(filename, std::ios::binary);
            if (fin.is_open() == false)
            {
                std::cout << "打开文件: " << filename << " 失败." << std::endl;
                return false;
            }
    
            int onread = 0;     // 每次调用fin.read()时打算读取的字节数.
            int totalbytes = 0; // 从文件中已读取的字节总数.
            char buffer[4096];  // 存放读取数据的buffer.
    
            while (true)
            {
                memset(buffer, 0, sizeof(buffer));
    
                // 计算本次应该读取的字节数,如果剩余的数据超过4096字节,就读4096字节.
                if (filesize - totalbytes > 4096)
                    onread = 4096;
                else
                    onread = filesize - totalbytes;
    
                // 从文件中读取数据.
                fin.read(buffer, onread);
    
                // 把读取到的数据发送给对端.
                if (send(buffer, onread) == false)
                    return false;
    
                // 计算文件已读取的字节总数,如果文件已读完,跳出循环.
                totalbytes += onread;
    
                if (totalbytes == filesize)
                    break;
            }
    
            return true;
        }
    
        ~ctcpclient() { close(); }
    };
    
    int main(int argc, char *argv[])
    {
        if (argc != 5)
        {
            std::cout << "using: ./sendfile_tcpclient 服务端的IP 服务端的端口 文件名 文件大小" << std::endl;
            std::cout << "example: ./sendfile_tcpclient 192.168.101.138 5005 test.txt 2424" << std::endl
                      << std::endl;
            return -1;
        }
    
        ctcpclient tcpclient;
        if (tcpclient.connect(argv[1], atoi(argv[2])) == false) // 向服务端发起连接请求.
        {
            perror("connect failed.");
            return -1;
        }
    
        // 以下是发送文件的流程.
        // 1)把待传输文件名和文件的大小告诉服务端.
        // 定义文件信息的结构体.
        struct st_fileinfo
        {
            char filename[256]; // 文件名.
            int filesize;       // 文件大小.
        } fileinfo;
        memset(&fileinfo, 0, sizeof(fileinfo));
        strncpy(fileinfo.filename, argv[3], sizeof(fileinfo.filename) - 1); // 文件名.
        fileinfo.filesize = atoi(argv[4]);                                  // 文件大小.
    
        // 把文件信息的结构体发送给服务端.
        if (tcpclient.send(&fileinfo, sizeof(fileinfo)) == false)
        {
            perror("send failed.");
            return -1;
        }
        std::cout << "发送文件信息的结构体: " << fileinfo.filename << " ( " << fileinfo.filesize << " )." << std::endl;
    
        // 2)等待服务端的确认报文(文件名和文件的大小的确认).
        std::string buffer;
        if (tcpclient.recv(buffer, 2) == false)
        {
            perror("recv failed.");
            return -1;
        }
        if (buffer != "ok")
        {
            std::cout << "服务端没有回复ok." << std::endl;
            return -1;
        }
    
        // 3)发送文件内容.
        if (tcpclient.sendfile(fileinfo.filename, fileinfo.filesize) == false)
        {
            perror("sendfile failed.");
            return -1;
        }
    
        // 4)等待服务端的确认报文(服务端已接收完文件).
        if (tcpclient.recv(buffer, 2) == false)
        {
            perror("recv failed.");
            return -1;
        }
        if (buffer != "ok")
        {
            std::cout << "发送文件内容失败." << std::endl;
            return -1;
        }
    
        std::cout << "发送文件内容成功." << std::endl;
        return 0;
    }
  • 实现文件传输的服务端

    c++ 复制代码
    #include <iostream>
    #include <fstream>
    #include <cstdio>
    #include <cstring>
    #include <cstdlib>
    #include <unistd.h>
    #include <netdb.h>
    #include <signal.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    class ctcpserver // TCP通讯的服务端类.
    {
    private:
        int m_listenfd;         // 监听的socket,-1表示未初始化.
        int m_clientfd;         // 客户端连上来的socket,-1表示客户端未连接.
        std::string m_clientip; // 客户端字符串格式的IP.
        unsigned short m_port;  // 服务端用于通讯的端口.
    public:
        ctcpserver() : m_listenfd(-1), m_clientfd(-1) {}
    
        // 初始化服务端用于监听的socket.
        bool initserver(const unsigned short in_port)
        {
            // 第1步:创建服务端的socket.
            if ((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
                return false;
    
            m_port = in_port;
    
            // 第2步:把服务端用于通信的IP和端口绑定到socket上.
            struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体.
            memset(&servaddr, 0, sizeof(servaddr));
            servaddr.sin_family = AF_INET;                // ①协议族,固定填AF_INET.
            servaddr.sin_port = htons(m_port);            // ②指定服务端的通信端口.
            servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯.
    
            // 绑定服务端的IP和端口(为socket分配IP和端口).
            if (bind(m_listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
            {
                close(m_listenfd);
                m_listenfd = -1;
                return false;
            }
    
            // 第3步:把socket设置为可连接(监听)的状态.
            if (listen(m_listenfd, 5) == -1)
            {
                close(m_listenfd);
                m_listenfd = -1;
                return false;
            }
    
            return true;
        }
    
        // 受理客户端的连接(从已连接的客户端中取出一个客户端),
        // 如果没有已连接的客户端,accept()函数将阻塞等待.
        bool accept()
        {
            struct sockaddr_in caddr;          // 客户端的地址信息.
            socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小.
            if ((m_clientfd = ::accept(m_listenfd, (struct sockaddr *)&caddr, &addrlen)) == -1)
                return false;
    
            m_clientip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串.
    
            return true;
        }
    
        // 获取客户端的IP(字符串格式).
        const std::string &clientip() const
        {
            return m_clientip;
        }
    
        // 向对端发送报文,成功返回true,失败返回false.
        bool send(const std::string &buffer)
        {
            if (m_clientfd == -1)
                return false;
    
            if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0)
                return false;
    
            return true;
        }
    
        // 接收对端的报文(字符串),成功返回true,失败返回false.
        // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度.
        bool recv(std::string &buffer, const size_t maxlen)
        {
            buffer.clear();                                               // 清空容器.
            buffer.resize(maxlen);                                        // 设置容器的大小为maxlen.
            int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存.
            if (readn <= 0)
            {
                buffer.clear();
                return false;
            }
            buffer.resize(readn); // 重置buffer的实际大小.
    
            return true;
        }
    
        // 接收客户端的报文(二进制数据),成功返回true,失败返回false.
        // buffer-存放接收到的报文的内容,size-本次接收报文的最大长度.
        bool recv(void *buffer, const size_t size)
        {
            if (::recv(m_clientfd, buffer, size, 0) <= 0)
                return false;
    
            return true;
        }
    
        // 关闭监听的socket.
        bool closelisten()
        {
            if (m_listenfd == -1)
                return false;
    
            ::close(m_listenfd);
            m_listenfd = -1;
            return true;
        }
    
        // 关闭客户端连上来的socket.
        bool closeclient()
        {
            if (m_clientfd == -1)
                return false;
    
            ::close(m_clientfd);
            m_clientfd = -1;
            return true;
        }
    
        // 接收文件内容.
        bool recvfile(const std::string &filename, const size_t filesize)
        {
            std::ofstream fout;
            fout.open(filename, std::ios::binary);
            if (fout.is_open() == false)
            {
                std::cout << "Failed to open file: " << filename << "." << std::endl;
                return false;
            }
    
            int totalbytes = 0; // 已接收文件的总字节数.
            int onread = 0;     // 本次打算接收的字节数.
            char buffer[4096];  // 接收文件内容的缓冲区.
    
            while (true)
            {
                // 计算本次应该接收的字节数.
                if (filesize - totalbytes > 4096)
                    onread = 4096;
                else
                    onread = filesize - totalbytes;
    
                // 接收文件内容.
                if (recv(buffer, onread) == false)
                    return false;
    
                // 把接收到的内容写入文件.
                fout.write(buffer, onread);
    
                // 计算已接收文件的总字节数,如果文件接收完,跳出循环.
                totalbytes = totalbytes + onread;
    
                if (totalbytes == filesize)
                    break;
            }
    
            return true;
        }
    
        ~ctcpserver()
        {
            closelisten();
            closeclient();
        }
    };
    
    ctcpserver tcpserver;
    
    void FatherEXIT(int sig); // 父进程的信号处理函数.
    void ChildEXIT(int sig);  // 子进程的信号处理函数.
    
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            std::cout << "using: ./sendfile_tcpserver 通讯端口 文件存放的目录" << std::endl;
            std::cout << "example: ./sendfile_tcpserver 5005 /tmp" << std::endl
                      << std::endl;
            return -1;
        }
    
        // 忽略全部的信号,不希望被打扰.顺便解决了僵尸进程的问题.
        for (int ii = 1; ii <= 64; ii++)
            signal(ii, SIG_IGN);
    
        // 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程
        // 但请不要用 "kill -9 +进程号" 强行终止
        signal(SIGTERM, FatherEXIT);
        signal(SIGINT, FatherEXIT); // SIGTERM 15 SIGINT 2
    
        if (tcpserver.initserver(atoi(argv[1])) == false) // 初始化服务端用于监听的socket.
        {
            perror("initserver failed.");
            return -1;
        }
    
        while (true)
        {
            // 受理客户端的连接(从已连接的客户端中取出一个客户端),
            // 如果没有已连接的客户端,accept()函数将阻塞等待.
            if (tcpserver.accept() == false)
            {
                perror("accept failed.");
                return -1;
            }
    
            int pid = fork();
            if (pid == -1)
            {
                perror("fork failed.");
                return -1;
            } // 系统资源不足.
            if (pid > 0)
            {                            // 父进程.
                tcpserver.closeclient(); // 父进程关闭客户端连接的socket.
                continue;                // 父进程返回到循环开始的位置,继续受理客户端的连接.
            }
    
            tcpserver.closelisten(); // 子进程关闭监听的socket.
    
            // 子进程需要重新设置信号.
            signal(SIGTERM, ChildEXIT); // 子进程的退出函数与父进程不一样.
            signal(SIGINT, SIG_IGN);    // 子进程不需要捕获SIGINT信号.
    
            // 子进程负责与客户端进行通讯.
            std::cout << "Client connected: ( " << tcpserver.clientip() << " )." << std::endl;
    
            // 以下是接收文件的流程.
            // 1)接收文件名和文件大小信息.
            // 定义文件信息的结构体.
            struct st_fileinfo
            {
                char filename[256]; // 文件名.
                int filesize;       // 文件大小.
            } fileinfo;
            memset(&fileinfo, 0, sizeof(fileinfo));
            // 用结构体存放接收报文的内容.
            if (tcpserver.recv(&fileinfo, sizeof(fileinfo)) == false)
            {
                perror("recv()");
                return -1;
            }
            std::cout << "File info: " << fileinfo.filename << " ( " << fileinfo.filesize << " )." << std::endl;
    
            // 2)给客户端回复确认报文,表示客户端可以发送文件了.
            if (tcpserver.send("ok") == false)
            {
                perror("send failed.");
                break;
            }
    
            // 3)接收文件内容.
            if (tcpserver.recvfile(std::string(argv[2]) + "/" + fileinfo.filename, fileinfo.filesize) == false)
            {
                std::cout << "Failed to receive file content." << std::endl;
                return -1;
            }
    
            std::cout << "File content received successfully." << std::endl;
    
            // 4)给客户端回复确认报文,表示文件已接收成功.
            tcpserver.send("ok");
    
            return 0; // 子进程一定要退出,否则又会回到accept()函数的位置.
        }
    }
    
    // 父进程的信号处理函数.
    void FatherEXIT(int sig)
    {
        // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断.
        signal(SIGINT, SIG_IGN);
        signal(SIGTERM, SIG_IGN);
    
        std::cout << "Parent process exiting, sig = " << sig << std::endl;
    
        kill(0, SIGTERM); // 向全部的子进程发送15的信号,通知它们退出.
    
        // 在这里增加释放资源的代码(全局的资源).
        tcpserver.closelisten(); // 父进程关闭监听的socket.
    
        exit(0);
    }
    
    // 子进程的信号处理函数.
    void ChildEXIT(int sig)
    {
        // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断.
        signal(SIGINT, SIG_IGN);
        signal(SIGTERM, SIG_IGN);
    
        std::cout << "Child process: " << getpid() << " exiting, sig = " << sig << std::endl;
    
        // 在这里增加释放资源的代码(只释放子进程的资源).
        tcpserver.closeclient(); // 子进程关闭客户端连上来的socket.
    
        exit(0);
    }

25.三次握手与四次挥手

  • TCP是面向连接的、可靠的协议,建立TCP连接需要三次对话(三次握手),拆除TCP连接需要四次对话(四次握/挥手)。

1.三次握手

  • 服务端调用listen()函数后进入监听(等待连接)状态,这时候,客户端就可以调用connect()函数发起TCP连接请求,connect()函数会触发三次握手,三次握手完成后,客户端和服务端将建立一个双向的传输通道。
  • 情景类似:
    1. 客户端对服务端说:我可以给你发送数据吗?
    2. 服务端回复:ok,不过,我也要给你发送数据。(这时候,客户端至服务端的单向传输通道已建立)。
    3. 客户端回复:ok。(这时候,服务端至客户端的单向传输通道已建立)。
  • 细节:
    1. 客户端的socket也有端口号,对程序员来说,不必关心客户端socket的端口号,所以系统随机分配。(socket通讯中的地址包括ip和端口号,但是,习惯中的地址仅指ip地址)。
    2. 服务端的bind()函数,普通用户只能使用1024以上的端口,root用户可以使用任意端口。
    3. listen()函数的第二个参数 + 1为已连接队列(ESTABLISHED状态,三次握手已完成但是没有被accept()socket,只存在于服务端)的大小。(在高并发的服务程序中,该参数应该调大一些)
    4. SYN_RECV状态的连接也称为半连接。
    5. CLOSED是假想状态,实际上不存在。

2.四次挥手

  • 断开一个TCP连接时,客户端和服务端需要相互总共发送四个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close()函数触发。

  • 情景类似:

    1. 一端(A)对另一端(B)说:我不会给你发数据了,断开连接吧。
    2. B回复:ok。(这时候A不能对B发数据了,但是,B仍可以对A发数据)
    3. B发完数据了,对A说:我也不会给你发数据了。(这时候B也不能对A发数据了)
    4. A回复:ok。
  • 细节:

    1. 1)主动断开的端在四次挥手后,socket的状态为TIME_WAIT,该状态将持续2MSL(30秒/1分钟/2分钟)。 MSL(Maximum Segment Lifetime)报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

    2. 如果是客户端主动断开,TIME_WAIT状态的socket几乎不会造成危害。

      1. 客户端程序的socket很少,服务端程序的socket很多(成千上万);
      2. 客户端的端口是随机分配的,不存在重用的问题。
    3. 如果是服务端主动断开,有两方面的危害:

      1. socket没有立即释放;
      2. 端口号只能在2MSL后才能重用。
    4. 在服务端程序中,用setsockopt()函数设置socket的属性(一定要放在bind()之前)

      c++ 复制代码
      int opt = 1;   
      setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

26.TCP缓存

  • 系统为每个socket创建了发送缓冲区和接收缓冲区,应用程序调用send()/write()函数发送数据的时候,内核把数据从应用进程拷贝socket的发送缓冲区中;应用程序调用recv()/read()函数接收数据的时候,内核把数据从socket的接收缓冲区拷贝应用进程中。

  • 发送数据即把数据放入发送缓冲区中,接收数据即从接收缓冲区中取数据。

  • 查看socket缓存的大小:

    c++ 复制代码
    int bufsize = 0;
    socklen_t optlen = sizeof(bufsize);     
    
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, &optlen); // 获取发送缓冲区的大小。
    cout << "send bufsize = " << bufsize << endl;
    
    getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, &optlen); // 获取接收缓冲区的大小。
    cout << "recv bufsize = " << bufsize << endl;
  • 问题:

    1. send()函数有可能会阻塞吗? 如果自己的发送缓冲区和对端的接收缓冲区都满了,会阻塞。
    2. socket中写入数据后,如果关闭了socket,对端还能接收到数据吗?
      • 如果使用shutdown关闭写入方向,另一端可以接收到数据。
      • 如果直接调用close,数据接收不确定,可能会丢失。
      • 使用SO_LINGER选项,可以确保数据发送完毕后再关闭。
  • Nagle算法

    • TCP协议中,无论发送多少数据,都要在数据前面加上协议头,同时,对方收到数据后,也需要回复ACK表示确认。为了尽可能的利用网络带宽,TCP希望每次都能够以MSS(Maximum Segment Size,最大报文长度)的数据块来发送数据。

    • Nagle算法就是为了尽可能发送大块的数据,避免网络中充斥着小数据块。

    • Nagle算法的定义是:任意时刻,最多只能有一个未被确认的小段,小段是指小于MSS的数据块,未被确认是指一个数据块发送出去后,没有收到对端回复的ACK

    • 举个例子:发送端调用send()函数将一个int型数据(称之为A数据块)写入到socket中,A数据块会被马上发送到接收端,接着,发送端又调用send()函数写入一个int型数据(称之为B数据块),这时候,A块的ACK没有返回(已经存在了一个未被确认的小段),所以B块不会立即被发送,而是等A块的ACK返回之后(大概40ms)才发送。

    • TCP协议中不仅仅有Nagle算法,还有一个ACK延迟机制:当接收端收到数据之后,并不会马上向发送端回复ACK,而是延迟40ms后再回复,它希望在40ms内接收端会向发送端回复应答数据,这样ACK就可以和应答数据一起发送,把ACK捎带过去。

    • 如果TCP连接的一端启用了Nagle算法,另一端启用了ACK延时机制,而发送的数据包又比较小,则可能会出现这样的情况:发送端在等待上一个包的ACK,而接收端正好延迟了此ACK,那么这个正要被发送的包就会延迟40ms

    • 解决方案:

      • 开启TCP_NODELAY选项,这个选项的作用就是禁用Nagle算法。

        c++ 复制代码
        #include <netinet/tcp.h>   // 注意,要包含这个头文件。
        int opt = 1;   
        setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
      • 对时效要求很高的系统,例如联机游戏、证券交易,一般会禁用Nagle算法。

27.I/O多路复用

  • IO多路复用是一种用于管理多个IO操作的技术,它允许一个单独的进程或线程同时监视多个IO流(如套接字、文件描述符等),并且在其中任何一个IO流准备好进行读取、写入或连接时立即进行相应的操作,而不需要阻塞其他流。这种技术提高了系统的性能和效率,尤其适用于需要处理大量并发连接的网络服务器应用。

  • 基本概念:

    1. IO(Input/Output): 指的是计算机与外部世界进行数据交换的过程,包括读取数据、写入数据和网络通信等操作。
    2. 多路(Multiplexing): 指的是一种技术,在同一个时间段内同时处理多个IO操作。
    3. 复用(Multiplexing): 指的是使用一种机制同时监视多个IO流,以便在有数据可读、可写或有连接请求时立即做出响应。
  • 工作原理:

    • IO多路复用通常基于操作系统提供的系统调用实现,如select()poll()epoll()等。
    1. select(): 最古老的IO多路复用机制,在一个或多个IO流上进行监视,当有IO流准备好读取、写入或连接时,select()函数会立即返回。但是,它存在一些性能和可扩展性问题,特别是在处理大量连接时。
    2. poll(): 类似于select(),但是没有文件描述符数目的限制,使用数组来存储待监视的文件描述符。
    3. epoll(): 是Linux特有的高性能IO多路复用机制,使用红黑树(epoll_create()创建的实例)或者哈希表(epoll_create1()创建的实例)来管理待监视的文件描述符。相比于select()poll()epoll()在处理大量连接时表现更优秀,因为它避免了遍历整个文件描述符集合的开销。
  • 优点:

    1. 高效: IO多路复用技术允许程序同时监视多个IO操作,而不需要创建多个线程或进程,因此可以降低系统开销。
    2. 可扩展: 在处理大量连接时,IO多路复用技术的性能表现更优秀,相比于多线程或多进程模型更容易扩展。
    3. 简单: 使用系统提供的API(如select()poll()epoll())可以相对容易地实现IO多路复用功能。
  • 适用场景:

    1. 高并发网络服务器: 如Web服务器、聊天服务器等需要同时处理大量连接的应用。
    2. 实时数据处理: 需要及时响应外部事件、传感器数据等的应用,如即时通讯、实时监控等。
  • 总结:

    • IO多路复用技术是一种高效、可扩展的IO操作管理方式,适用于需要处理大量并发IO操作的网络服务器和实时数据处理应用。通过合理地选择适合自身需求的IO多路复用机制,并结合非阻塞IO技术,可以提高系统的性能、可靠性和扩展性。
  • 多进程服务器的缺点和解决办法:

    • 多进程服务器的缺点和解决办法
      1. 资源消耗高: 每个客户端连接都需要创建一个新的进程,这会消耗大量的系统资源,包括内存、CPU时间和文件描述符等。
      2. 并发连接数受限: 操作系统对于进程的数量有一定的限制,当同时有大量客户端连接时,可能会导致无法创建更多的进程,从而限制了服务器的并发连接数。
      3. 进程切换开销大: 进程切换涉及到上下文的保存和恢复,会引入较大的开销,尤其在进程数量较多时,这种开销会明显增加。
      4. 同步与通信困难: 不同进程之间的通信通常需要使用IPC(Inter-Process Communication)机制,如管道、消息队列、信号量等,这增加了开发和维护的复杂度,容易引入死锁、竞态条件等问题。
    • 解决多进程服务器模型的缺点,可以采用以下方法:
      1. 使用多线程代替多进程: 多线程模型相比多进程模型,线程的创建和切换开销较小,而且线程共享同一地址空间,通信更加简单高效。但需要注意线程安全问题。
      2. 使用进程池: 提前创建一定数量的进程,并将它们放入一个进程池中。当有新的连接请求到来时,从进程池中取出一个空闲的进程处理,这样可以避免频繁创建和销毁进程的开销。
      3. 优化进程间通信: 合理使用IPC机制,选择合适的通信方式,并对通信进行精心设计,以减少不必要的同步开销和数据拷贝开销。
      4. 使用异步IO: 异步IO模型能够在单个线程中管理多个IO操作,避免了进程或线程创建的开销,同时提高了系统的吞吐量和响应速度。通过事件驱动的方式,使得服务器能够高效处理大量并发连接。
      5. 采用单进程多路复用模型: 使用IO多路复用技术(如select()poll()epoll()等),在单个进程中管理多个连接,从而减少了进程数量,降低了系统的开销,并提高了系统的并发性能。
    • 综上所述,通过合理的设计和技术选择,可以有效地克服多进程服务器模型的缺点,提高服务器的性能、可靠性和可扩展性。

1.Select模型以及实战案例

  • Select模型具体步骤

    1. 准备文件描述符(FDs) :在调用select()之前,需要准备要监视的文件描述符(FDs),这些FDs可以是套接字、文件或任何其他类型的I/O流。

    2. 初始化fd_sets :创建三个fd_set对象:readfdswritefdsexceptfds,它们分别表示要监视的读、写和异常事件的FD集合。

    3. 设置FDs在fd_sets中

      使用FD_ZERO()来清除每个fd_set对象。

      使用FD_SET()将要监视的FD添加到相应的fd_set中。

    4. 设置超时(可选) :可选地指定超时值以限制select()等待事件的时间。如果不想指定超时,可以传递NULL

    5. 调用Select :调用select()函数,传入任何一个集合中最高编号的FD加1,以及读、写和异常事件的fd_set对象,以及可选的超时值。

    6. 检查返回值select()将返回就绪并包含在集合中(readfdswritefdsexceptfds)的FD的总数。如果返回0,则表示发生超时。如果返回-1,则表示发生错误。

    7. 检查FDs的事件

      select()返回后,需要遍历fd_set对象,并检查哪些FD准备好了读取、写入,或者有异常。

      使用FD_ISSET()来检查特定的FD是否在集合中。

    8. 处理事件:处理就绪FD的I/O事件。例如,如果一个FD准备好读取,则从中读取数据。如果一个FD准备好写入,则向其写入数据。如果一个FD有异常,则相应地处理异常。

    9. 重复或退出:处理事件后可以通过返回第2步来重复这个过程,或者如果完成了,退出程序。

    10. 清理(可选) :根据需要清理资源,例如关闭FDs或重置fd_set对象。

  • 包含头文件:

    c++ 复制代码
    #include <sys/select.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>
  • 函数声明:

    c++ 复制代码
    /* `fd_set' 的访问宏.  */
    #define	FD_SET(fd, fdsetp)	__FD_SET (fd, fdsetp)
    #define	FD_CLR(fd, fdsetp)	__FD_CLR (fd, fdsetp)
    #define	FD_ISSET(fd, fdsetp)	__FD_ISSET (fd, fdsetp)
    #define	FD_ZERO(fdsetp)		__FD_ZERO (fdsetp)
    
    #define __FD_SET(d, set) \
      ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)))
    #define __FD_CLR(d, set) \
      ((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d)))
    #define __FD_ISSET(d, set) \
      ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0)
    
    # define __FD_ZERO(fdsp) \
      do {									      \
        int __d0, __d1;							      \
        __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS			      \
    			  : "=c" (__d0), "=D" (__d1)			      \
    			  : "a" (0), "0" (sizeof (fd_set)		      \
    					  / sizeof (__fd_mask)),	      \
    			    "1" (&__FDS_BITS (fdsp)[0])			      \
    			  : "memory");					      \
      } while (0)
  • 参数说明:

    • FD_SET(fd, fdsetp):在参数fdsetp指向的变量中注册文件描述符fd的信息。
    • FD_CLR(fd, fdsetp):从参数fdsetp指向的变量中清除文件描述符fd的信息。
    • FD_ISSET(fd, fdsetp):若参数fdsetp指向的变量中包含文件描述符fd的信息,则返回"真"。
    • FD_ZERO(fdsetp):将fdsetp变量的所有位初始化为0
  • select()函数:

    c++ 复制代码
    /* 检查 READFDS 中的第一个 NFDS 描述符(如果不是NULL)是否为读
       在WRITEFDS(如果不是NULL)中表示写准备情况, 在EXCEPTFDS中表示写准备情况
       (如果不是NULL)用于特殊情况.  如果 TIMEOUT 不为 NULL, 则
       在等待其中指定的时间间隔后超时.  返回就绪的文件描述符的数量, 或 -1 表示错误.
       这个函数是一个消去点,因此没有标记 __THROW.  */
    extern int select (int __nfds, fd_set *__restrict __readfds,
    		   fd_set *__restrict __writefds,
    		   fd_set *__restrict __exceptfds,
    		   struct timeval *__restrict __timeout);
    • 成功时返回大于0的值,失败时返回-1
  • 参数说明:

    • __nfds:监视对象文件描述符数量;
    • __readfds:用于检查可读性;
    • __writefds:用于检查可写性;
    • __exceptfds:用于检查带外数据;
    • __timeout:一个指向timeval结构体的指针,用于决定select等待I/O的最长时间,如果为空会一直等待。
  • 示例:

    • 服务端:

      c++ 复制代码
      #include <iostream>
      #include <cstring>
      #include <cstdlib>
      #include <unistd.h>
      #include <arpa/inet.h>
      #include <sys/socket.h>
      #include <sys/select.h>
      
      #define BUF_SIZE 100
      
      void error_handling(const char *message);
      
      int main(int argc, char *argv[])
      {
          int serv_sock, clnt_sock;
          struct sockaddr_in serv_adr, clnt_adr;
          socklen_t adr_sz;
          int str_len, fd_num, i;
          char buf[BUF_SIZE];
      
          if (argc != 2)
          {
              std::cout << "using: " << argv[0] << " <port>" << std::endl;
              exit(1);
          }
      
          serv_sock = socket(PF_INET, SOCK_STREAM, 0);
          if (serv_sock == -1)
              error_handling("socket() error");
      
          memset(&serv_adr, 0, sizeof(serv_adr));
          serv_adr.sin_family = AF_INET;
          serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
          serv_adr.sin_port = htons(atoi(argv[1]));
      
          if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
              error_handling("bind() error");
      
          if (listen(serv_sock, 5) == -1)
              error_handling("listen() error");
      
          fd_set reads, cpy_reads;
          FD_ZERO(&reads);
          FD_SET(serv_sock, &reads);
          int fd_max = serv_sock;
      
          while (1)
          {
              cpy_reads = reads;
              struct timeval timeout;
              timeout.tv_sec = 5;
              timeout.tv_usec = 5000;
      
              if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
                  break;
      
              if (fd_num == 0)
                  continue;
      
              for (i = 0; i < fd_max + 1; i++)
              {
                  if (FD_ISSET(i, &cpy_reads))
                  {
                      if (i == serv_sock)
                      { // 连接请求
                          adr_sz = sizeof(clnt_adr);
                          clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                          FD_SET(clnt_sock, &reads);
                          if (fd_max < clnt_sock)
                              fd_max = clnt_sock;
                          std::cout << "connected client: " << clnt_sock << std::endl;
                      }
                      else
                      { // Read message!
                          str_len = read(i, buf, BUF_SIZE);
                          if (str_len == 0)
                          { // Close request!
                              FD_CLR(i, &reads);
                              close(i);
                              std::cout << "closed client: " << i << std::endl;
                          }
                          else
                          {
                              write(i, buf, str_len); // Echo!
                          }
                      }
                  }
              }
          }
          close(serv_sock);
          return 0;
      }
      
      void error_handling(const char *message)
      {
          std::cerr << message << std::endl;
          exit(1);
      }
    • 客户端:

      c++ 复制代码
      #include <iostream>
      #include <cstring>
      #include <cstdlib>
      #include <unistd.h>
      #include <arpa/inet.h>
      #include <sys/socket.h>
      
      #define BUF_SIZE 1024
      
      void error_handling(const char *message);
      
      int main(int argc, char *argv[])
      {
          int sock;
          char message[BUF_SIZE];
          int str_len;
          struct sockaddr_in serv_adr;
      
          if (argc != 3)
          {
              std::cout << "Usage : " << argv[0] << " <IP> <port>" << std::endl;
              exit(1);
          }
      
          sock = socket(PF_INET, SOCK_STREAM, 0);
          if (sock == -1)
              error_handling("socket() error");
      
          memset(&serv_adr, 0, sizeof(serv_adr));
          serv_adr.sin_family = AF_INET;
          serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
          serv_adr.sin_port = htons(atoi(argv[2]));
      
          if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
              error_handling("connect() error!");
          else
              std::cout << "Connected..." << std::endl;
      
          while (1)
          {
              std::cout << "Input message (Q to quit): " << std::endl;
              fgets(message, BUF_SIZE, stdin);
      
              if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
                  break;
      
              write(sock, message, strlen(message));
              str_len = read(sock, message, BUF_SIZE - 1);
              message[str_len] = '\0';
              std::cout << "Message from server: " << message << std::endl;
          }
      
          close(sock);
          return 0;
      }
      
      void error_handling(const char *message)
      {
          std::cerr << message << std::endl;
          exit(1);
      }
    • 理解select()函数:

      1. 是否存在套接字接收数据?
        • 通过检查可读事件集合(readfds)来确定是否存在套接字可以接收数据。如果在调用 select() 后发现某个套接字在可读事件集合中,则表示该套接字可以接收数据。
      2. 无需阻塞传输数据的套接字有哪些?
        • 无需阻塞传输数据的套接字包括在可写事件集合(writefds)中的套接字。如果在调用 select() 后发现某个套接字在可写事件集合中,则表示该套接字可以立即向对端传输数据,而不会阻塞。
      3. 哪些套接字发生了异常?
        • 通过检查异常事件集合(exceptfds)来确定哪些套接字发生了异常。如果在调用 select() 后发现某个套接字在异常事件集合中,则表示该套接字发生了异常情况,可能需要关闭或处理。

2.Epoll模型

  • Select模型的缺点:

    1. 效率低下: Select 模型采用了轮询的方式来检查多个文件描述符的状态变化,当文件描述符数量增加时,需要不断遍历检查,导致性能下降。特别是当需要监视的文件描述符数量较大时,Select 的效率会显著降低。
    2. 文件描述符数量限制: 在很多操作系统中,Select 函数所能监视的文件描述符数量是有限制的,一般情况下,这个限制是固定的,例如1024或者更小。这意味着如果要同时处理大量的连接或者文件描述符,Select 就无法满足需求。
    3. 复制文件描述符集: 每次调用 Select 函数都需要传递一份文件描述符集的副本,这意味着当文件描述符数量非常大时,会产生较大的额外开销,包括内存和时间。
    4. 不支持跨平台: Select 函数在不同的操作系统上可能存在一些差异,而且有些操作系统并不支持 Select 函数,例如 Windows 下没有 Select 函数,而是使用了类似的函数如 WSAPoll 或者 WSAWaitForMultipleEvents。
    5. 不方便扩展: Select 模型的接口设计较为简单,不支持更复杂的事件处理,例如异步IO等。在需要处理更复杂场景的时候,Select 模型的扩展能力相对较弱。
    • 综上所述,虽然 Select 模型在一定程度上简单易用,并且适用于少量文件描述符的情况,但是在高并发场景下,效率和性能上存在一定的局限性,因此在实际开发中需要根据具体的应用场景选择合适的 IO 复用模型。
  • Epoll的三大函数:

    1. epoll_create
    2. epoll_wait
    3. epoll_ctl
  • 包含头文件:

    c++ 复制代码
    #include <sys/epoll.h>
  • 函数声明:

    c++ 复制代码
    /* 创建 epoll 实例.  返回新实例的 fd.
       "size" 参数是指定文件数量的提示要与新实例关联的描述符.
       epoll_create() 返回的 fd 值应该用 close() 关闭.  */
    extern int epoll_create (int __size) __THROW;
    // 该函数从2.3.2版本的开始加入的,2.6版开始引入内核Linux最新的内核稳定版本已经到了5.8.14,长期支持版本到了5.4.70,从2.6.8内核开始的Linux,会忽略这个参数,但是必须要大于0,这个是Linux独有的函数
    
    /* 等待 epoll 实例的 "epfd" 事件. 在 "events" 缓冲区中返回的触发事件的数目. 或者是 -1 将出错时 "errno" 变量设置为特定错误代码. 
       "events" 参数是一个缓冲区,将包含触发的事件. "maxevents" 要设置的最大事件数返回( 通常是 "events" 的大小 ). 
       "timeout" 参数指定以毫秒为单位的最大等待时间 (-1 == infinite).
       此函数是一个取消点因此没有标记为 __THROW.  */
    extern int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
    
    /* 操作epoll实例 "epfd". 成功时返回0,
       -1表示错误 ( "errno" 变量将包含特殊错误代码) "op" 参数是 EPOLL_CTL_* 上面定义的常量.
       "fd" 参数是操作. "event" 参数描述调用者感兴趣的事件以及任何相关的用户数据.  */
    extern int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event) __THROW;
  • epoll_wait参数说明:

    • __epfd:表示事件发生监视范围的epol例程的文件描述符;
    • __events:保存发生事件的文件描述符集合的结构体地址值;
    • __maxevents:第二个参数中可以保存的最大事件数目;
    • __timeout:以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件。
  • epoll_ctl参数说明:

    • __epfd:用于注册监视对象的epoll例程的文件描述符;
    • __op:用于指定监视对象的添加、删除或更改等操作;
      1. EPOLL_CTL_ADD
      2. EPOLL_CTL_DEL
      3. EPOLL_CTL_MOD
    • __fd:需要注册的监视对象文件描述符;
    • __event:监视对象的事件类型:
      1. EPOLLIN:需要读取数据的情况;
      2. EPOLLOUT:输出缓冲为空,可以立即发送数据的情况;
      3. EPOLLPRI:收到OOB数据的情况;
      4. EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用;
      5. EPOLLERR:发生错误的情况;
      6. EPOLLET:以边缘触发的方式得到事件通知;
      7. EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递;

3.示例

  • 服务端:

    c++ 复制代码
    #include <iostream>
    #include <cstdlib>
    #include <cstring>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    
    #define BUF_SIZE 100
    #define EPOLL_SIZE 50
    
    // 错误处理函数
    void error_handling(const std::string &message)
    {
        std::cerr << message << std::endl;
        exit(1);
    }
    
    int main(int argc, char *argv[])
    {
        int serv_sock, clnt_sock;
        sockaddr_in serv_adr, clnt_adr;
        socklen_t adr_sz;
        int str_len, i;
        char buf[BUF_SIZE];
    
        epoll_event *ep_events;
        epoll_event event;
        int epfd, event_cnt;
    
        // 检查参数个数
        if (argc != 2)
        {
            std::cerr << "using: " << argv[0] << " <port>" << std::endl;
            exit(1);
        }
    
        // 创建服务器套接字
        serv_sock = socket(PF_INET, SOCK_STREAM, 0);
        if (serv_sock == -1)
            error_handling("socket() error");
    
        // 初始化服务器地址结构体
        memset(&serv_adr, 0, sizeof(serv_adr));
        serv_adr.sin_family = AF_INET;
        serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_adr.sin_port = htons(atoi(argv[1]));
    
        // 绑定服务器套接字
        if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
            error_handling("bind() error");
        // 监听连接请求
        if (listen(serv_sock, 5) == -1)
            error_handling("listen() error");
    
        // 创建epoll实例
        epfd = epoll_create(EPOLL_SIZE);
        if (epfd == -1)
            error_handling("epoll_create() error");
    
        // 动态分配epoll事件数组
        ep_events = new epoll_event[EPOLL_SIZE];
    
        // 设置服务器套接字的事件类型并添加到epoll实例中
        event.events = EPOLLIN;
        event.data.fd = serv_sock;
        if (epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event) == -1)
            error_handling("epoll_ctl() error");
    
        while (true)
        {
            // 等待事件发生
            event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
            if (event_cnt == -1)
            {
                std::cerr << "epoll_wait() error" << std::endl;
                break;
            }
    
            for (i = 0; i < event_cnt; i++)
            {
                if (ep_events[i].data.fd == serv_sock)
                {
                    // 接受新的客户端连接
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                    if (clnt_sock == -1)
                        error_handling("accept() error");
                    // 将新的客户端套接字添加到epoll实例中
                    event.events = EPOLLIN;
                    event.data.fd = clnt_sock;
                    if (epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event) == -1)
                        error_handling("epoll_ctl() error");
                    std::cout << "connected client: " << clnt_sock << std::endl;
                }
                else
                {
                    // 处理客户端消息
                    str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                    if (str_len == 0)
                    {
                        // 客户端关闭连接
                        if (epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, nullptr) == -1)
                            error_handling("epoll_ctl() error");
                        close(ep_events[i].data.fd);
                        std::cout << "closed client: " << ep_events[i].data.fd << std::endl;
                    }
                    else
                    {
                        // 回显消息给客户端
                        write(ep_events[i].data.fd, buf, str_len);
                    }
                }
            }
        }
    
        // 关闭服务器套接字和epoll实例
        close(serv_sock);
        close(epfd);
        delete[] ep_events;
        return 0;
    }
  • 客户端与Select模型一致

4.条件触发和边缘触发

  • 条件触发(level-triggered,也被称为水平触发)LT:只要满足条件,就触发一个事件(只要有数据没有被获取,内核就不断通知你)。

  • 边缘触发(edge-triggered)ET:每当状态变化时,触发一个事件。

    • "举个读socket的例子,假定经过长时间的沉默后,现在来了100个字节,这时无论边缘触发和条件触发都会产生一个通知应用程序可读。应用程序读了50个字节,然后重新调用api等待io事件。

      这时水平触发的api会因为还有50个字节可读从而立即返回用户一个read ready notification。

      而边缘触发的api会因为可读这个状态没有发生变化而陷入长期等待。 因此在使用边缘触发的api时,要注意每次都要读到socket返回EWOULDBLOCK为止,否则这个socket就算作废了。而使用条件触发的api 时,如果应用程序不需要写就不要关注socket可写的事件,否则就会无限次的立即返回一个write ready notification

    • select模型属于典型的条件触发

  • 条件触发的代码示例:

    c++ 复制代码
    #include <iostream>
    #include <cstring>
    #include <unistd.h>
    #include <fcntl.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    
    #define BUF_SIZE 4
    #define EPOLL_SIZE 50
    
    void error_handling(const std::string &message)
    {
        std::cerr << message << std::endl;
        exit(1);
    }
    
    int main(int argc, char *argv[])
    {
        int serv_sock, clnt_sock;
        sockaddr_in serv_adr{}, clnt_adr{};
        socklen_t adr_sz;
        int str_len, i;
        char buf[BUF_SIZE];
    
        epoll_event *ep_events;
        epoll_event event{};
        int epfd, event_cnt;
    
        // 检查命令行参数
        if (argc != 2)
        {
            std::cerr << "using: " << argv[0] << " <port>" << std::endl;
            exit(1);
        }
    
        // 创建服务器套接字
        serv_sock = socket(PF_INET, SOCK_STREAM, 0);
        if (serv_sock == -1)
            error_handling("socket() error");
    
        // 初始化服务器地址结构体
        memset(&serv_adr, 0, sizeof(serv_adr));
        serv_adr.sin_family = AF_INET;
        serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_adr.sin_port = htons(atoi(argv[1]));
    
        // 绑定服务器套接字
        if (bind(serv_sock, (sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
            error_handling("bind() error");
    
        // 监听连接请求
        if (listen(serv_sock, 5) == -1)
            error_handling("listen() error");
    
        // 创建epoll实例
        epfd = epoll_create(EPOLL_SIZE);
        if (epfd == -1)
            error_handling("epoll_create() error");
    
        // 动态分配epoll事件数组
        ep_events = new epoll_event[EPOLL_SIZE];
    
        // 设置服务器套接字的事件类型并添加到epoll实例中
        event.events = EPOLLIN;
        event.data.fd = serv_sock;
        if (epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event) == -1)
            error_handling("epoll_ctl() error");
    
        while (true)
        {
            // 等待事件发生
            event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
            if (event_cnt == -1)
            {
                std::cerr << "epoll_wait() error" << std::endl;
                break;
            }
    
            std::cout << "return epoll_wait" << std::endl;
            for (i = 0; i < event_cnt; i++)
            {
                if (ep_events[i].data.fd == serv_sock)
                {
                    // 接受新的客户端连接
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock = accept(serv_sock, (sockaddr *)&clnt_adr, &adr_sz);
                    if (clnt_sock == -1)
                        error_handling("accept() error");
    
                    // 将新的客户端套接字添加到epoll实例中
                    event.events = EPOLLIN;
                    event.data.fd = clnt_sock;
                    if (epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event) == -1)
                        error_handling("epoll_ctl() error");
                    std::cout << "connected client: " << clnt_sock << std::endl;
                }
                else
                {
                    // 处理客户端消息
                    str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                    if (str_len == 0)
                    {
                        // 客户端关闭连接
                        if (epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, nullptr) == -1)
                            error_handling("epoll_ctl() error");
                        close(ep_events[i].data.fd);
                        std::cout << "closed client: " << ep_events[i].data.fd << std::endl;
                    }
                    else
                    {
                        // 回显消息给客户端
                        write(ep_events[i].data.fd, buf, str_len);
                    }
                }
            }
        }
    
        // 关闭服务器套接字和epoll实例
        close(serv_sock);
        close(epfd);
        delete[] ep_events;
        return 0;
    }
  • 边缘触发的示例代码:

    c++ 复制代码
    #include <iostream>
    #include <cstring>
    #include <unistd.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <sys/epoll.h>
    
    #define BUF_SIZE 4
    #define EPOLL_SIZE 50
    
    void setNonBlockingMode(int fd);
    void errorHandling(const std::string &message);
    
    int main(int argc, char *argv[])
    {
        int serv_sock, clnt_sock;
        sockaddr_in serv_adr{}, clnt_adr{};
        socklen_t adr_sz;
        int str_len;
        char buf[BUF_SIZE];
    
        epoll_event *ep_events;
        epoll_event event{};
        int epfd, event_cnt;
    
        // 检查命令行参数
        if (argc != 2)
        {
            std::cerr << "using: " << argv[0] << " <port>" << std::endl;
            exit(1);
        }
    
        // 创建服务器套接字
        serv_sock = socket(PF_INET, SOCK_STREAM, 0);
        if (serv_sock == -1)
            errorHandling("socket() error");
    
        // 初始化服务器地址结构体
        memset(&serv_adr, 0, sizeof(serv_adr));
        serv_adr.sin_family = AF_INET;
        serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
        serv_adr.sin_port = htons(atoi(argv[1]));
    
        // 绑定服务器套接字
        if (bind(serv_sock, (sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
            errorHandling("bind() error");
    
        // 监听连接请求
        if (listen(serv_sock, 5) == -1)
            errorHandling("listen() error");
    
        // 创建epoll实例
        epfd = epoll_create(EPOLL_SIZE);
        if (epfd == -1)
            errorHandling("epoll_create() error");
    
        // 动态分配epoll事件数组
        ep_events = new epoll_event[EPOLL_SIZE];
    
        // 设置非阻塞模式
        setNonBlockingMode(serv_sock);
        event.events = EPOLLIN;
        event.data.fd = serv_sock;
    
        // 将服务器套接字添加到epoll实例中
        if (epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event) == -1)
            errorHandling("epoll_ctl() error");
    
        while (true)
        {
            // 等待事件发生
            event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
            if (event_cnt == -1)
            {
                std::cerr << "epoll_wait() error" << std::endl;
                break;
            }
    
            std::cout << "return epoll_wait" << std::endl;
            for (int i = 0; i < event_cnt; i++)
            {
                if (ep_events[i].data.fd == serv_sock)
                {
                    // 接受新的客户端连接
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock = accept(serv_sock, (sockaddr *)&clnt_adr, &adr_sz);
                    if (clnt_sock == -1)
                        errorHandling("accept() error");
    
                    // 设置非阻塞模式
                    setNonBlockingMode(clnt_sock);
                    event.events = EPOLLIN | EPOLLET;
                    event.data.fd = clnt_sock;
                    if (epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event) == -1)
                        errorHandling("epoll_ctl() error");
                    std::cout << "connected client: " << clnt_sock << std::endl;
                }
                else
                {
                    while (true)
                    {
                        // 读取客户端消息
                        str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                        if (str_len == 0)
                        { // 关闭请求
                            if (epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, nullptr) == -1)
                                errorHandling("epoll_ctl() error");
                            close(ep_events[i].data.fd);
                            std::cout << "closed client: " << ep_events[i].data.fd << std::endl;
                            break;
                        }
                        else if (str_len < 0)
                        {
                            if (errno == EAGAIN)
                                break;
                        }
                        else
                        {
                            // 回显消息给客户端
                            write(ep_events[i].data.fd, buf, str_len);
                        }
                    }
                }
            }
        }
    
        // 关闭服务器套接字和epoll实例
        close(serv_sock);
        close(epfd);
        delete[] ep_events;
        return 0;
    }
    
    void setNonBlockingMode(int fd)
    {
        int flag = fcntl(fd, F_GETFL, 0);
        fcntl(fd, F_SETFL, flag | O_NONBLOCK);
    }
    
    void errorHandling(const std::string &message)
    {
        std::cerr << message << std::endl;
        exit(1);
    }
  • 运行结果中需要注意的是,客户端发送消息次数和服务器端epoll_wait()函数调用次数。客户端从请求连接到断开连接共发送5次数据,服务器端也相应产生5个事件。

相关推荐
羑悻的小杀马特13 分钟前
【AIGC篇】畅谈游戏开发设计中AIGC所发挥的不可或缺的作用
c++·人工智能·aigc·游戏开发
Wanliang Li16 分钟前
Linux电源管理——CPU Hotplug 流程
linux·嵌入式硬件·嵌入式·armv8·电源管理·cpuhotplug
闻缺陷则喜何志丹21 分钟前
【C++动态规划】1105. 填充书架|2104
c++·算法·动态规划·力扣·高度·最小·书架
fnd_LN23 分钟前
Linux文件目录 --- mkdir命令,创建目录,多级目录,设置目录权限
linux·运维·服务器
达帮主41 分钟前
7.C语言 宏(Macro) 宏定义,宏函数
linux·c语言·算法
行思理1 小时前
Linux 下SVN新手操作手册
linux·运维·svn
初学者丶一起加油1 小时前
C语言基础:指针(数组指针与指针数组)
linux·c语言·开发语言·数据结构·c++·算法·visual studio
一只搬砖的猹2 小时前
cJson系列——常用cJson库函数
linux·前端·javascript·python·物联网·mysql·json
CodeClimb2 小时前
【华为OD-E卷-租车骑绿道 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
易码智能2 小时前
【RealTimeCallBack】- KRTS C++示例精讲(4)
c++·定时器·kithara·windows 实时套件·krts