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

一、引言

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

核心内容包括:

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

💡 学习建议:本章的重点不是理解网络打印协议的所有细节,而是观察如何综合运用前面学过的知识(守护进程、套接字、线程、信号、记录锁等)构建一个实用的网络服务程序。建议将printprintd的代码与实际运行的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的核心架构

打印机

CUPS系统

用户空间

IPP请求

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

lpr/lp命令

cups库

cupsd守护进程
端口631

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

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

后端
USB/网络/IPP

物理打印机
支持IPP

网络打印机
LPD/IPP

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

三、print客户端程序

3.1 功能概述

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

print程序的主要职责

  1. 解析命令行参数(如-t表示标题)
  2. 建立与服务器的TCP连接
  3. 将待打印的文件发送给服务器
  4. 支持从标准输入读取要打印的数据

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守护进程是整个示例的核心组件。它的主要职责是:

  1. 监听来自print客户端的连接
  2. 接收打印作业(文件数据)
  3. 将打印作业提交到系统的打印假脱机队列
  4. 维护活动的客户端连接列表

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守护进程 DNS print客户端 用户 打印假脱机 printd守护进程 DNS print客户端 用户 print -t "doc" file.txt getaddrinfo(server, 7777) 服务器地址 TCP连接(端口7777) 连接建立 title:doc 文件数据 shutdown(写端) 调用lpr命令 打印完成 OK 显示成功信息

5.2 printd内部线程模型

共享数据结构

线程池

主线程

收到连接

添加到workers链表

提交

提交

提交

add_worker

add_worker

add_worker

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

监听循环 accept()等待连接

创建client_thread

client_thread1 接收打印数据

client_thread2 接收打印数据

client_threadN 接收打印数据

workers链表 受workerlock保护

lpr命令

5.3 网络打印协议演进

现代方式 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返回多个地址 遍历所有地址直到成功连接,确保网络环境变化时的健壮性

💡 关键要点

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

九、总结

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

  1. 网络打印协议:从传统的LPD协议到现代IPP协议的演进,以及CUPS打印系统的核心架构
  2. print客户端:使用套接字与服务器通信,支持从文件或标准输入读取打印数据
  3. printd守护进程
    • 以守护进程方式在后台持续运行
    • 使用TCP套接字监听客户端连接
    • 为每个客户端创建独立线程处理请求
    • 使用互斥量保护共享数据结构
    • 通过信号处理实现优雅关闭
  4. 代码勘误:原书中add_worker函数存在链表操作错误,需要添加workers = wtp;修正
  5. 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的知识,并将其转化为解决实际问题的能力。


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

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