云昴(Mao Yun)

【操作系统】油槽通信

| 【专业·学习】操作系统MFC

油槽通信简介

油槽是windows系统中最简单的一种进程间通讯的方式,一个进程可以创建一个油槽,其他进程可以通过打开此油槽与创建油槽的进程通讯;油槽的通讯时单向的,只有服务端才能从油槽中读取消息,客户端只能写入消息,消息被写入后以队列的方式保存(先进后出);油槽除了可以在本机内进行通讯外,还可以在主机间进程通讯(使用UDP协议),想要通过网络进行油槽通讯,必须要知道服务端的主机名或域名。

邮槽是一种单向的进程间通讯机制。用最简单的话来说,通过邮槽,客户机进程可将消息传送或广播给一个或多个服务器进程。在同一台计算机的不同进程之间,或在跨越整个网络的不同计算机的进程之间,协助进行消息的传输。但由于邮槽采用的是一种直接基于文件系统开发而成,所以它不依赖于某种具体的网络协议,而且邮槽是围绕一个广播通信体系设计出来的,所以当然不能指望能通过它实现数据的“可靠”传输,邮槽每次传送的消息长度不能长于422个字节。

邮槽最大的一个缺点便是只允许从客户机到服务器(即只能客户机由写入数据,而服务器读取数据),建立一种不可靠的单向数据通信。而另一方面,邮槽最大的一个优点在于,它们使客户机应用能够非常容易地将广播消息发送给一个或多个服务器应用。

命名规则

前面说过,邮槽是围绕Windows 文件系统接口设计出来的,采用“邮槽文件系统”(Mailslot File System, MSFS )接口,因此,客户机和服务器应用需要使用标准的Win32文件系统I/O函数,比如ReadFile和WriteFile等等,以便在邮槽上收发数据,同时还要利用Win32文件系统的命名规则。

对邮槽进行标识时,需遵守下述命名规则:

\\server\Mailslot\[path]name

请将上述字串分为三段来看:\\server、\Mailslot和\[path]name。第一部分\\server对应于服务器的名字,我们要在上面创建邮槽,并在上面运行服务器程序。第二部分\Mailslot是一个“硬编码”的固定字串,用于告诉系统这个文件名从属于MSFS 。第三部分\[path]name则允许应用程序独一无二地定义及标识一个邮槽名。其中,“pat h”代表路径,可指定多级目录。

举个例子来说,对一个邮槽进行标识时,下面这些形式的名字都是合法的(注意Mailslot不得变化,必须原文照输,亦即所谓的“硬编码”):

\\Oreo\Mailslot\Mymailslot

\\Testserver\Mailslot\Cooldirectory\Funtest\Anothermailslot

\\.\Mailslot\Easymailslot

\\*\Mailslot\Myslot

服务器字串部分可表示成一个小数点(.)、一个星号(*)、一个域名或者一个真正的服务器名字。所谓“域”,其实就是一系列工作站和服务器的组合,它们共用一个相同的组名。

相关函数

下面是与邮槽操作相关的一些函数:

CreateMailslot()函数

该函数用于以指定的名字来创建一个邮槽。函数原形如下:

HANDLE CreateMailslot(

LPCTSTR lpName,

DWORD nMaxMessageSize,

DWORD lReadTimeout,

LPSECURITY_ATTRIBUTES lpSecurityAttributes

);

第一个参数lpName,指定要创建的邮槽的名字。名字的格式如下:

\\.\Mailslot\[path]name

要注意的是,服务器的名字用一个小数点来表示,亦即服务器就是本地机器。这样做是

很有必要的,因为我们不能在远程计算机上创建邮槽。在lpName参数中,名字必须以一种独

一无二的形式表达。可将它设为一个独立的名字,也可以在它前面加上一个完整的目录路径。

第二个参数nMaxMessageSize,表示可写入邮槽的一条消息的最大长度(以字节为单位)。若要服务器接收任意长度的消息,则将该参数值设置为0。

