管道操作

概述

Windows 引入了多进程和多线程机制。同时也提供了多个进程之间的通信手段,包括剪贴板、DDE、OLE、管道等,和其他通信手段相比,管道有它自己的限制和特点,管道实际上是一段共享内存区,进程把共享消息放在那里。并通过一些 API 提供信息交换。
管道是两个头的东西,每个头各连接一个进程或者同一个进程的不同代码,按照管道的类别分有两种管道,匿名的和命名的;按照管道的传输方向分也可以分成两种,单向的双向的。根据管道的特点,命名管道通常用在网络环境下不同计算机上运行的进程之间的通信(当然也可以用在同一台机的不同进程中)它可以是单向或双向的;而匿名管道只能用在同一台计算机中,它只能是单向的。匿名管道其实是通过用给了一个指定名字的有名管道来实现的。
使用管道的好处在于:读写它使用的是对文件操作的 api,结果操作管道就和操作文件一样。即使你在不同的计算机之间用命名管道来通信,你也不必了解和自己去实现网络间通信的具体细节。

我们简单的介绍一下命名管道的使用。

命名管道是由服务器端的进程建立的,管道的命名必须遵循特定的命名方法,就是 “\.\pipe\管道名”,当作为客户端的进程要使用时,使用"\计算机名\pipe\管道名" 来打开使用,具体步骤如下:

服务端通过函数 CreateNamedPipe 创建一个命名管道的实例并返回用于今后操作的句柄,或为已存在的管道创建新的实例。
服务端侦听来自客户端的连接请求,该功能通过 ConnectNamedPipe 函数实现。
客户端通过函数 WaitNamedPipe 来等待管道的出现,如果在超时值变为零以前,有一个管道可以使用,则 WaitNamedPipe 将返回 True,并通过调用 CreateFile 或 CallNamedPipe 来呼叫对服务端的连接。
此时服务端将接受客户端的连接请求,成功建立连接,服务端 ConnectNamedPipe 返回 True
建立连接之后,客户端与服务器端即可通过 ReadFile 和 WriteFile,利用得到的管道文件句柄,彼此间进行信息交换。
当客户端与服务端的通信结束,客户端调用 CloseFile,服务端接着调用 DisconnectNamedPipe。最后调用函数CloseHandle来关闭该管道。
由于命名管道使用时作为客户端的程序必须知道管道的名称,所以更多的用在同一“作者”编写的服务器/工作站程序中,你不可能随便找出一个程序来要求它和你写的程序来通过命名管道通信。而匿名管道的使用则完全不同,它允许你和完全不相干的进程通信,条件是这个进程通过控制台“console”来输入输出,典型的例子是老的 Dos 应用程序,它们在运行时 Windows 为它们开了个 Dos 窗口,它们的输入输出就是 console 方式的。还有一些标准的 Win32 程序也使用控制台输入输出,如果在 Win32 编程中不想使用图形界面,你照样可以使用 AllocConsole 得到一个控制台,然后通过 GetStdHandle 得到输入或输出句柄,再通过 WriteConsole 或 WriteFile 把结果输出到控制台(通常是一个象 Dos 窗口)的屏幕上。虽然这些程序看起来象 Dos 程序,但它们是不折不扣的 Win32 程序,如果你在纯 Dos 下使用,就会显示“The program must run under Windows!”。

一个控制台有三个句柄:标准输入、标准输出和和标准错误句柄,标准输入、标准输出句柄是可以重新定向的,你可以用匿名管道来代替它,这样一来,你可以在管道的另一端用别的进程来接收或输入,而控制台一方并没有感到什么不同,就象 Dos 下的 > 或者 < 可以重新定向输出或输入一样。通常控制台程序的输入输出如下:

(控制台进程output) write ----> 标准输出设备(一般是屏幕)
(控制台进程input) read <---- 标准输入设备(一般是键盘)

而用管道代替后:

(作为子进程的控制台进程output) write ----> 管道1 ----> read (父进程)
(作为子进程的控制台进程input) read <----> 管道2 <---- write (父进程)

使用匿名管道的步骤如下:

