[Linux服务器] Linux C++

268 0
Honkers 2025-4-17 08:06:34 来自手机 | 显示全部楼层 |阅读模式

1.Linux环境配置

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

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

2.升级编译器

  • 升级软件包:

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

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

    或者:

    1. mv /usr/bin/gcc /usr/bin/gcc-4.8.5
    2. ln -s /opt/rh/devtoolset-8/root/bin/gcc /usr/bin/gcc
    3. mv /usr/bin/g++ /usr/bin/g++-4.8.5
    4. ln -s /opt/rh/devtoolset-8/root/bin/g++ /usr/bin/g++
    复制代码

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

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

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

4.编译

  1. 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. 制作静态库

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

    • 不规范的做法:

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

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

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

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

  4. 静态库的特点

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

2.动态库

  1. 制作动态库

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

    • 不规范的做法:

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

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

  3. 动态库的概念

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

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

  4. 动态库的特点

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

3.main函数的参数

1.main函数的参数

  • main函数有三个参数,argc、argv和envp,它的标准写法如下:

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

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

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

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

2.操作环境变量

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

    • value 环境变量的值。

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

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

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

    1. char* getenv(const char *name);
    复制代码

3.示例

  1. #include <iostream>
  2. #include <cstdlib>
  3. int main(int argc, char *argv[], char *envp[]) {
  4. // 检查参数数量是否正确
  5. if (argc != 3) {
  6. std::cout << "Usage: ./demo <arg1> <arg2>" << std::endl;
  7. return -1;
  8. }
  9. // 显示命令行参数
  10. std::cout << "Command line arguments:" << std::endl;
  11. for (int i = 0; i < argc; ++i) {
  12. std::cout << "argv[" << i << "] = " << argv[i] << std::endl;
  13. }
  14. // 显示环境变量
  15. std::cout << "\nEnvironment variables:" << std::endl;
  16. for (int i = 0; envp[i] != nullptr; ++i) {
  17. std::cout << "envp[" << i << "] = " << envp[i] << std::endl;
  18. }
  19. // 设置环境变量AA
  20. setenv("AA", "aaaa", 1);
  21. // 显示环境变量AA的值
  22. std::cout << "\nEnvironment variable AA=" << getenv("AA") << std::endl;
  23. return 0;
  24. }
复制代码

4.gdb的常用命令

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

1.安装gdb

  1. yum -y install gdb
复制代码

2.gdb常用命令

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

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

3.gdb调试core文件

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

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

    调试core文件的步骤如下:

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

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

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

5.Linux的时间操作

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

1.time_t别名

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

    1. typedef long time_t;
    复制代码

2.time()库函数

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

  • 包含头文件:

  • 声明:

    1. time_t time(time_t *tloc);
    复制代码

    有两种调用方法:

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

    或者:

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

3.tm结构体

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

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

4.localtime()库函数

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

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

  • 包含头文件:

  • 函数声明:

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

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

5.mktime()库函数

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

  • 包含头文件:

  • 函数声明:

    1. 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结构体转换成字符串。
    • 示例:

      1. #include <iostream>
      2. #include <time.h>
      3. #include <cstring>
      4. int main()
      5. {
      6. // 初始时间字符串
      7. const char *initial_time_str = "2024-01-01 00:00:00";
      8. // 解析时间字符串
      9. struct tm tm_time;
      10. memset(&tm_time, 0, sizeof(tm_time));
      11. if (strptime(initial_time_str, "%Y-%m-%d %H:%M:%S", &tm_time) == nullptr)
      12. {
      13. std::cerr << "Failed to parse time string" << std::endl;
      14. return -1;
      15. }
      16. // 转换 tm 结构体到 time_t
      17. time_t time = mktime(&tm_time);
      18. if (time == -1)
      19. {
      20. std::cerr << "Failed to convert to time_t" << std::endl;
      21. return -1;
      22. }
      23. // 增加 30 分钟(1800 秒)
      24. time += 30 * 60;
      25. // 转换 time_t 到 tm 结构体
      26. struct tm new_tm_time;
      27. localtime_r(&time, &new_tm_time);
      28. // 转换 tm 结构体到字符串
      29. char new_time_str[20];
      30. strftime(new_time_str, sizeof(new_time_str), "%Y-%m-%d %H:%M:%S", &new_tm_time);
      31. // 输出结果
      32. std::cout << "Initial time: " << initial_time_str << std::endl;
      33. std::cout << "New time: " << new_time_str << std::endl;
      34. return 0;
      35. }
      复制代码

6.gettimeofday()库函数

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

  • 包含头文件:

  • 函数声明:

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

    1. #include <iostream>
    2. #include <sys/time.h>
    3. int main()
    4. {
    5. timeval start, end;
    6. gettimeofday(&start, 0); // 计时开始。
    7. for (int i = 0; i < 1000000000; i++)
    8. ;
    9. gettimeofday(&end, 0); // 计时结束。
    10. // 计算消耗的时长。
    11. timeval tv;
    12. tv.tv_usec = end.tv_usec - start.tv_usec;
    13. tv.tv_sec = end.tv_sec - start.tv_sec;
    14. if (tv.tv_usec < 0)
    15. {
    16. tv.tv_usec = 1000000 - tv.tv_usec;
    17. tv.tv_sec--;
    18. }
    19. std::cout << "耗时: " << tv.tv_sec << " 秒和 " << tv.tv_usec << " 微秒。" << std::endl;
    20. return 0;
    21. }
    复制代码

7.程序睡眠

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

  • 包含头文件:

  • 函数声明:

    1. extern unsigned int sleep (unsigned int __seconds);
    2. extern int usleep (__useconds_t __useconds);
    复制代码

6.Linux的目录操作

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

1.获取当前工作目录

  • 包含头文件:

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

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

2.切换工作目录

  • 包含头文件:

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

3.创建目录

  • 包含头文件:

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

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

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

4.删除目录

  • 包含头文件:

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

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

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

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

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

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

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

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

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

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

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

    • 目录指针:

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

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

    • d_name:文件名或目录名。

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

    • 示例:

      1. #include <iostream>
      2. #include <dirent.h>
      3. int main(int argc, char *argv[])
      4. {
      5. if (argc != 2)
      6. {
      7. std::cout << "using ./test 目录名\n";
      8. return -1;
      9. }
      10. DIR *dir; // 定义目录指针。
      11. // 打开目录。
      12. if ((dir = opendir(argv[1])) == nullptr)
      13. return -1;
      14. // 用于存放从目录中读取到的内容。
      15. struct dirent *stdinfo = nullptr;
      16. while (1)
      17. {
      18. // 读取一项内容并显示出来。
      19. if ((stdinfo = readdir(dir)) == nullptr)
      20. break;
      21. std::cout << "文件名 = " << stdinfo->d_name << " 文件类型 = " << (int)stdinfo->d_type << std::endl;
      22. }
      23. closedir(dir); // 关闭目录指针。
      24. }
      复制代码

7.Linux的系统错误

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

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

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

1.strerror()库函数

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

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

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

    1. #include <iostream>
    2. #include <cstring>
    3. int main(int argc, char *argv[])
    4. {
    5. for(int i = 0; i < 150; i++)
    6. {
    7. std::cout << i << ":" << strerror(i) << std::endl;
    8. }
    9. return 0;
    10. }
    复制代码

2.perror()库函数

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

    1. extern void perror (const char *__s);
    复制代码

3.注意事项

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

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

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

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

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

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

1.access()库函数

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

  • 包含头文件:

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

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

    __name:目录或文件名

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

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

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

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

2.stat()库函数

  1. stat结构体

    1. typedef unsigned long __dev_t;
    2. typedef unsigned long __ino_t;
    3. typedef unsigned long __nlink_t;
    4. typedef unsigned int __mode_t;
    5. typedef unsigned int __uid_t;
    6. typedef unsigned int __gid_t;
    7. typedef unsigned long __dev_t;
    8. typedef long __blksize_t;
    9. typedef long __blkcnt_t;
    10. typedef long __time_t;
    11. typedef long __syscall_slong_t;
    12. struct timespec
    13. {
    14. __time_t tv_sec; /* 秒. */
    15. __syscall_slong_t tv_nsec; /* 纳秒. */
    16. };
    17. struct stat
    18. {
    19. __dev_t st_dev; /* 设备. */
    20. __ino_t st_ino; /* 文件序号. */
    21. __nlink_t st_nlink; /* 链接数. */
    22. __mode_t st_mode; /* 文件模式. */
    23. __uid_t st_uid; /* 文件所有者的用户ID. */
    24. __gid_t st_gid; /* 文件组所属组ID.*/
    25. int __pad0;
    26. __dev_t st_rdev; /* 设备号,如果是设备. */
    27. __blksize_t st_blksize; /* I/O的最佳块大小. */
    28. __blkcnt_t st_blocks; /* 分配的512字节块. */
    29. struct timespec st_atim; /* 最后一次访问时间. */
    30. struct timespec st_mtim; /* 最后一次修改时间. */
    31. struct timespec st_ctim; /* 最后一次状态更改的时间. */
    32. # define st_atime st_atim.tv_sec /* 向后兼容性. */
    33. # define st_mtime st_mtim.tv_sec
    34. # define st_ctime st_ctim.tv_sec
    35. __syscall_slong_t __unused[3];
    36. };
    复制代码
    • struct stat结构体的成员变量比较多,重点关注st_mode、st_size和st_mtime成员。注意:st_mtime是一个整数表示的时间,需要程序员自己写代码转换格式。

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

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

    • 包含头文件:

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

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

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

    • 示例:

      1. #include <iostream>
      2. #include <unistd.h>
      3. #include <cstring>
      4. #include <sys/stat.h>
      5. int main(int argc, char *argv[])
      6. {
      7. if (argc != 2)
      8. {
      9. std::cout << "using: ./test 文件或目录名\n";
      10. return -1;
      11. }
      12. struct stat st; // 存放目录或文件详细信息的结构体。
      13. // 获取目录或文件的详细信息
      14. if (stat(argv[1], &st) != 0)
      15. {
      16. std::cout << "stat(" << argv[1] << "):" << strerror(errno) << std::endl;
      17. return -1;
      18. }
      19. if (S_ISREG(st.st_mode))
      20. std::cout << argv[1] << " 是一个文件(" << "mtime = " << st.st_mtime << ", size = " << st.st_size << ")\n";
      21. if (S_ISDIR(st.st_mode))
      22. std::cout << argv[1] << " 是一个目录(" << "mtime = " << st.st_mtime << ", size = " << st.st_size << ")\n";
      23. return 0;
      24. }
      复制代码

3.utime()库函数

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

  • 包含头文件:

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

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

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

4.rename()库函数

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

  • 包含头文件:

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

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

    __old:源目录或文件名。

    __new:目标目录或文件名。

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

5.remove()库函数

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

  • 包含头文件:

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

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

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

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

9.Linux的信号

1.信号的基本概念

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

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

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

    1. kill -l
    复制代码

2.信号的类型

信号名信号值默认处理动作发出信号的原因
SIGHUP1A终端挂起或者控制进程终止
SIGINT2A键盘终端 ctrl+c
SIGQUIT3C键盘的退出键按下
SIGILL4C非法指令
SIGTRAP5C跟踪断点
SIGABRT6C由abort(3)发出的退出指令
SIGBUS7C总线错误(例如内存对齐错误)
SIGFPE8C浮点异常
SIGKILL9AEF采用 kill -9 进程编号 强制杀死程序
SIGUSR110A用户自定义信号 1
SIGSEGV11CEF无效的内存引用(数组越界、操作空指针和野指针等)
SIGUSR212A用户自定义信号 2
SIGPIPE13A管道破裂,写一个没有读端口的管道
SIGALRM14A由闹钟alarm()函数发出的信号
SIGTERM15A采用 kill 进程编号 或 killall 程序名 通知程序
SIGSTKFLT16A栈故障(不常被使用)
SIGCHLD17B子进程结束信号
SIGCOUT18C进程继续(曾被停止的进程)
SIGSTOP19DEF终止进程
SIGSTP20D控制终端(tty)上按下停止键
SIGTTIN21D后台进程企图从控制终端读
SIGTTOU22D后台进程企图从控制终端写
SIGURG23B套接字上有紧急数据到达
SIGXCPU24C超过CPU时间限制
SIGXFSZ25C超过文件大小限制
SIGVTALRM26A虚拟时钟信号,由setitimer()产生
SIGPROF27A统计时钟信号,由setitimer()产生
SIGWINCH28B终端窗口大小改变
SIGIO29B文件描述符上可以进行I/O操作
SIGPWR30A电源故障(不常被使用)
SIGSYS31C非法系统调用
SIGRTMIN34A实时信号,用户自定义
SIGRTMAX64A实时信号,用户自定义
其它<=64A自定义信号
  • 默认处理动作:
    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()函数可以设置程序对信号的处理方式。

  • 包含头文件:

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

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

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

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

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

4.信号的作用

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

5.信号的应用示例

  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <signal.h>
  4. void EXIT(int sig)
  5. {
  6. std::cout << "收到了信号:" << sig << std::endl;
  7. std::cout << "正在释放资源,程序将退出......\n";
  8. // 以下是释放资源的代码。
  9. std::cout << "程序退出。\n";
  10. exit(0); // 进程退出。
  11. }
  12. int main(int argc, char *argv[])
  13. {
  14. // 忽略全部的信号,防止程序被信号异常中止。
  15. for (int ii = 1; ii <= 64; ii++)
  16. signal(ii, SIG_IGN);
  17. // 如果收到 2 和 15 的信号(ctrl+c 和 kill、killall),本程序将主动退出。
  18. signal(2, EXIT);
  19. signal(15, EXIT);
  20. while (true)
  21. {
  22. std::cout << "执行了一次任务。\n";
  23. sleep(1);
  24. }
  25. return 0;
  26. }
复制代码

6.发送信号

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

  • 函数声明:

    1. 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中,查看进程终止的状态:

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

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

    __status也是进程终止的状态。

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

2.资源释放的问题

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

3.进程的终止函数

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

  • 包含头文件:

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

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

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

11.调用可执行程序

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

1.system()函数

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

  • 函数声明:

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

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