第三个参数lReadTimeout,指定了读操作需要等候消息写入邮槽的时间,以毫秒为单位。若该参数为MAILSLOT_WAIT_FOREVER ,表示在进入的数据可以读取之前,读操作便会无限期地等待下去;若该参数为0,表示若当前没有消息,读操作就会立即返回。

第四个参数lpSecurityAttributes,指向SECURITY_ATTRIBUTES结构的指针,表示要创建的邮槽的安全属性。结构SECURITY_ATTRIBUTES中的数据成员bInheritHandle决定了创建的邮槽是否可以被子进程继承。若参数lpSecurityAttributes设为NULL,则表示创建的邮槽不可以被子进程继承,并且SECURITY_ATTRIBUTES结构中的lpSecurityDescriptor数据成员忽略。

如果该函数调用成功,返回所创建的邮槽的句柄;若失败,则返回INVALID_HANDLE_VALUE值。当返回了一个有效的邮槽的句柄之后,服务器便可以开始数据的实际读取。服务器应使用ReadFile这个Win32函数来进行数据的读取。

ReadFile()函数

不要被这个函数的字面含义给迷惑了,该函数不仅可以从一个文件中读取数据,还可以读取邮槽和管道等等。此函数在第8章的文件I/O处理中使用过,这里再详细的介绍如何用它来读取邮槽,因为邮槽也是基于文件系统开发而成。函数原形如下:

BOOL ReadFile(

HANDLE hFile,

LPVOID lpBuffer,

DWORD nNumberOfBytesToRead,

LPDWORD lpNumberOfBytesRead,

LPOVERLAPPED lpOverlapped

);

第一个参数hFile,表示要读取的文件的句柄,这里指的是邮槽的句柄。

第二个参数lpBuffer,指向用户自定义缓冲区,用来保存用户从邮槽中读取的数据。这个缓冲区的大小应该比来自CreateMailslot函数调用的nMaxMessageSize 参数的设置值大。此外,缓冲区应该大于邮槽上的进入消息;如果不够大,ReadFile调用便会失败,并返回一个ERROR_INSUFFICIENT_BUFFER错误。

第三个参数nNumberOfBytesToRead,表示要从邮槽中读取的字节数。

第四个参数lpNumberOfBytesRead,保存着实际读取的数据的字节数。该参数用于在ReadFile操作完成后,报告读入的实际字节数量。

第五个参数lpOverlapped,采用Win32重叠I/O机制,可以通过异步方式进行数据的读取。该参数只在WindowsNT 和Windows 2000上支持,在Windows95或Windows98下不支持,应将该参数设为NULL。

该函数调用成功返回非0值,调用失败,返回0。

CreateFile()函数

该函数用于建立或打开一个对象(包括文件、通信源、磁盘设备等等),这里指的是打开一个邮槽的句柄。函数定义如下:

HANDLE CreateFile(

LPCTSTR lpFileName,

DWORD dwDesiredAccess,

DWORD dwShareMode,

LPSECURITY_ATTRIBUTES lpSecurityAttributes,

DWORD dwCreationDispostion ,

DWORD dwFlagsAndAttributes,

HANDLE hTemplateFile

);

第一个参数lpFileName,表示打开的对象的名字,这里用于描述一个或多个邮槽,可用之前讲过的邮槽命名格式,向其写入数据。

第二个参数dwDesiredAccess,表示对打开的对象的访问类别,包括读、写、读写。在这里,该参数必须为GENERIC_WRITE(读),因为此函数是用于客户机中,而客户机只能向服务器写入数据。

第三个参数dwShareMode,表示打开的对象的共享类别,包括共享读、共享写、共享读写、不共享。这里,该参数必须为FILE_SHARE_READ,允许服务器在邮槽上打开和进行读操作。

第四个参数lpSecurityAttributes,安全属性,对邮槽不会有什么效果,可忽略,设其为NULL。