使用 CreatePipe 建立两个管道,得到管道句柄,一个用来输入,一个用来输出
准备执行控制台子进程,首先使用 GetStartupInfo 得到 StartupInfo
使用第一个管道句柄代替 StartupInfo 中的 hStdInput,第二个代替 hStdOutput、hStdError,即标准输入、输出、错误句柄
使用 CreateProcess 执行子进程,这样建立的子进程输入和输出就被定向到管道中
父进程通过 ReadFile 读第二个管道来获得子进程的输出,通过 WriteFile 写第一个管道来将输入写到子进程
父进程可以通过 PeekNamedPipe 来查询子进程有没有输出
子进程结束后,要通过 CloseHandle 来关闭两个管道。
下面是具体的说明和定义:

  1. 建立匿名管道使用 CreatePipe 原形如下:

BOOL CreatePipe(
PHANDLE hReadPipe, // address of variable for read handle
PHANDLE hWritePipe, // address of variable for write handle
LPSECURITY_ATTRIBUTES lpPipeAttributes, // pointer to security attributes
DWORD nSize // number of bytes reserved for pipe
);

当管道建立后,结构中指向的 hReadPipe 和 hWritePipe 可用来读写管道,当然由于匿名管道是单向的,你只能使用其中的一个句柄,参数中的 SECURITY_ATTRIBUTES 的结构必须填写,定义如下:

typedef struct_SECURITY_ATTRIBUTES{
DWORD nLength: //定义以字节为单位的此结构的长度
LPVOID lpSecurityDescriptor; //指向控制这个对象共享的安全描述符,如果为NULL这个对象将被分配一个缺省的安全描述
BOOL bInheritHandle; //当一个新过程被创建时,定义其返回是否是继承的.供系统API函数使用.
}SECURITY_ATTRIBUTES;

  1. 填写创建子进程用的 STARTUPINFO 结构,一般我们可以先用 GetStartupInfo 来填写一个缺省的结构,然后改动我们用得到的地方,它们是:

hStdInput – 用其中一个管道的 hWritePipe 代替
hStdOutput、hStdError – 用另一个管道的 hReadPipe 代替
dwFlags – 设置为 STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW 表示输入输出句柄及 wShowWindow 字段有效
wShowWindow – 设置为 SW_HIDE,这样子进程执行时不显示窗口。
填写好以后,就可以用 CreateProcess 来执行子进程了,具体有关执行子进程的操作可以参考上一篇教程《进程控制》

  1. 在程序中可以用 PeekNamedPipe 查询子进程有没有输出,原形如下:

BOOL PeekNamedPipe(
HANDLE hNamedPipe, // handle to pipe to copy from
LPVOID lpBuffer, // pointer to data buffer
DWORD nBufferSize, // size, in bytes, of data buffer
LPDWORD lpBytesRead, // pointer to number of bytes read
LPDWORD lpTotalBytesAvail, // pointer to total number of bytes available
LPDWORD lpBytesLeftThisMessage // pointer to unread bytes in this message
);

我们可以将尝试读取 nBuffersize 大小的数据,然后可以通过返回的 BytesRead 得到管道中有多少数据,如果不等于零,则表示有数据可以读取。

  1. 用 ReadFile 和 WriteFile 来读写管道,它们的参数是完全一样的,原形如下:

ReadFile or WriteFile(
HANDLE hFile, // handle of file to read 在这里使用管道句柄
LPVOID lpBuffer, // address of buffer that receives data 缓冲区地址
DWORD nNumberOfBytesToRead, // number of bytes to read 准备读写的字节数
LPDWORD lpNumberOfBytesRead, // address of number of bytes read,实际读到的或写入的字节数
LPOVERLAPPED lpOverlapped // address of structure for data 在这里用 NULL
);

  1. 用 CloseHandle 关闭管道一和管道二的 hReadPipe和 hWritePipe 这四个句柄。

下面给出了一个例子程序,这个程序是上篇教程《进程控制》的例子的扩充,如果你对有的 api 感到陌生的话,请先阅读上一篇教程。

源程序 - 汇编源文件

