目录

第一部分

静态库与动态库的建立

静态库的建立和使用

  • 建立

    1
    g++ -c -o libpublic.a public.cpp

其中libpublic.a表示静态库,public.cpp表示需要建立静态库的代码

  • 使用

    1
    g++ -o demo01 demo01.cpp -L/home/pigcanstudy/tools -lpublic

其中-L表示静态库所在目录,-l表示静态库的库名
示例图片

动态库的建立和使用

  • 建立

    1
    2
    g++ -fPIC -shared -o lib库名.so 源代码文件
    g++ -fPIC -shared -o libpublic.so public.cpp
  • 使用

    1
    2
    g++ 选项 源代码文件 -l库名 -L库文件所在目录名
    g++ -o demo01 demo01.cpp -L/home/pigcanstudy/tools -lpublic

    需要注意的是如果使用了动态库需要提前设置LD_LIBRARY_PATH
    示例图片 用来查看路线有啥
    示例图片 用来设置路径,后面加的是动态库的地址

makefile

为什么要引入make这个工具

  • 静态库与动态库在建立的时候很麻烦,如果有很多项目文件选哟建立库,就可以使用make工具,使用.sh脚本工具也能实现,即建立一个.sh的脚本,里面加入你要建立库的语句,如图所示:
    示例图片
    可以用 sh命令来执行

makefile的使用与细节

  • 使用makefile的时候需要注意的事项
    注意在所有g++ rm cp 前面得需要TAB键分割,然后再换行使用\的时候\后面不要乱加空格
    否则会报如下错误:
    示例图片
    makefile的基本语法 如下图所示:
    示例图片

    1. 执行make 会生成两个库文件
    2. 执行make clean 会执行删除两个库文件
      执行make 时出现如下所示:
      表示
      表示不需要重新编译all中的库文件
    3. make采用的是增量编译(也就是只需要重新编译需要重新编译的文件,原本编译好的文件无需继续编译,这就是其与脚本比较更好的原因)
    4. 注意 当你改变了hpp或者cpp的时候,一定要重新make以下库文件,否则会很痛苦,会报错 因为用了之前的为改变的hpp与cpp
    5. 以上makefile的基本语法这么写建立在main文件长成这样的时候:
      示例图片
      这种头文件定义方式是不符合规范的,一旦头文件的路径发生改变,所有源代码都得修改,所以得换一种实现方式:

      1. 首先改成如下所示:
        示例图片
      2. 然后需要执行如下指令:

        1
        g++ -o demo01 demo01.cpp -L/home/pigcanstudy/test1/tools -lpublic -L/home/pigcanstudy/test1/api -lmyapi -I/home/pigcanstudy/test1/tools -I/home/pigcanstudy/test1/api/test1/api -lmyapi -I/home/pigcanstudy/test1/tools -I/home/pigcanstudy/test1/api
      3. 执行文件
        示例图片

      4. 为了不让每次执行都需要打那么的编译指令。可以使用makefile来代替之:如图所示:
        示例图片
      5. 之后执行make就行了加./demo就能执行了
      6. 项目很大,有很多需要编译的东西怎么办?
        答案是使用变量来表示 如图
        示例图片

main函数的参数

1
int main(int argc, char* argv[], char *envp[])

argc

存放了程序参数的个数,包括程序本身

argv

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

envp

字符串数组,存放了环境变量,数组最后一个元素为空
这参数常用于判断一个人是否能进入程序(如表白程序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  //int setenv(const char* name, const char* value, int overwrite){
// name 环境变量名
// value 环境变量值
//overwrite 0 如果环境不存在增加新的环境变量,存在不替换其值
//overwrite 非0 存在就替换其值
}
//只对本进程有效

#include<iostream>
int main(int argc, char* argv[],char* envp[]){
std::cout << "一共有" << argc << "个参数\n";
// 显示全部的参数
for(int i = 0; i < argc; i ++){
std:: cout << "第" << i << "个参数: " << argv[i] << std::endl;
}
setenv("hello", "HELLO", 0);
std::cout << getenv("hello") << std::endl;
/*for(int i = 0; envp[i]!=0; i ++){
std::cout << envp[i] << std::endl;
}*/
return 0;
}

gdb调试程序

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

gdb的常用命令

示例图片

示例图片

示例图片

用gdb调试core文件

示例图片

示例图片

ulimit 的参数 :示例图片
要修改哪个参数 就 - 啥

  • gdb排查死锁的方法

    1 . 先使用

    1
    g++ --std=c++11 -g -O0 dead_lock.cpp -o dead_lock
    1. 发生死锁后 程序会卡住,一般会有一个看门狗程序,对卡住的程序发送kill信号,这时候被杀死的信号会产生core文件
    2. 然后执行以下命令来分析core文件

      1
      gdb -c ./core-49237 ./dead_lock

      并用bt命令查看调用栈
      alt text

    3. 在gdb调试下使用 info threads,查看当前有多少线程
      alt text
    4. 使用thread 加对应编号以及 bt 查看线程在干啥
      alt text
    5. 使用f 加对应编号 来查看对应调用栈干了什么
      alt text
    6. 使用 p 加你要查看的变量查看其参数
      alt text
      owner 不为0就表示他被谁占有着,使用 info threads 能看线程的对应线程号

用gdb调试一个正在运行的程序

  • 首先需要知道运行程序的进程编号是什么?
    使用 ps -ef |grep demo
  • 然后使用 gdb demo -p 进程编号
  • 正在运行的程序被调试了会自动停下来

linux的时间操作

  • C++11提供了操作时间的chrono库
  • 但这个库屏蔽了很多细节 所以我们为了更好使用它,得了解他的底层

time_t 别名

  • 这是一个时间类型,他是一个long类型的别名,在文件中定义,表示从1970.1.1到现在的秒数
  • 推荐使用 time_t定义它
    可用这个 简化书写 typedef long time_t。

time()库函数

  • 获取操作系统的当前时间
    示例图片
    上述两种用法是一样的,都可以用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      include <iostream>
    #include <time.h>
    typedef long time_t;
    int main(){
    long t = time(0);
    long tt;
    time(&tt);
    std::cout << t << std::endl;
    std::cout << tt << std::endl;
    return 0;
    }

tm结构体(timeval)

示例图片

localtime()库函数

  • 这个函数的作用是把time_t表示的时间转变为tm结构体表示的时间
  • localtime()函数不是线程安全的,localtime_r()是线程安全的
  • 函数声明 示例图片

mktime()函数

与localtime()功能相反
示例图片

  • 这个函数主要用于时间的运算,这样更方便运算

示例图片

gettimeofday()函数

  • 用于获取从1970.1.1到现在的秒和当前秒中已逝去的微秒数,可以用于程序的计时
  • 函数声明:示例图片

程序的睡眠

  • 把程序挂起一段时间 sleep()函数
    与usleep()函数
  • 前者单位是秒 后者是微秒
  • 包含在头文件
    示例图片

linux 获取目录操作

几个简单的目录操作函数

获取当前工作目录

示例图片

  • 即 getcwd()函数,这个函数的作用是获取当前的工作目录,需要提前指定好容量

  • get_current_dir_name(void) 这个函数 功能也是获取当前的工作目录,但是它是 动态分配内存,其内部是使用malloc()函数 所以需要手动free()它

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>
    #include <unistd.h>

    int main(){
    char path1[256];
    getcwd(path1, 256);
    std::cout << "path1 = " << path1 << std::endl;
    char* path2 = get_current_dir_name();
    std::cout << "path2= " << path2 << std::endl;
    free(path2);//注意释放内存
    }
切换工作目录

示例图片

创建目录

示例图片
其中有两个参数 一个是目录名 一个是访问权限 注意:权限是八进制数不要省略前置的0 也就是说 0755

  • 如果上级目录不存在会失败
删除目录

示例图片

获取目录中文件的列表

  • 在处理文件之前必须先知道目录列表
包含头文件
1
#include <dirent.h>
相关的库函数

示例图片

  • 目录指针 DIR ,声明方式DIR* 指针名字
  • readdir(),返回结构体 dirent的地址,存放了本次读取到的内容
  • 结构体定义如下:示例图片
  • 重点关注d_type和d_name成员,其中文件类型有多种取值,最重要的是8和4,8表示常规文件,4表示目录
  • 示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
      #include <iostream>
    #include <dirent.h> // 目录操作的头文件

    int main(int argc, char* argv[]){
    if(argc != 2){
    std::cout << "Using ./mulu1 目录名\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;
    }
    return 0;
    }

linux 的系统错误

  • 在C++程序中,如果调用了库函数,如果失败了会在全局变量errno中存放调用过程产生错误代码的原因,所以我们可以用errno变量来查找原因
  • 头文件:
  • 需要配合sterror()和perror()两个函数库,来获取详细信息

sterror()库函数

  • 其是声明在
1
2
char* strerror(int errnum); //非线程安全
int strerror_r(int errnum, char*buf, size_t buflen); //线程安全

示例代码:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string.h>
#include <cerrno> //errno全局变量的头文件
#include <sys/stat.h> // mkdir函数所在头文件

int main(){
int ir = mkdir("/home/pigcanstudy/test1/123", 0755);
std::cout << "ir= " << ir << std::endl;
std::cout << errno << ":" << strerror(errno) << std::endl;
return 0;
}

perror()库函数

  • 中,用于在控制台显示最近一次系统错误的详细信息

注意事项

  1. 并不是所有库函数调用失败都会在errno中存储,以man 手册为主(不属于系统调用的函数不会设置errno)
  2. errno不能作为调用库函数的失败标志 因为errno 在调用成功后 不会改变值,也就是说不会主动置为0(0表示成功)

目录和文件的更多操作

access()库函数

  • 此函数用于判断当前用户对目录或文件的存储权限是否满足
  • 包含在头文件
    示例图片
    四种权限
    示例图片
  • 返回值 当pathname满足mode权限返回0,不满足返回-1,errno 被设置
  • 在实际开发中 access()函数主要用于判断目录或文件是否存在
    示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <cstdio>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char* argv[]){
if(argc != 2){
std::cout << "Using ./access 文件或目录名\n";
return -1;
}
if(access(argv[1],F_OK) != 0){
std::cout << "文件或目录" << argv[1] << "不存在\n";
return -1;
}
std::cout << "文件或目录" << argv[1] << "存在\n";
return 0;
}

stat结构体

  • stat 结构体用于存放目录或文件的详细信息,如下:示例图片
  • 重点关注st_mode,st_size和st_mtime成员, st_mtime是一个整数表示的时间,需要程序员自己写代码转换格式
    示例图片

