1 相关概述

命名管道(Named Pipes) 是一种简单的进程间通信(IPC)机制。命名管道可以在同一台计算机的不同进程之间,或者跨越一个网络的不同计算机的不同进程之间的可靠的双向或单向的数据通信。

命名管道利用了微软网络提供者(MSNP)重定向器,因此无需涉及底层的通信协议等细节。命名管道是围绕windows文件系统设计的一种机制,采用“命名管道文件系统”(Named Pipe File System,NPFS)接口。因此,客户端和服务端均可以使用标准的WIN32文件系统API函数(如ReadFile和WriteFile)来进行数据的收发。

命名管道的命名规范遵循“通用命名规范(UNC)” :

\\server\pipe[\path]\name

  • 其中\\server 指定一个服务器的名字,如果是本机则用\\.表示,\\192.168.1.100表示网络上的服务器。

  • \pipe 是一个不可变化的“硬编码”字串(不区分大小写),用于指出该文件从属于NPFS

  • [\path]\name 则唯一标识一个命名管道的名称。

2 相关函数

2.1 服务端函数

2.1.1 CreateNamedPipe 创建命名管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*************************************************************************
Purpose :  创建命名管道,如果存在指定名字的管道,则创建该管道的一个实例
Input   :  lpName              --  管道名称
dwOpenMode          --  打开模式
dwPipeMode          --  消息模式
nMaxInstances       --  最大实例数(1-255)
nOutBufferSize      --  输出缓冲区长度,0表示用默认设置
nInBufferSize       --  输入缓冲区长度,0表示用默认设置
nDefaultTimeOut     --  管道的默认超时时间(毫秒),0表示默认超时时间50毫秒
lpSecurityAttributes--  安全描述符,如无特殊需求默认为0即可
Return  :  成功 -- 返回管道句柄 失败 -- 返回INVALID_HANDLE_VALUE 通过GetLastError()获取错误代码
Remark  :
*************************************************************************/
HANDLE WINAPI CreateNamedPipe(
_In_ LPCTSTR lpName,
_In_ DWORD dwOpenMode,
_In_ DWORD dwPipeMode,
_In_ DWORD nMaxInstances,
_In_ DWORD nOutBufferSize,
_In_ DWORD nInBufferSize,
_In_ DWORD nDefaultTimeOut,
_In_opt_  LPSECURITY_ATTRIBUTES lpSecurityAttributes
);

dwOpenMode 为下列常数组合

常数之一:

  • PIPE_ACCESS_DUPLEX 管道是双向的

  • PIPE_ACCESS_INBOUND 数据从客户端流到服务器端

  • PIPE_ACCESS_OUTBOUND 数据从服务器端流到客户端

常数之二:

  • FILE_FLAG_WRITE_THROUGH 在网络中建立的字节型管道内,强迫数据在每次读写操作的时候通过网络传输。否则传输会缓存导致延迟

  • FILE_FLAG_OVERLAPPED 允许(但不要求)用这个管道进行异步(重叠式)操作

dwPipeMode 为下列常数组合

常数之一:

  • PIPE_TYPE_BYTE 数据作为一个连续的字节数据流写入管道

  • PIPE_TYPE_MESSAGE 数据用数据块(名为“消息”或“报文”)的形式写入管道

常数之二:

  • PIPE_READMODE_BYTE 数据以单独字节的形式从管道中读出

  • PIPE_READMODE_MESSAGE 数据以名为“消息”的数据块形式从管道中读出(要求指定PIPE_TYPE_MESSAGE)

常数之三:

  • PIPE_WAIT 同步操作在等待的时候挂起线程

  • PIPE_NOWAIT 操作立即返回。这样可为异步传输提供一种落后的实现方法,已由Win32的重叠式传输机制取代了(不推荐!)

2.1.2 ConnectNamedPipe 等待客户连接

1
2
3
4
5
6
7
8
9
10
11
/*************************************************************************
Purpose :  等待客户连接管道
Input   :  hNamedPipe              --  创建管道的句柄,由CreateNamedPipe成功返回
lpOverlapped            --  打开模式
Return  :  TRUE -- 成功 FALSE -- 失败,通过GetLastError()获取错误码
Remark  :
*************************************************************************/
BOOL WINAPI ConnectNamedPipe(
_In_ HANDLE hNamedPipe,
_Inout_opt_  LPOVERLAPPED lpOverlapped
);

