MobileLPR - 适用于 .NET Compact Framework 2.0 的 LPR 客户端





5.00/5 (4投票s)
适用于 LPR、LPRng 和 Socket API 的 Windows CE/Mobile 打印客户端。
引言
MobileLPR 是一个为 .NET Compact Framework 2.0 构建的 LPR 客户端库,它使 Windows Mobile 应用程序能够打印到 LPR(亦称 LPD)服务器、LPRng 服务器或 Socket API 打印机。所有这些协议的共同点是使用 TCP 套接字传输数据。LPR 协议已经存在了很长时间,打印服务器设备和带有内部网络接口的打印机非常普遍,而移动设备的打印解决方案却非常匮乏,这让我有些惊讶,因为网络上实际可用的移动设备打印代码是如此之少。
我为什么要构建它?在经过大约一周的网络搜索,结果却微乎其微,并且我对相对较小的 LPR 文档的心理消化能力有所提高之后,我决定这种情况给我提供了一个很好的机会,既可以创建我需要的功能,又可以将有用的代码回馈给编程社区,而我已经从中受益良久。
目标是组建一个小型、高性能的库,在移动环境中运行,包含最有用的 LPR 功能,同时不进行过度设计。有些部分当然可以写得更高效,但它们完成了预期的任务。我来自 C 语言编程背景,我知道如何编写非常紧凑和晦涩的代码来完成工作,但六个月后,即使是我也无法解读它。我的目标是编写可维护的代码。
背景
我过去几十年的职业生涯主要围绕着让移动设备为大大小小的企业高效工作。每隔一两年就会出现一批新设备,有些具有新功能,更快的硬件,或者只是以新颖且无用的方式重新包装。我看到这个行业每五年就会向前迈进一大步,不仅带来了更大的内存和更快的处理器,而且实际上在这些手掌大小的计算奇迹中融入了更多的功能。在 1990 年代后期,微软推出了一个新的操作系统 Windows CE,它将个人手持设备的角色商品化,并向普通程序员开放了以前专有的便携式计算世界。大约十年后,呼应互联网的发展,这些设备进化到包含网络连接和许多较不便携平台的各种功能。如今,几乎每个人都使用某种无线设备,其编程方式与其工业同类产品几乎相同。
用于固定站数据采集、扫描和打印的众多专用设备被取代,孤立的单一功能组件家族合并成今天使用的通用平台。许多常见功能在质量和易用性方面都得到了极大改进,但打印是移动计算平台几乎被时代遗忘的一项功能。MobileLPR 的编写正是为了解决这一不足。
在为一位长期忠诚的客户重写系统时,我面临着需要替换一个无线打印站的要求,该打印站是由一台工业标签打印机和一个配备无线电和嵌入式程序的小型“砖块”拼凑而成的。这个打印节点从一个自定义的 PC 服务器接收指令,为一小队配备移动计算机的用户提供特殊的库存标签请求服务。
现在有经济实惠的打印机,它们要么包含自己的内部打印服务器,要么可以配备外部打印服务器,这些服务器可以与多种标准打印协议进行通信。它们可以部署到有线或无线网络上,几乎不需要额外的基础设施或持续管理即可运行。这就是被选为新系统标签站的设备类型,并由此产生了我的需求:一个允许我通过标准协议打印到网络打印机的接口。
在开始这个项目之前,我只找到了几个听起来可能对我有用的 Windows Mobile 打印客户端软件包。在仔细阅读了它们的信息,熟悉了 LPR 协议,并盯着一个空钱包之后,我无法证明为解决这样一个相对简单的任务而支付数百美元购买商业软件包是合理的。因此,编写 MobileLPR 库以图乐趣(而非盈利)的想法应运而生。
我在网络上找到了几篇关于 LPR 的不可或缺的文档。主要文档是RFC 1179 - 行式打印机守护程序协议,这是一份关于现有 LPR 协议的指南,主要适用于 BSD Unix 服务器。它不是一个规范,留下了很多想象空间,但提供了对要遵循的命令集和数据流的基本理解。
我努力过程中最大的财富是优秀的 LPRng SourceForge Project,这是 Patrick Powell 撰写的“Berkely LPR 协议的增强、扩展和可移植实现”。尽管没有详尽的文档,但有大量关于 LPR 工作原理、LPRng 如何实现以及它如何处理 RFC 1179 中缺失的关键指令的实用信息。Powell 先生的源代码及其关于打印的论著使这项原本困难的工作变得相当简单。谢谢你,Powell 先生!
最终结果总结了 MobileLPR 支持的协议
- LPR
- 服务器端口:默认 515。可以是任何端口。
- 本地端口:保留端口 721-732。
- 打印队列:默认“打印机”。必须在服务器上指定一个有效的队列名称。
- LPRng
- 服务器端口:默认 515。可以是任何端口。
- 本地端口:保留端口 512-1023。
- 打印队列:默认“打印机”。必须在服务器上指定一个有效的队列名称。
- 直接(Socket API)
- 服务器端口:默认 9100。可以是任何端口。
- 本地端口:无需保留端口。
- 打印队列:不适用。
如何使用 MobileLPR
MobileLPR 库被设计成一个简单的 DLL,附加到应用程序中。从您的 Visual Studio SmartDevice 项目中引用此 DLL,即可让您的程序使用其所有强大的类。尽管有些平淡无奇,但您真正需要关心的只有一个类:LprJob
。正是这个类负责接收打印参数和数据文件,组织它们以创建由所用协议规定的打印作业,并将数据发送到打印服务器。
首先要从打印服务器入手,如今打印服务器通常内置于打印机中,以识别它支持的功能。为简单起见,本文的其余部分将不区分内部和外部打印服务器,并将它们统称为“打印服务器”。有些提供 Socket API,有些提供 LPD/LPR,有些两者都提供。即使您的打印机不具备网络功能,也有市售的打印服务器设备,价格便宜,并提供直接套接字连接或 LPR;甚至一些无线路由器和 NAS 设备也有。无论物理设备如何承载打印服务器,MobileLPR 都可以与之通信。
一旦确定要使用的打印协议,就需要服务器地址。如果您的网络提供动态 DHCP 服务,您可以为打印服务器配置一个名称并允许它被分配任何地址。如果您的打印服务器在没有动态 DNS 可用的情况下获取地址,可以将 IP 地址绑定到 MAC 地址,或者为打印服务器分配一个静态地址。通过名称还是数字 IP 地址访问取决于您的移动设备的能力。数字 IP 地址应该始终有效。
打印机队列名称是下一个部分。实际上,没有通用的命名约定。一些打印服务器,特别是外部打印服务器,能够从一个单元服务多台打印机。在这些设备中,输出打印机的选择要么通过单独的端口号,要么通过为每台打印机或接口设置单独的打印队列来完成。诸如“P0”、“P1”、“S0”或“S1”之类的队列名称对于分别与连接到并行和串行接口的打印机通信来说并不罕见。一些打印服务器允许您配置所需的任何名称并将其附加到所需的接口。如何配置打印服务器超出了本文的范围,因此在我的示例中,我将使用不明确的队列名称“printer”。
LprJob
类将作业设置分为三个逻辑区域:目的地、可打印数据和监控。每个 LprJob
对象实例都包含连接到打印服务器、传输要打印的数据以及了解传输何时完成所需的所有信息。它还提供了 LPR 协议定义的最基本的队列控制操作。可打印数据以一个或多个本地数据文件的列表形式提供。LprJob
类提供了几个事件,可用于监控打印作业的进度。
识别打印作业目的地
目的地信息识别要连接的打印服务器和端口号,要使用的打印机队列名称(如果有),以及数据传输方法。
指定打印作业目标的最直接方法是使用 SetPrinterURI
方法,用单个字符串标识所有组件。字符串的通用语法遵循通用资源标识符规范,可在 RFC 3986 和 统一资源标识符 (URI): 通用语法 中找到。对于 MobileLPR,其语法为:protocol://[userinfo@]servername[:serverport][/printername]。请注意,如果字符串格式不正确,SetPrinterURI
方法可能会生成 UriFormatException
。
MobileLPR 使用 URI 字符串的方案、权限和路径组件。方案组件标识作业传输协议。使用易读的协议名称(和别名)来选择要采用的作业传输方法
Scheme |
协议 |
直接 |
使用直接套接字 API (TCP) 连接到指定的打印服务器。标准端口为 9100。 |
lpd |
与 lpr 相同。 |
lpr |
使用标准 LPR 协议。标准端口为 515。如果未指定(或指定了无效方案),则这是默认使用的协议。 |
lprng |
在控制文件中使用 LPRng 参数,使用扩展的保留本地端口号,但执行基本的 LPR 传输。标准端口为 515。 |
原始 |
与 direct 相同。 |
授权组件包含打印服务器的名称、用于建立连接的非默认端口号以及用户信息,以防打印服务器需要授权才能执行打印作业。例如,某些 Linux 平台上的 LPR 服务器可能会限制某些用户打印,并为所有其他用户保留或丢弃作业;提供用户名可为打印服务器提供设置适当打印权限所需的信息。使用的默认用户名是“MobileLPR”。
如果您的程序在具有 DNS 解析能力的设备上运行,则服务器名称可以指定为字母数字域名。否则,在此处使用数字 IP 地址可能最安全。您的网络管理员应该能够告诉您哪些服务可用。
端口号可以是可选的。如果打印服务使用标准端口,通过 URI 选择协议也会设置标准端口号。如果您的打印服务使用非标准端口,或者您单独分配连接参数,则必须提供正确的端口号才能成功连接。
路径组件设置将处理打印作业的打印队列的名称。根据您的打印服务器,此名称可能区分大小写,也可能不区分大小写。打印服务器文档应包含您所需的信息。如有疑问,请严格按照打印服务器中配置的打印机队列名称使用。
或者,可以单独设置 Protocol
、ServerName
、ServerPort
、UserName
和 PrinterName
属性。如果您预计向同一目的地发送多个作业,则可以首先设置 DefaultProtocol
、DefaultServerName
、DefaultServerPort
和 DefaultPrinterName
属性,以便使用这些已分配的值创建任何新的 LprJob
类实例。
示例:要使用 LPR 协议将打印作业发送到名为“lprserver”的打印服务器,由端口 715 上的打印队列“P1”服务,允许用户“printuser”打印,您可以使用
LprJob job = new LprJob();
job.SetPrinterURI("lpr://printuser@lprserver:715/P1");
一个更简单的例子是使用所有默认信息通过直接套接字连接打印到带有内部打印服务器的打印机。打印服务器地址是 1.2.3.4
LprJob job = new LprJob();
job.SetPrinterURI("direct://1.2.3.4");
附加数据文件
现在已经指定了打印服务器,是时候将数据文件附加到作业中了。带有单个参数的 AddDataFile
方法将一个命名数据文件作为文字(原始、未翻译)文件附加到作业中。这意味着将发送到打印机的数据必须与打印机期望的完全一致。AddDataFile
方法的重载允许指定用于打印文件的格式,可从 LPR 设计的标准数据格式中选择。在大多数 LPR 安装中,格式控制数据将通过的过滤器以获得所需的打印机输出。
简而言之,格式可分为两组:使用 Literal
和 Raster
格式发送的文字(二进制、原始或未翻译)数据;以及使用所有其他格式发送的文本数据。MobileLPR 不改变地传递文字数据。文本格式假定数据是文本性质的,并且数据从移动设备的本地文本格式转换为 LPR 服务器期望接收的 ASCII 文本。文本表示的数据内容没有以任何方式更改。对于文字或文本数据,应用程序负责生成适当的内容。
格式 |
Content |
|
文字(未翻译,二进制)文件。* |
|
加州理工学院交换格式绘图文件。 |
|
ditroff 输出文件。 |
|
DVI(TeX 输出)文件。 |
|
要格式化的纯文本文件。 |
|
带有 FORTRAN 进纸控制的纯文本。 |
|
通过 Unix 'pr' (分页) 命令处理的文本。 |
|
Berkeley Unix 绘图文件。 |
|
标准 Postscript 文件。 |
|
Sun 光栅图像文件。* |
|
图形系统 C/A/T 照排机文件。 |
*数据未经任何翻译发送。 |
LPRng 协议将数据文件数量限制为 52 个,这对应于远程文件名可用的索引字母数量。LPR 协议没有指定限制,但许多服务器每作业无法处理超过一个数据文件。Direct 协议没有强制执行此限制。
MobileLPR 可能与标准协议有所不同的一点是允许打印文件的多个副本,因为没有明确的文档说明。Copies
指定了作业中所有数据文件要打印的副本数量,而不是针对单个文件。Copies
处理由 MobileLPR 完成。对于 LPR 协议,整个作业会根据指定的副本数量迭代发送。对于 LPRng,通过在作业中多次包含每个数据文件来处理副本。对于 Direct 协议,每个数据文件会通过同一个套接字连接多次发送。实际上,我认为这种设计选择不会给标准打印服务器带来任何问题,但这只是我的假设。
本文附带的 TestMobileLPR 示例程序提供了一个典型示例,展示了使用该库所需的正确步骤。
打印作业监控
在构建此库时的一个设计考虑是保持响应式用户界面。考虑到这一点,负责组织和发送打印作业到打印服务器的 SubmitJob
方法在后台线程上执行整个传输过程。提供了几种机制来确定作业传输何时完成。
最简单的机制是 IsComplete
属性。这是一个标志,在操作进行中时设置为 false,在操作完成时设置为 true。此属性可以定期用于轮询,以查看作业何时完成。
提供的另一种机制是 WaitForCompletion
方法。一个内部同步对象反映了 IsComplete
属性的状态。此方法会一直等待直到同步对象被发出信号,因此无需进行轮询。从您的 UI 代码中调用此方法将暂停用户输入/输出,直到打印作业完成。
最后的监控机制是一组相关的事件,名为 LprJobStarted
、LprJobProgressChanged
和 LprJobCompleted
。两个参数传递给任何指定的事件处理程序。第一个参数是引发事件的 LprJob
对象;第二个是 LprJobProgressEventArgs
的实例,填充了作业要发送的总字节数(计算本地数据中的字节)以及事件引发时已发送的累积字节数。LprJobProgressChanged
事件还传递正在传输的数据文件的本地和远程路径名(在 LPR 和 LPRng 协议的情况下)。
LprJobStarted
事件在作业传输开始时触发一次。Total
事件参数属性将包含要传输的总字节数,您的应用程序可以使用它来设置进度指示器。
LprJobProgressChanged
事件在每次发送新的数据块到服务器时触发。LprJobProgressEventArgs
事件参数属性会填充以反映打印作业的当前状态。
属性 |
Content |
|
整个打印作业要发送的总字节数。 |
|
到目前为止已发送的字节数。 |
|
正在传输的当前文件的本地路径名。 |
|
正在传输的当前文件的远程文件名。 |
|
正在执行的 LPR 命令号。 |
无论作业成功与否,LprJobComplete
事件都保证在作业结束时触发一次。Used
事件参数属性是作业结束时已发送的字节数。在任何操作挂起时,不应处置 LprJob
对象;换句话说,请等到 IsCompleted
为 true。
LastException
属性记录遇到的任何错误。当作业完成时,它将包含终止异常(如果有)。作业启动后不会引发任何异常,这是确定作业是否失败的唯一机制。作业失败的处理方式由您的应用程序决定。
支持的打印作业命令
LPR 协议指定了用于支持打印作业的其他命令。对于 Direct 协议,只有 SubmitJob
方法执行任何实际功能。
方法 |
执行的功能 |
|
将数据文件传输到打印服务器。对于 LPR 和 LPRng,这负责根据协议创建和删除控制文件,以及对命令和数据流进行排序。启动后台线程来执行工作。 |
|
简单地告诉打印服务器开始服务队列,如果尚未运行。通常不需要。 |
|
从打印服务器请求排队作业列表。提供短格式和长格式。响应的结构未定义,并且会因打印服务器而异。可以列出特定的作业编号以将输出限制为这些作业。作业编号必须是服务器生成的。 |
|
从打印队列中删除作业。可以列出特定的作业编号以将删除限制为这些作业,否则如果允许,则删除当前正在打印的作业。作业编号必须是服务器生成的。 |
启动打印作业
当连接已定义、数据文件已附加、监控已设置后,您就可以启动打印作业了。这只需通过调用 SubmitJob
方法即可完成。始终在 try-catch
块中调用命令方法。任何错误,无论是源自 MobileLPR 还是操作系统,都将生成异常。作业启动后,异常将被捕获到 LastException
属性中,供您的应用程序在作业传输完成后检查。
兴趣点
当打印机的宣传材料声称支持 LPD/LPR 协议时,要保持警惕。除非文档明确定义了如何使用它,否则不要相信它。在我的案例中,精美的广告单上明确写着打印机支持 LPD/LPR;然而,在现实生活中,打印机手册中却没有关于它的只言片语。反复试验表明,打印机的网络接口在端口 515 上响应。它会接受打印作业并返回状态消息,但无论我尝试了多少个打印队列名称(包括从相关手册中获取的队列名称),它都不会打印,并且状态消息从未改变。在我为我的漂亮小项目付出了所有努力之后,打印机对 LPR 充耳不闻。
在一个 LPRng 打印作业中最多可以发送 52 个数据文件(包括副本)。尽管 RFC 1179 对此点保持沉默,但一些 LPR 服务器每作业只能处理一个数据文件。LPRng 限制适用于 LPR 和 LPRng 协议,并将忽略额外的文件。
一些打印服务器非常字面地理解 RFC 1179,并将其建议视为金科玉律。对于这些打印服务器,如果指定了不严格遵守准则的远程控制文件或数据文件名,将导致输出失败。作业可能被接受,但什么也不会打印。我的建议是,当你不知道是否支持 LPRng 扩展时,请坚持使用 LPR 协议。
我只实现了 LPRng 的众多增强功能中的一小部分:扩展了连接客户端的保留端口;控制文件中的“A”唯一作业标识符和“Q”原始打印机队列名称参数;通过多次发送数据文件实现的多副本机制之一,通过在远程文件名的额外副本上添加“*.Cn”后缀进行区分;LPRng 针对被拒绝作业的错误消息。其他我认为不必要的功能,例如服务器上的队列操作,我决定省略。
示例用法
以下代码片段演示了如何最基本地使用此库。这是一个适用于智能设备的 Windows Forms 应用程序。它有一个窗体,上面有一个按钮,用于打印程序创建的两行文本文件。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using MobileLPR;
using System.IO;
namespace LprSample
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Exception lastex = null;
LprJob job = null;
try
{
// Create a text file to print.
String tempname = Path.GetTempFileName();
StreamWriter file = new StreamWriter(tempname);
file.WriteLine("Hello, LPR server!");
file.WriteLine("How are you?");
file.Close();
// Create a print job for our file.
job = new LprJob();
// Connect to LPR server "lprserver",
// port 515, queue "printer".
job.SetPrinterURI("lpr://lprserver");
// Send one text data file.
job.AddDataFile(tempname, LprDataFormats.Formatted);
// Transfer the print job to the print server.
job.SubmitJob();
}
catch (Exception ex)
{
lastex = ex;
}
finally
{
if (job != null)
{
// Wait here until job transfer is finished.
job.WaitForCompletion();
lastex = job.LastException;
}
// Handle any errors in lastex here....
MessageBox.Show(lastex.Message.ToString());
}
}
}
}
结论和未来可能性
这个项目还有很大的改进空间。在开始使用 LPR 协议,然后添加一些 LPRng 功能,再到 Socket API 之后,我意识到我可以以更灵活的方式构建基本的作业传输执行。传输协议可以封装在一组相关的类中,每个类提供一个通用的接口来连接、创建任何附加文件(例如,控制文件)或连接、传输数据和监控状态。这些对象将由 LprJob
类的 SendJobFiles
方法在更清晰的传输循环中使用。对于其正常的预期目的,我认为这是过度设计。
IPP 正在迅速成为一种标准化的网络打印协议。它定义了一套丰富而复杂的打印机、服务器和客户端之间的交互。如前所述,它将是一个很好的协议类候选者,但我尚未研究其资源使用或实现难度,以确定它是否会成为此移动打印库的有效补充。
文档总是有很大的改进空间。如果本文和代码中的内部注释不够清楚,我将努力编辑其中一个或两者,以添加缺失的信息或澄清模糊之处。我渴望呈现一个高质量的产品,欢迎您的反馈。
独自编程且工具包不足是一项令人沮丧且吃力不讨好的任务。我希望我在这里提供的能帮助您避免浪费大量时间,就像我不得不经历的那样。祝大家编程顺利,晚安!
历史
- 2010年12月15日发布
- 版本 1.0 - 初始发布。