Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Open Source

Remote Network Printing


JAN95: Remote Network Printing

Remote Network Printing

Using the Windows Sockets API to create a UNIX-like daemon

Zongnan H. Lu

Henry is a systems analyst for the Mental Health Research Institute at the University of Michigan. He can be contacted at [email protected].


In today's world of TCP/IP-based heterogeneous networks, it is increasingly common to find UNIX workstations and PCs linked and working together. Just as common in many of these network models are multiple printers and plotters residing on PC-based Novell NetWare or Banyan VINES networks, while printers directly attached to UNIX workstations are few and far between. This is particularly ironic since many distributed applications that use client/server databases reside on the UNIX side of the net because of its better performance, greater storage capacity, and multiuser/multitasking features. Consequently, various daily, monthly, and yearly reports created on the server typically wait for system administrators to print them out. On the other hand, PC networks which let you print files through shared printer queues are rarely fully utilized, particularly in the late night or early morning hours. Clearly, a solution to the printing bottleneck is to move the files from the UNIX to the PC side of the network to make the most efficient use of printing capabilities.

One way of doing this is to use FTP to download files to a local PC, then send them to a printer on the PC's network. Applications that implement standard UNIX network utilities such as lpd (line-printer daemon) make it possible for all files on UNIX 4.3BSD-based workstations to be automatically sent to printers on a PC network system at any time. In this article, I'll present an lpd server implemented for the PC that does just this. This PC lpd runs under Windows and is based on the Windows Sockets API.

Using the Windows Sockets API

Because of the differences between UNIX 4.3BSD and Windows internals and file structures, there are two basic ways to build a PC-based lpd server. In the first approach, you write two programs--one running on a UNIX workstation as a client that collects and sends files to a PC--and the other running as a server, sitting on the PC, receiving files from UNIX, and sending them to a shared printer. In this scheme, the client program has to be installed on all UNIX workstations which need remote printers, and somewhat duplicates existing UNIX-standard network programs.

A second approach, which I implement here, is to use the standard lpd program as a client program on UNIX workstations, then write a server program for the PC. This approach mirrors a common usage of lpd whereby the program runs as both a printer server and client to receive files from a user's print command (lp or lpr). The program then sends files to an attached or remote printer connected to another UNIX workstation. The advantage of this approach is its portability and simplicity. The program I wrote that does this is called "winlpd" and is written in Microsoft Visual C/C++ using the Windows Sockets API, WINSOCK.DLL.

The winlpd program handles four messages from the UNIX lpd: receiving a job; listing the queue, short form; listing the queue, long form; and canceling jobs from the queue. Since all files received by winlpd will be sent to a printer and then immediately deleted, winlpd does nothing but acknowledge when it gets the listing and canceling messages. On the UNIX side, the remote host must be set in the file /etc/hosts (or /etc/hosts.equiv or /etc/hosts.lpd; refer to UNIX Network Programming, by W. Richard Stevens, Prentice Hall, 1990) and in the file /etc/printcap. This file defines a mapping of symbolic printer names into physical device names, along with a complete specification of the printer's capabilities. If a remote PC's printer name is different from the UNIX printer name, the remote PC's printer name must be specified in the file /etc/printcap. In the local PC PRINTCAP file, I only specify symbolic printer names and their spooler directories. Listing One is winlpd.cpp, and Listing Two is mainfrm.cpp. Other required files, including winlpvw.cpp, the include files, and other Visual C++ standard files are provided electronically; see "Availability," page 3.

To call functions in the WINSOCK.DLL, winlpd first loads the DLL and all necessary functions by calling openLib() in the constructor of class CWinlpdApp. If there is a failure, the program terminates. The library is closed in the destructor of class CWinlpdApp when the program is terminated; see Listing One.

The winlpd menu selection is implemented as one that can be toggled Off/On. A global flag, stop_lpd, is set by Close in the system menu, and Start lpd and End lpd in the lpd menu. As soon as Start lpd is selected, the server is active and accepts connections by calling functions socket(), bind(), listen(), and select(). When the function stop_lpd is set to STOP_LOCK, the menu items Start lpd and Close are grayed out. To stop the server, the menu item End lpd must be selected, which sets stop_lpd to STOP_UNLOCK. The program sets the select() function to five seconds to wait for any incoming connections. After five seconds it checks the value of flag stop_lpd. The server is stopped if stop_lpd is not set to STOP_LOCK. Calling select() again or accepting a connection is dependent upon the value returned from select(). This process guarantees a complete job for the server and closes all sockets and the DLL; see Listing Two.