lpOverlapped 如设为NULL,表示将线程挂起,直到一个客户同管道连接为止。 否则就立即返回;此时,如管道尚未连接,客户同管道连接时就会触发lpOverlapped结构中的事件对象。随后,可用一个等待函数来监视连接。


2.2 客户端函数

2.2.1 CreateFile 连接到一个命名管道

2.2.2 WaitNamedPipe 等待管道实例是否可用

1
2
3
4
5
6
7
8
9
10
11
/*************************************************************************
Purpose :  等待管道实例是否可用
Input   :  lpNamedPipeName         --  管道名称
nTimeOut                --  等待时间
Return  :  TRUE -- 成功 FALSE -- 失败,通过GetLastError()获取错误码
Remark  :
*************************************************************************/
BOOL WINAPI WaitNamedPipe(
_In_ LPCTSTR lpNamedPipeName,
_In_ DWORD nTimeOut
);

2.3 管道收发数据函数

该函数同文件操作,不予过多介绍。

2.3.1 ReadFile 从管道读出数据

2.3.2 WriteFile 写数据到管道

3 服务端和客户端流程图


上图显示的为一个同步非阻塞的管道通信业务模型。具体过程如下所示:

Step1:服务端通过函数CreateNamedPipe() 创建管道实例。

Step2:服务端通过函数ConnectNamePipe() 等待客户端连接,此时服务端进入阻塞状态直到有客户端调用CreateFile连接管道成功。

Step3:客户端通过函数WaitNamePipe() 检测管道是否可用。当管道实例存在但不可用时,该函数阻塞,直到管道实例可用或者到超时时间才返回。如果没有管道实例,则该函数会立即返回错误,错误代码为2。 当服务端执行完CreateNamePipe函数时,该函数即可成功返回。

Step4:当客户端正确执行完WaitNamePipe后即可连接管道;MSDN上说即使WaitNamePipe当时返回成功,但是执行CreateFile 也有可能出错(会被其他进程等占用到时管道实例不可用)。所以需要后边设计了一段代码防止该情况的发生。当客户端连接成功后,服务端的ConnectNamePipe函数返回,执行第6步ReadFile进入阻塞状态,等待客户端数据写入。

Step5:当客户端成功连接管道后,即可进行通信,通过函数WriteFile写入管道数据,然后客户端调用ReadFile进入阻塞等待服务端写入数据。

Step6: 当客户端数据写入成功后,服务端程序会从ReadFile的阻塞中返回。

Step7:服务端对接收到的数据进行业务处理。

Step8:服务端对处理后的数据写入管道。

Step9:客户端ReadFile函数从阻塞中成功返回,接收到服务端处理的数据。

Step10:客户端调用CloseHandle断开管道连接,通信结束。

4 编程实现

4.1 简单实现

4.1.1 服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//服务端测试程序
void SrvTest()
{
HANDLE hSrvNamePipe;
char szPipeName[MAX_PATH] = {0};
char szReadBuf[MAX_BUFFER] = {0};
char szWritebuf[MAX_BUFFER] = {0};
DWORD dwNumRead = 0;
DWORD dwNumWrite = 0;
strcpy (szPipeName, "\\\\.\\pipe\\myTestPipe" );
//step1:创建管道实例
hSrvNamePipe = CreateNamedPipeA( szPipeName,
PIPE_ACCESS_DUPLEX|FILE_FLAG_WRITE_THROUGH,
PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES, 0, 0, 0, NULL);
if ( INVALID_HANDLE_VALUE == hSrvNamePipe )
{
WriteLog( "CreateNamedPipeA err[%#x]" , GetLastError());
return ;
}
WriteLog( "CreateNamedPipe succ..." );
//step2:等待客户端连接
BOOL bRt= ConnectNamedPipe(hSrvNamePipe, NULL );
if ( false ==bRt && GetLastError() != ERROR_PIPE_CONNECTED )
{
WriteLog( "等待客户端连接失败,[%#x]" , GetLastError());
return ;
}
WriteLog( "收到客户端的连接成功..." );
//step3:接收数据
memset ( szReadBuf, 0, MAX_BUFFER );
bRt = ReadFile( hSrvNamePipe, szReadBuf, MAX_BUFFER-1, &dwNumRead, NULL );
if ( !bRt || dwNumRead == 0 )
{
bRt = GetLastError();
if (bRt == ERROR_BROKEN_PIPE)
{
WriteLog( "客户已关闭链接" );
return ;
}
else
{
WriteLog( "读取客户数据失败!,GetLastError=%d" , GetLastError() );
return ;
}
}
WriteLog( "收到客户数据:[%s]" , szReadBuf);
//step4:业务逻辑处理 (只为测试用返回原来的数据)
//step5:发送数据
if ( !WriteFile( hSrvNamePipe, szReadBuf, dwNumRead, &dwNumWrite, NULL ) )
{
WriteLog( "向客户写入数据失败:[%#x]" , GetLastError());
return ;
}
WriteLog( "写入数据成功..." );
}
int main( int argc, char * argv[])
{
SrvTest();
system ( "pause" );
return 0;
}

