这篇文章将会介绍现有的Win32函数支持的网络通信功能,并且展示了如何在你自己的应用中使用它们。在API中,有两个支持网络通信的便利方法:mailslot和命名管道(name pipes)。这篇文章将分别讨论它们,并且分别介绍它们的优缺点。
因为Win32 API直接支持网络通信,因此要创建各种使用网络的应用是特别简单的。例如,你要在你的网络中建立一个多用户会议系统,与BBS的“CB”类似。在这个系统中,用户在各自的机器上运行一个会议系统程序,他们所打的全部信息都会广播给同一网络的所有其他用户。通常这种系统可使用mailslots实现,因为mailslots很容易广播信息。事实上,多人的网络游戏使用的也是类似的技术。
当你要在两部机器间传送大量的数据流时,你通常使用点对点的命名管道连接。例如,你会使用命名管道来实现一个网络数字电话或者视频系统。客户/服务器也都是使用命名管道的。其中的一个中央机器会作为服务器端,然后其它所有的客户端就可使用命名管道与它分别进行连接。
网络基础 为了进行更好地理解下面的例子,懂得一些网络的基本知识是很有必要的。下面的图展示了一个小型公司中常见的简单网络。每台机器都使用一个网络适配器连接到网络中,并且都拥有一个唯一的名字来标识它。网络适配器决定了网络的类型,常见的有以太网或者令牌网。适配器还决定了网络使用的媒体,可以是同轴线、双绞线等。要知道的一点是,在这样的简单网络中,所有的机器都可以平等地与其它的机器进行通信。

*******************图一*************************
通过传统的Win32 API函数在机器间通信有两种方法。一种是mailslot,一个机器可以广播一个信息,网络中的其它所有机器都可以接收到。使用命名管道的话,一部机器选择另一部进行通信,并且与它建立一个特别的连接。命名管道的好处是连接可靠。如果连接打断的话,例如一块网卡或者网线出现故障,连接的两端都可以马上接收到连接断开的信息。mailslots是不可靠的,因为发送者无法确认接收者是否已经收到了信息。mailslot的好处是它可以很容易地同时给许多机器发信息。
上面的图展示的是一个网段。一个网段的定义是直接互相连接的一组机器。一个网段中的机器数目是受到限制的,因为当机器的数量增加时,网络的通信量也会增大。通常的限制是大约为100台机器。在一个大的公司中,每个网段大约包括有20到30台机器。所有的网段之间可以通过路由器进行连接,这样它们之间就可以进行通信,如下图所示。了解到这种差别是重要的,因为通常一个mailslot信息只能在一个网段中传送,而命名管道的信息可以经过路由器传送到另一个网段中。

