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的级别:
用户命令- 系统接口
库函数- 特殊文件,比如设备文件
- 文件
- 游戏
- 系统的软件包
- 系统管理命令
- 内核
- 显示帮助的界面可以用vi的命令,
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是不推荐的- 如果使用了优化选项:
- 编译的时间将会更长
- 目标程序不可调试
- 有效果,但是不可能显著提升程序的性能
2.静态库和动态库
- 在实际开发中,我们把通用的函数和类分文件编写,称之为库。在其它的程序中,可以使用库中的函数和类
- 一般来说,通用的函数和类不提供源代码文件(安全性、商业机密),而是编译成二进制文件
- 库的二进制文件有两种:静态库和动态库
1.静态库
-
制作静态库
g++ -c -o lib 库名.a 源代码文件清单 -
使用静态库
-
不规范的做法:
g++ 选项 源代码文件名清单 静态库文件名 -
规范的做法:
g++ 选项 源代码文件名清单 -l 库名 -L 库文件所在的目录名
-
-
静态库的概念
-
程序在编译时会把库文件的二进制代码链接到目标程序中,这种方式称为静态链接。
如果多个程序中用到了同一静态库中的函数或类,就会存在多份拷贝。
-
-
静态库的特点
- 静态库的链接是在编译时期完成的,执行的时候代码加载速度快。
- 目标程序的可执行文件比较大,浪费空间。
- 程序的更新和发布不方便,如果某一个静态库更新了,所有使用它的程序都需要重新编译。
2.动态库
-
制作动态库
g++ -fPIC -shared -o lib 库名.so 源代码文件清单 -
使用动态库
-
不规范的做法:
g++ 选项 源代码文件名清单 动态库文件名 -
规范的做法:
g++ 选项 源代码文件名清单 -l 库名 -L 库文件所在的目录名 -
运行可执行程序的时候,需要提前设置
LD_LIBRARY_PATH环境变量。
-
-
动态库的概念
-
程序在编译时不会把库文件的二进制代码链接到目标程序中,而是在运行时候才被载入。
如果多个进程中用到了同一动态库中的函数或类,那么在内存中只有一份,避免了空间浪费问题。
-
-
动态库的特点
- 程序在运行的过程中,需要用到动态库的时候才把动态库的二进制代码载入内存。
- 可以实现进程之间的代码共享,因此动态库也称为共享库。
- 程序升级比较简单,不需要重新编译程序,只需要更新动态库就行了。
- 如果动态库和静态库同时存在,编译器将优先使用动态库。
3.main函数的参数
1.main函数的参数
-
main函数有三个参数,argc、argv和envp,它的标准写法如下:int main(int agrc, char *argv[], char *envp[]) {return 0; } -
argc存放了程序参数的个数,包括程序本身。 -
argv字符串的数组,存放了每个参数的值,包括程序本身。 -
envp字符串的数组,存放了环境变量,数组的最后一个元素是空。 -
在程序中,如果不关心
main()函数的参数,可以省略不写。
2.操作环境变量
-
int setenv(const char *name, const char *value, int overwrite);-
name环境变量名。 -
value环境变量的值。 -
overwrite0-如果环境如果环境不存在,增加新的环境变量,如果环境变量已存在,不替换其值;非0-如果环境不存在,增加新的环境变量,如果环境变量已存在,替换其值返回值:0-成功;-1-失败(失败的情况极少见)
注意:此函数设置的环境变量只对本进程有效,不会影响shell的环境变量。如果在运行程序时执行了setenv()函数,进程终止后再次运行该程序,上次的设置是无效的。
-
-
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 张三 李四 我是王五 break b 设置断点,b 20 表示在第20行设置断点,可以设置多个断点。 run r 开始运行程序,程序运行到断点的位置会停下来,如果没有遇到断点,程序一直运行下去。 next n 执行当前语句,如果该语句为函数调用,不会进入函数内部。相当于VS的F10 step s 执行当前语句,如果该语句为函数调用,则进入函数内部。详单与VS的F11;注意,如果函数是库函数或第三方提供的函数,用s也是进不去的,因为没有源代码,如果是自定义的函数,只要有源码就可以进去。 print p 显示变量或表达式的值,如果p后面是表达式,会执行这个表达式。 continue c 继续运行程序,遇到下一个断点停止,如果没有遇到断点,程序将一直运行。相当于VS的F5 set var 设置变量的值。假设程序中定义了两个变量:int i; char name[10]; set var i = 10把i的值设置为10; set var name = “张三”。 quit q 退出gdb - 注意:在gdb中,用上下光标键可以选择执行的gdb命令。
3.gdb调试core文件
-
如果程序在运行的过程中发生了内存泄漏,会被内核强行终止,提示“段错误(吐核)”,内存的状态将保存在core文件中,方便程序员进一步分析。
-
Linux缺省不会生成core文件,需要修改系统参数。
调试core文件的步骤如下:
- 用
ulimit -a查看当前用户的资源限制参数; - 用
ulimit -c unlimited把core file size改为unlimited; - 运行程序,产生core文件;
- 运行
gdb 程序名 core文件名; - 在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分钟。
-
思路:
- 解析字符串格式的时间,转换成
tm结构体; - 用
mktime()函数把tm结构体转换成time_t时间; - 把
time_t时间加30*60秒; - 用
localtime_r()函数把time_t时间转换成tm结构体; - 把
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.获取目录中文件的列表
- 文件存放在目录中,在处理文件之前,必须先知道目录中有哪些文件,所以要获取目录中文件的列表。
-
包含头文件
#include <dirent.h> -
相关的库函数
-
步骤一:用
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));
-
-
数据结构
-
目录指针:
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_name和d_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.注意事项
-
调用库函数失败不一定会设置
errno并不是全部的库函数在调用失败时都会设置
errno的值,以man手册为准(一般来说,不属于系统调用的函数不会设置errno,属于系统调用的函数才会设置errno)。 -
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()库函数
-
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_mode、st_size和st_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) // 是否为目录,如果是,返回真
-
-
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()函数用来修改参数__file的st_atime和st_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中,可以用kill和killall命令发送信号:kill -信号的类型 进程编号 killall -信号的类型 进程名 -
查看系统定义的信号列表:
kill -l
2.信号的类型
| 信号名 | 信号值 | 默认处理动作 | 发出信号的原因 |
|---|---|---|---|
| SIGHUP | 1 | A | 终端挂起或者控制进程终止 |
| SIGINT | 2 | A | 键盘终端 ctrl+c |
| SIGQUIT | 3 | C | 键盘的退出键按下 |
| SIGILL | 4 | C | 非法指令 |
| SIGTRAP | 5 | C | 跟踪断点 |
| SIGABRT | 6 | C | 由abort(3)发出的退出指令 |
| SIGBUS | 7 | C | 总线错误(例如内存对齐错误) |
| SIGFPE | 8 | C | 浮点异常 |
| SIGKILL | 9 | AEF | 采用 kill -9 进程编号 强制杀死程序 |
| SIGUSR1 | 10 | A | 用户自定义信号 1 |
| SIGSEGV | 11 | CEF | 无效的内存引用(数组越界、操作空指针和野指针等) |
| SIGUSR2 | 12 | A | 用户自定义信号 2 |
| SIGPIPE | 13 | A | 管道破裂,写一个没有读端口的管道 |
| SIGALRM | 14 | A | 由闹钟alarm()函数发出的信号 |
| SIGTERM | 15 | A | 采用 kill 进程编号 或 killall 程序名 通知程序 |
| SIGSTKFLT | 16 | A | 栈故障(不常被使用) |
| SIGCHLD | 17 | B | 子进程结束信号 |
| SIGCOUT | 18 | C | 进程继续(曾被停止的进程) |
| SIGSTOP | 19 | DEF | 终止进程 |
| SIGSTP | 20 | D | 控制终端(tty)上按下停止键 |
| SIGTTIN | 21 | D | 后台进程企图从控制终端读 |
| SIGTTOU | 22 | D | 后台进程企图从控制终端写 |
| SIGURG | 23 | B | 套接字上有紧急数据到达 |
| SIGXCPU | 24 | C | 超过CPU时间限制 |
| SIGXFSZ | 25 | C | 超过文件大小限制 |
| SIGVTALRM | 26 | A | 虚拟时钟信号,由setitimer()产生 |
| SIGPROF | 27 | A | 统计时钟信号,由setitimer()产生 |
| SIGWINCH | 28 | B | 终端窗口大小改变 |
| SIGIO | 29 | B | 文件描述符上可以进行I/O操作 |
| SIGPWR | 30 | A | 电源故障(不常被使用) |
| SIGSYS | 31 | C | 非法系统调用 |
| SIGRTMIN | 34 | A | 实时信号,用户自定义 |
| SIGRTMAX | 64 | A | 实时信号,用户自定义 |
| 其它 | <=64 | A | 自定义信号 |
- 默认处理动作:
A(Abort): 终止进程。B(Ignore): 忽略信号,将该信号丢弃,不做处理。C(Core): 产生核心转储文件(内核映像转储core dump), 终止进程。D(Stop): 停止进程,进入停止状态的程序还能重新继续,一般是在调试的过程中。E(Continue): 信号不能被捕获,继续执行进程。F(Force): 信号不能被忽略,强制终止进程。
3.信号的处理
-
进程对信号的处理方法有三种:
- 对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程。
- 设置信号的处理函数,收到信号后,由该函数来处理。
- 忽略某个信号,对该信号不做任何处理,就像未发生过一样。
-
signal()函数可以设置程序对信号的处理方式。 -
包含头文件:
#include <signal.h> -
函数声明:
typedef void (*__sighandler_t)(int);extern __sighandler_t signal (int __sig, __sighandler_t __handler) __THROW; -
参数说明:
__sig:信号的编号(信号的值)。__handler:信号的处理方式,有三种情况:SIG_DFL:恢复参数__sig信号的处理方法为默认行为。- 一个自定义的处理信号的函数,函数的形参是信号的编号。
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操作系统提供了
kill和killall命令向进程发送信号,在程序中,可以用kill()函数向其它进程发送信号。 -
函数声明:
extern int kill (__pid_t __pid, int __sig) __THROW; -
kill()函数将参数__sig指定的信号给参数__pid指定的进程。 -
参数
__pid有几种情况:__pid > 0将信号传给进程号为__pid的进程。__pid = 0将信号传给和当前进程相同进程组的所有进程,常用于父进程给子进程发送信号,注意,发送信号者进程也会收到自己发出的信号。__pid = -1将信号广播传送给系统内所有的进程,例如系统关机时,会向所有的登录窗口广播关机信息。
-
__sig:准备发送的信号代码,假如其值为0则没有任何信号送出,但是系统会执行错误检查,通常会利用__sig值为零来检验某个进程是否仍在运行。 -
返回值说明:成功执行时,返回0;失败返回-1,
errno被设置。
10.进程终止
- 有
8种方式可以中止进程,其中5种为正常终止,它们是:- 在
main()函数用return返回; - 在任意函数中调用
exit()函数; - 在任意函数中调用
_exit()或_Exit()函数; - 最后一个线程从其启动例程(线程主函数)用
return返回; - 在最后一个线程中调用
pthread_exit()返回。
- 在
- 异常终止有
3种方式,它们是:- 调用
abort()函数中止; - 接收到一个信号;
- 最后一个线程对取消请求做出响应。
- 调用
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()函数的返回值比较麻烦。- 如果执行的程序不存在,
system()函数返回非0; - 如果执行程序成功,并且被执行的程序终止状态是
0,system()函数返回0; - 如果执行程序成功,并且被执行的程序终止状态不是
0,system()函数返回非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,失败原因存于errno中; - 新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈;
- 如果执行成功则函数不会返回,当在主程序中成功调用
exec后,被调用的程序将取代调用者程序,也就是说,exec函数之后的代码都不会被执行; - 在实际开发中,最常用的是
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系统的全部进程是一个树形结构。
0号进程(系统进程)是所有进程的祖先,它创建了1号和2号进程;1号进程(systemd)负责执行内核的初始化工作和进行系统配置;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()的两种做法
- 父进程复制自己,然后父进程和子进程分别执行不同的代码。这种用法在网络服务程序中很常见,父进程等待客户端的连接请求,当请求到达时,父进程调用
fork(),让子进程处理些请求,而父进程则继续等待下一个连接请求。 - 进程要执行另一个程序。这种用法在
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()保证子进程先运行,在子进程调用exec或exit()之后父进程才恢复运行。 -
示例:
#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时间等。父进程如果处理了子进程退出的信息,内核就会释放这个数据结构,父进程如果没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程的进程编号将一直被占用。系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因为没有可用的进程编号而导致系统不能产生新的进程。 -
僵尸进程的避免:-
子进程退出的时候,内核会向父进程发头SIGCHLD信号,如果父进程用signal(SIGCHLD, SIG_IGN)通知内核,表示自己对子进程的退出不感兴趣,那么子进程退出后会立即释放数据结构。 -
父进程通过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:子进程终止的信息:- 如果是正常终止,宏
WIFEXITED(status)返回真,宏WEXITSTATUS(stat_loc)可获取终止状态; - 如果是异常终止,宏
WTERMSIG(status)可获取终止进程的信号; - 如果父进程很忙,可以捕获
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_CREAT,0666表示全部用户对它可读写,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; -
__shmaddr:shmat()函数返回的地址。 -
调用成功返回
0,失败返回-1。
4.shmctl()函数
-
该函数用于操作共享内存,最常用的操作是删除共享内存。
-
函数声明:
/* 共享内存控制操作. */ extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW; -
参数说明:
__shmid:shmget()函数返回的共享内存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.网络通讯的流程
- 服务器端流程:
- 创建Socket:使用
socket()函数创建一个套接字(socket),指定地址族和套接字类型。对于TCP通信,通常使用AF_INET和SOCK_STREAM参数。 - 绑定地址和端口:使用
bind()函数将套接字与服务器的地址和端口绑定。需要设置套接字地址结构体struct sockaddr_in的成员,包括地址族、端口号和IP地址。 - 监听连接:使用
listen()函数开始监听连接请求。指定服务器可以同时处理的最大连接数,即待处理的连接请求队列长度。 - 接受连接请求:使用
accept()函数接受客户端的连接请求,创建一个新的套接字来处理与客户端之间的通信。accept()函数会阻塞直到有新的连接请求到达。 - 接收数据并发送响应:使用
recv()函数从客户端接收数据,并使用send()函数向客户端发送响应。这个过程可以在一个循环中进行,直到通信结束 - 关闭连接:当通信结束后,使用
close()函数关闭连接套接字,释放资源。
- 创建Socket:使用
- 客户端流程:
- 创建Socket:使用
socket()函数创建一个套接字(socket),指定地址族和套接字类型。对于TCP通信,通常使用AF_INET和SOCK_STREAM参数。 - 连接到服务器:使用
connect()函数连接到服务器的套接字,指定服务器的地址和端口号。 - 发送请求并接收响应:使用
send()函数向服务器发送请求,并使用recv()函数从服务器接收响应。这个过程可以在一个循环中进行,直到通信结束。 - 关闭连接:当通信结束后,使用
close()函数关闭连接套接字,释放资源。
- 创建Socket:使用
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,失败返回-1,errno被设置。 -
全部网络编程的函数,失败时基本上都是返回
-1,errno被设置,只要参数没填错,基本上不会失败。 -
注意:单个进程中创建的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- 数据不会丢失;
- 数据的顺序不会错乱;
- 双向通道。
SOCK_DGRAM:无连接的socket- 数据可能丢失;
- 数据的顺序可能会错乱;
- 传输效率更高。
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的区别
TCPTCP面向连接,通过三次握手建立连接,四次挥手断开连接;TCP是可靠的通信方式,通过超时重传、数据校验等方式来确保数据无差错,不丢失,不重复,并且按序到达;TCP把数据当成字节流,当网络出现波动时,连接可能出现响应延迟的问题;TCP只支持点对点通信;TCP报文的首部较大,为20字节;TCP是全双工的可靠信道。
UDPUDP是无连接的,即发送数据之前不需要建立连接,这种方式为UDP带来了高效的传输效率,但也导致无法确保数据的发送成功;UDP以最大的速率进行传输,但不保证可靠交付,会出现丢失、重复等等问题;UDP没有拥塞控制,当网络出现拥塞时,发送方不会降低发送速率;UDP支持一对一,一对多,多对一和多对多的通信;UDP报文的首部比较小,只有8字节;UDP是不可靠信道。
2.TCP保证自身可靠的方式
- 数据分片:在发送端对用户数据进行分片,在接收端进行重组,由
TCP确定分片的大小并控制分片和重组; - 到达确认:接收端接收到分片数据时,根据分片的序号向对端回复一个确认包;
- 超时重发:发送方在发送分片后开始计时,若超时却没有收到对端的确认包,将会重发分片;
- 滑动窗口:
TCP中采用滑动窗口来进行传输控制,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为0时,发送方不会再发送数据; - 失序处理:
TCP的接收端会把接收到的数据重新排序; - 重复处理:如果传输的分片出现重复,
TCP的接收端会丢弃重复的数据; - 数据校验:
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()函数会触发三次握手,三次握手完成后,客户端和服务端将建立一个双向的传输通道。 - 情景类似:
- 客户端对服务端说:我可以给你发送数据吗?
- 服务端回复:ok,不过,我也要给你发送数据。(这时候,客户端至服务端的单向传输通道已建立)。
- 客户端回复:ok。(这时候,服务端至客户端的单向传输通道已建立)。
- 细节:
- 客户端的
socket也有端口号,对程序员来说,不必关心客户端socket的端口号,所以系统随机分配。(socket通讯中的地址包括ip和端口号,但是,习惯中的地址仅指ip地址)。 - 服务端的
bind()函数,普通用户只能使用1024以上的端口,root用户可以使用任意端口。 listen()函数的第二个参数 + 1为已连接队列(ESTABLISHED状态,三次握手已完成但是没有被accept()的socket,只存在于服务端)的大小。(在高并发的服务程序中,该参数应该调大一些)SYN_RECV状态的连接也称为半连接。CLOSED是假想状态,实际上不存在。
- 客户端的
2.四次挥手
-
断开一个
TCP连接时,客户端和服务端需要相互总共发送四个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close()函数触发。 -
情景类似:
- 一端(A)对另一端(B)说:我不会给你发数据了,断开连接吧。
- B回复:ok。(这时候A不能对B发数据了,但是,B仍可以对A发数据)
- B发完数据了,对A说:我也不会给你发数据了。(这时候B也不能对A发数据了)
- A回复:ok。
-
细节:
-
1)主动断开的端在四次挥手后,
socket的状态为TIME_WAIT,该状态将持续2MSL(30秒/1分钟/2分钟)。 MSL(Maximum Segment Lifetime)报文在网络上存在的最长时间,超过这个时间报文将被丢弃。 -
如果是客户端主动断开,
TIME_WAIT状态的socket几乎不会造成危害。- 客户端程序的socket很少,服务端程序的socket很多(成千上万);
- 客户端的端口是随机分配的,不存在重用的问题。
-
如果是服务端主动断开,有两方面的危害:
- socket没有立即释放;
- 端口号只能在2MSL后才能重用。
-
在服务端程序中,用
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; -
问题:
send()函数有可能会阻塞吗? 如果自己的发送缓冲区和对端的接收缓冲区都满了,会阻塞。- 向
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流准备好进行读取、写入或连接时立即进行相应的操作,而不需要阻塞其他流。这种技术提高了系统的性能和效率,尤其适用于需要处理大量并发连接的网络服务器应用。
-
基本概念:
- IO(Input/Output): 指的是计算机与外部世界进行数据交换的过程,包括读取数据、写入数据和网络通信等操作。
- 多路(Multiplexing): 指的是一种技术,在同一个时间段内同时处理多个IO操作。
- 复用(Multiplexing): 指的是使用一种机制同时监视多个IO流,以便在有数据可读、可写或有连接请求时立即做出响应。
-
工作原理:
- IO多路复用通常基于操作系统提供的系统调用实现,如
select()、poll()、epoll()等。
- select(): 最古老的IO多路复用机制,在一个或多个IO流上进行监视,当有IO流准备好读取、写入或连接时,
select()函数会立即返回。但是,它存在一些性能和可扩展性问题,特别是在处理大量连接时。 - poll(): 类似于
select(),但是没有文件描述符数目的限制,使用数组来存储待监视的文件描述符。 - epoll(): 是Linux特有的高性能IO多路复用机制,使用红黑树(
epoll_create()创建的实例)或者哈希表(epoll_create1()创建的实例)来管理待监视的文件描述符。相比于select()和poll(),epoll()在处理大量连接时表现更优秀,因为它避免了遍历整个文件描述符集合的开销。
- IO多路复用通常基于操作系统提供的系统调用实现,如
-
优点:
- 高效: IO多路复用技术允许程序同时监视多个IO操作,而不需要创建多个线程或进程,因此可以降低系统开销。
- 可扩展: 在处理大量连接时,IO多路复用技术的性能表现更优秀,相比于多线程或多进程模型更容易扩展。
- 简单: 使用系统提供的API(如
select()、poll()、epoll())可以相对容易地实现IO多路复用功能。
-
适用场景:
- 高并发网络服务器: 如Web服务器、聊天服务器等需要同时处理大量连接的应用。
- 实时数据处理: 需要及时响应外部事件、传感器数据等的应用,如即时通讯、实时监控等。
-
总结:
- IO多路复用技术是一种高效、可扩展的IO操作管理方式,适用于需要处理大量并发IO操作的网络服务器和实时数据处理应用。通过合理地选择适合自身需求的IO多路复用机制,并结合非阻塞IO技术,可以提高系统的性能、可靠性和扩展性。
-
多进程服务器的缺点和解决办法:
- 多进程服务器的缺点和解决办法
- 资源消耗高: 每个客户端连接都需要创建一个新的进程,这会消耗大量的系统资源,包括内存、CPU时间和文件描述符等。
- 并发连接数受限: 操作系统对于进程的数量有一定的限制,当同时有大量客户端连接时,可能会导致无法创建更多的进程,从而限制了服务器的并发连接数。
- 进程切换开销大: 进程切换涉及到上下文的保存和恢复,会引入较大的开销,尤其在进程数量较多时,这种开销会明显增加。
- 同步与通信困难: 不同进程之间的通信通常需要使用IPC(Inter-Process Communication)机制,如管道、消息队列、信号量等,这增加了开发和维护的复杂度,容易引入死锁、竞态条件等问题。
- 解决多进程服务器模型的缺点,可以采用以下方法:
- 使用多线程代替多进程: 多线程模型相比多进程模型,线程的创建和切换开销较小,而且线程共享同一地址空间,通信更加简单高效。但需要注意线程安全问题。
- 使用进程池: 提前创建一定数量的进程,并将它们放入一个进程池中。当有新的连接请求到来时,从进程池中取出一个空闲的进程处理,这样可以避免频繁创建和销毁进程的开销。
- 优化进程间通信: 合理使用IPC机制,选择合适的通信方式,并对通信进行精心设计,以减少不必要的同步开销和数据拷贝开销。
- 使用异步IO: 异步IO模型能够在单个线程中管理多个IO操作,避免了进程或线程创建的开销,同时提高了系统的吞吐量和响应速度。通过事件驱动的方式,使得服务器能够高效处理大量并发连接。
- 采用单进程多路复用模型: 使用IO多路复用技术(如
select()、poll()、epoll()等),在单个进程中管理多个连接,从而减少了进程数量,降低了系统的开销,并提高了系统的并发性能。
- 综上所述,通过合理的设计和技术选择,可以有效地克服多进程服务器模型的缺点,提高服务器的性能、可靠性和可扩展性。
- 多进程服务器的缺点和解决办法
1.Select模型以及实战案例
-
Select模型具体步骤
-
准备文件描述符(FDs):在调用
select()之前,需要准备要监视的文件描述符(FDs),这些FDs可以是套接字、文件或任何其他类型的I/O流。 -
初始化fd_sets:创建三个
fd_set对象:readfds、writefds和exceptfds,它们分别表示要监视的读、写和异常事件的FD集合。 -
设置FDs在fd_sets中:
使用
FD_ZERO()来清除每个fd_set对象。使用
FD_SET()将要监视的FD添加到相应的fd_set中。 -
设置超时(可选):可选地指定超时值以限制
select()等待事件的时间。如果不想指定超时,可以传递NULL。 -
调用Select:调用
select()函数,传入任何一个集合中最高编号的FD加1,以及读、写和异常事件的fd_set对象,以及可选的超时值。 -
检查返回值:
select()将返回就绪并包含在集合中(readfds、writefds、exceptfds)的FD的总数。如果返回0,则表示发生超时。如果返回-1,则表示发生错误。 -
检查FDs的事件:
在
select()返回后,需要遍历fd_set对象,并检查哪些FD准备好了读取、写入,或者有异常。使用
FD_ISSET()来检查特定的FD是否在集合中。 -
处理事件:处理就绪FD的I/O事件。例如,如果一个FD准备好读取,则从中读取数据。如果一个FD准备好写入,则向其写入数据。如果一个FD有异常,则相应地处理异常。
-
重复或退出:处理事件后可以通过返回第2步来重复这个过程,或者如果完成了,退出程序。
-
清理(可选):根据需要清理资源,例如关闭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()函数:- 是否存在套接字接收数据?
- 通过检查可读事件集合(
readfds)来确定是否存在套接字可以接收数据。如果在调用select()后发现某个套接字在可读事件集合中,则表示该套接字可以接收数据。
- 通过检查可读事件集合(
- 无需阻塞传输数据的套接字有哪些?
- 无需阻塞传输数据的套接字包括在可写事件集合(
writefds)中的套接字。如果在调用select()后发现某个套接字在可写事件集合中,则表示该套接字可以立即向对端传输数据,而不会阻塞。
- 无需阻塞传输数据的套接字包括在可写事件集合(
- 哪些套接字发生了异常?
- 通过检查异常事件集合(
exceptfds)来确定哪些套接字发生了异常。如果在调用select()后发现某个套接字在异常事件集合中,则表示该套接字发生了异常情况,可能需要关闭或处理。
- 通过检查异常事件集合(
- 是否存在套接字接收数据?
-
2.Epoll模型
-
Select模型的缺点:- 效率低下: Select 模型采用了轮询的方式来检查多个文件描述符的状态变化,当文件描述符数量增加时,需要不断遍历检查,导致性能下降。特别是当需要监视的文件描述符数量较大时,Select 的效率会显著降低。
- 文件描述符数量限制: 在很多操作系统中,Select 函数所能监视的文件描述符数量是有限制的,一般情况下,这个限制是固定的,例如1024或者更小。这意味着如果要同时处理大量的连接或者文件描述符,Select 就无法满足需求。
- 复制文件描述符集: 每次调用 Select 函数都需要传递一份文件描述符集的副本,这意味着当文件描述符数量非常大时,会产生较大的额外开销,包括内存和时间。
- 不支持跨平台: Select 函数在不同的操作系统上可能存在一些差异,而且有些操作系统并不支持 Select 函数,例如 Windows 下没有 Select 函数,而是使用了类似的函数如 WSAPoll 或者 WSAWaitForMultipleEvents。
- 不方便扩展: Select 模型的接口设计较为简单,不支持更复杂的事件处理,例如异步IO等。在需要处理更复杂场景的时候,Select 模型的扩展能力相对较弱。
- 综上所述,虽然 Select 模型在一定程度上简单易用,并且适用于少量文件描述符的情况,但是在高并发场景下,效率和性能上存在一定的局限性,因此在实际开发中需要根据具体的应用场景选择合适的 IO 复用模型。
-
Epoll的三大函数:epoll_createepoll_waitepoll_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:用于指定监视对象的添加、删除或更改等操作;EPOLL_CTL_ADDEPOLL_CTL_DELEPOLL_CTL_MOD
__fd:需要注册的监视对象文件描述符;__event:监视对象的事件类型:EPOLLIN:需要读取数据的情况;EPOLLOUT:输出缓冲为空,可以立即发送数据的情况;EPOLLPRI:收到OOB数据的情况;EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用;EPOLLERR:发生错误的情况;EPOLLET:以边缘触发的方式得到事件通知;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个事件。
相关文章:
Linux C++
1.Linux环境配置 1.安装C和C的编译器 yum -y install gcc* // centos72.升级编译器 升级软件包: yum -y install centos-release-scl devtoolset-8-gcc*启用软件包: echo "source /opt/rh/devtoolset-8/enable" >>/etc/profile # 每次…...
Apache Doris 基础 -- 部分数据类型及操作
您还可以使用SHOW DATA TYPES;查看Doris支持的所有数据类型。 部分类型如下: Type nameNumber of bytesDescriptionSTRING/可变长度字符串,默认支持1048576字节(1Mb),最大精度限制为2147483643字节(2gb)。大小可以通过BE配置string_type_le…...
大话C语言:第25篇 动态库
1 动态库概述 C语言动态库(也称为共享库)是在程序运行时被加载到内存中的库文件,它包含了可由多个程序共享的代码和数据。动态库在编译时不会被直接链接到目标程序中,而是在程序运行时动态加载。这种特性使得动态库具有一些优势&a…...
数据分析:RT-qPCR分析及R语言绘图
介绍 转录组分析是一种用于研究细胞或组织中所有RNA分子的表达水平的高通量技术。完成转录组分析后,科学家们通常需要通过定量实时聚合酶链式反应(qRT-PCR)来验证二代测序(Next-Generation Sequencing, NGS)结果的可靠…...
无线模块通过TCP/IP协议实现与PC端的数据传输解析
在当今的信息时代,无线通信技术的发展日新月异,为我们的工作和生活带来了极大的便利。其中,无线通信模块通过TCP/IP协议向PC端传送数据已经成为了一种常见的通信方式。 无线通信模块是一种能够在无线网络中进行数据传输的设备。它通常集成了…...
嵌入式实验---实验一 通用GPIO实验
一、实验目的 1、掌握STM32F103 GPIO程序设计流程; 2、熟悉STM32固件库的基本使用。 二、实验原理 1、通过按键实现:按键按下,LED点亮;按键释放,LED熄灭。 三、实验设备和器材 电脑、Keil uVision5软件、Proteus…...
中国首例!「DataKit」上架亚马逊云科技 Marketplace add-ons
在 2022 年的 re:Invent 大会上,亚马逊云科技宣布了一项重大更新:亚马逊云科技 Marketplace 为 Amazon Elastic Kubernetes Service(Amazon EKS)提供了附加组件的支持。这一创新功能极大地丰富了 EKS 的生态系统,使用户…...
【博士每天一篇文献-算法】Progressive Neural Networks
阅读时间:2023-12-12 1 介绍 年份:2016 作者:Andrei A. Rusu,Neil Rabinowitz,Guillaume Desjardins,DeepMind 研究科学家,也都是EWC(Overcoming catastrophic forgetting in neural networks)算法的共同作者。 期刊: 未录用&am…...
深圳中小企业融资攻略,贷款方法大盘点!
中小企业融资这事,可不是一个简单的事情。资金对中小企业来说,就像血液对人体一样重要。企业发展离不开资金支持,特别是在今年这个环境下,政策对中小企业还挺友好的。今天讲解一下中小微企业常用的几种贷款方法。希望能让大家更明…...
Android的自启动
最近要用到这个,所以也花时间看看。 从分层来说,安卓的自启动也分成三种,app的自启动,framework服务的自启动,HAL服务的自启动。现在简单说说这三种吧。当然,我主要关注的还是最后一种。。。 一 App的自启…...
开源VisualFbeditor中文版,vb7 IDE,VB6升级64位跨平台开发安卓APP,Linux程序
吴涛老矣,社区苦无64位易语言,用注入DLL增强菜单,做成VS一样的界面 终归是治标不治本,一来会报毒,二来闭源20年没更新了 开源的VB7,欢迎易语言的铁粉进群:1032313876 【Freebasic编程语言】编绎…...
github安全问题token和sshkeys
文章目录 sshkeys问题问题方法一:方法二:获取密钥添加密钥token问题问题:生成tokens设置tokenssshkeys问题 问题 当我们git clone代码时,会报如下错误,此时有2种解决方法。 git clone git@github.com:gjianw217/xboard-uboot.git Cloning into xboard-uboot... Permissio…...
超详细的selenium使用指南
🍅 视频学习:文末有免费的配套视频可观看 🍅 点击文末小卡片 ,免费获取软件测试全套资料,资料在手,涨薪更快 概述 selenium是网页应用中最流行的自动化测试工具,可以用来做自动化测试或者浏览器…...
LogicFlow 学习笔记——1. 初步使用 LogicFlow
什么是 LogicFlow LogicFlow 是一个开源的前端流程图编辑器和工作流引擎,旨在帮助开发者和业务人员在网页端创建、编辑和管理复杂的业务流程和工作流。它提供了一个直观的界面和强大的功能,使得设计和管理工作流变得更加高效和便捷。 官网地址ÿ…...
场外个股期权通道业务是什么意思?
今天带你了解场外个股期权通道业务是什么意思?场外个股期权业务是指在沪深交易所之外进行的个股期权交易。它是一种非标准化的合约,不在交易所内进行交割。 场外个股期权通道业务,是指投资者通过与场外个股期权机构通道签订合约,购…...
分页插件结合collection标签后分页数量不准确的问题
问题1:不使用collection 聚合分页正确 简单列子 T_ATOM_DICT表有 idname1原子12原子23原子34原子45原子56原子6 T_ATOM_DICT_AUDIT_ROUTE表审核记录表有 idaudit1拒绝1通过4拒绝 我要显示那些原子审核了,我把两个表inner join 就是那些原子审核过了 idnameaudit1原子1拒绝…...
git diff 命令
目录标题 [Q&A] git diff 作用常见用法比较工作目录与暂存区比较暂存区与最近一次提交比较工作目录与最近一次提交比较两个具体的提交之间差异 [Q&A] git diff 作用 git diff 用于展示不同版本之间文件内容的变化。 常见用法 比较工作目录与暂存区 显示工作目录中尚…...
Code Review常用术语
CR: Code Review. 请求代码审查。PR: pull request. 拉取请求,给其他项目提交代码。MR: merge request. 合并请求。LGTM: Looks Good To Me.对我来说,还不错。表示认可这次PR,同意merge合并代码到远程仓库。…...
HashMap 源码中的巧妙小技巧
根据容量计算大于容量的最小的哈希表的大小(table的length),这里的length需要满足length2^n,也就是我们需要根据容量算出最小的n的值 static final int tableSizeFor(int cap) {int n cap - 1;n | n >>> 1;n | n >>> 2;n | n >&g…...
极具吸引力的小程序 UI 风格
极具吸引力的小程序 UI 风格...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...
BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
k8s业务程序联调工具-KtConnect
概述 原理 工具作用是建立了一个从本地到集群的单向VPN,根据VPN原理,打通两个内网必然需要借助一个公共中继节点,ktconnect工具巧妙的利用k8s原生的portforward能力,简化了建立连接的过程,apiserver间接起到了中继节…...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
Java毕业设计:WML信息查询与后端信息发布系统开发
JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发,实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构,服务器端使用Java Servlet处理请求,数据库采用MySQL存储信息࿰…...
招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...