2.exec函数族

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

  • 包含头文件:

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

    1. /* 使用从 `environ` 获取的环境变量,执行 PATH 所指向的文件,并将 PATH 之后的所有参数传递给它,直到遇到一个空指针。 */
    2. extern int execl (const char *__path, const char *__arg, ...) __THROW __nonnull ((1, 2));
    3. /* 执行 FILE 所指向的文件,如果 FILE 不包含斜杠(/),则在 `PATH` 环境变量中搜索 FILE,并将 FILE 之后的所有参数传递给它,直到遇到一个空指针,同时使用 `environ` 中的环境变量。 */
    4. extern int execlp (const char *__file, const char *__arg, ...) __THROW __nonnull ((1, 2));
    5. /* 使用从 `environ` 获取的环境变量,执行 PATH 所指向的文件,并将 PATH 之后的所有参数传递给它,直到遇到一个空指针,之后的参数为环境变量。 */
    6. extern int execle (const char *__path, const char *__arg, ...) __THROW __nonnull ((1, 2));
    7. /* 使用从 `environ` 获取的环境变量,执行 PATH 所指向的文件,并将 ARGV 中的参数传递给它。 */
    8. extern int execv (const char *__path, char *const __argv[]) __THROW __nonnull ((1, 2));
    9. /* 执行 FILE 所指向的文件,如果 FILE 不包含斜杠(/),则在 `PATH` 环境变量中搜索 FILE,并将 ARGV 中的参数传递给它,同时使用 `environ` 中的环境变量。 */
    10. extern int execvp (const char *__file, char *const __argv[]) __THROW __nonnull ((1, 2));
    11. /* 执行 FILE 所指向的文件,如果 FILE 不包含斜杠(/),则在 `PATH` 环境变量中搜索 FILE,并将 ARGV 中的参数传递给它,同时使用 __envp 中的环境变量。 */
    12. 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(),其它的极少使用。
  • 示例:

    1. #include <iostream>
    2. #include <string.h>
    3. #include <unistd.h>
    4. int main(int argc, char *argv[])
    5. {
    6. int ret = execl("/bin/ls", "/bin/ls", "-lt", "/tmp", nullptr); // 最后一个参数 nullptr 不能省略。
    7. std::cout << "ret = " << ret << std::endl;
    8. perror("execl");
    9. /*
    10. char *args[10];
    11. args[0] = strdup("/bin/ls");
    12. args[1] = strdup("-lt");
    13. args[2] = strdup("/tmp");
    14. args[3] = nullptr;
    15. int ret = execv("/bin/ls", args);
    16. std::cout << "ret = " << ret << std::endl;
    17. perror("execv");
    18. // 释放动态分配的内存
    19. for (int i = 0; args[i] != nullptr; ++i)
    20. {
    21. free(args[i]);
    22. }
    23. */
    24. return 0;
    25. }
    复制代码

12.创建进程

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

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

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

    1. pstree -p 进程编号
    复制代码

2.进程标识

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

  • 包含头文件:

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

    1. typedef int __pid_t;
    2. /* 获取调用进程的进程ID. */
    3. extern __pid_t getpid (void) __THROW;
    4. /* 获取调用进程的父进程的进程ID. */
    5. extern __pid_t getppid (void) __THROW;
    复制代码

3.fork()函数

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

  • 包含头文件:

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

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

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

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

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

  • 示例:

    1. #include <iostream>
    2. #include <unistd.h>
    3. int main()
    4. {
    5. int num = 0;
    6. std::string message = "初始化信息.";
    7. pid_t pid = fork();
    8. if (pid > 0)
    9. { // 父进程将执行这段代码。
    10. sleep(1);
    11. std::cout << "父进程pid: " << pid << std::endl;
    12. std::cout << "父进程num: " << num << ", msg: " << message << std::endl;
    13. }
    14. else
    15. { // 子进程将执行这段代码。
    16. num = 1;
    17. message = "子进程修改后的信息.";
    18. std::cout << "子进程pid: " << pid << std::endl;
    19. std::cout << "子进程num: " << num << ", msg: " << message << std::endl;
    20. }
    21. return 0;
    22. }
    复制代码

4.fork()的两种做法

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

    1. #include <iostream>
    2. #include <unistd.h>
    3. int main()
    4. {
    5. if (fork() > 0)
    6. { // 父进程将执行这段代码。
    7. while (true)
    8. {
    9. sleep(1);
    10. std::cout << "父进程运行中..." << std::endl;
    11. }
    12. }
    13. else
    14. { // 子进程将执行这段代码。
    15. sleep(10);
    16. std::cout << "子进程开始执行任务..." << std::endl;
    17. execl("/bin/ls", "/bin/ls", "-lt", "/tmp", 0);
    18. std::cout << "子进程执行任务结束,退出." << std::endl;
    19. }
    20. return 0;
    21. }
    复制代码

5.共享文件

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

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

  • 示例:

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

6.vfork()函数

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

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

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

  • 示例:

    1. #include <iostream>
    2. #include <unistd.h>
    3. #include <sys/types.h>
    4. #include <sys/wait.h>
    5. int main()
    6. {
    7. int x = 0;
    8. pid_t pid;
    9. pid = vfork();
    10. if (pid < 0)
    11. {
    12. std::cerr << "vfork()失败." << std::endl;
    13. return 1;
    14. }
    15. else if (pid == 0)
    16. {
    17. // 子进程
    18. std::cout << "子进程: x = " << x << std::endl;
    19. x = 1; // 修改子进程中的变量 x
    20. sleep(3); // 子进程执行完毕后休息三秒再退出
    21. _exit(0); // 使用 _exit() 退出,避免在子进程中执行父进程的全局析构函数等
    22. }
    23. else
    24. {
    25. // 父进程
    26. // 等待子进程结束
    27. waitpid(pid, nullptr, 0);
    28. std::cout << "父进程: x = " << x << std::endl;
    29. }
    30. return 0;
    31. }
    复制代码

13.僵尸进程

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

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

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

  • 僵尸进程的避免:

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

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

      • 包含头文件:

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

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

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

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

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

        1. #include <iostream>
        2. #include <unistd.h>
        3. #include <sys/types.h>
        4. #include <sys/wait.h>
        5. void func(int sig) // 子进程退出的信号处理函数。
        6. {
        7. int sts;
        8. pid_t pid = wait(&sts);
        9. std::cout << "已终止的子进程编号是: " << pid << std::endl;
        10. if (WIFEXITED(sts))
        11. {
        12. std::cout << "子进程是正常退出的,退出状态是: " << WEXITSTATUS(sts) << std::endl;
        13. }
        14. else
        15. {
        16. std::cout << "子进程是异常退出的,终止它的信号是: " << WTERMSIG(sts) << std::endl;
        17. }
        18. }
        19. int main()
        20. {
        21. signal(SIGCHLD, func); // 捕获子进程退出的信号。
        22. if (fork() > 0)
        23. { // 父进程的流程。
        24. while (true)
        25. {
        26. std::cout << "父进程正在执行任务." << std::endl;
        27. sleep(1);
        28. }
        29. }
        30. else
        31. { // 子进程的流程。
        32. sleep(5);
        33. // int *p = nullptr; *p=10;
        34. exit(1);
        35. }
        36. return 0;
        37. }
        38. /*执行流程如下:
        39. 1. 父进程 fork 出子进程后,进入 while 循环,不断输出 "父进程正在执行任务." 的消息。
        40. 2. 子进程执行 sleep(5) 或对空指针解引用导致段错误,然后退出。
        41. 2.1. 如果子进程发生段错误:
        42. 2.1.1. 子进程异常退出时,操作系统发送 SIGCHLD 信号给父进程。
        43. 2.1.2. 父进程捕获到 SIGCHLD 信号,调用 func 函数处理。
        44. 2.1.3. 在 func 函数中,父进程调用 wait 函数等待子进程的退出状态。
        45. 2.1.4. 父进程输出已终止的子进程编号和子进程的退出状态,由于子进程是异常退出的,所以输出 "子进程是异常退出的,终止它的信号是: " 和相应的信号值。
        46. 2.2. 如果子进程没有发生段错误:
        47. 2.2.1. 子进程正常退出时,操作系统发送 SIGCHLD 信号给父进程。
        48. 2.2.2. 父进程捕获到 SIGCHLD 信号,调用 func 函数处理。
        49. 2.2.3. 在 func 函数中,父进程调用 wait 函数等待子进程的退出状态。
        50. 2.2.4. 父进程输出已终止的子进程编号和子进程的退出状态,由于子进程是正常退出的,所以输出 "子进程是正常退出的,退出状态是: " 和相应的退出状态值。 */
        复制代码

14.多线程和信号

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

  • 示例:

    1. #include <iostream>
    2. #include <unistd.h>
    3. #include <signal.h>
    4. void FatherEXIT(int sig); // 父进程的信号处理函数。
    5. void ChildEXIT(int sig); // 子进程的信号处理函数。
    6. int main()
    7. {
    8. // 忽略全部的信号,不希望被打扰。
    9. for (int i = 1; i <= 64; i++)
    10. signal(ii, SIG_IGN);
    11. // 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程
    12. // 但请不要用 "kill -9 +进程号" 强行终止
    13. signal(SIGTERM, FatherEXIT);
    14. signal(SIGINT, FatherEXIT); // SIGTERM 15 SIGINT 2
    15. while (true)
    16. {
    17. if (fork() > 0) // 父进程的流程。
    18. {
    19. sleep(5);
    20. continue;
    21. }
    22. else // 子进程的流程。
    23. {
    24. // 子进程需要重新设置信号。
    25. signal(SIGTERM, ChildEXIT); // 子进程的退出函数与父进程不一样。
    26. signal(SIGINT, SIG_IGN); // 子进程不需要捕获SIGINT信号。
    27. while (true)
    28. {
    29. std::cout << "子进程: " << getpid() << " 正在运行中." << std::endl;
    30. sleep(3);
    31. continue;
    32. }
    33. }
    34. }
    35. return 0;
    36. }
    37. // 父进程的信号处理函数。
    38. void FatherEXIT(int sig)
    39. {
    40. // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
    41. signal(SIGINT, SIG_IGN);
    42. signal(SIGTERM, SIG_IGN);
    43. std::cout << "父进程退出, sig = " << sig << std::endl;
    44. kill(0, SIGTERM); // 向全部的子进程发送15的信号,通知它们退出。
    45. // 在这里增加释放资源的代码(全局的资源)。
    46. exit(0);
    47. }
    48. // 子进程的信号处理函数。
    49. void ChildEXIT(int sig)
    50. {
    51. // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
    52. signal(SIGINT, SIG_IGN);
    53. signal(SIGTERM, SIG_IGN);
    54. std::cout << "子进程: " << getpid() << "退出, sig = " << sig << std::endl;
    55. // 在这里增加释放资源的代码(只释放子进程的资源)。
    56. exit(0);
    57. }
    复制代码

15.共享内存

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

1.shmget()函数

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

  • 包含头文件:

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

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

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

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

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

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

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

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

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

2.shmat()函数

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

  • 函数声明:

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

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

3.shmdt()函数

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

  • 函数声明:

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

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

4.shmctl()函数

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

  • 函数声明:

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

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

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

5.示例

  1. #include <iostream>
  2. #include <cstdio>
  3. #include <cstdlib>
  4. #include <cstring>
  5. #include <unistd.h>
  6. #include <sys/ipc.h>
  7. #include <sys/shm.h>
  8. // 共享内存结构体
  9. struct shmdata
  10. {
  11. int id; // 一个简单的整数标识
  12. char message[256]; // 一个消息字符串
  13. };
  14. int main(int argc, char *argv[])
  15. {
  16. if (argc != 3)
  17. {
  18. std::cout << "using: ./test <id> <msg>" << std::endl;
  19. return -1;
  20. }
  21. // 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。
  22. int shmid = shmget(0x5005, sizeof(shmdata), 0640 | IPC_CREAT);
  23. if (shmid == -1)
  24. {
  25. perror("共享内存创建失败");
  26. return -1;
  27. }
  28. std::cout << "共享内存ID = " << shmid << std::endl;
  29. // 第2步:把共享内存连接到当前进程的地址空间。
  30. shmdata *ptr = (shmdata *)shmat(shmid, nullptr, 0);
  31. if (ptr == (void *)-1)
  32. {
  33. perror("共享内存连接失败");
  34. return -1;
  35. }
  36. // 第3步:使用共享内存,对共享内存进行读/写。
  37. std::cout << "原始数据: id = " << ptr->id << ", 消息 = " << ptr->message << std::endl;
  38. // 更新共享内存中的数据
  39. ptr->id = std::atoi(argv[1]);
  40. std::strncpy(ptr->message, argv[2], sizeof(ptr->message) - 1);
  41. ptr->message[sizeof(ptr->message) - 1] = '\0'; // 确保字符串以null结尾
  42. std::cout << "更新后的数据: id = " << ptr->id << ", 消息 = " << ptr->message << std::endl;
  43. // 第4步:把共享内存从当前进程中分离。
  44. if (shmdt(ptr) == -1)
  45. {
  46. perror("共享内存分离失败");
  47. return -1;
  48. }
  49. // 第5步:删除共享内存(如果需要删除)。
  50. /* if (shmctl(shmid, IPC_RMID, nullptr) == -1)
  51. {
  52. perror("共享内存删除失败");
  53. return -1;
  54. } */
  55. return 0;
  56. }
