当前位置: 首页 > news >正文

Linux C++

1.Linux环境配置

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

yum -y install gcc*	// centos7

2.升级编译器

  • 升级软件包:

    yum -y install centos-release-scl devtoolset-8-gcc*
    
  • 启用软件包:

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

    或者:

    mv /usr/bin/gcc /usr/bin/gcc-4.8.5
    ln -s /opt/rh/devtoolset-8/root/bin/gcc /usr/bin/gcc
    mv /usr/bin/g++ /usr/bin/g++-4.8.5
    ln -s /opt/rh/devtoolset-8/root/bin/g++ /usr/bin/g++
    

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

yum -y install man-pages
  • 帮助文档的使用

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

4.编译

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

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

    • 不规范的做法:

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

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

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

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

  4. 静态库的特点

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

2.动态库

  1. 制作动态库

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

    • 不规范的做法:

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

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

  3. 动态库的概念

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

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

  4. 动态库的特点

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

3.main函数的参数

1.main函数的参数

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

    int main(int agrc, char *argv[], char *envp[])
    {return 0;
    }
    
  • 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()函数,进程终止后再次运行该程序,上次的设置是无效的。

  2. char* getenv(const char *name);
    

3.示例

#include <iostream>
#include <cstdlib>int main(int argc, char *argv[], char *envp[]) {// 检查参数数量是否正确if (argc != 3) {std::cout << "Usage: ./demo <arg1> <arg2>" << std::endl;return -1;}// 显示命令行参数std::cout << "Command line arguments:" << std::endl;for (int i = 0; i < argc; ++i) {std::cout << "argv[" << i << "] = " << argv[i] << std::endl;}// 显示环境变量std::cout << "\nEnvironment variables:" << std::endl;for (int i = 0; envp[i] != nullptr; ++i) {std::cout << "envp[" << i << "] = " << envp[i] << std::endl;}// 设置环境变量AAsetenv("AA", "aaaa", 1);// 显示环境变量AA的值std::cout << "\nEnvironment variable AA=" << getenv("AA") << std::endl;return 0;
}

4.gdb的常用命令

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

1.安装gdb

yum -y install gdb

2.gdb常用命令

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

    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 unlimitedcore file size改为unlimited
    3. 运行程序,产生core文件;
    4. 运行gdb 程序名 core文件名
    5. 在gdb中,用bt查看函数调用栈。

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

gdb 程序名 -p 进程编号

5.Linux的时间操作

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

1.time_t别名

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

    typedef long time_t;
    

2.time()库函数

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

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

  • 声明:

    time_t time(time_t *tloc);
    

    有两种调用方法:

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

    或者:

    time_t now;	time(&now);	// 将变量now的地址作为参数传递给time()函数
    

3.tm结构体

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

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

4.localtime()库函数

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

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

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

  • 函数声明:

    extern struct tm *localtime (const time_t *__timer) __THROW;
    extern struct tm *localtime_r (const time_t *__restrict __timer, struct tm *__restrict __tp) __THROW;
    
  • 示例:

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

5.mktime()库函数

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

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

  • 函数声明:

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

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

6.gettimeofday()库函数

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

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

  • 函数声明:

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

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

7.程序睡眠

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

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

  • 函数声明:

    extern unsigned int sleep (unsigned int __seconds);
    extern int usleep (__useconds_t __useconds);
    

6.Linux的目录操作

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

1.获取当前工作目录

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

    extern char *getcwd (char *__buf, size_t __size) __THROW __wur;
    extern char *get_current_dir_name (void) __THROW;
    
  • 示例:

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

2.切换工作目录

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

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

3.创建目录

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

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

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

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

4.删除目录

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

    extern int rmdir (const char *__path) __THROW __nonnull ((1));
    
  • __path:目录名

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

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

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

    #include <dirent.h>
    
  2. 相关的库函数

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

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

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

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

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

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

    • 目录指针:

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

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

    • d_name:文件名或目录名。

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

    • 示例:

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

7.Linux的系统错误

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

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

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

1.strerror()库函数

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

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

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

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

2.perror()库函数

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

    extern void perror (const char *__s);
    

3.注意事项

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

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

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

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

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

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

1.access()库函数

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

  • 包含头文件:

    #include <unistd.h>
    
  • 函数声明:

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

    __name:目录或文件名

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

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

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

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