******************图二***********************
使用mailslots和命名管道来进行网络通信,有三种不同的通信方式:广播、点对点和客户/服务器方式。将mailslot应用在广播模式时,一台机器发送信息到网段上的其它所有机器上。在点对点通信时,一台机器与另一台建立一个特别的连接,数据可以通过命名管道在它们之间往复传送。在一个客户/服务器的关系中,一台机器作为服务器,所有机器通过点对点的命名管道来与它进行连接。如果要使用客户/服务器的方式来模拟一个广播的操作,可以通过一台机器发送一个信息到服务器,然后服务器就可以将信息的副本分别发到每个客户端。
mailslot连接
使用Win32 API来进行通信时,Mailslots是最简单的方式。在同一网段中,Mailslots提供了一条单向的通信路径,将信息由一个发送者传送到一个或者多个接收者中。如果你想同时发送数据到许多接收者时,可以考虑使用mailslot的方式。
要创建mailslots是非常简单的,读取和写入都是通过API常见的ReadFile和WriteFile函数进行的。在创建mailslot时,一个特别的路径名会传送到CreateMailslot函数中,可让系统知道要创建一个mailslot而不是一个通常的文件。(这里提到的函数,你都可以从Win32编程者的参考指南、SDK或者Visual C++ V2的Win32帮助文件中找到)
文章最后有一些程序的列表,在列表1和2中的程序已经尽量地简化,从中你可以很容易地了解到通过一个mailslot来传送和接收数据的必要步骤。列表1展示了如何通过CreateMailslto函数来创建一个mailslot服务器,服务器保存有接收信息的一个队列,直到你使用ReadFile函数来读取它们。存储在队列中的信息以它们到达的顺序排列。
mailslot的名字必须以\\.\mailslot\[path]name的形式排列。这看来象一个文件名,事实上,它的ReadFile函数操作也与一个文件类似。有点不同的是,该函数并不会创建任何真正的文件:mailslot保存在内存中。在列表1中使用的mailslot名字是:"\\.\mailslot\sms"。为了进一步对mailslot进行分类,你可以在路径中加入“子目录”。
在你创建mailslot时,你可以指定信息的最大长度,以及读取的超时时间。在网络上,mailslots在一个信息(message)中可传送不超过400字节的信息。如果你将超时的值设置为0,那么不管缓冲中是否有信息,对ReadFile的任何调用都会马上返回。如果你将超时时间设置为一个特定值(以毫秒计),若在定义时间内都没有任何信息到达,读取的操作将会失败。你还可以使用MAILSLOT_WAIT_FOREVER常数来创建一个阻塞的读取。
列表1使用的是无阻塞的方式,并且使用GetMailslotInfo函数来确保在进行一个读取操作前,mailslot队列中有信息存在。该函数返回队列中信息的最大长度、下一信息的长度和等待的信息数目。程序不断地检测mailslot中是否有信息存在,如果有的话,它就读取第一个,从mailslot中读取信息的操作与读取一个文件类似。
在网络上的任何计算机,如果要发送信息给一台运行列表1程序的机器,需要提供发送者和接收者的mailslot名字。列表2展示了如何发送信息给一个mailslot。它首先通过常用的CreateFile函数来打开一个到mailslot的可写连接。该程序作为一个mailslot客户端使用,因为它写信息到已经在网络上运行的mailslot服务器上。CreateFile函数通过检查mailslot文件的名字,就知道不是要创建一个文件,而是要与一个mailslot通信。文件名可有4种不同的格式:
\\.\mailslot\[path]name \\*\mailslot\[path]name \\domain\mailslot\[path]name \\machine\mailslot\[path]name |
上面的例子中,这些名字分别指定了网络上的本地机器或者某台特定的机器。第二种形式指定了一个到本地主域所有机器的广播操作。第三种形式指定了到某个域的所有机器。对于域和域控制器的详细信息,可参考NT的书籍。
在打开mailslot后,列表2使用GetComputerName得到本地的计算机名字,然后广播该名字给当前域的所有mailslot,每5秒广播一次。
列表1使用一个轮询的技术来检查信息。每隔半秒,它就会调用GetMailslotInfo并且检查slot中是否有信息。通常轮询在一个多线程的环境中并不是一个好的技术,因为效率比较低。你可以不用轮询,而是通过设置CreateMailslot中的超时时间为一个适当的值,然后以缓冲为0的参数调用ReadFile来等待信息的到来。一旦ReadFlie返回,你就知道有信息存在,接着可调用GetMailslotInfo和ReadFile,如列表1所示。
当你运行列表2的程序时,它将广播给网络上的所有机器。如果你在同一或者不同的机器上运行读取器的多个副本,它们都将看到由写入者产生的信息。你也可以在网络上运行多个写入者,读取者都将会看到所有写入者产生的信息。在列表1和2的程序中,要注意到它们都是假定该程序将会从外部终止。你可以使用CloseHandle函数来关闭一个mailslot服务器或者客户端。
命名管道(Named Pipes)
命名管道提供了一个确认的传送技术。与网络上的广播方式不同,你通过一个命名管道与另一台机器建立一个不同的连接。如果连接中断。例如是由于一台机器关掉或者网络的某部分有故障,连接的双方都可以在尝试发送或者接收时,马上知道中断的信息。通过一个命名管道,可确保包顺序到达。命名管道的唯一问题是你不能广播包了。要广播任何信息,所有的目标机器都必须与中央的服务器建立一个连接,服务器必须分别传送信息到各个不同的机器上。
命名管道的创建只比mailslot难一点。列表4和5的程序展示了如何在两个使用命名管道的应用之间,创建一个简单的点对点连接。首先运行列表4中的接收程序,然后在同一机器上运行列表5中的发送程序。该程序将询问你要连接的机器名字。由于你在同一部机器上运行发送和接收的程序,因此可输入“.”或者是你的机器名。你将会看到每隔5秒左右,就有一个信息由发送者传到接收者上。当你关闭发送者的时候,在接收者上就会马上出现一个信息,指示它已经检测出管道连接中断。如果只启动发送的程序,发送者将会马上出错,因为它不能建立一个连接。与mailslot不同,管道可以告诉我们另一端的工作是否正常。
命名管道连接在网络上的使用与在同一部机器上一样简单。例如,如果列表4中的服务器程序运行在一部称为“orion”的机器上,使用与该机器同样的帐号和密码在另一台不同的机器上登录,在上面运行列表5的程序,要求机器名时,输入“orion”的名字。这样连接就被正确地建立起来了。要注意一点,使用命名管道的时候,你必须要知道运行服务器的机器名字。
你还要知道,如果使用另一个用户来尝试连接接收器时,连接将会失败。例如用户“jones”在“orion”的机器上运行接收程序,当用户“smith”尝试由另一台机器进行连接时,连接将会失败,并显示一个“拒绝访问”的错误。这是NT的安全系统造成的。你可以看NT书籍的相关部分,了解详细的原因和解决办法。
在列表4的程序中,通过使用CreateNamedPipe来创建一个命名管道服务器。与CreateNamedPipe函数一起使用的名字通常有以下的形式:
\\.\pipe\[path]name
与mailslot一样,你可以在管道名字的前面指定一个路径,以区分系统中不同的管道。
传送给CreateNamedPipe的openMode参数用来决定管道的方向。命名管道可以是单向的,也可以是双向的,在于openMode参数使用的常数,包括有:
PIPE_ACCESS_DUPLEX PIPE_ACCESS_INBOUND PIPE_ACCESS_OUTBOUND |
CreateNamedPipe的pipeMode参数决定管道的工作方式,可以是字节流或者是称为信息的字节包。字节流是没有逻辑边界的。信息将一组的字节组合起来作为一个单元传送。你可以在读取和写入时指定一种方式:
PIPE_TYPE_MESSAGE PIPE_TYPE_BYTE PIPE_READMODE_MESSAGE PIPE_READMODE_BYTE |
在一台机器上,一个管道可以有超过一个的实例。这可让一个程序处理多个的客户端,每个使用独立的线程,还必须创建一个命名管道服务器。由于列表4和5中的例子只是一个简单的点对点连接,只需要一个实例,一个实例的最大值在调用CreateNamedPipe时指定。
列表4中的程序接着会等待一个通过ConnectNamedPipe函数建立的连接。当一个客户端程序使用正确的机器名和命名管道调用CreateFile时,就会与服务器建立起一个连接。在连接时,ConnectNamedPipe函数会返回。你可以选择指定一个交迭的结构,ConnectNamedPipe 将马上返回,然后产生连接的事件。
列表4的程序然后进入一个循环,等待数据到达。ReadFile函数的工作与操作文件时有点不同。因为这个命名管道是信息的模式,ReadFile只要一接收到一个完整的信息,就会立刻返回。这里使用的是一个阻塞的读取,你也可以选择一个交迭的读取。
列表5的程序是列表4的一个简单的客户。列表5首先通过CreateFile函数创建连接到命名管道。然后通过使用WriteFile函数写入信息。每次调用WriteFile都会在命名管道的接收端建立一个信息,因此接收者的ReadFile函数将在接收信息时除去阻塞,服务器就会在屏幕上显示一个信息。
如果两个客户的副本同时尝试连接列表4中的程序,服务器将会拒绝第二个客户。不论是终止客户或者服务器,另一方都将在检测出连接中断时马上终止。
结论
命名管道通常用在客户/服务器系统,这时服务器使用一个多线程的方式来同时处理多个连接。
列表1 该程序创建一个mailslot服务器并且从中进行读取
//*************************************************************** // From the book "Win32 System Services: The Heart of Windows NT" // by Marshall Brain // Published by Prentice Hall file:// // This code implements a simple mailslot server (receiver) that // uses polling. file://***************************************************************
// sms_recv.cpp
#include #include
int main() { char toDisptxt[80]; HANDLE hSMS_Slot; DWORD nextSize; DWORD Msgs; DWORD NumBytesRead; BOOL Status;
/* Create a mailslot for receiving messages */ hSMS_Slot=CreateMailslot("\\\\.\\mailslot\\sms", 0, 0, (LPSECURITY_ATTRIBUTES) NULL);
/* Check and see if the mailslot was created */ if (hSMS_Slot == INVALID_HANDLE_VALUE) { cerr << "ERROR: Unable to create mailslot " << GetLastError() << endl; return (1); }
/* Repeatedly check for messages until the program is terminated */ while(1) { Status=GetMailslotInfo(hSMS_Slot, (LPDWORD) NULL, &nextSize, &Msgs, (LPDWORD) NULL); if (!Status) { cerr << "ERROR: Unable to get status. " << GetLastError() << endl; CloseHandle(hSMS_Slot); return (1); }
/* If messages are available, then get them */ if (Msgs) {
/* Read the message and check to see if read was successful */ if (!ReadFile(hSMS_Slot, toDisptxt, nextSize, &NumBytesRead, (LPOVERLAPPED) NULL)) { cerr << "ERROR: Unable to read from mailslot " << GetLastError() << endl; CloseHandle(hSMS_Slot); return (1); }
/* Display the Message */ cout << toDisptxt << endl; } else /* Check for new messages twice a second */ Sleep(500); } /* while */ }
|
列表2
该程序每隔5秒写一次mailslot
//*************************************************************** // From the book "Win32 System Services: The Heart of Windows NT" // by Marshall Brain // Published by Prentice Hall file:// // This code implements a simple mailslot sender. file://***************************************************************
// sms_send.c
// Usage: sms_send
#include #include #include
int main() { char toSendTxt[100], buffer[100]; DWORD bufferLen=100; HANDLE hSMS_Slot; BOOL Status; DWORD NumBytesWritten;
/* Create the mailslot file handle for sending messages */ hSMS_Slot=CreateFile("\\\\*\\mailslot\\sms", GENERIC_WRITE, FILE_SHARE_READ, (LPSECURITY_ATTRIBUTES) NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, (HANDLE) NULL);
/* Check and see if the mailslot file was opened, if not terminate program */ if (hSMS_Slot == INVALID_HANDLE_VALUE) { cerr << "ERROR: Unable to create mailslot " << GetLastError() << endl; return (1); }
/* form string to send */ GetComputerName(buffer, &bufferLen); strcpy(toSendTxt, "Test string from "); strcat(toSendTxt, buffer);
/* Repeatedly send message until program is terminated */ while(1) { cout << "Sending..." << endl; /* Write message to mailslot */ Status=WriteFile(hSMS_Slot, toSendTxt, (DWORD) strlen(toSendTxt)+1, &NumBytesWritten, (LPOVERLAPPED) NULL);
/* If error occurs when writing to mailslot, terminate program */ if (!Status) { cerr << "ERROR: Unable to write to mailslot " << GetLastError() << endl; CloseHandle(hSMS_Slot); return (1); }
/* Wait sending the message again */ Sleep(4800); } /* while*/ }
|
列表4 这个简单的程序用来创建一个命名管道服务器。该服务器将会等待并接受一个连接,然后从中接收信息。
//*************************************************************** // From the book "Win32 System Services: The Heart of Windows NT" // by Marshall Brain // Published by Prentice Hall file:// // This code implements a simple named pipe server (receiver). file://***************************************************************
// ssnprecv.cpp
// Usage: ssnprecv
#include #include
int main() { char toDisptxt[80]; HANDLE ssnpPipe; DWORD NumBytesRead;
/* Create a named pipe for receiving messages */ ssnpPipe=CreateNamedPipe("\\\\.\\pipe\\ssnp",PIPE_ACCESS_INBOUND, PIPE_TYPE_MESSAGE | PIPE_WAIT,1, 0, 0, 150,(LPSECURITY_ATTRIBUTES) NULL);
/* Check and see if the named pipe was created */ if (ssnpPipe == INVALID_HANDLE_VALUE) { cerr << "ERROR: Unable to create a named pipe. " << endl; return (1); }
/* Allow a client to connect to the name pipe, terminate if unsuccessful */ cout << "Waiting for connection... " << endl; if(!ConnectNamedPipe(ssnpPipe, (LPOVERLAPPED) NULL)) { cerr << "ERROR: Unable to connect a named pipe "<< GetLastError() << endl; CloseHandle(ssnpPipe); return (1); }
/* Repeatedly check for messages until the program is terminated */ while(1) { /* Read the message and check to see if read was successful */ if (!ReadFile(ssnpPipe, toDisptxt,sizeof(toDisptxt),&NumBytesRead, (LPOVERLAPPED) NULL)) { cerr << "ERROR: Unable to read from named pipe " << GetLastError() << endl; CloseHandle(ssnpPipe); return (1); }
/* Display the Message */ cout << toDisptxt << endl; } /* while */ }
|
列表5
一个命名管道客户,可连接到列表4中的程序,并且发送信息给它
//*************************************************************** // From the book "Win32 System Services: The Heart of Windows NT" // by Marshall Brain // Published by Prentice Hall file:// // Copyright 1994, by Prentice Hall. file:// // This code implements a simple named pipe sender. file://***************************************************************
// ssnpsend.cpp
// Usage: ssnpsend
#include #include
int main() { char *toSendtxt="Test String"; HANDLE ssnpPipe; DWORD NumBytesWritten; char machineName[80]; char pipeName[80];
cout << "Enter name of server machine: "; cin >> machineName; wsprintf(pipeName, "\\\\%s\\pipe\\ssnp",machineName);
/* Create the named pipe file handle for sending messages */ ssnpPipe=CreateFile(pipeName,GENERIC_WRITE, FILE_SHARE_READ, (LPSECURITY_ATTRIBUTES) NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, (HANDLE) NULL);
/* Check and see if the named pipe file was opened, if not terminate program */ if (ssnpPipe == INVALID_HANDLE_VALUE) { cerr << "ERROR: Unable to create a named pipe "<< endl; cerr << GetLastError() << endl; return (1); }
/* Repeatedly send message until program is terminated */ while(1) { cout << "Sending..." << endl; /* Write message to the pipe */ if (!WriteFile(ssnpPipe,toSendtxt, (DWORD) strlen(toSendtxt)+1, &NumBytesWritten, (LPOVERLAPPED) NULL)) { /* If error occurs when writing to named pipe, terminate program */ cerr << "ERROR: Unable to write to named pipe "<< GetLastError() << endl; CloseHandle(ssnpPipe); return (1); }
/* Wait before sending the message again */ Sleep(4800); } /* while*/ } |