《unix环境高级编程》— 与网络打印机通信

  • Post author:
  • Post category:其他


一般使用网络打印协议(Internet Printing Protocol, IPP)与打印机通信。

将描述两个程序:一个打印假脱机守护进程,用以将作业发送到打印机;一个命令行程序,用以将打印作业提交到假脱机守护进程。打印假脱机必须处理很多操作(与客户端通信以提交作业,与打印机通信,读文件,扫描目录,等)


网络打印机协议


通过将IPP服务嵌入到带网卡的打印机中,任意一台能够与打印机建立TCP/IP连接的计算机都能向打印机提交打印作业。

IPP是请求响应协议,客户端发送请求到服务器,服务器用响应报文回答这个请求。

这里写图片描述

这里写图片描述


超文本传输协议


HTTP也是请求响应协议。请求报文包含一个开始行,首部行,一个空白行,一个可选的实体主体。本例,实体主体

包含IPP首部和数据。

HTTP首部是ASCII码,每行以回车和换行符结束。

开始行包含method以指示客户端的请求操作,一个统一资源定位符URL来描述服务器和协议,一个字符表示HTTP版本。IPP所用的method为POST。

首部行指定属性。包含一个属性名,跟一个冒号,可选的空格符,然后是属性值,最后为回车和换行符。

HTTP首部样例:

POST /phaser860/ipp HTTP/1.1 开始行

Content-Length: 21931 首部行

Content-Type: application/ipp 首部行

Host: phaser860:ipp 首部行

HTTP响应报文的开始行包含一个版本字符串,跟一个状态码和状态消息,以回车和换行符结束。剩余部分与请求报文相同。


打印假脱机技术


开发一个假脱机系统以解决多用户访问单一资源(打印机)的问题。一个简单的用户名发送文件到打印假脱机程序;假脱机程序将其保存到磁盘,把请求送入队列,最终将文件发送到打印机。

一个简单的命令行程序print读取文件,并发送到打印假脱机守护进程。

一个打印假脱机守护进程printd使用多线程分担任务:

1、一个线程在套接字上监听从运行print命令的客户端发来的新打印请求。

2、对每个客户端产生一个独立的线程,用以将要打印的文件复制到假脱机区域。

3、一个线程与打印机通信,一次发送一个队列中的作业。

4、一个线程处理信号。

这里写图片描述


安全


1、打印机假脱机守护进程拥有超级用户特权,将特权TCP端口绑定一个套接字后,将守护进程的用户和组ID改为非root。

2、审查代码漏洞。

3、记录日志。


模拟配置

在打印机配置文件/etc/printer.conf中添加

printserver   yjp-VirtualBox     # 打印假脱机守护进程的计算机系统名
printer       yjp-VirtualBox     # 网络打印机的计算机系统名

/etc/services:

printer   515/tcp                # 打印假脱机守护进程的服务名,特权端口,应<1024  本来就存在
ipp       631/tcp                # 网络打印机的服务名,端口                     本来就存在
ipp       631/udp                # 网络打印机的服务名,端口                     本来就存在

通过查看/etc/passwd文件,“lp”用户存在

创建文件夹:/var/spool/printer/data /var/spool/printer/reqs

创建文件:/var/spool/printer/jobno


模拟结果


这里写图片描述

这里写图片描述


过程


Print.c

读取选项,是否按文本打印;

检查要打印的文件能否打开,是否为常规文件;

得到假脱机守护进程的计算机系统名,从配置文件 /etc/printer.conf 中搜;

将系统名转换为网络地址,getaddrinfo;

连接假脱机守护进程,socket,connect;

发送打印请求;

发送打印文件内容;

读取假脱机守护进程的响应信息。

Printd.c

初始化守护进程;

忽略sigpipe信号;sighup信号到来时重读配置文件;

在文件/var/spool/printer/jobno上放一个记录锁write_lock(jobfd, 0, SEEK_SET, 0),读取下一个作业号;

从/etc/printer.conf中搜索打印机系统名,写入日志;

将打印机系统名转换为网络地址,getaddrinfo;

Initserver;

创建与打印机交互的线程:

pthread_cond_wait(&jobwait, &joblock) 等待作业到来;

当一个作业准备好时,将其从作业列表删除;

将下一个作业号写到 /var/spool/printer/jobno;

加锁,检查配置文件有无改变;

检查 /var/spool/data/jobid是否能打开;

建立socket与打印机通信,connect;

发送请求头;

发送数据文件;

读取打印机响应。

创建处理信号的线程:

如果是 sighup,则reread=1;

如果是 sigterm,删除所有工作线程;

在 /var/spool/printer目录中搜索任何挂起的作业,对于找的每个作业,加入作业队列,pthread_cond_signal(&jobwait),通知工作线程;

select 等待客户端连接,对每一个客户端创建新线程以处理请求:

安装线程清理处理程序,即当线程调用pthread_exit时,用一个非零参数调用pthread_cleanup_pop时,或者响应一个删除请求时,将所在工作线程删除;

创建工作线程,并加入工作线程列表;

读取客户端请求头;

创建文件 /var/spool/data/jobid,从客户端读取并写入;

创建文件 /var/spool/printer/reqs/jobid以记住打印请求;

向客户端发送响应,包括作业ID和成功状态;

将接收到的作业加入到挂起的打印作业列表,完成清理过程。


源代码


ipp.h 包含IPP定义

print.h 包含常用的常数、数据结构定义及工具例程声明的头文件

util.c 用于print.c和printd.c的工具例程

print.c 用于打印文件的命令行程序

printd.c 打印假脱机守护进程程序

ipp.h

#ifndef _IPP_H
#define _IPP_H

/* 基于RFC 2911的状态类 */
#define STATCLASS_OK(x)       ((x) >= 0x0000 && (x) <= 0x00ff)
#define STATCLASS_INFO(x)     ((x) >= 0x0100 && (x) <= 0x01ff)
#define STATCLASS_REDIR(x)    ((x) >= 0x0200 && (x) <= 0x02ff)
#define STATCLASS_CLIERR(x)   ((x) >= 0x0400 && (x) <= 0x05ff)

/* 基于RFC 2911的状态码 */
#define STAT_OK          0x0000  /* success */
#define STAT_OK_ATTRLGN  0x0001  /* OK; some attrs ignored */
#define STAT_OK_ATTRCON  0x0002  /* OK; some attrs conflicted */