复制代码

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

  1. #ifndef __PUBLIC_HH
  2. #define __PUBLIC_HH
  3. #include <iostream>
  4. #include <cstdio>
  5. #include <cstdlib>
  6. #include <cstring>
  7. #include <unistd.h>
  8. #include <sys/ipc.h>
  9. #include <sys/shm.h>
  10. #include <sys/types.h>
  11. #include <sys/sem.h>
  12. // 循环队列模板类。
  13. template <class TT, int MaxLength>
  14. class squeue
  15. {
  16. private:
  17. bool m_inited; // 队列被初始化标志,true-已初始化;false-未初始化。
  18. TT m_data[MaxLength]; // 用数组存储循环队列中的元素。
  19. int m_head; // 队列的头指针。
  20. int m_tail; // 队列的尾指针,指向队尾元素。
  21. int m_length; // 队列的实际长度。
  22. squeue(const squeue &) = delete; // 禁用拷贝构造函数。
  23. squeue &operator=(const squeue &) = delete; // 禁用赋值函数。
  24. public:
  25. squeue() { init(); } // 构造函数。
  26. // 循环队列的初始化操作。
  27. // 注意:如果用于共享内存的队列,不会调用构造函数,必须调用此函数初始化。
  28. void init()
  29. {
  30. if (!m_inited)
  31. { // 循环队列的初始化只能执行一次。
  32. m_head = 0; // 头指针。
  33. m_tail = MaxLength - 1; // 为了方便写代码,初始化时,尾指针指向队列的最后一个位置。
  34. m_length = 0; // 队列的实际长度。
  35. std::memset(m_data, 0, sizeof(m_data)); // 数组元素清零。
  36. m_inited = true;
  37. }
  38. }
  39. // 元素入队,返回值:false-失败;true-成功。
  40. bool push(const TT &ee)
  41. {
  42. if (full())
  43. {
  44. std::cout << "循环队列已满,入队失败。\n";
  45. return false;
  46. }
  47. // 先移动队尾指针,然后再拷贝数据。
  48. m_tail = (m_tail + 1) % MaxLength; // 队尾指针后移。
  49. m_data[m_tail] = ee;
  50. m_length++;
  51. return true;
  52. }
  53. // 求循环队列的长度,返回值:>=0-队列中元素的个数。
  54. int size() const
  55. {
  56. return m_length;
  57. }
  58. // 判断循环队列是否为空,返回值:true-空,false-非空。
  59. bool empty() const
  60. {
  61. return m_length == 0;
  62. }
  63. // 判断循环队列是否已满,返回值:true-已满,false-未满。
  64. bool full() const
  65. {
  66. return m_length == MaxLength;
  67. }
  68. // 查看队头元素的值,元素不出队。
  69. TT &front()
  70. {
  71. return m_data[m_head];
  72. }
  73. // 元素出队,返回值:false-失败;true-成功。
  74. bool pop()
  75. {
  76. if (empty())
  77. return false;
  78. m_head = (m_head + 1) % MaxLength; // 队列头指针后移。
  79. m_length--;
  80. return true;
  81. }
  82. // 显示循环队列中全部的元素。
  83. // 这是一个临时的用于调试的函数,队列中元素的数据类型支持cout输出才可用。
  84. void printqueue() const
  85. {
  86. for (int i = 0; i < size(); i++)
  87. {
  88. std::cout << "m_data[" << (m_head + i) % MaxLength << "], value="
  89. << m_data[(m_head + i) % MaxLength] << std::endl;
  90. }
  91. }
  92. };
  93. // 信号量类。
  94. class csemp
  95. {
  96. private:
  97. union semun
  98. { // 用于信号量操作的联合体。
  99. int val;
  100. struct semid_ds *buf;
  101. unsigned short *arry;
  102. };
  103. int m_semid; // 信号量id(描述符)。
  104. short m_sem_flg; // 信号量的标志位。
  105. csemp(const csemp &) = delete; // 禁用拷贝构造函数。
  106. csemp &operator=(const csemp &) = delete; // 禁用赋值函数。
  107. public:
  108. csemp() : m_semid(-1) {}
  109. // 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
  110. // 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
  111. // 如果用于生产消费者模型,value填0,sem_flg填0。
  112. bool init(key_t key, unsigned short value = 1, short sem_flg = SEM_UNDO);
  113. // 信号量的P操作,如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
  114. bool wait(short value = -1);
  115. // 信号量的V操作。
  116. bool post(short value = 1);
  117. // 获取信号量的值,成功返回信号量的值,失败返回-1。
  118. int getvalue() const;
  119. // 销毁信号量。
  120. bool destroy();
  121. ~csemp();
  122. };
  123. #endif // __PUBLIC_HH
复制代码
  1. #include "public.h"
  2. // 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
  3. // 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
  4. // 如果用于生产消费者模型,value填0,sem_flg填0。
  5. bool csemp::init(key_t key, unsigned short value, short sem_flg)
  6. {
  7. if (m_semid != -1)
  8. return false; // 如果已经初始化了,不必再次初始化。
  9. m_sem_flg = sem_flg;
  10. // 信号量的初始化不能直接用semget(key, 1, 0666 | IPC_CREAT)
  11. // 因为信号量创建后,初始值是0,如果用于互斥锁,需要把它的初始值设置为1,
  12. // 而获取信号量则不需要设置初始值,所以,创建信号量和获取信号量的流程不同。
  13. // 信号量的初始化分三个步骤:
  14. // 1) 获取信号量,如果成功,函数返回。
  15. // 2) 如果失败,则创建信号量。
  16. // 3) 设置信号量的初始值。
  17. // 获取信号量。
  18. if ((m_semid = semget(key, 1, 0666)) == -1)
  19. {
  20. // 如果信号量不存在,创建它。
  21. if (errno == ENOENT)
  22. {
  23. // 用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取。
  24. if ((m_semid = semget(key, 1, 0666 | IPC_CREAT | IPC_EXCL)) == -1)
  25. {
  26. if (errno == EEXIST)
  27. { // 如果错误代码是信号量已存在,则再次获取信号量。
  28. if ((m_semid = semget(key, 1, 0666)) == -1)
  29. {
  30. perror("init 1 semget()");
  31. return false;
  32. }
  33. return true;
  34. }
  35. else
  36. { // 如果是其它错误,返回失败。
  37. perror("init 2 semget()");
  38. return false;
  39. }
  40. }
  41. // 信号量创建成功后,还需要把它初始化成value。
  42. union semun sem_union;
  43. sem_union.val = value; // 设置信号量的初始值。
  44. if (semctl(m_semid, 0, SETVAL, sem_union) < 0)
  45. {
  46. perror("init semctl()");
  47. return false;
  48. }
  49. }
  50. else
  51. {
  52. perror("init 3 semget()");
  53. return false;
  54. }
  55. }
  56. return true;
  57. }
  58. // 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
  59. bool csemp::wait(short value)
  60. {
  61. if (m_semid == -1)
  62. return false;
  63. struct sembuf sem_b;
  64. sem_b.sem_num = 0; // 信号量编号,0代表第一个信号量。
  65. sem_b.sem_op = value; // P操作的value必须小于0。
  66. sem_b.sem_flg = m_sem_flg;
  67. if (semop(m_semid, &sem_b, 1) == -1)
  68. {
  69. perror("wait semop()");
  70. return false;
  71. }
  72. return true;
  73. }
  74. // 信号量的V操作(把信号量的值增加value)。
  75. bool csemp::post(short value)
  76. {
  77. if (m_semid == -1)
  78. return false;
  79. struct sembuf sem_b;
  80. sem_b.sem_num = 0; // 信号量编号,0代表第一个信号量。
  81. sem_b.sem_op = value; // V操作的value必须大于0。
  82. sem_b.sem_flg = m_sem_flg;
  83. if (semop(m_semid, &sem_b, 1) == -1)
  84. {
  85. perror("post semop()");
  86. return false;
  87. }
  88. return true;
  89. }
  90. // 获取信号量的值,成功返回信号量的值,失败返回-1。
  91. int csemp::getvalue() const
  92. {
  93. return semctl(m_semid, 0, GETVAL);
  94. }
  95. // 销毁信号量。
  96. bool csemp::destroy()
  97. {
  98. if (m_semid == -1)
  99. return false;
  100. if (semctl(m_semid, 0, IPC_RMID) == -1)
  101. {
  102. perror("destroy semctl()");
  103. return false;
  104. }
  105. return true;
  106. }
  107. // 信号量析构函数。
  108. csemp::~csemp()
  109. {
  110. // 在析构函数中销毁信号量。
  111. destroy();
  112. }
复制代码
  1. // 本程序演示循环队列的使用。
  2. #include "public.h"
  3. int main()
  4. {
  5. using ElemType = int;
  6. squeue<ElemType, 5> Queue;
  7. ElemType element; // 创建一个数据元素。
  8. std::cout << "元素(1、2、3)入队" << std::endl;
  9. element = 1;
  10. Queue.push(element);
  11. element = 2;
  12. Queue.push(element);
  13. element = 3;
  14. Queue.push(element);
  15. std::cout << "队列的长度是: " << Queue.size() << std::endl;
  16. Queue.printqueue();
  17. element = Queue.front();
  18. Queue.pop();
  19. std::cout << "出队的元素值为: " << element << std::endl;
  20. element = Queue.front();
  21. Queue.pop();
  22. std::cout << "出队的元素值为: " << element << std::endl;
  23. std::cout << "队列的长度是: " << Queue.size() << std::endl;
  24. Queue.printqueue();
  25. std::cout << "元素(11、12、13、14、15)入队." << std::endl;
  26. element = 11;
  27. Queue.push(element);
  28. element = 12;
  29. Queue.push(element);
  30. element = 13;
  31. Queue.push(element);
  32. element = 14;
  33. Queue.push(element);
  34. element = 15;
  35. Queue.push(element);
  36. std::cout << "队列的长度是: " << Queue.size() << std::endl;
  37. Queue.printqueue();
  38. return 0;
  39. }
复制代码
  1. // shared_memory_cirucularqueue.cpp,本程序演示基于共享内存的循环队列。
  2. #include "public.h"
  3. int main()
  4. {
  5. using ElemType = int;
  6. // 初始化共享内存。
  7. int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);
  8. if (shmid == -1)
  9. {
  10. std::cout << "shmget(0x5005) failed." << std::endl;
  11. return -1;
  12. }
  13. // 把共享内存连接到当前进程的地址空间。
  14. squeue<ElemType, 5> *Queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);
  15. if (Queue == (void *)-1)
  16. {
  17. std::cout << "shmat() failed." << std::endl;
  18. return -1;
  19. }
  20. Queue->init(); // 初始化循环队列。
  21. ElemType element; // 创建一个数据元素。
  22. std::cout << "元素(1、2、3)入队。\n";
  23. element = 1;
  24. Queue->push(element);
  25. element = 2;
  26. Queue->push(element);
  27. element = 3;
  28. Queue->push(element);
  29. std::cout << "队列的长度是: " << Queue->size() << std::endl;
  30. Queue->printqueue();
  31. element = Queue->front();
  32. Queue->pop();
  33. std::cout << "出队的元素值为: " << element << std::endl;
  34. element = Queue->front();
  35. Queue->pop();
  36. std::cout << "出队的元素值为: " << element << std::endl;
  37. std::cout << "队列的长度是: " << Queue->size() << std::endl;
  38. Queue->printqueue();
  39. std::cout << "元素(11、12、13、14、15)入队." << std::endl;
  40. element = 11;
  41. Queue->push(element);
  42. element = 12;
  43. Queue->push(element);
  44. element = 13;
  45. Queue->push(element);
  46. element = 14;
  47. Queue->push(element);
  48. element = 15;
  49. Queue->push(element);
  50. std::cout << "队列的长度是: " << Queue->size() << std::endl;
  51. Queue->printqueue();
  52. shmdt(Queue); // 把共享内存从当前进程中分离。
  53. return 0;
  54. }
复制代码
  1. // shared_memory_lock.cpp,本程序演示用信号量给共享内存加锁。
  2. #include "public.h"
  3. struct PersonInfo
  4. { // 人员信息结构体。
  5. int id; // 编号。
  6. char name[32]; // 姓名。
  7. };
  8. int main(int argc, char *argv[])
  9. {
  10. if (argc != 3)
  11. {
  12. std::cout << "using: ./shared_memory_lock id name" << std::endl;
  13. return -1;
  14. }
  15. // 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。
  16. int shmid = shmget(0x5005, sizeof(PersonInfo), 0640 | IPC_CREAT);
  17. if (shmid == -1)
  18. {
  19. std::cout << "shmget(0x5005) failed." << std::endl;
  20. return -1;
  21. }
  22. std::cout << "shmid = " << shmid << std::endl;
  23. // 第2步:把共享内存连接到当前进程的地址空间。
  24. PersonInfo *ptr = (PersonInfo *)shmat(shmid, 0, 0);
  25. if (ptr == (void *)-1)
  26. {
  27. std::cout << "shmat() failed." << std::endl;
  28. return -1;
  29. }
  30. // 创建、初始化二元信号量。
  31. csemp mutex;
  32. if (!mutex.init(0x5005))
  33. {
  34. std::cout << "mutex.init(0x5005) failed." << std::endl;
  35. ;
  36. return -1;
  37. }
  38. std::cout << "申请加锁..." << std::endl;
  39. mutex.wait(); // 申请加锁。
  40. std::cout << "申请加锁成功." << std::endl;
  41. // 第3步:使用共享内存,对共享内存进行读/写。
  42. std::cout << "原值: id = " << ptr->id << ", name = " << ptr->name << std::endl; // 显示共享内存中的原值。
  43. ptr->id = atoi(argv[1]); // 对人员信息结构体的id成员赋值。
  44. strcpy(ptr->name, argv[2]); // 对人员信息结构体的name成员赋值。
  45. std::cout << "新值: id = " << ptr->id << ", name = " << ptr->name << std::endl; // 显示共享内存中的当前值。
  46. sleep(10);
  47. mutex.post(); // 解锁。
  48. std::cout << "解锁." << std::endl;
  49. // 查看信号量:ipcs -s // 删除信号量:ipcrm sem 信号量id
  50. // 查看共享内存:ipcs -m // 删除共享内存:ipcrm -m 共享内存id
  51. // 第4步:把共享内存从当前进程中分离。
  52. shmdt(ptr);
  53. // 第5步:删除共享内存。
  54. // if (shmctl(shmid,IPC_RMID,0) == -1)
  55. //{
  56. // std::cout << "shmctl failed"; << std::endl; return -1;
  57. //}
  58. }
复制代码
  1. #include "public.h" // 生产者 producer.cpp
  2. int main()
  3. {
  4. struct Person
  5. { // 生产队列的数据元素是人员信息结构体。
  6. int id;
  7. char name[31];
  8. };
  9. using ElemType = Person;
  10. // 初始化共享内存。
  11. int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);
  12. if (shmid == -1)
  13. {
  14. std::cout << "shmget(0x5005) failed." << std::endl;
  15. return -1;
  16. }
  17. // 把共享内存连接到当前进程的地址空间。
  18. squeue<ElemType, 5> *queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);
  19. if (queue == (void *)-1)
  20. {
  21. std::cout << "shmat() failed." << std::endl;
  22. return -1;
  23. }
  24. queue->init(); // 初始化循环队列。
  25. ElemType element; // 创建一个数据元素。
  26. csemp mutex;
  27. mutex.init(0x5001); // 用于给共享内存加锁。
  28. csemp cond;
  29. cond.init(0x5002, 0, 0); // 信号量的值用于表示队列中数据元素的个数。
  30. mutex.wait(); // 加锁。
  31. // 生产3个数据。
  32. element.id = 3;
  33. strncpy(element.name, "Tom", sizeof(element.name));
  34. queue->push(element);
  35. element.id = 7;
  36. strncpy(element.name, "Tomy", sizeof(element.name));
  37. queue->push(element);
  38. element.id = 8;
  39. strncpy(element.name, "Tony", sizeof(element.name));
  40. queue->push(element);
  41. mutex.post(); // 解锁。
  42. cond.post(3); // 实参是3,表示生产了3个数据。
  43. shmdt(queue); // 把共享内存从当前进程中分离。
  44. return 0;
  45. }
