Linux下基于C++11的socket网络编程(多进程)个人总结版

发布于:2025-07-01 ⋅ 阅读:(21) ⋅ 点赞:(0)

有一些函数的返回类型改掉了,具体改了哪些忘了

1.fork()

作用:调用 fork() 后,系统会创建一个与当前进程(称为父进程)几乎完全相同的新进程(称为子进程)。
● 返回值区分父子进程:
○ 在父进程中,fork() 返回子进程的进程 ID(PID)。
○ 在子进程中,fork() 返回 0。
○ 若创建失败,返回 -1。
● 执行流程:
undefined 父进程调用 fork(),系统创建子进程。
undefined 父子进程从 fork() 调用的下一行代码开始并行执行。
undefined 通过 pid 的值区分当前是父进程还是子进程。

1.1fork () 的关键特性

写时复制(Copy-on-Write, COW)
子进程不会立即复制父进程的全部内存,而是与父进程共享内存页。只有当其中一个进程修改内存时,系统才会为修改的页面创建副本,避免不必要的资源消耗。
文件描述符共享
子进程会继承父进程打开的文件描述符(如套接字、文件句柄),但父子进程对描述符的操作是独立的(如关闭一个进程的描述符不影响另一个)。这里可以理解为和共享指针一样,子进程复制父进程的文件描述符表,这些描述符指向内核中同一文件表项。文件表项有引用计数,当计数为0才会真正的关闭文件
进程地址空间隔离
父子进程有独立的地址空间,修改一个进程的变量不会影响另一个进程。

1.2 举个例子:

假设父进程已打开监听套接字(fd=3),接受新连接后 fd=4 指向客户端
调用fork()创建子进程后:

  1. 父进程关闭客户端 fd=4:
    ○ 父进程的描述符表中 fd=4 被标记为 “关闭”。
    ○ 内核中客户端连接的引用计数减 1(仍为 1,因为子进程还持有 fd=4)。
  2. 子进程关闭监听 fd=3:
    ○ 子进程的描述符表中 fd=3 被标记为 “关闭”。
    ○ 内核中监听套接字的引用计数减 1(仍为 1,因为父进程还持有 fd=3)。
  3. 最终结果:
    ○ 父进程专注监听新连接(持有 fd=3)。
    ○ 子进程专注处理客户端通信(持有 fd=4)。
    ○ 资源管理清晰,互不干扰。

2.僵尸进程

定义:是指已经结束运行,但其退出状态信息尚未被父进程读取(回收)的进程。它本质上是一个已经死亡的进程在进程表中的残留条目。

2.1过程


1.子进程终止通过调用exit()或者main函数返回,结束运行时,内核会向父进程发送一个SIGCHLD信号,通知父进程该子进程已终止
2.内核等待父进程,它会存一些状态码的东西(等待父进程把子进程的尸体拿去烧了)这时子进程会变成僵尸进程。
3.父进程调用wait()或者waitpid(),来:
● 读取子进程的退出状态码(了解子进程为什么以及如何终止)。
● 释放内核为该僵尸进程保留的所有信息(主要是进程表中的条目)。
● 系统回收该僵尸进程的 PID,使其可以被重新分配给新进程。

特殊情况

父进程比子进程先终止,或者崩溃了。init进程(PID=1)的会收养,然后定期调用wait()释放。
单个的僵尸进程危害很小,但是大量僵尸进程会占用PID资源。所以在服务端需要考虑处理。

3.代码

多进程实现就是用fork创建子进程然后并行执行,根据PID的值决定运行子进程逻辑还是父进程逻辑。
这里来解释如何防止僵尸线程的,涉及到的代码如下:

3.1 pid_t waitpid(pid_t pid, int *status, int options);

作用:等待pid子进程终止并回收其资源(避免僵尸进程),同时获取子进程的退出状态(status)
返回值:
● >0:成功回收的子进程 PID。
● 0:若指定 WNOHANG 且没有子进程终止。
● -1:出错(如没有子进程、权限不足)
pid:
● pid > 0:等待 PID 等于 pid 的子进程。
● pid = 0:等待当前进程组的任意子进程。
● pid < -1:等待进程组 ID 等于 abs(pid) 的子进程。
● pid = -1:等待任意子进程终止(不指定具体 PID)。
status
用于存储子进程的退出状态
● WIFEXITED(status):子进程是否正常退出(通过 exit() 或 return)。
● WEXITSTATUS(status):获取正常退出的状态码(如 exit(10) 中的 10)。
● WIFSIGNALED(status):子进程是否被信号终止。
● WTERMSIG(status):获取终止子进程的信号编号。
options
● 0:阻塞等待,直到有子进程终止。
● WUNTRACED:同时等待停止的子进程(由 SIGSTOP 等信号导致)。
● WCONTINUED:等待被停止后又继续的子进程。
● WNOHANG:非阻塞选项(no hang),若没有子进程终止,立即返回而不等待
ps:还有一个wait函数也可以用,但是功能比较单一,等价于waitpid(-1,&status,0)
ps:这个int sign,函数里也没用过啊。删了行不行?不行!因为我们要用库里的函数,把这个函数作为参数传进去,格式规定好了,所以不能删。