stat()库函数

示例图片

utime()库函数

  • 用于修改目录或文件的时间
    示例图片

  • 结构体 utimbuf
    示例图片

rename()库函数

示例图片

remove()库函数

示例图片

Linux的信号

信号的基本概念

  • 信号是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是,不能给进程传递任何数据
  • 在shell中使用kill 和 killall 命令发送信号
kill 与 killall 的区别

示例图片

信号的类型

示例图片

  • 重要的类型

    1. SIGINT 2 A 键盘中断

    2. SIGKILL 9 AEF 采用kill -9 进程编号强制杀死程序

    3. SIGALRM 14 A 由闹钟alarm()函数发出的信号
    4. SIGTERM 15 A 采用 kill 进程编号 或 killall 程序名来通知程序
    5. SIGCHLD 17 B 子进程结束信号
    6. SIGSEGV 11 CEF 无效的内存引用(数组越界,操作空指针和野指针)

其中字母表示默认缺省的类型
示例图片

信号的处理

  1. 大部分默认操作是终止进程
  2. 设置信号处理函数(signal() 函数)
  3. 忽略某个信号
    示例图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <unistd.h>
#include <signal.h>

void func(int signum){
std::cout << "收到了信号: " << signum << std::endl;
}
void func1(int sig){
std::cout << "收到了闹钟信号,完成了定时任务\n";
alarm(5); //此函数不能少否则只会响应一次
}
int main(){
signal(1,func);
signal(15,func);
signal(SIGINT, SIG_IGN); // 忽略2的信号
signal(9, func);//此代码无效,因为9不可被捕获
signal(9, SIG_IGN);//此代码无效,因为9不可被忽略

alarm(5);//闹钟(定时器),5秒后向进程发送14的信号
signal(14, func1);//设置定时任务函数

while(1){
std::cout << "执行了一次任务。\n";
sleep(1);
}
return 0;
}
  • SIG_DEF: 恢复参数signum 所指信号的处理方法为默认值
  • 自定义的信号比如func
  • SIG_IGN: 忽略参数signum所指的信号

信号有什么用

  1. 可以在杀死进程的时候进行善后工作,比如释放内存啥的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <signal.h>
#include <unistd.h>

void EXIT(int sig){
std::cout << "收到了信号:" << sig << std::endl;
std::cout << "正在释放资源,程序将退出...." << std::endl;
//以下是释放资源
std::cout << "程序退出" << std::endl;
exit(0);
}
int main(){
signal(2, EXIT);
signal(15, EXIT);
while(1){
sleep(1);
}
return 0;
}
  1. 向程序发送0信号,可以检测程序是否存活
    示例图片

发送信号

可以使用kill()函数向其他进程发送信号

  1. 函数声明 int kill(pid_t pid, int sig);
    1. sig表示是啥信号
    2. pid 表示指定的进程号
  2. pid的几种情况
    1. pid>0 将信号传给进程号为pid的进程
    2. pid=0 将信号传递给和目前进程相同进程组的所有进程常用于父进程给子进程发送信号,注意,发送信号者也会收到自己发出的信号
    3. pid=-1 将信号广播发送给系统内所有进程,例如系统关机时,会向所有的登录窗口广播关机信息

进程终止

进程终止方式

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

进程终止状态

  1. 默认终止状态为0
  2. 查看进程终止状态为 echo $?
  3. 3个正常终止函数的参数就是状态
    示例图片
  4. 异常终止 状态为非0

资源释放的问题

  • return 在哪个函数中返回就会清理哪个局部对象的析构函数,在main中还会调用全局对象的析构
  • exit()不会调用局部对象的析构,会调用全局对象析构
  • exit()会执行清理工作,在退出 而_exit(),_Exit()会直接退出,不执行清理工作

进程的终止函数

  • atexit()函数登记终止函数最多32个,这些函数由exit()自动调用
    1. int atexit(void(*function)(void));
    2. exit()调用终止函数顺序与登记时相反
    3. 可以干些进程终止前的收尾工作

调用可执行程序

system()库函数

功能

调用可执行程序,把需要执行的程序和参数用一个字符串传给system()就好了
头文件可用 man system()来查看

返回值
1
2
3
4
5
6
7
8
#include <iostream>
#include <unistd.h>

int main(){
int ret = system("/bin/ls -l /tmp");//注意使用全路径,因为可以避免环境变量问题
std::cout << "ret=" << ret << std::endl;
perror("system");
}

示例图片

exec函数族

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

示例图片

其中重要的已经加粗

execl()函数
1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <unistd.h>
#include <string.h>

int main(int argc, char* argv[]){
int ret = execl("/bin/ls", "/bin/ls", "-lt", "/tmp", 0);//最后一个0不能省略,因为他表示可变参数结束,第一个与第二个填需要执行的函数,后面是可变参数填需要执行函数的参数
std::cout << "ret=" << ret << std::endl;
perror("execl");
}

  • 注意事项:示例图片
    1. 其中第二点尤为重要,新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段,数据段,和堆栈
      举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <unistd.h>
#include <string.h>

int main(int argc, char* argv[]){
// 新进程的进程编号与原进程编号相同,但是,新进程取代了原进程的代码段,数据段和堆栈
std::cout << "demo 本进程的编号是: " << getpid() << std::endl;
int ret = execl("/home/pigcanstudy/test1/demo01","/home/pigcanstudy/test1/demo01",0);//最后一个0不能省略,因为他表示可变参数结束,第一个与第二个填需要执行的函数,后面是可变参数填需要执行函数的参数
std::cout << "ret=" << ret << std::endl;
perror("execl");
/*int res = system("/home/pigcanstudy/test1.demo01");
std::cout << "res=" << res << std::endl;
perror("system");*/
}

示例图片

execv()函数

示例图片
注意 char* 必不可少否则会报错

创建进程

Linux的0,1,2号进程

示例图片

  • 注意他是一个树形结构

进程标识

示例图片

fork()函数

用来创建一个子进程
示例图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <string>
#include <unistd.h>

int main(){
int bh = 8;
std::string message="我是一只小猪";
pid_t pid = fork();
pid_t pid1 = fork();
if(pid > 0){
std::cout << "fu" << std::endl;
std::cout << pid << std::endl;
}
else{
std::cout << "zi" << std::endl;
std::cout << pid << std::endl;
}
if(pid1 > 0){
std::cout << "fu" << std::endl;
std::cout << pid1 << std::endl;
}
else{
std::cout << "zi" << std::endl;
std::cout << pid1 << std::endl;
}
return 0;
}

fork()的两种用法

示例图片

  • 第一种为上述Cpp
  • 第二种示例为下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <unistd.h>

int main(){
if(fork() > 0){
//父进程执行这一段代码
while(true){
sleep(1);
std::cout << "父进程运行中...\n";
}
}
else{
// 子进程执行这一段代码
sleep(10);
std::cout << "子进程开始执行任务....\n";
execl("/home/pigcanstudy/test1/demo01", "/home/pigcanstudy/test1/demo01", 0);
std::cout << "子进程执行任务结束,退出\n";
}
return 0;
}
  • 补充 shell的原理是在bash这个进程下创建子进程来执行你写的操作

共享文件

示例图片
对这句话的解释就是父子进程中,如果父进程先执行完文件写操作,子进程会接着父进程的文件指针的下一个来写,也就是说不会覆盖文件的内容,而是接在后面

示例图片
需要采用进程同步

vfork()

示例图片

僵尸进程

僵尸进程的产生

示例图片

  • 对于第一句话的解释
    代码:
    示例图片
    结果:
    示例图片

    将一个可执行程序放在后台运行有两种方法:

    1. 直接执行 ./demo &
    2. 在程序的代码中 加入一行if(fork() > 0) return 0;
    • 对于第二句话解释

      代码:
      示例图片

      结果:
      示例图片

      当一个进程终止时,它的状态信息仍然被保留在系统中,直到其父进程调用wait或waitpid等系统调用来获取其终止状态信息。如果父进程没有及时调用这些系统调用来获取终止状态信息,那么这个已经终止的子进程就会成为一个僵尸进程(Zombie Process)。

      示例图片

僵尸进程地避免

  1. 子进程退出的时候,内核会给父进程发出SIGCHLD信号,如果父进程调用了signal(SIGCHLD,SIG_IGN)来通知内核,内核就会把这个子进程的数据结构释放,但是父进程得不到子进程被释放的信息
  2. 使用wait或waitpid 如图:
    示例图片

示例图片

  1. 如果父进程很忙,可以捕获SIFCHLD信号,在信号处理函数中调用wait()/waitpid()

示例图片

多进程与信号

  • 在多进程的服务程序中,如果子进程收到退出信号,子进程自行退出,如果父进程收到退出信号,应该先向所有子进程发送退出信号,再自己退出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <unistd.h>
#include <signal.h>

void FatherEXIT(int sig);
void ChildEXIT(int sig);

int main(){
// 忽视全部的信号,不希望被打扰
for(int i = 0; i < 65; i ++) signal(i, SIG_IGN);
//设置信号,在shell状态下,可用kill 进程号 或者 Cirl + c 正常终止这些进程
// 但请不要用 kill -9 + 进程号 强行终止
signal(SIGTERM, FatherEXIT); //15信号,这行的代码意思是自己规定一个信号处理函数
signal(SIGINT, FatherEXIT);// 2信号
while(true){
if(fork() > 0){ //父进程流程
sleep(5);
continue;
}
else{ //子进程流程
signal(SIGTERM, ChildEXIT);//规定一个子进程退出信号
signal(SIGINT, SIG_IGN);//子进程不需要捕获SIGINT信号

while(true){
std::cout << "子进程" << getpid() << "正在运行中。\n";
sleep(3);
continue;
}
}
}
return 0;
}

void FatherEXIT(int sig){
// 以下代码是为了防止信号处理函数在执行的过程中被信号再次中断
signal(SIGTERM, SIG_IGN);
signal(SIGINT, SIG_IGN);
std::cout << "父进程" << getpid() << "正在退出, sig=" << sig << std::endl;
kill(0,SIGTERM);
std::cout << "子进程" << getpid() << "正在释放父进程的资源以及全局资源" << std::endl;
// 这里些释放资源的代码

exit(0);
}

void ChildEXIT(int sig){
// 以下代码是为了防止信号处理函数在执行的过程中被信号再次中断
signal(SIGTERM, SIG_IGN);
signal(SIGINT, SIG_IGN);
std::cout << "子进程" << getpid() << "正在退出, sig=" << sig << std::endl;
std::cout << "子进程" << getpid() << "正在释放子进程的资源" << std::endl;
// 这里些释放资源的代码
exit(0);

}