#define STAT_CLI_BADREQ  0x0400  /* invalid client request */
#define STAT_CLI_FORBID  0x0401  /* request is forbidden */
#define STAT_CLI_NOAUTH  0x0402  /* authentication required */
#define STAT_CLI_NOPERM  0x0403  /* client not authorized */
#define STAT_CLI_NOTPOS  0x0404  /* request not possible */
#define STAT_CLI_TIMOUT  0x0405  /* client too slow */
#define STAT_CLI_NOTFND  0x0406  /* no object found for URI */
#define STAT_CLI_OBJGONE 0x0407  /* object no longer available */
#define STAT_CLI_TOOBIG  0x0408  /* requested entity too big */
#define STAT_CLI_TOOLNG  0x0409  /* attribute value too large */
#define STAT_CLI_BADFMT  0x040a  /* unsupported doc format */
#define STAT_CLI_NOTSUP  0x040b  /* attributes not supported */
#define STAT_CLI_NOSCHM  0x040c  /* URI scheme not supported */
#define STAT_CLI_NOCHAR  0x040d  /* charset not supported */
#define STAT_CLI_ATTRCON 0x040e  /* attributes conflicted */
#define STAT_CLI_NOCOMP  0x040f  /* compression not supported */
#define STAT_CLI_COMPERR 0x0410  /* data can't be decompressed */
#define STAT_CLI_FMTERR  0x0411  /* document format error */
#define STAT_CLI_ACCESS  0x0412  /* error accessing data */

#define STAT_SRV_INTERN  0x0500  /* unexpected internal error */
#define STAT_SRV_NOTSUP  0x0501  /* operation not supported */
#define STAT_SRV_UNAVAIL 0x0502  /* service unavailable */
#define STAT_SRV_BADVER  0x0503  /* version not supported */
#define STAT_SRV_DEVERR  0x0504  /* device error */
#define STAT_SRV_TMPERR  0x0505  /* temporary error */
#define STAT_SRV_REJECT  0x0506  /* server not accepting jobs */
#define STAT_SRV_TOOBUSY 0x0507  /* server too busy */
#define STAT_SRV_CANCEL  0x0508  /* job has been canceled */
#define STAT_SRV_NOMULTI 0x0509  /* multi-doc jobs unsuppotred */

/* 操作ID,本例中,仅用到打印作业操作 */
#define OP_PRINT_JOB     0x02
#define OP_PRINT_URI     0x03
#define OP_VALIDATE_JOB  0X04
#define OP_CREATE_JOB    0x05
#define OP_SEND_DOC      0x06
#define OP_SEND_URI      0x07
#define OP_CANCEL_JOB    0x08
#define OP_GET_JOB_ATTR  0x09
#define OP_GET_JOBS      0x0a
#define OP_GET_PRINTER_ATTR 0x0b
#define OP_HOLD_JOB      0x0c
#define OP_RELEASE_JOB   0x0d
#define OP_PAUSE_PRINTER 0x10
#define OP_RESUME_PRINTER 0x11
#define OP_PURGE_JOBS    0x12

/* 属性标志限定了IPP请求报文和响应报文中的属性组,定义于RFC 2910 */
#define TAG_OPERATION_ATTR 0x01  /* operation attributes tag */
#define TAG_JOB_ATTR       0x02  /* job attributes tag */
#define TAG_END_OF_ATTR    0x03  /* end of attributes tag */
#define TAG_PRINTER_ATTR   0x04  /* printer attributes tag */
#define TAG_UNSUPP_ATTR    0x05  /* unsupported attributes tag */

/* 值标志指示每个属性和参数的格式,由RFC 2910定义 */
#define TAG_UNSUPPORTED    0x10  /* unsupported value */
#define TAG_UNKOWN         0x12  /* unkown value */
#define TAG_NONE           0x13  /* no value */
#define TAG_INTEGER        0x21  /* integer */
#define TAG_BOOLEAN        0x22  /* boolean */
#define TAG_ENUM           0x23  /* enumeration */
#define TAG_OCTSTR         0x30  /* octet string */
#define TAG_DATETIME       0x31  /* date time */
#define TAG_RESOLUTION     0x32  /* resolution */
#define TAG_INTRANGE       0x33  /* range of interger */
#define TAG_TEXTWLANG      0X35  /* text with language */
#define TAG_NAMEWLANG      0x36  /* name with language */
#define TAG_TEXTWOLANG     0x41  /* text without language */
#define TAG_NAMEWOLANG     0x42  /* name without language */
#define TAG_KEYWORD        0x44  /* keyword */
#define TAG_URI            0x45  /* URI */
#define TAG_URISCHEME      0X46  /* URI scheme */
#define TAG_CHARSET        0x47  /* charset */
#define TAG_NATULANG       0x48  /* nature language */
#define TAG_MIMETYPE       0x49  /* mime media type */

/* IPP首部结构。请求报文于响应报文首部一样,只是请求操作
   中的操作ID被响应中的状态码代替 */
struct ipp_hdr
{
    int8_t  major_version;  /* always 1 */
    int8_t  minor_version;  /* always 1 */
    union
    {
        int16_t op;     /* request ID */
        int16_t st;     /* status */
    }u;
    int32_t request_id;     /* request ID */
    char attr_group[1]; /* start of optional attributes group */
    /* optional data follows */
};


#define operation u.op
#define Status u.st

#endif /* _IPP_H */

print.h

#ifndef _PRINT_H
#define _PRINT_H

#include <sys/socket.h>
#include <arpa/inet.h>
#if defined(BSD) || defined(MACOS)
#include <netinet/in.h>
#endif
#include <netdb.h>
#include <errno.h>

#define CONFIG_FILE "/etc/printer.conf"   
#define SPOOLDIR    "/var/spool/printer"  
#define JOBFILE     "jobno"               /* 包含下一个作业号的文件 */
#define DATADIR     "data"                /* 需要打印的文件副本 */
#define REQDIR      "reqs"                /* 对每个请求的控制信息 */

#define FILENMSZ    64
#define FILEPERM    (S_IRUSR|S_IWUSR)     /* 创建提交拓印文件副本时使用的权限,
                            不希望一个普通用户读取另外一个用户等待打印的文件 */
#define USERNM_MAX  64
#define JOBNM_MAX   256
#define MSGLEN_MAX  512

#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX 256
#endif

#define IPP_PORT    631   /* IPP端口 */
#define QLEN        10    /* 传给listen的参数 */
#define IBUFSZ      512   /* IPP header buffer size */
#define HBUFSZ      512   /* HTTP header buffer size */
#define IOBUFSZ     8192  /* data buffer size */

#ifndef ETIME
#define ETIME ETIMEDOUT
#endif

/* 包含于util.c */
extern int getaddrlist(const char *, const char *, struct addrinfo **);
extern char *get_printserver(void);
extern struct addrinfo *get_printaddr(void);
extern ssize_t tread(int, void *, size_t, unsigned int);
extern ssize_t treadn(int, void *, size_t, unsigned int);
extern int connect_retry(int, const struct sockaddr *, socklen_t);
extern int initserver(int, struct sockaddr *, socklen_t, int);

/* print命令发送printeq结构到打印假脱机守护进程 */
struct printreq
{
    long size;                /* size in bytes */
    long flags;               
    char usernm[USERNM_MAX];  /* user's name */
    char jobnm[JOBNM_MAX];    /* job's name */
};