When a connection is established, the doit() function is called (see the file winlpvw.cpp, available electronically). If remote files are ready to be received, the following occurs:

  1. The Winlpd selection finds a printer as required by the remote host in the file PRINTCAP. It does this by calling pgetent() in recvjob() and then readjob().
  2. The chksize() function is called to ensure that disk space is available to hold the incoming file. The readfile() function is called to download the file.
  3. The mapfilename() function (in readfile()) is invoked to get a PC's filename because the remote filename is often longer than eight characters.
  4. The printjob() function (in recvjob()) is called when all files have been received for the current job.
  5. Files specified in printit() are sent to print_lp() if a line printer is required, or to print_lsr() if a laser printer is required. Before printing, the NetWare Capture_ command is called in printjob() to capture the specified printer queue.
  6. If there is a failure on either side during the transaction, all of the job's unprinted files are deleted by rcleanup() (in readjob()), while the remaining files continue to be received.
The moveprintf() and strfwrite() functions are called by print_lsr() for laser printers. If fancy characters, different sizes, or special-text formats are required, these functions will need to be modified accordingly.

To send files from a UNIX workstation, use the commands lpr or lp; see Example 1.

Putting It All Together: An Example

To illustrate how to use winlpd, suppose that a database server is running on a UNIX workstation which has the host name "dbhost" and an IP address of 141.211.222.222. Further suppose that a report-creation process scheduled in a crontab produces two different report files--f1.rpt, sent to a line printer called "netlp," and f2.rpt, to a laser printer called "netlsr." Both printers are connected to a NetWare network with LAN WorkPlace for DOS installed. A PC on the network with IP address 141.211.111.11 and host name "henry" has the winlpd program running. With all this in place, there are three files you need to set properly: /etc/printcap, /etc/hosts, and /etc/services. On the UNIX side, two printer names are added to the file /etc/printcap; see Example 2, where lp1 and lp2 are symbolic printer names on the UNIX; netlp and netlsr are printer names on the PC server side, rm declares a remote host name or IP address, and rp is assigned a remote printer name (netlp or netlsr). For the network communication, the PC's IP address should be added to the file /etc/hosts as 141.211.111.11 pclpsrv, where pclpsrv is the PC's host name (we assume that the UNIX host name is already in /etc/hosts); an entry "printer 515/tcp spooler" must appear in the file /etc/services.

On the NetWare side, both the UNIX and PC host names (dbhost and pclpsrv, respectively) and their IP addresses, 141.211.222.22 and 141.211.111.11, should be set in the file HOSTS, which is located in the network TCP directory, ..\TCP\HOSTS. Additionally, entry "printer 515/tcp spooler" must be in the ..\TCP\SERVICES file. For printers on the Novell network, the ..\TCP\PRINTCAP file must be created; see Example 3. Note that the backslash (\) is treated as a symbol of continuation in a regular UNIX printcap file. With PCs, however, a backslash is a root directory or a separator of subdirectories. Winlpd eliminates this ambiguity by setting a syntax that each line has only one item; a backslash is viewed as a continuation character if the backslash is at the end of the line; otherwise it is viewed as a normal character.

When all settings are complete, winlpd is executed on the PC. As soon as a report file is ready on the UNIX workstation, the command lpr --Plp1 f1.rpt or lp --dlp1 f1.rpt is invoked to send the f1.rpt file to the remote printer netlp. Similarly, lpr --Plp2 f2.rpt or lp --dlp2 f2.rpt send as f2.rpt to the printer netlsr on the PC Novell network. If your crontab file looks like Example 4(a), the reportwiter.sch file should include the commands in Example 4(b).

Conclusion

Winlpd can be used not only for remote printing, but also for other remote activities such as sending faxes, transferring files, and the like. All you have to do is put files into appropriate directories shared with other programs for different tasks. For performance and flexibility, winlpd can be implemented as a server that only receives files. If you decide to use it this way, you'll probably need to rewrite displayq(), rmjob(), while disregarding printjob(), printit(), print_lp(), print_lsr(), moveprintf(), and strfwrite().