共享内存

示例图片

示例图片

shmget()函数

示例图片

  • 补充说明:shmget()函数,如果共享内存不存在就创建它,如果有就返回他的IP

  • ipcs -m 用来查看共享字段
    示例图片

  • ipcrm -m + shimd 用来删除共享内存
    示例图片

shmat()函数

示例图片

shmdt()函数

示例图片

shmctl()函数

示例图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

struct node{
int no;
char name[51];
};

int main(int argc, char* argv[]){
// 第一步创建共享内存
int shmid = shmget(0x5005, sizeof(node), 0640|IPC_CREAT);
if(shmid == -1){
std::cout << "shgmget(0x5005) failed.\n";
return -1;
}
std::cout << "shmid=" << shmid << std::endl;
//第二步:把共享内存连接到当前进程的地址空间
node *ptr = (node *)shmat(shmid,0,0);
if(ptr == (void *)-1){
std::cout << "shmat() failed\n";
return -1;
}
//第三步:使用共享内存,对共享内存进行读/写操作
std::cout << "原值:no=" << ptr->no << ",name=" << ptr->name << std::endl;
ptr->no = atoi(argv[1]);
strcpy(ptr->name, argv[2]);
std::cout << "新值:no=" << ptr->no << ",name=" << ptr->name << std::endl;
// 第四步把共享内存从当前进程中分离
shmdt(ptr);
//第五步:删除共享进程
if(shmctl(shmid, IPC_RMID,0) == -1){
std::cout << "shmctl failed\n";
}
return 0;
}
  • 注意分配共享内存的时候不能分配堆区,所以在数据结构中的stl那些都不能分配

循环队列

示例图片

原理

示例图片

细节:用来动态使用
https://www.bilibili.com/video/BV11Z4y157RY/?p=22&spm_id_from=pageDriver&vd_source=772b61fb408cdf9169f726ab21790987

信号量

示例图片

  • p操作wait 将信号量的值减一,如果信号量的值为0,将阻塞等待,知道信号量大于0
  • V操作post 将信号量的值加1 任何时候都不会阻塞

https://www.bilibili.com/video/BV11Z4y157RY/?p=23&spm_id_from=pageDriver&vd_source=772b61fb408cdf9169f726ab21790987

pthread 线程库

示例图片

第二部分

第一个网络通信程序户端

基于linux的文件操作

文件描述符的分配规则

示例图片

  • 其分配规则是找到最小的,没有被占用的文件描述符,也就是说如果我们把0,1,2三个描述符关掉后,再打开文件,分配的就是0,如果都不关则分配3,关掉0,对应就不能输入,其他亦然

  • 在Linux系统中,文件与socket没有区别,socket也会分配文件描述符给他

示例图片

  • linux底层的read系统调用和write系统调用,也能用在socket中

socket函数详解

创建socket

函数的声明

示例图片

  • 其中 domain 参数用来指定协议家族:
    示例图片
    重要参数 IP_INET
type 表示数据的传输类型

示例图片

protocol协议

示例图片

两种创建方式

示例图片

几乎全部的网络编程函数失败时都会返回-1,并且errno被设置,单个进程中创建的socket数量受系统的openfile限制(ulimit -a)

主机字节序和网络字节序

大端序和小端序
  • 大端序:低位字节存放在高位,高位字节存放在低位
  • 小端序:地位字节存放在地位,高位字节存放在高位

  • 举例:示例图片

  • 问题:
    示例图片

网络字节序
  • 为了解决主机字节序不同的问题,引入了网络字节序(是一种大端序)
    示例图片
    1. 其中 h 表示host(主机)
    2. to 转换
    3. n network 网络
    4. s short 二字节 16位整数
    5. l long 四字节 32位整数
IP地址和通讯端口

示例图片

如何处理大小端序
  • 数据收发的时候有自动转换,只有向sockaddr_in 结构体成员变量填充数据的时候才需要考虑字节序问题

万恶的结构体

示例图片

sockaddr结构体
  • 用来存放协议族,端口和地址信息,客户端和connect()函数和bind()函数需要这个结构体

示例图片

sockaddr_in结构体

sockaddr结构体是为了统一接口函数,操作起来不方便,所以加了这个等价的结构体

示例图片
也就是说 后者的结构体是为了方便操作而设计的

  • 在程序中我们操作的是sockaddr_in结构体,在调用函数的时候我们调用的soockaddr结构体
细说此结构体内部的细节
  • 首先由于它的大小与sockaddr相同所以可以强制转换为sockaddr
    示例图片

但操作ip地址成员的时候 我们该怎么办?

使用gethostbyname()函数
  • 函数声明以及其返回结构体的内容如下:
    示例图片

举例:

1
2
3
4
5
6
7
8
//此段代码就是使用gethostbuname函数的例子
struct hostent* h;
if((h = gethostbyname(argv[1])) == nullptr){
std::cout << "gethostbyname failed.\n";
close(sockfd);
return -1;
}
memcpy(&servaddr.sin_addr, h->h_addr,h->h_length);
字符串IP与大端序IP的转换
  • C语言提供了一个库函数,用于字符串格式的IP和大端序IP的互相转换,用于网络通信的服务端程序中
1
2
3
4
5
6
7
8
9
10
typedef unsigned int in_addr_t; //32位大端序的IP的值
// 把字符串格式的IP(192.168.168.1)转换为大端序IP,转换后IP赋值给sockaddr_in.int_addr.s_addr
in_addr_t inet_addr(const char* cp);

// 把字符串格式IP转换为大端序IP,将转换后IP填充给sockaddr_in.int_addr.s_addr
int inet_ation(const char* cp, struct in_addr* inp);

// 把大端序IP转换为字符串格式的IP,用于在服务器端程序中解析客户端的IP的值
char *inet_ntoa(struct in_addr in);

  • 在客户端程序中使用此方法就只能指定ip地址而不能指定域名或者主机号,而用gethostbyname 方法就行
1
2
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示操作系统里的所有IP都能通信
//servaddr.sin_addr.s_addr = inet_addr("192.168.168.1"); // 指定服务端用于通讯的IP

未封装的客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in需要
#include <netdb.h>//hostent需要


int main(int argc, char* argv[]){
if(argc != 3){
std::cout << "Using:./client 服务端IP地址 服务端的端口\n Example:./client 192.168.168.1 5005\n\n";
return -1;
}
// 第一步建立socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);// 分别指定IPV4协议族,面向有链接的,如果是 0 的话,你不必自己指定协议,而由服务提供者为你选择合适的协议类型(或者写tcp协议对应编号)
std::cout << sockfd << std::endl;
if(sockfd == -1){
perror("socket");
return -1;
}

//第二步向服务器发送链接请求
struct sockaddr_in servaddr;//用于存放协议,IP地址和端口的结构体
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族
servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口
struct hostent* h;
if((h = gethostbyname(argv[1])) == nullptr){
std::cout << "gethostbyname failed.\n";
close(sockfd);
return -1;
}
memcpy(&servaddr.sin_addr, h->h_addr,h->h_length);//指定服务端的通讯IP
if(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1){
perror("connect");
close(sockfd);
return -1;
}

//第三步 与服务端通信,发送一个请求报文后等待服务端的回复,收到下一个回复后,再发下一个请求报文
char buffer[1024];
for(int i = 0; i < 10; i ++){
int iret;
memset(buffer, 0, sizeof(buffer));
sprintf(buffer, "这是第%d个智鞭,编号%03d。", i + 1, i + 1);
if(iret = send(sockfd,buffer,sizeof(buffer), 0) <= 0){
perror("发送");
return -1;
}
std::cout << "发送" << buffer << std::endl;
memset(buffer, 0, sizeof(buffer));
// 接收服务端的回应报文,如果没有回应报文,recv()函数将阻塞等待。
if(recv(sockfd, buffer, sizeof(buffer), 0) <= 0){//接收函数存入buffer
std::cout << "iret=" << iret << std::endl;
break;
}
std::cout << "接收" << buffer << std::endl;
sleep(1);
}
// 第四步 关闭socket
close(sockfd);
return 0;
}

未封装的服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in需要
#include <netdb.h>//hostent需要

int main(int argc, char* argv[]){
if(argc != 2){
std::cout << "Using:./server 通讯端口\n Example:./server 5005\n\n";
std::cout << "注意:运行服务端的程序Linux系统的防火墙必须要开通5005端口\n";
std::cout << "如果是云服务器,还有开通云平台的访问策略";
return -1;
}
// 第一步建立服务端的socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == -1){
perror("socket");
return -1;
}

//第二部用bind绑定ip地址和端口到socket上
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;//固定填AF_INET
servaddr.sin_port = htons(atoi(argv[1]));
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示操作系统里的所有IP都能通信
//servaddr.sin_addr.s_addr = inet_addr("192.168.168.1"); // 指定服务端用于通讯的IP
//绑定服务器的IP和端口
if(bind(listenfd,(sockaddr *)&servaddr, sizeof(servaddr)) == -1){
perror("bind");
close(listenfd);
return -1;
}

//第三步打开socket监听状态
if(listen(listenfd,5) == -1){
perror("listen");
close(listenfd);
return -1;
}

//第四步:受理客户端的链接请求,如果没有客户端练上来,accept()将阻塞等待
//accept后面两个参数一个是地址信息,一个是地址结构体大小,两个都填0表示不关心地址信息
int clientfd = accept(listenfd, 0, 0);
if(clientfd == -1){
perror("accept");
close(listenfd);
return -1;
}
std::cout << "客户端已连接" << std::endl;

//第五步:接收客户端发来的信息,并发送回应报文
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, sizeof(buffer), 0) <= 0)){
perror("send");
break;
}
std::cout << "发送:" << buffer << std::endl;
}
// 关闭socket 释放资源
close(listenfd);
close(clientfd);
return 0;
}

封装的客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in需要
#include <netdb.h>//hostent需要
#include <string>

