用 ACE Framework 實現進程間通信

使用基本的 Socket API 有幾個問題。Adaptive Communication Environment Framework 定義了一組包裝器,可以解決這些問題。本文討論 ACE 爲相同或不同主機計算機之間的 IPC 提供的一些基於 C++ 的面向對象類。

Arpan Sen ([email protected]), SMTS, Mentor Graphics

2009 年 11 月 19 日

  • +內容

對於大多數程序員而言,進程間通信 (IPC) 就等於使用 Socket API。Socket API 原來是爲 UNIX® 平臺開發的,用於在 TCP/IP 協議之上提供應用層接口。它支持許多函數,表 1 列出部分函數。

常用縮寫詞

  • API:應用編程接口
  • TCP/IP:傳輸控制協議/Internet 協議
表 1. Socket API 包含的一部分 C 例程
C 函數名 提供的功能
socket 分配新的套接字句柄
bind 把套接字句柄與本地或遠程地址關聯起來
listen 被動地監聽到達的客戶機連接請求
connect 客戶機使用 connect 例程開始 TCP 握手;服務器使用 accept 例程接受連接請求
send 傳輸數據
recv 接收數據

使用基本的 Socket API 有幾個問題。首先,儘管它們的使用非常普遍,但是這個 API 的遺留 C 函數是不可移植的。例如,Windows® 中的socket 方法返回一個 SOCKET 類型的句柄,而在 UNIX 中相同的函數返回一個整數。一些方法(比如 closesocket)只在 Windows 中存在,更不用提那些不兼容的頭文件。另外,基本 API 有許多隻在運行時出現的錯誤;例如,地址或協議不匹配或未初始化的數據成員。

Adaptive Communication Environment (ACE) Framework 定義了一組包裝器來解決這些問題。本文討論 ACE 爲相同或不同主機計算機之間的 IPC 提供的一些基於 C++ 的面向對象類。

用於網絡編程的 ACE 類

表 2 給出 ACE 爲 TCP/IP 連接定義的一些基本類。

表 2. ACE 中用於網絡編程的類
ACE 類 提供的功能
ACE_Addr ACE 中的基類;用於網絡尋址
ACE_INET_Addr 派生自 ACE_Addr;用於 Internet 域尋址
ACE_SOCK ACE 套接字包裝器門面(facade)層次結構(ACE_SOCK_AcceptorACE_SOCK_Connector 等)的基類
ACE_SOCK_Acceptor 用於被動地建立連接;概念上與 Berkeley Software Distribution (BSD) 的 accept()  listen() 例程相似
ACE_SOCK_Connector 在流對象和遠程主機之間建立連接;概念上與 BSD 的 connect() 例程相似
ACE_SOCK_Stream 用於處理面向 TCP 連接的數據傳輸的類

對於 User Datagram Protocol (UDP) 通信,使用 ACE_SOCK_Dgram 類及其變體。聲明這些類的頭文件位於安裝目錄中的 $ACE_ROOT/ace/include 目錄。這些頭文件分別名爲 SOCK_Acceptor.h、SOCK_Listener.h、SOCK_Stream.h 等等。

創建客戶端連接

我們先來看看 清單 1 中的代碼。

清單 1. 用於從客戶端建立連接的 ACE API
ACE_INET_Addr server(458, “tintin.cstg.in”);
ACE_SOCK_Stream client_stream; 
ACE_SOCK_Connector client_connector;
if (-1 == connector.connect(client_stream, server)) { 
  printf (“Failure to establish connection!\n”);
  return;
}
client_stream.send_n(“Hello World”, 12, 0); 
client_stream.close( ); // close when done

客戶機需要兩段信息:要連接的主機名和端口號。這些信息封裝在 ACE_INET_Addr 類中。還有初始化 ACE_INET_Addr 類的其他方式:hostname: port no  hostip: port no 是比較流行的做法。例如,可以像下面這樣聲明服務器:

ACE_INET_Addr server ("tintin.cstg.in:458")

