途中书写在技术与生活的路上,持续记录
← 返回文章

所悟 · 2026-09-26

企业打印服务解决方案:CUPS + Avahi

在企业信息化的世界里,有些系统光鲜亮丽,比如 ERP、BI、AI 平台;有些系统则默默无闻,却每天都在支撑着办公秩序。打印系统就是典型的一类。

企业打印服务解决方案:CUPS + Avahi

在企业信息化的世界里,有些系统光鲜亮丽,比如 ERP、BI、AI 平台;有些系统则默默无闻,却每天都在支撑着办公秩序。打印系统就是典型的一类。

几乎每个企业 IT 都经历过这样的场景:员工电脑换了、驱动丢了、打印机 IP 不知道、某台打印机突然“离线”。看起来是小问题,但当企业规模扩大到几十、几百台打印机时,这些问题会不断重复出现,消耗大量 IT 支持时间。

于是很多企业在多年折腾之后,都会意识到一件事:

打印系统其实也需要一个像 ERP 一样的“服务器”。

这就是 打印服务器(Print Server) 的概念。

打印系统为什么会混乱

在很多公司,打印机最初都是这样接入的:

员工电脑 → 直接连接打印机IP → 打印

表面看很简单,但问题很快就会出现。

每个员工电脑都要安装驱动,驱动版本不同会导致各种兼容问题。 员工不知道打印机 IP,每次都要 IT 帮忙添加。 打印机一旦更换 IP,几十台电脑都要重新配置。

当企业规模稍微扩大一点,这种模式就会变成 IT 运维里的“隐形黑洞”。

打印服务器的核心思想

打印服务器的思路其实非常简单:

所有打印机统一接入服务器,由服务器统一对外提供打印服务。

结构变成:

graph LR
  A[打印机] --> B[打印服务器]
  B[打印服务器] --> C[用户电脑]

这样带来的变化非常明显。

员工不再直接连接打印机,而是连接服务器上的“打印队列”。 驱动只需要在服务器安装一次。 新增打印机只需要服务器配置一次。

IT 从“装驱动”变成“管理打印资源”。

一个简单但强大的开源方案

本文介绍一个开源解决方案:

CUPS(Common Unix Printing System)

CUPS 是 Linux 和 macOS 的核心打印系统,也是 OpenPrinting 项目的核心组件。很多企业级打印系统的底层,其实都是 CUPS。

配合另一个组件:

Avahi(mDNS / Bonjour 服务发现)

就可以实现自动发现局域网打印机。

整体结构是这样的:

  1. 打印机在局域网广播(Bonjour / mDNS)
  2. Avahi监听到广播
  3. CUPS自动发现打印机
  4. 用户从服务器添加打印机

这就是 零配置打印(Zeroconf Printing)。

打印机在局域网中广播自己的信息,服务器自动发现并接入,然后统一提供打印服务。

整个系统非常稳定,而且几乎不需要维护。

网络规划

如果你是刚开始规划企业网络,建议通过VLAN方式,将打印网络单独划分,并设置其他网段能访问到打印服务器所在网段,方便统一管理

结构示意:

打印服务器放置:

打印服务器
192.168.30.10

CUPS 会管理所有打印机:

192.168.20.10
192.168.20.11
192.168.20.12

员工电脑只需要连接服务器:

http://192.168.30.10:631/printers/finance_printer

而不是直接连打印机。

这种架构的好处

第一是 安全性。

办公电脑不能直接访问打印机管理界面。

第二是 管理统一。

所有打印机都通过服务器管理。

第三是 网络更清晰。

系统部署

准备服务器

推荐配置:

系统:Ubuntu Server 22.04
CPU:2核
内存:2GB
IP:固定IP

安装系统后先更新:

sudo apt update
sudo apt upgrade -y

安装 CUPS(打印服务器)

安装:

sudo apt install cups -y

启动服务:

sudo systemctl enable cups
sudo systemctl start cups

查看状态:

systemctl status cups

如果正常,会看到:

Active: active (running)

配置 CUPS

默认 CUPS 只允许本机访问。 编辑配置:

sudo nano /etc/cups/cupsd.conf

