发新话题
打印

[转载]Win32汇编教程12-管道操作

[转载]Win32汇编教程12-管道操作

作者:罗云彬

概述

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

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

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

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

hStdInput -- 用其中一个管道的 hWritePipe 代替
hStdOutput、hStdError -- 用另一个管道的 hReadPipe 代替
dwFlags -- 设置为 STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW 表示输入输出句柄及 wShowWindow 字段有效
wShowWindow -- 设置为 SW_HIDE,这样子进程执行时不显示窗口。
填写好以后,就可以用 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&#39;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  &#39;连接 MS-&DOS 方式&#39;,0
szExcuteError  db  &#39;启动应用程序错误!&#39;,0
szCaption  db  &#39;管道示例程序 ... http://asm.yeah.net&#39;,0
szClassName  db  &#39;PipeExample&#39;,0
;szDllName  db  &#39;riched32.dll&#39;,0
;szClassNameRedit db  &#39;RichEdit&#39;,0
szDllName  db  &#39;riched20.dll&#39;,0
szClassNameRedit db  &#39;richedit20a&#39;,0
szCommand  db  &#39;c:\command.com&#39;,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 之间的配合而已。

附件

源代码.rar (7 KB)

2006-1-5 03:06, 下载次数: 87

人情如冰六月寒,花做一份艳,为谁笑人间? 如果任何人发现我转载的有图像的文章中图像失效或者文章有问题,请及时短消息通知我。先谢谢。::)) coup de foudre

TOP

发新话题