2.stat()库函数

  1. stat结构体

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

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

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

    • 包含头文件:

      #include <sys/stat.h>
      
    • 函数声明:

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

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

    • 示例:

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

3.utime()库函数

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

  • 包含头文件:

    #include <sys/types.h>
    #include <utime.h>
    
  • 函数声明:

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

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

4.rename()库函数

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

  • 包含头文件:

    #include <stdio.h>
    
  • 函数声明:

    extern int rename (const char *__old, const char *__new) __THROW;
    
  • 参数说明:

    __old:源目录或文件名。

    __new:目标目录或文件名。

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

5.remove()库函数

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

  • 包含头文件:

    #include <stdio.h>
    
  • 函数声明:

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

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

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

9.Linux的信号

1.信号的基本概念

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

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

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

    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()函数可以设置程序对信号的处理方式。

  • 包含头文件:

    #include <signal.h>
    
  • 函数声明:

    typedef void (*__sighandler_t)(int);extern __sighandler_t signal (int __sig, __sighandler_t __handler) __THROW;
    
  • 参数说明:

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

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

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

4.信号的作用

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

5.信号的应用示例

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

6.发送信号

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

  • 函数声明:

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

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

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

    __status也是进程终止的状态。

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

2.资源释放的问题

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

3.进程的终止函数

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

  • 包含头文件:

    #include <stdlib.h>
    
  • 函数声明:

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

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

11.调用可执行程序

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

1.system()函数

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

  • 函数声明:

    extern int system (const char *__command) __wur;
    
  • system()函数的返回值比较麻烦。

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

2.exec函数族

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

  • 包含头文件:

    #include <unistd.h>
    
  • exec函数族的声明如下:

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

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

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

12.创建进程

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

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

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

    pstree -p 进程编号
    

2.进程标识

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

  • 包含头文件:

    #include <sys/types.h>
    #include <unistd.h>
    
  • 函数声明:

    typedef int __pid_t;/* 获取调用进程的进程ID.  */
    extern __pid_t getpid (void) __THROW;/* 获取调用进程的父进程的进程ID.  */
    extern __pid_t getppid (void) __THROW;
    

3.fork()函数

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

  • 包含头文件:

    #include <unistd.h>
    
  • 函数声明:

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

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

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

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

  • 示例:

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

4.fork()的两种做法

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

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

5.共享文件

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

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

  • 示例:

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

6.vfork()函数

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

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

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

  • 示例:

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

13.僵尸进程

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

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

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

  • 僵尸进程的避免:

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

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

      • 包含头文件:

        #include <sys/types.h>
        #include <sys/wait.h>
        
      • 函数声明:

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

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

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

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

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

14.多线程和信号

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

  • 示例:

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

15.共享内存

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

1.shmget()函数

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

  • 包含头文件:

    #include <sys/ipc.h>
    #include <sys/shm.h>
    
  • 函数声明:

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

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

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

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

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

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

      ipcs -m
      
    • 手动删除共享内存。

      ipcrm -m 共享内存id
      

2.shmat()函数

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

  • 函数声明:

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

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

3.shmdt()函数

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

  • 函数声明:

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

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

4.shmctl()函数

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

  • 函数声明:

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

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

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

5.示例

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>// 共享内存结构体
struct shmdata
{int id;            // 一个简单的整数标识char message[256]; // 一个消息字符串
};int main(int argc, char *argv[])
{if (argc != 3){std::cout << "using: ./test <id> <msg>" << std::endl;return -1;}// 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。int shmid = shmget(0x5005, sizeof(shmdata), 0640 | IPC_CREAT);if (shmid == -1){perror("共享内存创建失败");return -1;}std::cout << "共享内存ID = " << shmid << std::endl;// 第2步:把共享内存连接到当前进程的地址空间。shmdata *ptr = (shmdata *)shmat(shmid, nullptr, 0);if (ptr == (void *)-1){perror("共享内存连接失败");return -1;}// 第3步:使用共享内存,对共享内存进行读/写。std::cout << "原始数据: id = " << ptr->id << ", 消息 = " << ptr->message << std::endl;// 更新共享内存中的数据ptr->id = std::atoi(argv[1]);std::strncpy(ptr->message, argv[2], sizeof(ptr->message) - 1);ptr->message[sizeof(ptr->message) - 1] = '\0'; // 确保字符串以null结尾std::cout << "更新后的数据: id = " << ptr->id << ", 消息 = " << ptr->message << std::endl;// 第4步:把共享内存从当前进程中分离。if (shmdt(ptr) == -1){perror("共享内存分离失败");return -1;}// 第5步:删除共享内存(如果需要删除)。/* if (shmctl(shmid, IPC_RMID, nullptr) == -1){perror("共享内存删除失败");return -1;} */return 0;
}

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

