Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1144222
  • 博文数量: 53
  • 博客积分: 10025
  • 博客等级: 上将
  • 技术积分: 1640
  • 用 户 组: 普通用户
  • 注册时间: 2007-06-15 17:05
文章分类

全部博文(53)

文章存档

2011年(1)

2010年(3)

2009年(25)

2008年(24)

我的朋友

分类: 系统运维

2008-03-23 03:07:42

作者:dorainm
联系:


目录

1、在线打印
    1.1 前提
    1.2 设备文件
    1.3 重定向
    1.4 打印机权限
    1.5 替换重定向
2、客户端在线打印
    2.1 Web Service
    2.2 侦听端口
    2.3 客户连接
    2.4 处理表单
    2.5 打印文件
    2.6 打印共享
3、细节完善
    3.1 打印共享控制
    3.2 日志
    3.3 开机运行
    3.4 权限和安全
    3.5 进程守护
4、结束


1、在线打印

1.1 前提

    不清楚好多事情,比如 M$和 IE是怎么控制打印机的,见过交话费时候,B/S的收费系统支持在线打印!

    终于被自己遇到了,客户机打算用 Ubuntu,浏览 Web服务器,进行信息管理,但是客户端通过 Firefox获取到服务器端的列表信息后,可以在线打印,就是 Ubuntu边上有个打印机,打印出来这个网页的列表内容!

    在 Linux下接了一个 Epson LQ-630K,针式打印机,但是找不到合适的驱动!只找到 LQ850和 LQ500的,用850,用 Firefox的 Ctrl+P(打印)打印那页面,黑漆漆的而且文字的上半部分下半部分是错位的...用500,字淡得,能看到一个一个点阵,跟nokia 8250一样,拿着纸片距离眼睛1米左右,才能够阅读这些文字...

    所以,打印机只能在文本模式下工作了……
    嘻,dorainm最钟情的模式,而且在文本模式下,打印效果还算理想!比如

ls /home/dorainm > /dev/usb/lp0


    打印机就吱吱嘎嘎的,打印出 dorainm主目录下的文件列表(遥想当年,显示器未了,打印机就是计算机批处理结果的标准输出设备...)



1.2 设备文件

    UNIX,一切皆是文件

    往 Linux的打印机设备文件里面直接写入字符串,打印机就出来信息了(打印机设备文件?! Linuxer们应该都知道,在 Linux的世界里面,一切皆是文件,打印机是文件、显卡是文件、麦克风是文件、键盘是文件……往显卡这个文件写字符流,就是显示输出,当然要按照一定格式,从麦克风文件读字符流,就是捕捉麦克风的声音,读取键盘,就是读取用户的按键,往打印机文件写东西,理所当然,就是让打印机打印了!)

    这里的打印机文件是 /dev/usb/lp0,可以看出,是设备(dev)里的usb设备中的第一号打印机(lp0)!如果是并口的话,可能在 /dev/lp0……/dev/lpx 里面,具体情况具体找。



1.3 重定向

    重定向是 Linux Shell中,实现2个进程之间通讯的一种方式,之前可能听说过 rpc、管道pipe之类的。重定向可以把程序的输出,写入到 IO文件中(或者输出到 /dev/null中,直接屏蔽输出)也可以让 IO文件的内容,成为程序的输入,当年的一个闹剧就是这么样子的,

echo y > %tmp%/sayy
echo y >> %tmp%/sayy
……
echo y >> %tmp%/sayy
format c: /q < %tmp%/sayy


    嘻嘻,重定向的经典体现,硬盘就被活生生地、悄无声息地、不需要任何提示的情况下,格式化掉了;-P



1.4 打印机权限

    Linux的严谨的权限,对于 win32er来说是个灾难,初入 Linux大门就会被它绊一大跤,但是对于 Linuxer来说,读写执行 3个简单的权限,所属者、群体、其它三个组的接合,文件、所属文件夹的组合,变化出来 81种权限,无异不体现 Linux简约而不简单的性质,可以跟一切皆是文件的简介设计理念想娉美!
    详细可以查阅  类UNIX系统的文件系统权限初级篇

    对于打印机,先看看打印机文件

