• TCP/IP网络编程(8) 基于Linux的多进程服务器


    1. Linux下的多进程服务器

    1.1 进程的概念及应用

    并发服务器实现的模型和方法:

    • 多进程服务器   (通过创建多个进程提供服务)
    • 多路复用服务器   (通过捆绑并统一管理IO对象提供服务)
    • 多线程服务器  (通过创建多个线程提供服务)

    多进程技术是一种实现并发服务器的手段,在网络通信所占的时间中,数据通信时间比CPU运算时间的占比更大,向多个服务端同时提供服务是一种有效利用CPU资源的方式。

    进程定义:占用内存空间的正在运行的程序。例如在电脑上,同时打开文档编辑软件,聊天软件,以及MP3播放器,此时就是创建了三个进程,从操作系统的角度来看,进程是程序流的基本单位,若创建多个进程,操作系统将同时运行,有时一个进程运行的过程中也会产生多个进程。

    注:拥有n个运算设备(运算器)的CPU称为n核CPU,核的个数与同时可运行的进程数量相同,若进程数超过核数,进程将分时使用CPU资源,但是由于CPU运算速度足够快,使得用户感觉到所有进程都是同时运行的。

    1.2 进程ID

    在创建进程的时候,所有进程都会从操作系统分配到对应得分ID,其值为大于2的整数,1是分配给操作系统启动后的首个进程(Linux系统启动后,创建的第一个进程就是init进程),可通过如下命令查看当前Linux下的所有进程:

    1. ps au
    2. // 指定au参数可列出进程的所有详细信息
    3. ps -ef | grep xxx

    创建进程fork

    1. #include
    2. pid_t fork(void); // 成功时返回进程ID,失败时返回-1

    fork函数创建的是调用它的进程的副本,即它是复制正在运行的,调用fork函数的进程,此外,在fork()函数返回后,两个进程都将执行fork()函数后面的语句。但是因为在fork()的时候,是通过同一个进程,复制相同的内存空间,因此fork()之后的程序需要根据fork()函数的返回值加以区分:

    • 父进程:fork()函数返回子进程的ID
    • 子进程:fork()函数返回0

    注:这里的父进程指调用fork()函数的原进程,子进程是指父进程通过调用fork()函数复制出来的进程。

    代码实例:

    main.cpp

    1. #include
    2. #include
    3. int gval = 10;
    4. int main(int argc, char** argv)
    5. {
    6. pid_t pid;
    7. int localVal = 20;
    8. gval++; localVal+=5;
    9. pid = fork(); // # fork出一个新的进程
    10. if (pid == 0) // 子进程
    11. {
    12. /* code */
    13. gval+=2;
    14. localVal+=2;
    15. printf("The child Process: %d, %d\n", gval, localVal);
    16. }
    17. else // 父进程
    18. {
    19. gval-=2;
    20. localVal-=2;
    21. printf("The father Process: %d, %d\n", gval, localVal);
    22. }
    23. return 0;
    24. }

    输出结果:

     父进程在调用fork()函数的同事,复制出子进程,并且获取到fork函数的返回值,在复制前,父进程分别对局部变量和全局变量的值进行了修改,在这种状态下进行复制,子进程也将获取到修改后的值。复制完成后,根据fork()函数的返回值,区分父子进程。在父子进程中,修改变变量值不会相互影响,因为调用fork()函数进行复制之后,父子进程具有完全独立的内存结构,二者只是共享相同的代码而已。

    1.3 僵尸进程 zombie

    文件操作中,文件的打开和关闭同样重要。同样,进程的创建和销毁也同样重要,如果未成功销毁进程,他们将变成僵尸进程。进程在完成工作后(执行完main函数中的程序后)应该被销毁,但是有的进程会变成僵尸进程,占用系统中的重要资源,首先介绍僵尸进程产生的原因,和如何去销毁僵尸进程。

    子进程运行结束时,向exit()函数传递的参数值或者return语句的返回值都会传递给操作系统,而操作系统在此时不会将这个值传递给给产生该子进程的父进程,也不会销毁子进程。此时处于这种状态下的进程就是僵尸进程,也就是说,正是操作系统自己,将子进程变成了僵尸进程。

    销毁子进程的方法:

    将子进程exit()或者return的值传递个产生它的父进程,此时子进程会被销毁

    而操作系统不会主动把子进程的返回值传给父进程,只有父进程主动发起请求的时候,操作系统才会传递该值,此时自己成才会被销毁。如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存该值,且让子进程一直处于僵尸状态,即父进程负责回收自己产生的子进程。

    通过一个代码实例来演示僵尸进程的产生:

    1. #include
    2. #include
    3. int main(int argc, char** argv)
    4. {
    5. pid_t pid = fork(); // 创建一个新的进程
    6. if (pid == 0) // 子进程
    7. {
    8. printf("This is child process: %d\n", pid);
    9. }
    10. else
    11. {
    12. printf("This is Father process: %d\n", pid);
    13. sleep(30); // 父进程延时30s
    14. }
    15. if (pid == 0)
    16. printf("Child process finished\n");
    17. else
    18. printf("Father process finished\n");
    19. return 0;
    20. }

    运行结果:

    结果分析:可以看到在在30S以内查看进程,发现子进程为僵尸进程,当主进程到达30s而退出之后,处于僵尸状态的子进程将同时被销毁。

    1.3.1 利用wait函数销毁僵尸进程

    为了销毁子进程,父进程需要主动请求获取子进程的返回值,可用wait方法发起请求

    1. #include
    2. /*
    3. statloc: 包含子进程终止时传递回来的信息 (返回值,返回状态)
    4. 需要通过宏进行分离
    5. 子进程返回状态:WIFEXITED(statloc)
    6. 子进程的返回值:WEXITSTATUS(statloc)
    7. 返回值:成功时返回终止的子进程的ID,失败返回-1
    8. */
    9. pid_t wait(int* statloc)

    调用wait函数消灭僵尸进程的时候,如果没有已经终止的子进程,程序将阻塞直到有结束的子进程,因此在调用wait函数的时候需要谨慎。

    代码示例:

    1. #include
    2. #include
    3. #include
    4. #include
    5. int main(int argc, char** argv)
    6. {
    7. int status;
    8. pid_t pid = fork();
    9. if (pid == 0)
    10. {
    11. // 子进程
    12. return 3;
    13. }
    14. else
    15. {
    16. printf("Child pid: %d\n", pid);
    17. pid = fork();
    18. if (pid == 0)
    19. {
    20. // 子进程
    21. exit(5);
    22. }
    23. else
    24. {
    25. printf("Child pid %d\n", pid);
    26. // 调用wait函数结束子进程
    27. wait(&status);
    28. if (WIFEXITED(status))
    29. {
    30. printf("Child returned %d\n", WEXITSTATUS(status));
    31. }
    32. wait(&status);
    33. if (WIFEXITED(status))
    34. {
    35. printf("Child returned %d\n", WEXITSTATUS(status));
    36. }
    37. sleep(30);
    38. }
    39. }
    40. return 0;
    41. }

    运行结果:

    可以看待在系统下运行的线程中不存在僵尸进程。

    1.3.2 使用waitpid函数销毁僵尸进程

    wait函数会造成阻塞问题,waitpid()函数也是一种销毁僵尸进程的方法,且能够避免发生阻塞。

    1. #include
    2. /*
    3. pid: 等待终止的目标子进程ID
    4. statloc: 存储进程返回后的状态
    5. options: 传递头文件sys/wait.h中声明的常量WNOHANG, 即使没有终止的进程也不会进入阻塞状态,
    6. 而实返回0并退出。
    7. */
    8. pid_t waitpid(pid_t pid, int * statloc, int options);

    代码示例:

    1. #include
    2. #include
    3. #include
    4. #include
    5. int main(int argc, char** argv)
    6. {
    7. int status;
    8. pid_t pid = fork();
    9. if (pid == 0)
    10. {
    11. // 子进程
    12. sleep(15);
    13. return 1;
    14. }
    15. else
    16. {
    17. while(waitpid(pid, &status, WNOHANG) == 0)
    18. {
    19. sleep(3);
    20. printf("The child process %d is running.\n", pid);
    21. }
    22. if (WIFEXITED(status))
    23. {
    24. printf("Child process exited with %d\n", WEXITSTATUS(status));
    25. }
    26. }
    27. return 0;
    28. }

    运行结果:

     1.4 信号处理

    父进程创建子进程之后,子进程何时终止,父进程往往同子进程一样繁忙,因此不能只通过调用waitpid函数等待子进程结束。因此需要寻找其他的解决方案:
    子进程终止的识别主体是操作系统,若在子进程结束的时候,操作系统能将结束的信号告诉忙于处理其他业务的主进程,则将大大提高程序的运行效率。此时父进程将暂时放下其他的业务,来专门处理子进程终止的相关事宜,在Linux系统下,可以借助信操作系统的信号机制实现此想法。

    Linux中的信号是一种消息处理机制,不同的信号使用不同的值表示,代表不同的含义,虽然信号结构简单,不能携带很大的信息量,但是信号在系统中的优先级很高,在Linux系统下,很多常规的操作,都会产生响应的信号:

    • 键盘操作产生信号 :按下Ctrl+C,键盘输入一个硬件中断,产生一个信号,这个信号会杀死对应的某个进程
    • 通过shell命令产生信号:kill -9 pid ,终止某个进程
    • 函数调用产生信号:如进程中调用sleep()函数,进程收到相关信号,被迫挂起
    • 对硬件进程了非法访问产生了信号:程序访问内存错误,或者段错误,进程退出

    利用信号机制也可以实现进程间通信,但是由于信号的结构简单,不能携带大量信息,且信号的优先级很高,它对应的信号处理函数是通过回调完成,会打乱程序原有的处理流程,因此不适合用信号处理进程间通信。

    可通过kill -l 查看系统定义的信号列表:

    Linux中能够产生信号的函数有很多:
    (1)kill  发送指定的信号到指定的进程:

    1. // 发送指定的信号到指定的进程
    2. int kill(pid_t pid, int sig);
    3. kill(getpid(), 9); // 自己杀死自己

    (2)raise  给当前进程发送指定的信号

    1. // 给自己发送某一个信号
    2. #include
    3. int raise(int sig); // 参数就是要给当前进程发送的信号

    (3)abort  给当前进程发送一个固定信号 (SIGABRT)

    1. // 这是一个中断函数, 调用这个函数, 发送一个固定信号 (SIGABRT), 杀死当前进程
    2. #include
    3. void abort(void);

    (4)alarm  用于单次定时,定时完成向当前进程发出一个信号

    1. #include
    2. unsigned int alarm(unsigned int seconds);

    (5)setitimer  用于周期定时,没触发一次定时器就会发出对应的信号

    1. // 函数可实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号
    2. #include
    3. struct itimerval {
    4. struct timeval it_interval; /* 时间间隔 */
    5. struct timeval it_value; /* 第一次触发定时器的时长 */
    6. };
    7. // 表示一个时间段: tv_sec + tv_usec
    8. struct timeval {
    9. time_t tv_sec; /* 秒 */
    10. suseconds_t tv_usec; /* 微妙 */
    11. };
    12. int setitimer(int which, const struct itimerval *new_value,
    13. struct itimerval *old_value);
    14. // new_value : 输入值,为定时器设置的参数
    15. // old_value : 输出值,上一次为定时器设置的参数,如果不需要知到,传递NULL
    16. // which : 定时器的计数方式
    17. /*
    18. which参数可选项:
    19. ITIMER_REAL: 自然计时法, 最常用, 发出的信号为SIGALRM, 一般使用这个宏值,自然计时法时间 = 用户区 + 内核 + 消耗的时间(从进程的用户区到内核区切换使用的总时间)
    20. ITIMER_VIRTUAL:只计算程序在用户区运行使用的时间,发射的信号为 SIGVTALRM
    21. ITIMER_PROF:只计算内核运行使用的时间, 发出的信号为SIGPROF
    22. */

    信号与signal函数:

    进程首先需要告诉操作系统,在它创建的子进程结束之后,请求帮他调用zombie_handler函数,为了完成这一过程,进程首先需要向操作系统注册一个信号才能实现调用这个函数。操作系统调用的这个函数称为信号注册函数。

    1. #include
    2. void (*signal(int signo, void(*func)(int)))(int);

    函数名:signal

    参数:int signo, void (*func)(int)

    返回值:返回一个函数指针,这个函数指针指向的函数,具有一个int类型的参数,无返回值。

    signal函数原型的理解:signal函数为带有两个参数的函数,一个参数为int类型的整数,另一个参数为函数指针,这个函数指针指向的函数原型没有返回值,具有一个int参数。signal函数执行完毕后,其返回值的也是一个函数指针,这个函数指针指向的是没有返回值,参数为int类型的函数。

    在signal函数中可以注册的部分特殊情况和对应的常数值:

    • SIGALARM     已到了 通过调用alarm函数注册的时间
    • SIGINT            输入CTRL+C 程序中断
    • SIGCHLD        子进程终止

    在信号注册好之后,当注册的情况发生时,操作系统将调用该信号对应的函数

    代码实例:

    1. #include
    2. #include
    3. #include
    4. // 定义信号处理函数,这种函数称为信号处理器 Handler
    5. void timeOut(int signo)
    6. {
    7. if (signo == SIGALRM)
    8. {
    9. // 到达alram注册的超时时间
    10. printf("Time out\n");
    11. }
    12. alarm(2);
    13. }
    14. // 定义信号处理函数,这种函数称为信号处理器 Handler
    15. void keyControl(int signo)
    16. {
    17. if (signo == SIGINT)
    18. {
    19. printf("CTRL+C pressed!\n");
    20. }
    21. }
    22. int main(int argc, char** argv)
    23. {
    24. // 注册信号
    25. signal(SIGALRM, timeOut);
    26. signal(SIGINT, keyControl);
    27. alarm(2);
    28. for (size_t i = 0; i < 20; i++)
    29. {
    30. /* code */
    31. printf("Wait...\n");
    32. sleep(30);
    33. }
    34. return 0;
    35. }

    运行结果:

     发生信号时将会唤醒由于调用sleep而进入休眠状态的进程(即上述代码中,调用sleep之后,程序进入阻塞状态,当alarm(2)超时之后,会唤醒进程,直接的体现就是打印输出了"Wait")。调用函数的主体的确是操作系统,但是进程处于睡眠状态时无法调用函数,因此,产生信号的时候,为了调用信号处理函数,将唤醒由于调用sleep而处于阻塞状态的进程。而且,进程一旦被唤醒,就不会再进入休眠状态,即使还未到sleep中规定的睡眠时间。所以上述实例会很快运行结束。

    利用sigaction函数进行信号处理

    相比于signal函数,sigaction更加稳定,且具有通用性,因为signal在Unix系列的不同系统下,可能存在区别,但是sigaction函数完全相同。建议使用sigaction函数编写程序,以增强代码的可移植性。

    1. #include
    2. int sigaction(int signo, const struct sigaction* act, const struct sigaction* oldact);
    • signo          与signal函数相同,用于传递信号信息
    • act              对应于第一个参数的信号处理函数信息
    • oldact         通过此参数获取之前注册的信号处理函数的函数指针,若不需要则传递0

    结构体sigaction定义如下:

    1. struct sigaction
    2. {
    3. void (*sa_handler)(int);
    4. sigset_t sa_mask;
    5. int sa_flags;
    6. }
    • sa_handler用于保存信号处理函数的指针值
    • sa_mask 所有位均初始化0即可   (用于指定信号相关的选项和特性)
    • sa_flags所有位均初始化位0即可  (用于指定信号相关的选项和特性)

    代码实例:

    1. #include
    2. #include
    3. #include
    4. // 定义信号注册函数
    5. void timeOut(int signo)
    6. {
    7. if (signo == SIGALRM)
    8. {
    9. // 到达alram注册的超时时间
    10. printf("Time out\n");
    11. }
    12. alarm(2);
    13. }
    14. int main(int argc, char** argv)
    15. {
    16. // 注册信号
    17. struct sigaction act;
    18. act.sa_handler = timeOut;
    19. sigemptyset(&act.sa_mask);
    20. act.sa_flags = 0;
    21. sigaction(SIGALRM, &act, 0);
    22. alarm(2);
    23. for (size_t i = 0; i < 5; i++)
    24. {
    25. /* code */
    26. printf("Wait...\n");
    27. sleep(50);
    28. }
    29. return 0;
    30. }

    运行结果:

     利用信号处理技术消灭僵尸进程:

    1. /*
    2. 利用信号机制处理僵尸进程
    3. */
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. // 定义信号处理函数
    10. void readChildProc(int signo)
    11. {
    12. int status;
    13. pid_t pid = waitpid(-1, &status, WNOHANG);
    14. if (WIFEXITED(status))
    15. {
    16. printf("Terminated process %d\n", pid);
    17. printf("Process return %d at termination\n", WEXITSTATUS(status));
    18. }
    19. }
    20. int main(int argc, char** argv)
    21. {
    22. struct sigaction sigact;
    23. sigact.sa_handler = readChildProc;
    24. sigemptyset(&sigact.sa_mask);
    25. sigact.sa_flags = 0;
    26. // 注册信号
    27. sigaction(SIGCHLD, &sigact, 0);
    28. pid_t pid = fork(); // 创建进程
    29. if (pid == 0)
    30. {
    31. // 子进程
    32. printf("I am child process.\n");
    33. sleep(10);
    34. return 5;
    35. }
    36. else
    37. {
    38. // 父进程
    39. printf("Create child process %d\n", pid);
    40. // 再创建一个子进程
    41. pid = fork();
    42. if (pid == 0)
    43. {
    44. // 子进程
    45. printf("I am child process.\n");
    46. sleep(20);
    47. return 3;
    48. }
    49. else
    50. {
    51. // 父进程
    52. printf("Create child process %d\n", pid);
    53. for (size_t i = 0; i < 5; i++)
    54. {
    55. /* code */
    56. printf("Wait.....\n");
    57. sleep(15);
    58. }
    59. }
    60. }
    61. }

    运行结果:

     通过上述信号机制处理进程,可以避免创建的子进程变成僵尸进程。

    1.5 基于多进程的并发服务器

    对回声服务器的例子进行扩展,使其可以向多个客户端同时提供服务。每当有客户端请求服务的时候,回升服务器端都会创建一个子进程以提供服务,此时的服务器端运行主流程如下:

    1. 回声服务器端(父进程)通过调用accept函数受理连接请求
    2. 此时获取的套接字文件描述符创建并传递给子进程
    3. 子进程利用传递来的文件描述符为客户端提供服务

    代码示例:
    服务端:

    1. /*
    2. 多进程服务器
    3. create_date: 2022-7-27
    4. */
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    13. #define BUFF_SIZE 30 // 缓冲区大小
    14. #define PORT 13100 // 端口号
    15. void error_handler(char* msg)
    16. {
    17. printf("%s\n", msg);
    18. exit(1);
    19. }
    20. void read_child_proc(int signo)
    21. {
    22. int status;
    23. pid_t pid = waitpid(-1, &status, 0);
    24. printf("Removed process %d\n", pid);
    25. }
    26. int main(int argc, char** argv)
    27. {
    28. int serverSocket;
    29. int clientSocket;
    30. struct sockaddr_in serverAddr;
    31. struct sockaddr_in clientAddr;
    32. char buffer[BUFF_SIZE];
    33. socklen_t addrSize;
    34. // 注册信号处理函数
    35. struct sigaction sigact;
    36. sigact.sa_handler = read_child_proc;
    37. sigemptyset(&sigact.sa_mask);
    38. sigact.sa_flags = 0;
    39. sigaction(SIGCHLD, &sigact, 0);
    40. // 初始化服务端地址
    41. memset(&serverAddr, 0, sizeof(serverAddr));
    42. serverAddr.sin_family = AF_INET;
    43. serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    44. serverAddr.sin_port = htons(PORT);
    45. serverSocket = socket(PF_INET, SOCK_STREAM, 0); // TCP socket
    46. // 为服务端socket绑定法地址
    47. if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
    48. {
    49. close(serverSocket);
    50. error_handler("Failed to bind server address");
    51. }
    52. // 开始监听客户端
    53. if (listen(serverSocket, 5) == -1)
    54. {
    55. close(serverSocket);
    56. error_handler("Failed to listen client");
    57. }
    58. while (true)
    59. {
    60. addrSize = sizeof(clientAddr);
    61. printf("Successfully init server and wait for connect......\n");
    62. clientSocket = accept(serverSocket, (sockaddr*)&clientAddr, &addrSize);
    63. if (clientSocket == -1)
    64. continue;
    65. printf("Receive connection from %s : %d\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
    66. // 创建新的进程
    67. pid_t pid = fork();
    68. if (pid == -1) // 创建进程失败
    69. {
    70. close(clientSocket);
    71. continue;
    72. }
    73. if (pid != 0)
    74. {
    75. printf("Created new process for client.\n");
    76. // 主进程中需要关闭客户端的socket,因为在创建进程的时候,这个socket会被复制到子进程中去 重要!
    77. close(clientSocket);
    78. memset(&clientAddr, 0, sizeof(clientAddr));
    79. continue;
    80. }
    81. else
    82. {
    83. // 在子进程创建的时候,父进程服务端的socket也会复制到子进程中去,而在子进程中,需要将其关闭 重要!
    84. close(serverSocket);
    85. int str_len = 0;
    86. memset(buffer, 0, BUFF_SIZE);
    87. while ((str_len = read(clientSocket, buffer, BUFF_SIZE)) != 0)
    88. {
    89. /* code */
    90. write(clientSocket, buffer, str_len);
    91. memset(buffer, 0, BUFF_SIZE); // 清一下缓冲区
    92. }
    93. close(clientSocket);
    94. printf("Disconnect %s: %d client from server.\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
    95. return 0;
    96. }
    97. }
    98. close(serverSocket);
    99. return 0;
    100. }

    客户端:

    1. /*
    2. 客户端
    3. create_date: 2022-7-29
    4. */
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #define BUFF_SIZE 30
    12. #define ADDRESS "127.0.0.1"
    13. #define PORT 13100
    14. int main(int argc, char** argv)
    15. {
    16. int socket;
    17. char buffer[BUFF_SIZE];
    18. struct sockaddr_in serverAddr; // 服务端地址
    19. memset(&serverAddr, 0, sizeof(serverAddr));
    20. serverAddr.sin_family = AF_INET;
    21. serverAddr.sin_addr.s_addr = inet_addr(ADDRESS);
    22. serverAddr.sin_port = htons(PORT);
    23. memset(buffer, 0, BUFF_SIZE);
    24. socket = ::socket(PF_INET, SOCK_STREAM, 0);
    25. if (socket == -1)
    26. {
    27. printf("Failed to init socket.\n");
    28. return -1;
    29. }
    30. if (connect(socket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
    31. {
    32. printf("Failed to connect to server.\n");
    33. return -2;
    34. }
    35. printf("Successfully connect to the server.\n");
    36. while (true)
    37. {
    38. fputs("Input message(Type Q(q) to quit): ", stdout);
    39. fgets(buffer, BUFF_SIZE, stdin);
    40. if (strncmp(buffer, "Q\n", 2) == 0 || strncmp(buffer, "q\n", 2) == 0)
    41. break;
    42. int write_len = write(socket, buffer, strlen(buffer));
    43. int recv_len = 0;
    44. while (recv_len < write_len)
    45. {
    46. int recv_count = read(socket, &buffer[recv_len], BUFF_SIZE-1);
    47. recv_len += recv_count;
    48. // printf("Received %d bytes data from sever.\n");
    49. }
    50. printf("Receive data: %s", buffer);
    51. memset(buffer, 0, BUFF_SIZE);
    52. }
    53. close(socket);
    54. return 0;
    55. return 0;
    56. }

    运行结果:

    服务端运行结果:

     客户端1运行结果:

     客户端2运行结果:

     查看运行的线程:

    问题:

    在上述多进程服务器代码中,在调用fork函数创建子进程的过程中,父进程将两个套接字(一个服务端套接字,一个客户端套接字) 的文件描述符复制给了子进程,在这一过程中,是仅仅复制了文件描述符吗?是否对套接字也进行了复制?

    调用fork()函数的时候,会复制父进程的所有资源,同理文件客户端,服务端的描述符也属于父进程资源同样会被复制,但是不会复制套接字。因为套接字属于操作系统的资源,,而文件描述符属于父进程的资源(假设套接字被复制了,那么将会出现同一端口对应多个套接字的情况)。

    调用fork函数复制文件描述符

     如上图所示,如果一个套接字存在两个文件描述符的时候,只有当两个文件描述符都关闭之后,才能销毁套接字。如上图中所示,即使子进程销毁了与客户端连接的套接字的文件描述符,也无法完全销毁套接字。服务端套接字也是同样如此。因此在调用fork函数之后,需要将无关的套接字进行关闭。

    1. if (pid != 0)
    2. {
    3. printf("Created new process for client.\n");
    4. // 主进程中需要关闭客户端的socket,因为在创建进程的时候,这个socket会被复制到子进程中去 重要!
    5. close(clientSocket);
    6. memset(&clientAddr, 0, sizeof(clientAddr));
    7. continue;
    8. }
    9. else
    10. {
    11. // 在子进程创建的时候,父进程服务端的socket也会复制到子进程中去,而在子进程中,需要将其关闭 重要!
    12. close(serverSocket);
    13. int str_len = 0;
    14. ....

    1.6 分割TCP的IO程序

    对上述的回声服务器程序进行改进,将客户端进行IO分割。在之前的客户端实现中,客户端首先向服务端发送数据,发送完成之后,无条件等待服务端回复(客户端中调用read),只有服务端回复之后,客户端才能进行下一次数据的发送。现在可利用多进程方法对客户端的程序进行改进,将接收与发送的逻辑放在两个不同的进程中进行。

    设计方案:

    • 客户端父进程负责接收数据
    • 客户端子进程负责发送数据

    这样设计之后,无论客户端是否从服务端接收到数据,都可以进行数据发送,IO分割之后,可以提高频繁交换数据的程序性能,可以提高同一时间数据的传输量。

    客户端代码示例:

    1. /*
    2. 客户端
    3. create_date: 2022-7-29
    4. */
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #define BUFF_SIZE 30
    12. #define ADDRESS "127.0.0.1"
    13. #define PORT 13100
    14. void readRoutine(int sock, char* buf);
    15. void writeRoutine(int sock, char* buf);
    16. int main(int argc, char** argv)
    17. {
    18. char buffer[BUFF_SIZE];
    19. struct sockaddr_in serverAddr; // 服务端地址
    20. memset(&serverAddr, 0, sizeof(serverAddr));
    21. serverAddr.sin_family = AF_INET;
    22. serverAddr.sin_addr.s_addr = inet_addr(ADDRESS);
    23. serverAddr.sin_port = htons(PORT);
    24. memset(buffer, 0, BUFF_SIZE);
    25. int socket = ::socket(PF_INET, SOCK_STREAM, 0);
    26. if (socket == -1)
    27. {
    28. printf("Failed to init socket.\n");
    29. return -1;
    30. }
    31. if (connect(socket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1)
    32. {
    33. printf("Failed to connect to server.\n");
    34. return -2;
    35. }
    36. printf("Successfully connect to the server.\n");
    37. pid_t pid = fork();
    38. if (pid == 0)
    39. {
    40. // 子进程负责发送
    41. writeRoutine(socket, buffer);
    42. }
    43. else
    44. {
    45. // 父进程负责接收
    46. readRoutine(socket, buffer);
    47. }
    48. close(socket);
    49. return 0;
    50. }
    51. void readRoutine(int sock, char* buf)
    52. {
    53. while (true)
    54. {
    55. memset(buf, 0, BUFF_SIZE);
    56. int str_len = read(sock, buf, BUFF_SIZE);
    57. if (str_len == 0) // EOF
    58. {
    59. return;
    60. }
    61. printf("\nReceive data from server: %s\n", buf);
    62. }
    63. }
    64. void writeRoutine(int sock, char* buf)
    65. {
    66. while (true)
    67. {
    68. fputs("Input message(Type Q(q) to quit): ", stdout);
    69. fgets(buf, BUFF_SIZE, stdin);
    70. if (strncmp(buf, "Q\n", 2) == 0 || strncmp(buf, "q\n", 2) == 0)
    71. {
    72. shutdown(sock, SHUT_WR);
    73. return;
    74. }
    75. write(sock, buf, strlen(buf));
    76. }
    77. }

    运行结果:

    服务端:

     客户端1:

     客户端2:

     通过客户端数据结果可观察到,在提示输入之后,马上又会出现提示输入的语句,然后才出现服务端回复的内容,反映了在子进程中发送数据后,不用等待服务端回复,而又能马上发送数据,而父进程接收服务端回复数据会稍微慢于子进程发送数据,但是却不会对子进程的发送流程造成影响,适合客户端需要频繁发送数据的应用场景。

    注:

    在子进程中的发送流程中,有如下的代码:

    1. fputs("Input message(Type Q(q) to quit): ", stdout);
    2. fgets(buf, BUFF_SIZE, stdin);
    3. if (strncmp(buf, "Q\n", 2) == 0 || strncmp(buf, "q\n", 2) == 0)
    4. {
    5. shutdown(sock, SHUT_WR);
    6. return;
    7. }

    在用户输入Q/q结束发送流程时,客户端子进程会通过shutdown关闭客户端socket的写功能,此时调用shutdown,相当于向服务端传输EOF。在客户端调用完shutdown之后,继续调用后面的代码,也就是main的close和return

    1. close(socket);
    2. return 0;

    此时只是将子进程中的客户端进行一次关闭。

    接着服务端接收到客户端发送来的的EOF,并将其返回给客户端(此时客户端的接收功能并未关闭,还能正常接收服务端的EOF),客户端在判断接收到服务端发送来的EOF之后,结束接收流程,也同样调用main中后续的代码,执行close和return,此时客户端的socket又被关闭了一次,至此,客户端中的socket被成功关闭,子进程和父进程都结束。(可查看并没有出现僵尸进程,因为在子进程结束后,父进程马上结束)。

    另一种情况:

    如果没有在客户端发送流程中调用shutdown,而是直接return,此时客户端子进程不会发送任何内容便关闭socket结束了。此时服务端未收到任何内容,也就不会向客户端回复内容,此时客户端父进程将一致处于等待接收的状态而无法结束,且出现了僵尸进程。

     ---------------------------------The end---------------------------------------

  • 相关阅读:
    【Java基础】Java容器相关知识小结
    代码随想录算法训练营第三十九天| 62.不同路径 63. 不同路径 II
    智能算法和人工智能算法,人工智能算法概念股票
    【云原生之k8s】K8s 管理工具 kubectl 详解(二)
    面向城市巡防的多无人机协同航迹规划
    聚观早报 | 多款热门游戏停服一天;比亚迪下月在日本开售
    描述符——接口描述符
    基于微信小程序的美容院管理系统设计与实现-设计与实现-计算机毕业设计源码+LW文档
    tensorflow2 -------------LeNet--------------
    BI如何实现用户身份集成自定义安全程序开发
  • 原文地址:https://blog.csdn.net/zj1131190425/article/details/125728911