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

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

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

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

文章目录

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

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

客户端核心开发步骤

客户端比服务端少了bindlisten步骤,核心只有创建套接字→初始化服务端地址→连接服务端→读写通信→关闭套接字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
  1. 创建新会话,调用进程成为会话首进程,同时成为新的进程组组长;
  2. 脱离原控制终端,新会话无控制终端
  3. 调用进程不能是原进程组组长,否则调用失败(这是核心限制)。

守护进程化的标准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;
}

核心细节

  1. fork子进程:父进程退出后,子进程不是进程组组长,满足setsid的调用条件;
  2. chdir("/"):将工作目录切换到根目录,避免服务端运行在挂载目录(如U盘),卸载时导致进程出错;
  3. umask(0):清除文件掩码,让进程创建的文件/目录拥有完整权限(如0777);
  4. 重定向文件描述符 :将标准输入/输出/错误重定向到/dev/null(空设备),因为守护进程无终端,无需打印输出。

八、整合:带守护进程+JSON配置的TCP服务端&客户端

将以上所有内容整合,最终的TCP网络程序包含以下核心功能,完美结合所有手写笔记图片的知识点:

  1. 客户端:连接成败处理,自定义协议构建请求字符串;
  2. 服务端:读取JSON配置,可选守护进程化,解析自定义请求协议;
  3. 底层:基于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/会话、前后台进程的底层概念。

核心要点:

  1. 客户端无需bind,失败退出、成功进入通信是核心准则;
  2. 自定义协议是解决字节流通信无格式的关键,BuildRequestString封装规范请求;
  3. JSON配置文件让服务端配置更灵活,避免硬编码;
  4. 守护进程的核心是脱离终端,依赖setsid函数,且必须先fork子进程解决组长限制;
  5. 后台进程≠守护进程,前者仍属于原会话,终端关闭仍会退出。

掌握这些内容,就实现了从"基础TCP通信"到"实际生产级服务端"的跨越,后续可在此基础上扩展多客户端处理粘包/拆包解决信号处理 等高级功能。# Linux TCP网络编程:客户端开发+守护进程实战

上篇我们实现了TCP服务端的基础开发,本篇聚焦TCP客户端开发自定义请求协议构建服务端守护进程化,同时吃透Linux进程、会话、前后台进程的核心概念,所有内容结合手写笔记图片展开,从代码实现到底层原理一站式讲清,零基础也能上手。

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

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

客户端核心开发步骤

客户端比服务端少了bindlisten步骤,核心只有创建套接字→初始化服务端地址→连接服务端→读写通信→关闭套接字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
  1. 创建新会话,调用进程成为会话首进程,同时成为新的进程组组长;
  2. 脱离原控制终端,新会话无控制终端
  3. 调用进程不能是原进程组组长,否则调用失败(这是核心限制)。

守护进程化的标准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;
}

核心细节

  1. fork子进程:父进程退出后,子进程不是进程组组长,满足setsid的调用条件;
  2. chdir("/"):将工作目录切换到根目录,避免服务端运行在挂载目录(如U盘),卸载时导致进程出错;
  3. umask(0):清除文件掩码,让进程创建的文件/目录拥有完整权限(如0777);
  4. 重定向文件描述符 :将标准输入/输出/错误重定向到/dev/null(空设备),因为守护进程无终端,无需打印输出。

八、整合:带守护进程+JSON配置的TCP服务端&客户端

将以上所有内容整合,最终的TCP网络程序包含以下核心功能,完美结合所有手写笔记图片的知识点:

  1. 客户端:连接成败处理,自定义协议构建请求字符串;
  2. 服务端:读取JSON配置,可选守护进程化,解析自定义请求协议;
  3. 底层:基于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
相关推荐
永不复还2 小时前
linux 使用Xcb监听键盘鼠标输入
linux·x11·xcb
mango_mangojuice2 小时前
Linux学习笔记 1.19
linux·服务器·数据库·笔记·学习
i建模2 小时前
linux断点续传下载文件
linux·运维·服务器
Aaron15882 小时前
通信灵敏度计算与雷达灵敏度计算对比分析
网络·人工智能·深度学习·算法·fpga开发·信息与通信·信号处理
执笔论英雄2 小时前
【RL]分离部署与共置模式详解
服务器·网络·windows
木卫二号Coding2 小时前
Docker-构建自己的Web-Linux系统-Ubuntu:22.04
linux·前端·docker
拍客圈2 小时前
Discuz CC 防护规则
服务器·网络·安全
文章永久免费只为良心3 小时前
一站式综合查询工具:IP、企业信息与网络空间资产高效查询工具
网络·网络协议·tcp/ip
小天源3 小时前
CentOS 7介绍及其下载
linux·运维·ubuntu·centos·麒麟·windows11·windows10