/* Request flags */
#define PR_TEXT 0x01  /* treat file as plain text */

/* 打印假脱机守护进程用printesp结构回应 */
struct printresp
{
    long retcode;         /* 0=success, !0=error code */
    long jobid;           /* job ID */
    char msg[MSGLEN_MAX]; /* error message */
};

#endif /* _PRINT_H */

util.c

#include "apue.h"
#include "print.h"
#include <ctype.h>
#include <sys/select.h>

#define MAXCFGLINE 512  /* 打印配置文件中行的最大长度 */
#define MAXKWLEN   16   /* 配置文件中关键字的最大尺寸 */
#define MAXFMTLEN  16   /* 传给sscanf的格式化字符串的最大长度 */

/* getaddrinfo的封装 
   Get the address list for the given host ahd service and
   return through ailistpp. Returns 0 on success or an error
   code on failure. Note that we do not set errno if we encounter 
   an error.
*/
int getaddrlist(const char *host, const char *service,
    struct addrinfo **ailistpp)
{
    int err;
    struct addrinfo hint;

    hint.ai_flags = AI_CANONNAME;
    hint.ai_family = AF_INET;
    hint.ai_socktype = SOCK_STREAM;
    hint.ai_protocol = 0;
    hint.ai_addrlen = 0;
    hint.ai_canonname = NULL;
    hint.ai_addr = NULL;
    hint.ai_next = NULL;
    err = getaddrinfo(host, service, &hint, ailistpp);
    return (err);
}

/* 在打印机配置文件中搜索指定的关键字 */
static char *scan_configfile(char *keyword)
{
    int n, match;
    FILE *fp;
    char keybuf[MAXKWLEN], pattern[MAXFMTLEN];
    char line[MAXCFGLINE];
    static char valbuf[MAXCFGLINE];

    /* 以读方式打开配置文件,根据搜索模式建立格式字符串 */
    if((fp = fopen(CONFIG_FILE, "r")) == NULL)
        log_sys("can't open %s", CONFIG_FILE);
    sprintf(pattern, "%%%ds %%%ds", MAXKWLEN-1, MAXCFGLINE-1);
    match = 0;
    /* 一次读取一行,搜索用空格分开的两个字符串 */
    while(fgets(line, MAXLINE, fp) != NULL)
    {
        n = sscanf(line, pattern, keybuf, valbuf);
        if(n == 2 && strcmp(keyword, keybuf) == 0) /* 关键字匹配 */
        {
            match = 1;
            break;
        }
    }
    fclose(fp); /* 关闭文件 */
    if(match != 0)
        return (valbuf); /* 存放在静态缓冲区valbuf,因此不能用于多线程 */
    else
        return (NULL);
}

/* 由客户端调用
   获取运行打印假脱机守护进程的计算机系统名字 */
char *get_printserver(void)
{
    return (scan_configfile("printserver"));
}

/* 由守护进程调用
   获取网络打印机地址 */
struct addrinfo *get_printaddr(void)
{
    int err;
    char *p;    
    struct addrinfo *ailist;

    if((p = scan_configfile("printer")) != NULL)
    {
        if((err = getaddrlist(p, "ipp", &ailist)) != 0)
        {
            log_msg("no address information for %s", p);
            return (NULL);
        }
        return (ailist);
    }
    /* 可通过设置一个全局遍历安排日志函数将其打印到标志错误或日志文件 */
    log_msg("no printer address specified");
    return (NULL);
}

/* 读取指定的字节数,放弃前至多阻塞timout秒,预防拒绝服务攻击 */
ssize_t tread(int fd, void *buf, size_t nbytes, unsigned int timeout)
{
    int nfds;
    fd_set readfds;
    struct timeval tv;

    tv.tv_sec = timeout;
    tv.tv_usec = 0;
    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);
    /* 等待指定的文件描述符可读 */
    nfds = select(fd+1, &readfds, NULL, NULL, &tv);
    if(nfds <= 0)  /* 无数据 */
    {
        if(nfds == 0)  /* 超时 */
            errno = ETIME;
        return (-1);
    }
    return (read(fd, buf, nbytes)); /* 返回最多nbytes字节数据 */
}

/* 只正好读取请求的字节数。
   总等待时间最多为nbytes * timeout,最坏一次接收1字节 */
ssize_t treadn(int fd, void *buf, size_t nbytes, unsigned int timeout)
{
    size_t nleft;
    ssize_t nread;

    nleft = nbytes;
    while(nleft > 0)
    {
        if((nread = tread(fd, buf, nleft, timeout)) < 0)
        {
            if(nleft == nbytes)
                return (-1);  /* error */
            else
                break; /* error, return amount read so far */
        }
        else if(nread == 0)
            break;  /* EOF */
        nleft -= nread;
        buf += nread;
    }
    return (nbytes - nleft);  /* return >= 0 */
}

print.c

#include "apue.h"
#include "print.h"
#include <fcntl.h>
#include <pwd.h>

/* 非0,错误消息被送到标志错误输出,而非日志文件 */
int log_to_stderr = 1;  

void submit_file(int, int, const char *, size_t, int);

int main(int argc, char *argv[])
{
    int fd, sockfd, err, text, c;
    struct stat sbuf;
    char *host;
    struct addrinfo *ailist, *aip;

    err = 0;
    text = 0;
    while((c = getopt(argc, argv, "t")) != -1)
    {
        switch(c)
        {
        case 't':
            text = 1; /* 将文件按文本格式打印 */
            break;
        case '?':
            err = 1;
            break;
        }
    }

    /* optind为指向第一个非选项参数下标 */
    if(err || (optind != argc - 1))
        err_quit("usage: print [-t] filename");
    /* 检查是否能打开要打印的文件 */
    if((fd = open(argv[optind], O_RDONLY)) < 0)
        err_sys("print: can't open %s", argv[1]);
    /* 检查是否为一个常规文件 */
    if(fstat(fd, &sbuf) < 0)
        err_sys("print: can't stat %s", argv[1]);
    if(!S_ISREG(sbuf.st_mode))
        err_quit("print: %s must be a regular file\n", argv[1]);

    /* 取得打印假脱机守护进程的计算机系统名字 */
    if((host = get_printserver()) == NULL)
        err_quit("print: no print server defined");

    /* 将主机名换为网络地址
    服务名为"printer",需确保/etc/services由打印机服务的条目,
    最好选择特权端口,防止恶意用户假装为一个打印假脱机守护进程 */
    if((err = getaddrlist(host, "printer", &ailist)) != 0)
        err_quit("print: getaddrinfo error: %s", gai_strerror(err));

    /* 从返回的地址列表中,一次使用一个地址来尝试连接到守护进程,
    使用能够连接的第一个地址发送文件到守护进程 */
    for(aip = ailist; aip != NULL; aip = aip->ai_next)
    {
        if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
            err = errno;
        else if(connect_retry(sockfd, aip->ai_addr, aip->ai_addrlen) < 0)
            err = errno;
        else
        {
            submit_file(fd, sockfd, argv[1], sbuf.st_size, text);
            exit(0);
        }
    }   
    errno = err;
    err_ret("print: can't contact %s", host);
    exit(1);
}

