現在,把大型軟件項目分解為一些相交互的小程序似乎變得越來越普遍,程序各部分之間的通訊可使用某種類型的通訊協議,這些程序可能運行在不同的機器上、不同的操作系統中、以不同的語言編寫,但也有可能只在同一台機器上,實際上,這些程序可看成是同一程序中的不同線程。而本文主要討論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