// 功能:回收进程,打印出回收了哪个进程
// 如果只是单纯回收不用打印,根本不用写这个函数
void read_childproc(int sign){
	pid_t pid;
	int status;
	pid = waitpid(-1 , &status , WNOHANG);
	if(WIFEXITED(status)){
		cout<<"子进程返回的状态码:"<<WEXITSTATUS(status)<<endl;
		cout<<"移除子进程,他的PID:"<<pid<<endl;
	}
}

3.2 struct sigaction act;

作用:控制进程如何响应特定信号

struct sigaction {
    // 信号处理函数(两种形式,根据 sa_flags 选择)
    union {
        void (*sa_handler)(int);          // 普通处理函数(常用)
        void (*sa_sigaction)(int, siginfo_t *, void *);  // 带额外信息的处理函数
    } __sigaction_handler;  // 注意:这里是个 union,只能二选一
    
    sigset_t sa_mask;       // 信号掩码:处理信号时临时阻塞的信号集合
    int sa_flags;           // 控制信号处理行为的标志
    void (*sa_restorer)(void);  // 已废弃,不要使用
};
act.sa_handler = read_childproc;  // 之前定义的信号处理函数
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &act, NULL);

sa_handler(普通) 或 sa_sigaction(带额外信息)的处理函数。只能选一个
sa_mask:在执行信号处理函数期间,临时阻塞(屏蔽)的信号集合。防止处理当前信号时被其他信号中断。

sigemptyset(&act.sa_mask); // 清空掩码(默认不阻塞任何信号)
sigaddset(&act.sa_mask, SIGINT); // 添加 SIGINT 到掩码(处理期间阻塞 Ctrl+C)
sigdelset(&act.sa_mask, SIGQUIT); // 从掩码中移除 SIGQUIT

sa_flags:设置一些额外行为