/* 发送打印请求到守护进程并获取响应消息 */
void submit_file(int fd, int sockfd, const char *fname, size_t nbytes, int text)
{
    int nr, nw, len;
    struct passwd *pwd;
    struct printreq req;
    struct printresp res;
    char buf[IOBUFSZ];

    /* 建立请求首部 */
    if((pwd = getpwuid(geteuid())) == NULL)
        strcpy(req.usernm, "unkown");
    else
        strcpy(req.usernm, pwd->pw_name);
    req.size = htonl(nbytes);  /* 转换成网络字节序 */
    if(text)
        req.flags = htonl(PR_TEXT); /* 按纯文本格式打印 */
    else
        req.flags = 0;

    /* 如果作业名长度超长,则将开头部分截去,并代入省略符 */
    if((len = strlen(fname)) >= JOBNM_MAX)
    {
        strcpy(req.jobnm, "... ");
        strncat(req.jobnm, &fname[len-JOBNM_MAX+5], JOBNM_MAX-5);
    }
    else
        strcpy(req.jobnm, fname);

    /* 将请求首部发送到守护进程 */
    nw = writen(sockfd, &req, sizeof(struct printreq));
    if(nw != sizeof(struct printreq))
    {
        if(nw < 0)
            err_sys("can't write to print server");
        else 
            err_quit("short write (%d/%d) to print server", 
              nw, sizeof(struct printreq));
    }

    /* 将要打印的文件发送到守护进程 */
    while((nr = read(fd, buf, IOBUFSZ)) != 0)
    {
        nw = writen(sockfd, buf, nr);
        if(nw != nr)
        {
            if(nw < 0)
                err_sys("can't write to print server");
            else
                err_quit("short write (%d/%d) to print server",
                    nw, nr);
        }
    }

    /* 读取守护进程响应 */
    if((nr = readn(sockfd, &res, sizeof(struct printresp))) != sizeof(struct printresp))
        err_sys("can't read response from server");
    if(res.retcode != 0)
    {
        printf("rejected: %s\n", res.msg);
        exit(1);
    }
    else
    {
        /* 方便用户以后引用该请求,如取消打印 */
        printf("job ID %ld\n", ntohl(res.jobid));
    }
    exit(0);
}

printd.c

#include "apue.h"
#include "print.h"
#include "ipp.h"
#include <fcntl.h>
#include <dirent.h>
#include <ctype.h>
#include <pwd.h>
#include <pthread.h>
#include <strings.h>
#include <sys/select.h>
#include <sys/uio.h>

/* HTTP请求的状态 */
#define HTTP_INFO(x)     ((x) >= 100 && (x) <= 199)
#define HTTP_SUCCESS(x)  ((x) >= 200 && (x) <= 299)

/* 打印作业 */
struct job
{
    struct job *next;    /* next in list */
    struct job *prev;    /* previous in list */
    long jobid;          /* job ID */
    struct printreq req;  /* copy of print request */
};

/* 接收打印请求的线程 */
struct worker_thread
{
    struct worker_thread *next;  /* next in list */
    struct worker_thread *prev;  /* privious in list */
    pthread_t tid;               /* thread ID */
    int sockfd;                  /* socket */
};

int             log_to_stderr = 1;   /* 0表示将日志发送到系统日志而不是标准错误输出 */

struct addrinfo *printer;            /* 打印机的网络地址 */
char            *printer_name;       /* 打印机的主机名 */
pthread_mutex_t configlock = PTHREAD_MUTEX_INITIALIZER; /* 保护对reread的访问 */
int             reread;              /* 当改变了打印机或打印机的网络地址,需重读配置文件 */

/* 线程相关 */
struct worker_thread  *workers; /* 线程的双向链表的头部,接受来自客户端的文件 */
pthread_mutex_t workerlock = PTHREAD_MUTEX_INITIALIZER; /* 保护链表 */
sigset_t              mask;     /* 线程的信号掩码 */

/* 作业链表 */
struct job *jobhead, *jobtail;  
int        jobfd;
long       nextjob;
pthread_mutex_t  joblock = PTHREAD_MUTEX_INITIALIZER; /* 保护作业链表和jobwait代表的条件 */
pthread_cond_t   jobwait = PTHREAD_COND_INITIALIZER;

void init_request(void);
void init_printer(void);
void update_jobno(void);
long get_newjobno(void);
void add_job(struct printreq *, long);
void replace_job(struct job *);
void remove_job(struct job*);
void build_qonstart(void);
void *client_thread(void*);
void *printer_thread(void *);
void *signal_thread(void *);
ssize_t readmore(int, char **, int, int*);
int printer_status(int, struct job *);
void add_worker(pthread_t, int);
void kill_workder(pthread_t, int);
void client_cleanup(void *);

/* 1、初始化守护进程
   2、处理来自客户端的连接请求 */