class ctcpclient{
public:
int m_clientfd; // 客户端的socket,-1表示未连接或连接已断开,>=0 表示有效的socket
std::string m_ip; // 服务器的IP/域名
unsigned short m_port; // 通讯端口

//向服务端发起连接请求,成功返回true,失败false
bool connect(const std::string& in_ip, const unsigned short in_port){
// 建立套接字
m_clientfd = socket(AF_INET, SOCK_STREAM, 0);
if(m_clientfd == -1){
return false;
}
// 向服务端发送连接请求
struct sockaddr_in servaddr;
m_ip = in_ip;
m_port = in_port;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(m_port);
struct hostent* h;
if((h = gethostbyname(m_ip.c_str())) == nullptr){
close();
m_clientfd = -1;
return false;
}
memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
// 以下错误常犯,注意当你想使用全局函数或者全局同名变量的时候 要在前面+::
if(::connect(m_clientfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) == -1){
close();
m_clientfd = -1;
return false;
}
return true;
}

//为什么不用const char * 因为const char * 无法支持string,而下列可以支持从const char *
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){
//如果直接操作string对象的内存,必须保证1)不能越界,2)操作后手动设置数据大小
buffer.clear();
buffer.resize(maxlen);
int readn=::recv(m_clientfd, &buffer[0], buffer.size(), 0);
//-1表示失败,0表示断开连接,成功放回数据大小
if(readn <= 0) {
buffer.clear();
return false;
}
buffer.resize(readn); //重置buffer的实际大小
return true;
}

bool close(){
if(m_clientfd == -1) return false;
::close(m_clientfd);
m_clientfd = -1;
return true;
}

ctcpclient():m_clientfd(-1){}

~ctcpclient(){close();}
};

int main(int argc, char* argv[]){
if(argc != 3){
std::cout << "Using:./client 服务端IP地址 服务端的端口\n Example:./client 192.168.168.1 5005\n\n";
return -1;
}
ctcpclient tcpclient;
if(!tcpclient.connect(argv[1], atoi(argv[2]))){
perror("connect");
return -1;
}
//第三步 与服务端通信,发送一个请求报文后等待服务端的回复,收到下一个回复后,再发下一个请求报文
std::string buffer;
for(int i = 0; i < 10; i ++){
buffer = "这是第" + std::to_string(i + 1) + "个小智鞭, 编号" + std::to_string(i + 1) + "。";
if(!tcpclient.send(buffer)){
perror("发送");
return -1;
}
std::cout << "发送:" << buffer << std::endl;
// 接收服务端的回应报文,如果没有回应报文,recv()函数将阻塞等待。
if(!tcpclient.recv(buffer, 1024)){//接收函数存入buffer
perror("recv");
break;
}
std::cout << "接收:" << buffer << std::endl;
sleep(1);
}
// 第四步 关闭socket
tcpclient.close();
return 0;
}

封装socket的服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in需要
#include <netdb.h>//hostent需要
#include <arpa/inet.h>

// accept的过程理解为受理客户端的链接(从已连接的客户端中取出一个客户端),一个服务端可以和很多客户端通信
//如果没有已连接的客户端,accept将阻塞
class ctcpserver{
private:
int m_listenfd; //监听的socket,-1表示未初始化
int m_clientfd; //客户端练上来的socket,-1表示未初始化
unsigned short m_port;//服务器用于通信的端口
std::string m_clientip; //客户端的IP
public:
ctcpserver():m_clientfd(-1), m_listenfd(-1){}
//创建监听socket,并初始化它
bool initserver(const unsigned short in_port){
//第一步创建socket
if((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false;
// 第二部 将ip地址和端口绑在socket上
m_port = in_port;
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(m_port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(m_listenfd, (const sockaddr *)&servaddr, sizeof(servaddr)) == -1){
close(m_listenfd);
m_listenfd = -1;
return false;
}

//第三步打开监听状态
if(listen(m_listenfd, 5) == -1){
close(m_listenfd);
m_listenfd = -1;
return false;
}
return true;
}

bool accept(){
struct sockaddr_in caddr; // 客户端的地址信息
socklen_t addrlen = sizeof(caddr); // 上面的大小
//accept,三个参数第一个为监听socket的fd值,第二个为ip地址的地址,第三个为ip地址大小的地址
if((m_clientfd = ::accept(m_listenfd, (struct sockaddr*)&caddr, &addrlen)) == -1) return false;
m_clientip = inet_ntoa(caddr.sin_addr); //把客户端的地址从大端序转换成字符串
return truei;
}

const std::string & clientip() const{
return m_clientip;
}

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;
}

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);//重置大小
return true;
}
// 以下要分开两个成员函数 不能写在一个函数里面

bool closelisten(){
if(m_listenfd == -1) return false;

::close(m_listenfd);
m_listenfd = -1;
return true;
}

bool closeclient(){
if(m_clientfd == -1) return false;

::close(m_clientfd);
m_clientfd = -1;
return true;
}

// 关闭socket资源
~ctcpserver(){
closelisten();
closeclient();
}
};


int main(int argc, char* argv[]){
if(argc != 2){
std::cout << "Using:./server 通讯端口\n Example:./server 5005\n\n";
std::cout << "注意:运行服务端的程序Linux系统的防火墙必须要开通5005端口\n";
std::cout << "如果是云服务器,还有开通云平台的访问策略";
return -1;
}
ctcpserver tcpserver;
if(!tcpserver.initserver(atoi(argv[1]))){// 初始化服务端用于监听的socket
perror("initserver");
return -1;
}
//第四步:受理客户端的链接请求,如果没有客户端练上来,accept()将阻塞等待
if(!tcpserver.accept()){
perror("accept");
return -1;
}
std::cout << "客户端已连接( " << tcpserver.clientip() << ")。\n";

//第五步:接收客户端发来的信息,并发送回应报文
std::string buffer;
while(true){

//接收客户端的请求,没有哦请求就阻塞recv()等待
//如果客户端已断开连接,recv函数将返回0
if(!tcpserver.recv(buffer, 1024)){
perror("recv");
break;
}
std::cout << "接收" << buffer << std::endl;

buffer = "ok";
if(!tcpserver.send(buffer)){
perror("send");
break;
}
std::cout << "发送:" << buffer << std::endl;
}

return 0;
}

多进程的服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/*
程序名:server2.cpp 是用来演示多进程的socket服务端
*/
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in需要
#include <netdb.h>//hostent需要
#include <arpa/inet.h>
#include <signal.h>

void FatherEXIT(int sig);
void ChildEXIT(int sig);


// accept的过程理解为受理客户端的链接(从已连接的客户端中取出一个客户端),一个服务端可以和很多客户端通信
//如果没有已连接的客户端,accept将阻塞
class ctcpserver{
private:
int m_listenfd; //监听的socket,-1表示未初始化
int m_clientfd; //客户端练上来的socket,-1表示未初始化
unsigned short m_port;//服务器用于通信的端口
std::string m_clientip; //客户端的IP
public:
ctcpserver():m_clientfd(-1), m_listenfd(-1){}
//创建监听socket,并初始化它
bool initserver(const unsigned short in_port){
//第一步创建socket
if((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false;
// 第二部 将ip地址和端口绑在socket上
m_port = in_port;
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(m_port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(m_listenfd, (const sockaddr *)&servaddr, sizeof(servaddr)) == -1){
close(m_listenfd);
m_listenfd = -1;
return false;
}

//第三步打开监听状态
if(listen(m_listenfd, 5) == -1){
close(m_listenfd);
m_listenfd = -1;
return false;
}
return true;
}

bool accept(){
struct sockaddr_in caddr; // 客户端的地址信息
socklen_t addrlen = sizeof(caddr); // 上面的大小
//accept,三个参数第一个为监听socket的fd值,第二个为ip地址的地址,第三个为ip地址大小的地址
if((m_clientfd = ::accept(m_listenfd, (struct sockaddr*)&caddr, &addrlen)) == -1) return false;
m_clientip = inet_ntoa(caddr.sin_addr); //把客户端的地址从大端序转换成字符串
return true;
}

const std::string & clientip() const{
return m_clientip;
}

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;
}

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);//重置大小
return true;
}
// 以下要分开两个成员函数 不能写在一个函数里面

bool closelisten(){
if(m_listenfd == -1) return false;

::close(m_listenfd);
m_listenfd = -1;
return true;
}

bool closeclient(){
if(m_clientfd == -1) return false;

::close(m_clientfd);
m_clientfd = -1;
return true;
}

// 关闭socket资源
~ctcpserver(){
closelisten();
closeclient();
}
};

ctcpserver tcpserver;

int main(int argc, char* argv[]){
if(argc != 2){
std::cout << "Using:./server 通讯端口\n Example:./server 5005\n\n";
std::cout << "注意:运行服务端的程序Linux系统的防火墙必须要开通5005端口\n";
std::cout << "如果是云服务器,还有开通云平台的访问策略";
return -1;
}

// 忽视全部的信号,不希望被打扰,顺便解决了僵尸进程的问题
for(int i = 0; i < 65; i ++) signal(i, SIG_IGN);

//设置信号,在shell状态下,可用kill 进程号 或者 Cirl + c 正常终止这些进程
// 但请不要用 kill -9 + 进程号 强行终止
signal(SIGTERM, FatherEXIT); //15信号,这行的代码意思是自己规定一个信号处理函数
signal(SIGINT, FatherEXIT);// 2信号

if(!tcpserver.initserver(atoi(argv[1]))){// 初始化服务端用于监听的socket
perror("initserver");
return -1;
}
while(true){

//第四步:受理客户端的链接请求,如果没有客户端练上来,accept()将阻塞等待
if(!tcpserver.accept()){
perror("accept");
return -1;
}

int pid = fork();

//系统资源不足时
if(pid == -1){
perror("fork");
return -1;
}

// 父进程返回到循环开始的位置,继续受理客户端的链接
if(pid > 0) {
//父进程不需要客户端连接socket
//tcpserver.closeclient();
continue;
}

// 子进程负责与客户端进行通讯
// 子进程不需要监听窗口
// tcpserver.closelisten();
signal(SIGTERM, ChildEXIT);//规定一个子进程退出信号
signal(SIGINT, SIG_IGN);//子进程不需要捕获SIGINT信号

std::cout << "客户端已连接( " << tcpserver.clientip() << ")。\n";

//第五步:接收客户端发来的信息,并发送回应报文
std::string buffer;
while(true){

//接收客户端的请求,没有哦请求就阻塞recv()等待
//如果客户端已断开连接,recv函数将返回0
if(!tcpserver.recv(buffer, 1024)){
perror("recv");
break;
}
std::cout << "接收" << buffer << std::endl;
buffer = "ok";
if(!tcpserver.send(buffer)){
perror("send");
break;
}
std::cout << "发送:" << buffer << std::endl;
}
}
return 0;
}