修改几个关键配置:

1、修改监听地址

Listen localhost:631
改为:
Port 631

2、允许局域网访问

<Location />
  Order allow,deny
</Location>
改为:
<Location />
  Order allow,deny
  Allow @LOCAL
</Location>

<Location /admin>
  Order allow,deny
</Location>
改为:
<Location /admin>
  Order allow,deny
  Allow @LOCAL
</Location>

3、开启打印机共享

Browsing No
改为:
Browsing Yes

并确认:
BrowseLocalProtocols dnssd

重启 CUPS:

sudo systemctl restart cups

安装 Avahi(自动发现打印机)

安装:

sudo apt install avahi-daemon avahi-utils -y

启动:

sudo systemctl enable avahi-daemon
sudo systemctl start avahi-daemon

检查:

systemctl status avahi-daemon

安装驱动

很多打印机支持 IPP Everywhere,不需要驱动。

如果需要驱动:

安装通用驱动包:

sudo apt install printer-driver-all

HP打印机:

sudo apt install hplip

查看自动发现的打印机

运行:

avahi-browse -rt _ipp._tcp

如果打印机支持 IPP,会看到类似:

HP LaserJet M404 @ 192.168.1.23
Canon iR-ADV C3520 @ 192.168.1.25

这说明:

服务器已经发现打印机。

需要注意的是:CUPS能自动发现同网段打印机,跨网段情况下面会说明

CUPS 管理界面

浏览器打开(认证使用服务器账号密码即可):

http://服务器IP:631

例如:

http://192.168.1.10:631

进入:

Administration → Add Printer

通常会看到:

Discovered Network Printers

注意事项:

  1. 优先选择driverless免驱选项
  2. 添加后注意勾选Share This Printer才能将打印机进行发布
  3. 添加打印机时,相关信息填写完整(Name、Description、Location),方便列表中查看

跨网段添加: 进入:

Administration → Add Printer

选择:

AppSocket / HP JetDirect

这是最通用的打印协议。

然后输入地址:

socket://192.168.20.35:9100

这里的 9100 是打印机最常见端口。

如果打印机支持 IPP,也可以写:

ipp://192.168.20.35/ipp/print

注意前面网络架构提到的,跨VLAN需设置为网络必须允许访问

客户端添加

打印服务器添加好后,可直接在客户端访问打印机列表

https://服务器IP:631/printers

也可在控制面板-打印机添加中查询

如果未列出,可直接使用浏览器地址手动添加

移动端:

更进一步

在部署完服务端后,我们不再需要去找每台打印机的IP是什么,也不需要一台一台安装驱动,下面我们更进一步,无人值守安装

安装门户依赖(在 CUPS 服务器上)

Ubuntu/Debian 示例:

sudo apt update
sudo apt install -y python3 python3-venv python3-pip nginx
sudo apt install -y libcups2-dev
sudo pip3 install pycups flask gunicorn

pycups 用来直接读取 CUPS 的打印队列列表(稳定、简单、无需解析网页)。

创建门户程序

sudo mkdir -p /opt/printer-portal
sudo chown -R $USER:$USER /opt/printer-portal
cd /opt/printer-portal

新建 app.py:

from flask import Flask, Response, render_template_string, request
import cups

app = Flask(__name__)