int main(int argc, char *argv[])
{
    pthread_t  tid;
    struct addrinfo *ailist, *aip;
    int sockfd, err, i, n, maxfd;
    char *host;
    fd_set rendezvous, rset;
    struct sigaction sa;
    struct passwd *pwdp;

    if(argc != 1)
        err_quit("usage: printd");

    /* 成为一个守护进程,此后,不能在标志出错上打印出错消息,而是对其记录日志 */
    /*daemonize("printd");*/

    /* 忽略SIGPIPE,因为写错误触发SIGPIPE后,默认动作为杀死进程 */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = SIG_IGN;
    if(sigaction(SIGPIPE, &sa, NULL) < 0)
        log_sys("sigaction failed");

    /* 创建的所有线程均继承此信号掩码。SIGHUP告诉守护进程再次读取配置文件,
       SIGTERM告诉守护进程执行清理工作并退出 */
    sigemptyset(&mask);
    sigaddset(&mask, SIGHUP);
    sigaddset(&mask, SIGTERM);
    if((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0)
        log_sys("pthread_sigmask failed");

    init_request(); /* 初始化作业请求并确保只有一个守护进程的副本运行 */
    init_printer(); /* 初始化打印机信息 */

    /* 获取打印假脱机计算机系统名 */
  #ifdef _SC_HOST_NAME_MAX
    n = sysconf(_SC_HOST_NAME_MAX);
    if(n < 0)
  #endif
    n = HOST_NAME_MAX; /* print.h */
    if((host = malloc(n)) == NULL)
        log_sys("malloc error");
    if(gethostname(host, n) < 0)
        log_sys("gethostname error");

    /* 得到打印假脱机网络地址 */
    if((err = getaddrlist(host, "printer", &ailist)) != 0)
    {
        log_quit("getaddrinfo error: %s", gai_strerror(err));
        exit(1);
    }
    FD_ZERO(&rendezvous); /* 与select一起等待客户端的连接请求 */
    maxfd = -1; /* 初始化为-1,以确保分配的第一个文件描述符大于maxfd */
    for(aip = ailist; aip != NULL; aip = aip->ai_next)
    {
            /* 对每个需要提供服务的网络地址,分配和初始化套接字 */
        if((sockfd = initserver(SOCK_STREAM, aip->ai_addr,
            aip->ai_addrlen, QLEN)) >= 0)
        {
            /* 将套接字文件描述符加入fd_set */
            FD_SET(sockfd, &rendezvous);
            if(sockfd > maxfd)
                maxfd = sockfd;
        }   
    }
    /* 不能启动打印假脱机服务,记录日志后退出 */
    if(maxfd == -1)
        log_quit("service not enabled");

    /* 守护进程使用超级用户特权绑定套接字到保留端口后,
           将用户ID改变到用户lp,降低特权 */
    pwdp = getpwnam("lp");
    if(pwdp == NULL)       /* 没有lp用户 */
        log_sys("can't find user lp");
    if(pwdp->pw_uid == 0)  /* lp具有超级用户特权 */
        log_quit("user lp is privileged");
    if(setuid(pwdp->pw_uid) < 0)  /* 将实际和有效用户ID改为lp用户ID */
        log_sys("can't change IDs to user lp");

    /* 创建一个与打印机通信的线程 */
    pthread_create(&tid, NULL, printer_thread, NULL);

    /* 创建一个处理信号的线程 */
    pthread_create(&tid, NULL, signal_thread, NULL);

    /* 在/var/spool/printer目录中搜索任何挂起的作业。对于找的每个作业,
           将建立一个结构,让打印机线程指定要将改作业的文件送到打印机 */
    build_qonstart();

    /* 守护进程初始化完成 */
    log_msg("daemon initialized");

    for(;;)
    {
        /* select会修改传入的fd_set结构以保护满足事件的描述符 */
        rset = rendezvous;

        /* 等待其中一个描述符变为可读 */
        if(select(maxfd+1, &rset, NULL, NULL, NULL) < 0)
            log_sys("select failed");

        for(i=0; i<=maxfd; i++)
        {
            /* 如果一个描述符可读,说明一个连接请求需要处理,
               调用accept接受连接请求 */
            if(FD_ISSET(i, &rset))
            {
                sockfd = accept(i, NULL, NULL);
                if(sockfd < 0)
                    log_ret("accept failed");

                /* 创建一个线程处理客户端请求 */
                pthread_create(&tid, NULL, client_thread,
                        (void *)sockfd);
            }
        }
    }
    exit(1);
}

/* 在作业/var/spool/printer/jobno上放一个记录锁,然后
   读该文件以确定下一个要赋予的作业号 */
void init_request(void)
{
    int n;
    char name[FILENMSZ];

    sprintf(name, "%s/%s", SPOOLDIR, JOBFILE);
    jobfd = open(name, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR);

    /* 在整个文件上放一把写锁,表明守护进程正在运行 */
    if(write_lock(jobfd, 0, SEEK_SET, 0) < 0)
        log_quit("daemon already running");

    /* 作业文件中包含一个ASCII码整数字符串,表示下一个作业号 */
    if((n = read(jobfd, name, FILENMSZ)) < 0)
        log_sys("can't read job file");

    /* 如果文件刚创建 */
    if(n == 0)
        nextjob = 1;
    else
        nextjob= atol(name);
}

/* 设置打印机计算机系统名和地址 */
void init_printer(void)
{
    printer = get_printaddr();
    if(printer == NULL)
    {   
        log_msg("no printer device registered");
        exit(1);
    }
    printer_name = printer->ai_canonname;
    if(printer_name == NULL)
        printer_name = "printer";

    /* 将使用的打印机名字记录在日志中 */
    log_msg("printer is %s", printer_name);
}

/* 在作业文件/var/spool/printer/jobno中写入下一个作业号 */
void update_jobno(void)
{
    char buf[32];

    /* 找到文件开头 */
    lseek(jobfd, 0, SEEK_SET);

    /* 将作业号转换为字符串后写入文件 */
    sprintf(buf, "%ld", nextjob);
    if(write(jobfd, buf, strlen(buf)) < 0)
        log_sys("can't update job file");
}

/* 获得下一个作业号 */
long get_newjobno(void)
{
    long jobid;

    pthread_mutex_lock(&joblock);
    jobid = nextjob++;
    if(nextjob <= 0) /* 超出范围时回绕 */
        nextjob = 1;
    pthread_mutex_unlock(&joblock);
    return (jobid);
}

/* 在挂起的打印作业列表末尾加一个新的打印请求 */
void add_job(struct printreq *reqp, long jobid)
{
    struct job *jp;

    if((jp = malloc(sizeof(struct job))) == NULL)
        log_sys("malloc failed");

    /* 将客户端的请求复制到作业结构 
       作业结构包括一对指针,作业ID,从客户端print命令发来的printreq*/
    memcpy(&jp->req, reqp, sizeof(struct printreq));

    jp->jobid = jobid;
    jp->next = NULL;

    pthread_mutex_lock(&joblock); /* 获得对打印作业链表的独占访问 */
    jp->prev = jobtail;  /* 将作业添加到列表末尾 */
    if(jobtail == NULL)
        jobhead = jp;
    else
        jobtail->next = jp;
    jobtail = jp;
    pthread_mutex_unlock(&joblock); /* 解锁 */

    /* 告诉打印机线程有一个作业可用了 */
    pthread_cond_signal(&jobwait);
}

/* 将作业插入到挂起作业列表头部 */
void replace_job(struct job *jp)
{
    pthread_mutex_lock(&joblock);
    jp->prev = NULL;
    jp->next = jobhead;
    if(jobhead == NULL)
        jobtail = jp;
    else
        jobhead->prev = jp;
    jobhead = jp;
    pthread_mutex_unlock(&joblock);
}

/* 将作业从挂起的作用列表中删除 */
void remove_job(struct job *target)
{
    if(target->next != NULL)
        target->next->prev = target->prev;
    else
        jobtail = target->prev;
    if(target->prev != NULL)
        target->prev->next = target->next;
    else
        jobhead = target->next;
}

/* 当守护进程启动,从/var/spool/printer/reqs中的文件
   建立一个内存中的打印作业列表 */
void build_qonstart(void)
{
    int fd, err, nr;
    long jobid;
    DIR *dirp;
    struct dirent *entp;
    struct printreq req;
    char dname[FILENMSZ], fname[FILENMSZ];

    sprintf(dname, "%s/%s", SPOOLDIR, REQDIR);
    if((dirp = opendir(dname)) == NULL)
        return;

    while((entp = readdir(dirp)) != NULL)
    {
        /* 忽略.和.. */
        if(strcmp(entp->d_name, ".") == 0 ||
           strcmp(entp->d_name, "..") == 0)
            continue;

        /* 创建文件的完全路径名并打开 */
        sprintf(fname, "%s/%s/%s", SPOOLDIR, REQDIR, entp->d_name);
        if((fd = open(fname, O_RDONLY)) < 0)
            continue;

        /* 读取保存在文件中的printreq结构 */
        nr = read(fd, &req, sizeof(struct printreq));
        if(nr != sizeof(struct printreq))
        {
            if(nr < 0)
                err = errno;
            else
                err = EIO;
            close(fd);
            log_msg("build_qonstart: can't read %s: %s",
                fname, strerror(err));
            unlink(fname);
            sprintf(fname, "%s/%s/%s", SPOOLDIR, DATADIR, entp->d_name);
            unlink(fname);
            continue;
        }
        jobid = atol(entp->d_name);
        log_msg("adding job %ld to queue", jobid);
        add_job(&req, jobid);
    }
    closedir(dirp);
}

/* 当请求被接受时,main线程派生出client_thread,
   从客户端print命令中接收要打印的文件,
   为每个客户端打印请求分别创建一个独立的线程 */
void *client_thread(void *arg)
{
    int n, fd, sockfd, nr, nw, first;
    long jobid;
    pthread_t tid;
    struct printreq req;
    struct printresp res;
    char name[FILENMSZ];
    char buf[IOBUFSZ];

    tid = pthread_self();

    /* 安装线程清理处理程序 */
    pthread_cleanup_push(client_cleanup, (void *)tid);

    /* 创建一个worker_thread结构并加入到活跃的客户端线程列表 */
    sockfd = (int)arg;
    add_worker(tid, sockfd);

    /* 完成了线程初始化任务,从客户端读取请求首部 */
    if((n = treadn(sockfd, &req, sizeof(struct printreq), 10)) !=
        sizeof(struct printreq))
    {
        res.jobid = 0;
        if(n < 0)
            res.retcode = htonl(errno);
        else
            res.retcode = htonl(EIO);
        strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);
        writen(sockfd, &res, sizeof(struct printresp));
        pthread_exit((void *)1);
    }

    req.size = ntohl(req.size);
    req.flags = ntohl(req.flags);

    /* 建立数据文件 */
    jobid = get_newjobno();
    sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jobid);
    if((fd = creat(name, FILEPERM)) < 0)
    {
        /* 如不能创建,则记录错误日志,发送响应失败,结束线程 */
        res.jobid = 0;
        if(n < 0)
            res.retcode = htonl(errno);
        else
            res.retcode = htonl(EIO);
        log_msg("client_thread: can't create %s: %s", name,
            strerror(res.retcode));
        strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);
        writen(sockfd, &res, sizeof(struct printresp));
        pthread_exit((void *)1);    
    }

    /* 读取来自客户端的文件内容,将其写入数据文件的私有副本 */
    first = 1;
    while((nr = tread(sockfd, buf, IOBUFSZ, 20)) > 0)
    {
        if(first)
        {
            first = 0;
            /* 如果不是以%!PS开头,则不是PostScript文件,
                   可认为是纯文本文件 */
            if(strncmp(buf, "%!PS", 4) != 0)
                req.flags |= PR_TEXT;   
        }

        nw = write(fd, buf, nr);

        /* 如果写入失败,记录出错日志,关闭文件,向客户端发送错误消息,终止线程 */
        if(nw != nr)
        {
            if(nw < 0)  
                res.retcode = htonl(errno);
            else
                res.retcode = htonl(EIO);
            log_msg("client_thread: can't write %s: %s", name,
                strerror(res.retcode));
            close(fd);
            strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);
            writen(sockfd, &res, sizeof(struct printresp));
            unlink(name);
            pthread_exit((void *)1);
        }
    }
    close(fd);

    /* 创建文件/var/spool/printer/reqs/jobid以记住打印请求 */
    sprintf(name, "%s/%s/%ld", SPOOLDIR, REQDIR, jobid);
    fd = creat(name, FILEPERM);

    if(fd < 0)
    {
        res.jobid = 0;
        if(n < 0)
            res.retcode = htonl(errno);
        else
            res.retcode = htonl(EIO);
        log_msg("client_thread: can't create %s: %s", name, 
            strerror(res.retcode));
        strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);
        writen(sockfd, &res, sizeof(struct printresp));
        sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jobid);
        unlink(name);;
        pthread_exit((void *)1);
    }

    /* 将printreq写入控制文件 */
    nw = write(fd, &req, sizeof(struct printreq));

    if(nw != sizeof(struct printreq))
    {
        res.jobid = 0;
        if(nw < 0)
            res.retcode = htonl(errno);
        else
            res.retcode = htonl(EIO);
        log_msg("cliegnt_thread: can't write %s: %s", name,
            strerror(res.retcode));
        close(fd);
        strncpy(res.msg, strerror(res.retcode), MSGLEN_MAX);
        writen(sockfd, &res, sizeof(struct printresp));
        unlink(name);
        sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jobid);
            unlink(name);
        pthread_exit((void *)1);
    }
    close(fd);

    /* 向客户端发送响应,包括作业ID和成功状态 */
    res.retcode = 0;
    res.jobid = htonl(jobid);
    sprintf(res.msg, "request ID %ld", jobid);
    writen(sockfd, &res, sizeof(struct printresp));

    /* 将接收到的作业加入到挂起的打印作业列表,完成清理过程 */
    log_msg("adding job %ld to queue", jobid);
    add_job(&req, jobid);
    pthread_cleanup_pop(1);
    return ((void *)0);
}

