1. 程式人生 > >例項解析C++/CLI程式程序之間的通訊

例項解析C++/CLI程式程序之間的通訊

  作者:謝啟東編譯

  現在,把大型軟體專案分解為一些相互動的小程式似乎變得越來越普遍,程式各部分之間的通訊可使用某種型別的通訊協議,這些程式可能執行在不同的機器上、不同的作業系統中、以不同的語言編寫,但也有可能只在同一臺機器上,實際上,這些程式可看成是同一程式中的不同執行緒。而本文主要討論C++/CLI程式間的通訊,當然,在此是討論程序間通訊,而不是網路通訊。

  簡介

  試想一個包含資料庫查詢功能的應用,通常有一個被稱為服務端的程式,等待另一個被稱為客戶端程式傳送請求,當接收到請求時,服務端執行相應功能,並把結果(或者錯誤資訊)返回給客戶端。在許多情況中,有著多個客戶端,所有的請求都會在同一時間傳送到同一服務端,這就要求服務端程式要更加高階、完善。

  在某些針對此任務的環境中,服務端程式可能只是眾多程式中的一個程式,其他可能也是服務端或者客戶端程式,實際上,如果我們的資料庫服務端需要訪問不存在於本機的檔案,那麼它就可能成為其他某個檔案伺服器的一個客戶端。一個程式中可能會有一個服務執行緒及一個或多個客戶執行緒,因此,我們需小心使用客戶端及服務端這個術語,雖然它們表達了近似的抽象含義,但在具體實現上卻大不相同。從一般的觀點來看,客戶端即為服務端所提供服務的"消費者",而服務端也能成為其他某些服務的客戶端。

  服務端套接字



  讓我們從一個具體有代表性的服務端程式開始(請看例1),此程式等待客戶端傳送一對整數,把它們相加之後返回結果給客戶端。

  例1:

using namespace System;
using namespace System::IO;
using namespace System::Net;
using namespace System::Net::Sockets;

int main(array<String^>^ argv)
{
if (argv->Length != 1)
{
Console::WriteLine("Usage: Server port");
Environment::Exit(1);
}

int port = 0;

try
{
port = Int32::Parse(argv[0]);
}
catch (FormatException^ e)
{
Console::WriteLine("Port number {0} is ill-formed", argv[0]);
Environment::Exit(2);
}

/*1*/ if (port < IPEndPoint::MinPort || port > IPEndPoint::MaxPort)
{
Console::WriteLine("Port number must be in the range {0}-{1}",
IPEndPoint::MinPort, IPEndPoint::MaxPort);
Environment::Exit(3);
}

/*2*/ IPAddress^ ipAddress =
Dns::GetHostEntry(Dns::GetHostName())->AddressList[0];
/*3*/ IPEndPoint^ ipEndpoint = gcnew IPEndPoint(ipAddress, port);

/*4*/ Socket^ listenerSocket = gcnew Socket(AddressFamily::InterNetwork,
SocketType::Stream, ProtocolType::Tcp);

/*5*/ listenerSocket->Bind(ipEndpoint);

/*6*/ listenerSocket->Listen(1);
/*7*/ Console::WriteLine("Server listener blocking status is {0}",
listenerSocket->Blocking);

/*8*/ Socket^ serverSocket = listenerSocket->Accept();
Console::WriteLine("New connection accepted");
/*9*/ listenerSocket->Close();

/*10*/ NetworkStream^ netStream = gcnew NetworkStream(serverSocket);
/*11*/ BinaryReader^ br = gcnew BinaryReader(netStream);
/*12*/ BinaryWriter^ bw = gcnew BinaryWriter(netStream);

try
{
int value1, value2;
int result;

while (true)
{
/*13*/ value1 = br->ReadInt32();
/*14*/ value2 = br->ReadInt32();
Console::Write("Received values {0,3} and {1,3}",
value1, value2);

result = value1 + value2;
/*15*/ bw->Write(result);
Console::WriteLine(", sent result {0,3}", result);
}
}
/*16*/ catch (EndOfStreamException^ e)
{
}
/*17*/ catch (IOException^ e)
{
Console::WriteLine("IOException {0}", e);
}

/*18*/ serverSocket->Shutdown(SocketShutdown::Both);
/*19*/ serverSocket->Close();
/*20*/ netStream->Close();
Console::WriteLine("Shutting down server");
}

  此處與套接字相關的功能由名稱空間System::Net和System::Net::Sockets提供,並且需要在生成期間引用System.dll程式集。另外,因為通過套接字的通訊涉及到流,所以還要用到System:IO機制。

  當程式執行時,服務端需要知道其用來監聽客戶端連線請求的埠號,在此,這個整數值通過命令列引數提供。一般來說,埠號在0-65535範圍內,而0-1023保留給特定的用途,因此,服務端可用的埠號就為1024-65535。

  在標號1中,通過IPEndPoint類中的MinPort和MaxPort這兩個公共靜態欄位,就可得到特定系統上可用的埠範圍。

  而在標號2中,可得到我們自己的主機名,並解析到一個IpHostEntry,可從中取得本機的IP地址。接下來在標號3中,用IP地址和埠號建立了一個IPEndPoint物件,其可為某個連線提供某種服務。

  在標號4中,建立了一個Internet傳輸服務託管版本的套接字,一旦它被建立,就應通過Bind函式(標號5)繫結到一個特定的端點。接下來,套接字宣告其已經開始服務,並監聽連線請求(標號6)。傳遞給Listen的引數表明了請求佇列中連線掛起的長度,因為我們只有一個客戶端,所以在此1就足夠了。

  套接字預設以阻塞模式建立,如標號7中所示,這意味著,它會一直等待連線請求。

  當從客戶端接收到連線請求時,阻塞的套接字就會被喚醒,通過呼叫Accept(如標號8),接受請求並建立另一個套接字,並通過此套接字來與客戶端通訊。我們看到,此時的服務端有兩個套接字:一個用於監聽客戶連線請求,而另一個用於與連線的客戶端通訊。當然,一個服務端在同一時間,可與多個客戶端進行連線,且每個客戶端都有一個套接字。

  在這個簡單的例子中,我們只關心請求連線的第一個客戶端,一旦連線上了,便可關閉此監聽連線請求的套接字(參見標號9)。
