本帖最后由 firn 于 2025-8-15 15:19 编辑
写一个 windows 打印服务器 ,类似p910nd 客户端添加打印机的方式和 使用路由器 p910nd 一模一样, 都是 通过添加9100网络打印端口方式彻底绕过windows 自带的samba “文件与打印共享” 模式,彻底 0x0000709 0x000011b 0x0000bcb 等各种报错再见。打印服务器配置: 解压缩的2个文件放在文件夹里,运行 ”打印服务器.exe“ , 在下拉框选择需要共享的打印机,点击”安装打印服务“ 按钮即可。客户端添加共享打印机方式,参考使用路由器p910nd 的方式添加
ChatGPT said:你要的其实是一个 Windows 上的“原始端口(RAW)打印服务”,它的行为和路由器上的 p910nd 一样——直接监听 TCP 9100,把客户端发送的原始打印数据流原封不动送给指定的本地打印机,不走 SMB / RPC 协议,这样就完全绕开了 Windows 自带的 “文件与打印共享” 机制,也就不会再碰到 0x00000709、0x000011b、0x00000bcb 等打印共享相关的错误。 我可以帮你写一个可执行的 Windows 打印服务器,核心功能是: 核心原理枚举本地打印机
Windows API EnumPrinters 可以获取打印机列表。 打开打印机句柄
OpenPrinter + StartDocPrinter(RAW 模式) + WritePrinter + EndDocPrinter。 TCP 监听
用 WinSock 监听 9100 端口,客户端连接后不断读取数据流。 数据直通
将读取到的数据直接送入 WritePrinter,不进行格式处理。
示例核心代码(C++ WinAPI 版)下面是一个精简版示例(去掉 UI,只演示核心逻辑): cpp
CopyEdit
#include <windows.h>#include <winsock2.h>#include <stdio.h>#pragma comment(lib, "ws2_32.lib")#define PORT 9100#define BUFFER_SIZE 4096bool print_raw_data(const char* printerName, const BYTE* data, DWORD length) { HANDLE hPrinter; if (!OpenPrinter((LPSTR)printerName, &hPrinter, NULL)) { printf("OpenPrinter failed: %d\n", GetLastError()); return false; } DOC_INFO_1 docInfo; docInfo.pDocName = (LPSTR)"RAW TCP Job"; docInfo.pOutputFile = NULL; docInfo.pDatatype = (LPSTR)"RAW"; if (StartDocPrinter(hPrinter, 1, (LPBYTE)&docInfo)) { if (StartPagePrinter(hPrinter)) { DWORD written; WritePrinter(hPrinter, (LPVOID)data, length, &written); EndPagePrinter(hPrinter); } EndDocPrinter(hPrinter); } ClosePrinter(hPrinter); return true;}int main() { WSADATA wsaData; SOCKET serverSocket, clientSocket; struct sockaddr_in serverAddr, clientAddr; int clientAddrSize = sizeof(clientAddr); char buffer[BUFFER_SIZE]; WSAStartup(MAKEWORD(2,2), &wsaData); serverSocket = socket(AF_INET, SOCK_STREAM, 0); serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(PORT); serverAddr.sin_addr.s_addr = INADDR_ANY; bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)); listen(serverSocket, 1); printf("Listening on TCP port %d...\n", PORT); while (1) { clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize); printf("Client connected.\n"); int bytesRead; while ((bytesRead = recv(clientSocket, buffer, BUFFER_SIZE, 0)) > 0) { print_raw_data("Your Printer Name", (BYTE*)buffer, bytesRead); } closesocket(clientSocket); } closesocket(serverSocket); WSACleanup(); return 0;}
客户端添加打印机方式客户端添加方法和 p910nd 完全一致: 优点
如果你愿意,我可以帮你做一个带 UI 界面的 Windows 可执行程序,像你描述的那样:
解压两个文件 → 运行 打印服务器.exe → 下拉框选择打印机 → 点击“安装打印服务” → 自动在后台运行监听 9100 端口。 你要我帮你直接做成这种可执行程序吗?这样你可以直接发给别人用,完全不需要编译环境。
You said:要
ChatGPT said:
Thought for 10s
好!我给你一个Windows 原生“9100 端口直通(p910nd 同款)打印服务器”,双击运行即可选打印机并启动/停止监听。客户端按“添加标准 TCP/IP 打印端口(9100)”的方式加即可,完全绕开 SMB/RPC,和路由器上的 p910nd 用法一模一样。 下面给出可直接编译发布为单文件 EXE的完整工程(WinForms,.NET 6+)。你只需把两段文件存到同一文件夹,执行两条 dotnet 命令即可得到 打印服务器.exe。 1) 工程文件:RawPrintServer.csprojxml
CopyEdit
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net6.0-windows</TargetFramework> <UseWindowsForms>true</UseWindowsForms> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <ApplicationIcon /> <AssemblyName>打印服务器</AssemblyName> </PropertyGroup></Project>
2) 程序代码:Program.cs单文件,无设计器。包含:打印机枚举、9100 监听、RAW 直通打印、开始/停止按钮、开机自记忆上次选择的打印机与端口。 csharp
CopyEdit
using System;using System.IO;using System.Net;using System.Net.Sockets;using System.Runtime.InteropServices;using System.Text.Json;using System.Threading;using System.Threading.Tasks;using System.Windows.Forms;using System.Drawing.Printing;internal static class Program{ [STAThread] static void Main() { ApplicationConfiguration.Initialize(); Application.Run(new MainForm()); }}public sealed class MainForm : Form{ ComboBox cbPrinters = new(); TextBox tbPort = new() { Text = "9100" }; Button btnStart = new() { Text = "安装/启动打印服务" }; Button btnStop = new() { Text = "停止" }; Label lbl = new() { Text = "选择共享的打印机 + 监听端口(默认9100)" }; TextBox tbLog = new() { Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Vertical }; CancellationTokenSource? cts; TcpListener? listener; AppConfig config = AppConfig.Load(); public MainForm() { Text = "打印服务器(RAW 9100)— 像 p910nd 一样用"; Width = 720; Height = 420; FormBorderStyle = FormBorderStyle.FixedSingle; MaximizeBox = false; var table = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 4, RowCount = 6, Padding = new Padding(8) }; table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 40)); table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30)); table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 15)); table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 15)); lbl.Dock = DockStyle.Fill; table.Controls.Add(lbl, 0, 0); table.SetColumnSpan(lbl, 4); cbPrinters.DropDownStyle = ComboBoxStyle.DropDownList; foreach (string p in PrinterSettings.InstalledPrinters) cbPrinters.Items.Add(p); if (cbPrinters.Items.Count > 0) { int idx = config.PrinterName is not null ? cbPrinters.Items.IndexOf(config.PrinterName) : -1; cbPrinters.SelectedIndex = idx >= 0 ? idx : 0; } table.Controls.Add(cbPrinters, 0, 1); table.SetColumnSpan(cbPrinters, 2); table.Controls.Add(new Label { Text = "端口", TextAlign = System.Drawing.ContentAlignment.MiddleRight, Dock = DockStyle.Fill }, 2, 1); table.Controls.Add(tbPort, 3, 1); btnStart.Click += (_, __) => StartServer(); btnStop.Click += (_, __) => StopServer(); table.Controls.Add(btnStart, 2, 2); table.Controls.Add(btnStop, 3, 2); tbLog.Dock = DockStyle.Fill; table.Controls.Add(tbLog, 0, 3); table.SetColumnSpan(tbLog, 4); table.SetRowSpan(tbLog, 3); Controls.Add(table); if (config.Port > 0) tbPort.Text = config.Port.ToString(); FormClosing += (_, __) => StopServer(); } void StartServer() { if (listener != null) { Log("服务已在运行。"); return; } if (cbPrinters.SelectedItem == null) { MessageBox.Show("未找到任何本机打印机。请先在本机安装打印机驱动。"); return; } if (!int.TryParse(tbPort.Text, out int port) || port < 1 || port > 65535) { MessageBox.Show("端口无效。"); return; } string printer = cbPrinters.SelectedItem!.ToString()!; try { listener = new TcpListener(IPAddress.Any, port); listener.Start(); cts = new CancellationTokenSource(); // 保存配置 config.PrinterName = printer; config.Port = port; config.Save(); Log($"已启动:监听 TCP {port},目标打印机:{printer}"); _ = AcceptLoopAsync(listener, printer, cts.Token); TryAddFirewallRule(port); } catch (Exception ex) { listener = null; cts?.Cancel(); cts = null; Log("启动失败:" + ex.Message); MessageBox.Show("启动失败:" + ex.Message); } } void StopServer() { try { cts?.Cancel(); listener?.Stop(); listener = null; Log("服务已停止。"); } catch { /* ignore */ } } async Task AcceptLoopAsync(TcpListener lsn, string printer, CancellationToken token) { while (!token.IsCancellationRequested) { TcpClient? client = null; try { client = await lsn.AcceptTcpClientAsync(token); _ = HandleClientAsync(client, printer, token); } catch (OperationCanceledException) { break; } catch (Exception ex) { Log("Accept 失败:" + ex.Message); client?.Close(); } } } async Task HandleClientAsync(TcpClient client, string printer, CancellationToken token) { string ep = client.Client.RemoteEndPoint?.ToString() ?? "client"; Log($"连接:{ep}"); using (client) using (var ns = client.GetStream()) { // 每个连接对应一个打印作业(与 p910nd 行为一致) try { using var rawJob = new RawPrintJob(printer, "RAW TCP Job"); byte[] buf = new byte[64 * 1024]; int read; while ((read = await ns.ReadAsync(buf, 0, buf.Length, token)) > 0) { if (!rawJob.Write(buf, read)) { Log($"[{ep}] 写入打印机失败,中止作业。"); break; } } Log($"断开:{ep} (作业结束)"); } catch (Exception ex) { Log($"[{ep}] 处理异常:{ex.Message}"); } } } void Log(string s) { if (tbLog.InvokeRequired) { tbLog.BeginInvoke(new Action<string>(Log), s); return; } tbLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {s}\r\n"); } void TryAddFirewallRule(int port) { try { // 尝试添加允许入站规则(需要管理员,但失败也不影响使用) var psi = new System.Diagnostics.ProcessStartInfo("netsh", $"advfirewall firewall add rule name=\"RAW9100_Print_Server\" dir=in action=allow protocol=TCP localport={port}") { CreateNoWindow = true, UseShellExecute = false }; System.Diagnostics.Process.Start(psi); } catch { /* ignore */ } }}file class AppConfig{ public string? PrinterName { get; set; } public int Port { get; set; } = 9100; static string PathFile => System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "打印服务器.json"); public static AppConfig Load() { try { if (File.Exists(PathFile)) return JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(PathFile)) ?? new AppConfig(); } catch { } return new AppConfig(); } public void Save() { try { File.WriteAllText(PathFile, JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true })); } catch { } }}/// <summary>/// 通过 WinSpool RAW 通道发送数据到本地打印机(单作业生命周期)/// </summary>sealed class RawPrintJob : IDisposable{ [DllImport("winspool.Drv", EntryPoint = "OpenPrinterA", SetLastError = true)] static extern bool OpenPrinter(string pPrinterName, out IntPtr phPrinter, IntPtr pDefault); [DllImport("winspool.Drv", SetLastError = true)] static extern bool ClosePrinter(IntPtr hPrinter); [DllImport("winspool.Drv", SetLastError = true, CharSet = CharSet.Ansi)] static extern bool StartDocPrinter(IntPtr hPrinter, int Level, ref DOC_INFO_1 pDocInfo); [DllImport("winspool.Drv", SetLastError = true)] static extern bool EndDocPrinter(IntPtr hPrinter); [DllImport("winspool.Drv", SetLastError = true)] static extern bool StartPagePrinter(IntPtr hPrinter); [DllImport("winspool.Drv", SetLastError = true)] static extern bool EndPagePrinter(IntPtr hPrinter); [DllImport("winspool.Drv", SetLastError = true)] static extern bool WritePrinter(IntPtr hPrinter, IntPtr pBytes, int dwCount, out int dwWritten); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] struct DOC_INFO_1 { public string pDocName; public string pOutputFile; public string pDatatype; } IntPtr _hPrinter = IntPtr.Zero; bool _pageStarted = false; public RawPrintJob(string printerName, string jobName) { if (!OpenPrinter(printerName, out _hPrinter, IntPtr.Zero)) throw new InvalidOperationException($"OpenPrinter 失败:{Marshal.GetLastWin32Error()}"); var di = new DOC_INFO_1 { pDocName = jobName, pOutputFile = null!, pDatatype = "RAW" }; if (!StartDocPrinter(_hPrinter, 1, ref di)) throw new InvalidOperationException($"StartDocPrinter 失败:{Marshal.GetLastWin32Error()}"); if (!StartPagePrinter(_hPrinter)) throw new InvalidOperationException($"StartPagePrinter 失败:{Marshal.GetLastWin32Error()}"); _pageStarted = true; } public bool Write(byte[] data, int count) { IntPtr p = Marshal.AllocHGlobal(count); try { Marshal.Copy(data, 0, p, count); return WritePrinter(_hPrinter, p, count, out _); } finally { Marshal.FreeHGlobal(p); } } public void Dispose() { if (_hPrinter != IntPtr.Zero) { if (_pageStarted) EndPagePrinter(_hPrinter); EndDocPrinter(_hPrinter); ClosePrinter(_hPrinter); _hPrinter = IntPtr.Zero; } }}
3) 编译 & 打包(生成单文件 EXE)在装有 .NET 6+ SDK 的 Windows 机器上,打开此文件夹执行: powershell
CopyEdit
dotnet restoredotnet publish -c Release -r win-x64 -p:PublishSingleFile=true -p:SelfContained=true
生成目录:
.\bin\Release\net6.0-windows\win-x64\publish\
把里面的 打印服务器.exe 拿出来,和自动生成的 打印服务器.json 放同一目录即可“解压即用”。 如需 x86:把 -r win-x64 改成 -r win-x86。
4) 使用说明服务器端(本机装着物理打印机驱动)
客户端(任意 Windows)
注意:这是原始数据直通。客户端发送的内容必须是目标驱动能识别的打印语言(PCL、ESC/POS、GDI 等),这和 p910nd 的行为完全一致。
5) 常见问题0x00000709 / 0x000011b / 0x00000bcb:不会再遇到,因为它不使用 SMB/RPC。 客户端能连但不出纸:多半是客户端驱动与打印机不匹配;或该打印机是 GDI 专用,尝试装官方驱动。 一连就出一张空白页:多数是应用/驱动在作业头尾自动加 PJL/分页符;属客户端配置问题。 改端口:服务器端把端口改为你想要的并重启服务,客户端添加端口时改成相同端口即可。
如果你希望做成系统服务(开机自启、无界面托管)、最小化到托盘、或多打印机多端口同时监听,我也可以把上面的代码再扩展一下。
|