1. 程式人生 > >Java客戶端C++服務端Socket互動通訊

Java客戶端C++服務端Socket互動通訊

原文地址:http://www.drdobbs.com/jvm/java-and-c-socket-communication/222900697?pgno=1
文中有一些地方翻譯的著實不好,還請直接去閱讀原文。另外,本文使用的是XML來進行資料的傳輸,你也可以使用json進行傳輸,然後在伺服器端引入jsoncpp庫,使用json傳輸會更加方便。原始碼在本文的末尾,可直接下載。

通過socket網路通訊整合c++ Windows應用和Java應用
在單一平臺上只使用一種語言來部署執行你所有的應用可能是一件非常理想的事情,但這往往是不切實際的。有時候你需要將一個新的程式和一箇舊的程式整合,此時兩者之間的通訊問題可能就會比較棘手了。例如,你想將兩個程式分離開來,以便於新程式的設計不會受到干擾,而舊的程式在不影響新程式的情況下也可以得到升級。
可能的解決方案


過去,我探索了那些人們已經討論過的分散式計算解決方案,即通過Java Native Interface (JNI), Java Message System (JMS)和web services將Java和c++應用整合起來(見本文結尾的“結論”部分)。雖然這些方法在適合的情形下表現很好,但有些時候這些方法就會非常複雜並且表現的非常糟糕。例如,通過jni在Java中呼叫原生代碼是複雜的,耗時的,且易於出錯。使用JMS需要註冊,安裝,配置一個JMS Provider。而web service需要重要的基於網路的基礎開發。
另外一個解決方案是在Java和c++應用之間直接使用基於socket的網路通訊。雖然這種方法較為低階,但仍然是一種有效的解決方案。使用XML作為兩者之間的訊息傳輸協議,你可以在一定程度上維持平臺和語言的獨立性。圖1用一種簡單的方式進行了闡述。這裡我們展示一個使用Windows socket的c++應用和一個使用Java IO(情景c)的Java程式進行通訊的例子,其容易程度就和下面這兩個同類的例子(情景a和b)一樣,且無需改動程式碼。
這裡寫圖片描述

圖1 Java應用直接與c++ winsocket應用通訊
Socket 解決方案
我們來探索一個示例性的整合解決方案,包括一個使用java.io的Java程式和一個使用Windows sockets的c++程式的通訊。Windows應用支援以下三種簡單的請求:

  • Windows主機名
  • 安裝的記憶體
  • 虛擬隨機數
    這樣請求僅僅是為了闡述方便,但是在實際中這些請求的資料可能只能夠從遺留的應用程式中或者本地平臺介面中獲取到。所有的客戶端請求和伺服器響應都被格式化為了XML字串,特別地,客戶端和伺服器都從網路流中等待資料,讀取位元組,每一個完整的訊息都以NULL或’\n’字元來標記。
    例1展示了簡單的XML請求訊息。每一個訊息都有基本的XML結構,只有請求內容不一樣。
<Request>
  <Name>GetHostname</Name>
</Request>

<Request>
  <Name>GetMemory</Name>
</Request>

<Request>
  <Name>GetRandomNumber</Name>
</Request>
例1 客戶端傳送給伺服器的XML請求訊息

如果你想和每一條請求一起傳送資料,新增一個或多個XML節點到請求資料中即可。例如,如果你想改變GetMemory 訊息來請求顯示記憶體(物理的或虛擬的)的型別,XML可以改為如下形式:

<Request>
  <Name>GetMemory</Name>
  <Type>Physical</Type>
</Request>

伺服器響應訊息和請求訊息類似,但是有一些明顯的改變,見例2.主要的不同還是XML響應型別和請求的資料的內容。

<Response>
  <Name>HostnameResponse</Name>
  <Hostname> MyServer </Hostname>
</Response>

<Response>
  <Name>MemoryResponse</Name>
  <TotalPhysicalMemory> 1073201152</TotalPhysicalMemory>
</Response>

<Response>
  <Name>RandomNumberResponse</Name>
  <Number>20173</Number>
</Response>
例2 伺服器響應客戶端請求的XML響應訊息