或:

ACE_INET_Addr server ("132.132.2.61:458")

下一步是在客戶機數據流和服務器之間建立連接,這由 ACE_SOCK_Connector 類負責。最後,ACE_SOCK_Stream 類代表客戶機執行實際的消息傳遞。ACE_SOCK_Stream 類提供 send_n  recv_n 方法,分別用於向遠程機器傳輸數據和從遠程機器接收信息。注意,發送和接收方法不會傳輸部分消息:要麼傳輸完整的消息,要麼根本不傳輸(在這種情況下,例程返回 -1)。

用 ACE_SOCK_Connector 實現更好的錯誤處理

有許多原因會導致 connect 方法返回 -1。例如,服務器可能正忙於處理多個客戶機,負載太重了;服務器可能正在重新啓動,稍後才能開始接受請求;服務器也可能在比較慢的操作系統或硬件上運行。無論是哪種情況,在放棄之前再嘗試一段時間是有意義的。connect 方法接受一個超時參數,如果在指定的時間段內未能成功地連接,它就會放棄。清單 2 說明這個過程。

清單 2. 包含超時的客戶端 ACE API
…
ACE_SOCK_Connector client_connector;
ACE_Time_Value timeout(5); 
if (-1 == connector.connect(client_stream, server, &timeout)) { 
  printf (“Failure to establish connection!\n”);
  return;
}
…

建立服務器端連接

清單 3 說明 IPC 的服務器端的工作方式。

清單 3. 使用 ACE API 建立服務器端連接
ACE_INET_Addr server(458); 
ACE_SOCK_Acceptor client_responder(server);
ACE_SOCK_Stream client_stream;
ACE_Time_Value timeout(5);
ACE_INET_Addr client; 
if (-1 == client_responder.accept(client_stream, &client, &timeout)) { 
    printf(“Connection not established with client!\n”);
    return;
} else { 
    printf(“Client details: Host Name: %s Port Number: %d\n”, 
         client.get_host_name(), client.get_port_number());
}
// Reading 40 bytes of data from the client
char buffer[128];
if (-1 == client_stream.recv_n(buffer, 40, 0)) { 
   printf(“Error in reading client data!\n”);   
   return;
} else { 
   printf(“Client message: %s\n”, buffer);
}
client_stream.close();

在服務器端,需要應該運行服務的端口號。同樣,用端口號創建 ACE_INET_Addr 對象(不需要主機名:在默認情況下,它是當前主機)。ACE_SOCK_Acceptor 類與遺留的 BSD listen 例程相似,用於接受客戶機對服務器的連接請求。以相似的方式使用 ACE_SOCK_Stream:代表服務器把消息傳遞給客戶機。看一下 accept 例程調用中超時的使用方法 — [client_responder.accept (client_stream, &client)]:服務器會一直阻塞這個調用。但是,使用超時意味着如果在指定的時間間隔內沒有客戶機連接服務器,服務器可以輸出相應的消息並停止接受請求。

最後,accept 方法接受第四個參數。清單 4 給出 ace/include/SOCK_Acceptor.h 中 accept 方法的聲明。

清單 4. accept 方法的聲明
  /**
   * Accept a new ACE_SOCK_Stream connection using the QoS
   * information in @a qos_params.  A @a timeout of 0 means block
   * forever, a @a timeout of {0, 0} means poll.  @a restart == true means
   * "restart if interrupted," i.e., if errno == EINTR.  Note that
   * @a new_stream inherits the "blocking mode" of @c this
   * ACE_SOCK_Acceptor, i.e., if @c this acceptor factory is in
   * non-blocking mode, the @a new_stream will be in non-blocking mode
   * and vice versa.
   */
  int accept (ACE_SOCK_Stream &new_stream,
              ACE_Accept_QoS_Params qos_params,
              ACE_Addr *remote_addr = 0,
              ACE_Time_Value *timeout = 0,
              bool restart = true,
              bool reset_new_handle = false) const;