void FatherEXIT(int sig){
// 以下代码是为了防止信号处理函数在执行的过程中被信号再次中断
signal(SIGTERM, SIG_IGN);
signal(SIGINT, SIG_IGN);
std::cout << "父进程" << getpid() << "正在退出, sig=" << sig << std::endl;
kill(0,SIGTERM);
std::cout << "子进程" << getpid() << "正在释放父进程的资源以及全局资源" << std::endl;
// 这里些释放资源的代码
tcpserver.closelisten(); // 关闭监听socket
exit(0);
}

void ChildEXIT(int sig){
// 以下代码是为了防止信号处理函数在执行的过程中被信号再次中断
signal(SIGTERM, SIG_IGN);
signal(SIGINT, SIG_IGN);
std::cout << "子进程" << getpid() << "正在退出, sig=" << sig << std::endl;
std::cout << "子进程" << getpid() << "正在释放子进程的资源" << std::endl;
// 这里些释放资源的代码
tcpserver.closeclient(); // 关闭客户端连接socket

exit(0);

}
  • 注意一定要释放资源,在子进程中关闭监听socket,在父进程中关闭 客户端连接socket,杀死进程时一定要释放资源

void * 是什么?

  • void * 是一个跳跃力(+1 后跨越的幅度)未定的指针
    这就是它的神奇之处了,我们可以自己控制在需要的时候将它实现为需要的类型。这样的好处是:编程时候节约代码,实现泛型编程。
  • void 是一种指针类型,常用在函数参数、函数返回值中需要兼容不同指针类型的地方。我们可以将别的类型的指针无需强制类型转换的赋值给void类型。也可以将void *强制类型转换成任何别的指针类型,至于强转的类型是否合理,就需要我们程序员自己控制了。

文件传输

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
/*
程序名:server3.cpp 是用来演示文件传输的多进程的socket服务端
*/
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in需要
#include <netdb.h>//hostent需要
#include <arpa/inet.h>
#include <signal.h>
#include <fstream>

void FatherEXIT(int sig);
void ChildEXIT(int sig);



// accept的过程理解为受理客户端的链接(从已连接的客户端中取出一个客户端),一个服务端可以和很多客户端通信
//如果没有已连接的客户端,accept将阻塞
class ctcpserver{
private:
int m_listenfd; //监听的socket,-1表示未初始化
int m_clientfd; //客户端练上来的socket,-1表示未初始化
unsigned short m_port;//服务器用于通信的端口
std::string m_clientip; //客户端的IP
public:
ctcpserver():m_clientfd(-1), m_listenfd(-1){}
//创建监听socket,并初始化它
bool initserver(const unsigned short in_port){
//第一步创建socket
if((m_listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false;
// 第二部 将ip地址和端口绑在socket上
m_port = in_port;
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(m_port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(m_listenfd, (const sockaddr *)&servaddr, sizeof(servaddr)) == -1){
close(m_listenfd);
m_listenfd = -1;
return false;
}

//第三步打开监听状态
if(listen(m_listenfd, 5) == -1){
close(m_listenfd);
m_listenfd = -1;
return false;
}
return true;
}

bool accept(){
struct sockaddr_in caddr; // 客户端的地址信息
socklen_t addrlen = sizeof(caddr); // 上面的大小
//accept,三个参数第一个为监听socket的fd值,第二个为ip地址的地址,第三个为ip地址大小的地址
if((m_clientfd = ::accept(m_listenfd, (struct sockaddr*)&caddr, &addrlen)) == -1) return false;
m_clientip = inet_ntoa(caddr.sin_addr); //把客户端的地址从大端序转换成字符串
return true;
}

const std::string & clientip() const{
return m_clientip;
}

// 发送报文
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;
}


//接受报文
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);//重置大小
return true;
}

//接收文件名称和大小
bool recv(void *buffer, const size_t maxlen){
if(::recv(m_clientfd, buffer, maxlen, 0) <= 0) return false;
return true;
}

// 接收文件内容
bool recvfile(const std::string &filename, const size_t filesize){
std::ofstream fout;
fout.open(filename, std::ios::out);
if(!fout.is_open()) {
std::cout << "打开文件" << filename << "失败。\n";
return false;
}
int totalbytes = 0; //已经接受的文件总字节数
int onread = 0; //本次打算接受的字节数
char buffer[1000]; // 接收文件内容的缓冲区

while(true){
// 计算被你应该就收的字节数
if(filesize - totalbytes > 1000) onread = 1000;
else onread = filesize - totalbytes;

//接收文件内容
if(!recv(buffer, onread)) return false;

// 把接收到的内容写入文件
fout.write(buffer, onread);

//计算已经接收文件的总字节数,如果文件接受完跳出循环
totalbytes += onread;

if(totalbytes == filesize) break;
}
return true;
}
// 以下要分开两个成员函数 不能写在一个函数里面

bool closelisten(){
if(m_listenfd == -1) return false;

::close(m_listenfd);
m_listenfd = -1;
return true;
}

bool closeclient(){
if(m_clientfd == -1) return false;

::close(m_clientfd);
m_clientfd = -1;
return true;
}

// 关闭socket资源
~ctcpserver(){
closelisten();
closeclient();
}
};

ctcpserver tcpserver;

struct st_fileinfo{
char filename[256];
int filesize;
};

int main(int argc, char* argv[]){
if(argc != 3){
std::cout << "Using:./server 通讯端口 文件存放目录\n Example:./server 5005 /tmp\n\n";
std::cout << "注意:运行服务端的程序Linux系统的防火墙必须要开通5005端口\n";
std::cout << "如果是云服务器,还有开通云平台的访问策略";
return -1;
}

// 忽视全部的信号,不希望被打扰,顺便解决了僵尸进程的问题
for(int i = 0; i < 65; i ++) signal(i, SIG_IGN);

//设置信号,在shell状态下,可用kill 进程号 或者 Cirl + c 正常终止这些进程
// 但请不要用 kill -9 + 进程号 强行终止
signal(SIGTERM, FatherEXIT); //15信号,这行的代码意思是自己规定一个信号处理函数
signal(SIGINT, FatherEXIT);// 2信号

if(!tcpserver.initserver(atoi(argv[1]))){// 初始化服务端用于监听的socket
perror("initserver");
return -1;
}
while(true){

//第四步:受理客户端的链接请求,如果没有客户端练上来,accept()将阻塞等待
if(!tcpserver.accept()){
perror("accept");
return -1;
}

int pid = fork();

//系统资源不足时
if(pid == -1){
perror("fork");
return -1;
}

// 父进程返回到循环开始的位置,继续受理客户端的链接
if(pid > 0) {
//父进程不需要客户端连接socket
//tcpserver.closeclient();
continue;
}

// 子进程负责与客户端进行通讯
// 子进程不需要监听窗口
// tcpserver.closelisten();
signal(SIGTERM, ChildEXIT);//规定一个子进程退出信号
signal(SIGINT, SIG_IGN);//子进程不需要捕获SIGINT信号

std::cout << "客户端已连接( " << tcpserver.clientip() << ")。\n";

//以下是服务端的流程
//1)接收文件名和文件大小信息
struct st_fileinfo fileinfo;
memset(&fileinfo, 0, sizeof(fileinfo));
// 用结构体文件来接受发来的文件信息
if(!tcpserver.recv(&fileinfo, sizeof(fileinfo))){
perror("recv");
return -1;
}

std::cout << "收到文件名 (" << fileinfo.filename << ", " << fileinfo.filesize << std::endl;

//2)给客户端回复确认报文(表示客户端可以发文件)
if(!tcpserver.send("ok")){
perror("send");
return -1;
}

std::cout << "发送OK" << std::endl;

//3)接收文件内容
if(!tcpserver.recvfile(std::string(argv[2]) + "/" + fileinfo.filename, fileinfo.filesize)){
std::cout << "接收文件内容失败" << std::endl;
return -1;
}

std::cout << "接收文件内容成功" << std::endl;
//4)给客户端回复确认报文(表示服务端接收成功)
if(!tcpserver.send("ok")){
perror("send");
return -1;
}
std::cout << "文件传输完毕\n";
}
return 0;
}

void FatherEXIT(int sig){
// 以下代码是为了防止信号处理函数在执行的过程中被信号再次中断
signal(SIGTERM, SIG_IGN);
signal(SIGINT, SIG_IGN);
std::cout << "父进程" << getpid() << "正在退出, sig=" << sig << std::endl;
kill(0,SIGTERM);
std::cout << "子进程" << getpid() << "正在释放父进程的资源以及全局资源" << std::endl;
// 这里些释放资源的代码
tcpserver.closelisten(); // 关闭监听socket
exit(0);
}

void ChildEXIT(int sig){
// 以下代码是为了防止信号处理函数在执行的过程中被信号再次中断
signal(SIGTERM, SIG_IGN);
signal(SIGINT, SIG_IGN);
std::cout << "子进程" << getpid() << "正在退出, sig=" << sig << std::endl;
std::cout << "子进程" << getpid() << "正在释放子进程的资源" << std::endl;
// 这里些释放资源的代码
tcpserver.closeclient(); // 关闭客户端连接socket

exit(0);

}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in需要
#include <netdb.h>//hostent需要
#include <string>
#include <fstream>
/*
用来演示文件传输的客户端
*/
class ctcpclient{
public:
int m_clientfd; // 客户端的socket,-1表示未连接或连接已断开,>=0 表示有效的socket
std::string m_ip; // 服务器的IP/域名
unsigned short m_port; // 通讯端口

//向服务端发起连接请求,成功返回true,失败false
bool connect(const std::string& in_ip, const unsigned short in_port){
// 建立套接字
m_clientfd = socket(AF_INET, SOCK_STREAM, 0);
if(m_clientfd == -1){
return false;
}
// 向服务端发送连接请求
struct sockaddr_in servaddr;
m_ip = in_ip;
m_port = in_port;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(m_port);
struct hostent* h;
if((h = gethostbyname(m_ip.c_str())) == nullptr){
close();
m_clientfd = -1;
return false;
}
memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
// 以下错误常犯,注意当你想使用全局函数或者全局同名变量的时候 要在前面+::
if(::connect(m_clientfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) == -1){
close();
m_clientfd = -1;
return false;
}
return true;
}

//为什么不用const char * 因为const char * 无法支持string,而下列可以支持从const char *
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;
}

//用于发送文件结构体
bool send(void *buffer, const size_t maxlen){
if(m_clientfd == -1) return false;
if(::send(m_clientfd, buffer, maxlen, 0) <= 0) return false;
return true;
}