在標號10-12中,我們用最近連線的套接字,建立了一個NetworkStream,連同兩個讀寫函式一起,便可以從套接字接收請求,並返回結果。
服務端在此無限迴圈,讀入一對整數,計算它們的和,並把結果返回給客戶端。當服務端探測到輸入流中的檔案結束標誌時(由客戶端關閉了套接字),會丟擲EndOfStreamException異常,並關閉I/O流和套接字,服務結束。

  標號18中的Socket::ShutDown呼叫將同時關閉套接字上的接收和傳送功能,因為我們的服務端只需告之一個客戶端它的關閉,所以此函式呼叫有點多餘,但是,在服務端要過早地結束的某些情況下,這種做法還是有用的。

  為何要捕捉IOException異常的原因在標號17中,在此是為了處理客戶端在關閉套接字之前的過早結束。  

  客戶端套接字



  現在,讓我們來看一下客戶端程式(參見例2)。在連線到服務端之後,客戶端將傳送一對隨機的整數,並且在傳送下一對之前等待返回的結果。此處我們所看到的是服務端與客戶端的同步通訊,客戶端在接收到前一對值的結果之前,是不會發送另一對新值的。

  例2:

using namespace System;
using namespace System::IO;
using namespace System::Net;
using namespace System::Net::Sockets;
using namespace System::Threading;

