1.Linux环境配置
1.安装C和C++的编译器
bash
yum -y install gcc* // centos7
2.升级编译器
-
升级软件包:
bashyum -y install centos-release-scl devtoolset-8-gcc*
-
启用软件包:
bashecho "source /opt/rh/devtoolset-8/enable" >>/etc/profile # 每次启动shell的时候,会执行/etc/profile脚本
或者:
bashmv /usr/bin/gcc /usr/bin/gcc-4.8.5 ln -s /opt/rh/devtoolset-8/root/bin/gcc /usr/bin/gcc mv /usr/bin/g++ /usr/bin/g++-4.8.5 ln -s /opt/rh/devtoolset-8/root/bin/g++ /usr/bin/g++
3.安装库函数的帮助文档
bash
yum -y install man-pages
-
帮助文档的使用
bashman 级别 命令或者函数
- 显示帮助的界面可以用vi的命令,
q
退出 - man的级别:
用户命令
- 系统接口
库函数
- 特殊文件,比如设备文件
- 文件
- 游戏
- 系统的软件包
- 系统管理命令
- 内核
- 显示帮助的界面可以用vi的命令,
4.编译
bash
gcc/g++ 选项 源代码文件1 源代码文件2 源代码文件n
- 常用选项:
-o
指定输出的文件名,这个名称不能和源文件同名。如果不给出这个选项,则生成可执行文件a.out
-g
如果想对源代码进行调试,必须加入这个选项-On
在编译、链接过程中进行优化处理,生成的可执行程序效率将更高-c
只编译,不链接成为可执行文件,通常用于把源文件编译成静态库或动态库-std=c++11
支持C++11标准- 优化选项:
-O0
不做任何优化,这是默认的编译选项-O
或者-O1
对程序做部分编译优化,对于大函数,优化编译占用稍微多的时间和相当大的内存。使用本项优化,编译器会尝试减小生成代码的尺寸,以及缩短执行时间,但并不执行需要占用大量编译时间的优化-O2
这是推荐的优化等级。与O1
比较而言,O2
优化增加了编译时间的基础上,提高了生成代码的执行效率-O3
这是最高最危险的优化等级。用这个选项会延长编译代码的时间,并且在使用gcc4.x的系统里不应全局启用。自从3.x版本以来gcc的行为已经有了极大地改变。在3.x,-O3
生成的代码也只是比-O2
快一点点而已,而gcc4.x中还未必更快。用-O3
来编译所有的软件包将产生更大体积更耗内存的二进制文件,大大增加编译失败的机会或不可预知的程序行为(包括错误)。这样做将得不偿失,记住过犹不及。在gcc 4.x.中使用-O3
是不推荐的- 如果使用了优化选项:
- 编译的时间将会更长
- 目标程序不可调试
- 有效果,但是不可能显著提升程序的性能
2.静态库和动态库
- 在实际开发中,我们把通用的函数和类分文件编写,称之为库。在其它的程序中,可以使用库中的函数和类
- 一般来说,通用的函数和类不提供源代码文件(安全性、商业机密),而是编译成二进制文件
- 库的二进制文件有两种:静态库和动态库
1.静态库
-
制作静态库
bashg++ -c -o lib 库名.a 源代码文件清单
-
使用静态库
-
不规范的做法:
bashg++ 选项 源代码文件名清单 静态库文件名
-
规范的做法:
bashg++ 选项 源代码文件名清单 -l 库名 -L 库文件所在的目录名
-
-
静态库的概念
-
程序在编译时会把库文件的二进制代码链接到目标程序中,这种方式称为静态链接。
如果多个程序中用到了同一静态库中的函数或类,就会存在多份拷贝。
-
-
静态库的特点
- 静态库的链接是在编译时期完成的,执行的时候代码加载速度快。
- 目标程序的可执行文件比较大,浪费空间。
- 程序的更新和发布不方便,如果某一个静态库更新了,所有使用它的程序都需要重新编译。
2.动态库
-
制作动态库
bashg++ -fPIC -shared -o lib 库名.so 源代码文件清单
-
使用动态库
-
不规范的做法:
bashg++ 选项 源代码文件名清单 动态库文件名
-
规范的做法:
bashg++ 选项 源代码文件名清单 -l 库名 -L 库文件所在的目录名
-
运行可执行程序的时候,需要提前设置
LD_LIBRARY_PATH
环境变量。
-
-
动态库的概念
-
程序在编译时不会把库文件的二进制代码链接到目标程序中,而是在运行时候才被载入。
如果多个进程中用到了同一动态库中的函数或类,那么在内存中只有一份,避免了空间浪费问题。
-
-
动态库的特点
- 程序在运行的过程中,需要用到动态库的时候才把动态库的二进制代码载入内存。
- 可以实现进程之间的代码共享,因此动态库也称为共享库。
- 程序升级比较简单,不需要重新编译程序,只需要更新动态库就行了。
- 如果动态库和静态库同时存在,编译器将优先使用动态库。
3.main函数的参数
1.main函数的参数
-
main
函数有三个参数,argc
、argv
和envp
,它的标准写法如下:c++int main(int agrc, char *argv[], char *envp[]) { return 0; }
-
argc
存放了程序参数的个数,包括程序本身。 -
argv
字符串的数组,存放了每个参数的值,包括程序本身。 -
envp
字符串的数组,存放了环境变量,数组的最后一个元素是空。 -
在程序中,如果不关心
main()
函数的参数,可以省略不写。
2.操作环境变量
c++
int setenv(const char *name, const char *value, int overwrite);
-
name
环境变量名。 -
value
环境变量的值。 -
overwrite
0-如果环境如果环境不存在,增加新的环境变量,如果环境变量已存在,不替换其值;非0-如果环境不存在,增加新的环境变量,如果环境变量已存在,替换其值返回值:0-成功;-1-失败(失败的情况极少见)
注意:此函数设置的环境变量只对本进程有效,不会影响shell的环境变量。如果在运行程序时执行了setenv()函数,进程终止后再次运行该程序,上次的设置是无效的。
c++
char* getenv(const char *name);
3.示例
c++
#include <iostream>
#include <cstdlib>
int main(int argc, char *argv[], char *envp[]) {
// 检查参数数量是否正确
if (argc != 3) {
std::cout << "Usage: ./demo <arg1> <arg2>" << std::endl;
return -1;
}
// 显示命令行参数
std::cout << "Command line arguments:" << std::endl;
for (int i = 0; i < argc; ++i) {
std::cout << "argv[" << i << "] = " << argv[i] << std::endl;
}
// 显示环境变量
std::cout << "\nEnvironment variables:" << std::endl;
for (int i = 0; envp[i] != nullptr; ++i) {
std::cout << "envp[" << i << "] = " << envp[i] << std::endl;
}
// 设置环境变量AA
setenv("AA", "aaaa", 1);
// 显示环境变量AA的值
std::cout << "\nEnvironment variable AA=" << getenv("AA") << std::endl;
return 0;
}
4.gdb的常用命令
- 如果程序有问题,不要问别人为什么会这样,而是立即动手调试。
1.安装gdb
bash
yum -y install gdb
2.gdb常用命令
-
如果希望程序可调试,编译时需要加
-g
选项,并且,不能使用-O
的优化选项。bashgdb 目标程序
命令 简写 命令说明 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调试正在运行中的程序
bash
gdb 程序名 -p 进程编号
5.Linux的时间操作
- UNIX操作系统根据计算机产生的年代把1970年1月1日作为UNIX的纪元时间,1970年1月1日是时间的中间点,将从1970年1月1日起经过的秒数用一个整数存放。
1.time_t别名
-
time_t
用于表示时间类型,它是一个long
类型的别名,在<time.h>
文件中定义,表示从1970年1月1日0时0分0秒到现在的秒数。c++typedef long time_t;
2.time()库函数
-
time()
库函数用于获取操作系统的当前时间。 -
包含头文件:
<time.h>
-
声明:
c++time_t time(time_t *tloc);
有两种调用方法:
c++time_t now = time(0); // 将空地址传递给time()函数,并将time()返回值赋给变量now
或者:
c++time_t now; time(&now); // 将变量now的地址作为参数传递给time()函数
3.tm结构体
-
time_t
是一个长整数,不符合人类的使用习惯,需要转换成tm结构体,tm结构体在<time.h>
中声明,如下:c++struct tm { int tm_sec; /* 秒. [0-60] */ int tm_min; /* 分. [0-59] */ int tm_hour; /* 时. [0-23] */ int tm_mday; /* 日期. [1-31] */ int tm_mon; /* 月份. [0-11] */ int tm_year; /* 年份 - 1900. */ int tm_wday; /* 星期. [0-6] */ int tm_yday; /* 从每年的1月1日开始算起的天数.[0-365] */ int tm_isdst; /* 夏令时标识符. [-1/0/1]*/ };
4.localtime()库函数
-
localtime()
函数用于把time_t
表示的时间转换为tm
结构体表示的时间。 -
localtime()
函数不是线程安全的,localtime_r()
是线程安全的。 -
包含头文件:
<time.h>
-
函数声明:
c++extern struct tm *localtime (const time_t *__timer) __THROW; extern struct tm *localtime_r (const time_t *__restrict __timer, struct tm *__restrict __tp) __THROW;
-
示例:
c++#include <iostream> #include <time.h> #include <cstring> int main() { time_t now = time(0); // 获取当前时间,存放在now中。 std::cout << "now = " << now << std::endl; // 显示当前时间,1970年1月1日到现在的秒数。 tm tmnow; localtime_r(&now, &tmnow); // 把整数的时间转换成tm结构体。 // 根据tm结构体拼接成习惯的字符串格式。 std::string stime = std::to_string(tmnow.tm_year + 1900) + "-" + std::to_string(tmnow.tm_mon + 1) + "-" + std::to_string(tmnow.tm_mday) + " " + std::to_string(tmnow.tm_hour) + ":" + std::to_string(tmnow.tm_min) + ":" + std::to_string(tmnow.tm_sec); std::cout << "stime = " << stime << std::endl; return 0; }
5.mktime()库函数
-
mktime()
函数的功能与localtime()
函数相反,用于把tm
结构体时间转换为time_t
时间。 -
包含头文件:
<time.h>
-
函数声明:
c++extern time_t mktime (struct tm *__tp) __THROW;
-
该函数主要用于时间的运算,例如:把 2024-01-01 00:00:00加30分钟。
-
思路:
- 解析字符串格式的时间,转换成
tm
结构体; - 用
mktime()
函数把tm
结构体转换成time_t
时间; - 把
time_t
时间加30*60秒; - 用
localtime_r()
函数把time_t
时间转换成tm
结构体; - 把
tm
结构体转换成字符串。
- 解析字符串格式的时间,转换成
-
示例:
c++#include <iostream> #include <time.h> #include <cstring> int main() { // 初始时间字符串 const char *initial_time_str = "2024-01-01 00:00:00"; // 解析时间字符串 struct tm tm_time; memset(&tm_time, 0, sizeof(tm_time)); if (strptime(initial_time_str, "%Y-%m-%d %H:%M:%S", &tm_time) == nullptr) { std::cerr << "Failed to parse time string" << std::endl; return -1; } // 转换 tm 结构体到 time_t time_t time = mktime(&tm_time); if (time == -1) { std::cerr << "Failed to convert to time_t" << std::endl; return -1; } // 增加 30 分钟(1800 秒) time += 30 * 60; // 转换 time_t 到 tm 结构体 struct tm new_tm_time; localtime_r(&time, &new_tm_time); // 转换 tm 结构体到字符串 char new_time_str[20]; strftime(new_time_str, sizeof(new_time_str), "%Y-%m-%d %H:%M:%S", &new_tm_time); // 输出结果 std::cout << "Initial time: " << initial_time_str << std::endl; std::cout << "New time: " << new_time_str << std::endl; return 0; }
-
6.gettimeofday()库函数
-
用于获取1970年1月1日到现在的秒和当前秒中已逝去的微秒数,可以用于程序的计时。
-
包含头文件:
<sys/time.h>
-
函数声明:
c++typedef struct timezone *__restrict __timezone_ptr_t; extern int gettimeofday (struct timeval *__restrict __tv, __timezone_ptr_t __tz) __THROW __nonnull ((1)); struct timeval { __time_t tv_sec; /* 秒. */ __suseconds_t tv_usec; /* 微秒. */ }; struct timezone { int tz_minuteswest; /* 格林威治以西几分钟. */ int tz_dsttime; /* 如果DST生效,则非零. */ };
-
示例:
c++#include <iostream> #include <sys/time.h> int main() { timeval start, end; gettimeofday(&start, 0); // 计时开始。 for (int i = 0; i < 1000000000; i++) ; gettimeofday(&end, 0); // 计时结束。 // 计算消耗的时长。 timeval tv; tv.tv_usec = end.tv_usec - start.tv_usec; tv.tv_sec = end.tv_sec - start.tv_sec; if (tv.tv_usec < 0) { tv.tv_usec = 1000000 - tv.tv_usec; tv.tv_sec--; } std::cout << "耗时: " << tv.tv_sec << " 秒和 " << tv.tv_usec << " 微秒。" << std::endl; return 0; }
7.程序睡眠
-
如果需要把程序挂起一段时间,可以使用
sleep()
和usleep()
两个库函数。 -
包含头文件:
<unistd.h>
-
函数声明:
c++extern unsigned int sleep (unsigned int __seconds); extern int usleep (__useconds_t __useconds);
6.Linux的目录操作
1.几个简单的目录操作函数
1.获取当前工作目录
-
包含头文件:
<unistd.h>
c++extern char *getcwd (char *__buf, size_t __size) __THROW __wur; extern char *get_current_dir_name (void) __THROW;
-
示例:
c++#include <iostream> #include <unistd.h> int main() { char path1[256]; // linux系统目录的最大长度是255。 getcwd(path1, 256); std::cout << "path1 = " << path1 << std::endl; char *path2 = get_current_dir_name(); std::cout << "path2 = " << path2 << std::endl; free(path2); // 注意释放内存 return 0; }
2.切换工作目录
-
包含头文件:
<unistd.h>
c++extern int chdir (const char *__path) __THROW __nonnull ((1)) __wur;
-
返回值:0-成功;其他-失败(目录不存在或没有权限)
3.创建目录
-
包含头文件:
<sys/stat.h>
c++extern int mkdir (const char *__path, __mode_t __mode) __THROW __nonnull ((1));
-
__path
:目录名 -
__mode
:访问权限,如0755,不要省略前置的0 -
返回值:0-成功;其他-失败(上级目录不存在或没有权限)
4.删除目录
-
包含头文件:
<unistd.h>
c++extern int rmdir (const char *__path) __THROW __nonnull ((1));
-
__path
:目录名 -
返回值:0-成功;其他-失败(上级目录不存在或没有权限)
2.获取目录中文件的列表
- 文件存放在目录中,在处理文件之前,必须先知道目录中有哪些文件,所以要获取目录中文件的列表。
-
包含头文件
c++#include <dirent.h>
-
相关的库函数
-
步骤一:用
opendir()
函数打开目录。c++extern DIR *opendir (const char *__name) __nonnull ((1));
-
成功-返回目录的地址,失败-返回空地址
-
步骤二:用
readdir()
函数循环的读取目录。c++extern struct dirent *readdir (DIR *__dirp) __nonnull ((1));
-
成功-返回
struct dirent
结构体的地址,失败-返回空地址。 -
步骤三:用
closerdir()
关闭目录c++extern int closedir (DIR *__dirp) __nonnull ((1));
-
-
数据结构
-
目录指针:
c++Dir *目录指针变量名;
-
每次调用
readdir()
,函数返回struct dirent
的地址,存放了本次读取到的内容。c++typdef unsigned long __ino_t; typdef long __off_t; struct dirent { __ino_t d_ino; // 索引节点号 __off_t d_off; // 在目录文件中的偏移 unsigned short int d_reclen; // 文件名长度 unsigned char d_type; // 文件类型 char d_name[256]; // 文件名,最长255字符,不能包含<limits.h>头文件 };
-
重点关注结构体的
d_name
和d_type
成员。 -
d_name
:文件名或目录名。 -
d_type
:文件的类型,有多种取值,最重要的是8和4,8-常规文件(A regular file);4-子目录(A directory),其它的暂时不关心。注意,d_name的数据类型是字符,不可直接显示。 -
示例:
c++#include <iostream> #include <dirent.h> int main(int argc, char *argv[]) { if (argc != 2) { std::cout << "using ./test 目录名\n"; return -1; } DIR *dir; // 定义目录指针。 // 打开目录。 if ((dir = opendir(argv[1])) == nullptr) return -1; // 用于存放从目录中读取到的内容。 struct dirent *stdinfo = nullptr; while (1) { // 读取一项内容并显示出来。 if ((stdinfo = readdir(dir)) == nullptr) break; std::cout << "文件名 = " << stdinfo->d_name << " 文件类型 = " << (int)stdinfo->d_type << std::endl; } closedir(dir); // 关闭目录指针。 }
-
7.Linux的系统错误
-
在C++程序中,如果调用了库函数,可以通过函数的返回值判断调用是否成功。其实,还有一个整型的全局变量
errno
,存放了函数调用过程中产生的错误代码。如果调用库函数失败,可以通过
errno
的值来查找原因,这也是调试程序的一个重要方法。errno
在<errno.h>
中声明。配合
strerror()
和perror()
两个库函数,可以查看出错的详细信息。
1.strerror()库函数
-
strerror()
在<string.h>
中声明,用于获取错误代码对应的详细信息。c++extern char *strerror (int __errnum) __THROW; // 非线程安全 extern char *strerror_r (int __errnum, char *__buf, size_t __buflen) __THROW __nonnull ((2)) __wur; // 线程安全
-
gcc8.3.1
一共有133个错误代码 -
示例(查看所有错误代码):
c++#include <iostream> #include <cstring> int main(int argc, char *argv[]) { for(int i = 0; i < 150; i++) { std::cout << i << ":" << strerror(i) << std::endl; } return 0; }
2.perror()库函数
-
perror()
在<stdio.h>
中声明,用于在控制台显示最近一次系统错误的详细信息,在实际开发中,服务程序在后台运行,通过控制台显示错误信息意义不大。(对调试程序略有帮助)c++extern void perror (const char *__s);
3.注意事项
-
调用库函数失败不一定会设置
errno
并不是全部的库函数在调用失败时都会设置
errno
的值,以man
手册为准(一般来说,不属于系统调用的函数不会设置errno
,属于系统调用的函数才会设置errno
)。 -
errno
不能作为调用库函数失败的标志errno
的值只有在库函数调用发生错误时才会被设置,当库函数调用成功时,errno
的值不会被修改,不会主动的置为0
。在实际开发中,判断函数执行是否成功还得靠函数的返回值,只有在返回值是失败的情况下,才需要关注
errno
的值。
8.目录和文件的更多操作
1.access()库函数
-
access()
函数用于判断当前用户对目录或文件的存取权限。 -
包含头文件:
c++#include <unistd.h>
-
函数声明:
c++extern int access (const char *__name, int __type) __THROW __nonnull ((1));
-
参数说明:
__name
:目录或文件名__type
:需要判断的存取权限,在头文件<unistd.h>
中的预定如下:c++/* 第二个参数要访问的值. 这些可以放在一起. */ #define R_OK 4 /* 测试读权限. */ #define W_OK 2 /* 测试写权限. */ #define X_OK 1 /* 测试执行权限. */ #define F_OK 0 /* 是否存在. */
-
返回值:
当
__name
满足__mode
权限返回0,不满足返回-1,error
被设置。在实际开发中,
access()
函数主要用于判断目录或文件是否存在。
2.stat()库函数
-
stat结构体
c++typedef unsigned long __dev_t; typedef unsigned long __ino_t; typedef unsigned long __nlink_t; typedef unsigned int __mode_t; typedef unsigned int __uid_t; typedef unsigned int __gid_t; typedef unsigned long __dev_t; typedef long __blksize_t; typedef long __blkcnt_t; typedef long __time_t; typedef long __syscall_slong_t; struct timespec { __time_t tv_sec; /* 秒. */ __syscall_slong_t tv_nsec; /* 纳秒. */ }; struct stat { __dev_t st_dev; /* 设备. */ __ino_t st_ino; /* 文件序号. */ __nlink_t st_nlink; /* 链接数. */ __mode_t st_mode; /* 文件模式. */ __uid_t st_uid; /* 文件所有者的用户ID. */ __gid_t st_gid; /* 文件组所属组ID.*/ int __pad0; __dev_t st_rdev; /* 设备号,如果是设备. */ __blksize_t st_blksize; /* I/O的最佳块大小. */ __blkcnt_t st_blocks; /* 分配的512字节块. */ struct timespec st_atim; /* 最后一次访问时间. */ struct timespec st_mtim; /* 最后一次修改时间. */ struct timespec st_ctim; /* 最后一次状态更改的时间. */ # define st_atime st_atim.tv_sec /* 向后兼容性. */ # define st_mtime st_mtim.tv_sec # define st_ctime st_ctim.tv_sec __syscall_slong_t __unused[3]; };
-
struct stat
结构体的成员变量比较多,重点关注st_mode
、st_size
和st_mtime
成员。注意:st_mtime
是一个整数表示的时间,需要程序员自己写代码转换格式。 -
st_mode
成员的取值很多,用以下两个宏来判断:c++#define __S_ISTYPE(mode, mask) (((mode) & __S_IFMT) == (mask)) #define S_ISREG(mode) __S_ISTYPE((mode), __S_IFREG) #define S_ISDIR(mode) __S_ISTYPE((mode), __S_IFDIR) S_ISREG(st_mode) // 是否为普通文件,如果是,返回真 S_ISDIR(st_mode) // 是否为目录,如果是,返回真
-
-
stat()库函数
-
包含头文件:
c++#include <sys/stat.h>
-
函数声明:
c++/* 获取file的文件属性并将它们放在BUF中. */ extern int stat (const char *__restrict __file, struct stat *__restrict __buf) __THROW __nonnull ((1, 2));
-
stat()
函数获取__file)
参数指定目录或文件的详细信息,保存到__buf
结构体中。 -
返回值:0-成功,-1-失败,
errno
被设置。 -
示例:
c++#include <iostream> #include <unistd.h> #include <cstring> #include <sys/stat.h> int main(int argc, char *argv[]) { if (argc != 2) { std::cout << "using: ./test 文件或目录名\n"; return -1; } struct stat st; // 存放目录或文件详细信息的结构体。 // 获取目录或文件的详细信息 if (stat(argv[1], &st) != 0) { std::cout << "stat(" << argv[1] << "):" << strerror(errno) << std::endl; return -1; } if (S_ISREG(st.st_mode)) std::cout << argv[1] << " 是一个文件(" << "mtime = " << st.st_mtime << ", size = " << st.st_size << ")\n"; if (S_ISDIR(st.st_mode)) std::cout << argv[1] << " 是一个目录(" << "mtime = " << st.st_mtime << ", size = " << st.st_size << ")\n"; return 0; }
-
3.utime()库函数
-
utime()
函数用于修改目录或文件的时间。 -
包含头文件:
c++#include <sys/types.h> #include <utime.h>
-
函数声明:
c++/* 将FILE的访问和修改次数设置为中给出的次数*FILE_TIMES。 如果FILE_TIMES为NULL,则设置为当前时间. */ extern int utime (const char *__file, const struct utimbuf *__file_times) __THROW __nonnull ((1));
-
utime()
函数用来修改参数__file
的st_atime
和st_time
。如果参数__file_times
为空地址,则设置为当前时间。结构utimbuf
声明如下:c++typedef long __time_t; /* 描述文件时间的结构. */ struct utimbuf { __time_t actime; /* 访问时间. */ __time_t modtime; /* 修改时间. */ };
-
-
返回值:0-成功,-1-失败,
errno
被设置。
4.rename()库函数
-
rename()
函数用于重命名目录或文件,相当于操作系统的mv
命令。 -
包含头文件:
c++#include <stdio.h>
-
函数声明:
c++extern int rename (const char *__old, const char *__new) __THROW;
-
参数说明:
__old
:源目录或文件名。__new
:目标目录或文件名。返回值:0-成功,-1-失败,
errno
被设置。
5.remove()库函数
-
remove()
函数用于删除目录或文件,相当于操作系统的rm
命令。 -
包含头文件:
c++#include <stdio.h>
-
函数声明:
c++/* 删除目录/文件. */ extern int remove (const char *__filename) __THROW;
-
参数说明:
__filename
待删除的目录或文件名。返回值:0-成功,-1-失败,
errno
被设置。
9.Linux的信号
1.信号的基本概念
-
信号(signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是,不能给进程传递任何数据。
-
信号产生的原因有很多,在
shell
中,可以用kill
和killall
命令发送信号:bashkill -信号的类型 进程编号 killall -信号的类型 进程名
-
查看系统定义的信号列表:
bashkill -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()
函数可以设置程序对信号的处理方式。 -
包含头文件:
c++#include <signal.h>
-
函数声明:
c++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.信号的应用示例
c++
#include <iostream>
#include <unistd.h>
#include <signal.h>
void EXIT(int sig)
{
std::cout << "收到了信号:" << sig << std::endl;
std::cout << "正在释放资源,程序将退出......\n";
// 以下是释放资源的代码。
std::cout << "程序退出。\n";
exit(0); // 进程退出。
}
int main(int argc, char *argv[])
{
// 忽略全部的信号,防止程序被信号异常中止。
for (int ii = 1; ii <= 64; ii++)
signal(ii, SIG_IGN);
// 如果收到 2 和 15 的信号(ctrl+c 和 kill、killall),本程序将主动退出。
signal(2, EXIT);
signal(15, EXIT);
while (true)
{
std::cout << "执行了一次任务。\n";
sleep(1);
}
return 0;
}
6.发送信号
-
Linux操作系统提供了
kill
和killall
命令向进程发送信号,在程序中,可以用kill()
函数向其它进程发送信号。 -
函数声明:
c++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
中,查看进程终止的状态:bashecho $?
-
正常终止进程的
3
个函数(exit()
和_Exit()
是由ISO C
说明的,_exit()
是由POSIX
说明的)。c++extern void exit (int __status) __THROW __attribute__ ((__noreturn__)); // <stdlib.h> extern void _exit (int __status) __attribute__ ((__noreturn__)); // <unistd.h> extern void _Exit (int __status) __THROW __attribute__ ((__noreturn__)); // <stdlib.h>
-
参数说明:
__status
也是进程终止的状态。如果进程被异常终止,终止状态为非
0
, 它们在服务程序的调度、日志和监控中常被用到。
2.资源释放的问题
return
表示函数返回,会调用局部对象的析构函数,main()
函数中的return
还会调用全局对象的析构函数。exit()
表示终止进程,不会调用局部对象的析构函数,只调用全局对象的析构函数。exit()
会执行清理工作,然后退出,_exit()
和_Exit()
直接退出,不会执行任何清理工作。
3.进程的终止函数
-
进程可以用
atexit()
函数登记终止函数(最多32个),这些函数将由exit()
自动调用。 -
包含头文件:
c++#include <stdlib.h>
-
函数声明:
c++/* 注册一个在调用 'exit' 时调用的函数. */ extern int atexit (void (*__func) (void)) __THROW __nonnull ((1));
-
exit()
调用终止函数的顺序与登记时相反。 -
使用
atexit()
注册一个进程终止的清理函数,用于使用exit()
终止进程后自动调用清理函数。
11.调用可执行程序
- Linux提供了
system()
函数和exec
函数族,在C++程序中,可以执行其它的程序(二进制文件、操作系统命令或shell脚本)。
1.system()函数
-
system()
函数提供了一种简单的执行程序的方法,把需要执行的程序和参数用一个字符串传给system()
函数就行了。 -
函数声明:
c++extern int system (const char *__command) __wur;
-
system()
函数的返回值比较麻烦。- 如果执行的程序不存在,
system()
函数返回非0
; - 如果执行程序成功,并且被执行的程序终止状态是
0
,system()
函数返回0
; - 如果执行程序成功,并且被执行的程序终止状态不是
0
,system()
函数返回非0
。
- 如果执行的程序不存在,
2.exec函数族
-
exec
函数族提供了另一种在进程中调用程序(二进制文件或shell脚本)的方法。 -
包含头文件:
c++#include <unistd.h>
-
exec
函数族的声明如下:c++/* 使用从 `environ` 获取的环境变量,执行 PATH 所指向的文件,并将 PATH 之后的所有参数传递给它,直到遇到一个空指针。 */ extern int execl (const char *__path, const char *__arg, ...) __THROW __nonnull ((1, 2)); /* 执行 FILE 所指向的文件,如果 FILE 不包含斜杠(/),则在 `PATH` 环境变量中搜索 FILE,并将 FILE 之后的所有参数传递给它,直到遇到一个空指针,同时使用 `environ` 中的环境变量。 */ extern int execlp (const char *__file, const char *__arg, ...) __THROW __nonnull ((1, 2)); /* 使用从 `environ` 获取的环境变量,执行 PATH 所指向的文件,并将 PATH 之后的所有参数传递给它,直到遇到一个空指针,之后的参数为环境变量。 */ extern int execle (const char *__path, const char *__arg, ...) __THROW __nonnull ((1, 2)); /* 使用从 `environ` 获取的环境变量,执行 PATH 所指向的文件,并将 ARGV 中的参数传递给它。 */ extern int execv (const char *__path, char *const __argv[]) __THROW __nonnull ((1, 2)); /* 执行 FILE 所指向的文件,如果 FILE 不包含斜杠(/),则在 `PATH` 环境变量中搜索 FILE,并将 ARGV 中的参数传递给它,同时使用 `environ` 中的环境变量。 */ extern int execvp (const char *__file, char *const __argv[]) __THROW __nonnull ((1, 2)); /* 执行 FILE 所指向的文件,如果 FILE 不包含斜杠(/),则在 `PATH` 环境变量中搜索 FILE,并将 ARGV 中的参数传递给它,同时使用 __envp 中的环境变量。 */ extern int execvpe (const char *__file, char *const __argv[], char *const __envp[]) __THROW __nonnull ((1, 2));
-
注意:
- 如果执行程序失败则直接返回
-1
,失败原因存于errno
中; - 新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈;
- 如果执行成功则函数不会返回,当在主程序中成功调用
exec
后,被调用的程序将取代调用者程序,也就是说,exec
函数之后的代码都不会被执行; - 在实际开发中,最常用的是
execl()
和execv()
,其它的极少使用。
- 如果执行程序失败则直接返回
-
示例:
c++#include <iostream> #include <string.h> #include <unistd.h> int main(int argc, char *argv[]) { int ret = execl("/bin/ls", "/bin/ls", "-lt", "/tmp", nullptr); // 最后一个参数 nullptr 不能省略。 std::cout << "ret = " << ret << std::endl; perror("execl"); /* char *args[10]; args[0] = strdup("/bin/ls"); args[1] = strdup("-lt"); args[2] = strdup("/tmp"); args[3] = nullptr; int ret = execv("/bin/ls", args); std::cout << "ret = " << ret << std::endl; perror("execv"); // 释放动态分配的内存 for (int i = 0; args[i] != nullptr; ++i) { free(args[i]); } */ return 0; }
12.创建进程
1.Linux的0、1和2号进程
-
整个Liunx系统的全部进程是一个树形结构。
0
号进程(系统进程)是所有进程的祖先,它创建了1
号和2
号进程;1
号进程(systemd)负责执行内核的初始化工作和进行系统配置;2
号进程(kthreadd)负责所有内核线程的调度和管理。
-
用
pstree
命令可以查看进程树(yum -y install psmisc)
bashpstree -p 进程编号
2.进程标识
-
每个进程都有一个非负整数表示的唯一的进程ID,虽然是唯一的,但是进程ID可以复用。当一个进程终止后,其进程ID就成了复用的候选者。Linux采用延迟复用算法,让新建进程的ID不同于最近终止的进程所使用的ID。这样防止了新进程被误认为是使用了同一ID的某个已终止的进程。
-
包含头文件:
c++#include <sys/types.h> #include <unistd.h>
-
函数声明:
c++typedef int __pid_t; /* 获取调用进程的进程ID. */ extern __pid_t getpid (void) __THROW; /* 获取调用进程的父进程的进程ID. */ extern __pid_t getppid (void) __THROW;
3.fork()函数
-
一个现有的进程可以调用
fork()
函数创建一个新的进程。 -
包含头文件:
c++#include <unistd.h>
-
函数声明:
c++typedef int __pid_t; /* 克隆调用进程,创建一个精确的副本. 错误返回-1, 新进程返回0, 并将新进程的进程ID赋给旧进程. */ extern __pid_t fork (void) __THROWNL;
-
由
fork()
创建的新进程被称为子进程
。子进程是父进程的副本,父进程和子进程都从调用fork()
之后的代码开始执行。 -
fork()
函数被调用一次,但返回两次。两次返回的区别是子进程
的返回值是0
,而父进程
的返回值则是子进程
的进程ID
。 -
子进程
获得了父进程
数据空间、堆和栈的副本(注意:子进程拥有的是副本,不是和父进程共享)。 -
fork()
之后,父进程和子进程的执行顺序是不确定的 -
示例:
c++#include <iostream> #include <unistd.h> int main() { int num = 0; std::string message = "初始化信息."; pid_t pid = fork(); if (pid > 0) { // 父进程将执行这段代码。 sleep(1); std::cout << "父进程pid: " << pid << std::endl; std::cout << "父进程num: " << num << ", msg: " << message << std::endl; } else { // 子进程将执行这段代码。 num = 1; message = "子进程修改后的信息."; std::cout << "子进程pid: " << pid << std::endl; std::cout << "子进程num: " << num << ", msg: " << message << std::endl; } return 0; }
4.fork()的两种做法
- 父进程复制自己,然后父进程和子进程分别执行不同的代码。这种用法在网络服务程序中很常见,父进程等待客户端的连接请求,当请求到达时,父进程调用
fork()
,让子进程处理些请求,而父进程则继续等待下一个连接请求。 - 进程要执行另一个程序。这种用法在
shell
中很常见,子进程从fork()
返回后立即调用exec
。
-
示例:
c++#include <iostream> #include <unistd.h> int main() { if (fork() > 0) { // 父进程将执行这段代码。 while (true) { sleep(1); std::cout << "父进程运行中..." << std::endl; } } else { // 子进程将执行这段代码。 sleep(10); std::cout << "子进程开始执行任务..." << std::endl; execl("/bin/ls", "/bin/ls", "-lt", "/tmp", 0); std::cout << "子进程执行任务结束,退出." << std::endl; } return 0; }
5.共享文件
-
fork()
的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程和子进程共享同一个文件偏移量。 -
如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么它们的输出可能会相互混合。
-
示例:
c++#include <iostream> #include <unistd.h> #include <fstream> int main() { std::ofstream fout; fout.open("/tmp/tmp.txt"); // 打开文件。 fork(); for (int i = 0; i < 10000000; i++) // 向文件中写入一千万行数据。 { fout << "进程: " << getpid() << ", i = " << i << std::endl; // 写入的内容无所谓。 } fout.close(); // 关闭文件。 return 0; }
6.vfork()函数
-
vfork()
函数的调用和返回值与fork()
相同,但两者的语义不同。 -
vfork()
函数用于创建一个新进程,而该新进程的目的是exec
一个新程序,它不复制父进程的地址空间,因为子进程会立即调用exec
,于是也就不会使用父进程的地址空间。如果子进程使用了父进程的地址空间,可能会带来未知的结果。 -
vfork()
和fork()
的另一个区别是:vfork()
保证子进程先运行,在子进程调用exec
或exit()
之后父进程才恢复运行。 -
示例:
c++#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { int x = 0; pid_t pid; pid = vfork(); if (pid < 0) { std::cerr << "vfork()失败." << std::endl; return 1; } else if (pid == 0) { // 子进程 std::cout << "子进程: x = " << x << std::endl; x = 1; // 修改子进程中的变量 x sleep(3); // 子进程执行完毕后休息三秒再退出 _exit(0); // 使用 _exit() 退出,避免在子进程中执行父进程的全局析构函数等 } else { // 父进程 // 等待子进程结束 waitpid(pid, nullptr, 0); std::cout << "父进程: x = " << x << std::endl; } return 0; }
13.僵尸进程
-
如果
父进程
比子进程
先退出,子进程
将被1
号进程托管(这也是一种让程序在后台运行的方法)。 -
如果
子进程
比父进程
先退出,而父进程
没有处理子进程
退出的信息,那么,子进程
将成为僵尸进程
。 -
僵尸进程
有什么危害?内核
为每个子进程
保留了一个数据结构,包括进程编号
、终止状态
、使用CPU时间
等。父进程
如果处理了子进程
退出的信息,内核
就会释放这个数据结构,父进程
如果没有处理子进程
退出的信息,内核
就不会释放这个数据结构,子进程
的进程编号将一直被占用。系统可用的进程编号是有限的,如果产生了大量的僵尸进程
,将因为没有可用的进程编号而导致系统不能产生新的进程。 -
僵尸进程
的避免:-
子进程
退出的时候,内核
会向父进程
发头SIGCHLD
信号,如果父进程
用signal(SIGCHLD, SIG_IGN)
通知内核
,表示自己对子进程
的退出不感兴趣,那么子进程
退出后会立即释放数据结构。 -
父进程
通过wait()/waitpid()
等函数等待子进程
结束,在子进程
退出之前,父进程
将被阻塞待。-
包含头文件:
c++#include <sys/types.h> #include <sys/wait.h>
-
函数声明:
c++#define __WAIT_STATUS void * typedef int __pid_t; // 结构体 struct rusage 在 <sys/resource.h> 内定义 /* 等待一个子进程消亡. 如果有,将其状态放在 *STAT_LOC 中 并返回其进程ID. 对于错误, 返回 (pid_t) -1. 这个函数是一个消去点因此没有标记 __THROW. */ extern __pid_t wait (__WAIT_STATUS __stat_loc); /* 等待匹配PID的子进程消亡. 当 PID 大于 0 时, 匹配进程号为PID的进程. 如果 PID 为 (pid_t) -1, 匹配任何进程. 如果 PID 为 (pid_t) 0, 则匹配任何进程与当前进程相同的进程组. 如果 PID 小于 -1 , 匹配任何进程 进程组为PID的绝对值. 如果在 OPTIONS 中设置了 WNOHANG 位, 则该子节点还没有死, 返回 (pid_t) 0. 如果成功, 返回PID并将死亡子进程的状态存储在STAT_LOC中. 错误时返回 (pid_t) -1. 如果 wuntracked 位是在 OPTIONS 中设置, 停止子进程返回状态; 否则不. 这个函数是一个消去点因此没有标记 __THROW. */ extern __pid_t waitpid (__pid_t __pid, int *__stat_loc, int __options); /* 等待子进程退出. 如果有, 将其状态放入 *STAT_LOC 和返回其进程ID. 如果出现错误返回 (pid_t) -1. 如果 USAGE 不是 Nil , 存储关于子进程资源使用情况的信息. 如果在 OPTIONS 中设置了 untrace 位, 停止子进程返回状态; 否则不. */ extern __pid_t wait3 (__WAIT_STATUS __stat_loc, int __options, struct rusage * __usage) __THROWNL; /* PID 类似于 waitpid. 其他参数如 wait3. */ extern __pid_t wait4 (__pid_t __pid, __WAIT_STATUS __stat_loc, int __options, struct rusage *__usage) __THROWNL; # define WIFEXITED(status) __WIFEXITED (__WAIT_INT (status)) # define WTERMSIG(status) __WTERMSIG (__WAIT_INT (status))
-
返回值是
子进程
的编号。 -
__stat_loc
:子进程终止的信息:- 如果是正常终止,宏
WIFEXITED(status)
返回真,宏WEXITSTATUS(stat_loc)
可获取终止状态; - 如果是异常终止,宏
WTERMSIG(status)
可获取终止进程的信号; - 如果父进程很忙,可以捕获
SIGCHLD
信号,在信号处理函数中调用wait()/waitpid()
。
- 如果是正常终止,宏
-
示例一:
c++#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { // 创建子进程 if (fork() > 0) { // 父进程的流程。 int sts; pid_t pid = wait(&sts); // 输出已终止的子进程编号 std::cout << "已终止的子进程编号是: " << pid << std::endl; // 判断子进程是否正常退出,并输出退出状态 if (WIFEXITED(sts)) { std::cout << "子进程是正常退出的,退出状态是:" << WEXITSTATUS(sts) << std::endl; } else { std::cout << "子进程是异常退出的,终止它的信号是:" << WTERMSIG(sts) << std::endl; } } else { // 子进程的流程。 // sleep(100); /* 如果取消注释 sleep(100),即使子进程出现段错误并退出, 父进程也会在等待期间一直阻塞,直到子进程结束或异常退出,或者等待时间达到 100 秒 在这段时间内,父进程会一直等待子进程的退出状态,不会继续执行下面的代码。 这意味着你可能会在程序中看到一段时间的停滞,直到子进程的退出状态可用或等待超时. */ // 这段代码首先对一个空指针解引用,会导致段错误,然后调用 exit() 函数退出,并指定退出状态为 1 int *p = 0; *p = 10; exit(1); } return 0; }
-
示例二:
c++#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> void func(int sig) // 子进程退出的信号处理函数。 { int sts; pid_t pid = wait(&sts); std::cout << "已终止的子进程编号是: " << pid << std::endl; if (WIFEXITED(sts)) { std::cout << "子进程是正常退出的,退出状态是: " << WEXITSTATUS(sts) << std::endl; } else { std::cout << "子进程是异常退出的,终止它的信号是: " << WTERMSIG(sts) << std::endl; } } int main() { signal(SIGCHLD, func); // 捕获子进程退出的信号。 if (fork() > 0) { // 父进程的流程。 while (true) { std::cout << "父进程正在执行任务." << std::endl; sleep(1); } } else { // 子进程的流程。 sleep(5); // int *p = nullptr; *p=10; exit(1); } return 0; } /*执行流程如下: 1. 父进程 fork 出子进程后,进入 while 循环,不断输出 "父进程正在执行任务." 的消息。 2. 子进程执行 sleep(5) 或对空指针解引用导致段错误,然后退出。 2.1. 如果子进程发生段错误: 2.1.1. 子进程异常退出时,操作系统发送 SIGCHLD 信号给父进程。 2.1.2. 父进程捕获到 SIGCHLD 信号,调用 func 函数处理。 2.1.3. 在 func 函数中,父进程调用 wait 函数等待子进程的退出状态。 2.1.4. 父进程输出已终止的子进程编号和子进程的退出状态,由于子进程是异常退出的,所以输出 "子进程是异常退出的,终止它的信号是: " 和相应的信号值。 2.2. 如果子进程没有发生段错误: 2.2.1. 子进程正常退出时,操作系统发送 SIGCHLD 信号给父进程。 2.2.2. 父进程捕获到 SIGCHLD 信号,调用 func 函数处理。 2.2.3. 在 func 函数中,父进程调用 wait 函数等待子进程的退出状态。 2.2.4. 父进程输出已终止的子进程编号和子进程的退出状态,由于子进程是正常退出的,所以输出 "子进程是正常退出的,退出状态是: " 和相应的退出状态值。 */
-
-
14.多线程和信号
-
在多进程的服务程序中,如果子进程收到退出信号,子进程自行退出,如果父进程收到退出信号,则应该先向全部的子进程发送退出信号,然后自己再退出。
-
示例:
c++#include <iostream> #include <unistd.h> #include <signal.h> void FatherEXIT(int sig); // 父进程的信号处理函数。 void ChildEXIT(int sig); // 子进程的信号处理函数。 int main() { // 忽略全部的信号,不希望被打扰。 for (int i = 1; i <= 64; i++) signal(ii, SIG_IGN); // 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程 // 但请不要用 "kill -9 +进程号" 强行终止 signal(SIGTERM, FatherEXIT); signal(SIGINT, FatherEXIT); // SIGTERM 15 SIGINT 2 while (true) { if (fork() > 0) // 父进程的流程。 { sleep(5); continue; } else // 子进程的流程。 { // 子进程需要重新设置信号。 signal(SIGTERM, ChildEXIT); // 子进程的退出函数与父进程不一样。 signal(SIGINT, SIG_IGN); // 子进程不需要捕获SIGINT信号。 while (true) { std::cout << "子进程: " << getpid() << " 正在运行中." << std::endl; sleep(3); continue; } } } return 0; } // 父进程的信号处理函数。 void FatherEXIT(int sig) { // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。 signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN); std::cout << "父进程退出, sig = " << sig << std::endl; kill(0, SIGTERM); // 向全部的子进程发送15的信号,通知它们退出。 // 在这里增加释放资源的代码(全局的资源)。 exit(0); } // 子进程的信号处理函数。 void ChildEXIT(int sig) { // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。 signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN); std::cout << "子进程: " << getpid() << "退出, sig = " << sig << std::endl; // 在这里增加释放资源的代码(只释放子进程的资源)。 exit(0); }
15.共享内存
- 多线程共享进程的地址空间,如果多个线程需要访问同一块内存,用全局变量就可以了。
- 在多进程中,每个进程的地址空间是独立的,不共享的,如果多个进程需要访问同一块内存,不能用全局变量,只能用共享内存。
- 共享内存(Shared Memory)允许多个进程(不要求进程之间有血缘关系)访问同一块内存空间,是多个进程之间共享和传递数据最高效的方式。进程可以将共享内存连接到它们自己的地址空间中,如果某个进程修改了共享内存中的数据,其它的进程读到的数据也会改变。
- 共享内存没有提供锁机制,也就是说,在某一个进程对共享内存进行读/写的时候,不会阻止其它进程对它的读/写。如果要对共享内存的读/写加锁,可以使用信号量。
- Linux中提供了一组函数用于操作共享内存。
1.shmget()函数
-
该函数用于创建/获取共享内存。
-
包含头文件:
c++#include <sys/ipc.h> #include <sys/shm.h>
-
函数声明:
c++typedef int key_t; typedef unsigned long size_t; /* 获取共享内存段. */ extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW;
-
参数说明:
-
__key
:共享内存的键值,是一个整数(typedef int key_t
),一般采用十六进制,例如0x5005
,不同共享内存的key不能相同。 -
__size
:共享内存的大小,以字节为单位。 -
__shmflg
:共享内存的访问权限,与文件的权限一样,例如0666 | IPC_CREAT
,0666
表示全部用户对它可读写,IPC_CREAT
表示如果共享内存不存在,就创建它。 -
返回值:成功返回共享内存的id(一个非负的整数),失败返回-1(系统内存不足、没有权限)。
-
查看系统的共享内存,包括:键值(
key
),共享内存id(shmid
),拥有者(owner
),权限(perms
),大小(bytes
)。bashipcs -m
-
手动删除共享内存。
bashipcrm -m 共享内存id
-
2.shmat()函数
-
该函数用于把共享内存连接到当前进程的地址空间。
-
函数声明:
c++/* 附加共享内存段. */ extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg) __THROW;
-
参数说明:
__shmid
:由shmget()
函数返回的共享内存标识。__shmaddr
:指定共享内存连接到当前进程中的地址位置,通常填0
,表示让系统来选择共享内存的地址。__shmflg
:标志位,通常填0
。
-
调用成功时返回共享内存起始地址,失败返回
(void*)-1
并设置errno
以指示错误原因。
3.shmdt()函数
-
该函数用于将共享内存从当前进程中分离,相当于
shmat()
函数的反操作。 -
函数声明:
c++/* 分离共享内存段. */ extern int shmdt (const void *__shmaddr) __THROW;
-
__shmaddr
:shmat()
函数返回的地址。 -
调用成功返回
0
,失败返回-1
。
4.shmctl()函数
-
该函数用于操作共享内存,最常用的操作是删除共享内存。
-
函数声明:
c++/* 共享内存控制操作. */ extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW;
-
参数说明:
__shmid
:shmget()
函数返回的共享内存id。__cmd
:操作共享内存的指令,如果要删除共享内存,填IPC_RMID
。__buf
:操作共享内存的数据结构的地址,如果要删除共享内存,填0
。
-
调用成功返回
0
,失败返回-1
。 -
注意:使用root创建的共享内存,不管创建的权限是什么,普通用户都无法删除。
5.示例
c++
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
// 共享内存结构体
struct shmdata
{
int id; // 一个简单的整数标识
char message[256]; // 一个消息字符串
};
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "using: ./test <id> <msg>" << std::endl;
return -1;
}
// 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。
int shmid = shmget(0x5005, sizeof(shmdata), 0640 | IPC_CREAT);
if (shmid == -1)
{
perror("共享内存创建失败");
return -1;
}
std::cout << "共享内存ID = " << shmid << std::endl;
// 第2步:把共享内存连接到当前进程的地址空间。
shmdata *ptr = (shmdata *)shmat(shmid, nullptr, 0);
if (ptr == (void *)-1)
{
perror("共享内存连接失败");
return -1;
}
// 第3步:使用共享内存,对共享内存进行读/写。
std::cout << "原始数据: id = " << ptr->id << ", 消息 = " << ptr->message << std::endl;
// 更新共享内存中的数据
ptr->id = std::atoi(argv[1]);
std::strncpy(ptr->message, argv[2], sizeof(ptr->message) - 1);
ptr->message[sizeof(ptr->message) - 1] = '\0'; // 确保字符串以null结尾
std::cout << "更新后的数据: id = " << ptr->id << ", 消息 = " << ptr->message << std::endl;
// 第4步:把共享内存从当前进程中分离。
if (shmdt(ptr) == -1)
{
perror("共享内存分离失败");
return -1;
}
// 第5步:删除共享内存(如果需要删除)。
/* if (shmctl(shmid, IPC_RMID, nullptr) == -1)
{
perror("共享内存删除失败");
return -1;
} */
return 0;
}
16.循环队列、信号量、生产/消费者模源码
c++
#ifndef __PUBLIC_HH
#define __PUBLIC_HH
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/sem.h>
// 循环队列模板类。
template <class TT, int MaxLength>
class squeue
{
private:
bool m_inited; // 队列被初始化标志,true-已初始化;false-未初始化。
TT m_data[MaxLength]; // 用数组存储循环队列中的元素。
int m_head; // 队列的头指针。
int m_tail; // 队列的尾指针,指向队尾元素。
int m_length; // 队列的实际长度。
squeue(const squeue &) = delete; // 禁用拷贝构造函数。
squeue &operator=(const squeue &) = delete; // 禁用赋值函数。
public:
squeue() { init(); } // 构造函数。
// 循环队列的初始化操作。
// 注意:如果用于共享内存的队列,不会调用构造函数,必须调用此函数初始化。
void init()
{
if (!m_inited)
{ // 循环队列的初始化只能执行一次。
m_head = 0; // 头指针。
m_tail = MaxLength - 1; // 为了方便写代码,初始化时,尾指针指向队列的最后一个位置。
m_length = 0; // 队列的实际长度。
std::memset(m_data, 0, sizeof(m_data)); // 数组元素清零。
m_inited = true;
}
}
// 元素入队,返回值:false-失败;true-成功。
bool push(const TT &ee)
{
if (full())
{
std::cout << "循环队列已满,入队失败。\n";
return false;
}
// 先移动队尾指针,然后再拷贝数据。
m_tail = (m_tail + 1) % MaxLength; // 队尾指针后移。
m_data[m_tail] = ee;
m_length++;
return true;
}
// 求循环队列的长度,返回值:>=0-队列中元素的个数。
int size() const
{
return m_length;
}
// 判断循环队列是否为空,返回值:true-空,false-非空。
bool empty() const
{
return m_length == 0;
}
// 判断循环队列是否已满,返回值:true-已满,false-未满。
bool full() const
{
return m_length == MaxLength;
}
// 查看队头元素的值,元素不出队。
TT &front()
{
return m_data[m_head];
}
// 元素出队,返回值:false-失败;true-成功。
bool pop()
{
if (empty())
return false;
m_head = (m_head + 1) % MaxLength; // 队列头指针后移。
m_length--;
return true;
}
// 显示循环队列中全部的元素。
// 这是一个临时的用于调试的函数,队列中元素的数据类型支持cout输出才可用。
void printqueue() const
{
for (int i = 0; i < size(); i++)
{
std::cout << "m_data[" << (m_head + i) % MaxLength << "], value="
<< m_data[(m_head + i) % MaxLength] << std::endl;
}
}
};
// 信号量类。
class csemp
{
private:
union semun
{ // 用于信号量操作的联合体。
int val;
struct semid_ds *buf;
unsigned short *arry;
};
int m_semid; // 信号量id(描述符)。
short m_sem_flg; // 信号量的标志位。
csemp(const csemp &) = delete; // 禁用拷贝构造函数。
csemp &operator=(const csemp &) = delete; // 禁用赋值函数。
public:
csemp() : m_semid(-1) {}
// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool init(key_t key, unsigned short value = 1, short sem_flg = SEM_UNDO);
// 信号量的P操作,如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool wait(short value = -1);
// 信号量的V操作。
bool post(short value = 1);
// 获取信号量的值,成功返回信号量的值,失败返回-1。
int getvalue() const;
// 销毁信号量。
bool destroy();
~csemp();
};
#endif // __PUBLIC_HH
c++
#include "public.h"
// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool csemp::init(key_t key, unsigned short value, short sem_flg)
{
if (m_semid != -1)
return false; // 如果已经初始化了,不必再次初始化。
m_sem_flg = sem_flg;
// 信号量的初始化不能直接用semget(key, 1, 0666 | IPC_CREAT)
// 因为信号量创建后,初始值是0,如果用于互斥锁,需要把它的初始值设置为1,
// 而获取信号量则不需要设置初始值,所以,创建信号量和获取信号量的流程不同。
// 信号量的初始化分三个步骤:
// 1) 获取信号量,如果成功,函数返回。
// 2) 如果失败,则创建信号量。
// 3) 设置信号量的初始值。
// 获取信号量。
if ((m_semid = semget(key, 1, 0666)) == -1)
{
// 如果信号量不存在,创建它。
if (errno == ENOENT)
{
// 用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取。
if ((m_semid = semget(key, 1, 0666 | IPC_CREAT | IPC_EXCL)) == -1)
{
if (errno == EEXIST)
{ // 如果错误代码是信号量已存在,则再次获取信号量。
if ((m_semid = semget(key, 1, 0666)) == -1)
{
perror("init 1 semget()");
return false;
}
return true;
}
else
{ // 如果是其它错误,返回失败。
perror("init 2 semget()");
return false;
}
}
// 信号量创建成功后,还需要把它初始化成value。
union semun sem_union;
sem_union.val = value; // 设置信号量的初始值。
if (semctl(m_semid, 0, SETVAL, sem_union) < 0)
{
perror("init semctl()");
return false;
}
}
else
{
perror("init 3 semget()");
return false;
}
}
return true;
}
// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool csemp::wait(short value)
{
if (m_semid == -1)
return false;
struct sembuf sem_b;
sem_b.sem_num = 0; // 信号量编号,0代表第一个信号量。
sem_b.sem_op = value; // P操作的value必须小于0。
sem_b.sem_flg = m_sem_flg;
if (semop(m_semid, &sem_b, 1) == -1)
{
perror("wait semop()");
return false;
}
return true;
}
// 信号量的V操作(把信号量的值增加value)。
bool csemp::post(short value)
{
if (m_semid == -1)
return false;
struct sembuf sem_b;
sem_b.sem_num = 0; // 信号量编号,0代表第一个信号量。
sem_b.sem_op = value; // V操作的value必须大于0。
sem_b.sem_flg = m_sem_flg;
if (semop(m_semid, &sem_b, 1) == -1)
{
perror("post semop()");
return false;
}
return true;
}
// 获取信号量的值,成功返回信号量的值,失败返回-1。
int csemp::getvalue() const
{
return semctl(m_semid, 0, GETVAL);
}
// 销毁信号量。
bool csemp::destroy()
{
if (m_semid == -1)
return false;
if (semctl(m_semid, 0, IPC_RMID) == -1)
{
perror("destroy semctl()");
return false;
}
return true;
}
// 信号量析构函数。
csemp::~csemp()
{
// 在析构函数中销毁信号量。
destroy();
}
c++
// 本程序演示循环队列的使用。
#include "public.h"
int main()
{
using ElemType = int;
squeue<ElemType, 5> Queue;
ElemType element; // 创建一个数据元素。
std::cout << "元素(1、2、3)入队" << std::endl;
element = 1;
Queue.push(element);
element = 2;
Queue.push(element);
element = 3;
Queue.push(element);
std::cout << "队列的长度是: " << Queue.size() << std::endl;
Queue.printqueue();
element = Queue.front();
Queue.pop();
std::cout << "出队的元素值为: " << element << std::endl;
element = Queue.front();
Queue.pop();
std::cout << "出队的元素值为: " << element << std::endl;
std::cout << "队列的长度是: " << Queue.size() << std::endl;
Queue.printqueue();
std::cout << "元素(11、12、13、14、15)入队." << std::endl;
element = 11;
Queue.push(element);
element = 12;
Queue.push(element);
element = 13;
Queue.push(element);
element = 14;
Queue.push(element);
element = 15;
Queue.push(element);
std::cout << "队列的长度是: " << Queue.size() << std::endl;
Queue.printqueue();
return 0;
}
c++
// shared_memory_cirucularqueue.cpp,本程序演示基于共享内存的循环队列。
#include "public.h"
int main()
{
using ElemType = int;
// 初始化共享内存。
int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);
if (shmid == -1)
{
std::cout << "shmget(0x5005) failed." << std::endl;
return -1;
}
// 把共享内存连接到当前进程的地址空间。
squeue<ElemType, 5> *Queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);
if (Queue == (void *)-1)
{
std::cout << "shmat() failed." << std::endl;
return -1;
}
Queue->init(); // 初始化循环队列。
ElemType element; // 创建一个数据元素。
std::cout << "元素(1、2、3)入队。\n";
element = 1;
Queue->push(element);
element = 2;
Queue->push(element);
element = 3;
Queue->push(element);
std::cout << "队列的长度是: " << Queue->size() << std::endl;
Queue->printqueue();
element = Queue->front();
Queue->pop();
std::cout << "出队的元素值为: " << element << std::endl;
element = Queue->front();
Queue->pop();
std::cout << "出队的元素值为: " << element << std::endl;
std::cout << "队列的长度是: " << Queue->size() << std::endl;
Queue->printqueue();
std::cout << "元素(11、12、13、14、15)入队." << std::endl;
element = 11;
Queue->push(element);
element = 12;
Queue->push(element);
element = 13;
Queue->push(element);
element = 14;
Queue->push(element);
element = 15;
Queue->push(element);
std::cout << "队列的长度是: " << Queue->size() << std::endl;
Queue->printqueue();
shmdt(Queue); // 把共享内存从当前进程中分离。
return 0;
}
c++
// shared_memory_lock.cpp,本程序演示用信号量给共享内存加锁。
#include "public.h"
struct PersonInfo
{ // 人员信息结构体。
int id; // 编号。
char name[32]; // 姓名。
};
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "using: ./shared_memory_lock id name" << std::endl;
return -1;
}
// 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。
int shmid = shmget(0x5005, sizeof(PersonInfo), 0640 | IPC_CREAT);
if (shmid == -1)
{
std::cout << "shmget(0x5005) failed." << std::endl;
return -1;
}
std::cout << "shmid = " << shmid << std::endl;
// 第2步:把共享内存连接到当前进程的地址空间。
PersonInfo *ptr = (PersonInfo *)shmat(shmid, 0, 0);
if (ptr == (void *)-1)
{
std::cout << "shmat() failed." << std::endl;
return -1;
}
// 创建、初始化二元信号量。
csemp mutex;
if (!mutex.init(0x5005))
{
std::cout << "mutex.init(0x5005) failed." << std::endl;
;
return -1;
}
std::cout << "申请加锁..." << std::endl;
mutex.wait(); // 申请加锁。
std::cout << "申请加锁成功." << std::endl;
// 第3步:使用共享内存,对共享内存进行读/写。
std::cout << "原值: id = " << ptr->id << ", name = " << ptr->name << std::endl; // 显示共享内存中的原值。
ptr->id = atoi(argv[1]); // 对人员信息结构体的id成员赋值。
strcpy(ptr->name, argv[2]); // 对人员信息结构体的name成员赋值。
std::cout << "新值: id = " << ptr->id << ", name = " << ptr->name << std::endl; // 显示共享内存中的当前值。
sleep(10);
mutex.post(); // 解锁。
std::cout << "解锁." << std::endl;
// 查看信号量:ipcs -s // 删除信号量:ipcrm sem 信号量id
// 查看共享内存:ipcs -m // 删除共享内存:ipcrm -m 共享内存id
// 第4步:把共享内存从当前进程中分离。
shmdt(ptr);
// 第5步:删除共享内存。
// if (shmctl(shmid,IPC_RMID,0) == -1)
//{
// std::cout << "shmctl failed"; << std::endl; return -1;
//}
}
c++
#include "public.h" // 生产者 producer.cpp
int main()
{
struct Person
{ // 生产队列的数据元素是人员信息结构体。
int id;
char name[31];
};
using ElemType = Person;
// 初始化共享内存。
int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);
if (shmid == -1)
{
std::cout << "shmget(0x5005) failed." << std::endl;
return -1;
}
// 把共享内存连接到当前进程的地址空间。
squeue<ElemType, 5> *queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);
if (queue == (void *)-1)
{
std::cout << "shmat() failed." << std::endl;
return -1;
}
queue->init(); // 初始化循环队列。
ElemType element; // 创建一个数据元素。
csemp mutex;
mutex.init(0x5001); // 用于给共享内存加锁。
csemp cond;
cond.init(0x5002, 0, 0); // 信号量的值用于表示队列中数据元素的个数。
mutex.wait(); // 加锁。
// 生产3个数据。
element.id = 3;
strncpy(element.name, "Tom", sizeof(element.name));
queue->push(element);
element.id = 7;
strncpy(element.name, "Tomy", sizeof(element.name));
queue->push(element);
element.id = 8;
strncpy(element.name, "Tony", sizeof(element.name));
queue->push(element);
mutex.post(); // 解锁。
cond.post(3); // 实参是3,表示生产了3个数据。
shmdt(queue); // 把共享内存从当前进程中分离。
return 0;
}
c++
#include "public.h" // 消费者 consumer.cpp
int main()
{
struct Person
{ // 循环队列的数据元素是人员信息结构体。
int id;
char name[31];
};
using ElemType = Person;
// 初始化共享内存。
int shmid = shmget(0x5005, sizeof(squeue<ElemType, 5>), 0640 | IPC_CREAT);
if (shmid == -1)
{
std::cout << "shmget(0x5005) failed." << std::endl;
return -1;
}
// 把共享内存连接到当前进程的地址空间。
squeue<ElemType, 5> *queue = (squeue<ElemType, 5> *)shmat(shmid, 0, 0);
if (queue == (void *)-1)
{
std::cout << "shmat() failed." << std::endl;
return -1;
}
queue->init(); // 初始化循环队列。
ElemType element; // 创建一个数据元素。
csemp mutex;
mutex.init(0x5001); // 用于给共享内存加锁。
csemp cond;
cond.init(0x5002, 0, 0); // 信号量的值用于表示队列中数据元素的个数。
while (true)
{
mutex.wait(); // 加锁。
while (queue->empty())
{ // 如果队列空,进入循环,否则直接处理数据。必须用循环,不能用if
mutex.post(); // 解锁。
cond.wait(); // 等待生产者的唤醒信号。
mutex.wait(); // 加锁。
}
// 数据元素出队。
element = queue->front();
queue->pop();
mutex.post(); // 解锁。
// 处理出队的数据(把数据消费掉)。
std::cout << "id = " << element.id << ", name = " << element.name << std::endl;
usleep(100); // 假设处理数据需要时间,方便演示。
}
shmdt(queue); // 把共享内存从当前进程中分离。
return 0;
}
17.第一个网络通讯程序
1.网络通讯的流程
- 服务器端流程:
- 创建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.示例
-
客户端
c++#include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { if (argc != 3) { std::cout << "using: ./socketclient <server_ip> <server_port>" << std::endl << "example: ./ socketclient 192.168.101.139 5005" << std::endl; return -1; } // 创建客户端套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket failed."); return -1; } // 获取服务器地址 struct hostent *server_info = gethostbyname(argv[1]); if (server_info == nullptr) { std::cout << "Error: Failed to get server info." << std::endl; close(sockfd); return -1; } // 构建服务器地址结构 struct sockaddr_in server_address; memset(&server_address, 0, sizeof(server_address)); server_address.sin_family = AF_INET; memcpy(&server_address.sin_addr, server_info->h_addr, server_info->h_length); server_address.sin_port = htons(atoi(argv[2])); // 使用 atoi() 将字符串端口号转换为整数端口号 // 连接服务器 if (connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) { perror("connect failed."); close(sockfd); return -1; } // 发送和接收数据 char buffer[1024]; for (int i = 0; i < 3; ++i) { // 发送请求报文 sprintf(buffer, "Request #%d from client.", i + 1); ssize_t sent_bytes = send(sockfd, buffer, strlen(buffer), 0); if (sent_bytes <= 0) { perror("send failed."); break; } std::cout << "sent: " << buffer << std::endl; // 接收服务器响应报文 memset(buffer, 0, sizeof(buffer)); ssize_t recv_bytes = recv(sockfd, buffer, sizeof(buffer), 0); if (recv_bytes <= 0) { std::cout << "recv_bytes = " << recv_bytes << std::endl; break; } std::cout << "received: " << buffer << std::endl; sleep(1); // 等待1秒 } // 关闭套接字 close(sockfd); return 0; }
-
服务端:
c++#include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { if (argc != 2) { std::cout << "using: ./socketserver <port_number>" << std::endl; std::cout << "example: ./socketserver 5005." << std::endl; std::cout << "note: The firewall on the Linux system running the server program must open port 5005." << std::endl; std::cout << "if it is a cloud server, access policies on the cloud platform must also be opened." << std::endl; return -1; } // 创建服务端的socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { perror("socket failed."); return -1; } // 将服务端用于通信的IP和端口绑定到socket上 struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(atoi(argv[1])); // 使用 atoi() 将字符串端口号转换为整数端口号 if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) { perror("bind failed."); close(listenfd); return -1; } // 将socket设置为可连接(监听)的状态 if (listen(listenfd, 5) != 0) { perror("listen failed."); close(listenfd); return -1; } // 受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待 int clientfd = accept(listenfd, 0, 0); if (clientfd == -1) { perror("accept failed."); close(listenfd); return -1; } std::cout << "client connected." << std::endl; // 与客户端通信,接收客户端发过来的报文后,回复ok char buffer[1024]; while (true) { int iret; memset(buffer, 0, sizeof(buffer)); // 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待 // 如果客户端已断开连接,recv()函数将返回0 if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0) { std::cout << "iret = " << iret << std::endl; break; } std::cout << "received: " << buffer << std::endl; strcpy(buffer, "ok"); // 生成回应报文内容 // 向客户端发送回应报文 if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0) { perror("send failed."); break; } std::cout << "sent: " << buffer << std::endl; } // 关闭socket,释放资源 close(listenfd); // 关闭服务端用于监听的socket close(clientfd); // 关闭客户端连上来的socket return 0; }
18.基于Linux的文件操作
Linux底层文件的操作-创建文件并写入数据
c++
// filecw.cpp,本程序演示了Linux底层文件的操作-创建文件并写入数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd; // 文件描述符
// 打开文件,如果创建后的文件没有权限,可以手工授权 chmod 777 data.txt。
fd = open("data.txt", O_CREAT | O_RDWR | O_TRUNC, 0666); // 添加文件权限参数0666
if (fd == -1)
{
perror("open data.txt failed.");
return -1;
}
printf("file descriptor fd = %d\n", fd);
char buffer[1024];
strcpy(buffer, "This is a sample text.\n");
if (write(fd, buffer, strlen(buffer)) == -1)
{ // 把数据写入文件。
perror("write failed.");
return -1;
}
close(fd); // 关闭文件。
return 0; // 添加返回值,表示程序执行成功
}
Linux底层文件的操作-读取文件
c++
// fileread.cpp,本程序演示了Linux底层文件的操作-读取文件。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd; // 定义一个文件描述符/文件句柄。
fd = open("data.txt", O_RDONLY); // 打开文件。
if (fd == -1)
{
perror("open data.txt failed.");
return -1;
}
printf("文件描述符: fd = %d\n", fd);
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
if (read(fd, buffer, sizeof(buffer)) == -1) // 从文件中读取数据。
{
perror("write failed.");
return -1;
}
printf("%s", buffer);
close(fd); // 关闭文件。
}
19.socket()函数详解
1.什么是协议
- 人与人沟通的方式有很多种:书信、电话、QQ、微信。如果两个人想沟通,必须先选择一种沟通的方式,如果一方使用电话,另一方也应该使用电话,而不是书信。
- 协议是网络通讯的规则,是约定。
2.创建socket
-
包含头文件:
c++#include <sys/types.h> #include <sys/socket.h>
-
函数声明:
c++/* 在域 DOMAIN 中创建一个type类型的套接字, 使用协议 PROTOCOL. 如果 PROTOCOL 为 0, 则自动选择一个. 返回新套接字的文件描述符, 或-1表示错误. */ extern int socket (int __domain, int __type, int __protocol) __THROW;
-
成功返回一个有效的
socket
,失败返回-1
,errno
被设置。 -
全部网络编程的函数,失败时基本上都是返回
-1
,errno
被设置,只要参数没填错,基本上不会失败。 -
注意:单个进程中创建的socket数量与受系统参数
open files
的限制。-
使用以下命令查看:
bashulimit -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()
函数使用实例:c++socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建tcp的sock socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); // 创建udp的sock
3.TCP和UDP
1.TCP和UDP的区别
TCP
TCP
面向连接,通过三次握手建立连接,四次挥手断开连接;TCP
是可靠的通信方式,通过超时重传、数据校验等方式来确保数据无差错,不丢失,不重复,并且按序到达;TCP
把数据当成字节流
,当网络出现波动时,连接可能出现响应延迟的问题;TCP
只支持点对点通信;TCP
报文的首部较大,为20
字节;TCP
是全双工的可靠信道。
UDP
UDP
是无连接的,即发送数据之前不需要建立连接,这种方式为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语言提供了四个库函数,用于在主机字节序和网络字节序之间转换:
-
包含头文件:
c++#include <apra/inet.h>
-
函数声明:
c++/* 在主机和网络之间进行字节顺序转换的函数. 请注意这些函数通常使用 `unsigned long int' 或 `unsigned short int' 值作为参数并返回它们. 但 这是一个目光短浅的决定,因为在不同的系统上类型不同 可能有不同的表示 但值总是相同的. */ extern uint32_t ntohl (uint32_t __netlong) __THROW __attribute__ ((__const__)); extern uint16_t ntohs (uint16_t __netshort) __THROW __attribute__ ((__const__)); extern uint32_t htonl (uint32_t __hostlong) __THROW __attribute__ ((__const__)); extern uint16_t htons (uint16_t __hostshort) __THROW __attribute__ ((__const__))
-
函数命名拆解:
h
:host(主机);to
:转换;n
:network(网络);s
:short(2字节,16位的整数);l
:long(4字节,32位的整数)。
-
3.IP地址和通讯端口
-
在计算机中,
IPv4
的地址用4
字节的整数存放,通讯端口用2
字节的整数(0-65535)存放。 -
例如:192.168.190.134 3232284294 255.255.255.255
192 168 190 134
大端:11000000 10101000 10111110 10000110
小段:10000110 10111110 10101000 11000000
4.如何处理大小端
- 在网络编程中,数据收发的时候有自动转换机制,不需要手动转换,只有向
sockaddr_in
结体成员变量填充数据时,才需要考虑字节序的问题。
21.网络通讯的内部数据结构体
1.sockaddr结构体
-
存放协议族、端口和地址信息,客户端和
connect()
函数和服务端的bind()
函数需要这个结构体。c++typedef unsigned short sa_family_t; #define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family /* 描述通用套接字地址的结构. */ struct sockaddr { __SOCKADDR_COMMON (sa_); /* 常用数据:地址族和长度. */ char sa_data[14]; /* 地址数据. */ };
2.sockaddr_in结构体
-
sockaddr
结构体是为了统一地址结构的表示方法,统一接口函数,但是,操作不方便,所以定义了等价的sockaddr_in
结构体,它的大小与sockaddr
相同,可以强制转换成sockaddr
。c++typedef unsigned short sa_family_t; #define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family /* 网络地址. */ typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; }; #define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int)) typedef uint16_t in_port_t; /* 描述网络套接字地址的结构. */ struct sockaddr_in { __SOCKADDR_COMMON (sin_); in_port_t sin_port; /* 端口号. */ struct in_addr sin_addr; /* 网络地址. */ /* 填充到 `struct sockaddr' 的大小. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; };
3.gethostbyname()函数
-
根据
域名/主机名/字符串IP
获取大端序IP
,用于网络通讯的客户端程序中。 -
包含头文件:
c++#include <netdb.h>
-
函数声明:
c++/* 单个主机的数据库条目描述. */ struct hostent { char *h_name; /* 主机正式名. */ char **h_aliases; /* 别名列表. */ int h_addrtype; /* 主机地址类型. */ int h_length; /* 地址长度. */ char **h_addr_list; /* 来自名称服务器的地址列表. */ h_addr h_addr_list[0] /* 地址, 向后兼容.*/ }; /* 从主机数据库返回带有 NAME 的主机条目. 这个函数是一个可能的消去点,因此不是标记为__THROW. */ extern struct hostent *gethostbyname (const char *__name);
-
转换后,用以下代码把大端序的地址复制到
sockaddr_in
结构体的sin_addr
成员结构中。c++memcpy(&sockaddr_in.sin_addr, hostent->h_addr, hostent->h_length);
4.字符串IP与大端序IP的转换
-
C语言提供了几个库函数,用于字符串格式的IP和大端序IP的互相转换,用于网络通讯的服务端程序中。
-
包含头文件:
c++#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>
-
函数声明:
c++typedef unsigned int in_addr_t; /* 转换网络主机地址从数字和点符号在 CP 转换成网络字节序的二进制数据. */ extern in_addr_t inet_addr (const char *__cp) __THROW; /* 转换网络主机地址从数字和点符号在 CP 转换成二进制数据,并将结果存储在 INP 结构中. */ extern int inet_aton (const char *__cp, struct in_addr *__inp) __THROW; /* 将in中的Internet号码转换为ASCII表示. 返回值指针是否指向包含字符串的内部数组. */ extern char *inet_ntoa (struct in_addr __in) __THROW;
5.示例
-
基于TCP协议的客户端通信
c++// 本程序演示了基于TCP协议的客户端通信 #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { if (argc != 3) { std::cout << "using: ./socket_client <服务端的IP> <服务端的端口>" << std::endl << "example: ./socket_client 192.168.101.138 5005" << std::endl; return -1; } // 第1步:创建客户端的socket。 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket failed."); return -1; } // 第2步:向服务器发起连接请求。 struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET。 servaddr.sin_port = htons(atoi(argv[2])); // ②指定服务端的通信端口。 struct hostent *hostent; // 用于存放服务端IP地址(大端序)的结构体的指针。 if ((hostent = gethostbyname(argv[1])) == nullptr) // 把域名/主机名/字符串格式的IP转换成结构体。 { std::cout << "gethostbyname failed." << std::endl; close(sockfd); return -1; } memcpy(&servaddr.sin_addr, hostent->h_addr, hostent->h_length); // ③指定服务端的IP(大端序)。 // 向服务端发起连接请求。 if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { perror("connect failed."); close(sockfd); return -1; } // 第3步:与服务端通讯,客户端发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。 char buffer[1024]; for (int i = 0; i < 10; i++) // 循环10次,与服务端进行10次通讯。 { int iret; memset(buffer, 0, sizeof(buffer)); sprintf(buffer, "这是第 %d 个数据包,编号: %03d.", i + 1, i + 1); // 生成请求报文内容。 // 向服务端发送请求报文。 if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0) { perror("send failed."); break; } std::cout << "发送: " << buffer << std::endl; memset(buffer, 0, sizeof(buffer)); // 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。 if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0) { std::cout << "iret = " << iret << std::endl; break; } std::cout << "接收: " << buffer << std::endl; sleep(1); // 模拟处理时间 } // 第4步:关闭socket,释放资源。 close(sockfd); return 0; }
-
基于TCP协议的服务端通信
c++// 本程序演示了基于TCP协议的服务端通信 #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { if (argc != 2) { std::cout << "using: ./socket_server <通讯端口>" << std::endl << "example: ./socket_server 5005" << std::endl; return -1; } // 第1步:创建服务端的socket。 int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { perror("socket failed."); return -1; } // 第2步:把服务端用于通信的IP和端口绑定到socket上。 struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET。 servaddr.sin_port = htons(std::atoi(argv[1])); // ②指定服务端的通信端口。 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③服务端任意网卡的IP都可以用于通讯。 // 绑定服务端的IP和端口。 if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { perror("bind failed."); close(listenfd); return -1; } // 第3步:把socket设置为可连接(监听)的状态。 if (listen(listenfd, 5) == -1) { perror("listen failed."); close(listenfd); return -1; } // 第4步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。 int clientfd = accept(listenfd, nullptr, nullptr); if (clientfd == -1) { perror("accept failed."); close(listenfd); return -1; } std::cout << "客户端已连接." << std::endl; // 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。 char buffer[1024]; while (true) { int iret; memset(buffer, 0, sizeof(buffer)); // 接收客户端的请求报文,如果客户端没有发送请求报文,recv()函数将阻塞等待。 // 如果客户端已断开连接,recv()函数将返回0。 if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0) { std::cout << "iret = " << iret << std::endl; break; } std::cout << "接收: " << buffer << std::endl; strcpy(buffer, "ok"); // 生成回应报文内容。 // 向客户端发送回应报文。 if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0) { perror("send failed."); break; } std::cout << "发送: " << buffer << std::endl; } // 第6步:关闭socket,释放资源。 close(listenfd); // 关闭服务端用于监听的socket。 close(clientfd); // 关闭客户端连上来的socket。 return 0; }
22.封装socket
-
封装socket通讯的客户端
c++// tcp_clientcpp - 基于TCP协议的客户端通信. #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> class TCPClient // TCP通讯的客户端类. { private: int client_fd; // 客户端的socket,-1 表示未连接或连接已断开; >= 0 表示有效的socket. std::string ip; // 服务端的IP/域名. unsigned short port; // 通讯端口. public: TCPClient() : client_fd(-1) {} // 向服务端发起连接请求,成功返回true,失败返回false. bool connect(const std::string &in_ip, const unsigned short in_port) { if (client_fd != -1) return false; // 如果socket已连接,直接返回失败. ip = in_ip; port = in_port; // 把服务端的IP和端口保存到成员变量中. // 第1步:创建客户端的socket. if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false; // 第2步:向服务器发起连接请求. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体. memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; // ①协议族,固定填 AF_INET. servaddr.sin_port = htons(port); // ②指定服务端的通信端口. struct hostent *h; // 用于存放服务端IP地址(大端序)的结构体的指针. if ((h = gethostbyname(ip.c_str())) == nullptr) // 把域名/主机名/字符串格式的IP转换成结构体. { ::close(client_fd); client_fd = -1; return false; } memcpy(&servaddr.sin_addr, h->h_addr, h->h_length); // ③指定服务端的IP(大端序). // 向服务端发起连接请求. if (::connect(client_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { ::close(client_fd); client_fd = -1; return false; } return true; } // 向服务端发送报文,成功返回true,失败返回false. bool send(const std::string &buffer) // buffer不要用const char* { if (client_fd == -1) return false; // 如果socket的状态是未连接,直接返回失败. if ((::send(client_fd, buffer.data(), buffer.size(), 0)) <= 0) return false; return true; } // 接收服务端的报文,成功返回true,失败返回false. // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度. bool recv(std::string &buffer, const size_t maxlen) { buffer.clear(); // 清空容器. buffer.resize(maxlen); // 设置容器的大小为maxlen. int readn = ::recv(client_fd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存. if (readn <= 0) { buffer.clear(); return false; } buffer.resize(readn); // 重置buffer的实际大小. return true; } // 断开与服务端的连接. bool close() { if (client_fd == -1) return false; // 如果socket的状态是未连接,直接返回失败. ::close(client_fd); client_fd = -1; return true; } ~TCPClient() { close(); } }; int main(int argc, char *argv[]) { if (argc != 3) { std::cout << "using: ./tcp_client <服务端的IP> <服务端的端口>" << std::endl << "example: ./tcp_client 192.168.101.138 5005" << std::endl; return -1; } TCPClient tcpClient; if (tcpClient.connect(argv[1], std::atoi(argv[2])) == false) // 向服务端发起连接请求. { perror("connect failed."); return -1; } // 第3步:与服务端通讯,客户端发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文. std::string buffer; for (int i = 0; i < 10; i++) // 循环10次,与服务端进行10次通讯. { buffer = "这是第 " + std::to_string(i + 1) + " 个数据包, 编号: " + std::to_string(i + 1) + "."; // 向服务端发送请求报文. if (tcpClient.send(buffer) == false) { perror("send failed."); break; } std::cout << "发送: " << buffer << std::endl; // 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待. if (tcpClient.recv(buffer, 1024) == false) { perror("recv failed."); break; } std::cout << "接收: " << buffer << std::endl; sleep(1); } return 0; }
-
基于TCP协议的服务端通信
c++// tcp_server.cpp - 基于TCP协议的服务端通信. #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> class TCPServer // TCP通讯的服务端类. { private: int listen_fd; // 监听的socket,-1表示未初始化. int client_fd; // 客户端连上来的socket,-1表示客户端未连接. std::string client_ip; // 客户端字符串格式的IP. unsigned short port; // 服务端用于通讯的端口. public: TCPServer() : listen_fd(-1), client_fd(-1) {} // 初始化服务端用于监听的socket. bool initServer(const unsigned short in_port) { // 第1步:创建服务端的socket. if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false; port = in_port; // 第2步:把服务端用于通信的IP和端口绑定到socket上. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体. memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET. servaddr.sin_port = htons(port); // ②指定服务端的通信端口. servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯. // 绑定服务端的IP和端口(为socket分配IP和端口). if (bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { close(listen_fd); listen_fd = -1; return false; } // 第3步:把socket设置为可连接(监听)的状态. if (listen(listen_fd, 5) == -1) { close(listen_fd); listen_fd = -1; return false; } return true; } // 受理客户端的连接(从已连接的客户端中取出一个客户端), // 如果没有已连接的客户端,accept()函数将阻塞等待. bool acceptConnection() { struct sockaddr_in caddr; // 客户端的地址信息. socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小. if ((client_fd = ::accept(listen_fd, (struct sockaddr *)&caddr, &addrlen)) == -1) return false; client_ip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串. return true; } // 获取客户端的IP(字符串格式). const std::string &getClientIP() const { return client_ip; } // 向对端发送报文,成功返回true,失败返回false. bool sendMessage(const std::string &buffer) { if (client_fd == -1) return false; if ((::send(client_fd, buffer.data(), buffer.size(), 0)) <= 0) return false; return true; } // 接收对端的报文,成功返回true,失败返回false. // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度. bool receiveMessage(std::string &buffer, const size_t maxlen) { buffer.clear(); // 清空容器. buffer.resize(maxlen); // 设置容器的大小为maxlen. int readn = ::recv(client_fd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存. if (readn <= 0) { buffer.clear(); return false; } buffer.resize(readn); // 重置buffer的实际大小. return true; } // 关闭监听的socket. bool closeListenSocket() { if (listen_fd == -1) return false; ::close(listen_fd); listen_fd = -1; return true; } // 关闭客户端连上来的socket. bool closeClientSocket() { if (client_fd == -1) return false; ::close(client_fd); client_fd = -1; return true; } ~TCPServer() { closeListenSocket(); closeClientSocket(); } }; int main(int argc, char *argv[]) { if (argc != 2) { std::cout << "using: ./tcp_server <通讯端口>" << std::endl << "example: ./ tcp_server 5005" << std::endl; // 端口大于1024,不与其它的重复. return -1; } TCPServer tcpServer; if (tcpServer.initServer(std::atoi(argv[1])) == false) // 初始化服务端用于监听的socket. { perror("initServer failed"); return -1; } // 受理客户端的连接(从已连接的客户端中取出一个客户端), // 如果没有已连接的客户端,accept()函数将阻塞等待. if (tcpServer.acceptConnection() == false) { perror("acceptConnection failed."); return -1; } std::cout << "客户端已连接( " << tcpServer.getClientIP() << " )." << std::endl; std::string buffer; while (true) { // 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待. if (tcpServer.receiveMessage(buffer, 1024) == false) { perror("receiveMessage failed."); break; } std::cout << "接收: " << buffer << std::endl; buffer = "ok"; if (tcpServer.sendMessage(buffer) == false) // 向对端发送报文. { perror("sendMessage failed."); break; } std::cout << "发送: " << buffer << std::endl; } return 0; }
23.多进程的网络服务端
-
示例:
c++// multiprocess_tcpserver.cpp - 基于TCP协议的服务端通信,支持多客户端连接. #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <signal.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> class ctcpserver // TCP通讯的服务端类. { private: int m_listenfd; // 监听的socket,-1表示未初始化. int m_clientfd; // 客户端连上来的socket,-1表示客户端未连接. std::string m_clientip; // 客户端字符串格式的IP. unsigned short m_port; // 服务端用于通讯的端口. public: ctcpserver() : m_listenfd(-1), m_clientfd(-1) {} // 初始化服务端用于监听的socket. bool initserver(const unsigned short in_port) { // 第1步:创建服务端的socket. if ((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false; m_port = in_port; // 第2步:把服务端用于通信的IP和端口绑定到socket上. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体. memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET. servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口. servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯. // 绑定服务端的IP和端口(为socket分配IP和端口). if (bind(m_listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { close(m_listenfd); m_listenfd = -1; return false; } // 第3步:把socket设置为可连接(监听)的状态. if (listen(m_listenfd, 5) == -1) { close(m_listenfd); m_listenfd = -1; return false; } return true; } // 受理客户端的连接(从已连接的客户端中取出一个客户端), // 如果没有已连接的客户端,accept()函数将阻塞等待. bool accept() { struct sockaddr_in caddr; // 客户端的地址信息. socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小. if ((m_clientfd = ::accept(m_listenfd, (struct sockaddr *)&caddr, &addrlen)) == -1) return false; m_clientip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串. return true; } // 获取客户端的IP(字符串格式). const std::string &clientip() const { return m_clientip; } // 向对端发送报文,成功返回true,失败返回false. bool send(const std::string &buffer) { if (m_clientfd == -1) return false; if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0) return false; return true; } // 接收对端的报文,成功返回true,失败返回false. // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度. bool recv(std::string &buffer, const size_t maxlen) { buffer.clear(); // 清空容器. buffer.resize(maxlen); // 设置容器的大小为maxlen. int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存. if (readn <= 0) { buffer.clear(); return false; } buffer.resize(readn); // 重置buffer的实际大小. return true; } // 关闭监听的socket. bool closelisten() { if (m_listenfd == -1) return false; ::close(m_listenfd); m_listenfd = -1; return true; } // 关闭客户端连上来的socket. bool closeclient() { if (m_clientfd == -1) return false; ::close(m_clientfd); m_clientfd = -1; return true; } ~ctcpserver() { closelisten(); closeclient(); } }; ctcpserver tcpserver; void FatherEXIT(int sig); // 父进程的信号处理函数. void ChildEXIT(int sig); // 子进程的信号处理函数. int main(int argc, char *argv[]) { if (argc != 2) { std::cout << "using: ./muitilprocess_tcpserver 通讯端口" << std::endl << "example: ./muitilprocess_tcpserver 5005" << std::endl; return -1; } // 忽略全部的信号,不希望被打扰.顺便解决了僵尸进程的问题. for (int ii = 1; ii <= 64; ii++) signal(ii, SIG_IGN); // 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程 // 但请不要用 "kill -9 +进程号" 强行终止 signal(SIGTERM, FatherEXIT); signal(SIGINT, FatherEXIT); // SIGTERM 15 SIGINT 2 if (tcpserver.initserver(atoi(argv[1])) == false) // 初始化服务端用于监听的socket. { perror("initserver failed."); return -1; } while (true) { // 受理客户端的连接(从已连接的客户端中取出一个客户端), // 如果没有已连接的客户端,accept()函数将阻塞等待. if (tcpserver.accept() == false) { perror("accept failed."); return -1; } int pid = fork(); if (pid == -1) { perror("fork failed."); return -1; } // 系统资源不足. if (pid > 0) { // 父进程. tcpserver.closeclient(); // 父进程关闭客户端连接的socket. continue; // 父进程返回到循环开始的位置,继续受理客户端的连接. } tcpserver.closelisten(); // 子进程关闭监听的socket. // 子进程需要重新设置信号. signal(SIGTERM, ChildEXIT); // 子进程的退出函数与父进程不一样. signal(SIGINT, SIG_IGN); // 子进程不需要捕获SIGINT信号. // 子进程负责与客户端进行通讯. std::cout << "客户端已连接( " << tcpserver.clientip() << " )." << std::endl; std::string buffer; while (true) { // 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待. if (tcpserver.recv(buffer, 1024) == false) { perror("recv()"); break; } std::cout << "接收: " << buffer << std::endl; buffer = "ok"; if (tcpserver.send(buffer) == false) // 向对端发送报文. { perror("send"); break; } std::cout << "发送: " << buffer << std::endl; } return 0; // 子进程一定要退出,否则又会回到accept()函数的位置. } } // 父进程的信号处理函数. void FatherEXIT(int sig) { // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断. signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN); std::cout << "父进程退出,sig = " << sig << std::endl; kill(0, SIGTERM); // 向全部的子进程发送15的信号,通知它们退出. // 在这里增加释放资源的代码(全局的资源). tcpserver.closelisten(); // 父进程关闭监听的socket. exit(0); } // 子进程的信号处理函数. void ChildEXIT(int sig) { // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断. signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN); std::cout << "子进程: " << getpid() << "退出,sig = " << sig << std::endl; // 在这里增加释放资源的代码(只释放子进程的资源). tcpserver.closeclient(); // 子进程关闭客户端连上来的socket. exit(0); }
24.实现文件传输功能
-
实现文件传输的客户端
c++#include <iostream> #include <fstream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> class ctcpclient // TCP通讯的客户端类. { private: int m_clientfd; // 客户端的socket,-1表示未连接或连接已断开;>=0表示有效的socket. std::string m_ip; // 服务端的IP/域名. unsigned short m_port; // 通讯端口. public: ctcpclient() : m_clientfd(-1) {} // 向服务端发起连接请求,成功返回true,失败返回false. bool connect(const std::string &in_ip, const unsigned short in_port) { if (m_clientfd != -1) return false; // 如果socket已连接,直接返回失败. m_ip = in_ip; m_port = in_port; // 把服务端的IP和端口保存到成员变量中. // 第1步:创建客户端的socket. if ((m_clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false; // 第2步:向服务器发起连接请求. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体. memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET. servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口. struct hostent *h; // 用于存放服务端IP地址(大端序)的结构体的指针. if ((h = gethostbyname(m_ip.c_str())) == nullptr) // 把域名/主机名/字符串格式的IP转换成结构体. { ::close(m_clientfd); m_clientfd = -1; return false; } memcpy(&servaddr.sin_addr, h->h_addr, h->h_length); // ③指定服务端的IP(大端序). // 向服务端发起连接请求. if (::connect(m_clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { ::close(m_clientfd); m_clientfd = -1; return false; } return true; } // 向服务端发送报文(字符串),成功返回true,失败返回false. bool send(const std::string &buffer) // buffer不要用const char * { if (m_clientfd == -1) return false; // 如果socket的状态是未连接,直接返回失败. if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0) return false; return true; } // 向服务端发送报文(二进制数据),成功返回true,失败返回false. bool send(void *buffer, const size_t size) { if (m_clientfd == -1) return false; // 如果socket的状态是未连接,直接返回失败. if ((::send(m_clientfd, buffer, size, 0)) <= 0) return false; return true; } // 接收服务端的报文,成功返回true,失败返回false. // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度. bool recv(std::string &buffer, const size_t maxlen) { buffer.clear(); // 清空容器. buffer.resize(maxlen); // 设置容器的大小为maxlen. int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存. if (readn <= 0) { buffer.clear(); return false; } buffer.resize(readn); // 重置buffer的实际大小. return true; } // 断开与服务端的连接. bool close() { if (m_clientfd == -1) return false; // 如果socket的状态是未连接,直接返回失败. ::close(m_clientfd); m_clientfd = -1; return true; } // 向服务端发送文件内容. bool sendfile(const std::string &filename, const size_t filesize) { // 以二进制的方式打开文件. std::ifstream fin(filename, std::ios::binary); if (fin.is_open() == false) { std::cout << "打开文件: " << filename << " 失败." << std::endl; return false; } int onread = 0; // 每次调用fin.read()时打算读取的字节数. int totalbytes = 0; // 从文件中已读取的字节总数. char buffer[4096]; // 存放读取数据的buffer. while (true) { memset(buffer, 0, sizeof(buffer)); // 计算本次应该读取的字节数,如果剩余的数据超过4096字节,就读4096字节. if (filesize - totalbytes > 4096) onread = 4096; else onread = filesize - totalbytes; // 从文件中读取数据. fin.read(buffer, onread); // 把读取到的数据发送给对端. if (send(buffer, onread) == false) return false; // 计算文件已读取的字节总数,如果文件已读完,跳出循环. totalbytes += onread; if (totalbytes == filesize) break; } return true; } ~ctcpclient() { close(); } }; int main(int argc, char *argv[]) { if (argc != 5) { std::cout << "using: ./sendfile_tcpclient 服务端的IP 服务端的端口 文件名 文件大小" << std::endl; std::cout << "example: ./sendfile_tcpclient 192.168.101.138 5005 test.txt 2424" << std::endl << std::endl; return -1; } ctcpclient tcpclient; if (tcpclient.connect(argv[1], atoi(argv[2])) == false) // 向服务端发起连接请求. { perror("connect failed."); return -1; } // 以下是发送文件的流程. // 1)把待传输文件名和文件的大小告诉服务端. // 定义文件信息的结构体. struct st_fileinfo { char filename[256]; // 文件名. int filesize; // 文件大小. } fileinfo; memset(&fileinfo, 0, sizeof(fileinfo)); strncpy(fileinfo.filename, argv[3], sizeof(fileinfo.filename) - 1); // 文件名. fileinfo.filesize = atoi(argv[4]); // 文件大小. // 把文件信息的结构体发送给服务端. if (tcpclient.send(&fileinfo, sizeof(fileinfo)) == false) { perror("send failed."); return -1; } std::cout << "发送文件信息的结构体: " << fileinfo.filename << " ( " << fileinfo.filesize << " )." << std::endl; // 2)等待服务端的确认报文(文件名和文件的大小的确认). std::string buffer; if (tcpclient.recv(buffer, 2) == false) { perror("recv failed."); return -1; } if (buffer != "ok") { std::cout << "服务端没有回复ok." << std::endl; return -1; } // 3)发送文件内容. if (tcpclient.sendfile(fileinfo.filename, fileinfo.filesize) == false) { perror("sendfile failed."); return -1; } // 4)等待服务端的确认报文(服务端已接收完文件). if (tcpclient.recv(buffer, 2) == false) { perror("recv failed."); return -1; } if (buffer != "ok") { std::cout << "发送文件内容失败." << std::endl; return -1; } std::cout << "发送文件内容成功." << std::endl; return 0; }
-
实现文件传输的服务端
c++#include <iostream> #include <fstream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <signal.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> class ctcpserver // TCP通讯的服务端类. { private: int m_listenfd; // 监听的socket,-1表示未初始化. int m_clientfd; // 客户端连上来的socket,-1表示客户端未连接. std::string m_clientip; // 客户端字符串格式的IP. unsigned short m_port; // 服务端用于通讯的端口. public: ctcpserver() : m_listenfd(-1), m_clientfd(-1) {} // 初始化服务端用于监听的socket. bool initserver(const unsigned short in_port) { // 第1步:创建服务端的socket. if ((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false; m_port = in_port; // 第2步:把服务端用于通信的IP和端口绑定到socket上. struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体. memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET. servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口. servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯. // 绑定服务端的IP和端口(为socket分配IP和端口). if (bind(m_listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { close(m_listenfd); m_listenfd = -1; return false; } // 第3步:把socket设置为可连接(监听)的状态. if (listen(m_listenfd, 5) == -1) { close(m_listenfd); m_listenfd = -1; return false; } return true; } // 受理客户端的连接(从已连接的客户端中取出一个客户端), // 如果没有已连接的客户端,accept()函数将阻塞等待. bool accept() { struct sockaddr_in caddr; // 客户端的地址信息. socklen_t addrlen = sizeof(caddr); // struct sockaddr_in的大小. if ((m_clientfd = ::accept(m_listenfd, (struct sockaddr *)&caddr, &addrlen)) == -1) return false; m_clientip = inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串. return true; } // 获取客户端的IP(字符串格式). const std::string &clientip() const { return m_clientip; } // 向对端发送报文,成功返回true,失败返回false. bool send(const std::string &buffer) { if (m_clientfd == -1) return false; if ((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0) return false; return true; } // 接收对端的报文(字符串),成功返回true,失败返回false. // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度. bool recv(std::string &buffer, const size_t maxlen) { buffer.clear(); // 清空容器. buffer.resize(maxlen); // 设置容器的大小为maxlen. int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存. if (readn <= 0) { buffer.clear(); return false; } buffer.resize(readn); // 重置buffer的实际大小. return true; } // 接收客户端的报文(二进制数据),成功返回true,失败返回false. // buffer-存放接收到的报文的内容,size-本次接收报文的最大长度. bool recv(void *buffer, const size_t size) { if (::recv(m_clientfd, buffer, size, 0) <= 0) return false; return true; } // 关闭监听的socket. bool closelisten() { if (m_listenfd == -1) return false; ::close(m_listenfd); m_listenfd = -1; return true; } // 关闭客户端连上来的socket. bool closeclient() { if (m_clientfd == -1) return false; ::close(m_clientfd); m_clientfd = -1; return true; } // 接收文件内容. bool recvfile(const std::string &filename, const size_t filesize) { std::ofstream fout; fout.open(filename, std::ios::binary); if (fout.is_open() == false) { std::cout << "Failed to open file: " << filename << "." << std::endl; return false; } int totalbytes = 0; // 已接收文件的总字节数. int onread = 0; // 本次打算接收的字节数. char buffer[4096]; // 接收文件内容的缓冲区. while (true) { // 计算本次应该接收的字节数. if (filesize - totalbytes > 4096) onread = 4096; else onread = filesize - totalbytes; // 接收文件内容. if (recv(buffer, onread) == false) return false; // 把接收到的内容写入文件. fout.write(buffer, onread); // 计算已接收文件的总字节数,如果文件接收完,跳出循环. totalbytes = totalbytes + onread; if (totalbytes == filesize) break; } return true; } ~ctcpserver() { closelisten(); closeclient(); } }; ctcpserver tcpserver; void FatherEXIT(int sig); // 父进程的信号处理函数. void ChildEXIT(int sig); // 子进程的信号处理函数. int main(int argc, char *argv[]) { if (argc != 3) { std::cout << "using: ./sendfile_tcpserver 通讯端口 文件存放的目录" << std::endl; std::cout << "example: ./sendfile_tcpserver 5005 /tmp" << std::endl << std::endl; return -1; } // 忽略全部的信号,不希望被打扰.顺便解决了僵尸进程的问题. for (int ii = 1; ii <= 64; ii++) signal(ii, SIG_IGN); // 设置信号,在shell状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程 // 但请不要用 "kill -9 +进程号" 强行终止 signal(SIGTERM, FatherEXIT); signal(SIGINT, FatherEXIT); // SIGTERM 15 SIGINT 2 if (tcpserver.initserver(atoi(argv[1])) == false) // 初始化服务端用于监听的socket. { perror("initserver failed."); return -1; } while (true) { // 受理客户端的连接(从已连接的客户端中取出一个客户端), // 如果没有已连接的客户端,accept()函数将阻塞等待. if (tcpserver.accept() == false) { perror("accept failed."); return -1; } int pid = fork(); if (pid == -1) { perror("fork failed."); return -1; } // 系统资源不足. if (pid > 0) { // 父进程. tcpserver.closeclient(); // 父进程关闭客户端连接的socket. continue; // 父进程返回到循环开始的位置,继续受理客户端的连接. } tcpserver.closelisten(); // 子进程关闭监听的socket. // 子进程需要重新设置信号. signal(SIGTERM, ChildEXIT); // 子进程的退出函数与父进程不一样. signal(SIGINT, SIG_IGN); // 子进程不需要捕获SIGINT信号. // 子进程负责与客户端进行通讯. std::cout << "Client connected: ( " << tcpserver.clientip() << " )." << std::endl; // 以下是接收文件的流程. // 1)接收文件名和文件大小信息. // 定义文件信息的结构体. struct st_fileinfo { char filename[256]; // 文件名. int filesize; // 文件大小. } fileinfo; memset(&fileinfo, 0, sizeof(fileinfo)); // 用结构体存放接收报文的内容. if (tcpserver.recv(&fileinfo, sizeof(fileinfo)) == false) { perror("recv()"); return -1; } std::cout << "File info: " << fileinfo.filename << " ( " << fileinfo.filesize << " )." << std::endl; // 2)给客户端回复确认报文,表示客户端可以发送文件了. if (tcpserver.send("ok") == false) { perror("send failed."); break; } // 3)接收文件内容. if (tcpserver.recvfile(std::string(argv[2]) + "/" + fileinfo.filename, fileinfo.filesize) == false) { std::cout << "Failed to receive file content." << std::endl; return -1; } std::cout << "File content received successfully." << std::endl; // 4)给客户端回复确认报文,表示文件已接收成功. tcpserver.send("ok"); return 0; // 子进程一定要退出,否则又会回到accept()函数的位置. } } // 父进程的信号处理函数. void FatherEXIT(int sig) { // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断. signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN); std::cout << "Parent process exiting, sig = " << sig << std::endl; kill(0, SIGTERM); // 向全部的子进程发送15的信号,通知它们退出. // 在这里增加释放资源的代码(全局的资源). tcpserver.closelisten(); // 父进程关闭监听的socket. exit(0); } // 子进程的信号处理函数. void ChildEXIT(int sig) { // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断. signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN); std::cout << "Child process: " << getpid() << " exiting, sig = " << sig << std::endl; // 在这里增加释放资源的代码(只释放子进程的资源). tcpserver.closeclient(); // 子进程关闭客户端连上来的socket. exit(0); }
25.三次握手与四次挥手
- TCP是面向连接的、可靠的协议,建立TCP连接需要三次对话(三次握手),拆除TCP连接需要四次对话(四次握/挥手)。
1.三次握手
- 服务端调用
listen()
函数后进入监听(等待连接)状态,这时候,客户端就可以调用connect()
函数发起TCP连接请求,connect()
函数会触发三次握手,三次握手完成后,客户端和服务端将建立一个双向的传输通道。 - 情景类似:
- 客户端对服务端说:我可以给你发送数据吗?
- 服务端回复: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()
之前)c++int opt = 1; setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
26.TCP缓存
-
系统为每个
socket
创建了发送缓冲区和接收缓冲区,应用程序调用send()/write()
函数发送数据的时候,内核把数据从应用进程拷贝socket
的发送缓冲区中;应用程序调用recv()/read()
函数接收数据的时候,内核把数据从socket
的接收缓冲区拷贝应用进程中。 -
发送数据即把数据放入发送缓冲区中,接收数据即从接收缓冲区中取数据。
-
查看socket缓存的大小:
c++int bufsize = 0; socklen_t optlen = sizeof(bufsize); getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, &optlen); // 获取发送缓冲区的大小。 cout << "send bufsize = " << bufsize << endl; getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, &optlen); // 获取接收缓冲区的大小。 cout << "recv bufsize = " << bufsize << endl;
-
问题:
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
算法。c++#include <netinet/tcp.h> // 注意,要包含这个头文件。 int opt = 1; setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
-
对时效要求很高的系统,例如联机游戏、证券交易,一般会禁用
Nagle
算法。
-
-
27.I/O多路复用
-
IO多路复用是一种用于管理多个IO操作的技术,它允许一个单独的进程或线程同时监视多个IO流(如套接字、文件描述符等),并且在其中任何一个IO流准备好进行读取、写入或连接时立即进行相应的操作,而不需要阻塞其他流。这种技术提高了系统的性能和效率,尤其适用于需要处理大量并发连接的网络服务器应用。
-
基本概念:
- 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
对象。
-
-
包含头文件:
c++#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h>
-
函数声明:
c++/* `fd_set' 的访问宏. */ #define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp) #define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp) #define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp) #define FD_ZERO(fdsetp) __FD_ZERO (fdsetp) #define __FD_SET(d, set) \ ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d))) #define __FD_CLR(d, set) \ ((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d))) #define __FD_ISSET(d, set) \ ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0) # define __FD_ZERO(fdsp) \ do { \ int __d0, __d1; \ __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS \ : "=c" (__d0), "=D" (__d1) \ : "a" (0), "0" (sizeof (fd_set) \ / sizeof (__fd_mask)), \ "1" (&__FDS_BITS (fdsp)[0]) \ : "memory"); \ } while (0)
-
参数说明:
FD_SET(fd, fdsetp)
:在参数fdsetp
指向的变量中注册文件描述符fd
的信息。FD_CLR(fd, fdsetp)
:从参数fdsetp
指向的变量中清除文件描述符fd
的信息。FD_ISSET(fd, fdsetp)
:若参数fdsetp
指向的变量中包含文件描述符fd
的信息,则返回"真"。FD_ZERO(fdsetp)
:将fdsetp
变量的所有位初始化为0
。
-
select()
函数:c++/* 检查 READFDS 中的第一个 NFDS 描述符(如果不是NULL)是否为读 在WRITEFDS(如果不是NULL)中表示写准备情况, 在EXCEPTFDS中表示写准备情况 (如果不是NULL)用于特殊情况. 如果 TIMEOUT 不为 NULL, 则 在等待其中指定的时间间隔后超时. 返回就绪的文件描述符的数量, 或 -1 表示错误. 这个函数是一个消去点,因此没有标记 __THROW. */ extern int select (int __nfds, fd_set *__restrict __readfds, fd_set *__restrict __writefds, fd_set *__restrict __exceptfds, struct timeval *__restrict __timeout);
- 成功时返回大于
0
的值,失败时返回-1
。
- 成功时返回大于
-
参数说明:
__nfds
:监视对象文件描述符数量;__readfds
:用于检查可读性;__writefds
:用于检查可写性;__exceptfds
:用于检查带外数据;__timeout
:一个指向timeval
结构体的指针,用于决定select
等待I/O的最长时间,如果为空会一直等待。
-
示例:
-
服务端:
c++#include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/select.h> #define BUF_SIZE 100 void error_handling(const char *message); int main(int argc, char *argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_adr, clnt_adr; socklen_t adr_sz; int str_len, fd_num, i; char buf[BUF_SIZE]; if (argc != 2) { std::cout << "using: " << argv[0] << " <port>" << std::endl; exit(1); } serv_sock = socket(PF_INET, SOCK_STREAM, 0); if (serv_sock == -1) error_handling("socket() error"); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error"); if (listen(serv_sock, 5) == -1) error_handling("listen() error"); fd_set reads, cpy_reads; FD_ZERO(&reads); FD_SET(serv_sock, &reads); int fd_max = serv_sock; while (1) { cpy_reads = reads; struct timeval timeout; timeout.tv_sec = 5; timeout.tv_usec = 5000; if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1) break; if (fd_num == 0) continue; for (i = 0; i < fd_max + 1; i++) { if (FD_ISSET(i, &cpy_reads)) { if (i == serv_sock) { // 连接请求 adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); FD_SET(clnt_sock, &reads); if (fd_max < clnt_sock) fd_max = clnt_sock; std::cout << "connected client: " << clnt_sock << std::endl; } else { // Read message! str_len = read(i, buf, BUF_SIZE); if (str_len == 0) { // Close request! FD_CLR(i, &reads); close(i); std::cout << "closed client: " << i << std::endl; } else { write(i, buf, str_len); // Echo! } } } } } close(serv_sock); return 0; } void error_handling(const char *message) { std::cerr << message << std::endl; exit(1); }
-
客户端:
c++#include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 1024 void error_handling(const char *message); int main(int argc, char *argv[]) { int sock; char message[BUF_SIZE]; int str_len; struct sockaddr_in serv_adr; if (argc != 3) { std::cout << "Usage : " << argv[0] << " <IP> <port>" << std::endl; exit(1); } sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) error_handling("socket() error"); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) error_handling("connect() error!"); else std::cout << "Connected..." << std::endl; while (1) { std::cout << "Input message (Q to quit): " << std::endl; fgets(message, BUF_SIZE, stdin); if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break; write(sock, message, strlen(message)); str_len = read(sock, message, BUF_SIZE - 1); message[str_len] = '\0'; std::cout << "Message from server: " << message << std::endl; } close(sock); return 0; } void error_handling(const char *message) { std::cerr << message << std::endl; exit(1); }
-
理解
select()
函数:- 是否存在套接字接收数据?
- 通过检查可读事件集合(
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_create
epoll_wait
epoll_ctl
-
包含头文件:
c++#include <sys/epoll.h>
-
函数声明:
c++/* 创建 epoll 实例. 返回新实例的 fd. "size" 参数是指定文件数量的提示要与新实例关联的描述符. epoll_create() 返回的 fd 值应该用 close() 关闭. */ extern int epoll_create (int __size) __THROW; // 该函数从2.3.2版本的开始加入的,2.6版开始引入内核Linux最新的内核稳定版本已经到了5.8.14,长期支持版本到了5.4.70,从2.6.8内核开始的Linux,会忽略这个参数,但是必须要大于0,这个是Linux独有的函数 /* 等待 epoll 实例的 "epfd" 事件. 在 "events" 缓冲区中返回的触发事件的数目. 或者是 -1 将出错时 "errno" 变量设置为特定错误代码. "events" 参数是一个缓冲区,将包含触发的事件. "maxevents" 要设置的最大事件数返回( 通常是 "events" 的大小 ). "timeout" 参数指定以毫秒为单位的最大等待时间 (-1 == infinite). 此函数是一个取消点因此没有标记为 __THROW. */ extern int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout); /* 操作epoll实例 "epfd". 成功时返回0, -1表示错误 ( "errno" 变量将包含特殊错误代码) "op" 参数是 EPOLL_CTL_* 上面定义的常量. "fd" 参数是操作. "event" 参数描述调用者感兴趣的事件以及任何相关的用户数据. */ extern int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event) __THROW;
-
epoll_wait
参数说明:__epfd
:表示事件发生监视范围的epol
例程的文件描述符;__events
:保存发生事件的文件描述符集合的结构体地址值;__maxevents
:第二个参数中可以保存的最大事件数目;__timeout
:以1/1000秒
为单位的等待时间,传递-1
时,一直等待直到发生事件。
-
epoll_ctl
参数说明:__epfd
:用于注册监视对象的epoll
例程的文件描述符;__op
:用于指定监视对象的添加、删除或更改等操作;EPOLL_CTL_ADD
EPOLL_CTL_DEL
EPOLL_CTL_MOD
__fd
:需要注册的监视对象文件描述符;__event
:监视对象的事件类型:EPOLLIN
:需要读取数据的情况;EPOLLOUT
:输出缓冲为空,可以立即发送数据的情况;EPOLLPRI
:收到OOB
数据的情况;EPOLLRDHUP
:断开连接或半关闭的情况,这在边缘触发方式下非常有用;EPOLLERR
:发生错误的情况;EPOLLET
:以边缘触发的方式得到事件通知;EPOLLONESHOT
:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl
函数的第二个参数传递;
3.示例
-
服务端:
c++#include <iostream> #include <cstdlib> #include <cstring> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/epoll.h> #define BUF_SIZE 100 #define EPOLL_SIZE 50 // 错误处理函数 void error_handling(const std::string &message) { std::cerr << message << std::endl; exit(1); } int main(int argc, char *argv[]) { int serv_sock, clnt_sock; sockaddr_in serv_adr, clnt_adr; socklen_t adr_sz; int str_len, i; char buf[BUF_SIZE]; epoll_event *ep_events; epoll_event event; int epfd, event_cnt; // 检查参数个数 if (argc != 2) { std::cerr << "using: " << argv[0] << " <port>" << std::endl; exit(1); } // 创建服务器套接字 serv_sock = socket(PF_INET, SOCK_STREAM, 0); if (serv_sock == -1) error_handling("socket() error"); // 初始化服务器地址结构体 memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); // 绑定服务器套接字 if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error"); // 监听连接请求 if (listen(serv_sock, 5) == -1) error_handling("listen() error"); // 创建epoll实例 epfd = epoll_create(EPOLL_SIZE); if (epfd == -1) error_handling("epoll_create() error"); // 动态分配epoll事件数组 ep_events = new epoll_event[EPOLL_SIZE]; // 设置服务器套接字的事件类型并添加到epoll实例中 event.events = EPOLLIN; event.data.fd = serv_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event) == -1) error_handling("epoll_ctl() error"); while (true) { // 等待事件发生 event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); if (event_cnt == -1) { std::cerr << "epoll_wait() error" << std::endl; break; } for (i = 0; i < event_cnt; i++) { if (ep_events[i].data.fd == serv_sock) { // 接受新的客户端连接 adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); if (clnt_sock == -1) error_handling("accept() error"); // 将新的客户端套接字添加到epoll实例中 event.events = EPOLLIN; event.data.fd = clnt_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event) == -1) error_handling("epoll_ctl() error"); std::cout << "connected client: " << clnt_sock << std::endl; } else { // 处理客户端消息 str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); if (str_len == 0) { // 客户端关闭连接 if (epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, nullptr) == -1) error_handling("epoll_ctl() error"); close(ep_events[i].data.fd); std::cout << "closed client: " << ep_events[i].data.fd << std::endl; } else { // 回显消息给客户端 write(ep_events[i].data.fd, buf, str_len); } } } } // 关闭服务器套接字和epoll实例 close(serv_sock); close(epfd); delete[] ep_events; return 0; }
-
客户端与Select模型一致
4.条件触发和边缘触发
-
条件触发(level-triggered,也被称为水平触发)LT:
只要满足条件
,就触发一个事件(只要有数据没有被获取,内核就不断通知你)。 -
边缘触发(edge-triggered)ET:
每当状态变化时
,触发一个事件。-
"举个读socket的例子,假定经过长时间的沉默后,现在来了100个字节,这时无论边缘触发和条件触发都会产生一个通知应用程序可读。应用程序读了50个字节,然后重新调用api等待io事件。
这时水平触发的api会因为还有50个字节可读从而
立即返回
用户一个read ready notification。而边缘触发的
api
会因为可读这个状态没有发生变化而陷入长期等待
。 因此在使用边缘触发的api
时,要注意每次都要读到socket
返回EWOULDBLOCK
为止,否则这个socket
就算作废了。而使用条件触发的api 时,如果应用程序不需要写就不要关注socket
可写的事件,否则就会无限次的立即返回一个write ready notification
。 -
select
模型属于典型的条件触发
。
-
-
条件触发的代码示例:
c++#include <iostream> #include <cstring> #include <unistd.h> #include <fcntl.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/epoll.h> #define BUF_SIZE 4 #define EPOLL_SIZE 50 void error_handling(const std::string &message) { std::cerr << message << std::endl; exit(1); } int main(int argc, char *argv[]) { int serv_sock, clnt_sock; sockaddr_in serv_adr{}, clnt_adr{}; socklen_t adr_sz; int str_len, i; char buf[BUF_SIZE]; epoll_event *ep_events; epoll_event event{}; int epfd, event_cnt; // 检查命令行参数 if (argc != 2) { std::cerr << "using: " << argv[0] << " <port>" << std::endl; exit(1); } // 创建服务器套接字 serv_sock = socket(PF_INET, SOCK_STREAM, 0); if (serv_sock == -1) error_handling("socket() error"); // 初始化服务器地址结构体 memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); // 绑定服务器套接字 if (bind(serv_sock, (sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error"); // 监听连接请求 if (listen(serv_sock, 5) == -1) error_handling("listen() error"); // 创建epoll实例 epfd = epoll_create(EPOLL_SIZE); if (epfd == -1) error_handling("epoll_create() error"); // 动态分配epoll事件数组 ep_events = new epoll_event[EPOLL_SIZE]; // 设置服务器套接字的事件类型并添加到epoll实例中 event.events = EPOLLIN; event.data.fd = serv_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event) == -1) error_handling("epoll_ctl() error"); while (true) { // 等待事件发生 event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); if (event_cnt == -1) { std::cerr << "epoll_wait() error" << std::endl; break; } std::cout << "return epoll_wait" << std::endl; for (i = 0; i < event_cnt; i++) { if (ep_events[i].data.fd == serv_sock) { // 接受新的客户端连接 adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock, (sockaddr *)&clnt_adr, &adr_sz); if (clnt_sock == -1) error_handling("accept() error"); // 将新的客户端套接字添加到epoll实例中 event.events = EPOLLIN; event.data.fd = clnt_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event) == -1) error_handling("epoll_ctl() error"); std::cout << "connected client: " << clnt_sock << std::endl; } else { // 处理客户端消息 str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); if (str_len == 0) { // 客户端关闭连接 if (epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, nullptr) == -1) error_handling("epoll_ctl() error"); close(ep_events[i].data.fd); std::cout << "closed client: " << ep_events[i].data.fd << std::endl; } else { // 回显消息给客户端 write(ep_events[i].data.fd, buf, str_len); } } } } // 关闭服务器套接字和epoll实例 close(serv_sock); close(epfd); delete[] ep_events; return 0; }
-
边缘触发的示例代码:
c++#include <iostream> #include <cstring> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/epoll.h> #define BUF_SIZE 4 #define EPOLL_SIZE 50 void setNonBlockingMode(int fd); void errorHandling(const std::string &message); int main(int argc, char *argv[]) { int serv_sock, clnt_sock; sockaddr_in serv_adr{}, clnt_adr{}; socklen_t adr_sz; int str_len; char buf[BUF_SIZE]; epoll_event *ep_events; epoll_event event{}; int epfd, event_cnt; // 检查命令行参数 if (argc != 2) { std::cerr << "using: " << argv[0] << " <port>" << std::endl; exit(1); } // 创建服务器套接字 serv_sock = socket(PF_INET, SOCK_STREAM, 0); if (serv_sock == -1) errorHandling("socket() error"); // 初始化服务器地址结构体 memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); // 绑定服务器套接字 if (bind(serv_sock, (sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) errorHandling("bind() error"); // 监听连接请求 if (listen(serv_sock, 5) == -1) errorHandling("listen() error"); // 创建epoll实例 epfd = epoll_create(EPOLL_SIZE); if (epfd == -1) errorHandling("epoll_create() error"); // 动态分配epoll事件数组 ep_events = new epoll_event[EPOLL_SIZE]; // 设置非阻塞模式 setNonBlockingMode(serv_sock); event.events = EPOLLIN; event.data.fd = serv_sock; // 将服务器套接字添加到epoll实例中 if (epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event) == -1) errorHandling("epoll_ctl() error"); while (true) { // 等待事件发生 event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); if (event_cnt == -1) { std::cerr << "epoll_wait() error" << std::endl; break; } std::cout << "return epoll_wait" << std::endl; for (int i = 0; i < event_cnt; i++) { if (ep_events[i].data.fd == serv_sock) { // 接受新的客户端连接 adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock, (sockaddr *)&clnt_adr, &adr_sz); if (clnt_sock == -1) errorHandling("accept() error"); // 设置非阻塞模式 setNonBlockingMode(clnt_sock); event.events = EPOLLIN | EPOLLET; event.data.fd = clnt_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event) == -1) errorHandling("epoll_ctl() error"); std::cout << "connected client: " << clnt_sock << std::endl; } else { while (true) { // 读取客户端消息 str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); if (str_len == 0) { // 关闭请求 if (epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, nullptr) == -1) errorHandling("epoll_ctl() error"); close(ep_events[i].data.fd); std::cout << "closed client: " << ep_events[i].data.fd << std::endl; break; } else if (str_len < 0) { if (errno == EAGAIN) break; } else { // 回显消息给客户端 write(ep_events[i].data.fd, buf, str_len); } } } } } // 关闭服务器套接字和epoll实例 close(serv_sock); close(epfd); delete[] ep_events; return 0; } void setNonBlockingMode(int fd) { int flag = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flag | O_NONBLOCK); } void errorHandling(const std::string &message) { std::cerr << message << std::endl; exit(1); }
-
运行结果中需要注意的是,客户端发送消息次数和服务器端
epoll_wait()
函数调用次数。客户端从请求连接到断开连接共发送5
次数据,服务器端也相应产生5
个事件。