dorainm@coffee $ ls /dev/usb/lp0 -l
crw-rw---- 1 root lp 180,0 2008-03-22 22:44 lp0


    打印机设备 lp0,它属于root用户,属于lp群,所属者和群,对它都有读写权限,其它用户没有任何权限。现在要给当前用户添加打印机权限!
  
    当然,你可以修改所属者为当前用户,或者群,但是每次开机,设备被加载到 /dev/usb中,又会恢复回去。嘿嘿,既然群有读写权限,那么我们把当前用户加到组群里面

dorainm@coffee $ su -c "vi /etc/group"


    找到 lp群这条记录

lp:x:7:


    然后在后面添加用户

lp:x:7:dorainm


    那么,dorainm已经属于 lp群了,所以,dorainm也拥有rw权限,那么就可以操作打印机了!



1.5 替换重定向

    把客户提交的字符串,比如POST过来的,通过重定向,写入到打印机设备文件中,是可以实现在线打印了,但是 web提交会有一些特殊字符,比如\、<、>、"等,重定向时候,shell会对这些进行解释,让输出结果变得乱麻一团,而且用 `shell`执行shell的时候,会产生新进程,增加系统消耗,因此,程序就用标准的 IO输出函数,对设备文件进行写操作,
    比如 Perl写 CGI,可以用

open( LP, "> /dev/usb/lp0" );
printf LP "Hello, World\n";
close LP;


    如果 PHP的话,可以用

$lp = fopen( "/dev/usb/lp0", "w" )
fwrite( $lp, "Hello, World\n" );
fclose( $lp );


    到时候对 POST或者 GET提交过来的字符串进行过滤过滤,然后就输出到打印机了。



2、客户端在线打印

    总体解决方案大概就是这样子的,不知道在座看客们,有没有发现一处逻辑上的毛病!
    对,无论是 Perl的CGI,还是 PHP,运行时候,都是在服务器端,也就是说,大家在在线打印,点了半天没有反应,服务器上面的打印池,却排爆满了(如果运行 apache的 apache或者nobody用户也属于 lp群,那么打印机可以烤烧烤了)

    弄错了,要客户端执行!!
    不知道客户端的 javascript能不能对 IO进行操作,应该不能吧,开放这个权限,那么 Firefox也跟 IE一样脆弱了!那么怎么办,怎么让服务器上的网页里表单的内容,POST/GET到本地被运行重定向?在本地装个 web service?!

    嘻,太麻烦了,我们自己用 Perl写个超迷你的 Web Service吧!



2.1 Web Service

    Web Service可能是个庞大的项目,阅读 RFC中的 HTTP协议,什么 GET/POST/PUT各种请求,然后服务器怎么响应,还有HTTP头要描述这描述那,COOKIE、SESSION……唉!

    还好我们什么都不用管,迷你Web Service要做的事情,就是侦听一个端口,等待客户端浏览器提交请求,比如POST或者GET方式,获取到请求内容字符串,然后用 open函数打开文件句柄,写入到打印机设备文件中,完成我们的目的。

    我们在此不用 Perl的 HTTP::Deamon类,庞大,也累!
    当初我们决定要写 Perl的 Web Service,就是为了让事情变得简单,不然每台客户机装个 apache,不是更稳定更省事吗?就用 Socket吧,毛主席说,自动动手,丰衣足食!


2.2 侦听端口

    Web Service第一步,侦听端口!

sub create_server()
{
    socket( SERVER, PF_INET, SOCK_STREAM, $sys{'proto'} )
    or return "socket failed: $!";
    setsockopt( SERVER, SOL_SOCKET, SO_REUSEADDR, 1 )
    or return "setsockopt failed: $!";

    my $paddr = sockaddr_in( $sys{'port'}, INADDR_ANY );

    bind( SERVER, $paddr )
    or return "bind: $!";
    listen( SERVER, SOMAXCONN )
    or return "listen: $!";

    return 0;
}


    我们在主程序里面调用 create_server函数时,判断返回值,增强程序的健壮性