DEBUG equ 0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Programmed by 罗云彬, [email protected]
; Website: http://asm.yeah.net
; LuoYunBin’s Win32 ASM page (罗云彬的编程乐园)
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 版本信息
; 汇编教程附带例子程序 - 管道例子
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.386
.model flat, stdcall
option casemap :none ; case sensitive
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
include kernel32.inc
include comctl32.inc
include comdlg32.inc
include gdi32.inc

includelib user32.lib
includelib kernel32.lib
includelib comctl32.lib
includelib comdlg32.lib
includelib gdi32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Equ 数据
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN equ 1000
MENU_MAIN equ 2000
IDM_EXEC equ 2001
IDM_EXIT equ 2002

F_RUNNING equ 0001h ;进程在运行中
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?

stStartUp STARTUPINFO <?>

hInstance dd ?
hMenu dd ?
hWinMain dd ?
hWinText dd ?
hFont dd ?
hRunThread dd ?
hRead1 dd ?
hWrite1 dd ?
hRead2 dd ?
hWrite2 dd ?
szBuffer db 512 dup (?)

dwFlag dd ?
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

.data

szMenuExecute db ‘连接 MS-&DOS 方式’,0
szExcuteError db ‘启动应用程序错误!’,0
szCaption db ‘管道示例程序 … http://asm.yeah.net’,0
szClassName db ‘PipeExample’,0
;szDllName db ‘riched32.dll’,0
;szClassNameRedit db ‘RichEdit’,0
szDllName db ‘riched20.dll’,0
szClassNameRedit db ‘richedit20a’,0
szCommand db ‘c:\command.com’,0

stLogFont LOGFONT <24,0,0,0,FW_NORMAL,
0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,
CLIP_STROKE_PRECIS,DEFAULT_QUALITY,
DEFAULT_PITCH or FF_SWISS,“Fixedsys”>

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code

if DEBUG
include Debug.asm
endif
include Win.asm

;********************************************************************
; 执行程序用的线程
; 1. 用 CreateProcess 建立进程
; 2. 用 WaitForSingleOject 等待进程结束
;********************************************************************
_RunThread proc uses ebx ecx edx esi edi,
dwParam:DWORD
local @stSecurity:SECURITY_ATTRIBUTES
local @dwExitCode
local @dwBytesRead
local @stRange:CHARRANGE

or dwFlag,F_RUNNING
;********************************************************************
; “执行”菜单改为“结束”
;********************************************************************
invoke EnableMenuItem,hMenu,IDM_EXEC,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_EXIT,MF_GRAYED
;********************************************************************
; 建立管道
;********************************************************************
mov @stSecurity.nLength,sizeof SECURITY_ATTRIBUTES
mov @stSecurity.lpSecurityDescriptor,NULL
mov @stSecurity.bInheritHandle,TRUE
invoke CreatePipe,addr hRead1,addr hWrite1,addr @stSecurity,NULL
invoke CreatePipe,addr hRead2,addr hWrite2,addr @stSecurity,NULL

;********************************************************************
; 执行文件,如果成功则等待程序结束
;********************************************************************
invoke GetStartupInfo,addr stStartUp
mov eax,hRead1
mov stStartUp.hStdInput,eax
mov eax,hWrite2
mov stStartUp.hStdOutput,eax
mov stStartUp.hStdError,eax
mov stStartUp.dwFlags,STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW
mov stStartUp.wShowWindow,SW_HIDE
invoke CreateProcess,NULL,addr szCommand,NULL,NULL,
NULL,NORMAL_PRIORITY_CLASS,NULL,NULL,offset stStartUp,offset stProcInfo
.if eax != 0
.while TRUE
invoke GetExitCodeProcess,stProcInfo.hProcess,addr @dwExitCode
.break .if @dwExitCode != STILL_ACTIVE
invoke PeekNamedPipe,hRead2,addr szBuffer,511,addr @dwBytesRead,NULL,NULL
.if @dwBytesRead != 0
invoke RtlZeroMemory,addr szBuffer,512
invoke ReadFile,hRead2,addr szBuffer,@dwBytesRead,addr @dwBytesRead,NULL
mov @stRange.cpMin,-1
mov @stRange.cpMax,-1
invoke SendMessage,hWinText,EM_EXSETSEL,0,addr @stRange
invoke SendMessage,hWinText,EM_REPLACESEL,FALSE,addr szBuffer
invoke SendMessage,hWinText,EM_SCROLLCARET,NULL,NULL
invoke SendMessage,hWinText,WM_SETFONT,hFont,0
.endif
.endw
invoke CloseHandle,stProcInfo.hProcess
invoke CloseHandle,stProcInfo.hThread
.else
invoke MessageBox,hWinMain,addr szExcuteError,NULL,MB_OK or MB_ICONERROR
.endif
;********************************************************************
; 关闭管道
;********************************************************************
invoke CloseHandle,hRead1
invoke CloseHandle,hWrite1
invoke CloseHandle,hRead2
invoke CloseHandle,hWrite2
;********************************************************************
; 把“结束”菜单改为“执行”
;********************************************************************
invoke EnableMenuItem,hMenu,IDM_EXEC,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_EXIT,MF_ENABLED
invoke EnableWindow,hWinText,FALSE
and dwFlag,not F_RUNNING
ret