#ifndef __PUBLIC_HH
#define __PUBLIC_HH#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/sem.h>// 循环队列模板类。
template <class TT, int MaxLength>
class squeue
{
private:bool m_inited;        // 队列被初始化标志,true-已初始化;false-未初始化。TT m_data[MaxLength]; // 用数组存储循环队列中的元素。int m_head;           // 队列的头指针。int m_tail;           // 队列的尾指针,指向队尾元素。int m_length;         // 队列的实际长度。squeue(const squeue &) = delete;            // 禁用拷贝构造函数。squeue &operator=(const squeue &) = delete; // 禁用赋值函数。public:squeue() { init(); } // 构造函数。// 循环队列的初始化操作。// 注意:如果用于共享内存的队列,不会调用构造函数,必须调用此函数初始化。void init(){if (!m_inited){                                           // 循环队列的初始化只能执行一次。m_head = 0;                             // 头指针。m_tail = MaxLength - 1;                 // 为了方便写代码,初始化时,尾指针指向队列的最后一个位置。m_length = 0;                           // 队列的实际长度。std::memset(m_data, 0, sizeof(m_data)); // 数组元素清零。m_inited = true;}}// 元素入队,返回值:false-失败;true-成功。bool push(const TT &ee){if (full()){std::cout << "循环队列已满,入队失败。\n";return false;}// 先移动队尾指针,然后再拷贝数据。m_tail = (m_tail + 1) % MaxLength; // 队尾指针后移。m_data[m_tail] = ee;m_length++;return true;}// 求循环队列的长度,返回值:>=0-队列中元素的个数。int size() const{return m_length;}// 判断循环队列是否为空,返回值:true-空,false-非空。bool empty() const{return m_length == 0;}// 判断循环队列是否已满,返回值:true-已满,false-未满。bool full() const{return m_length == MaxLength;}// 查看队头元素的值,元素不出队。TT &front(){return m_data[m_head];}// 元素出队,返回值:false-失败;true-成功。bool pop(){if (empty())return false;m_head = (m_head + 1) % MaxLength; // 队列头指针后移。m_length--;return true;}// 显示循环队列中全部的元素。// 这是一个临时的用于调试的函数,队列中元素的数据类型支持cout输出才可用。void printqueue() const{for (int i = 0; i < size(); i++){std::cout << "m_data[" << (m_head + i) % MaxLength << "], value="<< m_data[(m_head + i) % MaxLength] << std::endl;}}
};// 信号量类。
class csemp
{
private:union semun{ // 用于信号量操作的联合体。int val;struct semid_ds *buf;unsigned short *arry;};int m_semid;     // 信号量id(描述符)。short m_sem_flg; // 信号量的标志位。csemp(const csemp &) = delete;            // 禁用拷贝构造函数。csemp &operator=(const csemp &) = delete; // 禁用赋值函数。public:csemp() : m_semid(-1) {}// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。// 如果用于生产消费者模型,value填0,sem_flg填0。bool init(key_t key, unsigned short value = 1, short sem_flg = SEM_UNDO);// 信号量的P操作,如果信号量的值是0,将阻塞等待,直到信号量的值大于0。bool wait(short value = -1);// 信号量的V操作。bool post(short value = 1);// 获取信号量的值,成功返回信号量的值,失败返回-1。int getvalue() const;// 销毁信号量。bool destroy();~csemp();
};#endif // __PUBLIC_HH
#include "public.h"// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool csemp::init(key_t key, unsigned short value, short sem_flg)
{if (m_semid != -1)return false; // 如果已经初始化了,不必再次初始化。m_sem_flg = sem_flg;// 信号量的初始化不能直接用semget(key, 1, 0666 | IPC_CREAT)// 因为信号量创建后,初始值是0,如果用于互斥锁,需要把它的初始值设置为1,// 而获取信号量则不需要设置初始值,所以,创建信号量和获取信号量的流程不同。// 信号量的初始化分三个步骤:// 1) 获取信号量,如果成功,函数返回。// 2) 如果失败,则创建信号量。// 3) 设置信号量的初始值。// 获取信号量。if ((m_semid = semget(key, 1, 0666)) == -1){// 如果信号量不存在,创建它。if (errno == ENOENT){// 用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取。if ((m_semid = semget(key, 1, 0666 | IPC_CREAT | IPC_EXCL)) == -1){if (errno == EEXIST){ // 如果错误代码是信号量已存在,则再次获取信号量。if ((m_semid = semget(key, 1, 0666)) == -1){perror("init 1 semget()");return false;}return true;}else{ // 如果是其它错误,返回失败。perror("init 2 semget()");return false;}}// 信号量创建成功后,还需要把它初始化成value。union semun sem_union;sem_union.val = value; // 设置信号量的初始值。if (semctl(m_semid, 0, SETVAL, sem_union) < 0){perror("init semctl()");return false;}}else{perror("init 3 semget()");return false;}}return true;
}// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool csemp::wait(short value)
{if (m_semid == -1)return false;struct sembuf sem_b;sem_b.sem_num = 0;    // 信号量编号,0代表第一个信号量。sem_b.sem_op = value; // P操作的value必须小于0。sem_b.sem_flg = m_sem_flg;if (semop(m_semid, &sem_b, 1) == -1){perror("wait semop()");return false;}return true;
}// 信号量的V操作(把信号量的值增加value)。
bool csemp::post(short value)
{if (m_semid == -1)return false;struct sembuf sem_b;sem_b.sem_num = 0;    // 信号量编号,0代表第一个信号量。sem_b.sem_op = value; // V操作的value必须大于0。sem_b.sem_flg = m_sem_flg;if (semop(m_semid, &sem_b, 1) == -1){perror("post semop()");return false;}return true;
}// 获取信号量的值,成功返回信号量的值,失败返回-1。
int csemp::getvalue() const
{return semctl(m_semid, 0, GETVAL);
}// 销毁信号量。
bool csemp::destroy()
{if (m_semid == -1)return false;if (semctl(m_semid, 0, IPC_RMID) == -1){perror("destroy semctl()");return false;}return true;
}// 信号量析构函数。
csemp::~csemp()
{// 在析构函数中销毁信号量。destroy();
}
// 本程序演示循环队列的使用。
#include "public.h"int main()
{using ElemType = int;squeue<ElemType, 5> Queue;ElemType element; // 创建一个数据元素。std::cout << "元素(1、2、3)入队" << std::endl;element = 1;Queue.push(element);element = 2;Queue.push(element);element = 3;Queue.push(element);std::cout << "队列的长度是: " << Queue.size() << std::endl;Queue.printqueue();element = Queue.front();Queue.pop();std::cout << "出队的元素值为: " << element << std::endl;element = Queue.front();Queue.pop();std::cout << "出队的元素值为: " << element << std::endl;std::cout << "队列的长度是: " << Queue.size() << std::endl;Queue.printqueue();std::cout << "元素(11、12、13、14、15)入队." << std::endl;element = 11;Queue.push(element);element = 12;Queue.push(element);element = 13;Queue.push(element);element = 14;Queue.push(element);element = 15;Queue.push(element);std::cout << "队列的长度是: " << Queue.size() << std::endl;Queue.printqueue();return 0;
}
// shared_memory_cirucularqueue.cpp,本程序演示基于共享内存的循环队列。
#include "public.h"int main()
{using ElemType = int;// 初始化共享内存。int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);if (shmid == -1){std::cout << "shmget(0x5005) failed." << std::endl;return -1;}// 把共享内存连接到当前进程的地址空间。squeue<ElemType, 5> *Queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);if (Queue == (void *)-1){std::cout << "shmat() failed." << std::endl;return -1;}Queue->init(); // 初始化循环队列。ElemType element; // 创建一个数据元素。std::cout << "元素(1、2、3)入队。\n";element = 1;Queue->push(element);element = 2;Queue->push(element);element = 3;Queue->push(element);std::cout << "队列的长度是: " << Queue->size() << std::endl;Queue->printqueue();element = Queue->front();Queue->pop();std::cout << "出队的元素值为: " << element << std::endl;element = Queue->front();Queue->pop();std::cout << "出队的元素值为: " << element << std::endl;std::cout << "队列的长度是: " << Queue->size() << std::endl;Queue->printqueue();std::cout << "元素(11、12、13、14、15)入队." << std::endl;element = 11;Queue->push(element);element = 12;Queue->push(element);element = 13;Queue->push(element);element = 14;Queue->push(element);element = 15;Queue->push(element);std::cout << "队列的长度是: " << Queue->size() << std::endl;Queue->printqueue();shmdt(Queue); // 把共享内存从当前进程中分离。return 0;
}
// shared_memory_lock.cpp,本程序演示用信号量给共享内存加锁。
#include "public.h"struct PersonInfo
{                  // 人员信息结构体。int id;        // 编号。char name[32]; // 姓名。
};int main(int argc, char *argv[])
{if (argc != 3){std::cout << "using: ./shared_memory_lock id name" << std::endl;return -1;}// 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。int shmid = shmget(0x5005, sizeof(PersonInfo), 0640 | IPC_CREAT);if (shmid == -1){std::cout << "shmget(0x5005) failed." << std::endl;return -1;}std::cout << "shmid = " << shmid << std::endl;// 第2步:把共享内存连接到当前进程的地址空间。PersonInfo *ptr = (PersonInfo *)shmat(shmid, 0, 0);if (ptr == (void *)-1){std::cout << "shmat() failed." << std::endl;return -1;}// 创建、初始化二元信号量。csemp mutex;if (!mutex.init(0x5005)){std::cout << "mutex.init(0x5005) failed." << std::endl;;return -1;}std::cout << "申请加锁..." << std::endl;mutex.wait(); // 申请加锁。std::cout << "申请加锁成功." << std::endl;// 第3步:使用共享内存,对共享内存进行读/写。std::cout << "原值: id = " << ptr->id << ", name = " << ptr->name << std::endl; // 显示共享内存中的原值。ptr->id = atoi(argv[1]);                                                        // 对人员信息结构体的id成员赋值。strcpy(ptr->name, argv[2]);                                                     // 对人员信息结构体的name成员赋值。std::cout << "新值: id = " << ptr->id << ", name = " << ptr->name << std::endl; // 显示共享内存中的当前值。sleep(10);mutex.post(); // 解锁。std::cout << "解锁." << std::endl;// 查看信号量:ipcs -s    // 删除信号量:ipcrm sem 信号量id// 查看共享内存:ipcs -m    // 删除共享内存:ipcrm -m  共享内存id// 第4步:把共享内存从当前进程中分离。shmdt(ptr);// 第5步:删除共享内存。// if (shmctl(shmid,IPC_RMID,0) == -1)//{// std::cout << "shmctl failed"; << std::endl; return -1;//}
}
#include "public.h"	// 生产者 producer.cppint main()
{struct Person{ // 生产队列的数据元素是人员信息结构体。int id;char name[31];};using ElemType = Person;// 初始化共享内存。int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);if (shmid == -1){std::cout << "shmget(0x5005) failed." << std::endl;return -1;}// 把共享内存连接到当前进程的地址空间。squeue<ElemType, 5> *queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);if (queue == (void *)-1){std::cout << "shmat() failed." << std::endl;return -1;}queue->init(); // 初始化循环队列。ElemType element; // 创建一个数据元素。csemp mutex;mutex.init(0x5001); // 用于给共享内存加锁。csemp cond;cond.init(0x5002, 0, 0); // 信号量的值用于表示队列中数据元素的个数。mutex.wait(); // 加锁。// 生产3个数据。element.id = 3;strncpy(element.name, "Tom", sizeof(element.name));queue->push(element);element.id = 7;strncpy(element.name, "Tomy", sizeof(element.name));queue->push(element);element.id = 8;strncpy(element.name, "Tony", sizeof(element.name));queue->push(element);mutex.post(); // 解锁。cond.post(3); // 实参是3,表示生产了3个数据。shmdt(queue); // 把共享内存从当前进程中分离。return 0;
}
#include "public.h"	// 消费者 consumer.cppint main()
{struct Person{ // 循环队列的数据元素是人员信息结构体。int id;char name[31];};using ElemType = Person;// 初始化共享内存。int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);if (shmid == -1){std::cout << "shmget(0x5005) failed." << std::endl;return -1;}// 把共享内存连接到当前进程的地址空间。squeue<ElemType, 5> *queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);if (queue == (void *)-1){std::cout << "shmat() failed." << std::endl;return -1;}queue->init(); // 初始化循环队列。ElemType element; // 创建一个数据元素。csemp mutex;mutex.init(0x5001); // 用于给共享内存加锁。csemp cond;cond.init(0x5002, 0, 0); // 信号量的值用于表示队列中数据元素的个数。while (true){mutex.wait(); // 加锁。while (queue->empty()){                 // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用ifmutex.post(); // 解锁。cond.wait();  // 等待生产者的唤醒信号。mutex.wait(); // 加锁。}// 数据元素出队。element = queue->front();queue->pop();mutex.post(); // 解锁。// 处理出队的数据(把数据消费掉)。std::cout << "id = " << element.id << ", name = " << element.name << std::endl;usleep(100); // 假设处理数据需要时间,方便演示。}shmdt(queue); // 把共享内存从当前进程中分离。return 0;
}

17.第一个网络通讯程序

1.网络通讯的流程

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

2.示例

  • 客户端

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

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

18.基于Linux的文件操作

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

// filecw.cpp,本程序演示了Linux底层文件的操作-创建文件并写入数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd; // 文件描述符// 打开文件,如果创建后的文件没有权限,可以手工授权 chmod 777 data.txt。fd = open("data.txt", O_CREAT | O_RDWR | O_TRUNC, 0666); // 添加文件权限参数0666if (fd == -1){perror("open data.txt failed.");return -1;}printf("file descriptor fd = %d\n", fd);char buffer[1024];strcpy(buffer, "This is a sample text.\n");if (write(fd, buffer, strlen(buffer)) == -1){ // 把数据写入文件。perror("write failed.");return -1;}close(fd); // 关闭文件。return 0;  // 添加返回值,表示程序执行成功
}

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

// fileread.cpp,本程序演示了Linux底层文件的操作-读取文件。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd; // 定义一个文件描述符/文件句柄。fd = open("data.txt", O_RDONLY); // 打开文件。if (fd == -1){perror("open data.txt failed.");return -1;}printf("文件描述符: fd = %d\n", fd);char buffer[1024];memset(buffer, 0, sizeof(buffer));if (read(fd, buffer, sizeof(buffer)) == -1) // 从文件中读取数据。{perror("write failed.");return -1;}printf("%s", buffer);close(fd); // 关闭文件。
}

19.socket()函数详解

1.什么是协议

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

2.创建socket

