Windows程序设计__孙鑫C++Lesson17《进程间的通信》

版权声明:使用署名-非商业性使用CC协议[http://creativecommons.org/licenses/by-nc/3.0/deed.zh] https://blog.csdn.net/ziyuanxiazai123/article/details/7233265

Windows程序设计__孙鑫C++Lesson17《进程间的通信》

本节要点:
本节主要讲述了四种进程间的通信技术,包括剪切板匿名管道命名管道油槽
//********************************************************************************************
1.剪切板ClipBoard通信操作
利用Windows剪切板实现进程间的通信
(1)打开剪切板OpenClipboard,当前窗口只有打开剪切板后
并调用EmptyClipboard()函数后才拥有剪切板.
(2)设置剪切板数据SetClipboardData函数,函数原型为HANDLE SetClipboardData(
  UINT uFormat, // 数据格式包括标准和注册的格式
  HANDLE hMem   // 数据句柄
);
使用延迟提交数据的方式使用剪切板,可以提高资源利用率。
延迟提交:一个提供数据的进程创建剪切板后一直到其他进程获取数据之前,如果在剪切板放置数据过多,
为了避免浪费资源,有数据提供进程提供一个指定格式的空的数据句柄(第二个参数为NULL),
直到其他进程或者自身结束之前才真正提交数据。
当要获取数据的进程时操作系统发送 WM_RENDERFORMAT和 WM_RENDERALLFORMATS 消息给数据提供进程,
此时数据提供进程可以使用SetClipboardData函数将实际的数据放置到剪切板上。调用SetClipboardData函数之后,
系统拥有了hMem参数指定的对象,应用程序可以读取数据但不能释放这个句柄或者锁定它知道调用了CloseClipboard函数,
在调用CloseClipboard之后应用程序可以访问这些数据。如果hMem参数指定的是一个内存对象,
那么这个内存对象必须是由 GlobalAlloc函数以GMEM_MOVEABLE标记来分配的。
(3)GlobalAlloc函数从堆中分配指定字节。当以GMEM_MOVEABLE分配时返回的是这个内存对象的句柄,而不是实际的指针,
要想获取实际内存块的指针必须使用GlobalLock函数来将此句柄进行装换。
对于一个内存对象内部的数据结构中包含了一个锁计数,初始化时为0.对于可移动的内存对象,
初始化时GlobalLock使锁计数加1而GlobalUnlock 使其减1.注意一个锁定的内存对象将被一直保持锁状态,知道它的锁计数减为0时才能被移动或者释放掉。
(3)GetClipboardData从剪切板获取数据和配合使用IsClipboardFormatAvailable函数,
该函数判断指定格式的数据当前可用不(剪切板上有没有该格式的数据).
(4)CloseClipboard函数关闭剪切板。当应用程序检测或者改变剪切板后关闭剪切板,这样其他程序才能访问剪切板。
关闭剪切板后应用程序可以访问剪切板数据。
(5)实验主要代码如下:
//********************************************************************************************

// ClipBoardDlg.cpp
//向剪切板发送数据
void CClipBoardDlg::OnBtnSend() 
{
	// TODO: Add your control notification handler code here
	CString strSend;
	if(OpenClipboard())
	{
		EmptyClipboard();
		HANDLE hClipMem;
		char *pBuf;
		GetDlgItemText(IDC_EDIT_SEND,strSend);
        hClipMem=GlobalAlloc(GMEM_MOVEABLE,strSend.GetLength()+1);//分配内存对象
	    pBuf=(char *)GlobalLock(hClipMem);//句柄转换为实际可引用的指针
		strcpy(pBuf,strSend);//装入数据到内存对象
        GlobalUnlock(hClipMem);//解锁
		SetClipboardData(CF_TEXT,hClipMem);//设置指定格式的剪切板数据 这里指定为CF_TEXT
		CloseClipboard();//关闭剪切板
		CloseHandle(hClipMem);//释放句柄
	}
}
//从剪切板获取数据
void CClipBoardDlg::OnBtnRecv() 
{
	// TODO: Add your control notification handler code here
	if(OpenClipboard())
	{
	   if(IsClipboardFormatAvailable(CF_TEXT))
	   {
		   HANDLE hClipMem;
		   char *pBuf;
		   //GetClipboardData()返回的句柄由剪切板控制,而不是应用程序
		   //应用程序应该立即拷贝数据
		   //应用程序不能释放或者锁定这个句柄
           hClipMem=GetClipboardData(CF_TEXT);
           pBuf=(char *)GlobalLock(hClipMem);
           GlobalUnlock(hClipMem);
	       SetDlgItemText(IDC_EDIT_RECV,pBuf);
		   CloseClipboard();
	   }
	}
}


//********************************************************************************************
程序运行时,向剪切板发送数据效果如下图:


程序运行时,从剪切板获取数据效果如下图:


//********************************************************************************************
2.匿名管道 (anonymous pipes)实现本地机器的父子进程通信
(1)CreatePipe函数创建一个匿名管道,返回管道的读写句柄。函数原型为:
BOOL CreatePipe(
  PHANDLE hReadPipe,                       // 获取管道读句柄
  PHANDLE hWritePipe,                      // 获取管道写句柄
  LPSECURITY_ATTRIBUTES lpPipeAttributes,  // 安全属性指针,指向SECURITY_ATTRIBUTES的结构体
                                           //这个参数决定这个返回的句柄是否可以被子进程继承,如果为NULL则不能被继承
  DWORD nSize                              //管道缓冲区大小
);
(2)CreateProcess用于创建进程,这里主要是用来创建子进程。这个函数中要注意进程lpApplicationName参数中执行模块名称填写正确。
(3)ReadFile 和WriteFile实现管道文件的读写操作。
(4)注意不是同时开启父子进程,而是开启父进程后由父进程启动子进程。匿名管道只适合父子进程的通信,
当然父进程写入管道后可以从管道读取数据。匿名管道的实现,实质是父进程传递读写句柄给子进程。
(5)匿名管道实验主要代码如下:
//********************************************************************************************

// ParentView.cpp
// CParentView construction/destruction
//管道的初始化和析构函数中的处理
CParentView::CParentView()
{
	// TODO: add construction code here
        hRead=NULL;//HANDLE  hRead
	hWrite=NULL;//HANDLE hWrite
}
CParentView::~CParentView()
{
	if(hRead!=NULL)
		CloseHandle(hRead);
	if(hWrite!=NULL)
		CloseHandle(hWrite);
}
// CParentView message handlers
//父进程创建管道
void CParentView::OnPipeCreate() 
{
	// TODO: Add your command handler code here
	SECURITY_ATTRIBUTES  sa;
	sa.bInheritHandle=TRUE;//设置为真
	sa.lpSecurityDescriptor=NULL;//默认安全描述符
	sa.nLength=sizeof(SECURITY_ATTRIBUTES);
	if(!CreatePipe(&hRead,&hWrite,&sa,0))
	{
		AfxMessageBox("创建匿名管道失败!");
	    return;
	}
	PROCESS_INFORMATION pi;//进程信息结构体
	STARTUPINFO sui;//进程主窗口初始信息结构体
	ZeroMemory(&sui,sizeof(STARTUPINFO));//全部置零
	sui.cb=sizeof(STARTUPINFO);
        sui.dwFlags=STARTF_USESTDHANDLES;
	sui.hStdInput=hRead;
	sui.hStdOutput=hWrite;//管道句柄设置为子进程的标准读写句柄  指明句柄读写属性
	sui.hStdError=GetStdHandle(STD_ERROR_HANDLE);//得到父进程的标准错误句柄
	//启动子进程 传递管道参数 注意第一个参数的填写方法  
	//第五个参数设置为真 让子进程继承刚刚创建的管道句柄
	if(!CreateProcess("..\\Child\\Debug\\Child.exe",NULL,NULL,NULL,TRUE,0,NULL,NULL,&sui,&pi))
	{   
		CloseHandle(hRead);
		hRead=NULL;
		CloseHandle(hWrite);
		hWrite=NULL;
	   	AfxMessageBox("创建子进程失败!");
	    return;
	}
	else
	{
       CloseHandle(pi.hProcess);
	   CloseHandle(pi.hThread);//使子进程的使用计数减1
	}
}
//父进程从管道读取数据
void CParentView::OnPipeRead() 
{
	// TODO: Add your command handler code here
	char chRead[100];
	DWORD dwRead;
	if(!ReadFile(hRead,chRead,100,&dwRead,NULL))
	{
	   AfxMessageBox("读取管道失败!");
	   return;
	}
	else
		AfxMessageBox(chRead);
}
//父进程向管道写入数据
void CParentView::OnPipeWrite() 
{
	// TODO: Add your command handler code here
	char chWrtie[]="pipe info from parent\n";
	DWORD dwWrite;
	if(!WriteFile(hWrite,chWrtie,strlen(chWrtie)+1,&dwWrite,NULL))
	{
	   AfxMessageBox("写入管道失败!");
	   return;
	}
}
//********************************************************************************************
// ChildView.cpp
// CChildView construction/destruction
//子进程的初始化和析构函数中的处理
CChildView::CChildView()
{
	// TODO: add construction code here
        hRead=NULL;//HANDLE hRead
	hWrite=NULL;//HANDLE hWrite
}
CChildView::~CChildView()
{
	if(hRead!=NULL)
		CloseHandle(hRead);
	if(hWrite!=NULL)
		CloseHandle(hWrite);
}
// CChildView message handlers
//子进程获取标准输入输出句柄 因为在父进程中设置了 实际获取的是父进程管道的读写句柄
void CChildView::OnInitialUpdate() 
{
	CView::OnInitialUpdate();
	
	// TODO: Add your specialized code here and/or call the base class
	hRead=GetStdHandle(STD_INPUT_HANDLE);
	hWrite=GetStdHandle(STD_OUTPUT_HANDLE);
}
//子进程读取管道
void CChildView::OnPipeRead() 
{
	// TODO: Add your command handler code here
	char chRead[100];
	DWORD dwRead;
	if(!ReadFile(hRead,chRead,100,&dwRead,NULL))
	{
	   AfxMessageBox("读取管道失败!");
	   return;
	}
	else
		AfxMessageBox(chRead);
}
//子进程写入管道
void CChildView::OnPipeWrite() 
{
	// TODO: Add your command handler code here
	char chWrtie[]="pipe info from child\n";
	DWORD dwWrite;
	if(!WriteFile(hWrite,chWrtie,strlen(chWrtie)+1,&dwWrite,NULL))
	{
	   AfxMessageBox("写入管道失败!");
	   return;
	}
}


//********************************************************************************************
程序运行时,父进程从子进程获取数据效果如下图所示:


程序运行时,子进程从父进程获取数据效果如下图所示:


//********************************************************************************************
3.命名管道 named pipes.
不仅可以实现本地机器实现两个进程通信,还可以跨网络实现两个进程的通信。命名管道介绍如下图所示:
(1)命名管道工作过程:服务器端创建管道----客户端连接管道----服务器端和客户端读写数据(读写方式由创建管道时指定)
(2)CreateNamedPipe创建一个命名管道的实例。
 该函数原型为:
HANDLE CreateNamedPipe(
  LPCTSTR lpName,                             // pipe name
  DWORD dwOpenMode,                           // pipe open mode
  DWORD dwPipeMode,                           // pipe-specific modes
  DWORD nMaxInstances,                        // maximum number of instances
  DWORD nOutBufferSize,                       // output buffer size
  DWORD nInBufferSize,                        // input buffer size
  DWORD nDefaultTimeOut,                      // time-out interval
  LPSECURITY_ATTRIBUTES lpSecurityAttributes  // SD
);
函数参数说明:
parameter 1:管道名称,唯一标识了管道
parameter 2:管道访问模式 三种主要模式   双向读写模式\客户端写服务器端读模式\服务器写客户端读模式
重叠操作相对于同步操作而言。FILE_FLAG_OVERLAPPED重叠标记,指定后读写操作的函数可以立即返回而耗时的读写实际操作则在后台操作,未指定时读写操作的函数处理完毕后函数才返回。
parameter 3:管道的读写模式,对于同一个管道必须使用相同类型。消息模式写和字节模式读、消息模式读兼容;
而字节写模式写不能和消息模式读兼容。因为以消息模式写入时,写入了定界符,
当以消息模式读时,将通过定界符读取完整信息,当以字节模式读时忽略定界符,读取完整信息;
而如果采用字节写模式(PIPE_TYPE_BYTE)后采用消息读模式(PIPE_READMODE_MESSAGE),

因为字节模式忽略了定界符,消息读模式将无法知晓读多少字节合适,因此二者不能匹配。
parameter 4:指定对于这个管道可以创建的示例的最大数目。对于同一个管道创建的实例,必须指定同一个数目。
注意这个数目是指对于同一个名字的管道可以创建的实例,要想连接5个客户端请求必须调用5CreateNamedPipe。
parameter 5:指定为管道的输出缓冲区大小。
parameter 6:指定为管道的输入缓冲区大小。
parameter 7:如果WaitNamedPipe函数指定NMPWAIT_USE_DEFAULT_WAIT参数时,这个参数用于指定连接超时值。一个命名管道的没一个实例必须具有相同的值。
parameter 8:安全属性指针
函数执行成功返回服务器端的命名管道实例句柄,否则返回INVALID_HANDLE_VALUE。
(3)ConnectNamedPipe函数使服务器端等待客户端进程来连接一个命名管道的实例。注意这个函数不是用于客户端连接服务器端,客户端连接服务器端,使用CreateFile 或CallNamedPipe函数 。
ConnectNamedPipe函数原型为:
BOOL ConnectNamedPipe(
  HANDLE hNamedPipe,          // handle to named pipe
  LPOVERLAPPED lpOverlapped   // overlapped structure
);
parameter 1:CreateNamedPipe函数返回的管道句柄.
parameter 2:如果hNamedPipe管道句柄创建时指定了FILE_FLAG_OVERLAPPED标记,

则该参数不能为空,否则工作不正常。
该参数指向一个 OVERLAPPED 结构的结构体,这个结构体必须包含一个有效的人工重置的事件对象的句柄(由CreateEvent函数创建).如果hNamedPipe管道不是以 FILE_FLAG_OVERLAPPED标记打开时,该函数知道一个客户端连接或者一个错误发生时才返回。函数调用成功是返回非零值,失败时返回0;

可以通过GetLatError函数来获取更多信息。
(4)连接服务端管道,使用函数WaitNamedPipe;函数原型为:
BOOL WaitNamedPipe(
  LPCTSTR lpNamedPipeName,  // pipe name
  DWORD nTimeOut            // time-out interval
);
parameter 1:lpNamedPipeName,指定命名管道的名称,名称包含了服务器名和管道名。
形如\\servername\pipe\pipename 格式。
parameter 2:nTimeOut,等待一个可利用的命名管道的超时值。指定为NMPWAIT_USE_DEFAULT_WAIT时,
超时值使用服务器端使用CreateNamedPipe函数创建时设置的默认值。指定为NMPWAIT_WAIT_FOREVER则一直等待直到有可利用的命名管道。
当没有指定名字的管道时函数立即返回;函数成功时应该使用CreateFile 函数来打开一个命名管道。
(5)打开管道使用函数CreateFile,其函数原型为:
HANDLE CreateFile(
  LPCTSTR lpFileName,                         // 管道名字
  DWORD dwDesiredAccess,                      // 访问模式如GENERIC_READ
  DWORD dwShareMode,                          // 共享模式
  LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全属性
  DWORD dwCreationDisposition,                // 如何打开如OPEN_EXISTING
  DWORD dwFlagsAndAttributes,                 // 文件属性如FILE_ATTRIBUTE_NORMAL
  HANDLE hTemplateFile                        // 模板文件句柄
);
打开管道后,服务端和客户端就可以实行通信了。
(6)命名管道实验主要代码如下:
//********************************************************************************************

//NamedPipeView.cpp
// CNamedPipeView construction/destruction
CNamedPipeView::CNamedPipeView()
{
	// TODO: add construction code here
   hPipe=NULL;// HANDLE  hPipe
}
CNamedPipeView::~CNamedPipeView()
{
	if(hPipe!=NULL)
		CloseHandle(hPipe);
}
// CNamedPipeView message handlers
//服务端创建管道并等待客户端连接
void CNamedPipeView::OnPipeCreate() 
{
	// TODO: Add your command handler code here
	//创建命名管道 获取其句柄
	hPipe=CreateNamedPipe("\\\\.\\pipe\\pipeEx",
		PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,0,1,1024,1024,0,NULL);
	if(hPipe==INVALID_HANDLE_VALUE)
	{
		AfxMessageBox("管道创建失败!");
		hPipe=NULL;
		return;
	}
	//创建管道时指定了FILE_FLAG_OVERLAPPED标记,为ConnectNamedPipe准备事件对象
	HANDLE hEvent;
	hEvent=CreateEvent(0,TRUE,FALSE,NULL);
    if(!hEvent)
	{
	    AfxMessageBox("创建事件对象失败!");
		CloseHandle(hPipe);
		hPipe=NULL;
		return;
	}
	OVERLAPPED ovlap;
	ZeroMemory(&ovlap,sizeof(OVERLAPPED));
	ovlap.hEvent=hEvent;//人工重置的事件对象
	//等待客户端请求 注意这不是客户端的连接请求
	if(!ConnectNamedPipe(hPipe,&ovlap))  
	{   
		//函数失败返回0时但是GetLastError时返回ERROR_IO_PENDING表示没有失败
		if(ERROR_IO_PENDING!=GetLastError())
		{
			AfxMessageBox("等待客户连接失败!");
			CloseHandle(hPipe);
			CloseHandle(hEvent);
			hPipe=NULL;
			return;
		}
	}
    //等待事件对象变为有信号状态   
	if(WAIT_FAILED==WaitForSingleObject(hEvent,INFINITE))
	{
       	    AfxMessageBox("等待对象失败!");
			CloseHandle(hPipe);
			CloseHandle(hEvent);
			hPipe=NULL;
			return;
	}
	//事件对象变为有信号时标明已经有客户端连接到了服务端 关闭事件对象句柄
	CloseHandle(hEvent);
}
//服务端读取命名管道
void CNamedPipeView::OnPipeRead() 
{
	// TODO: Add your command handler code here
	char chRead[100];
	DWORD dwRead;
	if(!ReadFile(hPipe,chRead,100,&dwRead,NULL))
	{
	   AfxMessageBox("读取管道失败!");
	   return;
	}
	else
		AfxMessageBox(chRead);
}
//服务端写入命名管道
void CNamedPipeView::OnPipeWrite() 
{
	// TODO: Add your command handler code here
	char chWrtie[]="pipe info from Srv\n";
	DWORD dwWrite;
	if(!WriteFile(hPipe,chWrtie,strlen(chWrtie)+1,&dwWrite,NULL))
	{
	   AfxMessageBox("写入管道失败!");
	   return;
	}
}
//********************************************************************************************
// NamedPipeCltView.cpp
// CNamedPipeCltView construction/destruction
CNamedPipeCltView::CNamedPipeCltView()
{
	// TODO: add construction code here
   hPipe=NULL;//HANDLE hPipe;
}

CNamedPipeCltView::~CNamedPipeCltView()
{
	if(hPipe!=NULL)
		CloseHandle(hPipe);
}
// CNamedPipeCltView message handlers
//客户端连接管道
void CNamedPipeCltView::OnConn() 
{
	// TODO: Add your command handler code here
	//等待服务端的管道并连接
	if(!WaitNamedPipe("\\\\.\\pipe\\pipeEx",NMPWAIT_USE_DEFAULT_WAIT))
	{
		AfxMessageBox("当前没有可利用的命名管道实例!");
		hPipe=NULL;
		return;
	}
	//当前有可利用的命名管道时打开管道
	hPipe=CreateFile("\\\\.\\pipe\\pipeEx",GENERIC_READ|GENERIC_WRITE,
		0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
	if(INVALID_HANDLE_VALUE==hPipe)
	{
		AfxMessageBox("打开命名管道失败!");
		hPipe=NULL;
		return;
	}
}
//客户端读取命名管道
void CNamedPipeCltView::OnPipeRead() 
{
	// TODO: Add your command handler code here
	char chRead[100];
	DWORD dwRead;
	if(!ReadFile(hPipe,chRead,100,&dwRead,NULL))
	{
	   AfxMessageBox("读取管道失败!");
	   return;
	}
	else
		AfxMessageBox(chRead);
}
//客户端写入命名管道
void CNamedPipeCltView::OnPipeWrite() 
{
	// TODO: Add your command handler code here
		char chWrtie[]="namedpipe info from Clt\n";
	DWORD dwWrite;
	if(!WriteFile(hPipe,chWrtie,strlen(chWrtie)+1,&dwWrite,NULL))
	{
	   AfxMessageBox("写入管道失败!");
	   return;
	}
}


//********************************************************************************************

程序运行时,服务端从客户端获取数据如下图所示:


程序运行时,客户端从服务端获取数据如下图所示:

//********************************************************************************************
4.油槽Mailslot
(1)油槽的特点:
油槽是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输;
油槽是一种简单的通信机制,创建油槽的进程读取数据,打开油槽的客户机进程写入数据;
为保证油槽在各种Windows平台下都能够正常工作,我们在传输信息的时候,

应该将信息的长度限制在424字节以下。
(2)创建油槽,使用函数CreateMailslot,该函数原型为:
HANDLE CreateMailslot(
  LPCTSTR lpName,                            // 油槽的名称形如\\.\mailslot\[path]name格式
  DWORD nMaxMessageSize,                     // 能够写入油槽的最大的信息量 字节为单位
  DWORD lReadTimeout,                        // 读取油槽时的超时值
  LPSECURITY_ATTRIBUTES lpSecurityAttributes //安全属性SECURITY_ATTRIBUTES
                                             //这个结构中bInheritHandle决定油槽句柄的子进程继承权
);
(3)油槽双向通信 则要在同一个程序中编写一个客户端和服务端,利用客户端发送数据,利用服务端读取数据。
利用油槽的广播特点,编写会议通知程序十分方便。缺点,发送的数据量较少。
(4)油槽实验主要代码如下:
//********************************************************************************************

//MailSlotSrvView.cpp
//油槽服务端只能读取数据
void CMailSlotSrvView::OnMailslotRecv() 
{
	// TODO: Add your command handler code here
	//创建油槽 MAILSLOT_WAIT_FOREVER参数指定读取等待时间为一直等待
	hMailSlot=CreateMailslot("\\\\.\\mailslot\\MailSlot",0,MAILSLOT_WAIT_FOREVER,NULL);
	if(INVALID_HANDLE_VALUE==hMailSlot)
	{
		AfxMessageBox("油槽创建失败!");
		hMailSlot=NULL;
	    return;
	}
	//读取油槽数据
    char chRead[100];
	DWORD dwRead;
	if(!ReadFile(hMailSlot,chRead,100,&dwRead,NULL))
	{
	   AfxMessageBox("读取油槽失败!");
	   return;
	}
	else
		AfxMessageBox(chRead);
}
//********************************************************************************************
//MailSlotCltView.cpp
//油槽客户端只能发送数据  打开油槽后写入数据
void CMailSlotCltView::OnMailslotSend() 
{
	// TODO: Add your command handler code here
	//打开油槽   设置FILE_SHARE_READ共享读数据 如果不是在本地机上 需要设置服务器名称
	hMailSlot=CreateFile("\\\\.\\mailslot\\MailSlot",GENERIC_WRITE,FILE_SHARE_READ,NULL
		,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
	if(INVALID_HANDLE_VALUE==hMailSlot)
	{
		AfxMessageBox("打开油槽失败!");
		hMailSlot=NULL;
		return;
	}
	DWORD dwWrite;
	if(!WriteFile(hMailSlot,"This is from LiMing!",strlen("This is from LiMing!")+1,&dwWrite,NULL))
	{  
	   AfxMessageBox("写入油槽失败!");
           return;
	}
}


//实验运行效果如下图所示:


//********************************************************************************************
本节小结:
本节介绍的四种通信技术各有其特点,使用时根据具体情况合理选择。
剪切板通信利用Windows操作系统提供的剪切板来实现进程间通信简单,但仅限于本地机器上;
匿名管道仅适合本地机器父子进程间的通信,不支持跨网络之间的两个进程之间的通信,而且注意是父子进程之间的通信;
命名管道的功能最强大,以服务器和客户端的模式不仅可以实现本地机器间进程通信还可以实现跨网络的进程通信;
油槽通信机制是一种单向的读写性质、以广播形式的发送和接受数据的,其形式非常简单,但是数据流向本身不是双向的,
要实现双向的通信还需添加成套的客户端和服务器端程序。
注意在命名管道或者油槽的命名上,要实现跨网络通信时注意填写正确的主机名或者域名。

展开阅读全文

没有更多推荐了,返回首页