if( $retval=&create_server() ){
    printf $retval . "\n";
    exit $!;
} else {
    printf "server socket listen on $sys{'port'} successfullly\n";
}





2.3 客户连接

    Perl 开始在 $sys{'port'}端口侦听了,如果运行成功的话,然后要开始接收客户请求,下面是实现的关键代码:

sub accept_client(){
    while( $client_socket=accept( CLIENT, SERVER ) ) {
    ( $client_port, $client_ip ) = sockaddr_in( $client_socket );
    $client_ipnum = inet_ntoa( $client_ip );
    $client_host = gethostbyaddr( $client_ip, AF_INET );

    sysread( CLIENT, $client_stream, 4096 );

    printf stdout "Got a Connection From: $client_host [ $client_ipnum ] on Port $client_port\n";
    printf CLIENT "Hello, World\n";
    
    close CLIENT;
    }
}


    这里选用 sysread读取 socket字符流,dorainm尝试了好多方法,比如while( $cllient_stream .= )等,发现都会出现阻塞等情况,唉,暂时用这个吧,等秋后再细细研究!

    当然,Perl 的 Web Service要一直在侦听一直接收客户请求,所以主程序中要

while( 1 ) {
    &accept_client();
}




2.4 处理表单

    好, Web Service已经能侦听端口,可以跟客户端接收和发送字符流了,下面来分析下,浏览器发送的一个HTTP头的意思,这是POST过来一个str=Hello,World的例子

POST / HTTP/1.1
Host: localhost:port
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,
image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 19

pstr=Hello%2C+World


    我们可以看到,这个头里面,我们最关心的,就是 pstr=后面的字符串,$client_stream读取到整个头以后,用正则提取出pstr=后面的内容,然后进行一些字符串的替换过虑

sub form_post()
{
    my $value = $_[0];
    my $null;

    ($null,$value) = split /pstr=/, $value;

    chomp $value;
    $value =~ tr/+/ /;
    $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
    $value =~ s/\|//g;
    $value =~ s/>/>/g;
    $value =~ s/</</g;
#    $value =~ s/\r\n/<br>/g;
#    $value =~ s/\n/<br>/g;
    return $value;
}


    调用这个函数,就可以从HTTP头里面,提取到 Hello,World 字符串了!

    如果是GET方式提交的,应该会是这个样子

GET /?pstr=Hello,World HTTP/1.1
Host: localhost:port
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive


    看到第一行了没有,GET /?pstr=Hello,World,Web Service只要用相同方法去提取、过滤,就可以得到 pstr的值了。


2.5 打印文件

    提交完毕后,本地的 Web Service分析得到客户提交的需要打印的字符串,现在要写入打印机设备文件中,实现代码如下:

# &print_string( $str_stream )

sub print_string()
{
    open( LP, ">> $sys{'lp'}" ) or return "Can not open LP: $!\n";
    printf LP $_[0] . "\n";
    close LP;
    return 0;
}


    完成了,大体功能!
    把我们的 Web Service运行起来,然后在服务器端的网页上,如果需要打印表单的话,只需要 POST或者 GET pstr到 {'port'}上来,本地的打印机就会欢快的工作了


2.6 打印共享

    与民同乐

    每台计算机身边陪一个廉价的打印机,成本不计算,光维护就可以让人崩溃,所以现在好多企业,都共享一台质量高性能优越的打印机,供所有授权的客户们打印!

    我们的 Web Service也比较容易实现这个功能了,因为它本身就已经在网络中侦听了,如果客户端装有 iptables之类的软件包过滤防火墙,设置规则,让port可以接收外部请求,那么其它客户端机器,都可以利用运行着 Web Service的机器来打印表单