复制代码
  1. #include "public.h" // 消费者 consumer.cpp
  2. int main()
  3. {
  4. struct Person
  5. { // 循环队列的数据元素是人员信息结构体。
  6. int id;
  7. char name[31];
  8. };
  9. using ElemType = Person;
  10. // 初始化共享内存。
  11. int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);
  12. if (shmid == -1)
  13. {
  14. std::cout << "shmget(0x5005) failed." << std::endl;
  15. return -1;
  16. }
  17. // 把共享内存连接到当前进程的地址空间。
  18. squeue<ElemType, 5> *queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);
  19. if (queue == (void *)-1)
  20. {
  21. std::cout << "shmat() failed." << std::endl;
  22. return -1;
  23. }
  24. queue->init(); // 初始化循环队列。
  25. ElemType element; // 创建一个数据元素。
  26. csemp mutex;
  27. mutex.init(0x5001); // 用于给共享内存加锁。
  28. csemp cond;
  29. cond.init(0x5002, 0, 0); // 信号量的值用于表示队列中数据元素的个数。
  30. while (true)
  31. {
  32. mutex.wait(); // 加锁。
  33. while (queue->empty())
  34. { // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if
  35. mutex.post(); // 解锁。
  36. cond.wait(); // 等待生产者的唤醒信号。
  37. mutex.wait(); // 加锁。
  38. }
  39. // 数据元素出队。
  40. element = queue->front();
  41. queue->pop();
  42. mutex.post(); // 解锁。
  43. // 处理出队的数据(把数据消费掉)。
  44. std::cout << "id = " << element.id << ", name = " << element.name << std::endl;
  45. usleep(100); // 假设处理数据需要时间,方便演示。
  46. }
  47. shmdt(queue); // 把共享内存从当前进程中分离。
  48. return 0;
  49. }
复制代码

17.第一个网络通讯程序

1.网络通讯的流程

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

2.示例

  • 客户端

    1. #include <iostream>
    2. #include <cstring>
    3. #include <cstdlib>
    4. #include <unistd.h>
    5. #include <netdb.h>
    6. #include <sys/types.h>
    7. #include <sys/socket.h>
    8. #include <arpa/inet.h>
    9. int main(int argc, char *argv[])
    10. {
    11. if (argc != 3)
    12. {
    13. std::cout << "using: ./socketclient <server_ip> <server_port>" << std::endl
    14. << "example: ./ socketclient 192.168.101.139 5005" << std::endl;
    15. return -1;
    16. }
    17. // 创建客户端套接字
    18. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    19. if (sockfd == -1)
    20. {
    21. perror("socket failed.");
    22. return -1;
    23. }
    24. // 获取服务器地址
    25. struct hostent *server_info = gethostbyname(argv[1]);
    26. if (server_info == nullptr)
    27. {
    28. std::cout << "Error: Failed to get server info." << std::endl;
    29. close(sockfd);
    30. return -1;
    31. }
    32. // 构建服务器地址结构
    33. struct sockaddr_in server_address;
    34. memset(&server_address, 0, sizeof(server_address));
    35. server_address.sin_family = AF_INET;
    36. memcpy(&server_address.sin_addr, server_info->h_addr, server_info->h_length);
    37. server_address.sin_port = htons(atoi(argv[2])); // 使用 atoi() 将字符串端口号转换为整数端口号
    38. // 连接服务器
    39. if (connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1)
    40. {
    41. perror("connect failed.");
    42. close(sockfd);
    43. return -1;
    44. }
    45. // 发送和接收数据
    46. char buffer[1024];
    47. for (int i = 0; i < 3; ++i)
    48. {
    49. // 发送请求报文
    50. sprintf(buffer, "Request #%d from client.", i + 1);
    51. ssize_t sent_bytes = send(sockfd, buffer, strlen(buffer), 0);
    52. if (sent_bytes <= 0)
    53. {
    54. perror("send failed.");
    55. break;
    56. }
    57. std::cout << "sent: " << buffer << std::endl;
    58. // 接收服务器响应报文
    59. memset(buffer, 0, sizeof(buffer));
    60. ssize_t recv_bytes = recv(sockfd, buffer, sizeof(buffer), 0);
    61. if (recv_bytes <= 0)
    62. {
    63. std::cout << "recv_bytes = " << recv_bytes << std::endl;
    64. break;
    65. }
    66. std::cout << "received: " << buffer << std::endl;
    67. sleep(1); // 等待1秒
    68. }
    69. // 关闭套接字
    70. close(sockfd);
    71. return 0;
    72. }
    复制代码
  • 服务端:

    1. #include <iostream>
    2. #include <cstdio>
    3. #include <cstring>
    4. #include <cstdlib>
    5. #include <unistd.h>
    6. #include <netdb.h>
    7. #include <sys/types.h>
    8. #include <sys/socket.h>
    9. #include <arpa/inet.h>
    10. int main(int argc, char *argv[])
    11. {
    12. if (argc != 2)
    13. {
    14. std::cout << "using: ./socketserver <port_number>" << std::endl;
    15. std::cout << "example: ./socketserver 5005." << std::endl;
    16. std::cout << "note: The firewall on the Linux system running the server program must open port 5005." << std::endl;
    17. std::cout << "if it is a cloud server, access policies on the cloud platform must also be opened." << std::endl;
    18. return -1;
    19. }
    20. // 创建服务端的socket
    21. int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    22. if (listenfd == -1)
    23. {
    24. perror("socket failed.");
    25. return -1;
    26. }
    27. // 将服务端用于通信的IP和端口绑定到socket上
    28. struct sockaddr_in servaddr;
    29. memset(&servaddr, 0, sizeof(servaddr));
    30. servaddr.sin_family = AF_INET;
    31. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    32. servaddr.sin_port = htons(atoi(argv[1])); // 使用 atoi() 将字符串端口号转换为整数端口号
    33. if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
    34. {
    35. perror("bind failed.");
    36. close(listenfd);
    37. return -1;
    38. }
    39. // 将socket设置为可连接(监听)的状态
    40. if (listen(listenfd, 5) != 0)
    41. {
    42. perror("listen failed.");
    43. close(listenfd);
    44. return -1;
    45. }
    46. // 受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待
    47. int clientfd = accept(listenfd, 0, 0);
    48. if (clientfd == -1)
    49. {
    50. perror("accept failed.");
    51. close(listenfd);
    52. return -1;
    53. }
    54. std::cout << "client connected." << std::endl;
    55. // 与客户端通信,接收客户端发过来的报文后,回复ok
    56. char buffer[1024];
    57. while (true)
    58. {
    59. int iret;
    60. memset(buffer, 0, sizeof(buffer));
    61. // 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待
    62. // 如果客户端已断开连接,recv()函数将返回0
    63. if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0)
    64. {
    65. std::cout << "iret = " << iret << std::endl;
    66. break;
    67. }
    68. std::cout << "received: " << buffer << std::endl;
    69. strcpy(buffer, "ok"); // 生成回应报文内容
    70. // 向客户端发送回应报文
    71. if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0)
    72. {
    73. perror("send failed.");
    74. break;
    75. }
    76. std::cout << "sent: " << buffer << std::endl;
    77. }
    78. // 关闭socket,释放资源
    79. close(listenfd); // 关闭服务端用于监听的socket
    80. close(clientfd); // 关闭客户端连上来的socket
    81. return 0;
    82. }
    复制代码

18.基于Linux的文件操作

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

  1. // filecw.cpp,本程序演示了Linux底层文件的操作-创建文件并写入数据
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. int main()
  8. {
  9. int fd; // 文件描述符
  10. // 打开文件,如果创建后的文件没有权限,可以手工授权 chmod 777 data.txt。
  11. fd = open("data.txt", O_CREAT | O_RDWR | O_TRUNC, 0666); // 添加文件权限参数0666
  12. if (fd == -1)
  13. {
  14. perror("open data.txt failed.");
  15. return -1;
  16. }
  17. printf("file descriptor fd = %d\n", fd);
  18. char buffer[1024];
  19. strcpy(buffer, "This is a sample text.\n");
  20. if (write(fd, buffer, strlen(buffer)) == -1)
  21. { // 把数据写入文件。
  22. perror("write failed.");
  23. return -1;
  24. }
  25. close(fd); // 关闭文件。
  26. return 0; // 添加返回值,表示程序执行成功
  27. }
复制代码

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

  1. // fileread.cpp,本程序演示了Linux底层文件的操作-读取文件。
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. int main()
  8. {
  9. int fd; // 定义一个文件描述符/文件句柄。
  10. fd = open("data.txt", O_RDONLY); // 打开文件。
  11. if (fd == -1)
  12. {
  13. perror("open data.txt failed.");
  14. return -1;
  15. }
  16. printf("文件描述符: fd = %d\n", fd);
  17. char buffer[1024];
  18. memset(buffer, 0, sizeof(buffer));
  19. if (read(fd, buffer, sizeof(buffer)) == -1) // 从文件中读取数据。
  20. {
  21. perror("write failed.");
  22. return -1;
  23. }
  24. printf("%s", buffer);
  25. close(fd); // 关闭文件。
  26. }
复制代码

19.socket()函数详解

1.什么是协议

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

2.创建socket

  • 包含头文件:

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

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

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

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

    • 使用以下命令查看:

      1. 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()函数使用实例:

    1. socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建tcp的sock
    2. 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语言提供了四个库函数,用于在主机字节序和网络字节序之间转换:

    • 包含头文件:

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

      1. /* 在主机和网络之间进行字节顺序转换的函数.
      2. 请注意这些函数通常使用 `unsigned long int' 或
      3. `unsigned short int' 值作为参数并返回它们. 但
      4. 这是一个目光短浅的决定,因为在不同的系统上类型不同
      5. 可能有不同的表示 但值总是相同的. */
      6. extern uint32_t ntohl (uint32_t __netlong) __THROW __attribute__ ((__const__));
      7. extern uint16_t ntohs (uint16_t __netshort) __THROW __attribute__ ((__const__));
      8. extern uint32_t htonl (uint32_t __hostlong) __THROW __attribute__ ((__const__));
      9. 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()函数需要这个结构体。

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

2.sockaddr_in结构体

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

    1. typedef unsigned short sa_family_t;
    2. #define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family
    3. /* 网络地址. */
    4. typedef uint32_t in_addr_t;
    5. struct in_addr
    6. {
    7. in_addr_t s_addr;
    8. };
    9. #define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
    10. typedef uint16_t in_port_t;
    11. /* 描述网络套接字地址的结构. */
    12. struct sockaddr_in
    13. {
    14. __SOCKADDR_COMMON (sin_);
    15. in_port_t sin_port; /* 端口号. */
    16. struct in_addr sin_addr; /* 网络地址. */
    17. /* 填充到 `struct sockaddr' 的大小. */
    18. unsigned char sin_zero[sizeof (struct sockaddr) -
    19. __SOCKADDR_COMMON_SIZE -
    20. sizeof (in_port_t) -
    21. sizeof (struct in_addr)];
    22. };
    复制代码

3.gethostbyname()函数

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

  • 包含头文件:

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

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

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

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

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

  • 包含头文件:

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

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