  • 包含头文件:

    #include <sys/types.h>
    #include <sys/socket.h>
    
  • 函数声明:

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

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

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

    • 使用以下命令查看:

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

    socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);    // 创建tcp的sock
    socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);    // 创建udp的sock
    

3.TCP和UDP

1.TCP和UDP的区别

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

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

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

3.UDP不可靠的原因

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

4.TCP和UDP使用场景

  • TCP使用场景

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

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

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

5.UDP能实现可靠传输吗

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

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

1.大端序/小端序

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

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

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

      0x00000001 0x12

      0x00000002 0x34

      0x00000003 0x56

      0x00000004 0x78

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

      0x00000001 0x78

      0x00000002 0x56

      0x00000003 0x34

      0x00000004 0x12

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

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

2.网络字节序

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

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

    • 包含头文件:

      #include <apra/inet.h>
      
    • 函数声明:

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

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

3.IP地址和通讯端口

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

  • 例如:192.168.190.134 3232284294 255.255.255.255

    ​ 192 168 190 134

    大端:11000000 10101000 10111110 10000110

    小段:10000110 10111110 10101000 11000000

4.如何处理大小端

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

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

1.sockaddr结构体

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

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

2.sockaddr_in结构体

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

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

3.gethostbyname()函数

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

  • 包含头文件:

    #include <netdb.h>
    
  • 函数声明:

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

    memcpy(&sockaddr_in.sin_addr, hostent->h_addr, hostent->h_length);
    

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

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

  • 包含头文件:

    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
  • 函数声明:

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