在本例中,伺服器返回的每一個具體的資料根據計算機的不同而不同。讓我們先從Java客戶端開始深入探究一下解決方案的具體實施。
Java客戶端
例子中Java客戶端是一個Java Swing應用,包含了這樣的一個使用者介面:用來發送請求的按鈕,一個list view來展示伺服器的響應訊息(見圖2)。也有一個“Connect”的按鈕,當按下此按鈕時客戶端就嘗試按照給定的地址來連線伺服器。地址預設是localhost,但是你可以更改為任何合法的主機名或IP地址。因此,你可以選擇在同一臺計算機上執行客戶端和伺服器程式,也可以在不同的計算機上執行。我們稍後會檢查c++伺服器程式。
這裡寫圖片描述
圖2 Java客戶端程式使用者介面
程式的核心是Java網路和IO程式碼,他們封裝在SocketClient類(例3展示了部分程式碼)的內部。這個類包含了2個嵌入類,Listener和Sender,分別用於監聽請求和傳送響應訊息。

public class SocketClient {
    private Listener listener = null; // listens for requests
    private Sender sender = null;     // sends responses
    public boolean connected = false;
    ServerResponses viewer = null;

    class Listener extends Thread {
        // ...
    }

    class Sender {
        static final String HOSTNAME = ...
        static final String MEMORY = ...
        static final String RANDOM_NUM = ... 
        // ...
    }

    public SocketClient(String IPAddress, 
                       ServerResponses viewer) {
            // ...
    }

    public boolean isConnected() {
        return connected;
    }

    public void requestHostname() {
        sender.requestHostname();
    }

    public void requestMemory() {
        sender.requestMemory();
    }

    public void requestRandomNumber() {
        sender.requestRandomNumber();
    }
}
例3 SocketClient類實現了傳送請求和監聽伺服器響應的所有程式碼

當SocketClient物件建立時,即建立了和伺服器的連線。建構函式中的第一個引數是要連線的伺服器的地址,第二個引數是實現ServerResponses介面的物件的引用。見例4.

public interface ServerResponses {
    public void onHostnameResponse(String msg);
    public void onMemoryResponse(String msg);
    public void onRandomNumberResponse(String msg);
}
例4 當響應訊息到達時就呼叫ServerResponses 介面方法

因此,當“Connect”按鈕被按下時,使用者介面程式碼以例項化SocketClient類來驅動連線和請求的過程,然後當相關按鈕被按下時再呼叫它的任何一個public方法–requestHostname(), requestMemory(), requestRandomNumber()。當接收到請求所對應的伺服器響應資料時,ServerResponses提供的相關方法就會被呼叫。
在本例中,每次你按下任何一個請求按鈕,Java客戶端就會發送一條請求訊息給伺服器,然後相關的響應訊息就會在客戶端的list view中顯示(見圖3)。
這裡寫圖片描述
圖3 Java客戶端顯示一些伺服器響應訊息
這裡你可以看到顯示列表已經更新了,並顯示建立了和地址為192.168.0.180的伺服器的連線。讓我們以更加詳細的方式來檢查一下Java網路通訊的程式碼。
Java網路程式設計
當SocketClient類被建立時,建構函式中的程式碼就嘗試以8080埠和給定IP地址的伺服器建立連線。類似程式碼如下:

try {
    java.net.Socket socket = 
        new java.net.Socket("localhost", 8080);
    this.listener = new Listener(socket);
    this.sender =  new Sender(socket);
}
catch ( Exception e ) {
    e.printStackTrace();
}

程式碼很簡單,如果連線失敗就丟擲異常,如果成功則被啟用的socket物件就傳給Listener和Sender類來實現真正的工作。
使用這個啟用的socket物件來發送網路資料,你需要使用java.io包中的Java IO 流。Sender類封裝好了具體實現,但總的來說,要傳送網路資訊,你需要以socket的輸出流開始,這個輸出流就是用來發送資料的。為了提高效率,我們將這個輸出流封裝進java.io.BufferedOutputStream物件,然後將要傳送資訊的位元組寫入進這個輸出流中。

String xml = "<Request><Name>GetHostname</Name></Request>";
BufferedOutputStream bos = 
    new BufferedOutputStrean( socket.getOutputStream() );
bos.write( xml.getBytes() ); // writes to the buffer
bos.flush() ; // writes the buffer to the network

監聽網路資訊的程式碼也是類似的,因為它也使用了Java IO流。可是,由於等待訊息的特性(即程式需要一直監聽是否有來自伺服器的響應訊息,譯者注),你應該在一個獨立的執行緒中執行等待訊息這項任務。Listener類繼承了Thread類,當執行緒啟動時就在Thread.run()方法中呼叫任何一個read()方法。(這句話翻譯的好爛)
在獨立的執行緒中執行這個程式碼能夠保證應用的其他部分能夠正常執行(如響應使用者輸入),因為read方法等待資料傳輸所需要的時間是不確定的。這個解決方案很簡單,對於本例子來說足夠完美了,但是對於那些需要建立許多socket連線來進行通訊的程式來說,即使在每一個socket中建立一個執行緒也會顯得難以應對。此時,你就可以在read方法中指定一個timeout,並定期呼叫,或者執行一個非阻塞的IO(這個超出了本文的討論範圍)。
Listener類中執行上述功能的程式碼如下:

public void run() {
    // Create a reader to wrap the socket's InputStream
    InputStreamReader inputReader = 
        new InputStreamReader(socket.getInputStream() );
    BufferedReader bufferedReader = 
        new BufferedReader( inputReader );
    while ( true ) {
        // readLine blocks until a message arrives
        String xml = reader.readLine(); 
        // process the XML ...
    }
}

c++Windows應用
我們來檢查一下c++ Windows伺服器程式,它仍然使用的是sockets來進行網路通訊。具體的實現中伺服器是不知道客戶端所用的語言是Java,C/C++還是其他的語言。XML的使用進一步保證了通訊能夠成功。
當伺服器程式啟動時,就會建立一個ServerSocket 類的例項,並在8080埠監聽新的客戶端的連線。總之呢,這個類使用了Windows Sockets來建立一個網路socket,然後監聽,接受新的客戶端連線請求(當接收到客戶端請求時)。

// Start winsock and create the socket
WSAStartup(...);
listenSocket = socket(...);

// TCP bind and listen 
bind( listenSocket, ... ); 
listen( listenSocket, SOMAXCONN);

while ( listening ) {
    // Accept a client socket (
    clientSocket = accept(listenSocket, NULL, NULL);
    // ...
}

考慮到一直等到有新的客戶端連線時才呼叫accept()程式碼塊,這些程式碼是在他們自己的執行緒中執行的。一旦有客戶端連線,產生的結果程式碼和Java socket的實現是類似的,因為我們建立的物件就會通過socket來發送資料並接收資料。(這段翻譯的好爛)

// Setup the socket sender and listener
sender = new SocketSender(clientSocket);
listener = new SocketListener(clientSocket);
listener->setSender(sender);
listener->startThread();

和Java socket程式碼一致,SocketListener 類執行了Windows sockets中的recv()方法,在自己的執行緒中,此方法會一直阻塞到資料到來。

char recvbuf[BUFFERLEN];
int bytes;
while ( true ) {
    bytes = recv(clientSocket, recvbuf, BUFFERLEN, 0);
    if (bytes > 0) {
        recvbuf[bytes-1] = 0; // null terminate the string
        HandleMessage(recvbuf);
    }
}

每一個請求會在HandleMessage()方法中做進一步處理,相應的響應也會發送回客戶端。

int num = rand();
sprintf_s(buffer, "<Response><Name>RandomNumberResponse</Name>\
  <Number>%i</Number></Response>\n", num);
send( clientSocket, buffer, strlen(buffer), 0 );

總結
現在你應該對Java和C/C++應用程式通過socket網路程式設計進行通訊有了基本的瞭解。實際上,雖然本文的例子使用的是Windows sockets,但相應的開發一個Linux端的伺服器程式也能夠與這裡的Java客戶端很好的互動。結合XML,在某些情況下,本文應該提供了一種合適的解決方案。想要了解更多的其他應用程式整合技術,比如JNI, JMS, 或 web services,那就點選以下連結吧

本文程式碼在這裡:http://download.csdn.net/detail/hnyzwtf/9467489
解壓後得到三個壓縮檔案,JavaClient.zip是客戶端,需要安裝NetBeans IDE才能執行, NativeServer.zip是伺服器端,listings.zip沒什麼大的卵用。其中在NativeServer.zip中有一個類SocketListener,有如下一段程式碼:

DWORD SocketListener::m_ThreadFunc() {
    listening = true;
    while ( listening ) {
        int iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
        if (iResult > 0) {
            printf("Bytes received: %d\n", iResult);
            ***recvbuf[iResult-1] = 0;*** // null terminate the string according to the length
            //注意這句話,我認為改成recvbuf[iResult] = 0;會更好一些。
            // The message spec indicates the XML will end in a "new line"
            // character which can be either a newline or caraiage feed
            // Search for either and replace with a NULL to terminate
            if ( recvbuf[iResult-2] == '\n' || recvbuf[iResult-2] == '\r' )
                recvbuf[iResult-2] = 0;

            HandleMessage(recvbuf);
        }
        else {
            printf("Client connection closing\n");
            closesocket(ClientSocket);
            return 1;
        }
    }

    return 0;
}