現在討論一下服務器應用(程序)的問題,我把它叫作NameCollecor(名字收集器)。假如多名用戶同時嘗試提交他們的E-mail地址,那麼會發生什麼情況呢?若NameCollector使用TCP/IP套接字,那麼必須運用早先介紹的多線程機制來實現對多個客戶的並發控制。但所有這些線程都試圖把數據寫到同一個文件裡,其中保存了所有E-mail地址。這便要求我們設立一種鎖定機制,保證多個線程不會同時訪問那個文件。一個“信號機”可在這裡幫助我們達到目的,但或許還有一種更簡單的方式。
如果我們換用數據報,就不必使用多線程了。用單個數據報即可“偵聽”進入的所有數據報。一旦監視到有進入的消息,程序就會進行適當的處理,並將答復數據作為一個數據報傳回原先發出請求的那名接收者。若數據報半路上丟失了,則用戶會注意到沒有答復數據傳回,所以可以重新提交請求。
服務器應用收到一個數據報,並對它進行解讀的時候,必須提取出其中的電子函件地址,並檢查本機保存的數據文件,看看裡面是否已經包含了那個地址(如果沒有,則添加之)。所以我們現在遇到了一個新的問題。Java 1.0似乎沒有足夠的能力來方便地處理包含了電子函件地址的文件(Java 1.1則不然)。但是,用C輕易就可以解決這個問題。因此,我們在這兒有機會學習將一個非Java程序同Java程序連接的最簡便方式。程序使用的Runtime對象包含了一個名為exec()的方法,它會獨立機器上一個獨立的程序,並返回一個Process(進程)對象。我們可以取得一個OutputStream,它同這個單獨程序的標准輸入連接在一起;並取得一個InputStream,它則同標准輸出連接到一起。要做的全部事情就是用任何語言寫一個程序,只要它能從標准輸入中取得自己的輸入數據,並將輸出結果寫入標准輸出即可。如果有些問題不能用Java簡便與快速地解決(或者想利用原有代碼,不想改寫),就可以考慮采用這種方法。亦可使用Java的“固有方法”(Native Method),但那要求更多的技巧,大家可以參考一下附錄A。
1. C程序
這個非Java應用是用C寫成,因為Java不適合作CGI編程;起碼啟動的時間不能讓人滿意。它的任務是管理電子函件(E-mail)地址的一個列表。標准輸入會接受一個E-mail地址,程序會檢查列表中的名字,判斷是否存在那個地址。若不存在,就將其加入,並報告操作成功。但假如名字已在列表裡了,就需要指出這一點,避免重復加入。大家不必擔心自己不能完全理解下列代碼的含義。它僅僅是一個演示程序,告訴你如何用其他語言寫一個程序,並從Java中調用它。在這裡具體采用何種語言並不重要,只要能夠從標准輸入中讀取數據,並能寫入標准輸出即可。
//: Listmgr.c // Used by NameCollector.java to manage // the email list file on the server #include <stdio.h> #include <stdlib.h> #include <string.h> #define BSIZE 250 int alreadyInList(FILE* list, char* name) { char lbuf[BSIZE]; // Go to the beginning of the list: fseek(list, 0, SEEK_SET); // Read each line in the list: while(fgets(lbuf, BSIZE, list)) { // Strip off the newline: char * newline = strchr(lbuf, '\n'); if(newline != 0) *newline = '\0'; if(strcmp(lbuf, name) == 0) return 1; } return 0; } int main() { char buf[BSIZE]; FILE* list = fopen("emlist.txt", "a+t"); if(list == 0) { perror("could not open emlist.txt"); exit(1); } while(1) { gets(buf); /* From stdin */ if(alreadyInList(list, buf)) { printf("Already in list: %s", buf); fflush(stdout); } else { fseek(list, 0, SEEK_END); fprintf(list, "%s\n", buf); fflush(list); printf("%s added to list", buf); fflush(stdout); } } } ///:~
該程序假設C編譯器能接受'//'樣式注釋(許多編譯器都能,亦可換用一個C++編譯器來編譯這個程序)。如果你的編譯器不能接受,則簡單地將那些注釋刪掉即可。
文件中的第一個函數檢查我們作為第二個參數(指向一個char的指針)傳遞給它的名字是否已在文件中。在這兒,我們將文件作為一個FILE指針傳遞,它指向一個已打開的文件(文件是在main()中打開的)。函數fseek()在文件中遍歷;我們在這兒用它移至文件開頭。fgets()從文件list中讀入一行內容,並將其置入緩沖區lbuf——不會超過規定的緩沖區長度BSIZE。所有這些工作都在一個while循環中進行,所以文件中的每一行都會讀入。接下來,用strchr()找到新行字符,以便將其刪掉。最後,用strcmp()比較我們傳遞給函數的名字與文件中的當前行。若找到一致的內容,strcmp()會返回0。函數隨後會退出,並返回一個1,指出該名字已經在文件裡了(注意這個函數找到相符內容後會立即返回,不會把時間浪費在檢查列表剩余內容的上面)。如果找遍列表都沒有發現相符的內容,則函數返回0。
在main()中,我們用fopen()打開文件。第一個參數是文件名,第二個是打開文件的方式;a+表示“追加”,以及“打開”(或“創建”,假若文件尚不存在),以便到文件的末尾進行更新。fopen()函數返回的是一個FILE指針;若為0,表示打開操作失敗。此時需要用perror()打印一條出錯提示消息,並用exit()中止程序運行。
如果文件成功打開,程序就會進入一個無限循環。調用gets(buf)的函數會從標准輸入中取出一行(記住標准輸入會與Java程序連接到一起),並將其置入緩沖區buf中。緩沖區的內容隨後會簡單地傳遞給alreadyInList()函數,如內容已在列表中,printf()就會將那條消息發給標准輸出(Java程序正在監視它)。fflush()用於對輸出緩沖區進行刷新。
如果名字不在列表中,就用fseek()移到列表末尾,並用fprintf()將名字“打印”到列表末尾。隨後,用printf()指出名字已成功加入列表(同樣需要刷新標准輸出),無限循環返回,繼續等候一個新名字的進入。
記住一般不能先在自己的計算機上編譯此程序,再把編譯好的內容上載到Web服務器,因為那台機器使用的可能是不同類的處理器和操作系統。例如,我的Web服務器安裝的是Intel的CPU,但操作系統是Linux,所以必須先下載源碼,再用遠程命令(通過telnet)指揮Linux自帶的C編譯器,令其在服務器端編譯好程序。
2. Java程序
這個程序先啟動上述的C程序,再建立必要的連接,以便同它“交談”。隨後,它創建一個數據報套接字,用它“監視”或者“偵聽”來自程序片的數據報包。
//: NameCollector.java // Extracts email names from datagrams and stores // them inside a file, using Java 1.02. import java.net.*; import java.io.*; import java.util.*; public class NameCollector { final static int COLLECTOR_PORT = 8080; final static int BUFFER_SIZE = 1000; byte[] buf = new byte[BUFFER_SIZE]; DatagramPacket dp = new DatagramPacket(buf, buf.length); // Can listen & send on the same socket: DatagramSocket socket; Process listmgr; PrintStream nameList; DataInputStream addResult; public NameCollector() { try { listmgr = Runtime.getRuntime().exec("listmgr.exe"); nameList = new PrintStream( new BufferedOutputStream( listmgr.getOutputStream())); addResult = new DataInputStream( new BufferedInputStream( listmgr.getInputStream())); } catch(IOException e) { System.err.println( "Cannot start listmgr.exe"); System.exit(1); } try { socket = new DatagramSocket(COLLECTOR_PORT); System.out.println( "NameCollector Server started"); while(true) { // Block until a datagram appears: socket.receive(dp); String rcvd = new String(dp.getData(), 0, 0, dp.getLength()); // Send to listmgr.exe standard input: nameList.println(rcvd.trim()); nameList.flush(); byte[] resultBuf = new byte[BUFFER_SIZE]; int byteCount = addResult.read(resultBuf); if(byteCount != -1) { String result = new String(resultBuf, 0).trim(); // Extract the address and port from // the received datagram to find out // where to send the reply: InetAddress senderAddress = dp.getAddress(); int senderPort = dp.getPort(); byte[] echoBuf = new byte[BUFFER_SIZE]; result.getBytes( 0, byteCount, echoBuf, 0); DatagramPacket echo = new DatagramPacket( echoBuf, echoBuf.length, senderAddress, senderPort); socket.send(echo); } else System.out.println( "Unexpected lack of result from " + "listmgr.exe"); } } catch(SocketException e) { System.err.println("Can't open socket"); System.exit(1); } catch(IOException e) { System.err.println("Communication error"); e.printStackTrace(); } } public static void main(String[] args) { new NameCollector(); } } ///:~
NameCollector中的第一個定義應該是大家所熟悉的:選定端口,創建一個數據報包,然後創建指向一個DatagramSocket的句柄。接下來的三個定義負責與C程序的連接:一個Process對象是C程序由Java程序啟動之後返回的,而且那個Process對象產生了InputStream和OutputStream,分別代表C程序的標准輸出和標准輸入。和Java IO一樣,它們理所當然地需要“封裝”起來,所以我們最後得到的是一個PrintStream和DataInputStream。
這個程序的所有工作都是在構建器內進行的。為啟動C程序,需要取得當前的Runtime對象。我們用它調用exec(),再由後者返回Process對象。在Process對象中,大家可看到通過一簡單的調用即可生成數據流:getOutputStream()和getInputStream()。從這個時候開始,我們需要考慮的全部事情就是將數據傳給數據流nameList,並從addResult中取得結果。
和往常一樣,我們將DatagramSocket同一個端口連接到一起。在無限while循環中,程序會調用receive()——除非一個數據報到來,否則receive()會一起處於“堵塞”狀態。數據報出現以後,它的內容會提取到String rcvd裡。我們首先將該字串兩頭的空格剔除(trim),再將其發給C程序。如下所示:
nameList.println(rcvd.trim());
之所以能這樣編碼,是因為Java的exec()允許我們訪問任何可執行模塊,只要它能從標准輸入中讀,並能向標准輸出中寫。還有另一些方式可與非Java代碼“交談”,這將在附錄A中討論。
從C程序中捕獲結果就顯得稍微麻煩一些。我們必須調用read(),並提供一個緩沖區,以便保存結果。read()的返回值是來自C程序的字節數。若這個值為-1,意味著某個地方出現了問題。否則,我們就將resultBuf(結果緩沖區)轉換成一個字串,然後同樣清除多余的空格。隨後,這個字串會象往常一樣進入一個DatagramPacket,並傳回當初發出請求的那個同樣的地址。注意發送方的地址也是我們接收到的DatagramPacket的一部分。
記住盡管C程序必須在Web服務器上編譯,但Java程序的編譯場所可以是任意的。這是由於不管使用的是什麼硬件平台和操作系統,編譯得到的字節碼都是一樣的。就是Java的“跨平台”兼容能力。