5.示例

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

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

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

22.封装socket

  • 封装socket通讯的客户端

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

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

23.多进程的网络服务端

  • 示例:

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

24.实现文件传输功能

  • 实现文件传输的客户端

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

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

25.三次握手与四次挥手

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

1.三次握手

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

2.四次挥手

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

  • 情景类似:

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

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

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

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

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

      int opt = 1;   
      setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
      

26.TCP缓存

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

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

  • 查看socket缓存的大小:

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

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

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

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

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

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

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

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

    • 解决方案:

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

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

27.I/O多路复用

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

  • 基本概念:

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

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

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

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

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

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

1.Select模型以及实战案例

  • Select模型具体步骤

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

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

    3. 设置FDs在fd_sets中

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

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

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

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

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

    7. 检查FDs的事件

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

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

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

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

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

  • 包含头文件:

    #include <sys/select.h>
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>
    
  • 函数声明:

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

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

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

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

    • 服务端:

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

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

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

2.Epoll模型

  • Select模型的缺点:

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

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

    #include <sys/epoll.h>
    
  • 函数声明:

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

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

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

3.示例

  • 服务端:

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

4.条件触发和边缘触发

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

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

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

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

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

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

  • 条件触发的代码示例:

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

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

相关文章:

  • 图卷积网络(Graph Convolutional Network, GCN)
  • 一文详解扩散模型
  • 王思聪隐形女儿曝光
  • Centos离线安装Python3
  • 人工智能—美国加利福尼亚州房价预测实战
  • Ribbon与Nginx的区别
  • 华为IPD体系中三大流程之IPD流程的六个阶段和七个评审点介绍
  • object类教程
  • 图像的高频和低频细节
  • [C#]winform使用onnxruntime部署LYT-Net轻量级低光图像增强算法
  • 宏任务与微任务
  • 对于补码的个人理解
  • Python考前综合练习-第六章[python123题库]
  • 人工智能和机器学习的应用日益广泛,在医疗健康领域的具体应用是什么?
  • 机器学习_SVM支持向量机
  • 【140天】尚学堂高淇Java300集视频精华笔记(86-87)
  • 【mysql】环境安装、服务启动、密码设置
  • Git 使用集
  • input实现文字超出省略号功能
  • Invalidate和postInvalidate的区别
  • java中具有继承关系的类及其对象初始化顺序
  • k8s如何管理Pod
  • php ci框架整合银盛支付
  • ucore操作系统实验笔记 - 重新理解中断
  • ViewService——一种保证客户端与服务端同步的方法
  • Vue UI框架库开发介绍
  • Vue2 SSR 的优化之旅
  • 分享自己折腾多时的一套 vue 组件 --we-vue
  • 前言-如何学习区块链
  • 深入浅出Node.js
  • 使用putty远程连接linux
  • 微信开源mars源码分析1—上层samples分析
  • 微信小程序--------语音识别(前端自己也能玩)
  • 限制Java线程池运行线程以及等待线程数量的策略
  • 一天一个设计模式之JS实现——适配器模式
  • RDS-Mysql 物理备份恢复到本地数据库上
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • #DBA杂记1
  • #systemverilog# 之 event region 和 timeslot 仿真调度(十)高层次视角看仿真调度事件的发生
  • #控制台大学课堂点名问题_课堂随机点名
  • #我与Java虚拟机的故事#连载10: 如何在阿里、腾讯、百度、及字节跳动等公司面试中脱颖而出...
  • (C语言)strcpy与strcpy详解,与模拟实现
  • (Forward) Music Player: From UI Proposal to Code
  • (笔试题)分解质因式
  • (初研) Sentence-embedding fine-tune notebook
  • (二)【Jmeter】专栏实战项目靶场drupal部署
  • (二)斐波那契Fabonacci函数
  • (机器学习的矩阵)(向量、矩阵与多元线性回归)
  • (全部习题答案)研究生英语读写教程基础级教师用书PDF|| 研究生英语读写教程提高级教师用书PDF
  • (算法)区间调度问题
  • .bat批处理(七):PC端从手机内复制文件到本地
  • .libPaths()设置包加载目录
  • .NET 4.0网络开发入门之旅-- 我在“网” 中央(下)
  • .net 受管制代码
  • .NET大文件上传知识整理