Example 1: Sending files using the UNIX lpr and lp commands.

lp -dlp1 filename1, filename2 ...
lpr -Plp1 filename1, filename2 ...

Example 2: Setting printer names on the UNIX side of the net.

(a)
lp1|network line printer:\
  :rm=141.211.111.11:rp=netlp:
lp2|network laser printer:\
  :rm=141.211.111.11:rp=netlsr:
(b)
lp1|network line printer:\
  :rm=pclpsrv:rp=netlp:
lp2|network laser printer:\
  :rm=pclpsrv:rp=netlsr:

Example 3: Setting printers on the PC side of the net.

netlp:\
  :sd=C:\TMP\PRINTERS\NETLP:
netlsr:\
  :sd=C:\TMP\PRINTERS\NETLSR:

Example 4: (a) Sample crontab-file contents; (b) commands for sample crontab file.

(a)
0 4 * * 1,2,3,4,5 /usr/henry
/reportwriter.sch
(b)
# run report creator program
/usr/henry/report_writer
# send file to PC server
lpr -Plp1 /usr/henry/f1.rpt
lpr -Plp2 /usr/henry/f2.rpt

Listing One

// winlpd.cpp : Defines the class behaviors for the application.

#include "stdafx.h"
#include "winlpd.h"

#include "mainfrm.h"
#include "winlpdoc.h"
#include "winlpvw.h"

#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif

// CWinlpdApp
BEGIN_MESSAGE_MAP(CWinlpdApp, CWinApp)
   //{{AFX_MSG_MAP(CWinlpdApp)
   ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
      // NOTE - the ClassWizard will add and remove mapping macros here.
      //    DO NOT EDIT what you see in these blocks of generated code !
   //}}AFX_MSG_MAP
   // Standard file based document commands
   ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
   ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
END_MESSAGE_MAP()

// CWinlpdApp construction
HINSTANCE m_hLibrary;
SOCKET (FAR PASCAL *lpfn_accept)(SOCKET, LPSOCKADDR, LPINT);
int (FAR PASCAL *lpfn_bind)(SOCKET, LPCSOCKADDR, int);
int (FAR PASCAL *lpfn_closesocket)(SOCKET);
int (FAR PASCAL *lpfn_err)(void);

LPHOSTENT (FAR PASCAL *lpfn_gethostbyaddr)(LPCSTR, int, int);
LPSERVENT (FAR PASCAL *lpfn_getservbyname)(LPCSTR, LPCSTR);