_RunThread endp

;********************************************************************
; 窗口程序
;********************************************************************
WndMainProc proc uses ebx edi esi,
hWnd:DWORD,wMsg:DWORD,wParam:DWORD,lParam:DWORD

mov eax,wMsg
;********************************************************************
.if eax == WM_CREATE
mov eax,hWnd
mov hWinMain,eax
call _Init
;********************************************************************
.elseif eax == WM_SIZE
mov edx,lParam
mov ecx,edx
shr ecx,16
and edx,0ffffh
invoke MoveWindow,hWinText,0,0,edx,ecx,TRUE
invoke PostMessage,hWinText,WM_SIZE,wParam,lParam
;********************************************************************
.elseif eax == WM_CLOSE
test dwFlag,F_RUNNING
.if ZERO?
invoke DestroyWindow,hWinMain
invoke PostQuitMessage,NULL
.endif
;********************************************************************
.elseif eax == WM_COMMAND
mov eax,wParam
.if ax == IDM_EXEC
;********************************************************************
; 如果没有在执行中(dwFlag 没有置位) 则建立线程,在线程中执行程序
; 如果已经在执行中,则用 TerminateProcess 终止执行
;********************************************************************
test dwFlag,F_RUNNING
.if ZERO?
invoke EnableWindow,hWinText,TRUE
invoke SetFocus,hWinText
invoke CreateThread,NULL,NULL,offset _RunThread,
NULL,NULL,offset hRunThread
.else
invoke TerminateProcess,stProcInfo.hProcess,-1
.endif
.elseif ax == IDM_EXIT
invoke DestroyWindow,hWinMain
invoke PostQuitMessage,NULL
.endif
.else
invoke DefWindowProc,hWnd,wMsg,wParam,lParam
ret
.endif
xor eax,eax
ret

WndMainProc endp
;********************************************************************
; 程序入口
;********************************************************************
start:
call _WinMain
invoke ExitProcess,NULL
;********************************************************************
_WinMain proc
local @stWcMain:WNDCLASSEX
local @stMsg:MSG
local @hRichEdit

invoke LoadLibrary,offset szDllName
mov @hRichEdit,eax

invoke InitCommonControls
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke LoadMenu,hInstance,MENU_MAIN
mov hMenu,eax
;***************** 注册窗口类 ***************************************
invoke LoadCursor,0,IDC_ARROW
mov @stWcMain.hCursor,eax
mov @stWcMain.cbSize,sizeof WNDCLASSEX
mov @stWcMain.hIconSm,0
mov @stWcMain.style,CS_HREDRAW or CS_VREDRAW
mov @stWcMain.lpfnWndProc,offset WndMainProc
mov @stWcMain.cbClsExtra,0
mov @stWcMain.cbWndExtra,0
mov eax,hInstance
mov @stWcMain.hInstance,eax
invoke LoadIcon,hInstance,ICO_MAIN
mov @stWcMain.hIcon,eax
mov @stWcMain.hbrBackground,COLOR_BTNFACE+1
mov @stWcMain.lpszClassName,offset szClassName
mov @stWcMain.lpszMenuName,0
invoke RegisterClassEx,addr @stWcMain
;***************** 建立输出窗口 *****************************************
invoke CreateWindowEx,NULL,
offset szClassName,offset szCaption,
WS_OVERLAPPEDWINDOW,
0,0,680,420,
NULL,hMenu,hInstance,NULL

