前言:欢迎 各位光临本博客,这里IFMAXUE带你直接手撕,文章并不复杂,愿诸君**耐其心性,忘却杂尘,道有所长!!!!

IF'Maxue :个人主页
🔥 个人专栏 :
《C语言》
《C++深度学习》
《Linux》
《数据结构》
《数学建模》
⛺️生活是默默的坚持,毅力是永久的享受。不破不立!
文章目录
-
- 一、TCP客户端开发:连接成败逻辑
- 二、自定义请求协议:构建规范请求字符串
- 三、服务端守护进程化:后台常驻运行
- 四、Linux核心概念:PID/PGID/会话
-
- [1. PID和PGID](#1. PID和PGID)
- [2. Linux会话(Session)](#2. Linux会话(Session))
- 关键特性
- 五、前台/后台进程:终端与进程的关系
-
- [1. 前台进程vs后台进程](#1. 前台进程vs后台进程)
- [2. Linux与Windows的会话差异](#2. Linux与Windows的会话差异)
- [3. 注销与关闭会话的影响](#3. 注销与关闭会话的影响)
- 六、守护进程vs后台进程vs前台进程
- 七、守护进程实现:setsid函数与核心步骤
- 八、整合:带守护进程+JSON配置的TCP服务端&客户端
- 总结
- 一、TCP客户端开发:连接成败逻辑
- 二、自定义请求协议:构建规范请求字符串
- 三、服务端守护进程化:后台常驻运行
- 四、Linux核心概念:PID/PGID/会话
-
- [1. PID和PGID](#1. PID和PGID)
- [2. Linux会话(Session)](#2. Linux会话(Session))
- 关键特性
- 五、前台/后台进程:终端与进程的关系
-
- [1. 前台进程vs后台进程](#1. 前台进程vs后台进程)
- [2. Linux与Windows的会话差异](#2. Linux与Windows的会话差异)
- [3. 注销与关闭会话的影响](#3. 注销与关闭会话的影响)
- 六、守护进程vs后台进程vs前台进程
- 七、守护进程实现:setsid函数与核心步骤
- 八、整合:带守护进程+JSON配置的TCP服务端&客户端
一、TCP客户端开发:连接成败逻辑
客户端核心逻辑很简单:与服务端建立连接,失败则直接退出程序,成功则进入通信流程 ,这是客户端开发的基础准则,也是手写笔记的核心思路。

客户端核心开发步骤
客户端比服务端少了bind和listen步骤,核心只有创建套接字→初始化服务端地址→连接服务端→读写通信→关闭套接字5步,且所有系统调用失败后直接退出,符合"失败退出"的逻辑。
客户端完整代码(含成败处理)
c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define SERV_IP "127.0.0.1" // 服务端IP
#define SERV_PORT 8888 // 服务端端口
int main() {
// 1. 创建套接字(和服务端一致,TCP用SOCK_STREAM)
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1) {
perror("socket error");
exit(EXIT_FAILURE); // 失败:直接退出
}
// 2. 初始化服务端地址结构体(要连接的服务端IP+端口)
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr); // 字符串IP转网络字节序
// 3. 连接服务端(核心:connect失败则退出)
int ret = connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if (ret == -1) {
perror("connect error");
close(cfd);
exit(EXIT_FAILURE); // 失败:释放资源后退出
}
printf("connect server success...\n"); // 成功:进入通信流程
// 4. 读写通信(向服务端发送数据)
char buf[1024] = "hello tcp server!";
write(cfd, buf, strlen(buf));
bzero(buf, sizeof(buf));
ret = read(cfd, buf, sizeof(buf)); // 读取服务端回显
printf("server say: %s\n", buf);
// 5. 关闭套接字
close(cfd);
return 0;
}
核心细节
- 客户端无需绑定自身IP和端口,系统会自动分配随机端口,这是和服务端的关键区别;
inet_pton用于将字符串格式的IP (如127.0.0.1)转为网络字节序的整型,比htonl更易使用;- 所有系统调用失败后,先释放已申请的文件描述符,再退出,避免资源泄漏。
二、自定义请求协议:构建规范请求字符串
基础的字节流通信没有统一格式,服务端无法解析复杂请求,因此需要搭建自定义请求协议 ,通过BuildRequestString函数封装规范的请求字符串 ,让客户端和服务端的通信有统一标准。

协议设计思路
手写笔记的核心是封装请求头+请求体 ,请求头包含请求的核心信息(如请求类型、数据长度),请求体是实际的业务数据,这样服务端能先解析请求头,再根据数据长度读取请求体,避免粘包/拆包问题。

BuildRequestString函数实现
该函数的作用是按自定义协议,拼接请求头和请求体,生成规范的请求字符串,参数为请求类型、业务数据,返回拼接后的请求字符串。
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 自定义请求协议:请求头|请求体 例:type=1&len=12|hello server
#define REQ_HEAD_FMT "type=%d&len=%d" // 请求头格式:请求类型+数据长度
#define SEP "|" // 头和体的分隔符
// 构建请求字符串:req_type=请求类型,data=请求体,buf=输出的请求字符串
int BuildRequestString(int req_type, const char *data, char *buf, int buf_len) {
if (data == NULL || buf == NULL || buf_len <= 0) {
return -1;
}
int data_len = strlen(data);
char req_head[128] = {0};
// 1. 拼接请求头
snprintf(req_head, sizeof(req_head), REQ_HEAD_FMT, req_type, data_len);
// 2. 拼接请求头+分隔符+请求体,生成最终请求字符串
int ret = snprintf(buf, buf_len, "%s%s%s", req_head, SEP, data);
if (ret >= buf_len || ret < 0) {
return -1; // 缓冲区不足或拼接失败
}
return ret; // 返回请求字符串长度
}
// 测试函数
int main() {
char req_buf[1024] = {0};
int len = BuildRequestString(1, "get server info", req_buf, sizeof(req_buf));
if (len > 0) {
printf("build request success: %s\n", req_buf);
} else {
printf("build request error\n");
}
return 0;
}
运行结果(成功构建)
编译运行后,会生成规范的请求字符串,客户端直接发送该字符串,服务端按协议解析即可,完美契合手写笔记的"成功"逻辑。

三、服务端守护进程化:后台常驻运行
实际开发中,TCP服务端需要脱离终端、后台常驻运行 ,这就是守护进程(精灵进程) ,同时服务端的配置(IP、端口、守护进程开关)需要通过JSON配置文件 管理,避免硬编码,方便运维。

第一步:JSON配置文件设计
JSON配置文件简洁易读,适合存储服务端的基础配置,手写笔记中包含服务端IP、端口、是否守护进程、工作目录等核心配置,示例如下:
json
{
"serv_ip": "0.0.0.0",
"serv_port": 8888,
"daemon": 1,
"work_dir": "/home/tcp_server"
}

第二步:C语言读取JSON配置(cJSON库)
C语言本身不支持JSON解析,需使用cJSON轻量级库 ,核心步骤是读取配置文件→解析JSON节点→获取配置值 ,代码如下(需链接cJSON库:gcc xxx.c -o xxx -lcjson):
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
// 定义配置结构体
typedef struct {
char serv_ip[32];
int serv_port;
int daemon;
char work_dir[128];
} ServerConfig;
// 读取JSON配置文件
int read_server_config(const char *config_path, ServerConfig *config) {
if (config_path == NULL || config == NULL) {
return -1;
}
// 1. 读取文件内容
FILE *fp = fopen(config_path, "r");
if (fp == NULL) {
perror("fopen error");
return -1;
}
fseek(fp, 0, SEEK_END);
int file_len = ftell(fp);
fseek(fp, 0, SEEK_SET);
char *file_buf = (char*)malloc(file_len + 1);
fread(file_buf, 1, file_len, fp);
fclose(fp);
// 2. 解析JSON
cJSON *root = cJSON_Parse(file_buf);
free(file_buf);
if (root == NULL) {
printf("cJSON parse error\n");
return -1;
}
// 3. 获取各配置节点
cJSON *ip_node = cJSON_GetObjectItem(root, "serv_ip");
cJSON *port_node = cJSON_GetObjectItem(root, "serv_port");
cJSON *daemon_node = cJSON_GetObjectItem(root, "daemon");
cJSON *dir_node = cJSON_GetObjectItem(root, "work_dir");
// 4. 赋值到配置结构体
strcpy(config->serv_ip, ip_node->valuestring);
config->serv_port = port_node->valueint;
config->daemon = daemon_node->valueint;
strcpy(config->work_dir, dir_node->valuestring);
// 5. 释放cJSON资源
cJSON_Delete(root);
return 0;
}
// 测试
int main() {
ServerConfig config;
if (read_server_config("server_config.json", &config) == 0) {
printf("serv_ip: %s\n", config.serv_ip);
printf("serv_port: %d\n", config.serv_port);
printf("daemon: %d\n", config.daemon);
printf("work_dir: %s\n", config.work_dir);
}
return 0;
}

四、Linux核心概念:PID/PGID/会话
要实现守护进程,必须先吃透Linux的PID(进程ID)、PGID(进程组ID)、会话(Session) 三个核心概念,这是守护进程脱离终端的底层基础。

1. PID和PGID
- PID:每个进程的唯一标识,由系统自动分配,非0正整数;
- PGID :进程组ID,一个进程组包含一个或多个进程,有一个组长进程(组长进程的PID=PGID);
- 进程组的作用:统一管理多个相关进程 ,比如向进程组发送信号,所有进程都会收到。

2. Linux会话(Session)
会话是一个或多个进程组的集合 ,有一个会话首进程 (会话首进程的PID=会话ID),手写笔记用物业公司和保洁公司的比喻,通俗讲清了三者的关系:
- 物业公司 = 会话:管理多个保洁公司;
- 保洁公司 = 进程组:管理多个保洁员;
- 保洁员 = 进程 ;


关键特性
- 会话有一个控制终端,通常是创建会话的终端,会话内的所有进程共享该终端;
- 终端关闭时,会向会话内的所有前台进程发送
SIGHUP信号,导致进程退出(这也是普通进程脱离终端就退出的原因); - 守护进程的核心就是脱离控制终端,创建独立会话 ,避免终端关闭导致进程退出。


五、前台/后台进程:终端与进程的关系
手写笔记通过物业公司和保洁公司 、前台/后台办公 的比喻,讲清了前台进程、后台进程与终端的关系,同时对比了Linux和Windows的会话差异。

1. 前台进程vs后台进程
- 前台进程:占用终端,接受终端的输入输出和信号,终端关闭则进程退出;
- 后台进程 :不占用终端,在后台运行,仍属于原会话,终端关闭仍会收到
SIGHUP信号退出; - Linux中用
./program &可将进程放到后台运行,但这不是守护进程 。


2. Linux与Windows的会话差异
- Linux:会话与终端强绑定,终端是会话的控制终端,注销/关闭终端会销毁会话,发送信号给进程;
- Windows :会话是用户级别的,注销用户会关闭该用户的所有会话和进程,与终端无关。



3. 注销与关闭会话的影响
- 注销:Linux中注销用户,会关闭该用户的所有会话,所有进程都会收到信号退出;
- 关闭会话:直接销毁会话,会话内的所有进程无论前台/后台,都会退出;
- 守护进程的核心目标就是摆脱这种依赖,实现无终端、无会话依赖的后台运行 。






六、守护进程vs后台进程vs前台进程
三者是网络编程中极易混淆的概念,手写笔记清晰梳理了三者的核心区别 ,关键在于是否依赖终端、是否属于原会话、是否常驻运行 。

三者核心区别表
| 进程类型 | 终端依赖 | 会话归属 | 常驻运行 | 信号影响(终端关闭) | 启动方式 |
|---|---|---|---|---|---|
| 前台进程 | 强依赖,占用终端 | 原会话 | 否 | 收到SIGHUP退出 | 直接./program |
| 后台进程 | 不占用,仍依赖 | 原会话 | 否 | 收到SIGHUP退出 | ./program & |
| 守护进程 | 完全脱离,无终端 | 独立新会话 | 是 | 无影响 | 代码实现守护进程化 |
七、守护进程实现:setsid函数与核心步骤
实现守护进程的核心函数是setsid() ,其作用是创建一个新的会话,让进程成为会话首进程,脱离原控制终端 ,但手写笔记明确标注了关键限制 :调用setsid的进程不能是进程组组长 。

setsid函数核心特性
c
#include <unistd.h>
pid_t setsid(void);
// 返回值:成功返回新的会话ID,失败返回-1
- 创建新会话,调用进程成为会话首进程,同时成为新的进程组组长;
- 脱离原控制终端,新会话无控制终端;
- 调用进程不能是原进程组组长,否则调用失败(这是核心限制)。
守护进程化的标准5步(解决setsid限制)
为了解决"不能是进程组组长"的问题,守护进程化的核心是先fork子进程,父进程退出,子进程调用setsid ,完整步骤如下,贴合手写笔记思路:

守护进程化代码实现
c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
// 守护进程化函数
int daemonize() {
// 第一步:fork子进程,父进程退出(子进程不是进程组组长,满足setsid要求)
pid_t pid = fork();
if (pid > 0) {
exit(EXIT_SUCCESS); // 父进程退出,子进程成为孤儿进程,由init进程收养
} else if (pid < 0) {
perror("fork error");
return -1;
}
// 第二步:调用setsid,创建新会话,脱离原终端
if (setsid() == -1) {
perror("setsid error");
return -1;
}
// 第三步:修改工作目录,避免挂载点卸载失败(如/目录)
if (chdir("/") == -1) {
perror("chdir error");
return -1;
}
// 第四步:设置文件掩码为0,方便进程创建文件/目录(继承的掩码会限制权限)
umask(0);
// 第五步:重定向标准输入/输出/错误到/dev/null(无终端,无需打印)
int fd = open("/dev/null", O_RDWR);
if (fd == -1) {
perror("open error");
return -1;
}
dup2(fd, 0); // 重定向标准输入
dup2(fd, 1); // 重定向标准输出
dup2(fd, 2); // 重定向标准错误
close(fd);
return 0;
}
// 测试:服务端主函数中调用
int main() {
// 先读取JSON配置,判断是否开启守护进程
ServerConfig config;
if (read_server_config("server_config.json", &config) == 0 && config.daemon == 1) {
if (daemonize() == -1) {
perror("daemonize error");
exit(EXIT_FAILURE);
}
printf("daemonize success...\n"); // 该输出会被重定向到/dev/null,无显示
}
// 后续执行TCP服务端的核心逻辑(socket/bind/listen/accept)
// ... 上篇的服务端代码 ...
return 0;
}
核心细节
- fork子进程:父进程退出后,子进程不是进程组组长,满足setsid的调用条件;
- chdir("/"):将工作目录切换到根目录,避免服务端运行在挂载目录(如U盘),卸载时导致进程出错;
- umask(0):清除文件掩码,让进程创建的文件/目录拥有完整权限(如0777);
- 重定向文件描述符 :将标准输入/输出/错误重定向到
/dev/null(空设备),因为守护进程无终端,无需打印输出。
八、整合:带守护进程+JSON配置的TCP服务端&客户端
将以上所有内容整合,最终的TCP网络程序包含以下核心功能,完美结合所有手写笔记图片的知识点:
- 客户端:连接成败处理,自定义协议构建请求字符串;
- 服务端:读取JSON配置,可选守护进程化,解析自定义请求协议;
- 底层:基于Linux PID/PGID/会话实现守护进程,脱离终端后台常驻。
同时附上完整的编译运行指令:
bash
# 编译客户端(带自定义协议)
gcc tcp_client.c -o tcp_client
# 编译服务端(带JSON配置+守护进程,链接cJSON库)
gcc tcp_server.c cJSON.c -o tcp_server -lcjson
# 运行服务端(守护进程化,后台常驻)
./tcp_server
# 运行客户端,测试通信
./tcp_client
# 查看守护进程是否运行
ps -ef | grep tcp_server

总结
本篇基于手写笔记,从TCP客户端开发 入手,讲解了连接成败的核心逻辑,然后通过自定义请求协议 规范客户端与服务端的通信,最后重点实现了服务端守护进程化,同时吃透了Linux PID/PGID/会话、前后台进程的底层概念。
核心要点:
- 客户端无需bind,失败退出、成功进入通信是核心准则;
- 自定义协议是解决字节流通信无格式的关键,BuildRequestString封装规范请求;
- JSON配置文件让服务端配置更灵活,避免硬编码;
- 守护进程的核心是脱离终端,依赖setsid函数,且必须先fork子进程解决组长限制;
- 后台进程≠守护进程,前者仍属于原会话,终端关闭仍会退出。
掌握这些内容,就实现了从"基础TCP通信"到"实际生产级服务端"的跨越,后续可在此基础上扩展多客户端处理 、粘包/拆包解决 、信号处理 等高级功能。# Linux TCP网络编程:客户端开发+守护进程实战
上篇我们实现了TCP服务端的基础开发,本篇聚焦TCP客户端开发 、自定义请求协议构建 和服务端守护进程化,同时吃透Linux进程、会话、前后台进程的核心概念,所有内容结合手写笔记图片展开,从代码实现到底层原理一站式讲清,零基础也能上手。
一、TCP客户端开发:连接成败逻辑
客户端核心逻辑很简单:与服务端建立连接,失败则直接退出程序,成功则进入通信流程 ,这是客户端开发的基础准则,也是手写笔记的核心思路。

客户端核心开发步骤
客户端比服务端少了bind和listen步骤,核心只有创建套接字→初始化服务端地址→连接服务端→读写通信→关闭套接字5步,且所有系统调用失败后直接退出,符合"失败退出"的逻辑。
客户端完整代码(含成败处理)
c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define SERV_IP "127.0.0.1" // 服务端IP
#define SERV_PORT 8888 // 服务端端口
int main() {
// 1. 创建套接字(和服务端一致,TCP用SOCK_STREAM)
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1) {
perror("socket error");
exit(EXIT_FAILURE); // 失败:直接退出
}
// 2. 初始化服务端地址结构体(要连接的服务端IP+端口)
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr); // 字符串IP转网络字节序
// 3. 连接服务端(核心:connect失败则退出)
int ret = connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if (ret == -1) {
perror("connect error");
close(cfd);
exit(EXIT_FAILURE); // 失败:释放资源后退出
}
printf("connect server success...\n"); // 成功:进入通信流程
// 4. 读写通信(向服务端发送数据)
char buf[1024] = "hello tcp server!";
write(cfd, buf, strlen(buf));
bzero(buf, sizeof(buf));
ret = read(cfd, buf, sizeof(buf)); // 读取服务端回显
printf("server say: %s\n", buf);
// 5. 关闭套接字
close(cfd);
return 0;
}
核心细节
- 客户端无需绑定自身IP和端口,系统会自动分配随机端口,这是和服务端的关键区别;
inet_pton用于将字符串格式的IP (如127.0.0.1)转为网络字节序的整型,比htonl更易使用;- 所有系统调用失败后,先释放已申请的文件描述符,再退出,避免资源泄漏。
二、自定义请求协议:构建规范请求字符串
基础的字节流通信没有统一格式,服务端无法解析复杂请求,因此需要搭建自定义请求协议 ,通过BuildRequestString函数封装规范的请求字符串 ,让客户端和服务端的通信有统一标准。

协议设计思路
手写笔记的核心是封装请求头+请求体 ,请求头包含请求的核心信息(如请求类型、数据长度),请求体是实际的业务数据,这样服务端能先解析请求头,再根据数据长度读取请求体,避免粘包/拆包问题。

BuildRequestString函数实现
该函数的作用是按自定义协议,拼接请求头和请求体,生成规范的请求字符串,参数为请求类型、业务数据,返回拼接后的请求字符串。
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 自定义请求协议:请求头|请求体 例:type=1&len=12|hello server
#define REQ_HEAD_FMT "type=%d&len=%d" // 请求头格式:请求类型+数据长度
#define SEP "|" // 头和体的分隔符
// 构建请求字符串:req_type=请求类型,data=请求体,buf=输出的请求字符串
int BuildRequestString(int req_type, const char *data, char *buf, int buf_len) {
if (data == NULL || buf == NULL || buf_len <= 0) {
return -1;
}
int data_len = strlen(data);
char req_head[128] = {0};
// 1. 拼接请求头
snprintf(req_head, sizeof(req_head), REQ_HEAD_FMT, req_type, data_len);
// 2. 拼接请求头+分隔符+请求体,生成最终请求字符串
int ret = snprintf(buf, buf_len, "%s%s%s", req_head, SEP, data);
if (ret >= buf_len || ret < 0) {
return -1; // 缓冲区不足或拼接失败
}
return ret; // 返回请求字符串长度
}
// 测试函数
int main() {
char req_buf[1024] = {0};
int len = BuildRequestString(1, "get server info", req_buf, sizeof(req_buf));
if (len > 0) {
printf("build request success: %s\n", req_buf);
} else {
printf("build request error\n");
}
return 0;
}
运行结果(成功构建)
编译运行后,会生成规范的请求字符串,客户端直接发送该字符串,服务端按协议解析即可,完美契合手写笔记的"成功"逻辑。

三、服务端守护进程化:后台常驻运行
实际开发中,TCP服务端需要脱离终端、后台常驻运行 ,这就是守护进程(精灵进程) ,同时服务端的配置(IP、端口、守护进程开关)需要通过JSON配置文件 管理,避免硬编码,方便运维。

第一步:JSON配置文件设计
JSON配置文件简洁易读,适合存储服务端的基础配置,手写笔记中包含服务端IP、端口、是否守护进程、工作目录等核心配置,示例如下:
json
{
"serv_ip": "0.0.0.0",
"serv_port": 8888,
"daemon": 1,
"work_dir": "/home/tcp_server"
}

第二步:C语言读取JSON配置(cJSON库)
C语言本身不支持JSON解析,需使用cJSON轻量级库 ,核心步骤是读取配置文件→解析JSON节点→获取配置值 ,代码如下(需链接cJSON库:gcc xxx.c -o xxx -lcjson):
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
// 定义配置结构体
typedef struct {
char serv_ip[32];
int serv_port;
int daemon;
char work_dir[128];
} ServerConfig;
// 读取JSON配置文件
int read_server_config(const char *config_path, ServerConfig *config) {
if (config_path == NULL || config == NULL) {
return -1;
}
// 1. 读取文件内容
FILE *fp = fopen(config_path, "r");
if (fp == NULL) {
perror("fopen error");
return -1;
}
fseek(fp, 0, SEEK_END);
int file_len = ftell(fp);
fseek(fp, 0, SEEK_SET);
char *file_buf = (char*)malloc(file_len + 1);
fread(file_buf, 1, file_len, fp);
fclose(fp);
// 2. 解析JSON
cJSON *root = cJSON_Parse(file_buf);
free(file_buf);
if (root == NULL) {
printf("cJSON parse error\n");
return -1;
}
// 3. 获取各配置节点
cJSON *ip_node = cJSON_GetObjectItem(root, "serv_ip");
cJSON *port_node = cJSON_GetObjectItem(root, "serv_port");
cJSON *daemon_node = cJSON_GetObjectItem(root, "daemon");
cJSON *dir_node = cJSON_GetObjectItem(root, "work_dir");
// 4. 赋值到配置结构体
strcpy(config->serv_ip, ip_node->valuestring);
config->serv_port = port_node->valueint;
config->daemon = daemon_node->valueint;
strcpy(config->work_dir, dir_node->valuestring);
// 5. 释放cJSON资源
cJSON_Delete(root);
return 0;
}
// 测试
int main() {
ServerConfig config;
if (read_server_config("server_config.json", &config) == 0) {
printf("serv_ip: %s\n", config.serv_ip);
printf("serv_port: %d\n", config.serv_port);
printf("daemon: %d\n", config.daemon);
printf("work_dir: %s\n", config.work_dir);
}
return 0;
}

四、Linux核心概念:PID/PGID/会话
要实现守护进程,必须先吃透Linux的PID(进程ID)、PGID(进程组ID)、会话(Session) 三个核心概念,这是守护进程脱离终端的底层基础。

1. PID和PGID
- PID:每个进程的唯一标识,由系统自动分配,非0正整数;
- PGID :进程组ID,一个进程组包含一个或多个进程,有一个组长进程(组长进程的PID=PGID);
- 进程组的作用:统一管理多个相关进程 ,比如向进程组发送信号,所有进程都会收到。

2. Linux会话(Session)
会话是一个或多个进程组的集合 ,有一个会话首进程 (会话首进程的PID=会话ID),手写笔记用物业公司和保洁公司的比喻,通俗讲清了三者的关系:
- 物业公司 = 会话:管理多个保洁公司;
- 保洁公司 = 进程组:管理多个保洁员;
- 保洁员 = 进程 ;


关键特性
- 会话有一个控制终端,通常是创建会话的终端,会话内的所有进程共享该终端;
- 终端关闭时,会向会话内的所有前台进程发送
SIGHUP信号,导致进程退出(这也是普通进程脱离终端就退出的原因); - 守护进程的核心就是脱离控制终端,创建独立会话 ,避免终端关闭导致进程退出。


五、前台/后台进程:终端与进程的关系
手写笔记通过物业公司和保洁公司 、前台/后台办公 的比喻,讲清了前台进程、后台进程与终端的关系,同时对比了Linux和Windows的会话差异。

1. 前台进程vs后台进程
- 前台进程:占用终端,接受终端的输入输出和信号,终端关闭则进程退出;
- 后台进程 :不占用终端,在后台运行,仍属于原会话,终端关闭仍会收到
SIGHUP信号退出; - Linux中用
./program &可将进程放到后台运行,但这不是守护进程 。


2. Linux与Windows的会话差异
- Linux:会话与终端强绑定,终端是会话的控制终端,注销/关闭终端会销毁会话,发送信号给进程;
- Windows :会话是用户级别的,注销用户会关闭该用户的所有会话和进程,与终端无关。



3. 注销与关闭会话的影响
- 注销:Linux中注销用户,会关闭该用户的所有会话,所有进程都会收到信号退出;
- 关闭会话:直接销毁会话,会话内的所有进程无论前台/后台,都会退出;
- 守护进程的核心目标就是摆脱这种依赖,实现无终端、无会话依赖的后台运行 。






六、守护进程vs后台进程vs前台进程
三者是网络编程中极易混淆的概念,手写笔记清晰梳理了三者的核心区别 ,关键在于是否依赖终端、是否属于原会话、是否常驻运行 。

三者核心区别表
| 进程类型 | 终端依赖 | 会话归属 | 常驻运行 | 信号影响(终端关闭) | 启动方式 |
|---|---|---|---|---|---|
| 前台进程 | 强依赖,占用终端 | 原会话 | 否 | 收到SIGHUP退出 | 直接./program |
| 后台进程 | 不占用,仍依赖 | 原会话 | 否 | 收到SIGHUP退出 | ./program & |
| 守护进程 | 完全脱离,无终端 | 独立新会话 | 是 | 无影响 | 代码实现守护进程化 |
七、守护进程实现:setsid函数与核心步骤
实现守护进程的核心函数是setsid() ,其作用是创建一个新的会话,让进程成为会话首进程,脱离原控制终端 ,但手写笔记明确标注了关键限制 :调用setsid的进程不能是进程组组长 。

setsid函数核心特性
c
#include <unistd.h>
pid_t setsid(void);
// 返回值:成功返回新的会话ID,失败返回-1
- 创建新会话,调用进程成为会话首进程,同时成为新的进程组组长;
- 脱离原控制终端,新会话无控制终端;
- 调用进程不能是原进程组组长,否则调用失败(这是核心限制)。
守护进程化的标准5步(解决setsid限制)
为了解决"不能是进程组组长"的问题,守护进程化的核心是先fork子进程,父进程退出,子进程调用setsid ,完整步骤如下,贴合手写笔记思路:

守护进程化代码实现
c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
// 守护进程化函数
int daemonize() {
// 第一步:fork子进程,父进程退出(子进程不是进程组组长,满足setsid要求)
pid_t pid = fork();
if (pid > 0) {
exit(EXIT_SUCCESS); // 父进程退出,子进程成为孤儿进程,由init进程收养
} else if (pid < 0) {
perror("fork error");
return -1;
}
// 第二步:调用setsid,创建新会话,脱离原终端
if (setsid() == -1) {
perror("setsid error");
return -1;
}
// 第三步:修改工作目录,避免挂载点卸载失败(如/目录)
if (chdir("/") == -1) {
perror("chdir error");
return -1;
}
// 第四步:设置文件掩码为0,方便进程创建文件/目录(继承的掩码会限制权限)
umask(0);
// 第五步:重定向标准输入/输出/错误到/dev/null(无终端,无需打印)
int fd = open("/dev/null", O_RDWR);
if (fd == -1) {
perror("open error");
return -1;
}
dup2(fd, 0); // 重定向标准输入
dup2(fd, 1); // 重定向标准输出
dup2(fd, 2); // 重定向标准错误
close(fd);
return 0;
}
// 测试:服务端主函数中调用
int main() {
// 先读取JSON配置,判断是否开启守护进程
ServerConfig config;
if (read_server_config("server_config.json", &config) == 0 && config.daemon == 1) {
if (daemonize() == -1) {
perror("daemonize error");
exit(EXIT_FAILURE);
}
printf("daemonize success...\n"); // 该输出会被重定向到/dev/null,无显示
}
// 后续执行TCP服务端的核心逻辑(socket/bind/listen/accept)
// ... 上篇的服务端代码 ...
return 0;
}
核心细节
- fork子进程:父进程退出后,子进程不是进程组组长,满足setsid的调用条件;
- chdir("/"):将工作目录切换到根目录,避免服务端运行在挂载目录(如U盘),卸载时导致进程出错;
- umask(0):清除文件掩码,让进程创建的文件/目录拥有完整权限(如0777);
- 重定向文件描述符 :将标准输入/输出/错误重定向到
/dev/null(空设备),因为守护进程无终端,无需打印输出。
八、整合:带守护进程+JSON配置的TCP服务端&客户端
将以上所有内容整合,最终的TCP网络程序包含以下核心功能,完美结合所有手写笔记图片的知识点:
- 客户端:连接成败处理,自定义协议构建请求字符串;
- 服务端:读取JSON配置,可选守护进程化,解析自定义请求协议;
- 底层:基于Linux PID/PGID/会话实现守护进程,脱离终端后台常驻。
同时附上完整的编译运行指令:
bash
# 编译客户端(带自定义协议)
gcc tcp_client.c -o tcp_client
# 编译服务端(带JSON配置+守护进程,链接cJSON库)
gcc tcp_server.c cJSON.c -o tcp_server -lcjson
# 运行服务端(守护进程化,后台常驻)
./tcp_server
# 运行客户端,测试通信
./tcp_client
# 查看守护进程是否运行
ps -ef | grep tcp_server