int (FAR PASCAL *lpfn_listen)(SOCKET, int);
int (FAR PASCAL *lpfn_recv)(SOCKET, LPCSTR, int, int);
int (FAR PASCAL *lpfn_select)(int, LPFD_SET, LPFD_SET, LPFD_SET, LPTIMEVAL);
int (FAR PASCAL *lpfn_send)(SOCKET, LPCSTR, int, int);
int (FAR PASCAL *lpfn_shutdown)(SOCKET, int);
SOCKET (FAR PASCAL *lpfn_socket)(int, int, int);
int (FAR PASCAL *lpfn_WSACleanup)(void);
int (FAR PASCAL *lpfn_WSACancelBlockingCall)(void);
int (FAR PASCAL *lpfn_WSAStartup)(WORD, LPWSADATA);
SOCKET finet;
CWinlpdApp::CWinlpdApp()
{
   // TODO: add construction code here,
   // Place all significant initialization in InitInstance
   finet = INVALID_SOCKET;
   if (!openLib())
      exit(0);
}
CWinlpdApp::~CWinlpdApp()
{
   // TODO: add destruction code here,
   closeLib();
}
// The one and only CWinlpdApp object
CWinlpdApp NEAR theApp;
// CWinlpdApp initialization
BOOL CWinlpdApp::InitInstance()
{
   // Standard initialization
   SetDialogBkColor();        // set dialog background color to gray
   LoadStdProfileSettings();  // Load standard INI file options
   // Register the application's document templates.  Document templates
   //  serve as the connection between documents, frame windows and views.
   AddDocTemplate(new CSingleDocTemplate(IDR_MAINFRAME,
         RUNTIME_CLASS(CWinlpdDoc),
         RUNTIME_CLASS(CMainFrame),     // main SDI frame window
         RUNTIME_CLASS(CWinlpdView)));
   // create a new (empty) document
   OnFileNew();
   return TRUE;
}
// CAboutDlg dialog used for App About
class CAboutDlg : public CDialog
{
public:
   CAboutDlg();
// Dialog Data
   //{{AFX_DATA(CAboutDlg)
   enum { IDD = IDD_ABOUTBOX };
   //}}AFX_DATA
// Implementation
protected:
   virtual void DoDataExchange(CDataExchange* pDX);   // DDX/DDV support
   //{{AFX_MSG(CAboutDlg)
      // No message handlers
   //}}AFX_MSG
   DECLARE_MESSAGE_MAP()
};
CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD)
{
   //{{AFX_DATA_INIT(CAboutDlg)
   //}}AFX_DATA_INIT
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
   CDialog::DoDataExchange(pDX);
   //{{AFX_DATA_MAP(CAboutDlg)
   //}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)
   //{{AFX_MSG_MAP(CAboutDlg)
      // No message handlers
   //}}AFX_MSG_MAP
END_MESSAGE_MAP()
// App command to run the dialog
void CWinlpdApp::OnAppAbout()
{
   CAboutDlg aboutDlg;
   aboutDlg.DoModal();
}
// CWinlpdApp commands
#include <io.h>
/* closeLib() - closes sockets and DLL library */
void CWinlpdApp::closeLib()
{  
   int ret;
   if (finet != INVALID_SOCKET) {
      ret=(*lpfn_shutdown)(finet, 2);
      if (ret == SOCKET_ERROR) {
         ret=(*lpfn_err)();
         sprintf(m_err,"shutdown failed, ret=%d", ret);
         AfxMessageBox(m_err);
      }
   }
   ret=(*lpfn_WSACleanup)();
   if (ret == SOCKET_ERROR) {
      ret=(*lpfn_err)();
      sprintf(m_err,"WSACleanup failed, ret=%d", ret);
      AfxMessageBox(m_err);
   }
   FreeLibrary(m_hLibrary);
}
/* openLib() - opens DLL library and socket functions */
int  CWinlpdApp::openLib()
{
   int     ret;
   WORD    wvr;
   WSADATA wsad;

   if (_access("WINSOCK.DLL",0)) {
      AfxMessageBox("Need WINSOCK.DLL.");
      return 0;
   }
   /*               LOADING WINSOCK.DLL and FUNCTIONS              */
   if ((m_hLibrary = LoadLibrary("WINSOCK.DLL")) <= HINSTANCE_ERROR)
   {
      AfxMessageBox("Can not load lib WINSOCK.DLL");
      return 0;
   }
   lpfn_WSACleanup=(int (FAR PASCAL*)(void))
      GetProcAddress(m_hLibrary, "WSACleanup");
   if (lpfn_WSACleanup == NULL) {
      AfxMessageBox("GetProcAddress-WSACleanup failed");
      return 0;
   }
   wvr = (WORD)MAKEWORD(1,1);
   lpfn_WSAStartup=(int (FAR PASCAL*)(WORD, LPWSADATA))
      GetProcAddress(m_hLibrary, "WSAStartup");
   if (lpfn_WSAStartup == NULL) {
      AfxMessageBox("GetProcAddress-WSAStartup failed");
      return 0;
   }
   lpfn_err=(int (FAR PASCAL*)(void))
      GetProcAddress(m_hLibrary, "WSAGetLastError");
   if (lpfn_err == NULL) {
      AfxMessageBox("GetProcAddress-err failed");
      return 0 ;
   }
   lpfn_getservbyname=
      (struct servent FAR* (FAR PASCAL*)(LPCSTR,LPCSTR))
      GetProcAddress(m_hLibrary, "getservbyname");
   if (lpfn_getservbyname == NULL) {
      AfxMessageBox("GetProcAddress-getservbyname failed");
      return 0;
   }
   lpfn_gethostbyaddr=
      (LPHOSTENT (FAR PASCAL*)(LPCSTR, int, int))
      GetProcAddress(m_hLibrary, "gethostbyaddr");
   if (lpfn_gethostbyaddr == NULL) {
      AfxMessageBox("GetProcAddress-gethostbyaddr failed");
      return 0;
   }
   lpfn_socket=(SOCKET (FAR PASCAL*)(int, int, int))
      GetProcAddress(m_hLibrary, "socket");
   if (lpfn_socket == NULL) {
      AfxMessageBox("GetProcAddress-socket failed");
      return 0;
   }
   lpfn_bind=(int (FAR PASCAL*)(SOCKET, LPCSOCKADDR, int))
      GetProcAddress(m_hLibrary, "bind");
   if (lpfn_bind == NULL) {
      AfxMessageBox("GetProcAddress-bind failed");
      return 0;
   }
   lpfn_select=
      (int (FAR PASCAL*)(int, LPFD_SET, LPFD_SET, LPFD_SET, LPTIMEVAL))
      GetProcAddress(m_hLibrary, "select");
   if (lpfn_select == NULL) {
      AfxMessageBox("GetProcAddress-select failed");
      return 0;
   }
   lpfn_listen=(int (FAR PASCAL*)(SOCKET, int))
      GetProcAddress(m_hLibrary, "listen");
   if (lpfn_bind == NULL) {
      AfxMessageBox("GetProcAddress-listen failed");
      return 0;
   }
   lpfn_accept=
      (SOCKET (FAR PASCAL*)(SOCKET, LPSOCKADDR, LPINT))
      GetProcAddress(m_hLibrary, "accept");
   if (lpfn_accept == NULL) {
      AfxMessageBox("GetProcAddress-accept failed");
      return 0;
   }
   lpfn_closesocket=(int (FAR PASCAL*)(SOCKET))
      GetProcAddress(m_hLibrary, "closesocket");
   if (lpfn_closesocket == NULL) {
      AfxMessageBox("GetProcAddress-closesocket failed");
      return 0;
   }
   lpfn_shutdown=(int (FAR PASCAL*)(SOCKET, int))
      GetProcAddress(m_hLibrary, "shutdown");
   if (lpfn_shutdown == NULL) {
      AfxMessageBox("GetProcAddress-shutdown failed");
      return 0;
   }
   lpfn_send=(int (FAR PASCAL*)(SOCKET, LPCSTR, int, int))
      GetProcAddress(m_hLibrary, "send");
   if (lpfn_send == NULL) {
      AfxMessageBox("GetProcAddress-send failed");
      return 0;
   }
   lpfn_recv=(int (FAR PASCAL*)(SOCKET, LPCSTR, int, int))
      GetProcAddress(m_hLibrary, "recv");
   if (lpfn_recv == NULL) {
      AfxMessageBox("GetProcAddress-recv failed");
      return 0;
   }
   ret=(*lpfn_WSAStartup)(wvr, &wsad);
   if (ret != 0) {
      sprintf(m_err,"WSAStartup failed, ret=%d", ret);
      AfxMessageBox(m_err);
      return 0;
   }
   return 1;
}


Listing Two


// mainfrm.cpp : implementation of the CMainFrame class

#include "stdafx.h"
#include "winlpd.h"
#include "mainfrm.h"
#ifdef _DEBUG
#undef THIS_FILE
static char BASED_CODE THIS_FILE[] = __FILE__;
#endif
// CMainFrame
IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
   //{{AFX_MSG_MAP(CMainFrame)
   ON_WM_INITMENUPOPUP()
   //}}AFX_MSG_MAP
END_MESSAGE_MAP()
// CMainFrame construction/destruction
int  stop_lpd;
CMainFrame::CMainFrame()
{
   // TODO: add member initialization code here
   stop_lpd = STOP_OK;
}
CMainFrame::~CMainFrame()
{
}
// CMainFrame message handlers
void CMainFrame::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
{
   CFrameWnd::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu);
   // TODO: Add your message handler code here
   if (bSysMenu == TRUE) {
      if (stop_lpd != STOP_OK)
         pPopupMenu->EnableMenuItem(6, MF_BYPOSITION | MF_GRAYED);
      else
         pPopupMenu->EnableMenuItem(6, MF_BYPOSITION | MF_ENABLED);
   }
}



Copyright © 1995, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.