欢迎光临
我们一直在努力

【Linux网络编程】 Linux TCP网络编程:客户端开发+守护进程实战

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

在这里插入图片描述

IF’Maxue:个人主页

 🔥 个人专栏: 《C语言》 《C++深度学习》 《Linux》 《数据结构》 《数学建模》

⛺️生活是默默的坚持,毅力是永久的享受。不破不立!

文章目录

    • 一、TCP客户端开发:连接成败逻辑
      • 客户端核心开发步骤
      • 客户端完整代码(含成败处理)
      • 核心细节
    • 二、自定义请求协议:构建规范请求字符串
      • 协议设计思路
      • BuildRequestString函数实现
      • 运行结果(成功构建)
    • 三、服务端守护进程化:后台常驻运行
      • 第一步:JSON配置文件设计
      • 第二步:C语言读取JSON配置(cJSON库)
    • 四、Linux核心概念:PID/PGID/会话
      • 1. PID和PGID
      • 2. Linux会话(Session)
      • 关键特性
    • 五、前台/后台进程:终端与进程的关系
      • 1. 前台进程vs后台进程
      • 2. Linux与Windows的会话差异
      • 3. 注销与关闭会话的影响
    • 六、守护进程vs后台进程vs前台进程
      • 三者核心区别表
    • 七、守护进程实现:setsid函数与核心步骤
      • setsid函数核心特性
      • 守护进程化的标准5步(解决setsid限制)
      • 守护进程化代码实现
      • 核心细节
    • 八、整合:带守护进程+JSON配置的TCP服务端&客户端
    • 总结
    • 一、TCP客户端开发:连接成败逻辑
      • 客户端核心开发步骤
      • 客户端完整代码(含成败处理)
      • 核心细节
    • 二、自定义请求协议:构建规范请求字符串
      • 协议设计思路
      • BuildRequestString函数实现
      • 运行结果(成功构建)
    • 三、服务端守护进程化:后台常驻运行
      • 第一步:JSON配置文件设计
      • 第二步:C语言读取JSON配置(cJSON库)
    • 四、Linux核心概念:PID/PGID/会话
      • 1. PID和PGID
      • 2. Linux会话(Session)
      • 关键特性
    • 五、前台/后台进程:终端与进程的关系
      • 1. 前台进程vs后台进程
      • 2. Linux与Windows的会话差异
      • 3. 注销与关闭会话的影响
    • 六、守护进程vs后台进程vs前台进程
      • 三者核心区别表
    • 七、守护进程实现:setsid函数与核心步骤
      • setsid函数核心特性
      • 守护进程化的标准5步(解决setsid限制)
      • 守护进程化代码实现
      • 核心细节
    • 八、整合:带守护进程+JSON配置的TCP服务端&客户端

一、TCP客户端开发:连接成败逻辑

客户端核心逻辑很简单:与服务端建立连接,失败则直接退出程序,成功则进入通信流程,这是客户端开发的基础准则,也是手写笔记的核心思路。 image.png

客户端核心开发步骤

客户端比服务端少了bind和listen步骤,核心只有创建套接字→初始化服务端地址→连接服务端→读写通信→关闭套接字5步,且所有系统调用失败后直接退出,符合“失败退出”的逻辑。

客户端完整代码(含成败处理)

#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函数封装规范的请求字符串,让客户端和服务端的通信有统一标准。 image.png

协议设计思路

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

BuildRequestString函数实现

该函数的作用是按自定义协议,拼接请求头和请求体,生成规范的请求字符串,参数为请求类型、业务数据,返回拼接后的请求字符串。

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

运行结果(成功构建)

编译运行后,会生成规范的请求字符串,客户端直接发送该字符串,服务端按协议解析即可,完美契合手写笔记的“成功”逻辑。 image.png

三、服务端守护进程化:后台常驻运行

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

第一步:JSON配置文件设计

JSON配置文件简洁易读,适合存储服务端的基础配置,手写笔记中包含服务端IP、端口、是否守护进程、工作目录等核心配置,示例如下:

{
"serv_ip": "0.0.0.0",
"serv_port": 8888,
"daemon": 1,
"work_dir": "/home/tcp_server"
}

image.png

第二步:C语言读取JSON配置(cJSON库)

C语言本身不支持JSON解析,需使用cJSON轻量级库,核心步骤是读取配置文件→解析JSON节点→获取配置值,代码如下(需链接cJSON库:gcc xxx.c -o xxx -lcjson):

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

image.png

四、Linux核心概念:PID/PGID/会话

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

1. PID和PGID

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

2. Linux会话(Session)

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

  • 物业公司 = 会话:管理多个保洁公司;
  • 保洁公司 = 进程组:管理多个保洁员;
  • 保洁员 = 进程; image.png

image.png

关键特性

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

image.png

五、前台/后台进程:终端与进程的关系

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

1. 前台进程vs后台进程

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

image.png

2. Linux与Windows的会话差异

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

image.png

image.png

3. 注销与关闭会话的影响

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

image.png

image.png

image.png

image.png

image.png

六、守护进程vs后台进程vs前台进程

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

三者核心区别表

进程类型终端依赖会话归属常驻运行信号影响(终端关闭)启动方式
前台进程 强依赖,占用终端 原会话 收到SIGHUP退出 直接./program
后台进程 不占用,仍依赖 原会话 收到SIGHUP退出 ./program &
守护进程 完全脱离,无终端 独立新会话 无影响 代码实现守护进程化