SA_RESTART 被信号中断的系统调用(如 read、accept)自动重启
SA_NOCLDSTOP 子进程暂停(如 SIGSTOP)时不发送 SIGCHLD,仅在退出时发送
SA_NOCLDWAIT 子进程退出后直接销毁,不变成僵尸进程(即使父进程未调用 wait)
SA_SIGINFO 使用 sa_sigaction 而非 sa_handler 作为处理函数
SA_ONESHOT 处理函数仅执行一次,之后恢复为默认行为(等同于 signal() 的行为)
SA_NODEFER 处理信号期间不自动阻塞该信号本身(可能导致递归调

**int sigaction(int signum, const struct sigaction act, struct sigaction oldact)
作用:设置进程如何响应 SIGINT 信号
● signum:信号编号(使用的SIGCHLD就是内核通知父进程,子进程死啦!的编号)。
● act:指向 struct sigaction 的指针,指定新的信号处理方式。
● oldact:若不为 NULL,则存储旧的信号处理配置(用于后续恢复)。
辅助理解:struct sigaction和函数sigaction这两个的关系其实和上一节提到的socket与sock_addr的关系非常相似。上一个理解了这个就很容易理解了。

	● 信号处理流程:
			a. 填充 struct sigaction(设置处理函数、掩码、标志)。
			b. 调用 sigaction() 应用配置。
	● 网络编程流程:
		 a. 填充 struct sockaddr_in(设置 IP、端口、协议族)。
		 b. 调用 bind() 将地址绑定到套接字。

这行代码告诉操作系统内核:“将此进程的所有后续接收到的SIGCHLD信号都交给read_childproc函数处理”。这是一个进程级别的全局设置,不是函数调用后就失效。会维护在进程结构体里(PCB)里,持续存在于进程的整个生命周期。

4.粘包拆包问题

基础版已经实现这个功能了,我就是把内容拆成了两个函数放进baseutil里。

//BaseUtil
// Created by root on 2025/6/29.
//

#include "BaseUtil.h"

#include <vector>

// 把socket 接收的值放result里
int TcpBaseRead(const int& sockfd , std::string& result){
	//首先判断socket描述符是否可以使用
	if(sockfd == -1){
		return 0;
	}
	char buf[BUFF_SIZE];
	uint32_t net_len;
	int buf_len=read(sockfd,&net_len,sizeof(net_len));
	if (buf_len<=0) return buf_len;
	// 转换为主机字节序
	uint32_t msg_len = ntohl(net_len);
	int total_body_bytes = 0;
	while(total_body_bytes < msg_len){
		int to_read=std::min(msg_len-total_body_bytes,static_cast<uint32_t>(BUFF_SIZE-1));
		buf_len = read(sockfd , buf , to_read);
		result.append(buf,buf_len);
		total_body_bytes+=buf_len;
	}
	if (result.empty())  return 0;
	return true;
}

// 把result的值写到socket里
bool TcpBaseWrite(const int& sockfd ,const std::string& result){
	if(sockfd == -1) return false;
	int content_len=result.size();
	uint32_t net_len=htonl(content_len);// 转换成网络字节序
	std::vector<char> packet(sizeof(net_len)+content_len);
	// 把包头和内容全都拷贝到packet中。
	memcpy(packet.data(),&net_len,sizeof(net_len));
	memcpy(packet.data()+sizeof(net_len),result.data(),content_len);
	int write_victory=write(sockfd,packet.data(),packet.size());
	if (write_victory==0) {
		return false;
	}
	else if (write_victory<0) {
		cout << "写的不对" << endl;
		return false;
	}
	return true;
}
//TcpClient.cpp
int TcpClient::Read(){
    if(clnt_sock == -1){
        cout<<"客户端未开启socket"<<endl;
        return 0;
    }
    string result="";
    int flag=TcpBaseRead(clnt_sock, result);
    if (flag<=0) {
        cout << "【" << clnt_sock << "】断开了连接" << endl;
        return 0;
    }

    cout << "【" << clnt_sock << "】说:" << result << endl;
    return flag;
}

void TcpClient::Write(const string& str){
    if(clnt_sock < 0){
        cout<<"客户端未开启socket,写入失败!"<<endl;
        return;
    }
    if (!TcpBaseWrite(clnt_sock, str)) {
        cout << "发送消息失败,对端关闭" << endl;
    }
    return;
}
//TcpServer.cpp
int TcpServer::StartFork() {
	while(1) {
		int accept_res = Accept();
		cout << "【" << accept_res << "】连接成功..." << endl;

		pid_t pid = fork();
		if(pid < 0) {
			perror("fork失败");
			close(accept_res);
			continue;
		}
		if(pid == 0) {
			// 子进程
			cout<<"子进程开启,将对连接【"<<accept_res<<"】提供通信服务"<<endl;
			//子进程的操作区域
			//关闭父进程中服务端的socket
			close(serv_sock);
			// 循环处理客户端消息
			while (true) {
				string result = "";
				if (TcpBaseRead(accept_res, result)<=0) {
					cout << "【" << accept_res << "】断开了连接" << endl;
					break;
				}

				cout << "【" << accept_res << "】说:" << result << endl;

				if (!TcpBaseWrite(accept_res, result)) {
					cout << "发送消息失败,对端关闭" << endl;
					break;
				}
			}
			cout << "【" << accept_res << "】断开了连接" << endl;
			close(accept_res);
			exit(0); // 子进程退出
		}
		close(accept_res); // 父进程关闭客户端socket
	}
	return 0;
}

总结一下:
所有的连接请求来了都是先父进程里的listen_fd2(监听套接字)听到了,就调用accept(),accept问内核要fd(客户端套接字),要到了fd4就创建一个子进程,把fd4给子进程,父进程fd4就不要了。子进程listen_fd2也不要了。子进程专心和fd4对应的客户端通信。这时父进程的listen->fd2可能又听到,调用accept(),accept问内核要fd5(客户端套接字)…这时fd4的客户端通信结束了,内核知道子进程结束了,内核给父进程发信号。3.2说了调用什么已经写进程块里了。所以不管父进程代码走到哪儿都能马上调用之前预定的函数。父进程开始回收子进程PID.
在这里插入图片描述
其实还有问题:如果客户端只连服务端一直不输内容呢?难道我服务器这边的子进程就一直等着它?所以是不是应该加个超时机制比如多久没回应我就断开。还有,万一我发送的内容有一部分很复杂呢?服务端算不过来一直卡在那儿,那我后面的内容都做不了了,是不是应该加个跳过?但我决定先放过自己,想想就头大。看看后面再说。


网站公告

今日签到

点亮在社区的每一天
去签到