HTML = """
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8">
<title>公司打印机自助安装</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body{font-family:system-ui;margin:30px;max-width:1100px;}
h1{font-size:24px;}
.group{margin-top:25px}
.card{border:1px solid #ddd;border-radius:10px;padding:12px;margin:10px 0}
.btn{display:inline-block;border:1px solid #444;padding:6px 10px;border-radius:6px;margin-right:6px;text-decoration:none;color:#000;font-size:13px}
.search{margin-top:10px;margin-bottom:15px}
.note{color:#666;font-size:13px;margin:6px 0 14px 0}
</style>

<script>
function filterPrinters(){
  let q=document.getElementById("search").value.toLowerCase()
  let cards=document.getElementsByClassName("card")

  for(let c of cards){
    if(c.innerText.toLowerCase().indexOf(q)>-1)
      c.style.display=""
    else
      c.style.display="none"
  }
}
</script>

</head>

<body>

<h1>公司打印机自助安装</h1>
<div class="note">说明:Windows 建议优先使用“Windows一键安装(推荐)”。PowerShell 脚本可能会被执行策略限制。</div>

<div class="search">
  <input id="search" placeholder="搜索打印机..." onkeyup="filterPrinters()" style="padding:6px;width:260px">
</div>

{% for group, items in printers.items() %}

<div class="group">
  <h2>{{group}}</h2>

  {% for p in items %}
  <div class="card">
    <b>{{p.info}}</b><br>
    队列:{{p.queue}}<br>

    <div style="margin-top:8px">
      <a class="btn" href="/install/windows/{{p.queue}}.cmd">Windows一键安装(推荐)</a>
      <a class="btn" href="/install/windows/{{p.queue}}.bat">Windows一键安装(BAT)</a>
      <a class="btn" href="/install/windows/{{p.queue}}.ps1">PowerShell安装(备用)</a>
      <a class="btn" href="/howto/{{p.queue}}">Mac / Linux</a>
    </div>
  </div>
  {% endfor %}
</div>

{% endfor %}

</body>
</html>
"""

HOWTO = """
<html>
<body style="font-family:system-ui;margin:30px;max-width:1000px">

<h2>{{name}}</h2>

<p>IPP 地址:</p>
<pre>ipp://{{server}}/printers/{{queue}}</pre>

<h3>macOS</h3>
<p>系统设置 → 打印机与扫描仪 → 添加打印机 → IP → 填写上面 IPP 地址</p>

<h3>Linux</h3>
<pre>
sudo lpadmin -p {{queue}} -E -v ipp://{{server}}/printers/{{queue}} -m everywhere
</pre>

<p><a href="/">返回</a></p>

</body>
</html>
"""

def list_printers():
    conn = cups.Connection()
    printers = conn.getPrinters()

    groups = {}

    for q, attr in printers.items():
        location = attr.get("printer-location", "未分类")
        info = attr.get("printer-info", q)

        if location not in groups:
            groups[location] = []

        groups[location].append({
            "queue": q,
            "info": info
        })

    # 让分组和组内打印机都有稳定排序
    for k in groups:
        groups[k] = sorted(groups[k], key=lambda x: (x["info"], x["queue"]))
    groups = dict(sorted(groups.items(), key=lambda x: x[0]))

    return groups

@app.route("/")
def index():
    return render_template_string(HTML, printers=list_printers())

@app.route("/howto/<queue>")
def howto(queue):
    server = request.host.split(":")[0]
    return render_template_string(
        HOWTO,
        queue=queue,
        server=server,
        name=queue
    )

def _text_response(content: str, filename: str):
    # 避免浏览器/系统对编码做奇怪处理:统一 text/plain
    return Response(
        content,
        mimetype="text/plain",
        headers={
            "Content-Disposition": f'attachment; filename="{filename}"',
            "Cache-Control": "no-store",
        },
    )

