《UNIX环境高级编程》读书笔记22: 与网络打印机通信
早期UNIX系统使用行式打印机后台程序(LPD,Line Printer Daemon)作为打印服务的基础设施。LPD协议定义在RFC 1179中,它使用TCP端口515进行通信。当用户执行lpr命令时,lpr会通过套接字连接到lpd守护进程,将要打印的文件和打印参数传递给守护进程,然后lpd将文件送到适当的打印机。print程序是本章示例中的客户端程序,负责接收用户的打印请求并与服务器进行通信。
作者: andylin02
学习章节: 第21章 与网络打印机通信
关键词:网络打印机;打印假脱机;printd;CUPS;IPP;LPD;套接字
一、引言
本章是APUE全书的最后一个实例章节。在此前的章节中,我们学习了进程间通信、套接字、守护进程、线程、信号等核心概念。本章将这些知识综合运用,构建一个与网络打印机通信的实际应用程序。
核心内容包括:
- 网络打印协议(IPP)的基本概念
- 打印假脱机系统(CUPS)的架构
- print客户端程序的设计与实现
- printd打印守护进程的设计与实现
💡 学习建议:本章的重点不是理解网络打印协议的所有细节,而是观察如何综合运用前面学过的知识(守护进程、套接字、线程、信号、记录锁等)构建一个实用的网络服务程序。建议将
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的核心架构:
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);
}
📖 代码说明:
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交互流程图
5.2 printd内部线程模型
5.3 网络打印协议演进
六、多实例保护与信号处理
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全书知识的综合体现,融合了守护进程、套接字、线程、互斥量、信号处理和记录锁- 客户端-服务器模式:
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的知识,并将其转化为解决实际问题的能力。
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)