欢迎光临
我们一直在努力

《UNIX环境高级编程》读书笔记22: 与网络打印机通信

作者: andylin02
学习章节: 第21章 与网络打印机通信
关键词:网络打印机;打印假脱机;printd;CUPS;IPP;LPD;套接字

一、引言

本章是APUE全书的最后一个实例章节。在此前的章节中,我们学习了进程间通信、套接字、守护进程、线程、信号等核心概念。本章将这些知识综合运用,构建一个与网络打印机通信的实际应用程序。

核心内容包括:

  • 网络打印协议(IPP)的基本概念
  • 打印假脱机系统(CUPS)的架构
  • print客户端程序的设计与实现
  • printd打印守护进程的设计与实现

💡 学习建议:本章的重点不是理解网络打印协议的所有细节,而是观察如何综合运用前面学过的知识(守护进程、套接字、线程、信号、记录锁等)构建一个实用的网络服务程序。建议将print和printd的代码与实际运行的CUPS系统进行对比,思考两者在设计上的异同。

二、网络打印协议概述

2.1 从本地打印到网络打印

早期UNIX系统使用行式打印机后台程序(LPD,Line Printer Daemon)作为打印服务的基础设施。LPD协议定义在RFC 1179中,它使用TCP端口515进行通信。

当用户执行lpr命令时,lpr会通过套接字连接到lpd守护进程,将要打印的文件和打印参数传递给守护进程,然后lpd将文件送到适当的打印机。

2.2 IPP(互联网打印协议)

随着互联网的发展,传统的LPD协议逐渐被IPP(Internet Printing Protocol,互联网打印协议)所取代。IPP是一个在互联网上打印的标准网络协议,它允许用户透过互联网进行远程打印及管理打印工作。

IPP的核心特点:

特点说明
基于HTTP IPP建立在HTTP/1.1之上,使用HTTP作为传输协议
标准端口 使用TCP端口631
安全性 支持存取控制、认证及加密
功能丰富 支持纸张种类、分辨率等各种打印参数的控制

IPP的设计初衷之一是为了取代传真,让用户可以通过安装相应的驱动程序来进行远程打印。用户可以通过浏览器从远端了解办公室内各个打印机的状况。

📖 IPP与LPD的对比:LPD协议相对简单,但功能有限;IPP基于HTTP,功能更强大但协议也更复杂。现代UNIX/Linux系统(如macOS和Linux发行版)普遍采用CUPS(Common UNIX Printing System)作为打印系统,而CUPS的核心就是IPP协议。

2.3 CUPS(通用UNIX打印系统)

所有UNIX系统至少提供一个打印假脱机系统。在Linux和macOS X上,这个系统是CUPS(Common UNIX Printing System)。

CUPS的核心架构:

#mermaid-svg-yvu2OtYc2xwCBQnA{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yvu2OtYc2xwCBQnA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yvu2OtYc2xwCBQnA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yvu2OtYc2xwCBQnA .error-icon{fill:#552222;}#mermaid-svg-yvu2OtYc2xwCBQnA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yvu2OtYc2xwCBQnA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yvu2OtYc2xwCBQnA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yvu2OtYc2xwCBQnA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yvu2OtYc2xwCBQnA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yvu2OtYc2xwCBQnA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yvu2OtYc2xwCBQnA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yvu2OtYc2xwCBQnA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yvu2OtYc2xwCBQnA .marker.cross{stroke:#333333;}#mermaid-svg-yvu2OtYc2xwCBQnA svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yvu2OtYc2xwCBQnA p{margin:0;}#mermaid-svg-yvu2OtYc2xwCBQnA .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-yvu2OtYc2xwCBQnA .cluster-label text{fill:#333;}#mermaid-svg-yvu2OtYc2xwCBQnA .cluster-label span{color:#333;}#mermaid-svg-yvu2OtYc2xwCBQnA .cluster-label span p{background-color:transparent;}#mermaid-svg-yvu2OtYc2xwCBQnA .label text,#mermaid-svg-yvu2OtYc2xwCBQnA span{fill:#333;color:#333;}#mermaid-svg-yvu2OtYc2xwCBQnA .node rect,#mermaid-svg-yvu2OtYc2xwCBQnA .node circle,#mermaid-svg-yvu2OtYc2xwCBQnA .node ellipse,#mermaid-svg-yvu2OtYc2xwCBQnA .node polygon,#mermaid-svg-yvu2OtYc2xwCBQnA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yvu2OtYc2xwCBQnA .rough-node .label text,#mermaid-svg-yvu2OtYc2xwCBQnA .node .label text,#mermaid-svg-yvu2OtYc2xwCBQnA .image-shape .label,#mermaid-svg-yvu2OtYc2xwCBQnA .icon-shape .label{text-anchor:middle;}#mermaid-svg-yvu2OtYc2xwCBQnA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yvu2OtYc2xwCBQnA .rough-node .label,#mermaid-svg-yvu2OtYc2xwCBQnA .node .label,#mermaid-svg-yvu2OtYc2xwCBQnA .image-shape .label,#mermaid-svg-yvu2OtYc2xwCBQnA .icon-shape .label{text-align:center;}#mermaid-svg-yvu2OtYc2xwCBQnA .node.clickable{cursor:pointer;}#mermaid-svg-yvu2OtYc2xwCBQnA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yvu2OtYc2xwCBQnA .arrowheadPath{fill:#333333;}#mermaid-svg-yvu2OtYc2xwCBQnA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yvu2OtYc2xwCBQnA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yvu2OtYc2xwCBQnA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yvu2OtYc2xwCBQnA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yvu2OtYc2xwCBQnA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yvu2OtYc2xwCBQnA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yvu2OtYc2xwCBQnA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yvu2OtYc2xwCBQnA .cluster text{fill:#333;}#mermaid-svg-yvu2OtYc2xwCBQnA .cluster span{color:#333;}#mermaid-svg-yvu2OtYc2xwCBQnA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yvu2OtYc2xwCBQnA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yvu2OtYc2xwCBQnA rect.text{fill:none;stroke-width:0;}#mermaid-svg-yvu2OtYc2xwCBQnA .icon-shape,#mermaid-svg-yvu2OtYc2xwCBQnA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yvu2OtYc2xwCBQnA .icon-shape p,#mermaid-svg-yvu2OtYc2xwCBQnA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yvu2OtYc2xwCBQnA .icon-shape .label rect,#mermaid-svg-yvu2OtYc2xwCBQnA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yvu2OtYc2xwCBQnA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yvu2OtYc2xwCBQnA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yvu2OtYc2xwCBQnA :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

打印机

CUPS系统

用户空间

IPP请求

应用程序vi/浏览器/命令行

lpr/lp命令

cups库

cupsd守护进程端口631

打印队列每个打印机一个队列

过滤器链PDF→PS→打印机格式

后端USB/网络/IPP

物理打印机支持IPP

网络打印机LPD/IPP

CUPS的设计围绕一个中央打印调度进程展开,该进程负责调度打印作业、处理管理命令、向本地和远程程序提供打印机状态信息以及根据需要通知用户。

三、print客户端程序

3.1 功能概述

print程序是本章示例中的客户端程序,负责接收用户的打印请求并与服务器进行通信。