5.示例

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

    1. // 本程序演示了基于TCP协议的客户端通信
    2. #include <iostream>
    3. #include <cstdio>
    4. #include <cstring>
    5. #include <cstdlib>
    6. #include <unistd.h>
    7. #include <netdb.h>
    8. #include <sys/types.h>
    9. #include <sys/socket.h>
    10. #include <arpa/inet.h>
    11. int main(int argc, char *argv[])
    12. {
    13. if (argc != 3)
    14. {
    15. std::cout << "using: ./socket_client <服务端的IP> <服务端的端口>" << std::endl
    16. << "example: ./socket_client 192.168.101.138 5005" << std::endl;
    17. return -1;
    18. }
    19. // 第1步:创建客户端的socket。
    20. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    21. if (sockfd == -1)
    22. {
    23. perror("socket failed.");
    24. return -1;
    25. }
    26. // 第2步:向服务器发起连接请求。
    27. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。
    28. memset(&servaddr, 0, sizeof(servaddr));
    29. servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET。
    30. servaddr.sin_port = htons(atoi(argv[2])); // ②指定服务端的通信端口。
    31. struct hostent *hostent; // 用于存放服务端IP地址(大端序)的结构体的指针。
    32. if ((hostent = gethostbyname(argv[1])) == nullptr) // 把域名/主机名/字符串格式的IP转换成结构体。
    33. {
    34. std::cout << "gethostbyname failed." << std::endl;
    35. close(sockfd);
    36. return -1;
    37. }
    38. memcpy(&servaddr.sin_addr, hostent->h_addr, hostent->h_length); // ③指定服务端的IP(大端序)。
    39. // 向服务端发起连接请求。
    40. if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    41. {
    42. perror("connect failed.");
    43. close(sockfd);
    44. return -1;
    45. }
    46. // 第3步:与服务端通讯,客户端发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。
    47. char buffer[1024];
    48. for (int i = 0; i < 10; i++) // 循环10次,与服务端进行10次通讯。
    49. {
    50. int iret;
    51. memset(buffer, 0, sizeof(buffer));
    52. sprintf(buffer, "这是第 %d 个数据包,编号: %03d.", i + 1, i + 1); // 生成请求报文内容。
    53. // 向服务端发送请求报文。
    54. if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0)
    55. {
    56. perror("send failed.");
    57. break;
    58. }
    59. std::cout << "发送: " << buffer << std::endl;
    60. memset(buffer, 0, sizeof(buffer));
    61. // 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。
    62. if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0)
    63. {
    64. std::cout << "iret = " << iret << std::endl;
    65. break;
    66. }
    67. std::cout << "接收: " << buffer << std::endl;
    68. sleep(1); // 模拟处理时间
    69. }
    70. // 第4步:关闭socket,释放资源。
    71. close(sockfd);
    72. return 0;
    73. }
    复制代码
  • 基于TCP协议的服务端通信

    1. // 本程序演示了基于TCP协议的服务端通信
    2. #include <iostream>
    3. #include <cstdio>
    4. #include <cstring>
    5. #include <cstdlib>
    6. #include <unistd.h>
    7. #include <netdb.h>
    8. #include <sys/types.h>
    9. #include <sys/socket.h>
    10. #include <arpa/inet.h>
    11. int main(int argc, char *argv[])
    12. {
    13. if (argc != 2)
    14. {
    15. std::cout << "using: ./socket_server <通讯端口>" << std::endl
    16. << "example: ./socket_server 5005" << std::endl;
    17. return -1;
    18. }
    19. // 第1步:创建服务端的socket。
    20. int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    21. if (listenfd == -1)
    22. {
    23. perror("socket failed.");
    24. return -1;
    25. }
    26. // 第2步:把服务端用于通信的IP和端口绑定到socket上。
    27. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。
    28. memset(&servaddr, 0, sizeof(servaddr));
    29. servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET。
    30. servaddr.sin_port = htons(std::atoi(argv[1])); // ②指定服务端的通信端口。
    31. servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③服务端任意网卡的IP都可以用于通讯。
    32. // 绑定服务端的IP和端口。
    33. if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    34. {
    35. perror("bind failed.");
    36. close(listenfd);
    37. return -1;
    38. }
    39. // 第3步:把socket设置为可连接(监听)的状态。
    40. if (listen(listenfd, 5) == -1)
    41. {
    42. perror("listen failed.");
    43. close(listenfd);
    44. return -1;
    45. }
    46. // 第4步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。
    47. int clientfd = accept(listenfd, nullptr, nullptr);
    48. if (clientfd == -1)
    49. {
    50. perror("accept failed.");
    51. close(listenfd);
    52. return -1;
    53. }
    54. std::cout << "客户端已连接." << std::endl;
    55. // 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
    56. char buffer[1024];
    57. while (true)
    58. {
    59. int iret;
    60. memset(buffer, 0, sizeof(buffer));
    61. // 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。
    62. // 如果客户端已断开连接,recv()函数将返回0。
    63. if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0)
    64. {
    65. std::cout << "iret = " << iret << std::endl;
    66. break;
    67. }
    68. std::cout << "接收: " << buffer << std::endl;
    69. strcpy(buffer, "ok"); // 生成回应报文内容。
    70. // 向客户端发送回应报文。
    71. if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0)
    72. {
    73. perror("send failed.");
    74. break;
    75. }
    76. std::cout << "发送: " << buffer << std::endl;
    77. }
    78. // 第6步:关闭socket,释放资源。
    79. close(listenfd); // 关闭服务端用于监听的socket。
    80. close(clientfd); // 关闭客户端连上来的socket。
    81. return 0;
    82. }
    复制代码