第五个参数dwCreationDispostion,指定对于将要打开的对象(已存在或不存在)的打开方式,包括以下几种:

  • CREATE_NEW 创建新的,如果已经存在,则函数调用失败

  • CREATE_ALWAYS 创建新的,如果已经存在,覆盖掉原来的

  • OPEN_EXISTING 打开已存在的,如果不存在,则函数调用失败

  • OPEN_ALWAYS 如果文件存在就打开,如果不存在则创建

这里应将dwCreationDispostion参数应设为OPEN_EXISTING,若一台机器既是客户机,也是服务器,这一设置便显得尤其重要,因为如果服务器没有创建邮槽的话,那么对函数CreateFile的调用便会失败。如果服务器在远程工作,那么dwCreationDispostion参数没什么意义。

第六个参数dwFlagsAndAttributes,指定对象的属性和标记,通常情况下应该设置为FILE_ATTRIBUTE_NORMAL。

第七个参数hTemplateFile,忽略,应设置为NULL。

该函数若调用成功返回打开的对象的句柄,若调用失败,返回的值和调用该函数时设置的dwCreationDispostion参数的值有关。

WriteFile()函数

该函数用于向对象(包括文件、邮槽、管道等)中写入数据,这里指的是邮槽,请记住,作为客户机,只能将数据写

入邮槽。函数原形如下:

BOOL WriteFile(

HANDLE hFile,

LPCVOID lpBuffer,

DWORD nNumberOfBytesToWrite,

LPDWORD lpNumberOfBytesWritten,

LPOVERLAPPED lpOverlapped

);

第一个参数hFile,表示要写入数据的文件句柄,该文件句柄在创建时必须以GENERIC_WRITE方式来确定。

第二个参数lpBuffer,指向用户自定义缓冲区,该缓冲区里包含要写入邮槽的数据。

第三个参数nNumberOfBytesToWrite,要写入邮槽的数据的字节数。

第四个参数lpNumberOfBytesWritten,实际写入的数据的字节数,由函数返回。

第五个参数lpOverlapped,采用Win32重叠I/O机制,可以通过异步方式进行数据的写入。该参数只在WindowsNT 和Windows 2000上支持,在Windows95或Windows98下不支持,应将该参数设为NULL。

下面讲述一下服务器和客户机操作邮槽的大致过程。邮槽建立了一个简单的客户机/服务器设计体系。在这个体系中,数据只能从客户机传到服务器,数据通信是单向进行的。服务器进程的职责是创建一个邮槽,而且是能从邮槽读取数据的唯一一个进程。客户机进程则负责打开邮槽的“实例”,该进程是能够向其中写入数据的唯一一种进程。分述如下:

  • 服务器端

若想实现一个邮槽,要求开发一个服务器应用,来负责邮槽的创建。下述步骤解释了如

何编写一个基本的服务器应用:

  1. 用CreateMailslot函数创建一个邮槽句柄。

  2. 调用ReadFile函数,并使用现成的邮槽句柄,从任何客户机接收数据。

  3. 调用CloseHandle函数来关闭邮槽句柄。

    • 客户机端

要想实现一个客户机,需要开发一个应用程序,对一个现有的邮槽进行引用和写入。下

述步骤解释了如何编写一个基本的客户机应用:

  1. 使用CreateFile函数,针对想向其传送数据的邮槽,打开指向它的一个引用句柄。

  2. 调用WriteFile函数,向邮槽写入数据。

  3. 完成了数据的写入后,用CloseHandle 函数,关闭打开的邮槽句柄。

下面将给出一个实例使读者更好的掌握如何使用邮槽来实现进程间的通讯。完整例程请参见光盘中例子代码EX12_01Srv,操作步骤如下:

  • 服务器端:

    • 步骤1:新建一个MFC单文档应用程序,工程名为EX12_01Srv或用户自定义。

    • 步骤2:在原菜单资源基础上添加一个菜单 “读取邮槽”,ID为IDM_READ。将此菜单放在顶层菜单中,但是并不是弹出式菜单(将Pop-up复选框去掉),而是作为菜单项。

    • 步骤3:在CEX12_01SrvView类中添加该菜单的COMMAND命令消息处理函数OnRead,并编辑如清单12-00所示的代码:

清单12-00 CEX12_01SrvView::OnRead函数代码

    void CEX12_01SrvView::OnRead() 
    {
        // TODO: Add your command handler code here
        //定义一个句柄变量用来保存返回的邮槽句柄
        HANDLE hMail;
        //创建邮槽句柄
        hMail=CreateMailslot("\\\\.\\Mailslot\\slot",0,
                MAILSLOT_WAIT_FOREVER,NULL);
        //判断是否创建成功
        if (hMail==INVALID_HANDLE_VALUE)
        {
            MessageBox("Create mailslot error");
            return;
        }
        //定义字符型变量用来保存读取的数据
        char szchar[100];
        DWORD dwRead;
        OVERLAPPED ov;
        memset(szchar,0,100);
        //将ov结构清空
        ZeroMemory(&ov,sizeof(OVERLAPPED));
        HANDLE hevent;
        hevent=CreateEvent(NULL,true,false,NULL);
        ov.hEvent=hevent;
        //读取数据
        if (ReadFile(hMail,szchar,100,&dwRead,&ov)==0)
        {
            if (GetLastError()!=ERROR_IO_PENDING)
            {
                MessageBox("read out error");
                CloseHandle(hMail);
                return;
            }
        }
      //等待客户端写入
        WaitForSingleObject(hevent,INFINITE);
        ResetEvent(hevent);
        MessageBox(szchar);
    }

代码中用到了Win32重叠I/O机制,在用ReadFile和WriteFile读写时,既可以同步执行,也可以重叠(异步)执行。在同步执行时,函数直到操作完成后才返回,这意味着在同步执行时线程会被阻塞,从而导致效率下降。在重叠执行时,即使操作还未完成,调用的函数也会立即返回,费时的I/O操作在后台进行,这样线程就可以干别的事情。

ReadFile和WriteFile函数是否执行重叠操作是由CreateFile函数决定的。如果在调用CreateFile创建句柄时指定了FILE_FLAG_OVERLAPPED标志,那么调用ReadFile和WriteFile对该句柄进行的读写操作就是重叠的,如果未指定重叠标志,则读写操作是同步的。

第18行在使用重叠I/O时,线程需要创建OVERLAPPED结构以供读写函数使用。OVERLAPPED结构最重要的成员是hEvent,第23行代码hEvent是一个事件对象句柄,线程应该用CreateEvent函数为hEvent成员创建一个手工重置事件,hEvent成员将作为线程的同步对象使用。如果读写函数未完成操作就返回,就那么把hEvent成员设置成无信号的,操作完成后(包括超时),hEvent会变成有信号的。

  • 客户机端:

  • 步骤1:在上一个服务器端工作台的基础上再添加一个工程名为EX12_01Cli工程,在新建对话框中选择Add to current worksapce,仍然选择MFC应用程序单文档应用程序。

  • 步骤2:在原菜单资源基础上添加一个菜单 “写入邮槽”,ID为IDM_WRITE。将此菜单放在顶层菜单中,但是并不是弹出式菜单(将Pop-up复选框去掉),而是作为菜单项。

  • 步骤3:在CEX12_01CliView类中添加该菜单的COMMAND命令消息处理函数OnWrite,并编辑如下代码:

    void CEX12_01CliView::OnWrite() 
    {
    // TODO: Add your command handler code here
    //定义句柄型变量用来保存打开的邮槽句柄
    HANDLE hMailslot;
    //打开邮槽句柄
    hMailslot=CreateFile("\\\\.\\Mailslot\\slot",
            GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,NULL);
    if (INVALID_HANDLE_VALUE==hMailslot)
    {
        MessageBox("create handle error");
        return;
    }
    DWORD dwWrite;
    //写入数据
    WriteFile(hMailslot,"创建客户程序成功",
            strlen("创建客户程序成功"),&dwWrite,NULL);
    }
    

    测试该程序,运行服务器和客户机,先执行服务器的“读取邮槽”菜单,再执行客户机的“写入邮槽”菜单。

云昴(Mao Yun)