/* 将一个worder_htread结构加入到活动线程列表头部 */
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;
    pthread_mutex_lock(&workerlock);
    wtp->prev = NULL;
    wtp->next = workers;
    if(workers == NULL)
        workers = wtp;
    else
        workers->prev = wtp;
    pthread_mutex_unlock(&workerlock);
}

/* 遍历工作者线程列表然后一一删除 */
void kill_workers(void)
{
    struct worker_thread *wtp;

    pthread_mutex_lock(&workerlock);
    for(wtp = workers; wtp != NULL; wtp = wtp->next)
    {
        pthread_cancel(wtp->tid);
    }
    pthread_mutex_unlock(&workerlock);
}

/* 与客户端命令通信的工作者线程的线程清理程序 
   当线程调用pthread_exit时,用一个非零参数调用pthread_cleanup_pop时,
   或者响应一个删除请求时,该函数被调用,参数为终止线程的线程ID*/
void client_cleanup(void *arg)
{
    struct worker_thread *wtp;
    pthread_t tid;

    tid = (pthread_t)arg;
    pthread_mutex_lock(&workerlock);
    for(wtp = workers; wtp != NULL; wtp = wtp->next)
    {
        if(wtp->tid == tid)
        {
            if(wtp->next != NULL)
                wtp->next->prev = wtp->prev;
            if(wtp->prev != NULL)
                wtp->prev->next = wtp->next;
            else
                workers = wtp->next;
            break;
        }
    }
    pthread_mutex_unlock(&workerlock);
    if(wtp != NULL)
    {
        close(wtp->sockfd);
        free(wtp);
    }
}