int main(array<String^>^ argv)
{
 if (argv->Length != 2)
 {
  Console::WriteLine("Usage: Client port message-count");
  Environment::Exit(1);
 }

 int port = 0;

 try
 {
  port = Int32::Parse(argv[0]);
 }
 catch (FormatException^ e)
 {
  Console::WriteLine("Port number {0} is ill-formed", argv[0]);
  Environment::Exit(2);
 }

 if (port < IPEndPoint::MinPort || port > IPEndPoint::MaxPort)
 {
  Console::WriteLine("Port number must be in the range {0}-{1}",IPEndPoint::MinPort, IPEndPoint::MaxPort);
  Environment::Exit(3);
 }

 int messageCount = 0;

 try
 {
  messageCount = Int32::Parse(argv[1]);
 }
 catch (FormatException^ e)
 {
  Console::WriteLine("Message count {0} is ill-formed", argv[1]);
  Environment::Exit(4);
 }

 IPAddress^ ipAddress = nullptr;
 try
 {
  /*1*/ ipAddress = Dns::GetHostEntry(Dns::GetHostName())->AddressList[0];
  /*2*/ IPEndPoint^ ipEndpoint = gcnew IPEndPoint(ipAddress, port);

  /*3*/ Socket^ clientSocket = gcnew Socket(AddressFamily::InterNetwork,
  SocketType::Stream, ProtocolType::Tcp);

  /*4*/ clientSocket->Connect(ipEndpoint);

  NetworkStream^ netStream = gcnew NetworkStream(clientSocket);
  BinaryReader^ br = gcnew BinaryReader(netStream);
  BinaryWriter^ bw = gcnew BinaryWriter(netStream);

  int value1, value2;
  int result;

  Random^ random = gcnew Random;
  (int i = 1; i <= messageCount; ++i)
  {
   /*5*/ value1 = static_cast<int>(random->NextDouble() * 100);
   /*6*/ value2 = static_cast<int>(random->NextDouble() * 100);
 
   /*7*/ bw->Write(value1);
   /*8*/ bw->Write(value2);
   Console::Write("Sent values {0,3} and {1,3}",value1, value2);
 
   /*9*/ result = br->ReadInt32();
   Console::WriteLine(", received result {0,3}", result);
   /*10*/ Thread::Sleep(3000);
  }

  /*11*/ clientSocket->Shutdown(SocketShutdown::Both);
  Console::WriteLine("Notified server we're shutting down");
  /*12*/ clientSocket->Close();
  /*13*/ netStream->Close();
  Console::WriteLine("Shutting down client");
 }
 /*14*/ catch (SocketException^ e)
 {
  Console::WriteLine("Request to connect to {0} on port {1} failed"+ "/nbecause of {2}", ipAddress, port, e);
  Environment::Exit(5);
 }
}

  如同服務端一樣,客戶端取得一個IP地址,把它與埠號繫結以生成一個IPEndPoint,並連線到服務端,而服務端在此之前一直處於阻塞監聽模式。

  在每一個傳送與接收操作之間,我們有意延遲三秒,以便觀察程式的輸出。

  以下是一個服務端程式使用埠2500時的輸出:

Server listener blocking status is True
New connection accepted
Received values 42 and 69, sent result 111
Received values 66 and 71, sent result 137
Received values 7 and 93, sent result 100
Received values 43 and 65, sent result 108
Received values 45 and 3, sent result 48
Shutting down server

  而以下是對應的客戶端程式,在傳送5對值之後的輸出:

Sent values 42 and 69, received result 111
Sent values 66 and 71, received result 137
Sent values 7 and 93, received result 100
Sent values 43 and 65, received result 108
Sent values 45 and 3, received result 48
Notified server we're shutting down
Shutting down client

  套接字上的序列化

  前面所演示的服務端與客戶端程式以簡單的方式進行數值交換,如int,然而,程式很有可能也會需要傳送與接收各種不同的使用者自定義的物件型別,這就涉及到序列化。

  試想某些金融程式所涉及到的許多事務型別,如存款、轉賬、取款,每一種都與事務有關。在此,只需簡單地設定好適當的序列化與反序列化機制,服務端就能處理多個客戶端請求,並可返回這些事務的任意數量與任意組合。

  以下有一些練習來加深對此的瞭解:

  1、 如果一個服務端連線佇列已滿,那對新的客戶端連線請求來說,會發生什麼呢?

  2、 如果當客戶端還有一個開啟的套接字,而服務端此時卻關閉了,會發生什麼呢?反之呢?

  3、 試著執行一個服務端和兩個客戶端。我們前面說過,服務端只能處理一個客戶端,為使服務端能同時處理多個客戶端,需要進行多執行緒設計,建議對服務端作一些適當的修改,並用兩個、三個、或更多客戶端來測試。

  4、 以下是當有兩個客戶端執行時的輸出:

Client 2600 4
Sent values 56 and 35, received result 91
Sent values 48 and 20, received result 68
Sent values 6 and 97, received result 103
Sent values 76 and 9, received result 85
Notified server we're shutting down
Shutting down client
Client 2600 2
Sent values 69 and 66, received result 135
Sent values 84 and 45, received result 129
Notified server we're shutting down
Shutting down client
Server 2600
Waiting for new connection request
New connection accepted
Started thread Thread-1
Waiting for new connection request
Executing thread Thread-1
Received values 56 and 35, sent result 91
New connection accepted
Started thread Thread-2
Waiting for new connection request
Executing thread Thread-2
Received values 69 and 66, sent result 135
Received values 48 and 20, sent result 68
Received values 84 and 45, sent result 129
Received values 6 and 97, sent result 103
Shutting down server thread Thread-2
Received values 76 and 9, sent result 85
Shutting down server thread Thread-1