七、守护进程实现:setsid函数与核心步骤

实现守护进程的核心函数是setsid(),其作用是创建一个新的会话,让进程成为会话首进程,脱离原控制终端,但手写笔记明确标注了关键限制:调用setsid的进程不能是进程组组长。 image.png

setsid函数核心特性

#include <unistd.h>
pid_t setsid(void);
// 返回值:成功返回新的会话ID,失败返回-1

  • 创建新会话,调用进程成为会话首进程,同时成为新的进程组组长;
  • 脱离原控制终端,新会话无控制终端;
  • 调用进程不能是原进程组组长,否则调用失败(这是核心限制)。
  • 守护进程化的标准5步(解决setsid限制)

    为了解决“不能是进程组组长”的问题,守护进程化的核心是先fork子进程,父进程退出,子进程调用setsid,完整步骤如下,贴合手写笔记思路: image.png

    守护进程化代码实现

    #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/会话实现守护进程,脱离终端后台常驻。
  • 同时附上完整的编译运行指令:

    # 编译客户端(带自定义协议)
    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

    image.png

    总结

    本篇基于手写笔记,从TCP客户端开发入手,讲解了连接成败的核心逻辑,然后通过自定义请求协议规范客户端与服务端的通信,最后重点实现了服务端守护进程化,同时吃透了Linux PID/PGID/会话、前后台进程的底层概念。

    核心要点:

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

    一、TCP客户端开发:连接成败逻辑

    客户端核心逻辑很简单:与服务端建立连接,失败则直接退出程序,成功则进入通信流程,这是客户端开发的基础准则,也是手写笔记的核心思路。 image.png

    客户端核心开发步骤

    客户端比服务端少了bind和listen步骤,核心只有创建套接字→初始化服务端地址→连接服务端→读写通信→关闭套接字5步,且所有系统调用失败后直接退出,符合“失败退出”的逻辑。

    客户端完整代码(含成败处理)

    #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函数封装规范的请求字符串,让客户端和服务端的通信有统一标准。 image.png

    协议设计思路

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

    BuildRequestString函数实现

    该函数的作用是按自定义协议,拼接请求头和请求体,生成规范的请求字符串,参数为请求类型、业务数据,返回拼接后的请求字符串。

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

    运行结果(成功构建)

    编译运行后,会生成规范的请求字符串,客户端直接发送该字符串,服务端按协议解析即可,完美契合手写笔记的“成功”逻辑。 image.png

    三、服务端守护进程化:后台常驻运行

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

    第一步:JSON配置文件设计

    JSON配置文件简洁易读,适合存储服务端的基础配置,手写笔记中包含服务端IP、端口、是否守护进程、工作目录等核心配置,示例如下:

    {
    "serv_ip": "0.0.0.0",
    "serv_port": 8888,
    "daemon": 1,
    "work_dir": "/home/tcp_server"
    }

    image.png

    第二步:C语言读取JSON配置(cJSON库)

    C语言本身不支持JSON解析,需使用cJSON轻量级库,核心步骤是读取配置文件→解析JSON节点→获取配置值,代码如下(需链接cJSON库:gcc xxx.c -o xxx -lcjson):

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

    image.png

    四、Linux核心概念:PID/PGID/会话

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

    1. PID和PGID

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

    2. Linux会话(Session)

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

    • 物业公司 = 会话:管理多个保洁公司;
    • 保洁公司 = 进程组:管理多个保洁员;
    • 保洁员 = 进程; image.png

    image.png

    关键特性

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

    image.png

    五、前台/后台进程:终端与进程的关系

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

    1. 前台进程vs后台进程

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

    image.png

    2. Linux与Windows的会话差异

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

    image.png

    image.png

    3. 注销与关闭会话的影响

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

    image.png

    image.png

    image.png

    image.png

    image.png

    六、守护进程vs后台进程vs前台进程

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

    三者核心区别表

    进程类型终端依赖会话归属常驻运行信号影响(终端关闭)启动方式
    前台进程 强依赖,占用终端 原会话 收到SIGHUP退出 直接./program
    后台进程 不占用,仍依赖 原会话 收到SIGHUP退出 ./program &
    守护进程 完全脱离,无终端 独立新会话 无影响 代码实现守护进程化

    七、守护进程实现:setsid函数与核心步骤

    实现守护进程的核心函数是setsid(),其作用是创建一个新的会话,让进程成为会话首进程,脱离原控制终端,但手写笔记明确标注了关键限制:调用setsid的进程不能是进程组组长。 image.png

    setsid函数核心特性

    #include <unistd.h>
    pid_t setsid(void);
    // 返回值:成功返回新的会话ID,失败返回-1

  • 创建新会话,调用进程成为会话首进程,同时成为新的进程组组长;
  • 脱离原控制终端,新会话无控制终端;
  • 调用进程不能是原进程组组长,否则调用失败(这是核心限制)。
  • 守护进程化的标准5步(解决setsid限制)

    为了解决“不能是进程组组长”的问题,守护进程化的核心是先fork子进程,父进程退出,子进程调用setsid,完整步骤如下,贴合手写笔记思路: image.png

    守护进程化代码实现

    #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/会话实现守护进程,脱离终端后台常驻。
  • 同时附上完整的编译运行指令:

    # 编译客户端(带自定义协议)
    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

    image.png

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux网络编程】 Linux TCP网络编程:客户端开发+守护进程实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址