3、细节完善

    客户需要稳定地支持在线打印,我们的 Web Service当然也要很稳定地运行着,有些细节方面,我们可以完善!


3.1 打印共享控制

    现在 Web Service在侦听着端口,如果不想接收他人的提交,或者只允许某些人使用我的打印机。一可以拜托 iptables来作过滤,二我们可以在 accept_client 函数中,对 $client_ipnum函数进行判断,比如仅支持本地打印,那么判断 $client_ipnum是否 eq "127.0.0.1"。



3.2 日志

    像个软件一样运行着,我们当然要监控它的运行状况,哪天都谁来打印什么内容了,后来接收什么请求崩溃掉的 Web Service,这些都是要关心的我们可以给 Web Service添加日志功能,比如记录日志的字段有,时间日期、客户IP、客户端口、客户浏览器信息、提交内容。

    时间日期可以用 localtime来获取,比如

sub get_date()
{
    ……
    ($sec,$min,$hour,$mday,$month,$year,$yday,$isdst) = localtime(time());
    $year += 1900;
    $month += 1;

    return ( sprintf "%04d-%02d-%02d %02d:%02d:%02d", $year, $month, $mday, $hour, $min, $sec );
}


    客户IP、端口,在 accept_client()函数中已经实现了;

    客户浏览器信息,主要是 HTTP头里面的

User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12


    一行,我们用正则把它扣出来!
   
    最后提交内容,嘿嘿嘿,这个还没有做啊?那打印什么东西?!

    下面是记录日志的函数

#&report_log( $client_ipnum, $client_host, $client_port, $user_agent, $form_post );

sub report_log()
{
    my $nowdate = &get_date();

    open( LOG, ">> $sys{'log'}" ) or return "Can not write logs: $!\n";
    printf LOG $nowdate . " $_[0] $_[1] $_[2] $_[3] $_[4]\n";
    close LOG;
    return;
}





3.3 开机运行

    本地的打印 Web Service需要开机就自动运行,以免客户忘记双击运行出现无法打印之类的异常时间,影响客户对程序的易用性!

    开机运行方法很多,这里介绍,把程序链接到 /etc/rcX.d里面(X是当前用户运行的init级别,比如3是文本模式,5是X模式,可以在 /etc/inittab里面查看)链接文件一定要以S开头,后面跟个数字,然后是名字,比如 S31print(S表示要被运行的,K表示不运行的,后面数字表示开机运行的先后顺序)

    如果经常更换init启动的话,也可以在 /etc/rc.local里面添加一行

/print.pl-path/print.pl &


    这样子开机也会被运行起来



3.4 权限和安全

    这个是很必要关心的, Web Service以 root身份运行,是严谨的 Linux系统不太愿意的!

    我们可以设置一个伪帐户,比如

# useradd -u 97 -G lp -s /sbin/nologin(-s /bin/false) printer

    让 Web Service运行时,不需要特权的话,就用 setuid和 setgid用伪用户权限。当然,相应的,比如日志文件的所属者权限,都需要跟 printer配套起来。



3.5 进程守护

    嘿嘿,不怕一万,只怕万一呐,哪天真出现个意想不到的 bug,或者硬件原因,当掉了 Web Service,自己写一个守护进程,时刻查询进程列表中是否还有 Web Service,如果没有那进程了,重新激活。

    其实 Linux已经有好些公用的守护进程了,相信它们在自己领域已经做了这么多年了,稳定性应该可以相信,我们就不重复发明轮子了,拜托 inetd或者 xinetd吧!


4、结束

    貌似这片文章,除了 dorainm的愚昧之外(不知道 Linux环境怎么配置在线打印),体现了自己动手、丰衣足食的良好精神,还稍顺介绍了 Linux系统启动及权限的一些切身体会,挖咔咔~天怎么这么黑了? :-D

    嘿嘿嘿,抛砖引玉,不管哪方面的意见建议,都砸过来吧


阅读(3585) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~