本節(jié)是《Java與Internet編程》的第三部分,也是最后一部分。在前面兩節(jié)中,我們介紹了網(wǎng)絡(luò)編程的基礎(chǔ)知識(shí),如協(xié)議、端口、套接字、UDP等,并給出了一些客戶程序和服務(wù)程序的實(shí)現(xiàn)實(shí)例。本節(jié)我們介紹兩個(gè)更高級(jí)的協(xié)議:POP3和HTTP,并給出一個(gè)POP3客戶程序和一個(gè)HTTP服務(wù)器的實(shí)現(xiàn)。
㈠ POP3協(xié)議簡(jiǎn)介
POP3是一種高級(jí)網(wǎng)絡(luò)協(xié)議,它的全稱是Post Office Protocol Version 3。使用該協(xié)議,客戶程序能夠動(dòng)態(tài)地、有效地訪問(wèn)服務(wù)器上的郵件。簡(jiǎn)單地說(shuō),POP3是一種能夠讓客戶程序提取駐留于服務(wù)器的郵件的協(xié)議。有關(guān)POP3的操作可以概括為:
服務(wù)器在端口110監(jiān)聽客戶請(qǐng)求。
客戶程序發(fā)出連接請(qǐng)求并通過(guò)身份驗(yàn)證。
客戶程序發(fā)送命令;服務(wù)器處理命令并將結(jié)果發(fā)送給客戶程序;重復(fù)這個(gè)過(guò)程直至客戶程序結(jié)束或中止連接為止。
POP3命令都是單行的,它由一個(gè)關(guān)鍵字開頭,后面加上一個(gè)或多個(gè)參數(shù),最后為一個(gè)回車符加一個(gè)換行符(CRLF)。服務(wù)器的應(yīng)答可以由一行或多行組成,開頭內(nèi)容總是命令處理結(jié)果(+OK或-ERR),緊接著是其他附加信息,最后是一個(gè)CRLF。對(duì)于多行應(yīng)答,最后一行是英文句點(diǎn)(“.")加一個(gè)CRLF。
下表是部分POP3命令的說(shuō)明:
命令 說(shuō) 明
STAT 獲得郵箱的狀態(tài)信息,即郵件數(shù)量及大小。
RETR msg 下載指定的郵件。
DELE msg 將指定的郵件標(biāo)記為刪除。
NOOP 空操作。
RSET 取消所有的刪除標(biāo)記。
QUIT 結(jié)束會(huì)話。
TOP msg n 下載指定郵件的頭信息及前面的n行。
UIDL [msg] 獲得所有郵件或指定郵件的唯一標(biāo)識(shí)符。
USER name 標(biāo)示將要訪問(wèn)的郵箱(即用戶名字)。
PASS pw 發(fā)送由USER命令指定的用戶的密碼(以明文發(fā)送)。
㈡ POP3客戶程序?qū)嵗?
下面的MailStat.java程序演示了POP3協(xié)議的基本用法。該程序的功能是檢查指定服務(wù)器上的郵件狀態(tài)。
【MailStat.java】
public class MailStat{
private static final int POP3_PORT = 110;
public static void main(String[] args) {
String host;
InetAddress hostAddress;
String username;
String password;
Socket mailSocket;
BufferedReader socketInput;
DataOutputStream socketOutput;
// 檢查參數(shù)
if (args.length < 3) {
System.out.println("用法: MailStat [服務(wù)器] [用戶名字] [密碼]");
}
else {
host = args[0];
username = args[1];
password = args[2];
try {
hostAddress = InetAddress.getByName(host);
System.out.println("正在連接服務(wù)器" + hostAddress + "...");
mailSocket = new Socket(host, POP3_PORT);
try {
socketInput = new BufferedReader(
new InputStreamReader(mailSocket.getInputStream()) );
socketOutput = new DataOutputStream(mailSocket.getOutputStream());
// 從服務(wù)器讀入初始應(yīng)答
readReply(socketInput);
// 驗(yàn)證身份
sendCommand(socketOutput, "USER " + username);
readReply(socketInput);
sendCommand(socketOutput, "PASS " + password);
readReply(socketInput);
// 獲得狀態(tài)信息
sendCommand(socketOutput, "STAT");
readReply(socketInput);
// 結(jié)束會(huì)話
sendCommand(socketOutput, "QUIT");
readReply(socketInput);
} finally {
mailSocket.close();
}
}
catch(Exception theException) {
System.out.println(theException);
}
}
System.exit(0);
}
/**
* sendCommand() 發(fā)送一個(gè)POP3命令
*/
private static void sendCommand(DataOutputStream out, String command)
throws IOException {
…略…
}
/**
* readReply() 讀取并顯示POP3服務(wù)器的應(yīng)答
*/
private static String readReply(BufferedReader reader)
throws IOException, Exception {
…略…
}
}
下面是其算法說(shuō)明:
獲得命令行參數(shù),包括郵件主機(jī)、用戶名稱、密碼。如果沒(méi)有指定這些參數(shù),則輸出提示信息并退出。
獲得郵件服務(wù)器的IP地址。
打開與郵件服務(wù)器通訊的Socket。
引用Socket的輸入、輸出流。
讀取服務(wù)器的初始應(yīng)答信息。
發(fā)送用戶名字并讀取應(yīng)答。
發(fā)送密碼并讀取應(yīng)答。
讀取狀態(tài)信息(郵件總數(shù),郵箱大小)。
結(jié)束會(huì)話。
關(guān)閉Socket并退出。
為簡(jiǎn)單計(jì),我們沒(méi)有為MailStat加上任何“特色”功能。您可以自己修改它使之更為實(shí)用,比如增加每隔幾分鐘檢查一次的功能,或同時(shí)檢查多個(gè)郵箱的功能,或一個(gè)圖形用戶界面,等等。
㈢ HTTP協(xié)議簡(jiǎn)介
HTTP協(xié)議也是一種高級(jí)網(wǎng)絡(luò)協(xié)議,是瀏覽器與Web服務(wù)器通信的標(biāo)準(zhǔn)協(xié)議。HTTP 1.1規(guī)范可以在RFC 2616找到,HTTP 1.0 規(guī)范可以在RFC 1945找到。
有關(guān)HTTP的基本操作為:
服務(wù)器在端口80監(jiān)聽。
客戶程序(如瀏覽器)連接到服務(wù)器并發(fā)送請(qǐng)求信息。
服務(wù)器發(fā)送應(yīng)答信息。
由客戶程序或服務(wù)器關(guān)閉連接。
客戶請(qǐng)求的一般格式為:
< command> /< url> < HTTP-version>CRLF
[< keyword>: < value>CRLF]
...
[< keyword>: < value>CRLF]
其中:
< command> = 請(qǐng)求服務(wù)器處理的命令,如
GET —— 提取文件
HEAD —— 提取文件頭
POST —— 發(fā)送表單數(shù)據(jù)
PUT —— 上載文件
< url> = 要求提取文件的URL
< HTTP-version> = 客戶程序能夠理解的HTTP版本,如
HTTP/1.0、HTTP/1.1等等
< keyword> = 提供給服務(wù)器的附加信息關(guān)鍵詞。
常見的關(guān)鍵詞如:
Accept —— 可以接受的數(shù)據(jù)類型
User-Agent —— 用來(lái)標(biāo)識(shí)瀏覽器
下面是客戶請(qǐng)求的幾個(gè)示例:
●GET /foo.html HTTP/1.1
●GET /foo.html HTTP/1.1
Accept: text/html
Accept: text/plain
Accept: image/gif
Accept: image/jpg
User-Agent: Netscape/4.5
服務(wù)器應(yīng)答的基本格式如下:
< HTTP-version> < response-code>CRLF
Server: < server-identity>CRLF
MIME-version: < MIME-version>CRLF
Content-type: < content-type>CRLF
Content-length: 11160
CRLF
< data>
其中:
< HTTP-version> = 服務(wù)器所使用的HTTP版本號(hào)。
< response-code> = 應(yīng)答類型。它由兩部分組成,即一個(gè)編號(hào)及文本說(shuō)明。最常見的應(yīng)答是“200 OK”和“404 Not Found”。編號(hào)為200-299的應(yīng)答表示成功,300-399表示重定向,400-499表示客戶錯(cuò)誤,500-599表示服務(wù)器錯(cuò)誤。
< server-identity> = 服務(wù)器標(biāo)識(shí)。
< MIME-version> = 服務(wù)器所使用的MIME版本號(hào)。
< content-type> = 所發(fā)送內(nèi)容的MIME類型:text/html,image/gif等。
< content-length> = 以字節(jié)計(jì)的發(fā)送內(nèi)容長(zhǎng)度。
< data> = 內(nèi)容。
下面是服務(wù)器應(yīng)答的一個(gè)實(shí)例:
HTTP/1.1 200 OK
Server: NCSA/1.4.2
MIME-version: 1.0
Content-type: text/html
Content-length: 37756
< html>
< head>< title>Foo< /title>< /head>
< body>Foo< /body>
< /html>
㈣ 一個(gè)多線程HTTP服務(wù)器的實(shí)現(xiàn)
下面的HttpServer.java給出了一個(gè)簡(jiǎn)單的Web服務(wù)器。它僅由兩個(gè)類構(gòu)成,只支持HTML、TEXT、GIF和JPEG文件的GET命令。
【HttpServer.java】
public class HttpServer {
private static int DEFAULT_PORT = 80;
private int serverPort;
public static void main(String[] args) {
int port = DEFAULT_PORT;
// 獲取命令行參數(shù)
if (args.length >= 1) {
try {
port = Integer.parseInt(args[0]);
} catch(NumberFormatException ex) {
System.out.println("Usage: HttpServer [端口]");
System.exit(0);
}
}
(new HttpServer(port)).go();
}
public HttpServer() {
this(DEFAULT_PORT);
}
public HttpServer(int port) {
super();
this.serverPort = port;
}
/**
* 啟動(dòng)服務(wù)器
*/
public void go() {
ServerSocket httpSocket;
Socket clientSocket;
HttpRequestThread requestThread;
try {
httpSocket = new ServerSocket(serverPort);
System.out.println("HttpServer在端口" + serverPort + "監(jiān)聽.");
try {
while (true) {
clientSocket = httpSocket.accept();
requestThread = new HttpRequestThread(clientSocket);
requestThread.start();
}
} finally {
httpSocket.close();
}
} catch(Exception ex) {
System.out.println(ex.toString());
}
System.exit(0);
}
}
下面是它的算法說(shuō)明:
從命令行獲取端口參數(shù)。若沒(méi)有指定,則默認(rèn)為80。
在指定的端口打開一個(gè)服務(wù)器Socket。
開始循環(huán):
等待客戶程序的連接請(qǐng)求,當(dāng)請(qǐng)求到達(dá)時(shí)獲得客戶Socket的引用。
創(chuàng)建一個(gè)新的請(qǐng)求服務(wù)線程,并以客戶Socket為參數(shù)啟動(dòng)該線程。
結(jié)束循環(huán)。
關(guān)閉服務(wù)器Socket并退出。
客戶請(qǐng)求的服務(wù)線程類實(shí)現(xiàn)如下:
【HttpRequestThread.java】
public class HttpRequestThread extends Thread {
private Socket clientSocket;
public HttpRequestThread(Socket clientSocket) {
super();
this.clientSocket = clientSocket;
}
/**
* 啟動(dòng)服務(wù)線程
*/
public void run() {
OutputStream out;
BufferedReader in;
String line;
StringTokenizer tokenizer;
String method;
String url;
String httpVersion;
try {
try {
// 引用輸入、輸出流
out = clientSocket.getOutputStream();
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 從客戶請(qǐng)求讀入一行
line = in.readLine();
tokenizer = new StringTokenizer(line);
if (tokenizer.countTokens() == 3) {
// 獲得命令類型、URL、HTTP版本號(hào)
…略…
if (method.equalsIgnoreCase("get")) {
sendResponse(out, url);
}
}
} finally {
clientSocket.close();
}
} catch (Exception ex) {
System.out.println("RequestThread: " + ex.toString());
}
}
/**
* 將服務(wù)器應(yīng)答發(fā)送給瀏覽器
*/
public void sendResponse(OutputStream out, String url) throws IOException {
…略…
}
}
服務(wù)線程的算法說(shuō)明如下:
引用Socket的輸入、輸出流。
打印客戶請(qǐng)求內(nèi)容。
如果客戶請(qǐng)求命令為GET,則發(fā)送應(yīng)答:若所請(qǐng)求的文件存在,則將該文件作為應(yīng)答的內(nèi)容發(fā)送;否則,發(fā)送“404 Not Found”。
編譯這兩個(gè)Java類,執(zhí)行“java HttpServer”之后,就可以用瀏覽器打開class文件所在目錄下的頁(yè)面文件了。