/* 负责处理信号的线程运行 */
void *signal_thread(void *arg)
{
    int err, signo;

    for(;;)
    {
        /* 等待SIGHUP或SIGTERM信号出现 */
        err = sigwait(&mask, &signo);
        if(err != 0)
            log_quit("sigwait failed: %s", strerror(err));
        switch(signo)
        {
        case SIGHUP:
            pthread_mutex_lock(&configlock);
            reread = 1;
            pthread_mutex_unlock(&configlock);
            break;
        case SIGTERM:
            kill_workers();  /* 杀死所有工作者线程 */
            log_msg("terminate with signal %s", strsignal(signo));
            exit(0);
        default:
            kill_workers();
            log_quit("unexpected signal %d", signo);
        }
    }
}

/* 在送往打印机的IPP首部添加选项 */
char *add_option(char *cp, int tag, char *optname, char *optval)
{
    int n;
    union
    {
        int16_t s;
        char c[2];
    }u;

    *cp++ = tag;   /* 描述属性的1字节标志 */
    n = strlen(optname);
    u.s = htons(n); 
    *cp++ = u.c[0]; /* 以2字节的二进制整数形式存储的属性名长度 */
    *cp++ = u.c[1]; 
    strcpy(cp, optname);  /* 属性名 */
    cp += n;
    n = strlen(optval);
    u.s = htons(n);       /* 属性值大小 */
    *cp++ = u.c[0];
    *cp++ = u.c[1];       
    strcpy(cp, optval);   /* 属性值 */
    return (cp + n);
}

/* 由于网络打印机通信的线程运行 */
void *printer_thread(void *arg)
{
    struct job *jp;
    int hlen, ilen, sockfd, fd, nr, nw;
    char *icp, *hcp;
    struct ipp_hdr *hp;
    struct stat sbuf;
    struct iovec    iov[2];
    char name[FILENMSZ];
    char hbuf[HBUFSZ];
    char ibuf[IBUFSZ];
    char buf[IOBUFSZ];
    char str[64];

    for(;;)
    {
        /* Get a job to print */
        pthread_mutex_lock(&joblock);
        while(jobhead == NULL)
        {
            log_msg("printer_thread: waiting...");
            /* 等待到来的作业 */
            pthread_cond_wait(&jobwait, &joblock); 
        }
        /* 当一个作业准备好时,将其从列表删除 */
        remove_job(jp = jobhead);
        log_msg("printer_thread: picked up job %ld", jp->jobid);
        pthread_mutex_unlock(&joblock);

        /* 将下一个作业号写到/var/spool/printer/jobno */
        update_jobno();

        /* 检查配置文件有无改变 */
        pthread_mutex_lock(&configlock);
        if(reread)
        {
            freeaddrinfo(printer);
            printer = NULL;
            printer_name = NULL;
            reread = 0;
            pthread_mutex_unlock(&configlock);
            init_printer();
        }
        else
            pthread_mutex_unlock(&configlock);

        /* Send job to printer */

        sprintf(name, "%s/%s/%ld", SPOOLDIR, DATADIR, jp->jobid);
        if((fd = open(name, O_RDONLY)) < 0)
        {
            log_msg("job %ld canceled - can't open %s: %s",
                jp->jobid, name, strerror(errno));
            free(jp);
            continue;;
        }
        if(fstat(fd, &sbuf) < 0)
        {
            log_msg("job %ld canceled - can't fstat %s: %s",
                jp->jobid, name, strerror(errno));
            free(jp);
            close(fd);
            continue;
        }

        /* 建立一个流套接字与打印机通信 */
        if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        {
            log_msg("job %ld deferred - can't create socket: %s",
                jp->jobid, strerror(errno));
            goto defer;
        }
        /* 连接打印机 */
        if(connect_retry(sockfd, printer->ai_addr, printer->ai_addrlen) < 0)
        {
            log_msg("job %ld deferred - can't contact printer: %s",
                jp->jobid, strerror(errno));
            goto defer;
        }

        /* 建立IPP首部 */
        icp = ibuf;
        hp = (struct ipp_hdr *)icp;
        hp->major_version = 1;
        hp->minor_version = 1;
        hp->operation = htons(OP_PRINT_JOB); /* 2字节操作ID */
        hp->request_id = htonl(jp->jobid);   /* 4字节作业ID */
        icp += offsetof(struct ipp_hdr, attr_group);
        *icp++ = TAG_OPERATION_ATTR;  /* 设置标志值以显示其后跟随的操作属性 */
        icp = add_option(icp, TAG_CHARSET, "attributes-charset",
                 "utf-8");    /* 字符集 */
        icp = add_option(icp, TAG_NATULANG,   /* 指定语言 */
                 "attribtes-natural-language", "en-us");
        /* URI  http://printer_name:631*/
        sprintf(str, "http://%s:%d", printer_name, IPP_PORT);
        icp = add_option(icp, TAG_URI, "printer-uri", str); 
        icp = add_option(icp, TAG_NAMEWOLANG, /* 推荐,非必需 */
                "requesting-user-name", jp->req.usernm); 
        icp = add_option(icp, TAG_NAMEWOLANG, "job-name",  /* 可选 */
                 jp->req.jobnm);
        if(jp->req.flags & PR_TEXT)
        {
            icp = add_option(icp, TAG_MIMETYPE, "document-format",
                    "text/plain");
        }
        else
        {
            icp = add_option(icp, TAG_MIMETYPE, "document-format",
                    "application/postscript");
        }
        *icp++ = TAG_END_OF_ATTR;  /* 在属性结束出用属性结束标志定界 */
        ilen = icp - ibuf;  /* 计算首部大小 */

        /* 在知道IPP首部大小的情况下,可建立HTTP首部 */
        hcp = hbuf;
        sprintf(hcp, "POST/%s/ipp HTTP/1.1\r\n", printer_name);
        hcp += strlen(hcp);
        sprintf(hcp, "Content-Length: %ld\r\n", 
            (long)sbuf.st_size + ilen);
        hcp += strlen(hcp);
        strcpy(hcp, "Content-Type: application/ipp\r\n");
        hcp += strlen(hcp);
        sprintf(hcp, "Host: %s:%d\r\n", printer_name, IPP_PORT);
        hcp += strlen(hcp);
        *hcp++ = '\r';   /* 用回车换行符结束HTTP首部 */
        *hcp++ = '\n';
        hlen = hcp - hbuf;

        /* Write the headers first. Then send the file */

        iov[0].iov_base = hbuf;  /* 指向HTTP首部 */
        iov[0].iov_len = hlen;   
        iov[1].iov_base = ibuf;  /* 指向IPP首部 */
        iov[1].iov_len = ilen;
        if((nw = writev(sockfd, iov, 2)) != hlen + ilen)
        {
            log_ret("can't write to printer");
            goto defer;
        }
        /* 将数据文件发送到打印机 
           把数据文件读入IOBUFSZ缓冲区并写入与打印机相连的套接字*/
        while((nr = read(fd, buf, IOBUFSZ)) > 0)
        {
            if((nw = write(sockfd, buf, nr)) != nr)
            {
                if(nw < 0)
                    log_ret("can't write to printer");
                else
                    log_msg("short write (%d/%d) to printer",
                        nw, nr);
                goto defer;
            }
        }
        if(nr < 0)
        {
            log_ret("can't read %s", name);
            goto defer;
        }

        /* 接收打印机发回的请求响应 */
        if(printer_status(sockfd, jp))  /* 返回正值,则成功 */
        {
            unlink(name);
            sprintf(name, "%s/%s/%ld", SPOOLDIR, REQDIR, jp->jobid);
            unlink(name);
            free(jp);
            jp = NULL;
        }

    defer:  /* 清理并延迟一段时间后再尝试 */
        close(fd);
        if(sockfd >= 0)
            close(sockfd);
        if(jp != NULL)
        {
            replace_job(jp);
            sleep(60);
        }
    }
}