如果 restart 標誌設置爲 True(默認設置),那麼在某些 UNIX 信號造成中斷時,會重新啓動例程。您必須判斷自己的應用程序是否需要這個特性。

使用 ACE 實現基於 UDP 的 IPC

下面快速回顧一下 UDP。UDP 是一種面向數據包的協議,這意味着如果客戶機向服務器發送 5 個 1KB 的信息塊,服務器可能收到零到 5 個塊。但是,服務器收到的任何塊都是完整的 1KB 塊 — 要麼接收整個塊,要麼根本不接收。UDP 不保證數據到達或到達的次序,所以第四個塊有可能在第二個塊之前到達。另外,數據報可能在傳輸過程中丟失;但是,UDP 會盡可能傳輸數據。清單 5 定義基於 UDP 的基本客戶機通信框架。

清單 5. 使用 ACE 實現 UDP 連接的客戶機代碼
ACE_INET_Addr server(458, “tintin.cstg.in”);
ACE_INET_Addr client(9000);
ACE_SOCK_Dgram client_data(client);

char* message = “Hello World!\n”;
size_t sent_data_length = client_data.send(message, 
                                              strlen(message) + 1, server);
if (sent_data_length == -1) 
  printf(“Error in data transmission\n”);

client_data.close();

同樣,爲服務器端口信息創建一個 ACE_INET_Addr 對象。與 TCP 的情況不同,對於客戶機還需要一個 ACE_INET_Addr 對象,它指定將發送和接收數據報的端口。UDP 的代碼不需要任何連接器或接受器類。必須創建一個 ACE_SOCK_Dgram 類型的類並在數據傳輸期間顯式地指定服務器地址,這是 UDP 連接另一個有意思的特點:服務器地址不必是惟一的。同一個客戶機實際上可以與多個遠程對等計算機通信。基於 TCP 的連接是一對一的,而 UDP 有三種模式:單播(一對一連接)、廣播(數據報發送給網絡中的每個遠程對等計算機)和多播(數據報只發送給網絡中的一部分遠程對等計算機)。

在討論廣播和多播之前,先看看簡單的基於 UDP 的服務器框架(清單 6)。

清單 6. 使用 ACE 實現 UDP 連接的服務器代碼
ACE_INET_Addr server(458);
ACE_SOCK_Dgram server_data(server);
ACE_INET_Addr client(“haddock.cstg.in”, 9000);

size_t sent_data_length = client_data.send(message, 
                                              strlen(message) + 1, server);
if (sent_data_length == -1) 
  printf(“Error in data transmission\n”);

server_data.close();

與 TCP 不同,用於 UDP 通信的客戶機代碼和服務器代碼非常相似。最後,注意 ACE_SOCK_Dgram 的銷燬方法並不自動地關閉底層 UDP 套接字。必須顯式地調用 close 方法;否則,會出現對象泄漏。

面向連接的基於數據報的通信

對於兩個對等計算機之間長期存在的通信,反覆提供服務器地址是不合適的。ACE 提供了 ACE_SOCK_CODgram 類(這裏的 COD 代表Connection-oriented Datagram),它可以避免反覆提供對等計算機地址。只在調用 open 方法時提供對等計算機地址一次。後續調用只需要兩個參數:字符緩衝區及其長度。清單 7 給出代碼。

清單 7. 使用 ACE 實現基於 COD 連接的客戶機代碼
ACE_INET_Addr server(458, “tintin.cstg.in”);
ACE_INET_Addr client(9000);
ACE_SOCK_CODgram client_data(client);

if (0 != client_data.open(server)) { 
   printf(“Unable to establish connection with remote host\n”);
   return;
}

char* message = “Hello World!\n”;
size_t sent_data_length = client_data.send(message, 
                                              strlen(message) + 1);
if (sent_data_length == -1) 
  printf(“Error in data transmission\n”);

message = “Initiating”;
client_data.send(message, strlen(message) + 1); 