print程序的主要职责:

  • 解析命令行参数(如-t表示标题)
  • 建立与服务器的TCP连接
  • 将待打印的文件发送给服务器
  • 支持从标准输入读取要打印的数据
  • 3.2 print命令的使用方式

    # 打印本地文件
    print filename

    # 打印带标题的文件
    print -t "My Document" filename

    # 从标准输入打印
    cat file | print

    3.3 print客户端核心代码

    #include "apue.h"
    #include <sys/socket.h>
    #include <sys/un.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <netdb.h>
    #include <errno.h>

    #define MAXLINE 4096

    /* 打印服务器配置 */
    #define PRINT_SERVER_PORT "7777" /* 打印服务端口 */
    #define PRINT_SERVER_HOST "localhost"

    /* 解析命令行参数并提交打印作业 */
    int main(int argc, char *argv[])
    {
    int sockfd, n, fd;
    struct addrinfo hints, *res, *ressave;
    char buf[MAXLINE];
    char *filename = NULL;
    char *title = NULL;
    int c;

    /* 解析命令行参数 */
    while ((c = getopt(argc, argv, "t:")) != 1) {
    switch (c) {
    case 't':
    title = optarg;
    break;
    case '?':
    fprintf(stderr, "usage: print [-t title] [filename]\\n");
    exit(1);
    }
    }

    /* 处理文件名参数 */
    if (optind < argc) {
    filename = argv[optind];
    }

    /* 建立与打印服务器的连接 */
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if (getaddrinfo(PRINT_SERVER_HOST, PRINT_SERVER_PORT, &hints, &res) != 0) {
    err_quit("getaddrinfo error");
    }

    ressave = res;
    do {
    sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (sockfd < 0) {
    continue;
    }
    if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) {
    break; /* 连接成功 */
    }
    close(sockfd);
    } while ((res = res->ai_next) != NULL);

    if (res == NULL) {
    err_quit("无法连接到打印服务器");
    }
    freeaddrinfo(ressave);

    /* 发送标题(如果提供) */
    if (title != NULL) {
    snprintf(buf, MAXLINE, "title:%s\\n", title);
    write(sockfd, buf, strlen(buf));
    }

    /* 打开并发送文件内容 */
    if (filename != NULL) {
    fd = open(filename, O_RDONLY);
    if (fd < 0) {
    err_sys("无法打开文件 %s", filename);
    }
    while ((n = read(fd, buf, MAXLINE)) > 0) {
    write(sockfd, buf, n);
    }
    close(fd);
    } else {
    /* 从标准输入读取 */
    while ((n = read(STDIN_FILENO, buf, MAXLINE)) > 0) {
    write(sockfd, buf, n);
    }
    }

    /* 发送文件结束标记 */
    shutdown(sockfd, SHUT_WR);

    /* 等待服务器响应 */
    while ((n = read(sockfd, buf, MAXLINE)) > 0) {
    write(STDOUT_FILENO, buf, n);
    }

    close(sockfd);
    exit(0);
    }

    📖 代码说明:print程序展示了客户端网络编程的典型模式:getaddrinfo解析主机名和服务名 → socket创建套接字 → connect建立连接 → 发送数据 → shutdown关闭写端 → 读取响应 → close关闭套接字。

    四、printd打印守护进程

    4.1 功能概述

    printd守护进程是整个示例的核心组件。它的主要职责是:

  • 监听来自print客户端的连接
  • 接收打印作业(文件数据)
  • 将打印作业提交到系统的打印假脱机队列
  • 维护活动的客户端连接列表
  • printd的核心技术构成:

    技术用途
    守护进程 在后台持续运行,接受客户请求
    套接字 监听TCP端口,接收客户端连接
    线程 为每个客户端请求创建独立的处理线程
    互斥量 保护共享数据结构(活动worker链表)
    信号处理 处理SIGTERM等信号进行优雅关闭
    记录锁 确保只有一个守护进程实例在运行

    4.2 关键数据结构

    #include "apue.h"
    #include <pthread.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>

    #define PORT "7777"
    #define BACKLOG 10

    /* 工作线程结构体,用于双向链表管理 */
    struct worker_thread {
    pthread_t tid; /* 线程ID */
    int sockfd; /* 客户端套接字 */
    struct worker_thread *prev; /* 前驱指针 */
    struct worker_thread *next; /* 后继指针 */
    };

    /* 全局数据结构 */
    static struct worker_thread *workers = NULL; /* 活动worker链表头 */
    static pthread_mutex_t workerlock = PTHREAD_MUTEX_INITIALIZER; /* 保护链表的互斥量 */
    static volatile sig_atomic_t quitflag = 0; /* 退出标志,用于信号处理 */

    /* 信号处理函数 */
    static void sig_term(int signo)
    {
    quitflag = 1;
    }

    4.3 添加worker的代码(含勘误说明)

    APUE原书中的add_worker函数存在一个链表操作错误,在第二版第633页代码499-504行中,当向非空链表插入新节点时,忘记将workers指针更新到新节点。原代码:

    /* 原代码(存在错误) */
    void add_worker(pthread_t tid, int sockfd)
    {
    struct worker_thread *wtp;
    if ((wtp = malloc(sizeof(struct worker_thread))) == NULL) {
    log_ret("add_worker: can't malloc");
    pthread_exit((void *)1);
    }
    wtp->tid = tid;
    wtp->sockfd = sockfd;
    log_msg("prepare to add worker");
    pthread_mutex_lock(&workerlock);
    wtp->prev = NULL;
    wtp->next = workers;
    if (workers == NULL)
    workers = wtp;
    else
    workers->prev = wtp; /* 缺少 workers = wtp; */
    pthread_mutex_unlock(&workerlock);
    }

    问题分析:当workers不为NULL时,代码只将旧头节点的prev指向新节点,但未将workers指针更新到新节点,导致新节点实际上未被插入链表。正确的做法是添加workers = wtp;。

    修正后的代码:

    /* 添加worker到链表头部(修正版本) */
    void add_worker(pthread_t tid, int sockfd)
    {
    struct worker_thread *wtp;
    if ((wtp = malloc(sizeof(struct worker_thread))) == NULL) {
    log_ret("add_worker: can't malloc");
    pthread_exit((void *)1);
    }
    wtp->tid = tid;
    wtp->sockfd = sockfd;
    log_msg("prepare to add worker");
    pthread_mutex_lock(&workerlock);
    wtp->prev = NULL;
    wtp->next = workers;
    if (workers != NULL)
    workers->prev = wtp;
    workers = wtp; /* 将链表头更新为新节点 */
    pthread_mutex_unlock(&workerlock);
    }

    4.4 移除worker

    void remove_worker(struct worker_thread *wtp)
    {
    pthread_mutex_lock(&workerlock);
    if (wtp->prev != NULL)
    wtp->prev->next = wtp->next;
    else
    workers = wtp->next;
    if (wtp->next != NULL)
    wtp->next->prev = wtp->prev;
    pthread_mutex_unlock(&workerlock);
    free(wtp);
    }

    4.5 打印作业提交

    printd的核心功能是将接收到的数据提交给系统的打印假脱机系统:

    /* 将数据提交到系统的打印假脱机 */
    static int submit_to_spooler(const char *data, size_t len, const char *title)
    {
    FILE *fp;
    char cmd[MAXLINE];

    /* 构建lpr命令,使用标题(如果提供) */
    if (title != NULL) {
    snprintf(cmd, sizeof(cmd), "lpr -T '%s'", title);
    } else {
    snprintf(cmd, sizeof(cmd), "lpr");
    }

    /* 通过管道将数据发送给lpr命令 */
    if ((fp = popen(cmd, "w")) == NULL) {
    return 1;
    }
    fwrite(data, 1, len, fp);
    return pclose(fp);
    }

    4.6 客户端处理线程

    每个客户端连接由一个独立的线程处理:

    /* 处理单个客户端连接的线程函数 */
    void *client_thread(void *arg)
    {
    int sockfd = *(int *)arg;
    char buf[MAXLINE];
    ssize_t n;
    char *title = NULL;
    char *data = NULL;
    size_t data_len = 0;

    /* 读取标题行 */
    n = readline(sockfd, buf, MAXLINE);
    if (n > 0 && strncmp(buf, "title:", 6) == 0) {
    buf[n 1] = '\\0'; /* 去掉换行符 */
    title = strdup(buf + 6);
    }

    /* 读取文件数据 */
    while ((n = read(sockfd, buf, MAXLINE)) > 0) {
    data = realloc(data, data_len + n);
    memcpy(data + data_len, buf, n);
    data_len += n;
    }

    /* 提交到打印假脱机 */
    if (data != NULL) {
    submit_to_spooler(data, data_len, title);
    }

    /* 发送响应 */
    write(sockfd, "OK\\n", 3);

    /* 清理资源 */
    free(title);
    free(data);
    close(sockfd);
    return NULL;
    }

    4.7 printd主程序框架

    int main(void)
    {
    int listenfd, connfd;
    struct addrinfo hints, *res;
    pthread_t tid;

    /* 守护进程化 */
    daemonize("printd");

    /* 建立监听套接字 */
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;

    if (getaddrinfo(NULL, PORT, &hints, &res) != 0) {
    err_quit("getaddrinfo error");
    }

    listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    bind(listenfd, res->ai_addr, res->ai_addrlen);
    listen(listenfd, BACKLOG);
    freeaddrinfo(res);

    /* 设置信号处理 */
    signal(SIGTERM, sig_term);

    /* 主循环:接受连接,为每个客户端创建线程 */
    while (!quitflag) {
    connfd = accept(listenfd, NULL, NULL);
    if (connfd < 0) {
    if (errno == EINTR) continue;
    err_sys("accept error");
    }
    pthread_create(&tid, NULL, client_thread, &connfd);
    add_worker(tid, connfd);
    pthread_detach(tid);
    }

    /* 清理工作:等待所有worker线程结束 */
    /* … */
    close(listenfd);
    exit(0);
    }

    五、架构图与流程图

    5.1 print客户端与printd交互流程图

    打印假脱机printd守护进程DNSprint客户端用户打印假脱机printd守护进程DNSprint客户端用户#mermaid-svg-RN0hXa6WTIstkU5y{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RN0hXa6WTIstkU5y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RN0hXa6WTIstkU5y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RN0hXa6WTIstkU5y .error-icon{fill:#552222;}#mermaid-svg-RN0hXa6WTIstkU5y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RN0hXa6WTIstkU5y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RN0hXa6WTIstkU5y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RN0hXa6WTIstkU5y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RN0hXa6WTIstkU5y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RN0hXa6WTIstkU5y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RN0hXa6WTIstkU5y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RN0hXa6WTIstkU5y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RN0hXa6WTIstkU5y .marker.cross{stroke:#333333;}#mermaid-svg-RN0hXa6WTIstkU5y svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RN0hXa6WTIstkU5y p{margin:0;}#mermaid-svg-RN0hXa6WTIstkU5y .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RN0hXa6WTIstkU5y text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-RN0hXa6WTIstkU5y .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RN0hXa6WTIstkU5y .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-RN0hXa6WTIstkU5y .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-RN0hXa6WTIstkU5y .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-RN0hXa6WTIstkU5y #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-RN0hXa6WTIstkU5y .sequenceNumber{fill:white;}#mermaid-svg-RN0hXa6WTIstkU5y #sequencenumber{fill:#333;}#mermaid-svg-RN0hXa6WTIstkU5y #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-RN0hXa6WTIstkU5y .messageText{fill:#333;stroke:none;}#mermaid-svg-RN0hXa6WTIstkU5y .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RN0hXa6WTIstkU5y .labelText,#mermaid-svg-RN0hXa6WTIstkU5y .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-RN0hXa6WTIstkU5y .loopText,#mermaid-svg-RN0hXa6WTIstkU5y .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-RN0hXa6WTIstkU5y .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-RN0hXa6WTIstkU5y .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-RN0hXa6WTIstkU5y .noteText,#mermaid-svg-RN0hXa6WTIstkU5y .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-RN0hXa6WTIstkU5y .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RN0hXa6WTIstkU5y .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RN0hXa6WTIstkU5y .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-RN0hXa6WTIstkU5y .actorPopupMenu{position:absolute;}#mermaid-svg-RN0hXa6WTIstkU5y .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-RN0hXa6WTIstkU5y .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-RN0hXa6WTIstkU5y .actor-man circle,#mermaid-svg-RN0hXa6WTIstkU5y line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-RN0hXa6WTIstkU5y :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}print -t "doc" file.txtgetaddrinfo(server, 7777)服务器地址TCP连接(端口7777)连接建立title:doc文件数据shutdown(写端)调用lpr命令打印完成OK显示成功信息

    5.2 printd内部线程模型

    #mermaid-svg-gBdU1E2urapZ5A0o{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gBdU1E2urapZ5A0o .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gBdU1E2urapZ5A0o .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gBdU1E2urapZ5A0o .error-icon{fill:#552222;}#mermaid-svg-gBdU1E2urapZ5A0o .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gBdU1E2urapZ5A0o .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gBdU1E2urapZ5A0o .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gBdU1E2urapZ5A0o .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gBdU1E2urapZ5A0o .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gBdU1E2urapZ5A0o .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gBdU1E2urapZ5A0o .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gBdU1E2urapZ5A0o .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gBdU1E2urapZ5A0o .marker.cross{stroke:#333333;}#mermaid-svg-gBdU1E2urapZ5A0o svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gBdU1E2urapZ5A0o p{margin:0;}#mermaid-svg-gBdU1E2urapZ5A0o .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-gBdU1E2urapZ5A0o .cluster-label text{fill:#333;}#mermaid-svg-gBdU1E2urapZ5A0o .cluster-label span{color:#333;}#mermaid-svg-gBdU1E2urapZ5A0o .cluster-label span p{background-color:transparent;}#mermaid-svg-gBdU1E2urapZ5A0o .label text,#mermaid-svg-gBdU1E2urapZ5A0o span{fill:#333;color:#333;}#mermaid-svg-gBdU1E2urapZ5A0o .node rect,#mermaid-svg-gBdU1E2urapZ5A0o .node circle,#mermaid-svg-gBdU1E2urapZ5A0o .node ellipse,#mermaid-svg-gBdU1E2urapZ5A0o .node polygon,#mermaid-svg-gBdU1E2urapZ5A0o .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gBdU1E2urapZ5A0o .rough-node .label text,#mermaid-svg-gBdU1E2urapZ5A0o .node .label text,#mermaid-svg-gBdU1E2urapZ5A0o .image-shape .label,#mermaid-svg-gBdU1E2urapZ5A0o .icon-shape .label{text-anchor:middle;}#mermaid-svg-gBdU1E2urapZ5A0o .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gBdU1E2urapZ5A0o .rough-node .label,#mermaid-svg-gBdU1E2urapZ5A0o .node .label,#mermaid-svg-gBdU1E2urapZ5A0o .image-shape .label,#mermaid-svg-gBdU1E2urapZ5A0o .icon-shape .label{text-align:center;}#mermaid-svg-gBdU1E2urapZ5A0o .node.clickable{cursor:pointer;}#mermaid-svg-gBdU1E2urapZ5A0o .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gBdU1E2urapZ5A0o .arrowheadPath{fill:#333333;}#mermaid-svg-gBdU1E2urapZ5A0o .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gBdU1E2urapZ5A0o .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gBdU1E2urapZ5A0o .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gBdU1E2urapZ5A0o .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gBdU1E2urapZ5A0o .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gBdU1E2urapZ5A0o .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gBdU1E2urapZ5A0o .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gBdU1E2urapZ5A0o .cluster text{fill:#333;}#mermaid-svg-gBdU1E2urapZ5A0o .cluster span{color:#333;}#mermaid-svg-gBdU1E2urapZ5A0o div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gBdU1E2urapZ5A0o .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gBdU1E2urapZ5A0o rect.text{fill:none;stroke-width:0;}#mermaid-svg-gBdU1E2urapZ5A0o .icon-shape,#mermaid-svg-gBdU1E2urapZ5A0o .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gBdU1E2urapZ5A0o .icon-shape p,#mermaid-svg-gBdU1E2urapZ5A0o .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gBdU1E2urapZ5A0o .icon-shape .label rect,#mermaid-svg-gBdU1E2urapZ5A0o .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gBdU1E2urapZ5A0o .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gBdU1E2urapZ5A0o .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gBdU1E2urapZ5A0o :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    共享数据结构

    线程池

    主线程

    收到连接

    添加到workers链表

    提交

    提交

    提交

    add_worker

    add_worker

    add_worker

    main() 守护进程化 创建监听套接字

    监听循环 accept()等待连接

    创建client_thread

    client_thread1 接收打印数据

    client_thread2 接收打印数据

    client_threadN 接收打印数据

    workers链表 受workerlock保护

    lpr命令

    5.3 网络打印协议演进

    #mermaid-svg-xJSTaGF6lFdjcQxH{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xJSTaGF6lFdjcQxH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xJSTaGF6lFdjcQxH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xJSTaGF6lFdjcQxH .error-icon{fill:#552222;}#mermaid-svg-xJSTaGF6lFdjcQxH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xJSTaGF6lFdjcQxH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xJSTaGF6lFdjcQxH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xJSTaGF6lFdjcQxH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xJSTaGF6lFdjcQxH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xJSTaGF6lFdjcQxH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xJSTaGF6lFdjcQxH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xJSTaGF6lFdjcQxH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xJSTaGF6lFdjcQxH .marker.cross{stroke:#333333;}#mermaid-svg-xJSTaGF6lFdjcQxH svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xJSTaGF6lFdjcQxH p{margin:0;}#mermaid-svg-xJSTaGF6lFdjcQxH .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-xJSTaGF6lFdjcQxH .cluster-label text{fill:#333;}#mermaid-svg-xJSTaGF6lFdjcQxH .cluster-label span{color:#333;}#mermaid-svg-xJSTaGF6lFdjcQxH .cluster-label span p{background-color:transparent;}#mermaid-svg-xJSTaGF6lFdjcQxH .label text,#mermaid-svg-xJSTaGF6lFdjcQxH span{fill:#333;color:#333;}#mermaid-svg-xJSTaGF6lFdjcQxH .node rect,#mermaid-svg-xJSTaGF6lFdjcQxH .node circle,#mermaid-svg-xJSTaGF6lFdjcQxH .node ellipse,#mermaid-svg-xJSTaGF6lFdjcQxH .node polygon,#mermaid-svg-xJSTaGF6lFdjcQxH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xJSTaGF6lFdjcQxH .rough-node .label text,#mermaid-svg-xJSTaGF6lFdjcQxH .node .label text,#mermaid-svg-xJSTaGF6lFdjcQxH .image-shape .label,#mermaid-svg-xJSTaGF6lFdjcQxH .icon-shape .label{text-anchor:middle;}#mermaid-svg-xJSTaGF6lFdjcQxH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xJSTaGF6lFdjcQxH .rough-node .label,#mermaid-svg-xJSTaGF6lFdjcQxH .node .label,#mermaid-svg-xJSTaGF6lFdjcQxH .image-shape .label,#mermaid-svg-xJSTaGF6lFdjcQxH .icon-shape .label{text-align:center;}#mermaid-svg-xJSTaGF6lFdjcQxH .node.clickable{cursor:pointer;}#mermaid-svg-xJSTaGF6lFdjcQxH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xJSTaGF6lFdjcQxH .arrowheadPath{fill:#333333;}#mermaid-svg-xJSTaGF6lFdjcQxH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xJSTaGF6lFdjcQxH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xJSTaGF6lFdjcQxH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xJSTaGF6lFdjcQxH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xJSTaGF6lFdjcQxH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xJSTaGF6lFdjcQxH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xJSTaGF6lFdjcQxH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xJSTaGF6lFdjcQxH .cluster text{fill:#333;}#mermaid-svg-xJSTaGF6lFdjcQxH .cluster span{color:#333;}#mermaid-svg-xJSTaGF6lFdjcQxH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xJSTaGF6lFdjcQxH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xJSTaGF6lFdjcQxH rect.text{fill:none;stroke-width:0;}#mermaid-svg-xJSTaGF6lFdjcQxH .icon-shape,#mermaid-svg-xJSTaGF6lFdjcQxH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xJSTaGF6lFdjcQxH .icon-shape p,#mermaid-svg-xJSTaGF6lFdjcQxH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xJSTaGF6lFdjcQxH .icon-shape .label rect,#mermaid-svg-xJSTaGF6lFdjcQxH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xJSTaGF6lFdjcQxH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xJSTaGF6lFdjcQxH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xJSTaGF6lFdjcQxH :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    现代方式 CUPS

    IPP

    应用程序

    lp命令

    cupsd守护进程 端口631

    网络打印机 IPP支持

    IPP协议

    传统方式

    lpr命令

    lpd守护进程 端口515

    打印机

    六、多实例保护与信号处理

    6.1 使用记录锁防止多实例

    守护进程通常需要确保只有一个实例在运行,否则可能导致资源竞争。printd可以通过文件锁实现单实例保护:

    #include <fcntl.h>
    #include <sys/file.h>

    #define LOCKFILE "/var/run/printd.pid"
    #define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

    int already_running(void)
    {
    int fd;
    char buf[16];

    fd = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE);
    if (fd < 0) {
    return 1;
    }

    /* 尝试对整个文件加写锁 */
    if (flock(fd, LOCK_EX | LOCK_NB) < 0) {
    if (errno == EWOULDBLOCK) {
    close(fd);
    return 1; /* 已有实例运行 */
    }
    return 1;
    }

    /* 将当前进程ID写入文件 */
    ftruncate(fd, 0);
    snprintf(buf, sizeof(buf), "%ld", (long)getpid());
    write(fd, buf, strlen(buf));
    return 0;
    }

    6.2 信号处理与优雅关闭

    守护进程应该能够优雅地处理终止信号,在退出前清理资源:

    static volatile sig_atomic_t quitflag = 0;

    static void sig_term(int signo)
    {
    quitflag = 1;
    }

    /* 在主循环中 */
    while (!quitflag) {
    /* 处理连接… */
    }

    /* 退出前等待所有worker线程完成 */
    while (workers != NULL) {
    pthread_join(workers->tid, NULL);
    remove_worker(workers);
    }

    💡 信号处理要点:在信号处理函数中只能调用异步信号安全的函数。这里只设置quitflag标志,而将实际的清理工作放在主循环中,这是一种安全的信号处理模式。

    七、CUPS编程简介

    虽然APUE第21章的示例使用传统的lpr命令作为后端,但在实际开发中,我们更可能直接使用CUPS API与打印机通信。

    7.1 CUPS API核心函数

    #include <cups/cups.h>

    /* 获取打印机列表 */
    int cupsGetDests(cups_dest_t **dests);

    /* 打印文件 */
    int cupsPrintFile(const char *name, const char *filename,
    const char *title, int num_options, cups_option_t *options);

    /* 获取打印机状态 */
    ipp_status_t cupsGetJobs(http_t *http, cups_job_t **jobs, const char *name,
    int myjobs, int completed);

    7.2 使用CUPS API打印的示例

    #include <cups/cups.h>

    int print_with_cups(const char *filename, const char *printer, const char *title)
    {
    cups_dest_t *dests;
    int num_dests;
    int job_id;
    cups_option_t *options = NULL;
    int num_options = 0;

    /* 获取可用的打印机列表 */
    num_dests = cupsGetDests(&dests);
    if (num_dests == 0) {
    fprintf(stderr, "未找到打印机\\n");
    return 1;
    }

    /* 添加打印选项 */
    num_options = cupsAddOption("media", "A4", num_options, &options);
    num_options = cupsAddOption("sides", "one-sided", num_options, &options);

    /* 提交打印作业 */
    if (printer == NULL) {
    printer = cupsGetDefault();
    }
    job_id = cupsPrintFile(printer, filename, title, num_options, options);

    if (job_id == 0) {
    fprintf(stderr, "打印失败: %s\\n", cupsLastErrorString());
    return 1;
    }

    printf("作业已提交,作业ID: %d\\n", job_id);

    /* 清理资源 */
    cupsFreeDests(num_dests, dests);
    cupsFreeOptions(num_options, options);

    return 0;
    }

    📖 CUPS的现代化演进:CUPS 2.2.2引入了cups-driverd动态后端,支持对支持IPP Everywhere、AirPrint等标准的打印机进行免驱动打印。它通过IPP查询打印机的功能(如支持的文档格式、介质尺寸、分辨率和装订选项),并据此按需生成PPD文件。在CUPS 3.x中,传统的PPD驱动将被完全淘汰,转向基于PAPPL(Printer Application Framework)的打印机应用框架。

    八、常见问题与注意事项

    问题解决方法
    原书add_worker链表操作错误 在else分支中添加workers = wtp;,或使用简化版本的插入逻辑
    printd与系统CUPS冲突 使用非标准端口(如7777)避免与cupsd(631)冲突
    大文件打印导致内存溢出 使用流式传输而非一次性读入内存
    打印作业乱码 确保提交的数据格式与打印机兼容(如纯文本、PDF或PostScript)
    守护进程无法正常退出 正确设置信号处理,等待所有worker线程结束后再退出
    getaddrinfo返回多个地址 遍历所有地址直到成功连接,确保网络环境变化时的健壮性

    💡 关键要点:

  • 综合应用:printd是APUE全书知识的综合体现,融合了守护进程、套接字、线程、互斥量、信号处理和记录锁
  • 客户端-服务器模式:print客户端与printd服务器通过TCP套接字通信,是典型的C/S架构
  • 并发处理:printd为每个客户端创建独立线程,使用互斥量保护共享链表
  • 现代打印系统:实际生产环境使用CUPS而非自定义打印守护进程,理解CUPS的架构对系统管理员和开发者都有价值
  • IPP的统治地位:现代网络打印机普遍支持IPP,CUPS通过IPP与打印机直接通信,实现免驱动打印
  • 九、总结

    本章通过构建一个与网络打印机通信的完整应用,展示了如何将APUE全书所学的知识综合运用到一个实际项目中。主要内容包括:

  • 网络打印协议:从传统的LPD协议到现代IPP协议的演进,以及CUPS打印系统的核心架构
  • print客户端:使用套接字与服务器通信,支持从文件或标准输入读取打印数据
  • printd守护进程:
    • 以守护进程方式在后台持续运行
    • 使用TCP套接字监听客户端连接
    • 为每个客户端创建独立线程处理请求
    • 使用互斥量保护共享数据结构
    • 通过信号处理实现优雅关闭
  • 代码勘误:原书中add_worker函数存在链表操作错误,需要添加workers = wtp;修正
  • CUPS API:现代Linux系统的标准打印接口,了解其基本用法有助于实际开发
  • 本章是APUE全书知识体系的最后一次综合实践。掌握本章内容,意味着能够运用UNIX系统编程的核心技能构建实用的网络服务程序。

    十、全书回顾与下篇预告

    本书(《UNIX环境高级编程》,第3版)至此已完成了全部21章的学习。

    全书核心知识体系回顾:

    阶段章节范围核心内容
    基础知识 第1-6章 UNIX基础、文件I/O、文件和目录、标准I/O库、系统数据文件
    进程控制 第7-10章 进程环境、进程控制、进程关系、信号
    多线程 第11-12章 线程、线程控制
    系统服务 第13-18章 守护进程、高级I/O、进程间通信、网络IPC、终端I/O、伪终端
    实践应用 第19-21章 伪终端(第19章)、数据库函数库(第20章)、与网络打印机通信(第21章)

    学习寄语:APUE被誉为UNIX系统编程领域的“圣经”,但纸上得来终觉浅,绝知此事要躬行。建议在学习完成后,尝试独立完成以下实践项目:

    • 使用epoll/IOCP构建高并发网络服务器
    • 实现一个支持断点续传的文件传输工具
    • 开发一个基于共享内存和信号量的高性能IPC组件
    • 编写一个支持插件机制的守护进程框架

    这些实践将帮助你真正消化APUE的知识,并将其转化为解决实际问题的能力。


    本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
    👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

    赞(0)
    未经允许不得转载:171主机测评 » 《UNIX环境高级编程》读书笔记22: 与网络打印机通信
    分享到: 更多 (0)

    评论 抢沙发

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