网络编程 25_阻塞 I/O 进程模型

目标

为每一个连接创建一个独立的进程去服务

一、父子进程

进程是程序执行的最小单位,创建进程使用 fork 函数

1.1 fork 创建进程

pid_t fork(void);

调用一次,在父子进程中各返回一次,在父进程中返回进程 ID 号,在子进程中返回 0,只能通过返回值判断当前执行的进程是父进程还是子进程

if (fork() == 0) {
    // 子进程
} else {
    // 父进程
}

在 Linux 下僵尸进程会被挂到进程号为 1 的 init 进程上。由父进程派生的子进程,必须由父进程负责回收,否则子进程会变成僵尸进程(占用不必要的内存空间,数量达到一定数量级,会耗尽系统资源)

1.2 回收进程资源

wait 和 waitpid
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);

wait 系统调用暂停调用进程的执行,直到其子进程之一终止
waitpid 系统调用暂停调用进程的执行,直到由 pid 参数指定的子进程更改了状态。默认情况下,waitpid 只等待终止的子项,但此行为可通过 options 参数修改

SIGCHILD 信号

注册信号处理函数,捕捉信号 SIGCHILD 信号,在信号处理函数中调用 waitpid 函数完成子进程的资源回收

// void ( *signal(int signum, void (*handler)(int)) ) (int);
// 第二个参数格式:void (*handler)(int)
signal(SIGCHLD, sigchld_handler); // sigchld_handler:自定义子进程回收资源的函数指针,满足第二个参数格式即可

二、阻塞 I/O 进程模型

服务器监听在连接套接字 listenfd 上,客户端发起连接请求时,服务器产生连接套接字,同时派生出一个子进程,服务器在子进程中使用连接套接字处理和客户端的通信

  • 父进程只关心监听套接字,不关心连接套接字
  • 子进程只关心连接套接字,不关心监听套接字

服务端

fork_server.c

#include "common.h"

char rot13_char(char c) {
	if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) {
		return c + 13;
	} else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) {
		return c - 13;
	}
	return c;
}

void child_run(int fd) {
	char outbuf[512];
	int i;
	ssize_t result;

	while (1) {
		result = recv(fd, &outbuf, sizeof(outbuf), 0);
		if (result == 0) {
			break;
		} else if (result == -1) {
			perror("read");
			break;
		}

		for (i = 0; i < result; i++) {
			outbuf[i] = rot13_char(outbuf[i]);

			if (outbuf[i] == '\n') {
				send(fd, outbuf, result, 0);
				break;
			}
		}
	}
}

void sigchld_handler(int sig) {
	// 0:meaning wait for any child process whose  process  group  ID  is equal to that of the calling process
	// WNOHANG   即使没有子进程退出,也立即返回
	while (waitpid(-1, 0, WNOHANG) > 0) {}
	return;
}

int main(int argc, char **argv) {
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);

	struct sockaddr_in servaddr;
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);

	// 端口复用
	int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

	socklen_t servlen = sizeof(servaddr);
	int bind_rt = bind(listenfd, (struct sockaddr *)&servaddr, servlen);
	if (bind_rt < 0) {
		error(1, errno, "bind failed");
	}

	int listen_rt = listen(listenfd, LISTENQ);
	if (listen_rt < 0) {
		error(1, errno, "listen failed");
	}

	// 注册信号处理函数,回收子进程资源
	// sigchld_handler:函数指针,格式对应 void (*handler)(int)
	signal(SIGCHLD, sigchld_handler);

	while (1) {
		struct sockaddr_storage ss;
		socklen_t slen = sizeof(ss);

		int fd = accept(listenfd, (struct sockaddr *)&ss, &slen);
		if (fd < 0) {
			error(1, errno, "accept failed");
			exit(1);
		}

		// fork 创建子进程,描述符会复制一份,即监听套接字 listenfd 和连接套接字 fd 引用计数都加 1
		if (fork() == 0) { // 子进程
			// close 函数会将引用计数减 1
			close(listenfd); // 将监听套接字 listenfd 引用计数减 1,减到 0 时会将套接字资源回收
			child_run(fd);
			exit(0);
		} else { // 父进程
			close(fd);
		}
	}
}

头文件 common.h

#ifndef CHAP_25_COMMON_H
#define CHAP_25_COMMON_H

#include    <stdio.h>
#include    <stdlib.h>
#include    <string.h>
#include    <strings.h>
#include    <sys/socket.h>    /* basic socket definitions */
#include    <netinet/in.h>    /* sockaddr_in{} and other Internet defns */
#include    <arpa/inet.h>    /* inet(3) functions */
#include    <errno.h>

#include    <unistd.h>

#include    <signal.h>

#include    <sys/wait.h>

void error(int status, int err, char *fmt, ...);

#define    SERV_PORT      43211

#define    LISTENQ        1024

#endif //CHAP_25_COMMON_H

三、CMake 管理当前项目

① 代码组成

-CMakeLists.txt
-include:存放头文件
-src:存放源代码
代码组成

CMakeLists.txt

CMAKE_MINIMUM_REQUIRED(VERSION 3.1)
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/include)
ADD_SUBDIRECTORY(src)

include 目录:include/common.h(common.h 上面有)
include 目录

src 目录(fork_server.c 上面有)
src目录

src/CmakeLists.txt

ADD_EXECUTABLE(fork_server fork_server.c)
TARGET_LINK_LIBRARIES(fork_server)

② 创建并进入 build 目录

mkdir build && cd build

创建并进入 build 目录

③ 外部编译

cmake .. && make

外部编译

四、测试

可以使用一个或多个 telnet 客户端连接服务器,检验交互是否正常

测试步骤
① 打开三个命令行窗口
② 其中一个窗口先执行服务器命令,输入命令 ./nonblockingserver 后回车
③ 其余窗口执行客户端命令,输入命令 ./telnet-client 127.0.0.1 43211 后回车

左:服务端;右上、右下:客户端
测试

上面的阻塞 I/O 进程模型的服务端程序,可以并发处理多个不同的客户端连接,互不干扰

总结

使用阻塞 I/O + 进程的方式,为每一个连接创建一个独立的子进程,服务器在子进程中使用连接套接字处理和客户端的通信

  • 及时关闭套接字
    服务器端 fork 子进程,套接字引用计数加 1,close 使套接字引用计数减 1,引用计数减为 0 时会将套接字资源回收,避免服务器端资源泄漏
  • 及时回收子进程资源,避免出现僵尸进程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值