4.1.2 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//客户端测试程序
void ClientTest()
{
char szPipeName[MAX_PATH] = {0};
HANDLE hPipe;
DWORD dwRet;
char szReadBuf[MAX_BUFFER] = {0};
char szWritebuf[MAX_BUFFER] = {0};
DWORD dwNumRead = 0;
DWORD dwNumWrite = 0;
strcpy (szPipeName, "\\\\.\\pipe\\myTestPipe" );
//step1:检测管道是否可用
if (!WaitNamedPipeA(szPipeName, 10000))
{
WriteLog( "管道[%s]无法打开" , szPipeName);
return ;
}
//step2:连接管道
hPipe = CreateFileA(szPipeName,
GENERIC_READ|GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (INVALID_HANDLE_VALUE == hPipe)
{
//成功
WriteLog( "连接管道失败[%#x]" , GetLastError());
return ;
}
WriteLog( "管道连接成功..." );
printf ( "请输入要发送的数据:" );
scanf ( "%s" , szWritebuf );
//step3:发送数据
if ( !WriteFile( hPipe, szWritebuf, strlen (szWritebuf), &dwNumWrite, NULL ))
{
WriteLog( "发送数据失败,GetLastError=[%#x]" , GetLastError());
return ;
}
printf ( "发送数据成功:%s\n" , szWritebuf );
//step4:接收数据
if ( !ReadFile( hPipe, szReadBuf, MAX_BUFFER-1, &dwNumRead, NULL ) )
{
WriteLog( "接收数据失败,GetLastError=[%#x]" , GetLastError() );
return ;
}
WriteLog( "接收到服务器返回:%s" , szReadBuf );
//step5:关闭管道
CloseHandle(hPipe);
}
int main( int argc, char * argv[])
{
ClientTest();
system ( "pause" );
return 0;
}

4.1.3 运行结果


到此,一个简单的利用命名管道的服务端和客户端的程序设计完成。

4.2 字节模式和消息模式

首先要弄清楚管道的字节模式和消息模式:

两者没有太大的区别,只是在字节模式的时候是以字节流接收与发送的。每次可以发送不同的字节数,在没有把发送的数据收走(用ReadFile)前也可以再次发送,只要没有超过管道的默认的缓冲区大小 。其实也可以说是我们用的是PIPE_WAIT,所以会是在阻塞模式,也就是说会让数据直接发送到了服务器端的缓冲区中,虽然sever端没有调用ReadFile来读取数据,其实已经发到那一边去了,它不读出来没有关系,我们客户端还是可以继续发送数据的。而且对每次发送的数据大小没有限制,不会说每次都得发送固定大小的数据 。读的那一端也会把接收到的数据作为一个无机的整体,也就是说是没有结构的二进制流。读的时候可以想读出任意的字节数都可以的。如一次发送了5个字节,然后再发送了40个字节 ,服务器端可以一次性全部读出45个字节 也可以每次读出一个字节 。以及小于45的任意字节。

而对于消息模式的话,对于写的大小与次数也没有限制。只是说若是以消息模式写的,则会发给服务器的时候说,这是一个消息,若你用消息模式读的话不能只读其中的一部分。你得全部都读走,若有一个消息发到了服务器端,10个字节。现在你读了8字节(用ReadFile函数)会返回错误,错误代码为ERRORM_More_data.也就是说即使没有超过缓冲区中已有的数据,你没有读够也会不让你读成功。

注意:以消息模式发送的数据,在服务器端 是可以用字节模式接收的,如上所说。若是这样的话也会把有格式的消息化为没有格式的字节流。 但是,不能以消息模式来读取以字节模式发送的消息。

以上内容转载自: http://blog.sina.com.cn/s/blog_71b3a9690100usem.html

代码中采用字节模式,因此为了处理完整数据,采用的数据格式为: LEN+DATA 其中LEN为4字节DWORD类型表示数据的长度(不包含自身长度)

读管道数据代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*************************************************************************
Purpose :  读取管道数据
Input   :  hPipe           --  [IN] 管道句柄
pbData          --  [OUT] 读出的数据
pdwDataLen      --  [OUT] 读出数据长度
Return  :  0 -- 成功 其他 -- 失败,返回对应的错误码
Modify  :
Remark  :
*************************************************************************/
DWORD ReadPipeData( HANDLE hPipe, BYTE * pbData, DWORD * pdwDataLen)
{
BOOL bRet;
DWORD dwRet;
BYTE bTemp[1024] = {0};
DWORD dwLen = 0;
DWORD dwTempLen = 0;
BYTE *   p = NULL;
//先读取长度
bRet = ReadFile(hPipe, bTemp, sizeof ( DWORD ), &dwLen, NULL);
if ( !bRet || dwLen == 0)
{
dwRet = GetLastError();
if (dwRet == ERROR_BROKEN_PIPE)
{
WriteLog( "客户端已关闭链接" );
return dwRet;
}
else
{
WriteLog( "读取客户端数据失败,[%#x]" , dwRet);
return dwRet;
}
}
if (dwLen != sizeof ( DWORD ))
{
WriteLog( "接收数据长度不正确,recv len[%d]" , dwLen);
return 1;
}
memcpy (&dwTempLen, bTemp, sizeof ( DWORD ));
//读取数据
p = pbData;
dwLen = 0;
*pdwDataLen = 0;
while (dwTempLen)
{
bRet = ReadFile(hPipe, p+dwLen, dwTempLen, &dwLen, NULL );
if ( !bRet || dwLen == 0)
{
dwRet = GetLastError();
if (dwRet == ERROR_BROKEN_PIPE)
{
WriteLog( "客户端已关闭链接" );
return dwRet;
}
else
{
WriteLog( "读取数据失败,[%#x]" , dwRet);
return dwRet;
}
}
dwTempLen -= dwLen;
p += dwLen;
*pdwDataLen += dwLen;
}
return 0;
}

写管道数据代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*************************************************************************
Purpose :  写入管道数据
Input   :  hPipe           --  [IN] 管道句柄
pbData          --  [IN] 写入的数据
dwDataLen       --  [IN] 写入数据长度
Return  :  0 -- 成功 其他 -- 失败,返回对应的错误码
Modify  :
Remark  :
*************************************************************************/
DWORD WritePipeData( HANDLE hPipe, BYTE * pbData, DWORD dwDataLen)
{
BOOL bRet;
DWORD dwRet;
BYTE bTemp[1024] = {0};
DWORD dwTempLen = 0;
BYTE *       p = NULL;
DWORD dwNumWrite = 0;
DWORD dwTotleLen = 0;
p = bTemp;
memcpy (p, &dwDataLen, sizeof ( DWORD ));
p += sizeof ( DWORD );
memcpy (p, pbData, dwDataLen);
dwTotleLen = dwDataLen+ sizeof ( DWORD );
p = bTemp;
while (dwTotleLen)
{
bRet = WriteFile(hPipe, p, dwTotleLen, &dwNumWrite, NULL );
if (!bRet || dwNumWrite == 0)
{
dwRet = GetLastError();
WriteLog( "WriteFile err[%#x]" , dwRet);
return dwRet;
}
dwTotleLen -= dwNumWrite;
p += dwNumWrite;
}
return 0;
}

后续用这两个函数即可完成完整的收发数据操作。

4.3 多客户端管道通信

因为是服务端和客户端的通信问题,所以存在多个客户端同时访问一个服务端的问题,对于管道通信也不例外。

设计思路为服务端创建一个管道实例,每来一个客户端连接,服务端就会创建一个线程去处理该客户端的业务,然后再次创建一个该管道的实例,等待客户端连接进来。

这里用客户端用多线程代替多进程进行实验。

服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
void SrvTest_2()
{
HANDLE hSrvNamePipe;
char szPipeName[MAX_PATH] = {0};
int nClientCount = 0;
int nRunning = 1;
strcpy (szPipeName, "\\\\.\\pipe\\myTestPipe" );
while (nRunning)
{
//step1:创建管道实例
hSrvNamePipe = CreateNamedPipeA( szPipeName,
PIPE_ACCESS_DUPLEX|FILE_FLAG_WRITE_THROUGH,
PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES, 0, 0, 0, NULL);
if ( INVALID_HANDLE_VALUE == hSrvNamePipe )
{
WriteLog( "CreateNamedPipeA err[%#x]" , GetLastError());
return ;
}
WriteLog( "CreateNamedPipe succ..." );
//step2:等待客户端连接
BOOL bRt= ConnectNamedPipe(hSrvNamePipe, NULL );
if ( false ==bRt && GetLastError() != ERROR_PIPE_CONNECTED )
{
WriteLog( "等待客户端连接失败,[%#x]" , GetLastError());
return ;
}
WriteLog( "收到第[%d]个客户端的连接成功" , nClientCount);
//step3:创建工作线程与此客户端通信
CLIENTINFO *pNewClient= new CLIENTINFO;
pNewClient->nClinetNum= nClientCount;
pNewClient->hPipe     = hSrvNamePipe;
HANDLE hWorkThread= CreateThread( NULL, 0, WorkThreadProc, pNewClient, 0, NULL);
if ( NULL == hWorkThread )
{
WriteLog( "创建工作线程[%d]失败,[%#x]" , nClientCount, GetLastError());
break ;
}
nClientCount++;
if (nClientCount > 32767)
{
nClientCount = 0;
}
}
}

线程处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*************************************************************************
Purpose :  开启单独线程处理客户端消息
Input   :  pArg            --  [IN] 客户端结构体
Return  :  0 -- 成功 其他 -- 失败,返回对应的错误码
Modify  :
Remark  :
*************************************************************************/
ULONG WINAPI WorkThreadProc( void * pArg)
{
CLIENTINFO*     pClientInfo= (CLIENTINFO*)pArg;
HANDLE hPipe= pClientInfo->hPipe;
int nNum= pClientInfo->nClinetNum;
DWORD dwRet;
BYTE bReadBuf[MAX_BUFFER] = {0};
BYTE bWritebuf[MAX_BUFFER] = {0};
DWORD dwReadLen = 0;
DWORD dwWriteLen = 0;
//step1:接收数据
dwRet = ReadPipeData(pClientInfo->hPipe, bReadBuf, &dwReadLen);
if (dwRet)
{
if (ERROR_BROKEN_PIPE != dwRet)
{
WriteLog( "客户端[%d] ReadPipeData err[%#x]" , nNum, dwRet);
}
goto stop;
}
WriteLog( "客户端[%d] 接收数据[%s]" , nNum, ( char *)bReadBuf);
//step2:处理数据
memcpy (bWritebuf, bReadBuf, dwReadLen);
dwWriteLen = dwReadLen;
//step3:发送数据
dwRet = WritePipeData(pClientInfo->hPipe, bWritebuf, dwWriteLen);
if (dwRet)
{
WriteLog( "客户端[%d] WritePipeData err[%#x]" , nNum, dwRet);
goto stop;
}
WriteLog( "客户端[%d] 发送数据[%s]" , nNum, ( char *)bWritebuf);
stop:
//step4:关闭管道
FlushFileBuffers(hPipe);
DisconnectNamedPipe(hPipe);
CloseHandle( hPipe);
WriteLog( "客户端[%d] 线程退出" , nNum);
return 0;
}

服务端用到一个while大循环,不断接收客户端的连接,接收到的连接都通过new新线程来处理数据及业务逻辑。

客户端代码:(用到多线程代替多进程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
void Tread_Proc3( LPVOID lpParameter)
{
char szPipeName[MAX_PATH] = {0};
HANDLE hPipe;
DWORD dwRet;
int nID = ( int )lpParameter;
char szReadBuf[MAX_BUFFER] = {0};
char szWritebuf[MAX_BUFFER] = {0};
DWORD dwNumRead = 0;
DWORD dwNumWrite = 0;
WriteLog( "线程[%d]开始..." , nID);
strcpy (szPipeName, "\\\\.\\pipe\\myTestPipe" );
//step1:检测管道是否可用
if (!WaitNamedPipeA(szPipeName, 10000))
{
WriteLog( "线程[%d] 管道[%s]无法打开" , nID, szPipeName);
return ;
}
//step2:连接管道
hPipe = CreateFileA(szPipeName,
GENERIC_READ|GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (INVALID_HANDLE_VALUE == hPipe)
{
//成功
WriteLog( "线程[%d]连接管道失败[%#x]" , nID, GetLastError());
return ;
}
WriteLog( "线程[%d] 管道连接成功..." , nID);
sprintf (szWritebuf, "THREADDATA--[%d]" , nID);
//step3:发送数据
dwRet = WritePipeData(hPipe, ( PBYTE )szWritebuf, strlen (szWritebuf)+1);
if (dwRet)
{
WriteLog( "线程[%d] 发送数据失败,[%#x]" , nID, dwRet);
return ;
}
WriteLog( "线程[%d] 发送数据成功:%s" , nID, szWritebuf );
//step4:接收数据
dwRet = ReadPipeData(hPipe, ( PBYTE )szReadBuf, &dwNumRead);
if (dwRet)
{
WriteLog( "线程[%d] 接收数据失败,[%#x]" , nID, dwRet);
return ;
}
WriteLog( "线程[%d] 接收到服务器返回:%s" , nID, szReadBuf );
//step5:关闭管道
CloseHandle(hPipe);
WriteLog( "线程[%d] 结束..." , nID);
}
//客户端多线程测试
void ClientTest_3()
{
HANDLE m_hThreadSendReadData[MAX_THREAD_NUM];
for ( int i=0;i<MAX_THREAD_NUM;++i)
{
m_hThreadSendReadData[i] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Tread_Proc3,( LPVOID )i,0,NULL);
}
WaitForMultipleObjects(MAX_THREAD_NUM, m_hThreadSendReadData, TRUE, INFINITE);
}

MAX_THREAD_NUM为1时,程序能正常运行。

当线程数为10个时,程序出错,如下图所示:

出现的错误代码为0xE7 ,通过ERRORLOOKUP查询原因为: 所有的管道范例都在使用中 。根据该错误代码猜想,服务端在接收到一个连接并在创建新的管道实例之前,有客户端进行了连接请求导致没有一个可用的管道实例可用。通过如下修改客户端代码验证了该问题:(在每个线程启动时加入sleep),经过验证,程序正确运行。

1
2
3
4
5
6
7
8
9
10
11
//客户端多线程测试
void ClientTest_3()
{
HANDLE m_hThreadSendReadData[MAX_THREAD_NUM];
for ( int i=0;i<MAX_THREAD_NUM;++i)
{
m_hThreadSendReadData[i] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Tread_Proc3,( LPVOID )i,0,NULL);
Sleep(10);
}
WaitForMultipleObjects(MAX_THREAD_NUM, m_hThreadSendReadData, TRUE, INFINITE);
}

因为我们不能保证客户端是否同一时刻请求连接,所以修改连接代码部分为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*************************************************************************
Purpose :  客户端连接管道
Input   :  phPipe          -- [OUT] 管道句柄
Return  :  0 -- 成功 其他 -- 失败,返回对应的错误码
Modify  :
Remark  :
*************************************************************************/
DWORD Pipe_ConnSrv( HANDLE * phPipe)
{
int i;
DWORD dwRet = 0;
char szPipeName[MAX_PATH] = {0};
strcpy (szPipeName, "\\\\.\\pipe\\myTestPipe" );
//连接管道
for (i=0;i<10;i++)
{
*phPipe = CreateFileA(szPipeName,
GENERIC_READ|GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (INVALID_HANDLE_VALUE != *phPipe)
{
//成功
WriteLog( "连接管道成功" );
break ;
}
dwRet = GetLastError();
if (dwRet != ERROR_PIPE_BUSY)
{
WriteLog( "连接管道失败,err[%#x]" , dwRet);
return dwRet;
}
WriteLog( "管道正忙..." );
//等待1s
if (!WaitNamedPipeA(szPipeName, 1000))
{
WriteLog( "管道[%s]无法打开" , szPipeName);
return 1;
}
}
return 0;
}

为了验证不同管道实例间数据是否有影响,在客户端发送的数据中加入了线程编号,同时对于客户端和服务端收发数据做了while循环(每个线程都不停的收发数据)。结果证明不同管道实例相互不受影响。

注:MSDN上说创建的实例个数1-255,当我把客户端线程开启到260的时候,程序没报错,暂不明白这个实例怎么理解的。