client_data.close();

使用 ACE 實現廣播

對於向多個進程廣播的 send 操作,可以使用 ACE_SOCK_Dgram_Bcast 類(派生自 ACE_SOCK_Dgram)。不需要顯式地指定 IP 廣播地址 —ACE_SOCK_Dgram_Bcast 類負責提供正確的 IP 地址 — 您只需提供適當的遠程主機端口號。清單 8 中的代碼與 清單 5 相似,但是 send 例程現在只需要應該廣播到的端口號。

清單 8. 使用 ACE 實現廣播的客戶機代碼
ACE_INET_Addr local_host(4158, “tintin.cstg.in”);
ACE_SOCK_Dgram_Bcast local_broadcast_dgram(local_host);
int remote_port = 7671; 

char* message = “Hello World!\n”;

size_t sent_data_length = local_broadcast_dgram.send(
                                         message, strlen(message) + 1, remote_port);

if (sent_data_length == -1) 
  printf(“Error in data transmission\n”);

local_broadcast_dgram.close();

所有監聽 7671 端口的遠程機器都可以使用 ACE_SOCK_Dgram::recv 處理消息。

使用 ACE 實現多播

對於多播操作,使用 ACE_SOCK_Dgram_Mcast 類(也派生自 ACE_SOCK_Dgram)。ACE_SOCK_Dgram_Mcast 類可以對一組對等計算機收發消息。希望加入這個多播組的對等計算機必須顯式地預訂這個組。清單 9 演示如何預訂多播消息或取消預訂。

清單 9. 預訂從對等計算機集接收多播消息的客戶機代碼
#define MULTICAST_ADDR “132.132.2.7”

ACE_INET_Addr multicast_addr(4158, MULTICAST_ADDR);
ACE_SOCK_Dgram_Mcast multicast_dgram;

// Subscribe
if (-1 == multicast_dgram.subscribe(multicast_addr)) { 
   printf(“Error subscribing to multicast address\n”);
   return;
} 

// Do Processing
…

// Unsubscribe
if (-1 == multicast_dgram.unsubscribe(multicast_addr)) { 
   printf(“Error unsubscribing to multicast address\n”);
   return;
}

已經預訂某一多播組的對等計算機會收到任何成員發送給這個組的所有消息。如果網絡中的任何主機希望向這個多播組發送消息,但是對接收響應不感興趣,那麼使用 ACE_SOCK_Dgram 發送消息也可以。清單 10 演示主機如何使用多播數據報接收和發送消息。在真實的應用程序中,接收/發送代碼通常放在循環中,從而持續地發送/接收消息。注意 recv 方法中對 ACE_INET_Addr 的使用方法:這有助於捕捉正在傳輸數據的對等計算機。

清單 10. 預訂對對等計算機集發送/接收多播消息的客戶機代碼
#define MULTICAST_ADDR “132.132.2.7”

ACE_INET_Addr multicast_addr(4158, MULTICAST_ADDR);
ACE_SOCK_Dgram_Mcast multicast_dgram;

//.. Subscribe
// Do Processing for a 256 byte message 
char buffer[1024];
if (-1 == multicast_dgram.send(buffer, 256, multicast_addr)) { 
  printf(“Failure in message transmission\n”);
} 

ACE_INET_Addr peer_address; 
if (-1 == multicast_dgram.recv(buffer, 256, peer_address)) { 
  printf(“Failure in message reception\n”);
} else {
  printf(“Message %s received from Host %s : Port %d\n”, 
    buffer, peer_address.get_host_name(),
    peer_address.get_port_number()); 
} 
// .. Unsubscribe

結束語

本文討論瞭如何使用 ACE Framework 實現基於 TCP/IP 和 UDP 的通信。還有使用 ACE 實現 IPC 的其他方法,比如共享內存或 UNIX 風格的套接字尋址(ACE_LSOCK* 組中的類)。在 參考資料 中可以找到更高級信息的鏈接。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章