# 15.5 一個Web應用
現在讓我們想想如何創建一個應用,令其在真實的Web環境中運行,它將把Java的優勢表現得淋漓盡致。這個應用的一部分是在Web服務器上運行的一個Java程序,另一部分則是一個“程序片”或“小應用程序”(Applet),從服務器下載至瀏覽器(即“客戶”)。這個程序片從用戶那里收集信息,并將其傳回Web服務器上運行的應用程序。程序的任務非常簡單:程序片會詢問用戶的E-mail地址,并在驗證這個地址合格后(沒有包含空格,而且有一個`@`符號),將該E-mail發送給Web服務器。服務器上運行的程序則會捕獲傳回的數據,檢查一個包含了所有E-mail地址的數據文件。如果那個地址已包含在文件里,則向瀏覽器反饋一條消息,說明這一情況。該消息由程序片負責顯示。若是一個新地址,則將其置入列表,并通知程序片已成功添加了電子函件地址。
若采用傳統方式來解決這個問題,我們要創建一個包含了文本字段及一個“提交”(`Submit`)按鈕的HTML頁。用戶可在文本字段里鍵入自己喜歡的任何內容,并毫無阻礙地提交給服務器(在客戶端不進行任何檢查)。提交數據的同時,Web頁也會告訴服務器應對數據采取什么樣的操作——知會“通用網關接口”(CGI)程序,收到這些數據后立即運行服務器。這種CGI程序通常是用Perl或C寫的(有時也用C++,但要求服務器支持),而且必須能控制一切可能出現的情況。它首先會檢查數據,判斷是否采用了正確的格式。若答案是否定的,則CGI程序必須創建一個HTML頁,對遇到的問題進行描述。這個頁會轉交給服務器,再由服務器反饋回用戶。用戶看到出錯提示后,必須再試一遍提交,直到通過為止。若數據正確,CGI程序會打開數據文件,要么把電子函件地址加入文件,要么指出該地址已在數據文件里了。無論哪種情況,都必須格式化一個恰當的HTML頁,以便服務器返回給用戶。
作為Java程序員,上述解決問題的方法顯得非常笨拙。而且很自然地,我們希望一切工作都用Java完成。首先,我們會用一個Java程序片負責客戶端的數據有效性校驗,避免數據在服務器和客戶之間傳來傳去,浪費時間和帶寬,同時減輕服務器額外構建HTML頁的負擔。然后跳過Perl CGI腳本,換成在服務器上運行一個Java應用。事實上,我們在這兒已完全跳過了Web服務器,僅僅需要從程序片到服務器上運行的Java應用之間建立一個連接即可。
正如大家不久就會體驗到的那樣,盡管看起來非常簡單,但實際上有一些意想不到的問題使局面顯得稍微有些復雜。用Java 1.1寫程序片是最理想的,但實際上卻經常行不通。到本書寫作的時候,擁有Java 1.1能力的瀏覽器仍為數不多,而且即使這類瀏覽器現在非常流行,仍需考慮照顧一下那些升級緩慢的人。所以從安全的角度看,程序片代碼最好只用Java 1.0編寫。基于這一前提,我們不能用JAR文件來合并(壓縮)程序片中的`.class`文件。所以,我們應盡可能減少`.class`文件的使用數量,以縮短下載時間。
好了,再來說說我用的Web服務器(寫這個示范程序時用的就是它)。它確實支持Java,但僅限于Java 1.0!所以服務器應用也必須用Java 1.0編寫。
## 15.5.1 服務器應用
現在討論一下服務器應用(程序)的問題,我把它叫作`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的“跨平臺”兼容能力。
## 15.5.2 `NameSender`程序片
正如早先指出的那樣,程序片必須用Java 1.0編寫,使其能與絕大多數的瀏覽器適應。也正是由于這個原因,我們產生的類數量應盡可能地少。所以我們在這兒不考慮使用前面設計好的`Dgram`類,而將數據報的所有維護工作都轉到代碼行中進行。此外,程序片要用一個線程監視由服務器傳回的響應信息,而非實現`Runnable`接口,用集成到程序片的一個獨立線程來做這件事情。當然,這樣做對代碼的可讀性不利,但卻能產生一個單類(以及單個服務器請求)程序片:
```
//: NameSender.java
// An applet that sends an email address
// as a datagram, using Java 1.02.
import java.awt.*;
import java.applet.*;
import java.net.*;
import java.io.*;
public class NameSender extends Applet
implements Runnable {
private Thread pl = null;
private Button send = new Button(
"Add email address to mailing list");
private TextField t = new TextField(
"type your email address here", 40);
private String str = new String();
private Label
l = new Label(), l2 = new Label();
private DatagramSocket s;
private InetAddress hostAddress;
private byte[] buf =
new byte[NameCollector.BUFFER_SIZE];
private DatagramPacket dp =
new DatagramPacket(buf, buf.length);
private int vcount = 0;
public void init() {
setLayout(new BorderLayout());
Panel p = new Panel();
p.setLayout(new GridLayout(2, 1));
p.add(t);
p.add(send);
add("North", p);
Panel labels = new Panel();
labels.setLayout(new GridLayout(2, 1));
labels.add(l);
labels.add(l2);
add("Center", labels);
try {
// Auto-assign port number:
s = new DatagramSocket();
hostAddress = InetAddress.getByName(
getCodeBase().getHost());
} catch(UnknownHostException e) {
l.setText("Cannot find host");
} catch(SocketException e) {
l.setText("Can't open socket");
}
l.setText("Ready to send your email address");
}
public boolean action (Event evt, Object arg) {
if(evt.target.equals(send)) {
if(pl != null) {
// pl.stop(); Deprecated in Java 1.2
Thread remove = pl;
pl = null;
remove.interrupt();
}
l2.setText("");
// Check for errors in email name:
str = t.getText().toLowerCase().trim();
if(str.indexOf(' ') != -1) {
l.setText("Spaces not allowed in name");
return true;
}
if(str.indexOf(',') != -1) {
l.setText("Commas not allowed in name");
return true;
}
if(str.indexOf('@') == -1) {
l.setText("Name must include '@'");
l2.setText("");
return true;
}
if(str.indexOf('@') == 0) {
l.setText("Name must preceed '@'");
l2.setText("");
return true;
}
String end =
str.substring(str.indexOf('@'));
if(end.indexOf('.') == -1) {
l.setText("Portion after '@' must " +
"have an extension, such as '.com'");
l2.setText("");
return true;
}
// Everything's OK, so send the name. Get a
// fresh buffer, so it's zeroed. For some
// reason you must use a fixed size rather
// than calculating the size dynamically:
byte[] sbuf =
new byte[NameCollector.BUFFER_SIZE];
str.getBytes(0, str.length(), sbuf, 0);
DatagramPacket toSend =
new DatagramPacket(
sbuf, 100, hostAddress,
NameCollector.COLLECTOR_PORT);
try {
s.send(toSend);
} catch(Exception e) {
l.setText("Couldn't send datagram");
return true;
}
l.setText("Sent: " + str);
send.setLabel("Re-send");
pl = new Thread(this);
pl.start();
l2.setText(
"Waiting for verification " + ++vcount);
}
else return super.action(evt, arg);
return true;
}
// The thread portion of the applet watches for
// the reply to come back from the server:
public void run() {
try {
s.receive(dp);
} catch(Exception e) {
l2.setText("Couldn't receive datagram");
return;
}
l2.setText(new String(dp.getData(),
0, 0, dp.getLength()));
}
} ///:~
```
程序片的UI(用戶界面)非常簡單。它包含了一個`TestField`(文本字段),以便我們鍵入一個電子函件地址;以及一個`Button`(按鈕),用于將地址發給服務器。兩個`Label`(標簽)用于向用戶報告狀態信息。
到現在為止,大家已能判斷出`DatagramSocket`、`InetAddress`、緩沖區以及`DatagramPacket`都屬于網絡連接中比較麻煩的部分。最后,大家可看到`run()`方法實現了線程部分,使程序片能夠“監聽”由服務器傳回的響應信息。
`init()`方法用大家熟悉的布局工具設置GUI,然后創建`DatagramSocket`,它將同時用于數據報的收發。
`action()`方法只負責監視我們是否按下了“發送”(`send`)按鈕。記住,我們已被限制在Java 1.0上面,所以不能再用較靈活的內部類了。按鈕按下以后,采取的第一項行動便是檢查線程`pl`,看看它是否為`null`(空)。如果不為`null`,表明有一個活動線程正在運行。消息首次發出時,會啟動一個新線程,用它監視來自服務器的回應。所以假若有個線程正在運行,就意味著這并非用戶第一次發送消息。`pl`引用被設為`null`,同時中止原來的監視者(這是最合理的一種做法,因為`stop()`已被Java 1.2“反對”,這在前一章已解釋過了)。
無論這是否按鈕被第一次按下,`I2`中的文字都會清除。
下一組語句將檢查E-mail名字是否合格。`String.indexOf()`方法的作用是搜索其中的非法字符。如果找到一個,就把情況報告給用戶。注意進行所有這些工作時,都不必涉及網絡通信,所以速度非常快,而且不會影響帶寬和服務器的性能。
名字校驗通過以后,它會打包到一個數據報里,然后采用與前面那個數據報示例一樣的方式發到主機地址和端口編號。第一個標簽會發生變化,指出已成功發送出去。而且按鈕上的文字也會改變,變成“重發”(`resend`)。這時會啟動線程,第二個標簽則會告訴我們程序片正在等候來自服務器的回應。
線程的`run()`方法會利用`NameSender`中包含的`DatagramSocket`來接收數據(`receive()`),除非出現來自服務器的數據報包,否則`receive()`會暫時處于“堵塞”或者“暫停”狀態。結果得到的數據包會放進`NameSender`的`DatagramPacketdp`中。數據會從包中提取出來,并置入`NameSender`的第二個標簽。隨后,線程的執行將中斷,成為一個“死”線程。若某段時間里沒有收到來自服務器的回應,用戶可能變得不耐煩,再次按下按鈕。這樣做會中斷當前線程(數據發出以后,會再建一個新的)。由于用一個線程來監視回應數據,所以用戶在監視期間仍然可以自由使用UI。
(1) Web頁
當然,程序片必須放到一個Web頁里。下面列出完整的Web頁源碼;稍微研究一下就可看出,我用它從自己開辦的郵寄列表(Mailling List)里自動收集名字。
```
<HTML>
<HEAD>
<META CONTENT="text/html">
<TITLE>
Add Yourself to Bruce Eckel's Java Mailing List
</TITLE>
</HEAD>
<BODY LINK="#0000ff" VLINK="#800080" BGCOLOR="#ffffff">
<FONT SIZE=6><P>
Add Yourself to Bruce Eckel's Java Mailing List
</P></FONT>
The applet on this page will automatically add your email address to the mailing list, so you will receive update information about changes to the online version of "Thinking in Java," notification when the book is in print, information about upcoming Java seminars, and notification about the “Hands-on Java Seminar” Multimedia CD. Type in your email address and press the button to automatically add yourself to this mailing list. <HR>
<applet code=NameSender width=400 height=100>
</applet>
<HR>
If after several tries, you do not get verification it means that the Java application on the server is having problems. In this case, you can add yourself to the list by sending email to
<A HREF="mailto:Bruce@EckelObjects.com">
Bruce@EckelObjects.com</A>
</BODY>
</HTML>
```
程序片標記(`<applet>`)的使用非常簡單,和第13章展示的那一個并沒有什么區別。
## 15.5.3 要注意的問題
前面采取的似乎是一種完美的方法。沒有CGI編程,所以在服務器啟動一個CGI程序時不會出現延遲。數據報方式似乎能產生非常快的響應。此外,一旦Java 1.1得到絕大多數人的采納,服務器端的那一部分就可完全用Java編寫(盡管利用標準輸入和輸出同一個非Java程序連接也非常容易)。
但必須注意到一些問題。其中一個特別容易忽略:由于Java應用在服務器上是連續運行的,而且會把大多數時間花在`Datagram.receive()`方法的等候上面,這樣便為CPU帶來了額外的開銷。至少,我在自己的服務器上便發現了這個問題。另一方面,那個服務器上不會發生其他更多的事情。而且假如我們使用一個任務更為繁重的服務器,啟動程序用`nice`(一個Unix程序,用于防止進程貪吃CPU資源)或其他等價程序即可解決問題。在許多情況下,都有必要留意象這樣的一些應用——一個堵塞的`receive()`完全可能造成CPU的癱瘓。
第二個問題涉及防火墻。可將防火墻理解成自己的本地網與因特網之間的一道墻(實際是一個專用機器或防火墻軟件)。它監視進出因特網的所有通信,確保這些通信不違背預設的規則。
防火墻顯得多少有些保守,要求嚴格遵守所有規則。假如沒有遵守,它們會無情地把它們拒之門外。例如,假設我們位于防火墻后面的一個網絡中,開始用Web瀏覽器同因特網連接,防火墻要求所有傳輸都用可以接受的http端口同服務器連接,這個端口是80。現在來了這個Java程序片`NameSender`,它試圖將一個數據報傳到端口8080,這是為了越過“受保護”的端口范圍0-1024而設置的。防火墻很自然地把它想象成最壞的情況——有人使用病毒或者非法掃描端口——根本不允許傳輸的繼續進行。
只要我們的客戶建立的是與因特網的原始連接(比如通過典型的ISP接駁Internet),就不會出現此類防火墻問題。但也可能有一些重要的客戶隱藏在防火墻后,他們便不能使用我們設計的程序。
在學過有關Java的這么多東西以后,這是一件使人相當沮喪的事情,因為看來必須放棄在服務器上使用Java,改為學習如何編寫C或Perl腳本程序。但請大家不要絕望。
一個出色方案是由Sun公司提出的。如一切按計劃進行,Web服務器最終都裝備“小服務程序”或者“服務程序片”(Servlet)。它們負責接收來自客戶的請求(經過防火墻允許的80端口)。而且不再是啟動一個CGI程序,它們會啟動小服務程序。根據Sun的設想,這些小服務程序都是用Java編寫的,而且只能在服務器上運行。運行這種小程序的服務器會自動啟動它們,令其對客戶的請求進行處理。這意味著我們的所有程序都可以用Java寫成(100%純咖啡)。這顯然是一種非常吸引人的想法:一旦習慣了Java,就不必換用其他語言在服務器上處理客戶請求。
由于只能在服務器上控制請求,所以小服務程序API沒有提供GUI功能。這對`NameCollector.java`來說非常適合,它本來就不需要任何圖形界面。
在本書寫作時,`java.sun.com`已提供了一個非常廉價的小服務程序專用服務器。Sun鼓勵其他Web服務器開發者為他們的服務器軟件產品加入對小服務程序的支持。
- Java 編程思想
- 寫在前面的話
- 引言
- 第1章 對象入門
- 1.1 抽象的進步
- 1.2 對象的接口
- 1.3 實現方案的隱藏
- 1.4 方案的重復使用
- 1.5 繼承:重新使用接口
- 1.6 多態對象的互換使用
- 1.7 對象的創建和存在時間
- 1.8 異常控制:解決錯誤
- 1.9 多線程
- 1.10 永久性
- 1.11 Java和因特網
- 1.12 分析和設計
- 1.13 Java還是C++
- 第2章 一切都是對象
- 2.1 用引用操縱對象
- 2.2 所有對象都必須創建
- 2.3 絕對不要清除對象
- 2.4 新建數據類型:類
- 2.5 方法、參數和返回值
- 2.6 構建Java程序
- 2.7 我們的第一個Java程序
- 2.8 注釋和嵌入文檔
- 2.9 編碼樣式
- 2.10 總結
- 2.11 練習
- 第3章 控制程序流程
- 3.1 使用Java運算符
- 3.2 執行控制
- 3.3 總結
- 3.4 練習
- 第4章 初始化和清除
- 4.1 用構造器自動初始化
- 4.2 方法重載
- 4.3 清除:收尾和垃圾收集
- 4.4 成員初始化
- 4.5 數組初始化
- 4.6 總結
- 4.7 練習
- 第5章 隱藏實現過程
- 5.1 包:庫單元
- 5.2 Java訪問指示符
- 5.3 接口與實現
- 5.4 類訪問
- 5.5 總結
- 5.6 練習
- 第6章 類復用
- 6.1 組合的語法
- 6.2 繼承的語法
- 6.3 組合與繼承的結合
- 6.4 到底選擇組合還是繼承
- 6.5 protected
- 6.6 累積開發
- 6.7 向上轉換
- 6.8 final關鍵字
- 6.9 初始化和類裝載
- 6.10 總結
- 6.11 練習
- 第7章 多態性
- 7.1 向上轉換
- 7.2 深入理解
- 7.3 覆蓋與重載
- 7.4 抽象類和方法
- 7.5 接口
- 7.6 內部類
- 7.7 構造器和多態性
- 7.8 通過繼承進行設計
- 7.9 總結
- 7.10 練習
- 第8章 對象的容納
- 8.1 數組
- 8.2 集合
- 8.3 枚舉器(迭代器)
- 8.4 集合的類型
- 8.5 排序
- 8.6 通用集合庫
- 8.7 新集合
- 8.8 總結
- 8.9 練習
- 第9章 異常差錯控制
- 9.1 基本異常
- 9.2 異常的捕獲
- 9.3 標準Java異常
- 9.4 創建自己的異常
- 9.5 異常的限制
- 9.6 用finally清除
- 9.7 構造器
- 9.8 異常匹配
- 9.9 總結
- 9.10 練習
- 第10章 Java IO系統
- 10.1 輸入和輸出
- 10.2 增添屬性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File類
- 10.5 IO流的典型應用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 壓縮
- 10.9 對象序列化
- 10.10 總結
- 10.11 練習
- 第11章 運行期類型識別
- 11.1 對RTTI的需要
- 11.2 RTTI語法
- 11.3 反射:運行期類信息
- 11.4 總結
- 11.5 練習
- 第12章 傳遞和返回對象
- 12.1 傳遞引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只讀類
- 12.5 總結
- 12.6 練習
- 第13章 創建窗口和程序片
- 13.1 為何要用AWT?
- 13.2 基本程序片
- 13.3 制作按鈕
- 13.4 捕獲事件
- 13.5 文本字段
- 13.6 文本區域
- 13.7 標簽
- 13.8 復選框
- 13.9 單選鈕
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 視窗化應用
- 13.16 新型AWT
- 13.17 Java 1.1用戶接口API
- 13.18 可視編程和Beans
- 13.19 Swing入門
- 13.20 總結
- 13.21 練習
- 第14章 多線程
- 14.1 反應靈敏的用戶界面
- 14.2 共享有限的資源
- 14.3 堵塞
- 14.4 優先級
- 14.5 回顧runnable
- 14.6 總結
- 14.7 練習
- 第15章 網絡編程
- 15.1 機器的標識
- 15.2 套接字
- 15.3 服務多個客戶
- 15.4 數據報
- 15.5 一個Web應用
- 15.6 Java與CGI的溝通
- 15.7 用JDBC連接數據庫
- 15.8 遠程方法
- 15.9 總結
- 15.10 練習
- 第16章 設計模式
- 16.1 模式的概念
- 16.2 觀察器模式
- 16.3 模擬垃圾回收站
- 16.4 改進設計
- 16.5 抽象的應用
- 16.6 多重分發
- 16.7 訪問器模式
- 16.8 RTTI真的有害嗎
- 16.9 總結
- 16.10 練習
- 第17章 項目
- 17.1 文字處理
- 17.2 方法查找工具
- 17.3 復雜性理論
- 17.4 總結
- 17.5 練習
- 附錄A 使用非JAVA代碼
- 附錄B 對比C++和Java
- 附錄C Java編程規則
- 附錄D 性能
- 附錄E 關于垃圾收集的一些話
- 附錄F 推薦讀物