Win32汇编教程十二
管道操作

在这儿下载本节的所有源程序

概述

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

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

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

  1. 服务端通过函数 CreateNamedPipe 创建一个命名管道的实例并返回用于今后操作的句柄,或为已存在的管道创建新的实例。
  2. 服务端侦听来自客户端的连接请求,该功能通过 ConnectNamedPipe 函数实现。
  3. 客户端通过函数 WaitNamedPipe 来等待管道的出现,如果在超时值变为零以前,有一个管道可以使用,则 WaitNamedPipe 将返回 True,并通过调用 CreateFile 或 CallNamedPipe 来呼叫对服务端的连接。
  4. 此时服务端将接受客户端的连接请求,成功建立连接,服务端 ConnectNamedPipe 返回 True
  5. 建立连接之后,客户端与服务器端即可通过 ReadFile 和 WriteFile,利用得到的管道文件句柄,彼此间进行信息交换。
  6. 当客户端与服务端的通信结束,客户端调用 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 (父进程)

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

  1. 使用 CreatePipe 建立两个管道,得到管道句柄,一个用来输入,一个用来输出
  2. 准备执行控制台子进程,首先使用 GetStartupInfo 得到 StartupInfo
  3. 使用第一个管道句柄代替 StartupInfo 中的 hStdInput,第二个代替 hStdOutput、hStdError,即标准输入、输出、错误句柄
  4. 使用 CreateProcess 执行子进程,这样建立的子进程输入和输出就被定向到管道中
  5. 父进程通过 ReadFile 读第二个管道来获得子进程的输出,通过 WriteFile 写第一个管道来将输入写到子进程
  6. 父进程可以通过 PeekNamedPipe 来查询子进程有没有输出
  7. 子进程结束后,要通过 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;

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

填写好以后,就可以用 CreateProcess 来执行子进程了,具体有关执行子进程的操作可以参考上一篇教程《进程控制

3. 在程序中可以用 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 得到管道中有多少数据,如果不等于零,则表示有数据可以读取。

4. 用 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
);

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

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

源程序 - 汇编源文件

DEBUG	equ	0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;	Programmed by 罗云彬, bigluo@telekbird.com.cn
;	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 之间的配合而已。





(C) Copyright by LuoYunBin's Win32 ASM Page,http://asm.yeah.net