bool sendfile(const std::string &filename, const size_t filesize){
// 以二进制形式打开文件
std::ifstream fin(filename, std::ios::in);
if(fin.is_open() == false){
std::cout << "打开文件" << filename << "失败.\n";
return false;
}
int onread = 0; //每次调用fin.read()打算读取的字节数
int totalbytes = 0;// 从文件中已经读取的字节总数
char buffer[1000]; // 存放读取数据的buffer

while(true){
memset(buffer, 0, sizeof(buffer));

// 计算本次应该读取的字节数,如果剩余超过1000字节,就读1000字节
if(filesize - totalbytes > 1000) onread = 1000;
else onread = filesize - totalbytes;

// 从文件中读取数据
fin.read(buffer, onread);

// 把读取到的数据发送给对端
if(!send(buffer, onread)) return false;

// 计算文件已经读取的字节总个数,如果文件已经读完,跳出循环
totalbytes += onread;

if(totalbytes == filesize) break;
}
return true;
}

// 接收服务端的报文,成功返回true,失败返回false
//buffer存放接收到的报文内容,maxlen本次接收报文的长度
bool recv(std::string& buffer,const size_t maxlen){
//如果直接操作string对象的内存,必须保证1)不能越界,2)操作后手动设置数据大小
buffer.clear();
std::cout << buffer << std::endl;
buffer.resize(maxlen);
int readn=::recv(m_clientfd, &buffer[0], buffer.size(), 0);
//-1表示失败,0表示断开连接,成功放回数据大小
if(readn <= 0) {
buffer.clear();
return false;
}
buffer.resize(readn); //重置buffer的实际大小
return true;
}

bool close(){
if(m_clientfd == -1) return false;
::close(m_clientfd);
m_clientfd = -1;
return true;
}

ctcpclient():m_clientfd(-1){}

~ctcpclient(){close();}
};

int main(int argc, char* argv[]){
if(argc != 5){
std::cout << "Using:./client 服务端IP地址 服务端的端口, 文件名,文件大小\n";
std::cout << "Example:./client 192.168.168.1 5005 aaa.txt 2424\n\n";
return -1;
}
ctcpclient tcpclient;
if(!tcpclient.connect(argv[1], atoi(argv[2]))){
perror("connect");
return -1;
}

// 以下是发送文件的流程
// 1)把传输的文件名和文件大小告诉服务端

// 定义文件信息结构体
struct st_fileinfo{
char filename[256];
int filesize;
}fileinfo;
memset(&fileinfo, 0, sizeof(fileinfo));
strcpy(fileinfo.filename, argv[3]);//文件名
fileinfo.filesize = atoi(argv[4]);//文件大小
//把文件信息的结构题发送给服务端
if(!tcpclient.send(&fileinfo, sizeof(fileinfo))){
perror("send");
return -1;
}
std::cout << "发送文件信息的结构体" << fileinfo.filename << "(" << fileinfo.filesize << ")" << std::endl;

//2)等待服务端的确认报文(对文件名和文件的大小的确认)
std::string buffer;
if(!tcpclient.recv(buffer, 1024)){
perror("recv");
return -1;
}
if(buffer != "ok") std::cout << "服务端没有回复ok\n" << std::endl;
// 3)发送文件内容
if(!tcpclient.sendfile(fileinfo.filename, fileinfo.filesize)){
perror("sendfile");
return -1;
}
// 4)等待服务端的确认报文(服务端已接受完文件)
if(!tcpclient.recv(buffer, 2)){
perror("recv");
return -1;
}
if(buffer != "ok") {
std::cout << "发送文件内容失败。\n";
return -1;
}
return 0;
}

三次握手,四次挥手

  • yum install net-tools -y; 下载工具包
  • netstat -na|less 查看套接字状态

三次握手

示例图片

示例图片

细节
  • 客户端也有端口号,对于程序员来说,可以不必关心客户端的端口号,所以系统随机分配(socket 通讯中的地址包括ip和端口号,但是习惯地址仅仅指ip地址)
  • 服务端的bind函数,普通用户必须是使用1024以上端口,root任意
  • listen函数第二个参数+1为已连接队列的大小,一次能接受多少
  • SYN_RECV状态的连接也称半连接
  • CLOSED是假象状态,实际不存在

四次挥手

示例图片

示例图片

细节
  • 主动断开短端在四次挥手后,socket的状态为 TIME_WAIT状态,将持续2MSL(30s/1min/2min)MSL表示报文在网路中存在的最长时间
  • 如果客户端主动断开连接,TIME_WAIT状态几乎不会造成危害
    1. 客户端程序的socket很少
    2. 客户端的窗口是随机分配的,不存在重用问题
  • 如果客户端主动断开,有两方面的危害:1)socket没有立即释放;2)端口后只能在2MSL后才能重用
    也就是会报这个错误:
    示例图片
解决上述问题办法

在服务端程序中,用setsockopt()函数设置socket的属性(一定要放在bind()前)

1
2
3
// TIME_WAIT状态还在,但是仍然可以打开
int opt = 1;
setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));

TCP缓存

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

示例图片

查看缓冲区的大小

1
2
3
4
5
6
7
8
9
10
int bufsize = 0;
socklen_t optlen = sizeof(bufsize);

gersockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, &optlen);
std::cout << "Send bufsize=" << buffer << std::endl;

getsockopt(sockfd, SOL_SOCKET,SO_RCVBUF,&bufsizem &optlen);

std::cout << "recv bufsize =" << bufsize << std::endl;

可能遇到的问题:

  1. send()函数可能会阻塞吗?

    会,当发送缓冲区满了和服务端接收满了 就会

  2. 向socket中写入数据后,如果关闭了socket,对端还能接收到数据吗?

    能,因为这些数据已经存入了对端的接收缓存区中了,即使关闭了socket,之前发送的数据只要还在缓冲区中就能接受

Nagle 算法

示例图片

示例图片

示例图片

IO多路复用 -select模型

示例图片

什么是IO多路复用

  • 用一个进/线程处理多个TCP连接,减少系统开销
  • 三种模型:select(1024), poll(数千)和epoll(百万)

括号中表示其处理TCP连接的数量

网络通讯-读事件

  • 已连接队列中有已经准备好的socket(有新的客户端连上来)
  • 接收缓存中有数据可以读
  • tcp连接已断开(对端关闭了链接)

网络通讯-写事件

  • 发送缓冲区没有满,可以写入数据(可以向对端发送报文).

select 的原理

select之于服务器server,相当于秘书之于老板,select是由内核提供的。之前没有select时,server要一直accept()阻塞,等待有客户端的连接。

有了select之后,相当于给各个客户端留电话,谁有事就给秘书打电话,然后秘书告诉老板去调用accept(),创建cfd,和客户端建立连接。

select自己不会创建出cfd,与客户端连接。

所以select做的就是监听,lfd。其实请求连接本质是一个读事件,只是读到的数据是连接请求

示例图片

alt text

select 函数的使用

select函数的声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//头文件 
#include <sys/select.h>
//成功返回大于0的数,失败返回-1;这个大于0的数是发生事件的文件描述符数,返回0就是没有哪个文件描述符有事件
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

// 用于从bitmap中删除socket
void FD_CLR(int fd, fd_set *set);

//用于询问socket 是否存在于bitmap
int FD_ISSET(int fd, fd_set *set);

//用于将socket 存入bitmap里
void FD_SET(int fd, fd_set *set);

// 用于初始化bitmap(都置为0)
void FD_ZERO(fd_set *set);

//fd_set是一个集合,fd_set的size值在linux下一般被定义为1024,意思是select管理的文件描述符数量不能大于1024,继而文件描述符取值为0~1023

//readset、 writeset、exceptset分别对应文件描述符的三种事件,分别是读事件,写事件,异常事件
//timeout是设置的超时时间,防止陷入无限阻塞

fd_set的本质如图所示

是一个bitmap(共1024位)
示例图片

selec服务吨端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/*
此程序用于演示采用select模型实现网络通讯的服务器
*/
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>

//初始化服务器的监听端口
int initserver(int port);

int main(int argc, char * argv[]){
if(argc != 2){
std::cout << "usage:./tcpselect port\n";
return -1;
}

//初始化服务端用于监听的socket
int listensock = initserver(atoi(argv[1]));
std::cout << "listensock= " << listensock << std::endl;

if(listensock < 0){
std::cout << "initserver() failed.\n";
return -1;
}



fd_set readfds; //需要监视读事件的socket的集合,大小为16字节(1024位)的位图(bitmap)
FD_ZERO(&readfds); //初始化readfds,把bitmap每位设置为0
FD_SET(listensock, &readfds); //把服务端用于监控的socket加入readfds

int maxfd = listensock; //readfds中socket的最大值

while(true){
struct timeval timeout; //用于表示超时间的结构体
timeout.tv_sec = 10;
timeout.tv_usec = 0;

// 在select函数中,会修改bitmap 所以要复制一份给tmpfds,在将tmpfds传回给select
fd_set tmpfds = readfds;

//select()等待事件的发生(监视哪些socket发生了事件), 第三个为写事件的bitmap,四个为检测异常事件(可以普通IO)的bitmap
int infds = select(maxfd + 1, &tmpfds, nullptr, nullptr, &timeout);

//如果infds < 0 表示用于表示调用select失败
if(infds < 0){
perror("select() failed");
break;
}

// 如果infds == 0 表示select()超时
if(infds == 0){
perror("timeout");
continue;
}

//如果infds>0 表示有事件发生,infds存放了已发生事件的个数
for(int eventfd = 0; eventfd <= maxfd; eventfd ++){
//如果eventfd在bitmap中标志为0, 表示它没有事件,continue
if(FD_ISSET(eventfd, &tmpfds) == 0) continue;

//如果发生事件的是listensock,表示已连接队列中有已经准备好的socket(有新的客户端连上来了)
if(eventfd == listensock){
sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock, (sockaddr *)&client, &len);
if(clientsock < 0){
perror("accept() failed");
continue;
}
printf("accept client(socket = %d)pk.\n", clientsock);

// 把bitmap中新连上来的客户端标志位置为1
FD_SET(clientsock, &readfds);

//更新maxfd的值,因为如果建立了连接描述符会建立在之前建完后的下一位,不管那个描述符是否被删除
if(maxfd < clientsock) maxfd = clientsock;
}
else{
//如果是客户端连接的socket有事件,表示接收缓存中有数据可以读,或者客户端已断开连接
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
if(recv(eventfd, buffer, sizeof(buffer), 0) <= 0){
//如果客户端的链接已经断开
printf("client(eventfd = %d) disconnected.\n", eventfd);

close(eventfd);

FD_CLR(eventfd, &readfds);

//重新计算maxfd的值,注意只有当eventfd==maxfd时才需要计算
if(eventfd == maxfd){
for(int i = maxfd; i > 0; i --){ //从后往前找
if(FD_ISSET(i,&readfds)){
maxfd = i;
break;
}
}
}
}
else{
//如果客户端有报文发过来
printf("recv(eventfd=%d)%s\n", eventfd, buffer);

//把接收到的报文内容原封不动的发回去
send(eventfd, buffer, strlen(buffer), 0);
}
}
}
}
}

