Windows网络编程笔记3 ---- 邮槽和命名管道
邮槽和命名管道的使用方法也很简单,只需几个有限的函数就可以实现双方的通信。
第三、邮槽
邮槽----进程间通信机制。
通过邮槽客户进程可以将消息通过广播给一个或多个服务进程。这是一个单向通信机制,缺点是只允许从客户机到服务器,优点也是这个原理,使客户机应用能够非常容易地将广播消息发送给一个或多个服务器应用。邮槽是一种无连接方式,是一种”不可靠“的数据传输。
邮槽名也使用UNC路径,第二个关键字是Mailslot,不可改变
\\\\server\\Mailslot\\[path]name
服务器实现过程:
CreateMailslot();//创建一个邮槽句柄
ReadFile();//接受任何客户机的数据
CloseHandle();//关闭邮槽句柄
服务器端邮槽实现
1 //server1.cpp 2 //邮槽的实现 3 #include "windows.h" 4 #include "winbase.h" 5 #include "stdio.h" 6 7 void main() 8 { 9 HANDLE Mailslot; 10 char buffer[256]; 11 DWORD NumberOfBytesRead; 12 13 //创建邮槽 14 if ((Mailslot = CreateMailslot("\\\\.\\Mailslot\\Myslot",0,MAILSLOT_WAIT_FOREVER,NULL)) 15 == INVALID_HANDLE_VALUE) 16 { 17 printf("Failed to create a mailslot %d\n",GetLastError()); 18 getchar(); 19 return ; 20 } 21 22 //从邮槽中读取数据,只有服务器才能从邮槽中读取数据 23 while(ReadFile(Mailslot,buffer,256,&NumberOfBytesRead,NULL) != 0) 24 { 25 printf("%.*s\n",NumberOfBytesRead,buffer); 26 } 27 if (!Mailslot) 28 { 29 CloseHandle(Mailslot); 30 } 31 }
客户端实现过程
CreateFile();//打开指向邮槽的句柄
WriteFile();//写入数据
CloseHandle();//关闭句柄
客户端邮槽代码实现
1 //client.cpp 2 //邮槽客户端 3 4 #include "windows.h" 5 #include "stdio.h" 6 7 void main() 8 { 9 HANDLE Mailslot; 10 DWORD ByteWritten; 11 CHAR ServerName[256]; 12 13 if((Mailslot=CreateFile("\\\\.\\Mailslot\\Myslot",GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIB UTE_NORMAL,NULL)) == INVALID_HANDLE_VALUE)//这里就是邮槽的创建 14 { 15 printf("Createfile failed with error %d\n",GetLastError()); 16 getchar(); 17 return ; 18 } 19 20 if (WriteFile(Mailslot,"This is a test",15,&ByteWritten,NULL) == 0)//写入数据 21 { 22 printf("WriteFile failed with error %d\n",GetLastError()); 23 getchar(); 24 return ; 25 } 26 27 printf("Write %d bytes\n",ByteWritten); 28 getchar(); 29 CloseHandle(Mailslot); 30 }
下面在服务器上添加线程,用于自己终止程序运行,而不是让其一直处于挂起状态。这种方式是为了避免服务器在ReadFile()时服务器程序因某种原因而终止,此时ReadFile()没有完成,程序会一直处于挂起状态,直到有数据可读。
服务器端可以自己终止接受数据
1 //server2.cpp 2 //邮槽的线程实现 3 #include "windows.h" 4 #include "winbase.h" 5 #include "stdio.h" 6 #include "conio.h" 7 8 BOOL StopProcessing; 9 10 DWORD WINAPI ServerMailslot(LPVOID lpParameter); 11 void SendMessageToMailslot(void);//自己向邮槽发送数据 12 13 void main() 14 { 15 HANDLE MailslotThread; 16 DWORD ThreadID; 17 StopProcessing = FALSE; 18 MailslotThread = CreateThread(NULL,0,ServerMailslot,NULL,0,&ThreadID); 19 20 printf("Press a key to stop the server\n"); 21 _getch(); 22 23 //按下按键以后,赋值为TRUE,在线程中终止程序运行 24 StopProcessing = TRUE; 25 26 //发送消息,之后线程会进入while()循环,并且会终止循环 27 SendMessageToMailslot();//自己向邮槽发送数据 28 29 // 30 if (WaitForSingleObject(MailslotThread,INFINITE) == WAIT_FAILED) 31 { 32 printf("WaitForSingleObject Failed with error %d\n",GetLastError()); 33 getchar(); 34 return ; 35 } 36 } 37 38 //线程入口函数 39 DWORD WINAPI ServerMailslot(LPVOID lpParameter) 40 { 41 char buffer[2048]; 42 DWORD NumberOfBytesRead; 43 DWORD Ret; 44 HANDLE Mailslot; 45 46 if ((Mailslot = CreateMailslot("\\\\.\\mailslot\\myslot",2048,MAILSLOT_WAIT_FOREVER,NULL)) 47 == INVALID_HANDLE_VALUE) 48 { 49 printf("Failed to create a mailslot %d\n",GetLastError()); 50 getchar(); 51 return 0; 52 } 53 54 while ((Ret = ReadFile(Mailslot,buffer,2048,&NumberOfBytesRead,NULL)) != 0) 55 { 56 if(StopProcessing) 57 break; 58 printf("Receive %d bytes \n",NumberOfBytesRead); 59 } 60 CloseHandle(Mailslot); 61 return 0; 62 } 63 64 //发送终止信息 65 void SendMessageToMailslot() 66 { 67 HANDLE Mailslot; 68 DWORD BytesWritten; 69 if ((Mailslot = CreateFile("\\\\.\\mailslot\\myslot",GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING 70 ,FILE_ATTRIBUTE_NORMAL,NULL)) == INVALID_HANDLE_VALUE) 71 { 72 printf("CreateFile Failed with error %d\n",GetLastError()); 73 getchar(); 74 return ; 75 } 76 if (WriteFile(Mailslot,"STOP",4,&BytesWritten,NULL) == 0) 77 { 78 printf("WriteFile Failed with error %d\n",GetLastError()); 79 getchar(); 80 return ; 81 } 82 CloseHandle(Mailslot); 83 }
这样可以避免服务器进入无限的等待。
邮槽总结:
使用邮槽,应用程序可以在Windows重定向器的帮助下,实现简单的单向进程间数据通信。对邮槽来说,它最有价值的一项功能便是通过网络,将一条消息广播给一台或多台计算机。然而,邮槽并未提供对数据可靠传输的保障。是一种不可靠的数据传输。
第四、命名管道
命名管道实际上建立一个简单的客户机/服务器数据通信体系,可在其中可靠地传输数据。
规则:
命名管道的标识是采用 U N C格式进行的:\ \ server\pipe\ [ path ]name
其中的pipe是一个标记,不能改变,不区分大小写。例子如下:
\\\\ myserver\\pipe\\mypipe
通信方式
命名管道提供了两种基本通信模式:字节模式和消息模式。
在字节模式中,消息以一个连续的字节流的形式,在客户机与服务器之间流动。在一方写入某个数量的字节,并不表示在另一方会读出等量的字节。这样一来,客户机和服务器在传输数据的时候,便不必关心数据的内容。
在消息模式中,客户机和服务器则通过一系列不连续的数据单位,进行数据的收发。每次在管道上发出了一条消息后,它必须作为一条完整的消息读入。
服务器与客户机的区别
命名管道的最大特点就是建立了一个基于服务器/客户机的程序设计体系。在这个体系结构中,数据既可以单向流动,也可以双向流动。但是服务器是唯一一个有权利创建命名管道的进程,也只有它有权利接受来自客户端的链接请求。
服务器的实现过程
CreateNamedPipe();//创建命名管道实例句柄
ConnectNamedPipe();//监听来自客户机的链接请求
ReadFile(),WriteFile();//读写数据
DisconnectNmaePipe();//关闭命名通道连接
CloseHandle();//关闭命名管道实例句柄
客户端实现过程
WaitNamedPipe();// 等候一个命名管道实例可供自己使用
CreateFile();// 建立与命名管道的连接
WriteFile(); ,ReadFile();//读写数据
CloseHandle();// 关闭命名管道会话
简单命名管道的实现
1 //命名管道的实现 2 3 #include "windows.h" 4 #include "stdio.h" 5 6 void main() 7 { 8 HANDLE PipeHnadle; 9 DWORD BytesRead; 10 CHAR buffer[256]; 11 12 if ((PipeHnadle = CreateNamedPipe("\\\\.\\Pipe\\Song",PIPE_ACCESS_DUPLEX,PIPE_TYPE_BYTE | PIPE_READMODE_BYTE 13 ,1,0,0,1000,NULL)) == INVALID_HANDLE_VALUE)//创建 14 { 15 printf("CreateNamedPipe failed with error %d\n",GetLastError()); 16 getchar(); 17 return ; 18 } 19 printf("Server is now running!\n"); 20 21 if (ConnectNamedPipe(PipeHnadle,NULL) == 0)//连接客户端 22 { 23 printf("ConnectNamedPipe failed with error %d\n",GetLastError()); 24 CloseHandle(PipeHnadle); 25 getchar(); 26 return ; 27 } 28 29 if (ReadFile(PipeHnadle,buffer,sizeof(buffer),&BytesRead,NULL) <= 0)//读取数据 30 { 31 printf("ReadFile failed with error %d\n",GetLastError()); 32 CloseHandle(PipeHnadle); 33 getchar(); 34 return ; 35 } 36 37 printf("%.*s\n",BytesRead,buffer); 38 39 if (DisconnectNamedPipe(PipeHnadle) == 0)//关闭 40 { 41 printf("DisconnectNamedPipe failed with error %d\n",GetLastError()); 42 getchar(); 43 return ; 44 } 45 46 CloseHandle(PipeHnadle); 47 getchar(); 48 getchar(); 49 }
同时控制多个管道实例,命名管道可以实现多个实例的连接。这个数量有函数CreateNamedPipe()控制
1 HANDLE WINAPI CreateNamedPipe( 2 _In_ LPCTSTR lpName,//管道名称 3 _In_ DWORD dwOpenMode,//打开模式,如PIPE_ACCESS_DUPLES 双向管道,PIPE_FLAG_OVEERLAPPED(重叠I/O) 4 _In_ DWORD dwPipeMode,//管道模式,字节流还是信息流 5 _In_ DWORD nMaxInstances,//最大连接实例数量 6 _In_ DWORD nOutBufferSize,//输出缓冲区大小 7 _In_ DWORD nInBufferSize,//输入缓冲区大小 8 _In_ DWORD nDefaultTimeOut,//超时设置 9 _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes//安全描述符 10 );
多管道实现,使用线程
1 //命名管道的实现,多线程 2 3 #include "windows.h" 4 #include "stdio.h" 5 #include "conio.h" 6 7 #define NUM_PIPES 5 8 9 DWORD WINAPI PipeInstanceProc(LPVOID lpParameter); 10 void main() 11 { 12 HANDLE ThreadHandle; 13 INT i; 14 DWORD ThreadID; 15 16 for(i = 0 ; i < NUM_PIPES ;i ++ ) 17 { 18 //创建线程保存管道实例 19 if ((ThreadHandle = CreateThread(NULL,0,PipeInstanceProc, 20 NULL,0,&ThreadID)) == NULL) 21 { 22 printf("CreateThread failed with error %d \n",GetLastError()); 23 getchar(); 24 return ; 25 } 26 CloseHandle(ThreadHandle); 27 } 28 29 printf("Press any key to stop the server!\n"); 30 _getch(); 31 } 32 33 //入口函数 34 35 DWORD WINAPI PipeInstanceProc(LPVOID lpParameter) 36 { 37 HANDLE PipeHandle; 38 DWORD BytesRead; 39 DWORD BytesWritten; 40 CHAR buffer[256]; 41 42 if ((PipeHandle = CreateNamedPipe("\\\\.\\Pipe\\Song",PIPE_ACCESS_DUPLEX,PIPE_TYPE_BYTE | PIPE_READMODE_BYTE 43 ,NUM_PIPES,0,0,1000,NULL)) == INVALID_HANDLE_VALUE) 44 { 45 printf("CreateNamedPipe failed with error %d \n",GetLastError()); 46 getchar(); 47 return 0; 48 } 49 //一直尝试连接客户端 50 while (true) 51 { 52 if (ConnectNamedPipe(PipeHandle,NULL) == 0) 53 { 54 printf("ConnectNamedPipe failed with error %d \n",GetLastError()); 55 getchar(); 56 break; 57 } 58 //读取数据 59 while (ReadFile(PipeHandle,buffer,sizeof(buffer),&BytesRead,NULL) > 0) 60 { 61 printf("Echo %d bytes to client \n",BytesRead); 62 if (WriteFile(PipeHandle,buffer,BytesRead,&BytesWritten,NULL) == 0) 63 { 64 printf("WriteFile failed with error %d \n",GetLastError()); 65 getchar(); 66 break; 67 } 68 } 69 if (DisconnectNamedPipe(PipeHandle) == 0) 70 { 71 printf("DisconnectNamedPipe failed with error %d \n",GetLastError()); 72 getchar(); 73 break; 74 } 75 } 76 CloseHandle(PipeHandle); 77 return 0; 78 }
最后看一下基于重叠I/O模式的管道通信
这个重叠I/O的设置在CreateNamedPipe();的第二个参数 dwOpenMode,只需在里面包含 PIPE_FLAG_OVEERLAPPED就行。
1 //overlapped_server.cpp 2 3 //重叠I/O方式是下命名管道 4 5 #include "windows.h" 6 #include "stdio.h" 7 8 #define NUM_PIPES 5 9 #define BUFFER_SIZE 256 10 11 void main() 12 { 13 HANDLE PipeHandles[NUM_PIPES]; 14 DWORD BytesTransferred; 15 CHAR buffer[NUM_PIPES][BUFFER_SIZE]; 16 int i; 17 OVERLAPPED ovlap[NUM_PIPES]; 18 HANDLE Event[NUM_PIPES]; 19 20 ////////////////////////////////////////////////////////////////////////// 21 22 BOOL DataRead[NUM_PIPES]; 23 DWORD Ret; 24 DWORD Pipe; 25 26 for (i = 0; i < NUM_PIPES ; i ++) 27 { 28 //创建命名管道实例 29 if ((PipeHandles[i] = CreateNamedPipe("\\\\.\\pipe\\Song",PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, 30 PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,NUM_PIPES,0,0,1000,NULL)) == INVALID_HANDLE_VALUE) 31 { 32 printf("CreateNamedPipe for pipe %d failed with error %d\n",i,GetLastError()); 33 getchar(); 34 return ; 35 } 36 //创建事件 37 if ((Event[i] = CreateEvent(NULL,TRUE,FALSE,NULL)) == NULL) 38 { 39 printf("CreateEvent for pipe %d failed with error %d\n",i,GetLastError()); 40 getchar(); 41 continue ; 42 } 43 //保存事件状态 44 DataRead[i] = FALSE; 45 ZeroMemory(&ovlap[i],sizeof(OVERLAPPED)); 46 47 //监听事件 48 if (ConnectNamedPipe(PipeHandles[i],&ovlap[i]) == 0) 49 { 50 if (GetLastError() != ERROR_IO_PENDING) 51 { 52 printf("ConnectNamedPipe for pipe %d failed with error %d\n",i,GetLastError()); 53 CloseHandle(PipeHandles[i]); 54 getchar(); 55 return ; 56 } 57 } 58 } 59 60 // 61 printf("Server is running!\n"); 62 ////////////////////////////////////////////////////////////////////////// 63 64 //读取数据 65 while (true) 66 { 67 if ((Ret = WaitForMultipleObjects(NUM_PIPES,Event,FALSE,INFINITE)) == WAIT_FAILED) 68 { 69 printf("WaitForMultipleObjects failed with error %d\n",GetLastError()); 70 getchar(); 71 return ; 72 } 73 Pipe = Ret - WAIT_OBJECT_0; 74 ResetEvent(Event[Pipe]); 75 76 //检查I/O状态,如果失败,就断开连接并重新尝试读取数据 77 if (GetOverlappedResult(PipeHandles[Pipe],&ovlap[Pipe],&BytesTransferred,TRUE) == 0) 78 { 79 printf("GetOverlappedResult failed with error %d\n",GetLastError()); 80 //断开连接 81 if (DisconnectNamedPipe(PipeHandles[Pipe]) == 0) 82 { 83 printf("DisconnectNamedPipe failed with error %d\n",GetLastError()); 84 return ; 85 } 86 if (ConnectNamedPipe(PipeHandles[Pipe],&ovlap[pipe]) == 0) 87 { 88 if (GetLastError() != ERROR_IO_PENDING) 89 { 90 //服务器出错,关闭句柄 91 printf("ConnectNamedPipe for pipe %d failed with error %d\n",i,GetLastError()); 92 CloseHandle(PipeHandles[Pipe]); 93 } 94 } 95 96 DataRead[Pipe] = FALSE; 97 } 98 else 99 { 100 //如果管道上有数据就读取并发送客户端,如果没有就一直尝试读取 101 if (DataRead[Pipe] == FALSE) 102 { 103 ZeroMemory(&ovlap[Pipe],sizeof(OVERLAPPED)); 104 ovlap[Pipe].hEvent = Event[Pipe]; 105 106 if (ReadFile(PipeHandles[Pipe],buffer[Pipe],BUFFER_SIZE,NULL,&ovlap[Pipe]) == 0) 107 { 108 if (GetLastError() != ERROR_IO_PENDING) 109 { 110 printf("ReadFile failed with error %d\n",GetLastError()); 111 112 } 113 } 114 DataRead[Pipe] = TRUE; 115 } 116 else 117 { 118 //向管道写入数据 119 printf("Received %d bytes ,echo bytes back \n",BytesTransferred); 120 ZeroMemory(&ovlap[Pipe],sizeof(OVERLAPPED)); 121 ovlap[Pipe].hEvent = Event[Pipe]; 122 123 if (WriteFile(PipeHandles[Pipe],buffer[Pipe],BUFFER_SIZE,NULL,&ovlap[Pipe]) == 0) 124 { 125 if (GetLastError() != ERROR_IO_PENDING) 126 { 127 printf("WriteFile failed with error %d\n",GetLastError()); 128 129 } 130 } 131 DataRead[Pipe] = FALSE; 132 } 133 134 } 135 } 136 }
再看一下客户端的实现
1 //client3.cpp 2 3 //简单命名管道客户机 4 5 6 #include "windows.h" 7 #include "stdio.h" 8 #include "conio.h" 9 10 #define PIPE_NAME "\\\\.\\pipe\\Song" 11 12 void main() 13 { 14 15 HANDLE PipeHandle; 16 DWORD BytesWritten; 17 DWORD BytesRead; 18 CHAR buffer[256]; 19 memset(&buffer,0,sizeof(buffer)); 20 //等待可用的命名管道 21 if (WaitNamedPipe(PIPE_NAME,NMPWAIT_WAIT_FOREVER) == 0) 22 { 23 printf("WaitNamedPipe failed with error %d \n",GetLastError()); 24 getchar(); 25 return ; 26 } 27 //创建命名管道句柄 28 29 if ((PipeHandle = CreateFile(PIPE_NAME,GENERIC_READ | GENERIC_WRITE, 30 0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL)) == INVALID_HANDLE_VALUE) 31 { 32 printf("CreateFile failed with error %d \n",GetLastError()); 33 getchar(); 34 return ; 35 } 36 //写入数据 37 if ((WriteFile(PipeHandle,"This is a test",14,&BytesWritten,NULL)) == 0) 38 { 39 printf("WriteFile failed with error %d \n",GetLastError()); 40 CloseHandle(PipeHandle); 41 getchar(); 42 return ; 43 } 44 45 printf("Wrote %d bytes\n",BytesWritten); 46 47 //读取数据 48 if (ReadFile(PipeHandle,buffer,sizeof(buffer),&BytesRead,NULL) > 0) 49 { 50 printf("Receive:dfj %s\n",buffer); 51 printf("Read %d bytes\n",BytesWritten); 52 getchar(); 53 return ; 54 } 55 56 getchar(); 57 CloseHandle(PipeHandle); 58 59 }
最后还有几个简化后的API,使用起来更加方便
CallNamedPipe();//客户机,可同时读写数据
TransactNamedPipe();//客户机、服务器,可同时读写数据
1 BOOL WINAPI CallNamedPipe( 2 _In_ LPCTSTR lpNamedPipeName,//管道名称,采用UNC格式 3 _In_ LPVOID lpInBuffer,//发送数据缓存区 4 _In_ DWORD nInBufferSize,//数据缓存区大小 5 _Out_ LPVOID lpOutBuffer,//接受数据缓存区 6 _In_ DWORD nOutBufferSize,//接受数据缓存区大小 7 _Out_ LPDWORD lpBytesRead,//从管道中读取的数据大小 8 _In_ DWORD nTimeOut//超时 9 );
其中nTimeOut 的参数可供选择为:
NMPWAIT_NOWAIT //不可用则直接退出,并返回错误
WMPWAIT_WAIT_FOREVER//一直等待
NMPWAIT_USE_DEFAULT_WAIT//使用默认的超时设置
1 BOOL WINAPI TransactNamedPipe( 2 _In_ HANDLE hNamedPipe,//命名管道句柄 3 _In_ LPVOID lpInBuffer,//发送数据缓冲区 4 _In_ DWORD nInBufferSize,//缓冲区大小 5 _Out_ LPVOID lpOutBuffer,//接受数据缓冲区 6 _In_ DWORD nOutBufferSize,//缓冲区大小 7 _Out_ LPDWORD lpBytesRead,//实际读取的数据多少 8 _Inout_opt_ LPOVERLAPPED lpOverlapped//重叠I/O 9 );
GetNamedPipeHandleState();//
用于接收与一个指定命名管道对应的信息,比如运行模式(消息或字节模式)、管道实例数以及缓冲区信息等等。
1 BOOL WINAPI GetNamedPipeHandleState( 2 _In_ HANDLE hNamedPipe,//命名管道句柄 3 _Out_opt_ LPDWORD lpState,//管道状态PIPE_NOWAIT,PIPE_READMODE_MESSAGE 4 _Out_opt_ LPDWORD lpCurInstances,//当前管道的实例数量 5 _Out_opt_ LPDWORD lpMaxCollectionCount,//实际最大字节数 6 _Out_opt_ LPDWORD lpCollectDataTimeout,//超时 7 _Out_opt_ LPTSTR lpUserName,//客户机名称 8 _In_ DWORD nMaxUserNameSize//客户机数量 9 );
SetNamedPipeHandleState ();//设置管道的一些参数,传输模式
BOOL WINAPI SetNamedPipeHandleState( _In_ HANDLE hNamedPipe,//命名管道句柄 _In_opt_ LPDWORD lpMode,//传输模式,字节流或者信息流 _In_opt_ LPDWORD lpMaxCollectionCount,//最大字节数 _In_opt_ LPDWORD lpCollectDataTimeout//超时 );
lpMode的参数有两个PIPE_READMODE_BYTE(字节流),PIPE_READMODE_MESSAGE(消息流)
GetNamedPipeInfo();//获得缓冲区大小以及管道实例最大数量信息
1 BOOL WINAPI GetNamedPipeInfo( 2 _In_ HANDLE hNamedPipe, 3 _Out_opt_ LPDWORD lpFlags,//管道类型,服务器或者客户端 4 _Out_opt_ LPDWORD lpOutBufferSize, 5 _Out_opt_ LPDWORD lpInBufferSize, 6 _Out_opt_ LPDWORD lpMaxInstances//管道最大实例数量 7 );
lpFlags的参数选择为:
PIPE_CLIENT_END//客户机
PIPE_SERVER_END//服务器
PIPE_TYPE_BYTE//字节流
PIPE_TYPE_MESSAGE//消息流
PeekNamedPipe();//可用它对命令管道内的数据进行浏览,同时毋需将其从管道的内部缓冲区挪出。
1 BOOL WINAPI PeekNamedPipe( 2 _In_ HANDLE hNamedPipe, 3 _Out_opt_ LPVOID lpBuffer,//读取数据缓冲区 4 _In_ DWORD nBufferSize,//缓冲区大小 5 _Out_opt_ LPDWORD lpBytesRead,//读取数据大小 6 _Out_opt_ LPDWORD lpTotalBytesAvail,//接收可从管道发出的字节总数 7 _Out_opt_ LPDWORD lpBytesLeftThisMessage//于接收消息内尚存的字节数量(前提是管道用消息模式打开 8 );
本文来自博客园,作者:struggle_time,转载请注明原文链接:https://www.cnblogs.com/songliquan/p/3396201.html