@app.route("/install/windows/<path:fname>")
def win_install(fname):
    server = request.host.split(":")[0]

    # =========================
    # PowerShell (备用):
    # - 纯英文输出避免乱码
    # - 仍可能被执行策略拦截,所以门户上把 .cmd 作为“推荐”
    # =========================
    if fname.endswith(".ps1"):
        queue = fname[:-4]
        content = f"""$server = "{server}"
$queue  = "{queue}"
$port   = "IPP_$queue"

# Create port (ignore error if exists)
& cscript.exe //nologo "$env:windir\\System32\\Printing_Admin_Scripts\\zh-CN\\prnport.vbs" -a -r $port -h $server -o raw -n 9100 | Out-Null

# Install printer
& "$env:windir\\System32\\rundll32.exe" "printui.dll,PrintUIEntry" /if /b "$queue" /r "$port" /m "Microsoft IPP Class Driver"

"Done."
"""
        return _text_response(content, f"{queue}.ps1")

    # =========================
    # BAT(兼容修复版):
    # - 强制 CRLF
    # - 纯 ASCII(无中文)
    # - 第一行必须是 @echo off 且行首不出现任何 BOM/空白
    # =========================
    if fname.endswith(".bat"):
        queue = fname[:-4]
        # 用拼接的方式强制 CRLF,避免 cmd 在某些环境对 LF 解析异常
        content = (
            f"@echo off\r\n"
            f"setlocal\r\n"
            f"set server={server}\r\n"
            f"set queue={queue}\r\n"
            f"set port=IPP_%queue%\r\n"
            f"\r\n"
            f"cscript //nologo %windir%\\System32\\Printing_Admin_Scripts\\zh-CN\\prnport.vbs -a -r %port% -h %server% -o raw -n 9100\r\n"
            f"\r\n"
            f"rundll32 %windir%\\System32\\printui.dll,PrintUIEntry /if /b \"%queue%\" /r \"%port%\" /m \"Microsoft IPP Class Driver\"\r\n"
            f"\r\n"
            f"echo Done.\r\n"
            f"pause\r\n"
        )
        return _text_response(content, f"{queue}.bat")

    # =========================
    # CMD(推荐):
    # - 双击可执行
    # - 通过 powershell -ExecutionPolicy Bypass 运行安装逻辑
    # - 基本不受“PS1右键/策略/MOTW”影响
    # =========================
    if fname.endswith(".cmd"):
        queue = fname[:-4]
        content = (
            "@echo off\r\n"
            "setlocal\r\n"
            "cd /d %~dp0\r\n"
            "\r\n"
            ":: Run installation via PowerShell with ExecutionPolicy Bypass\r\n"
            f"powershell -NoProfile -ExecutionPolicy Bypass -Command "
            f"\"$s='{server}'; $q='{queue}'; $port='IPP_'+$q; "
            f"& cscript.exe //nologo $env:windir\\System32\\Printing_Admin_Scripts\\zh-CN\\prnport.vbs -a -r $port -h $s -o raw -n 9100 | Out-Null; "
            f"& $env:windir\\System32\\rundll32.exe 'printui.dll,PrintUIEntry' /if /b $q /r $port /m 'Microsoft IPP Class Driver'\"\r\n"
            "\r\n"
            "echo Done.\r\n"
            "pause\r\n"
        )
        return _text_response(content, f"{queue}.cmd")

    return "error", 404

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

用 systemd 把门户做成服务(长期运行)

新建 /etc/systemd/system/printer-portal.service:

[Unit]
Description=Printer Portal (Flask)
After=network.target

[Service]
WorkingDirectory=/opt/printer-portal
ExecStart=/usr/bin/python3 /opt/printer-portal/app.py
Restart=always
User=root

[Install]
WantedBy=multi-user.target

启动并开机自启:

sudo systemctl daemon-reload
sudo systemctl enable --now printer-portal
sudo systemctl status printer-portal

Nginx 反代成标准 80 端口

新建 Nginx 配置 /etc/nginx/sites-available/printer-portal:

server {
    listen 80;
    server_name printer.company.local;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

启用:

sudo ln -s /etc/nginx/sites-available/printer-portal /etc/nginx/sites-enabled/printer-portal
sudo nginx -t
sudo systemctl restart nginx

然后在 DNS(或内网 hosts)里把:

printer.company.local -> CUPS服务器IP

访问

实现多客户端一键安装

列表会根据Location分类,可自行在代码中调整

写在最后

在企业内部,很多问题其实并不复杂。

真正复杂的,是长期没有人愿意去解决它们。

打印机就是一个典型例子。

每个人电脑上装一遍驱动、换电脑再装一遍、换网段又要重新配置,IT 部门不断重复这些低价值工作,而员工也在不断消耗时间。

这些事情看起来很小,但当公司规模变大时,就会变成一种隐性的效率损耗。

而像 CUPS 这样的开源工具,其实早就提供了成熟的解决方案:

把复杂度放在服务器上,让用户端尽可能简单。

这也是企业信息化里一个很重要的原则:

复杂留在系统,简单留给用户。

当这些“看似微小”的问题逐渐被解决时,信息化的价值才会慢慢显现出来。

很多时候,企业的信息化并不是来自某一个宏大的系统,而是来自无数个这样的细节。

写在途中,持续记录。继续阅读 →