//初始化服务端的监听端口
int initserver(int port){
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket() failed");
return -1;
}

int opt = 1;
unsigned int len = sizeof(opt);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,&opt,len);

sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock, (sockaddr *)&servaddr, sizeof(servaddr)) < 0){
perror("bind() failed");
close(sock);
return -1;
}

if(listen(sock, 5) != 0){
perror("listen() failed");
close(sock);
return -1;
}

return sock;
}

select模型的细节

select模型-写事件
  1. 如果tcp的发送缓冲区没有满,那么, socket连接是可写的
  2. 如果发送数据量太大,或网络带宽不够,发送缓冲区就有填满的可能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/*
此程序用于演示采用select模型实现网络通讯的服务器
*/
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>

//初始化服务器的监听端口
int initserver(int port);

int main(int argc, char * argv[]){
if(argc != 2){
std::cout << "usage:./tcpselect port\n";
return -1;
}

//初始化服务端用于监听的socket
int listensock = initserver(atoi(argv[1]));
std::cout << "listensock= " << listensock << std::endl;

if(listensock < 0){
std::cout << "initserver() failed.\n";
return -1;
}



fd_set readfds; //需要监视读事件的socket的集合,大小为16字节(1024位)的位图(bitmap)
FD_ZERO(&readfds); //初始化readfds,把bitmap每位设置为0
FD_SET(listensock, &readfds); //把服务端用于监控的socket加入readfds

int maxfd = listensock; //readfds中socket的最大值

while(true){
struct timeval timeout; //用于表示超时间的结构体
timeout.tv_sec = 10;
timeout.tv_usec = 0;

// 在select函数中,会修改bitmap 所以要复制一份给tmpfds,在将tmpfds传回给select
fd_set tmpfds = readfds;
fd_set tmpfds1 = readfds;//用于监视写事件

//select()等待事件的发生(监视哪些socket发生了事件), 第三个为写事件的bitmap,四个为检测异常事件(可以普通IO)的bitmap
// 该函数调用后 会修改bitmap,把为准备好的置为0,所以得传入拷贝
int infds = select(maxfd + 1, &tmpfds, &tmpfds1, nullptr, &timeout);

//如果infds < 0 表示用于表示调用select失败
if(infds < 0){
perror("select() failed");
break;
}

// 如果infds == 0 表示select()超时
if(infds == 0){
perror("timeout");
continue;
}

//判断是否有写事件
for(int eventfd = 0; eventfd <= maxfd; eventfd ++){
//如果eventfd在bitmap中标志为0, 表示它没有事件,continue
if(FD_ISSET(eventfd, &tmpfds) == 0) continue;

printf("eventfd=%d可以写。\n", eventfd);
}

//如果infds>0 表示有事件发生,infds存放了已发生事件的个数
for(int eventfd = 0; eventfd <= maxfd; eventfd ++){
//如果eventfd在bitmap中标志为0, 表示它没有事件,continue
if(FD_ISSET(eventfd, &tmpfds) == 0) continue;

//如果发生事件的是listensock,表示已连接队列中有已经准备好的socket(有新的客户端连上来了)
if(eventfd == listensock){
sockaddr_in client;
socklen_t len = sizeof(client);
// accept 函数调用的时候, 会把客户端的值存储在client 里
int clientsock = accept(listensock, (sockaddr *)&client, &len);
if(clientsock < 0){
perror("accept() failed");
continue;
}
printf("accept client(socket = %d)pk.\n", clientsock);

// 把bitmap中新连上来的客户端标志位置为1
FD_SET(clientsock, &readfds);

//更新maxfd的值,因为如果建立了连接描述符会建立在之前建完后的下一位,不管那个描述符是否被删除
if(maxfd < clientsock) maxfd = clientsock;
}
else{
//如果是客户端连接的socket有事件,表示接收缓存中有数据可以读,或者客户端已断开连接
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
if(recv(eventfd, buffer, sizeof(buffer), 0) <= 0){
//如果客户端的链接已经断开
printf("client(eventfd = %d) disconnected.\n", eventfd);

close(eventfd);

FD_CLR(eventfd, &readfds);

//重新计算maxfd的值,注意只有当eventfd==maxfd时才需要计算
if(eventfd == maxfd){
for(int i = maxfd; i > 0; i --){ //从后往前找
if(FD_ISSET(i,&readfds)){
maxfd = i;
break;
}
}
}
}
else{
//如果客户端有报文发过来
printf("recv(eventfd=%d)%s\n", eventfd, buffer);

//把接收到的报文内容原封不动的发回去
send(eventfd, buffer, strlen(buffer), 0);
}
}
}
}
}

//初始化服务端的监听端口
int initserver(int port){
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket() failed");
return -1;
}

int opt = 1;
unsigned int len = sizeof(opt);
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,&opt,len);

sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock, (sockaddr *)&servaddr, sizeof(servaddr)) < 0){
perror("bind() failed");
close(sock);
return -1;
}

if(listen(sock, 5) != 0){
perror("listen() failed");
close(sock);
return -1;
}

return sock;
}
select 模型 水平触发
  • select()监视的socket如果发送了事件就会立即返回(通知应用程序处理事件),如果发现数据一次性没有处理完,就会再发通知来处理,也就是说原本一次的,用了三次
    示例图片

  • 如果事件没有被处理,再次调用select()的时候会立即再通知(因某种原因休眠了)

select 模型性能
  • 1s大概能处理十二万个业务请求
存在的问题
  1. 采用轮询方式扫描bitmap,性能随着socket数量增多而下降
  2. 调用一次就需要拷贝一份bitmap
  3. bitmap的大小由FD_SETSIZE宏设置,默认是1024个,可以修改但是效率更低

POLL模型

pollfd 结构体

这个结构体有三个成员,第一个为socket,第二个为请求事件,第三个为返回的事件,并且修改的时候只会修改第三个成员。
示例图片

poll可要求socket数

其不是固定的,可以由程序员自己决定

pollfd结构体数组初始化的细节

不能用memset 而得用循环
示例图片
poll 对socket值为-1的会忽略

poll的事件常用

读事件:POLLIN, 写事件:POLLOUT

如果即想读又想写可以使用|来连接

1
2
3
//listensock 是用于监听的socket符
pollfd fds[2048];
fds[listensock].fd = POLLIN|POLLOUT

pollfd结构体的使用方法有两种

填在与请求的socket符号编码一样的位置

示例图片

填在结构体数组最前面

如下图的三行
示例图片

两者的区别

第一个对于编码来说比较方便,第二个对数组利用率更高

poll模型存在的问题

示例图片

poll相对于select的区别

  • 函数功能实现过程

(1)应用程序通过poll函数的pollfd结构体将感兴趣的文件描述符和事件类型

(2)调用poll系统调用,将pollfd类型的结构体数组拷贝给内核空间,内核程序采用轮询方式查找所传入的数组中的就绪文件描述符,将内核处理之后的结果通过revents带回,这里的revents中不但有就绪的文件描述符,也有未就绪的,比select有优势的地方是内核并没有直接在传进的数组中修改状态,而是将传入的数组拷贝一份进行修改并由revents带回,所以下一次调用时不需要重新设置要监听的文件描述符;但是采用轮询方式时间复杂度为O(n)

(3)应用程序接受到内核返回的pollfd之后,同样采用轮询方式将events与revents遍历查找,时间复杂度为O(n),找出就绪的文件描述符

poll相对于select的优势

poll所能监听的文件描述符数达到系统允许打开的最大文件描述符数,而select由于_FD_SETSIZE的限制只能达到1023个

poll函数的缺点

(1)应用程序和内核程序都采用轮询方式查找就绪文件描述符,时间复杂度为O(n),

(2)poll仍需要每次调用将文件描述符数组拷贝一份给内核空间

(3)poll之工作在效率低下的LT模式下,在未处理就绪事件时会一直提醒就绪信息

alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/*
用于演示采用poll模型实现网络通讯的服务端
*/
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>

//初始化服务端的监听端口
int initserver(int port);

int main(int argc, char *argv[]){
if(argc != 2){
printf("usage:./tcppoll port\n");
return -1;
}

// 初始化服务端用于监听的窗口
int listensock = initserver(atoi(argv[1]));
std::cout << "listensock=" << listensock << std::endl;

if(listensock < 0){
perror("initserver failed");
}

// 用于存放需要监视的socket
pollfd fds[2048];

// 初始化数组,把全部socket都设置为-1, poll会忽略-1
for(int i = 0; i < 2048; i ++){
fds[i].fd = -1;
}

// 打算让poll监视listensock 读事件
fds[listensock].fd = listensock;
//读事件POLLIN 写事件POLLOUT
fds[listensock].events = POLLIN;

int maxfd = listensock; // 需要监听的实际大小

while(true){
//调用poll等待事件的发生(监听哪些socket发生了事件)
int infds = poll(fds, maxfd + 1, 10000); // 超时时间10s

//poll 失败
if(infds < 0){
perror("poll failed");
break;
}

// poll 超时
else if(infds == 0){
perror("poll timeout");
continue;
}

// poll正常运行
for(int eventfd = 0; eventfd <= maxfd; eventfd ++){
if(fds[eventfd].fd < 0) continue; //fd 为-1 就忽略
//没有读时间就continue
if(fds[eventfd].events & POLLIN == 0) continue;
//如果发生的事件是listensock 表示已连接队列中已经有准备好的socket(有新的客户端练上来)
if(eventfd == listensock){
sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(sockaddr *) &client, &len);
if(clientsock < 0){
perror("accept(*) failed");
continue;
}
printf("accept client(socket = %d)ok.\n", clientsock);

// 修改fds数组中clientsock位置的元素
fds[clientsock].fd = clientsock;
fds[clientsock].events = POLLIN;

if(maxfd < clientsock) maxfd = clientsock; // 更新maxfd
}
else{
// 如果是客户端连接的socket有事件目标是有报文发过来或者连接已断开

char buffer[1024];
memset(buffer, 0, sizeof(buffer));
if(recv(eventfd, buffer, sizeof(buffer), 0) <= 0){
// 如果客户端的链接已断开
printf("client(eventfd = %d) disconnected.\n", eventfd);

//关闭客户端的socket
close(eventfd);
fds[eventfd].fd = -1;

//重新计算maxfd的值,只有当eventfd等于maxfd才开始计算
if(eventfd == maxfd){
for(int i = maxfd; i > 0; i --){
if(fds[i].fd != -1){
maxfd = i;
break;
}
}
}
}
else{
// 如果客户端有报文发过来
printf("recv(eventfd = %d).%s\n", eventfd,buffer);
//把接收到的报文内容原封不动的发回去
send(eventfd, buffer, strlen(buffer), 0);
}
}
}
}
}