22.封装socket

  • 封装socket通讯的客户端

    1. // tcp_clientcpp - 基于TCP协议的客户端通信.
    2. #include <iostream>
    3. #include <cstdio>
    4. #include <cstring>
    5. #include <cstdlib>
    6. #include <unistd.h>
    7. #include <netdb.h>
    8. #include <sys/types.h>
    9. #include <sys/socket.h>
    10. #include <arpa/inet.h>
    11. class TCPClient // TCP通讯的客户端类.
    12. {
    13. private:
    14. int client_fd; // 客户端的socket,-1 表示未连接或连接已断开; >= 0 表示有效的socket.
    15. std::string ip; // 服务端的IP/域名.
    16. unsigned short port; // 通讯端口.
    17. public:
    18. TCPClient() : client_fd(-1) {}
    19. // 向服务端发起连接请求,成功返回true,失败返回false.
    20. bool connect(const std::string &in_ip, const unsigned short in_port)
    21. {
    22. if (client_fd != -1)
    23. return false; // 如果socket已连接,直接返回失败.
    24. ip = in_ip;
    25. port = in_port; // 把服务端的IP和端口保存到成员变量中.
    26. // 第1步:创建客户端的socket.
    27. if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    28. return false;
    29. // 第2步:向服务器发起连接请求.
    30. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体.
    31. memset(&servaddr, 0, sizeof(servaddr));
    32. servaddr.sin_family = AF_INET; // ①协议族,固定填 AF_INET.
    33. servaddr.sin_port = htons(port); // ②指定服务端的通信端口.
    34. struct hostent *h; // 用于存放服务端IP地址(大端序)的结构体的指针.
    35. if ((h = gethostbyname(ip.c_str())) == nullptr) // 把域名/主机名/字符串格式的IP转换成结构体.
    36. {
    37. ::close(client_fd);
    38. client_fd = -1;
    39. return false;
    40. }
    41. memcpy(&servaddr.sin_addr, h->h_addr, h->h_length); // ③指定服务端的IP(大端序).
    42. // 向服务端发起连接请求.
    43. if (::connect(client_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    44. {
    45. ::close(client_fd);
    46. client_fd = -1;
    47. return false;
    48. }
    49. return true;
    50. }
    51. // 向服务端发送报文,成功返回true,失败返回false.
    52. bool send(const std::string &buffer) // buffer不要用const char*
    53. {
    54. if (client_fd == -1)
    55. return false; // 如果socket的状态是未连接,直接返回失败.
    56. if ((::send(client_fd, buffer.data(), buffer.size(), 0)) <= 0)
    57. return false;
    58. return true;
    59. }
    60. // 接收服务端的报文,成功返回true,失败返回false.
    61. // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度.
    62. bool recv(std::string &buffer, const size_t maxlen)
    63. {
    64. buffer.clear(); // 清空容器.
    65. buffer.resize(maxlen); // 设置容器的大小为maxlen.
    66. int readn = ::recv(client_fd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存.
    67. if (readn <= 0)
    68. {
    69. buffer.clear();
    70. return false;
    71. }
    72. buffer.resize(readn); // 重置buffer的实际大小.
    73. return true;
    74. }
    75. // 断开与服务端的连接.
    76. bool close()
    77. {
    78. if (client_fd == -1)
    79. return false; // 如果socket的状态是未连接,直接返回失败.
    80. ::close(client_fd);
    81. client_fd = -1;
    82. return true;
    83. }
    84. ~TCPClient() { close(); }
    85. };
    86. int main(int argc, char *argv[])
    87. {
    88. if (argc != 3)
    89. {
    90. std::cout << "using: ./tcp_client <服务端的IP> <服务端的端口>" << std::endl
    91. << "example: ./tcp_client 192.168.101.138 5005" << std::endl;
    92. return -1;
    93. }
    94. TCPClient tcpClient;
    95. if (tcpClient.connect(argv[1], std::atoi(argv[2])) == false) // 向服务端发起连接请求.
    96. {
    97. perror("connect failed.");
    98. return -1;
    99. }
    100. // 第3步:与服务端通讯,客户端发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文.
    101. std::string buffer;
    102. for (int i = 0; i < 10; i++) // 循环10次,与服务端进行10次通讯.
    103. {
    104. buffer = "这是第 " + std::to_string(i + 1) + " 个数据包, 编号: " + std::to_string(i + 1) + ".";
    105. // 向服务端发送请求报文.
    106. if (tcpClient.send(buffer) == false)
    107. {
    108. perror("send failed.");
    109. break;
    110. }
    111. std::cout << "发送: " << buffer << std::endl;
    112. // 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待.
    113. if (tcpClient.recv(buffer, 1024) == false)
    114. {
    115. perror("recv failed.");
    116. break;
    117. }
    118. std::cout << "接收: " << buffer << std::endl;
    119. sleep(1);
    120. }
    121. return 0;
    122. }
    复制代码
  • 基于TCP协议的服务端通信

    1. // tcp_server.cpp - 基于TCP协议的服务端通信.
    2. #include <iostream>
    3. #include <cstdio>
    4. #include <cstring>
    5. #include <cstdlib>
    6. #include <unistd.h>
    7. #include <netdb.h>
    8. #include <sys/types.h>
    9. #include <sys/socket.h>
    10. #include <arpa/inet.h>
    11. class TCPServer // TCP通讯的服务端类.
    12. {
    13. private:
    14. int listen_fd; // 监听的socket,-1表示未初始化.
    15. int client_fd; // 客户端连上来的socket,-1表示客户端未连接.
    16. std::string client_ip; // 客户端字符串格式的IP.
    17. unsigned short port; // 服务端用于通讯的端口.
    18. public:
    19. TCPServer() : listen_fd(-1), client_fd(-1) {}
    20. // 初始化服务端用于监听的socket.
    21. bool initServer(const unsigned short in_port)
    22. {
    23. // 第1步:创建服务端的socket.
    24. if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    25. return false;
    26. port = in_port;
    27. // 第2步:把服务端用于通信的IP和端口绑定到socket上.
    28. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体.
    29. memset(&servaddr, 0, sizeof(servaddr));
    30. servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET.
    31. servaddr.sin_port = htons(port); // ②指定服务端的通信端口.
    32. servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯.
    33. // 绑定服务端的IP和端口(为socket分配IP和端口).
    34. if (bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    35. {
    36. close(listen_fd);
    37. listen_fd = -1;
    38. return false;
    39. }
    40. // 第3步:把socket设置为可连接(监听)的状态.
    41. if (listen(listen_fd, 5) == -1)
    42. {
    43. close(listen_fd);
    44. listen_fd = -1;
    45. return false;
    46. }
    47. return true;
    48. }
    49. // 受理客户端的连接(从已连接的客户端中取出一个客户端),
    50. // 如果没有已连接的客户端,accept()函数将阻塞等待.
    51. bool acceptConnection()
    52. {
    53. struct sockaddr_in caddr; // 客户端的地址信息.
    54. socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小.
    55. if ((client_fd = ::accept(listen_fd, (struct sockaddr *)&caddr, &addrlen)) == -1)
    56. return false;
    57. client_ip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串.
    58. return true;
    59. }
    60. // 获取客户端的IP(字符串格式).
    61. const std::string &getClientIP() const
    62. {
    63. return client_ip;
    64. }
    65. // 向对端发送报文,成功返回true,失败返回false.
    66. bool sendMessage(const std::string &buffer)
    67. {
    68. if (client_fd == -1)
    69. return false;
    70. if ((::send(client_fd, buffer.data(), buffer.size(), 0)) <= 0)
    71. return false;
    72. return true;
    73. }
    74. // 接收对端的报文,成功返回true,失败返回false.
    75. // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度.
    76. bool receiveMessage(std::string &buffer, const size_t maxlen)
    77. {
    78. buffer.clear(); // 清空容器.
    79. buffer.resize(maxlen); // 设置容器的大小为maxlen.
    80. int readn = ::recv(client_fd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存.
    81. if (readn <= 0)
    82. {
    83. buffer.clear();
    84. return false;
    85. }
    86. buffer.resize(readn); // 重置buffer的实际大小.
    87. return true;
    88. }
    89. // 关闭监听的socket.
    90. bool closeListenSocket()
    91. {
    92. if (listen_fd == -1)
    93. return false;
    94. ::close(listen_fd);
    95. listen_fd = -1;
    96. return true;
    97. }
    98. // 关闭客户端连上来的socket.
    99. bool closeClientSocket()
    100. {
    101. if (client_fd == -1)
    102. return false;
    103. ::close(client_fd);
    104. client_fd = -1;
    105. return true;
    106. }
    107. ~TCPServer()
    108. {
    109. closeListenSocket();
    110. closeClientSocket();
    111. }
    112. };
    113. int main(int argc, char *argv[])
    114. {
    115. if (argc != 2)
    116. {
    117. std::cout << "using: ./tcp_server <通讯端口>" << std::endl
    118. << "example: ./ tcp_server 5005" << std::endl; // 端口大于1024,不与其它的重复.
    119. return -1;
    120. }
    121. TCPServer tcpServer;
    122. if (tcpServer.initServer(std::atoi(argv[1])) == false) // 初始化服务端用于监听的socket.
    123. {
    124. perror("initServer failed");
    125. return -1;
    126. }
    127. // 受理客户端的连接(从已连接的客户端中取出一个客户端),
    128. // 如果没有已连接的客户端,accept()函数将阻塞等待.
    129. if (tcpServer.acceptConnection() == false)
    130. {
    131. perror("acceptConnection failed.");
    132. return -1;
    133. }
    134. std::cout << "客户端已连接( " << tcpServer.getClientIP() << " )." << std::endl;
    135. std::string buffer;
    136. while (true)
    137. {
    138. // 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待.
    139. if (tcpServer.receiveMessage(buffer, 1024) == false)
    140. {
    141. perror("receiveMessage failed.");
    142. break;
    143. }
    144. std::cout << "接收: " << buffer << std::endl;
    145. buffer = "ok";
    146. if (tcpServer.sendMessage(buffer) == false) // 向对端发送报文.
    147. {
    148. perror("sendMessage failed.");
    149. break;
    150. }
    151. std::cout << "发送: " << buffer << std::endl;
    152. }
    153. return 0;
    154. }
    复制代码

23.多进程的网络服务端

  • 示例:

    1. // multiprocess_tcpserver.cpp - 基于TCP协议的服务端通信,支持多客户端连接.
    2. #include <iostream>
    3. #include <cstdio>
    4. #include <cstring>
    5. #include <cstdlib>
    6. #include <unistd.h>
    7. #include <netdb.h>
    8. #include <signal.h>
    9. #include <sys/types.h>
    10. #include <sys/socket.h>
    11. #include <arpa/inet.h>
    12. class ctcpserver // TCP通讯的服务端类.
    13. {
    14. private:
    15. int m_listenfd; // 监听的socket,-1表示未初始化.
    16. int m_clientfd; // 客户端连上来的socket,-1表示客户端未连接.
    17. std::string m_clientip; // 客户端字符串格式的IP.
    18. unsigned short m_port; // 服务端用于通讯的端口.
    19. public:
    20. ctcpserver() : m_listenfd(-1), m_clientfd(-1) {}
    21. // 初始化服务端用于监听的socket.
    22. bool initserver(const unsigned short in_port)
    23. {
    24. // 第1步:创建服务端的socket.
    25. if ((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    26. return false;
    27. m_port = in_port;
    28. // 第2步:把服务端用于通信的IP和端口绑定到socket上.
    29. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体.
    30. memset(&servaddr, 0, sizeof(servaddr));
    31. servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET.
    32. servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口.
    33. servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯.
    34. // 绑定服务端的IP和端口(为socket分配IP和端口).
    35. if (bind(m_listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    36. {
    37. close(m_listenfd);
    38. m_listenfd = -1;
    39. return false;
    40. }
    41. // 第3步:把socket设置为可连接(监听)的状态.
    42. if (listen(m_listenfd, 5) == -1)
    43. {
    44. close(m_listenfd);
    45. m_listenfd = -1;
    46. return false;
    47. }
    48. return true;
    49. }
    50. // 受理客户端的连接(从已连接的客户端中取出一个客户端),
    51. // 如果没有已连接的客户端,accept()函数将阻塞等待.
    52. bool accept()
    53. {
    54. struct sockaddr_in caddr; // 客户端的地址信息.
    55. socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小.
    56. if ((m_clientfd = ::accept(m_listenfd, (struct sockaddr *)&caddr, &addrlen)) == -1)
    57. return false;
    58. m_clientip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串.
    59. return true;
    60. }
    61. // 获取客户端的IP(字符串格式).
    62. const std::string &clientip() const
    63. {
    64. return m_clientip;
    65. }
    66. // 向对端发送报文,成功返回true,失败返回false.
    67. bool send(const std::string &buffer)
    68. {
    69. if (m_clientfd == -1)
    70. return false;
    71. if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0)
    72. return false;
    73. return true;
    74. }
    75. // 接收对端的报文,成功返回true,失败返回false.
    76. // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度.
    77. bool recv(std::string &buffer, const size_t maxlen)
    78. {
    79. buffer.clear(); // 清空容器.
    80. buffer.resize(maxlen); // 设置容器的大小为maxlen.
    81. int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存.
    82. if (readn <= 0)
    83. {
    84. buffer.clear();
    85. return false;
    86. }
    87. buffer.resize(readn); // 重置buffer的实际大小.
    88. return true;
    89. }
    90. // 关闭监听的socket.
    91. bool closelisten()
    92. {
    93. if (m_listenfd == -1)
    94. return false;
    95. ::close(m_listenfd);
    96. m_listenfd = -1;
    97. return true;
    98. }
    99. // 关闭客户端连上来的socket.
    100. bool closeclient()
    101. {
    102. if (m_clientfd == -1)
    103. return false;
    104. ::close(m_clientfd);
    105. m_clientfd = -1;
    106. return true;
    107. }
    108. ~ctcpserver()
    109. {
    110. closelisten();
    111. closeclient();
    112. }
    113. };
    114. ctcpserver tcpserver;
    115. void FatherEXIT(int sig); // 父进程的信号处理函数.
    116. void ChildEXIT(int sig); // 子进程的信号处理函数.
    117. int main(int argc, char *argv[])
    118. {
    119. if (argc != 2)
    120. {
    121. std::cout << "using: ./muitilprocess_tcpserver 通讯端口" << std::endl
    122. << "example: ./muitilprocess_tcpserver 5005" << std::endl;
    123. return -1;
    124. }
    125. // 忽略全部的信号,不希望被打扰.顺便解决了僵尸进程的问题.
    126. for (int ii = 1; ii <= 64; ii++)
    127. signal(ii, SIG_IGN);
    128. // 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程
    129. // 但请不要用 "kill -9 +进程号" 强行终止
    130. signal(SIGTERM, FatherEXIT);
    131. signal(SIGINT, FatherEXIT); // SIGTERM 15 SIGINT 2
    132. if (tcpserver.initserver(atoi(argv[1])) == false) // 初始化服务端用于监听的socket.
    133. {
    134. perror("initserver failed.");
    135. return -1;
    136. }
    137. while (true)
    138. {
    139. // 受理客户端的连接(从已连接的客户端中取出一个客户端),
    140. // 如果没有已连接的客户端,accept()函数将阻塞等待.
    141. if (tcpserver.accept() == false)
    142. {
    143. perror("accept failed.");
    144. return -1;
    145. }
    146. int pid = fork();
    147. if (pid == -1)
    148. {
    149. perror("fork failed.");
    150. return -1;
    151. } // 系统资源不足.
    152. if (pid > 0)
    153. { // 父进程.
    154. tcpserver.closeclient(); // 父进程关闭客户端连接的socket.
    155. continue; // 父进程返回到循环开始的位置,继续受理客户端的连接.
    156. }
    157. tcpserver.closelisten(); // 子进程关闭监听的socket.
    158. // 子进程需要重新设置信号.
    159. signal(SIGTERM, ChildEXIT); // 子进程的退出函数与父进程不一样.
    160. signal(SIGINT, SIG_IGN); // 子进程不需要捕获SIGINT信号.
    161. // 子进程负责与客户端进行通讯.
    162. std::cout << "客户端已连接( " << tcpserver.clientip() << " )." << std::endl;
    163. std::string buffer;
    164. while (true)
    165. {
    166. // 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待.
    167. if (tcpserver.recv(buffer, 1024) == false)
    168. {
    169. perror("recv()");
    170. break;
    171. }
    172. std::cout << "接收: " << buffer << std::endl;
    173. buffer = "ok";
    174. if (tcpserver.send(buffer) == false) // 向对端发送报文.
    175. {
    176. perror("send");
    177. break;
    178. }
    179. std::cout << "发送: " << buffer << std::endl;
    180. }
    181. return 0; // 子进程一定要退出,否则又会回到accept()函数的位置.
    182. }
    183. }
    184. // 父进程的信号处理函数.
    185. void FatherEXIT(int sig)
    186. {
    187. // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断.
    188. signal(SIGINT, SIG_IGN);
    189. signal(SIGTERM, SIG_IGN);
    190. std::cout << "父进程退出,sig = " << sig << std::endl;
    191. kill(0, SIGTERM); // 向全部的子进程发送15的信号,通知它们退出.
    192. // 在这里增加释放资源的代码(全局的资源).
    193. tcpserver.closelisten(); // 父进程关闭监听的socket.
    194. exit(0);
    195. }
    196. // 子进程的信号处理函数.
    197. void ChildEXIT(int sig)
    198. {
    199. // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断.
    200. signal(SIGINT, SIG_IGN);
    201. signal(SIGTERM, SIG_IGN);
    202. std::cout << "子进程: " << getpid() << "退出,sig = " << sig << std::endl;
    203. // 在这里增加释放资源的代码(只释放子进程的资源).
    204. tcpserver.closeclient(); // 子进程关闭客户端连上来的socket.
    205. exit(0);
    206. }
    复制代码

24.实现文件传输功能

  • 实现文件传输的客户端

    1. #include <iostream>
    2. #include <fstream>
    3. #include <cstdio>
    4. #include <cstring>
    5. #include <cstdlib>
    6. #include <unistd.h>
    7. #include <netdb.h>
    8. #include <sys/types.h>
    9. #include <sys/socket.h>
    10. #include <arpa/inet.h>
    11. class ctcpclient // TCP通讯的客户端类.
    12. {
    13. private:
    14. int m_clientfd; // 客户端的socket,-1表示未连接或连接已断开;>=0表示有效的socket.
    15. std::string m_ip; // 服务端的IP/域名.
    16. unsigned short m_port; // 通讯端口.
    17. public:
    18. ctcpclient() : m_clientfd(-1) {}
    19. // 向服务端发起连接请求,成功返回true,失败返回false.
    20. bool connect(const std::string &in_ip, const unsigned short in_port)
    21. {
    22. if (m_clientfd != -1)
    23. return false; // 如果socket已连接,直接返回失败.
    24. m_ip = in_ip;
    25. m_port = in_port; // 把服务端的IP和端口保存到成员变量中.
    26. // 第1步:创建客户端的socket.
    27. if ((m_clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    28. return false;
    29. // 第2步:向服务器发起连接请求.
    30. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体.
    31. memset(&servaddr, 0, sizeof(servaddr));
    32. servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET.
    33. servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口.
    34. struct hostent *h; // 用于存放服务端IP地址(大端序)的结构体的指针.
    35. if ((h = gethostbyname(m_ip.c_str())) == nullptr) // 把域名/主机名/字符串格式的IP转换成结构体.
    36. {
    37. ::close(m_clientfd);
    38. m_clientfd = -1;
    39. return false;
    40. }
    41. memcpy(&servaddr.sin_addr, h->h_addr, h->h_length); // ③指定服务端的IP(大端序).
    42. // 向服务端发起连接请求.
    43. if (::connect(m_clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    44. {
    45. ::close(m_clientfd);
    46. m_clientfd = -1;
    47. return false;
    48. }
    49. return true;
    50. }
    51. // 向服务端发送报文(字符串),成功返回true,失败返回false.
    52. bool send(const std::string &buffer) // buffer不要用const char *
    53. {
    54. if (m_clientfd == -1)
    55. return false; // 如果socket的状态是未连接,直接返回失败.
    56. if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0)
    57. return false;
    58. return true;
    59. }
    60. // 向服务端发送报文(二进制数据),成功返回true,失败返回false.
    61. bool send(void *buffer, const size_t size)
    62. {
    63. if (m_clientfd == -1)
    64. return false; // 如果socket的状态是未连接,直接返回失败.
    65. if ((::send(m_clientfd, buffer, size, 0)) <= 0)
    66. return false;
    67. return true;
    68. }
    69. // 接收服务端的报文,成功返回true,失败返回false.
    70. // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度.
    71. bool recv(std::string &buffer, const size_t maxlen)
    72. {
    73. buffer.clear(); // 清空容器.
    74. buffer.resize(maxlen); // 设置容器的大小为maxlen.
    75. int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存.
    76. if (readn <= 0)
    77. {
    78. buffer.clear();
    79. return false;
    80. }
    81. buffer.resize(readn); // 重置buffer的实际大小.
    82. return true;
    83. }
    84. // 断开与服务端的连接.
    85. bool close()
    86. {
    87. if (m_clientfd == -1)
    88. return false; // 如果socket的状态是未连接,直接返回失败.
    89. ::close(m_clientfd);
    90. m_clientfd = -1;
    91. return true;
    92. }
    93. // 向服务端发送文件内容.
    94. bool sendfile(const std::string &filename, const size_t filesize)
    95. {
    96. // 以二进制的方式打开文件.
    97. std::ifstream fin(filename, std::ios::binary);
    98. if (fin.is_open() == false)
    99. {
    100. std::cout << "打开文件: " << filename << " 失败." << std::endl;
    101. return false;
    102. }
    103. int onread = 0; // 每次调用fin.read()时打算读取的字节数.
    104. int totalbytes = 0; // 从文件中已读取的字节总数.
    105. char buffer[4096]; // 存放读取数据的buffer.
    106. while (true)
    107. {
    108. memset(buffer, 0, sizeof(buffer));
    109. // 计算本次应该读取的字节数,如果剩余的数据超过4096字节,就读4096字节.
    110. if (filesize - totalbytes > 4096)
    111. onread = 4096;
    112. else
    113. onread = filesize - totalbytes;
    114. // 从文件中读取数据.
    115. fin.read(buffer, onread);
    116. // 把读取到的数据发送给对端.
    117. if (send(buffer, onread) == false)
    118. return false;
    119. // 计算文件已读取的字节总数,如果文件已读完,跳出循环.
    120. totalbytes += onread;
    121. if (totalbytes == filesize)
    122. break;
    123. }
    124. return true;
    125. }
    126. ~ctcpclient() { close(); }
    127. };
    128. int main(int argc, char *argv[])
    129. {
    130. if (argc != 5)
    131. {
    132. std::cout << "using: ./sendfile_tcpclient 服务端的IP 服务端的端口 文件名 文件大小" << std::endl;
    133. std::cout << "example: ./sendfile_tcpclient 192.168.101.138 5005 test.txt 2424" << std::endl
    134. << std::endl;
    135. return -1;
    136. }
    137. ctcpclient tcpclient;
    138. if (tcpclient.connect(argv[1], atoi(argv[2])) == false) // 向服务端发起连接请求.
    139. {
    140. perror("connect failed.");
    141. return -1;
    142. }
    143. // 以下是发送文件的流程.
    144. // 1)把待传输文件名和文件的大小告诉服务端.
    145. // 定义文件信息的结构体.
    146. struct st_fileinfo
    147. {
    148. char filename[256]; // 文件名.
    149. int filesize; // 文件大小.
    150. } fileinfo;
    151. memset(&fileinfo, 0, sizeof(fileinfo));
    152. strncpy(fileinfo.filename, argv[3], sizeof(fileinfo.filename) - 1); // 文件名.
    153. fileinfo.filesize = atoi(argv[4]); // 文件大小.
    154. // 把文件信息的结构体发送给服务端.
    155. if (tcpclient.send(&fileinfo, sizeof(fileinfo)) == false)
    156. {
    157. perror("send failed.");
    158. return -1;
    159. }
    160. std::cout << "发送文件信息的结构体: " << fileinfo.filename << " ( " << fileinfo.filesize << " )." << std::endl;
    161. // 2)等待服务端的确认报文(文件名和文件的大小的确认).
    162. std::string buffer;
    163. if (tcpclient.recv(buffer, 2) == false)
    164. {
    165. perror("recv failed.");
    166. return -1;
    167. }
    168. if (buffer != "ok")
    169. {
    170. std::cout << "服务端没有回复ok." << std::endl;
    171. return -1;
    172. }
    173. // 3)发送文件内容.
    174. if (tcpclient.sendfile(fileinfo.filename, fileinfo.filesize) == false)
    175. {
    176. perror("sendfile failed.");
    177. return -1;
    178. }
    179. // 4)等待服务端的确认报文(服务端已接收完文件).
    180. if (tcpclient.recv(buffer, 2) == false)
    181. {
    182. perror("recv failed.");
    183. return -1;
    184. }
    185. if (buffer != "ok")
    186. {
    187. std::cout << "发送文件内容失败." << std::endl;
    188. return -1;
    189. }
    190. std::cout << "发送文件内容成功." << std::endl;
    191. return 0;
    192. }
    复制代码
  • 实现文件传输的服务端

    1. #include <iostream>
    2. #include <fstream>
    3. #include <cstdio>
    4. #include <cstring>
    5. #include <cstdlib>
    6. #include <unistd.h>
    7. #include <netdb.h>
    8. #include <signal.h>
    9. #include <sys/types.h>
    10. #include <sys/socket.h>
    11. #include <arpa/inet.h>
    12. class ctcpserver // TCP通讯的服务端类.
    13. {
    14. private:
    15. int m_listenfd; // 监听的socket,-1表示未初始化.
    16. int m_clientfd; // 客户端连上来的socket,-1表示客户端未连接.
    17. std::string m_clientip; // 客户端字符串格式的IP.
    18. unsigned short m_port; // 服务端用于通讯的端口.
    19. public:
    20. ctcpserver() : m_listenfd(-1), m_clientfd(-1) {}
    21. // 初始化服务端用于监听的socket.
    22. bool initserver(const unsigned short in_port)
    23. {
    24. // 第1步:创建服务端的socket.
    25. if ((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    26. return false;
    27. m_port = in_port;
    28. // 第2步:把服务端用于通信的IP和端口绑定到socket上.
    29. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体.
    30. memset(&servaddr, 0, sizeof(servaddr));
    31. servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET.
    32. servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口.
    33. servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯.
    34. // 绑定服务端的IP和端口(为socket分配IP和端口).
    35. if (bind(m_listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    36. {
    37. close(m_listenfd);
    38. m_listenfd = -1;
    39. return false;
    40. }
    41. // 第3步:把socket设置为可连接(监听)的状态.
    42. if (listen(m_listenfd, 5) == -1)
    43. {
    44. close(m_listenfd);
    45. m_listenfd = -1;
    46. return false;
    47. }
    48. return true;
    49. }
    50. // 受理客户端的连接(从已连接的客户端中取出一个客户端),
    51. // 如果没有已连接的客户端,accept()函数将阻塞等待.
    52. bool accept()
    53. {
    54. struct sockaddr_in caddr; // 客户端的地址信息.
    55. socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小.
    56. if ((m_clientfd = ::accept(m_listenfd, (struct sockaddr *)&caddr, &addrlen)) == -1)
    57. return false;
    58. m_clientip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串.
    59. return true;
    60. }
    61. // 获取客户端的IP(字符串格式).
    62. const std::string &clientip() const
    63. {
    64. return m_clientip;
    65. }
    66. // 向对端发送报文,成功返回true,失败返回false.
    67. bool send(const std::string &buffer)
    68. {
    69. if (m_clientfd == -1)
    70. return false;
    71. if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0)
    72. return false;
    73. return true;
    74. }
    75. // 接收对端的报文(字符串),成功返回true,失败返回false.
    76. // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度.
    77. bool recv(std::string &buffer, const size_t maxlen)
    78. {
    79. buffer.clear(); // 清空容器.
    80. buffer.resize(maxlen); // 设置容器的大小为maxlen.
    81. int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存.
    82. if (readn <= 0)
    83. {
    84. buffer.clear();
    85. return false;
    86. }
    87. buffer.resize(readn); // 重置buffer的实际大小.
    88. return true;
    89. }
    90. // 接收客户端的报文(二进制数据),成功返回true,失败返回false.
    91. // buffer-存放接收到的报文的内容,size-本次接收报文的最大长度.
    92. bool recv(void *buffer, const size_t size)
    93. {
    94. if (::recv(m_clientfd, buffer, size, 0) <= 0)
    95. return false;
    96. return true;
    97. }
    98. // 关闭监听的socket.
    99. bool closelisten()
    100. {
    101. if (m_listenfd == -1)
    102. return false;
    103. ::close(m_listenfd);
    104. m_listenfd = -1;
    105. return true;
    106. }
    107. // 关闭客户端连上来的socket.
    108. bool closeclient()
    109. {
    110. if (m_clientfd == -1)
    111. return false;
    112. ::close(m_clientfd);
    113. m_clientfd = -1;
    114. return true;
    115. }
    116. // 接收文件内容.
    117. bool recvfile(const std::string &filename, const size_t filesize)
    118. {
    119. std::ofstream fout;
    120. fout.open(filename, std::ios::binary);
    121. if (fout.is_open() == false)
    122. {
    123. std::cout << "Failed to open file: " << filename << "." << std::endl;
    124. return false;
    125. }
    126. int totalbytes = 0; // 已接收文件的总字节数.
    127. int onread = 0; // 本次打算接收的字节数.
    128. char buffer[4096]; // 接收文件内容的缓冲区.
    129. while (true)
    130. {
    131. // 计算本次应该接收的字节数.
    132. if (filesize - totalbytes > 4096)
    133. onread = 4096;
    134. else
    135. onread = filesize - totalbytes;
    136. // 接收文件内容.
    137. if (recv(buffer, onread) == false)
    138. return false;
    139. // 把接收到的内容写入文件.
    140. fout.write(buffer, onread);
    141. // 计算已接收文件的总字节数,如果文件接收完,跳出循环.
    142. totalbytes = totalbytes + onread;
    143. if (totalbytes == filesize)
    144. break;
    145. }
    146. return true;
    147. }
    148. ~ctcpserver()
    149. {
    150. closelisten();
    151. closeclient();
    152. }
    153. };
    154. ctcpserver tcpserver;
    155. void FatherEXIT(int sig); // 父进程的信号处理函数.
    156. void ChildEXIT(int sig); // 子进程的信号处理函数.
    157. int main(int argc, char *argv[])
    158. {
    159. if (argc != 3)
    160. {
    161. std::cout << "using: ./sendfile_tcpserver 通讯端口 文件存放的目录" << std::endl;
    162. std::cout << "example: ./sendfile_tcpserver 5005 /tmp" << std::endl
    163. << std::endl;
    164. return -1;
    165. }
    166. // 忽略全部的信号,不希望被打扰.顺便解决了僵尸进程的问题.
    167. for (int ii = 1; ii <= 64; ii++)
    168. signal(ii, SIG_IGN);
    169. // 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程
    170. // 但请不要用 "kill -9 +进程号" 强行终止
    171. signal(SIGTERM, FatherEXIT);
    172. signal(SIGINT, FatherEXIT); // SIGTERM 15 SIGINT 2
    173. if (tcpserver.initserver(atoi(argv[1])) == false) // 初始化服务端用于监听的socket.
    174. {
    175. perror("initserver failed.");
    176. return -1;
    177. }
    178. while (true)
    179. {
    180. // 受理客户端的连接(从已连接的客户端中取出一个客户端),
    181. // 如果没有已连接的客户端,accept()函数将阻塞等待.
    182. if (tcpserver.accept() == false)
    183. {
    184. perror("accept failed.");
    185. return -1;
    186. }
    187. int pid = fork();
    188. if (pid == -1)
    189. {
    190. perror("fork failed.");
    191. return -1;
    192. } // 系统资源不足.
    193. if (pid > 0)
    194. { // 父进程.
    195. tcpserver.closeclient(); // 父进程关闭客户端连接的socket.
    196. continue; // 父进程返回到循环开始的位置,继续受理客户端的连接.
    197. }
    198. tcpserver.closelisten(); // 子进程关闭监听的socket.
    199. // 子进程需要重新设置信号.
    200. signal(SIGTERM, ChildEXIT); // 子进程的退出函数与父进程不一样.
    201. signal(SIGINT, SIG_IGN); // 子进程不需要捕获SIGINT信号.
    202. // 子进程负责与客户端进行通讯.
    203. std::cout << "Client connected: ( " << tcpserver.clientip() << " )." << std::endl;
    204. // 以下是接收文件的流程.
    205. // 1)接收文件名和文件大小信息.
    206. // 定义文件信息的结构体.
    207. struct st_fileinfo
    208. {
    209. char filename[256]; // 文件名.
    210. int filesize; // 文件大小.
    211. } fileinfo;
    212. memset(&fileinfo, 0, sizeof(fileinfo));
    213. // 用结构体存放接收报文的内容.
    214. if (tcpserver.recv(&fileinfo, sizeof(fileinfo)) == false)
    215. {
    216. perror("recv()");
    217. return -1;
    218. }
    219. std::cout << "File info: " << fileinfo.filename << " ( " << fileinfo.filesize << " )." << std::endl;
    220. // 2)给客户端回复确认报文,表示客户端可以发送文件了.
    221. if (tcpserver.send("ok") == false)
    222. {
    223. perror("send failed.");
    224. break;
    225. }
    226. // 3)接收文件内容.
    227. if (tcpserver.recvfile(std::string(argv[2]) + "/" + fileinfo.filename, fileinfo.filesize) == false)
    228. {
    229. std::cout << "Failed to receive file content." << std::endl;
    230. return -1;
    231. }
    232. std::cout << "File content received successfully." << std::endl;
    233. // 4)给客户端回复确认报文,表示文件已接收成功.
    234. tcpserver.send("ok");
    235. return 0; // 子进程一定要退出,否则又会回到accept()函数的位置.
    236. }
    237. }
    238. // 父进程的信号处理函数.
    239. void FatherEXIT(int sig)
    240. {
    241. // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断.
    242. signal(SIGINT, SIG_IGN);
    243. signal(SIGTERM, SIG_IGN);
    244. std::cout << "Parent process exiting, sig = " << sig << std::endl;
    245. kill(0, SIGTERM); // 向全部的子进程发送15的信号,通知它们退出.
    246. // 在这里增加释放资源的代码(全局的资源).
    247. tcpserver.closelisten(); // 父进程关闭监听的socket.
    248. exit(0);
    249. }
    250. // 子进程的信号处理函数.
    251. void ChildEXIT(int sig)
    252. {
    253. // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断.
    254. signal(SIGINT, SIG_IGN);
    255. signal(SIGTERM, SIG_IGN);
    256. std::cout << "Child process: " << getpid() << " exiting, sig = " << sig << std::endl;
    257. // 在这里增加释放资源的代码(只释放子进程的资源).
    258. tcpserver.closeclient(); // 子进程关闭客户端连上来的socket.
    259. exit(0);
    260. }
    复制代码

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()之前)

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

26.TCP缓存

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

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

  • 查看socket缓存的大小:

    1. int bufsize = 0;
    2. socklen_t optlen = sizeof(bufsize);
    3. getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, &optlen); // 获取发送缓冲区的大小。
    4. cout << "send bufsize = " << bufsize << endl;
    5. getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, &optlen); // 获取接收缓冲区的大小。
    6. 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算法。

        1. #include <netinet/tcp.h> // 注意,要包含这个头文件。
        2. int opt = 1;
        3. 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对象:readfds、writefds和exceptfds,它们分别表示要监视的读、写和异常事件的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()将返回就绪并包含在集合中(readfds、writefds、exceptfds)的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对象。

  • 包含头文件:

    1. #include <sys/select.h>
    2. #include <sys/time.h>
    3. #include <sys/types.h>
    4. #include <unistd.h>
    复制代码
  • 函数声明:

    1. /* `fd_set' 的访问宏. */
    2. #define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)
    3. #define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp)
    4. #define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp)
    5. #define FD_ZERO(fdsetp) __FD_ZERO (fdsetp)
    6. #define __FD_SET(d, set) \
    7. ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)))
    8. #define __FD_CLR(d, set) \
    9. ((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d)))
    10. #define __FD_ISSET(d, set) \
    11. ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0)
    12. # define __FD_ZERO(fdsp) \
    13. do { \
    14. int __d0, __d1; \
    15. __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS \
    16. : "=c" (__d0), "=D" (__d1) \
    17. : "a" (0), "0" (sizeof (fd_set) \
    18. / sizeof (__fd_mask)), \
    19. "1" (&__FDS_BITS (fdsp)[0]) \
    20. : "memory"); \
    21. } 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()函数:

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

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

    • 服务端:

      1. #include <iostream>
      2. #include <cstring>
      3. #include <cstdlib>
      4. #include <unistd.h>
      5. #include <arpa/inet.h>
      6. #include <sys/socket.h>
      7. #include <sys/select.h>
      8. #define BUF_SIZE 100
      9. void error_handling(const char *message);
      10. int main(int argc, char *argv[])
      11. {
      12. int serv_sock, clnt_sock;
      13. struct sockaddr_in serv_adr, clnt_adr;
      14. socklen_t adr_sz;
      15. int str_len, fd_num, i;
      16. char buf[BUF_SIZE];
      17. if (argc != 2)
      18. {
      19. std::cout << "using: " << argv[0] << " <port>" << std::endl;
      20. exit(1);
      21. }
      22. serv_sock = socket(PF_INET, SOCK_STREAM, 0);
      23. if (serv_sock == -1)
      24. error_handling("socket() error");
      25. memset(&serv_adr, 0, sizeof(serv_adr));
      26. serv_adr.sin_family = AF_INET;
      27. serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
      28. serv_adr.sin_port = htons(atoi(argv[1]));
      29. if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
      30. error_handling("bind() error");
      31. if (listen(serv_sock, 5) == -1)
      32. error_handling("listen() error");
      33. fd_set reads, cpy_reads;
      34. FD_ZERO(&reads);
      35. FD_SET(serv_sock, &reads);
      36. int fd_max = serv_sock;
      37. while (1)
      38. {
      39. cpy_reads = reads;
      40. struct timeval timeout;
      41. timeout.tv_sec = 5;
      42. timeout.tv_usec = 5000;
      43. if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
      44. break;
      45. if (fd_num == 0)
      46. continue;
      47. for (i = 0; i < fd_max + 1; i++)
      48. {
      49. if (FD_ISSET(i, &cpy_reads))
      50. {
      51. if (i == serv_sock)
      52. { // 连接请求
      53. adr_sz = sizeof(clnt_adr);
      54. clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
      55. FD_SET(clnt_sock, &reads);
      56. if (fd_max < clnt_sock)
      57. fd_max = clnt_sock;
      58. std::cout << "connected client: " << clnt_sock << std::endl;
      59. }
      60. else
      61. { // Read message!
      62. str_len = read(i, buf, BUF_SIZE);
      63. if (str_len == 0)
      64. { // Close request!
      65. FD_CLR(i, &reads);
      66. close(i);
      67. std::cout << "closed client: " << i << std::endl;
      68. }
      69. else
      70. {
      71. write(i, buf, str_len); // Echo!
      72. }
      73. }
      74. }
      75. }
      76. }
      77. close(serv_sock);
      78. return 0;
      79. }
      80. void error_handling(const char *message)
      81. {
      82. std::cerr << message << std::endl;
      83. exit(1);
      84. }
      复制代码
    • 客户端:

      1. #include <iostream>
      2. #include <cstring>
      3. #include <cstdlib>
      4. #include <unistd.h>
      5. #include <arpa/inet.h>
      6. #include <sys/socket.h>
      7. #define BUF_SIZE 1024
      8. void error_handling(const char *message);
      9. int main(int argc, char *argv[])
      10. {
      11. int sock;
      12. char message[BUF_SIZE];
      13. int str_len;
      14. struct sockaddr_in serv_adr;
      15. if (argc != 3)
      16. {
      17. std::cout << "Usage : " << argv[0] << " <IP> <port>" << std::endl;
      18. exit(1);
      19. }
      20. sock = socket(PF_INET, SOCK_STREAM, 0);
      21. if (sock == -1)
      22. error_handling("socket() error");
      23. memset(&serv_adr, 0, sizeof(serv_adr));
      24. serv_adr.sin_family = AF_INET;
      25. serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
      26. serv_adr.sin_port = htons(atoi(argv[2]));
      27. if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
      28. error_handling("connect() error!");
      29. else
      30. std::cout << "Connected..." << std::endl;
      31. while (1)
      32. {
      33. std::cout << "Input message (Q to quit): " << std::endl;
      34. fgets(message, BUF_SIZE, stdin);
      35. if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
      36. break;
      37. write(sock, message, strlen(message));
      38. str_len = read(sock, message, BUF_SIZE - 1);
      39. message[str_len] = '\0';
      40. std::cout << "Message from server: " << message << std::endl;
      41. }
      42. close(sock);
      43. return 0;
      44. }
      45. void error_handling(const char *message)
      46. {
      47. std::cerr << message << std::endl;
      48. exit(1);
      49. }
      复制代码
    • 理解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
  • 包含头文件:

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

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

  • 服务端:

    1. #include <iostream>
    2. #include <cstdlib>
    3. #include <cstring>
    4. #include <unistd.h>
    5. #include <arpa/inet.h>
    6. #include <sys/socket.h>
    7. #include <sys/epoll.h>
    8. #define BUF_SIZE 100
    9. #define EPOLL_SIZE 50
    10. // 错误处理函数
    11. void error_handling(const std::string &message)
    12. {
    13. std::cerr << message << std::endl;
    14. exit(1);
    15. }
    16. int main(int argc, char *argv[])
    17. {
    18. int serv_sock, clnt_sock;
    19. sockaddr_in serv_adr, clnt_adr;
    20. socklen_t adr_sz;
    21. int str_len, i;
    22. char buf[BUF_SIZE];
    23. epoll_event *ep_events;
    24. epoll_event event;
    25. int epfd, event_cnt;
    26. // 检查参数个数
    27. if (argc != 2)
    28. {
    29. std::cerr << "using: " << argv[0] << " <port>" << std::endl;
    30. exit(1);
    31. }
    32. // 创建服务器套接字
    33. serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    34. if (serv_sock == -1)
    35. error_handling("socket() error");
    36. // 初始化服务器地址结构体
    37. memset(&serv_adr, 0, sizeof(serv_adr));
    38. serv_adr.sin_family = AF_INET;
    39. serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    40. serv_adr.sin_port = htons(atoi(argv[1]));
    41. // 绑定服务器套接字
    42. if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
    43. error_handling("bind() error");
    44. // 监听连接请求
    45. if (listen(serv_sock, 5) == -1)
    46. error_handling("listen() error");
    47. // 创建epoll实例
    48. epfd = epoll_create(EPOLL_SIZE);
    49. if (epfd == -1)
    50. error_handling("epoll_create() error");
    51. // 动态分配epoll事件数组
    52. ep_events = new epoll_event[EPOLL_SIZE];
    53. // 设置服务器套接字的事件类型并添加到epoll实例中
    54. event.events = EPOLLIN;
    55. event.data.fd = serv_sock;
    56. if (epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event) == -1)
    57. error_handling("epoll_ctl() error");
    58. while (true)
    59. {
    60. // 等待事件发生
    61. event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
    62. if (event_cnt == -1)
    63. {
    64. std::cerr << "epoll_wait() error" << std::endl;
    65. break;
    66. }
    67. for (i = 0; i < event_cnt; i++)
    68. {
    69. if (ep_events[i].data.fd == serv_sock)
    70. {
    71. // 接受新的客户端连接
    72. adr_sz = sizeof(clnt_adr);
    73. clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
    74. if (clnt_sock == -1)
    75. error_handling("accept() error");
    76. // 将新的客户端套接字添加到epoll实例中
    77. event.events = EPOLLIN;
    78. event.data.fd = clnt_sock;
    79. if (epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event) == -1)
    80. error_handling("epoll_ctl() error");
    81. std::cout << "connected client: " << clnt_sock << std::endl;
    82. }
    83. else
    84. {
    85. // 处理客户端消息
    86. str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
    87. if (str_len == 0)
    88. {
    89. // 客户端关闭连接
    90. if (epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, nullptr) == -1)
    91. error_handling("epoll_ctl() error");
    92. close(ep_events[i].data.fd);
    93. std::cout << "closed client: " << ep_events[i].data.fd << std::endl;
    94. }
    95. else
    96. {
    97. // 回显消息给客户端
    98. write(ep_events[i].data.fd, buf, str_len);
    99. }
    100. }
    101. }
    102. }
    103. // 关闭服务器套接字和epoll实例
    104. close(serv_sock);
    105. close(epfd);
    106. delete[] ep_events;
    107. return 0;
    108. }
    复制代码
  • 客户端与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模型属于典型的条件触发。

  • 条件触发的代码示例:

    1. #include <iostream>
    2. #include <cstring>
    3. #include <unistd.h>
    4. #include <fcntl.h>
    5. #include <arpa/inet.h>
    6. #include <sys/socket.h>
    7. #include <sys/epoll.h>
    8. #define BUF_SIZE 4
    9. #define EPOLL_SIZE 50
    10. void error_handling(const std::string &message)
    11. {
    12. std::cerr << message << std::endl;
    13. exit(1);
    14. }
    15. int main(int argc, char *argv[])
    16. {
    17. int serv_sock, clnt_sock;
    18. sockaddr_in serv_adr{}, clnt_adr{};
    19. socklen_t adr_sz;
    20. int str_len, i;
    21. char buf[BUF_SIZE];
    22. epoll_event *ep_events;
    23. epoll_event event{};
    24. int epfd, event_cnt;
    25. // 检查命令行参数
    26. if (argc != 2)
    27. {
    28. std::cerr << "using: " << argv[0] << " <port>" << std::endl;
    29. exit(1);
    30. }
    31. // 创建服务器套接字
    32. serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    33. if (serv_sock == -1)
    34. error_handling("socket() error");
    35. // 初始化服务器地址结构体
    36. memset(&serv_adr, 0, sizeof(serv_adr));
    37. serv_adr.sin_family = AF_INET;
    38. serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    39. serv_adr.sin_port = htons(atoi(argv[1]));
    40. // 绑定服务器套接字
    41. if (bind(serv_sock, (sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
    42. error_handling("bind() error");
    43. // 监听连接请求
    44. if (listen(serv_sock, 5) == -1)
    45. error_handling("listen() error");
    46. // 创建epoll实例
    47. epfd = epoll_create(EPOLL_SIZE);
    48. if (epfd == -1)
    49. error_handling("epoll_create() error");
    50. // 动态分配epoll事件数组
    51. ep_events = new epoll_event[EPOLL_SIZE];
    52. // 设置服务器套接字的事件类型并添加到epoll实例中
    53. event.events = EPOLLIN;
    54. event.data.fd = serv_sock;
    55. if (epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event) == -1)
    56. error_handling("epoll_ctl() error");
    57. while (true)
    58. {
    59. // 等待事件发生
    60. event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
    61. if (event_cnt == -1)
    62. {
    63. std::cerr << "epoll_wait() error" << std::endl;
    64. break;
    65. }
    66. std::cout << "return epoll_wait" << std::endl;
    67. for (i = 0; i < event_cnt; i++)
    68. {
    69. if (ep_events[i].data.fd == serv_sock)
    70. {
    71. // 接受新的客户端连接
    72. adr_sz = sizeof(clnt_adr);
    73. clnt_sock = accept(serv_sock, (sockaddr *)&clnt_adr, &adr_sz);
    74. if (clnt_sock == -1)
    75. error_handling("accept() error");
    76. // 将新的客户端套接字添加到epoll实例中
    77. event.events = EPOLLIN;
    78. event.data.fd = clnt_sock;
    79. if (epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event) == -1)
    80. error_handling("epoll_ctl() error");
    81. std::cout << "connected client: " << clnt_sock << std::endl;
    82. }
    83. else
    84. {
    85. // 处理客户端消息
    86. str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
    87. if (str_len == 0)
    88. {
    89. // 客户端关闭连接
    90. if (epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, nullptr) == -1)
    91. error_handling("epoll_ctl() error");
    92. close(ep_events[i].data.fd);
    93. std::cout << "closed client: " << ep_events[i].data.fd << std::endl;
    94. }
    95. else
    96. {
    97. // 回显消息给客户端
    98. write(ep_events[i].data.fd, buf, str_len);
    99. }
    100. }
    101. }
    102. }
    103. // 关闭服务器套接字和epoll实例
    104. close(serv_sock);
    105. close(epfd);
    106. delete[] ep_events;
    107. return 0;
    108. }
    复制代码
  • 边缘触发的示例代码:

    1. #include <iostream>
    2. #include <cstring>
    3. #include <unistd.h>
    4. #include <fcntl.h>
    5. #include <errno.h>
    6. #include <arpa/inet.h>
    7. #include <sys/socket.h>
    8. #include <sys/epoll.h>
    9. #define BUF_SIZE 4
    10. #define EPOLL_SIZE 50
    11. void setNonBlockingMode(int fd);
    12. void errorHandling(const std::string &message);
    13. int main(int argc, char *argv[])
    14. {
    15. int serv_sock, clnt_sock;
    16. sockaddr_in serv_adr{}, clnt_adr{};
    17. socklen_t adr_sz;
    18. int str_len;
    19. char buf[BUF_SIZE];
    20. epoll_event *ep_events;
    21. epoll_event event{};
    22. int epfd, event_cnt;
    23. // 检查命令行参数
    24. if (argc != 2)
    25. {
    26. std::cerr << "using: " << argv[0] << " <port>" << std::endl;
    27. exit(1);
    28. }
    29. // 创建服务器套接字
    30. serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    31. if (serv_sock == -1)
    32. errorHandling("socket() error");
    33. // 初始化服务器地址结构体
    34. memset(&serv_adr, 0, sizeof(serv_adr));
    35. serv_adr.sin_family = AF_INET;
    36. serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    37. serv_adr.sin_port = htons(atoi(argv[1]));
    38. // 绑定服务器套接字
    39. if (bind(serv_sock, (sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
    40. errorHandling("bind() error");
    41. // 监听连接请求
    42. if (listen(serv_sock, 5) == -1)
    43. errorHandling("listen() error");
    44. // 创建epoll实例
    45. epfd = epoll_create(EPOLL_SIZE);
    46. if (epfd == -1)
    47. errorHandling("epoll_create() error");
    48. // 动态分配epoll事件数组
    49. ep_events = new epoll_event[EPOLL_SIZE];
    50. // 设置非阻塞模式
    51. setNonBlockingMode(serv_sock);
    52. event.events = EPOLLIN;
    53. event.data.fd = serv_sock;
    54. // 将服务器套接字添加到epoll实例中
    55. if (epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event) == -1)
    56. errorHandling("epoll_ctl() error");
    57. while (true)
    58. {
    59. // 等待事件发生
    60. event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
    61. if (event_cnt == -1)
    62. {
    63. std::cerr << "epoll_wait() error" << std::endl;
    64. break;
    65. }
    66. std::cout << "return epoll_wait" << std::endl;
    67. for (int i = 0; i < event_cnt; i++)
    68. {
    69. if (ep_events[i].data.fd == serv_sock)
    70. {
    71. // 接受新的客户端连接
    72. adr_sz = sizeof(clnt_adr);
    73. clnt_sock = accept(serv_sock, (sockaddr *)&clnt_adr, &adr_sz);
    74. if (clnt_sock == -1)
    75. errorHandling("accept() error");
    76. // 设置非阻塞模式
    77. setNonBlockingMode(clnt_sock);
    78. event.events = EPOLLIN | EPOLLET;
    79. event.data.fd = clnt_sock;
    80. if (epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event) == -1)
    81. errorHandling("epoll_ctl() error");
    82. std::cout << "connected client: " << clnt_sock << std::endl;
    83. }
    84. else
    85. {
    86. while (true)
    87. {
    88. // 读取客户端消息
    89. str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
    90. if (str_len == 0)
    91. { // 关闭请求
    92. if (epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, nullptr) == -1)
    93. errorHandling("epoll_ctl() error");
    94. close(ep_events[i].data.fd);
    95. std::cout << "closed client: " << ep_events[i].data.fd << std::endl;
    96. break;
    97. }
    98. else if (str_len < 0)
    99. {
    100. if (errno == EAGAIN)
    101. break;
    102. }
    103. else
    104. {
    105. // 回显消息给客户端
    106. write(ep_events[i].data.fd, buf, str_len);
    107. }
    108. }
    109. }
    110. }
    111. }
    112. // 关闭服务器套接字和epoll实例
    113. close(serv_sock);
    114. close(epfd);
    115. delete[] ep_events;
    116. return 0;
    117. }
    118. void setNonBlockingMode(int fd)
    119. {
    120. int flag = fcntl(fd, F_GETFL, 0);
    121. fcntl(fd, F_SETFL, flag | O_NONBLOCK);
    122. }
    123. void errorHandling(const std::string &message)
    124. {
    125. std::cerr << message << std::endl;
    126. exit(1);
    127. }
    复制代码
  • 运行结果中需要注意的是,客户端发送消息次数和服务器端epoll_wait()函数调用次数。客户端从请求连接到断开连接共发送5次数据,服务器端也相应产生5个事件。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Honkers

特级红客

关注
  • 3156
    主题
  • 36
    粉丝
  • 0
    关注
这家伙很懒,什么都没留下!

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行