/* 读取来自打印机的部分响应消息 */
ssize_t readmore(int sockfd, char **bpp, int off, int *bszp)
{
    ssize_t nr;
    char *bp = *bpp;
    int bsz = *bszp;

    if(off >= bsz)
    {
        bsz += IOBUFSZ;
        if((bp = realloc(*bpp, bsz)) == NULL)
            log_sys("readmore: can't allocate bigger read buffer");
        *bszp = bsz;
        *bpp = bp;
    }
    /* 从缓冲区已读数据的末尾开始读取缓冲区 */
    if((nr = tread(sockfd, &bp[off], bsz-off, 1)) > 0)
        return (off + nr); /* 返回相应的已读数据末尾的新偏移量 */
    else
        return (-1);
}

/* 读取打印机对一个打印作业请求的响应消息 
   打印机响应的方式:
   在多个报文里回送一个响应;
   或在一个报文里回送完整的响应;
   或包括一个中间确认,如HTTP 100 Continue报文 */
int printer_status(int sockfd, struct job *jp)
{
    int i, success, code, len, found, bufsz;
    long jobid;
    ssize_t nr;
    char *statcode, *reason, *cp, *contentlen;
    struct ipp_hdr *hp;
    char *bp;

    success = 0;
    bufsz = IOBUFSZ;  /* 存储来自打印机的数据 */
    if((bp = malloc(IOBUFSZ)) == NULL)
        log_sys("printer_status: can't allocate read buffer");
    /* 期望5秒内有可用的响应 */
    while((nr = tread(sockfd, bp, IOBUFSZ, 5)) > 0)
    {
        /* 跳过HTTP/1.1和报文开始的所有空格 */
        cp = bp + 8;
        while(isspace((int)*cp))
            cp++;
        /* 如果是状态码 */
        statcode = cp;
        while(isdigit((int)*cp))
            cp++;
        if(cp == statcode) /* Bad format; log it and move on */
        {
            log_msg(bp);
        }
        else
        {
            *cp++ = '\0';
            reason = cp;  /* 出错原因 */
            while(*cp != '\r' && *cp != '\n')
                cp++;
            *cp = '\0';
            code = atoi(statcode);
            if(HTTP_INFO(code)) /* 仅是提供信息的报文 */
                continue;
            if(!HTTP_SUCCESS(code))  /* 出错消息 */
            {
                bp[nr] = '\0';
                log_msg("error: %s", reason);
                break;
            }

            /* HTTP请求成功,检查IPP状态 
               首先找到Content-Length属性 */
            i = cp - bp;
            for(;;)
            {
                /* HTTP首部关键字大小写敏感 */
                while(*cp != 'C' && *cp != 'c' && i < nr)
                {
                    cp++;
                    i++;
                }
                /* 如果缓冲区空间耗尽,需再次读 */
                if(i >= nr && 
                  ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0))
                    goto out;
                cp = &bp[i]; /* readmore调用realloc可能会改变缓冲区地址 */

                /* 忽略大小写比较字符串
                   如果找到Content-Length属性字符串,搜索其值,并转换为整数 */
                if(strncasecmp(cp, "Content-Length:", 15) == 0)
                {
                    cp += 15;
                    while(isspace((int)*cp))
                        cp++;
                    contentlen = cp;
                    while(isdigit((int)*cp))
                        cp++;
                    *cp++ = '\0';
                    i = cp - bp;
                    len = atoi(contentlen);
                    break;
                }
                else
                {
                    cp++;
                    i++;
                }
            }
            if(i >= nr && 
              ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0))
                goto out;
            cp = &bp[i];

            /* 搜索HTTP首部的结束部分(一个空白行)*/
            found = 0;
            while(!found)
            {
                while(i < nr - 2)
                {
                    if(*cp == '\n' && *(cp + 1) == '\r' &&
                       *(cp + 2) == '\n')
                    {
                        found = 1;
                        cp += 3;
                        i += 3;
                        break;
                    }
                    cp++;
                    i++;
                }
                if(i >= nr && 
                  ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0))
                    goto out;
                cp = &bp[i];
            }
            /* 如果所读取的数据大小减去HTTP首部的大小后不等于IPP报文的数据长度
               需再次读取 */
            if(nr - i < len && 
              ((nr = readmore(sockfd, &bp, i, &bufsz)) < 0))
                goto out;
            cp = &bp[i];

            /* 从报文IPP首部中获取状态和作业ID 
                   两者均以网络字节序的整数形式存储,因此需转换为主机字节序*/
            hp = (struct ipp_hdr *)cp;
            i = ntohs(hp->Status);
            jobid = ntohl(hp->request_id);
            if(jobid != jp->jobid)
            {
                log_msg("jobid %ld status code %d", jobid, i);
                break;
            }
            if(STATCLASS_OK(i))
                success = 1;
            break;
        }
    }

    out:
    free(bp);
    if(nr < 0)
    {
        log_msg("jobid %ld: error reading printer response: %s", 
                jobid, strerror(errno));

    }

    return (success);
}



版权声明:本文为u012319493原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。