//初始化服务端的监听端口
int initserver(int port){

int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket() failed");
return -1;
}

// TIME_WAIT状态还在,但是仍然可以打开,用来打开/关闭地址复用功能,1为打开,0为关闭
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));

sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock, (sockaddr *) &servaddr, sizeof(servaddr)) < 0){
perror("bind() failed");
close(sock);
return -1;
}

if(listen(sock, 5) != 0){
perror("listen() failed");
close(sock);
return -1;
}

return sock;

}

epoll模型

  • epoll实例的创造(一个文件描述符)

epoll_create()函数

这个函数是用来创造epoll的,参数为一个整数,这个参数在Linux2.6.8之后被忽略,只要填大于0就行

1
2
int epollfd = epoll_create(1);

epoll_ctl()函数

epoll_ctl 函数

  • int epoll_ctl(int epfd, int op, int fd, struct - - epoll_event *event);
  • 功能:epoll 的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
  • 参数epfd: epoll 专用的文件描述符,epoll_create()的返回值
  • 参数op: 表示动作,用三个宏来表示:
    EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
  • 参数fd: 需要监听的文件描述符
  • 参数event: 告诉内核要监听什么事件,struct epoll_event 结构如:
    events可以是以下几个宏的集合:
    EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
    EPOLLOUT:表示对应的文件描述符可以写;
    EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    EPOLLERR:表示对应的文件描述符发生错误;
    EPOLLHUP:表示对应的文件描述符被挂断;
    EPOLLET :将 EPOLL 设为边缘触发(Edge Trigger)模式,这是相对于水平触发(Level Trigger)来说的。
    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里
  • 返回值:0表示成功,-1表示失败。

epoll_wait函数

  • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • 功能:等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select() 调用。
  • 参数epfd: epoll 专用的文件描述符,epoll_create()的返回值
  • 参数events: 分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。
  • 参数maxevents: maxevents 告之内核这个 events 有多少个 。
  • 参数timeout: 超时时间,单位为毫秒,为 -1 时,函数为阻塞。
  • 返回值
    如果成功,表示返回需要处理的事件数目
    如果返回0,表示已超时
    如果返回-1,表示失败

epoll的相关结构体

epoll_event
1
2
3
4
5
6
7
8
9
10
11
struct epoll_event{
uint32_t events; //事件
epoll_data_t data; //用户可用数据
}

typedef union epoll_data{
void *ptr;
int fd;
uint32_t;
uint64_t;
}epoll_data_t;

alt text

alt text

  • epoll的视频演示在同级目录下

  • epoll为什么快?

  • 事件触发模式 事件到来唤醒fd的等待队列中的进程,回调进程之前的注册的回调函数ep_poll_callback ,这个回调函数实际上就是将红黑树上收到event的epitem插入到它的就绪队列中并唤醒调用epoll_wait进程。不用逐个遍历
epoll服务端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/*
用于演示采用epoll模型实现网络通讯的服务端
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/fcbtl.h>
#include <sys/epoll.h>

//初始化服务端的监听端口
int initserver(int port);

int main(int argc, char *argv[]){
if(argc != 2){
printf("usage:./tcppoll port\n");
return -1;
}

// 初始化服务端用于监听的窗口
int listensock = initserver(atoi(argv[1]));
std::cout << "listensock=" << listensock << std::endl;

if(listensock < 0){
perror("initserver failed");
}

// 创建epoll句柄
int epollfd = epoll_create(1);

// 初始化数组,把全部socket都设置为-1, poll会忽略-1
for(int i = 0; i < 2048; i ++){
fds[i].fd = -1;
}

//为服务端的listensock准备可读事件
epoll_event ev; //声明事件的数据结构
ev.data.fd = listensock; //指定事件的自定义数据,会随着epoll_wait()返回的事件一并返回
ev.events = EPOLLIN; //打算让epoll监视的listensock的读事件

epoll_ctl(epollfd, EPOLL_CTL_ADD, listensock, &ev); // 把需要监视的socket加入epollfd中
epoll_event evs[10]; //存放epoll返回的事件

while(true){
//等待监视的socket有事件发生
int infds = epoll_wait(epollfd, evs, 10, -1); // 不设置超时时间

//epoll 失败
if(infds < 0){
perror("poll failed");
break;
}

// epoll 超时
else if(infds == 0){
perror("poll timeout");
continue;
}

// epoll正常运行
for(int i = 0; i < infds; i ++){ // 遍历epoll的返回数组evs

//printf("socket=%d,events=%d\n",evs[i].data.fd, evs[i].events);

//如果发生的事件是listensock 表示已连接队列中已经有准备好的socket(有新的客户端练上来)
if(evs[i].data.fd == listensock){
sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(sockaddr *) &client, &len);
if(clientsock < 0){
perror("accept(*) failed");
continue;
}
printf("accept client(socket = %d)ok.\n", clientsock);

// 为新客户端准备可读事件,并添加到epoll中
ev.data.fd = clientsock;
ev.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, clientsock, &ev);
}
else{
// 如果是客户端连接的socket有事件目标是有报文发过来或者连接已断开

char buffer[1024];
memset(buffer, 0, sizeof(buffer));
if(recv(evs[i].data.fd, buffer, sizeof(buffer), 0) <= 0){
// 如果客户端的链接已断开
printf("client(eventfd = %d) disconnected.\n", evs[i].data.fd);

//关闭客户端的socket
close(evs[i].data.fd);
}
else{
// 如果客户端有报文发过来
printf("recv(eventfd = %d).%s\n",evs[i].data.fd,buffer);
//把接收到的报文内容原封不动的发回去,如果接收缓冲区满了就会阻塞
send(evs[i].data.fd, buffer, strlen(buffer), 0);
}
}
}
}
return 0;
}

//初始化服务端的监听端口
int initserver(int port){

int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket() failed");
return -1;
}

// TIME_WAIT状态还在,但是仍然可以打开,用来打开/关闭地址复用功能,1为打开,0为关闭
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));

sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock, (sockaddr *) &servaddr, sizeof(servaddr)) < 0){
perror("bind() failed");
close(sock);
return -1;
}

if(listen(sock, 5) != 0){
perror("listen() failed");
close(sock);
return -1;
}

return sock;

}

阻塞I/O 与 非阻塞I/O

  • 阻塞: 在进/线程中,发起一个调用时,在调用返回前,进/线程会被阻塞等待,等待中的进/线让出CPU的使用权
  • 非阻塞: 在进/线程中,发起一个调用时,会立即返回
  • 会阻塞的四个函数:connect(), accept(), send(), recv()

阻塞与非阻塞IO的应用场景

  • 在传统的网络服务端程序中(每连接每线/进程), 采用阻塞IO
  • 在IO复用模型中 是采用非阻塞IO,在事件循环中是不能被阻塞的

非阻塞IO-connect()函数

  • 对非阻塞的IO调用connect()函数,不管是否能连接成功,connect()都会立即返回失败,errno == EINPROGRESS
  • 对非阻塞的IO调用connect()函数后,如果socket的状态是可写的,证明连接是成功的,否则是失败的

对于上面两句话的详细解释

  • 首先connect()函数是发生在客户端的,当它使用以下方法变为非阻塞模式的时候,不管连接成功与否都会失败

  • 以下代码是将此套接字设置为非阻塞状态
    -

1
2
3
4
5
6
7
8
9
int setnonblocking(int fd){
int flags;

// 获取fd的状态
if(flags = fcntl(fd, F_GETFL, 0) == -1){
flags = 0;
}
return fcntl(fd, F_SETTFL, flags|O_NONBKLOCK);
}
  • 这样的话怎么判断是否连接成功呢?
  • 需要在连接的地方加入一个判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(connect(sockfd, (sockaddr *)&servaddr, sizeof(servaddr)) != 0){
if(errno!=EINPROGRESS){
printf("connect(%s:%s)failed.\n", argv[1], argv[2]);
close(sockfd);
return -1;
}
}
// 再看socket是否可写
pollfd fds;
fds.fd = sockfd;
fds.events = POLLOUT;
poll(&fds, 1, -1);
if(fds.revents == POLLOUT) printf("connect ok\n");
else printf("connect failed\n");

非阻塞IO accept()函数

  • 对非阻塞的IO调用accept(), 如果已连接队列中没有socket, 函数立即返回失败, errno == EAGAIN;
1
2
setnonblocking(listensock);

非阻塞IO recv

  • 对非阻塞的IO调用recv(),如果没有数据可读(接收缓冲区为空), 函数立即返回失败, errno == EAGAIN

非阻塞IO send()

  • 对非阻塞IO调用send(), 如果socket 不可写(发送缓冲区已满), 函数立即返回失败,errno==EAGAIN

水平触发与边缘触发

  • select 和 poll 采用 水平触发
  • epoll 既有 水平触发 又有 边缘触发, 默认水平触发

水平触发含义

  • 读事件:如果epoll_wait触发了读事件,表示有数据可读,如果程序没有把数据读完,再次调用epoll_wait的时候,将立即再次触发读事件
  • 写事件: 如果发送缓冲区没有满,表示可以写入数据,此时如果再次调用epoll_wait的时候,将立即再次触发写事件

边缘触发的含义

  • 对于读事件:epoll_wait触发读事件后,不管程序有没有处理读事件,epoll_wait不会再触发读事件,只有新数据到达才会触发,也就是说数据从无到有或者从少到多的时候,会通知,类似菜鸟驿站
  • 对于写事件:epoll_wait触发读事件后,如果发送缓冲区没满,epoll_wait不会再触发写事件, 只用当发送缓冲区由变成不满时,才再次触发写事件

  • 如果epoll使用边缘触发一定要设置为非阻塞模式

  • 有新客户端连接情况
    示例图片

  • 对于接收发送报文的时候如果使用边缘触发要这么编码,因为这样才能读完

示例图片

1
2
//将epoll设置为边缘触发
ev.events = EPOLLOUT|EPOLLET;

示例图片