invoke ShowWindow,hWinMain,SW_SHOWNORMAL
invoke UpdateWindow,hWinMain
;********************************************************************
.while TRUE
invoke GetMessage,addr @stMsg,NULL,0,0
.break .if eax == 0
invoke TranslateMessage,addr @stMsg
invoke DispatchMessage,addr @stMsg
.endw
invoke FreeLibrary,@hRichEdit
invoke DeleteObject,hFont
ret

_WinMain endp

;********************************************************************
; 输入程序
;********************************************************************
_InputProc proc uses ebx edi esi,
hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
local @szBuffer[4]:BYTE
local @dwBytesWrite

mov eax,uMsg
.if eax == WM_CHAR
mov eax,wParam
movzx eax,al
mov dword ptr @szBuffer,eax
test dwFlag,F_RUNNING
.if !ZERO?
invoke WriteFile,hWrite1,addr @szBuffer,1,addr @dwBytesWrite,NULL
.endif
xor eax,eax
ret
.endif
invoke GetWindowLong,hWnd,GWL_USERDATA
invoke CallWindowProc,eax,hWnd,uMsg,wParam,lParam
ret

_InputProc endp
;********************************************************************
_Init proc

;*************** 建立输出 RICHEDIT 窗口 ***********************************
invoke CreateWindowEx,WS_EX_CLIENTEDGE,offset szClassNameRedit,
NULL,WS_CHILD OR WS_VISIBLE OR WS_VSCROLL OR WS_HSCROLL
OR ES_MULTILINE OR ES_AUTOHSCROLL OR ES_AUTOVSCROLL,
0,0,0,0,
hWinMain,NULL,hInstance,NULL
mov hWinText,eax
;*************** 设置字体 ***********************************************
invoke CreateFontIndirect,offset stLogFont
mov hFont,eax
invoke SendMessage,hWinText,WM_SETFONT,hFont,0
invoke SendMessage,hWinText,EM_SETREADONLY,TRUE,NULL

invoke SetWindowLong,hWinText,GWL_WNDPROC,offset _InputProc
invoke SetWindowLong,hWinText,GWL_USERDATA,eax
invoke EnableWindow,hWinText,FALSE

invoke _CenterWindow,hWinMain
invoke SetFocus,hWinText

ret

_Init endp
;********************************************************************
end start

程序的分析和要点

在程序中,我先建立了一个 Richedit 控件用来显示子进程的输出,同时将 RichEdit 子类化,截取它的键盘输入以便把它发给子进程

invoke SetWindowLong,hWinText,GWL_WNDPROC,offset _InputProc

这条语句将 RichEdit 的过程指到了 _InputProc 中,然后在 _InputProc 的 WM_CHAR 中将键入的字符 WriteFile 到管道中,我在程序中先建立了两个管道,然后执行 c:\command.com,这样就得到了一个 dos 的命令行进程,然后在循环中通过 PeekNamedPipe 检测子进程有无输出,如果有的话则通过 ReadFile 读出,在显示到 RichEdit 中。

在运行例子程序的时候要注意,你可以在这个“Command.com” 中执行几乎所有的别的程序,但是不要执行如 ucdos,pctools 之类不使用标准输入输出的程序(就是在 dos 下用不了“>”或者“<”重定向的程序),由于我们在装载子进程的时候用了 WS_HIDE,所以原来的 command.com 的窗口是隐藏的,如果你执行了这种程序那就意味着你失去的对子进程的控制,因为它们不使用标准输入来接收键盘,你也就无法通过管道让它们退出。

在这里还可以引申出匿名管道的另一个用法,如果你执行的不是 command.com 而是类似于 arj.exe 的程序,然后也不用把它的输出显示到 RichEdit 中,而是在程序中处理,那么,你就可以编写一个 winarj,当然你只需编写窗口界面和 arj.exe 之间的配合而已。

多谢分享。。。
迟下再睇。。。

这个这个 …[s:19]

[s:21] [s:23] [s:29]