????? 本文先講解一下Java web server都是怎么工作的。web server也叫HTTP server——顧名思義它是用HTTP協議和客戶端交互的。客戶端一般就是各種各樣的瀏覽器了。相信所有朋友都清楚這個基本事實,否則你也不會看到這個系列文章了。
????? 基于Java的web server必然用到兩個極其重要的類:java.net.Socket和java.net.ServerSocket,然后以HTTP消息進行交互。
1. HTTP協議簡介(The Hypertext Transfer Protocol)
????? HTTP是用于web server和瀏覽器之間發送、接收數據的基礎核心協議——客戶端發起請求然后服務端進行響應。它使用應答式TCP連接,默認情況下監聽在80端口上。第一版協議是HTTP/0.9,然后又被HTTP/1.0重寫了,隨后HTTP/1.1又替換掉了HTTP/1.0——當前我們使用的正是HTTP/1.1,它的協議文件叫RFC2616,有興趣的可以去w3網站上下載回來研究一下,對你理解和掌握HTTP以及整個互聯網的核心有著無可替代的作用。接地氣的說法就是:明白了RFC2616,你就明白了易筋經和九陽神功,自此之后橫行天下無所顧忌。。。
????? HTTP里,永遠都是客戶端主動發起請求,然后服務端才有可能和它建立連接。web server永遠不會主動連接或者回調客戶端,但是兩邊都可以直接斷開連接。
????? 總結成一句話就是:服務端永遠處于絕對優勢地位,客戶端你不連我我就絕對不會連你,只有你客戶端發起請求了,我服務端才會和你連接,當然,心情不好時我也照樣可以不對你的請求做出任何響應。像極了男人追女人的戀愛過程吧。。。
1.1 HTTP請求
????? 它由以下部分組成:
????? 第一部分:方式 — URI — 協議/版本號
????? 第二部分:請求頭
????? 第三部分:實體數據
????? 典型例如如下:
1:
POST /baidu.com/小蘋果歌詞.txt HTTP/1.1
2:
?
3:
Accept: text/plain; text/html
4:
Accept-Language: en-gb
5:
Connection: Keep-Alive
6:
Host: localhost
7:
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
8:
Content-Length: 33
9:
Content-Type: application/x-www-form-urlencoded
10:
Accept-Encoding: gzip, deflate
11:
?
12:
lastName=Franks&firstName=Michael
?
???? 對應的第一部分就是這段請求信息的第一行,下面再詳細講解下:
???? POST /baidu.com/小蘋果歌詞.txt HTTP/1.1
???? 這一行的“POST”是請求方式,“/baidu.com/小蘋果歌詞.txt”是對應的URI,而“HTTP/1.1”就是對應的協議/版本號了。
???? HTTP協議定義了很多請求方式,每一個HTTP請求都可以使用其中的一種。HTTP 1.1 支持7種請求類型:GET,POST,HEAD,OPTIONS,PUT,DELETE以及TRACE。一般情況下,我們只用到GET和POST就足夠了。
???? URI完整的指定了一個網絡資源,一般情況下它都是相對于服務器的根目錄進行資源定位,你看到的URI才經常以斜杠“/”開頭,當然,通常我們只知道URL,URL實際上只是URI的一種而已(細節可研究RFC2396協議)。第一行的協議版本號,顧名思義就是當前使用的是哪版HTTP協議了。
???? 請求頭包含了一些關于客戶端環境和請求體的有用信息。例如,它可以指示瀏覽器使用的語言、請求體的數據長度等等。每一個請求頭和請求體之間都通過回車換行符(CRLF)分隔。
???? 請求頭和請求體之間的空白行(CRLF)是HTTP請求格式中不可或缺的一部分,它用于指明請求體數據開始的w位置。甚至在一些網絡編程書中,這個空白行(CRLF)直接被當作了HTTP請求標準格式的第四個組成部分。
???? 在上面那個例子中,請求體的實體數據只有簡單的一行,不過實際應用中實體數據往往比較多:
???? lastName=Franks&firstName=Michael
???? 1.2 HTTP響應
???? 和HTTP請求相似,HTTP響應也由三部分組成:
???? 第一部分:協議 -- 狀態碼 --描述
???? 第二部分:響應頭
???? 第三部分:響應體
???? 舉個例子:
1:
HTTP/1.1 200 OK
2:
Server: Microsoft-IIS/4.0
3:
Date: Mon, 5 Jan 2004 13:13:33 GMT
4:
Content-Type: text/html
5:
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
6:
Content-Length: 112
7:
?
8: < html >
9: < head >
10: < title > HTTP Response Example </ title >
11: </ head >
12: < body >
13:
Welcome to Brainy Software
14: </ body >
15: </ html >
?
???? 響應頭第一行和請求頭極為相似,它指示了當前使用的協議是HTTP/1.1版本,而且請求成功了(200=成功),一切順利。
響應頭包含的有用信息類似于請求頭的。響應體是一段HTML內容,響應頭和響應體之間以CRLF分隔。
2. Socket類
????? socket是網絡連接的一個端點,它賦予應用程序讀寫網絡流的能力。兩臺電腦通過發送和接收基于連接的字節流來進行交流溝通。若要發消息給另一個程序,你需要知道這個程序socket的ip地址和端口號。在java里,socket指的是java.net.Socket類。
????? 你可以使用Socket類的諸多構造器中任意一個來創建socket,下面這個構造器接收主機名和端口號作為參數:
????? public Socket (java.lang.String host, int port)
????? 在此,host可以是主機名或者ip地址,端口號就是對應的程序占用的端口。例如,要想連接80端口上的yahoo.com,你需要如下構造方式:
????? new Socket("yahoo.com", 80);
????? 一旦成功創建Socket實例,你就可以用它來發送接收字節流了。要發送字節流,你必須首先調用Socket類的getOutputStream方法獲取java.io.OutputStream對象,要發送純文本的話,我們通常構造一個OutputStream對象返回的java.io.PrintWriter對象。要接收字節流,你就應該調用Socket類的getInputStream方法來獲取 java.io.InputStream。
下面就是代碼展示了,各位看官請好:
1: Socket socket = new Socket( "127.0.0.1" , "8080" );
2:
OutputStream os = socket.getOutputStream();
3: boolean autoflush = true ;
4: PrintWriter out = new PrintWriter( socket.getOutputStream(), autoflush);
5: BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputstream() ));
6:
?
7: // 向web server發送HTTP請求
8: out .println( "GET /index.jsp HTTP/1.1" );
9: out .println( "Host: localhost:8080" );
10: out .println( "Connection: Close" );
11: out .println();
12:
?
13: // 讀取響應
14: boolean loop = true ;
15: StringBuffer sb = new StringBuffer(8096);
16: while (loop) {
17: if ( in .ready() ) {
18: int i=0;
19: while (i!=-1) {
20: i = in .read();
21: sb.append(( char ) i);
22:
}
23: loop = false ;
24:
}
25:
Thread.currentThread().sleep(50);
26:
}
27:
?
28: // 輸出響應內容
29: System. out .println(sb.toString());
30:
socket.close();
?
3. ServerSocket類
????? Socket類代表的是客戶端Socket,例如IE瀏覽器、chrome、火狐、safari等發起的連接。如果你想實現一個服務器應用程序,像HTTP server或者FTP server的話,你就必須使用不同的方法了。這是因為服務端根本不知道客戶端會發起請求建立連接,它必須永不停歇的等待客戶端請求。為此,你必須使用java.net.ServerSocket類,它是服務端socket的實現。
????? ServerSocket不同于Socket,服務端的ServerSocket必須一直等著客戶端請求的到來。一旦server socket接到連接請求,它必須創建一個Socket實例來處理和客戶端的交互。
????? 要創建server socket,你得用ServerSocket類提供的四個構造器之一。它需要你指明IP地址和server socket要監聽的端口號。經典的127.0.0.1意味著server socket將監聽本機。server socket監聽的IP地址通常也叫綁定地址。另一個重要的屬性是backlog,它意味著接入的連接請求超過此數值之后server socket就會拒絕后續請求。
????? public ServerSocket(int port, int backlog, InetAddress bindingAddress);
????? 值得注意的是,這個構造器的綁定地址必須是java.net.InetAddress類的實例。構造InetAddress對象的簡易方法就是調用它的靜態方法getByname,并傳一個主機名字符創參數,如下所示:
????? InetAddress.getByName("127.0.0.1");
????? 下面這行代碼構造了一個ServerSocket,監聽本機8080端口,同時backlog為1:
????? new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
????? 一旦ServerSocket實例構造完成,它就可以一直監聽在綁定地址的對應端口上等待請求的到來,你需要做的就是調用ServerSocket類的accept方法來啟動這個監聽過程。這個方法只返回何時產生了連接請求并且返回值是一個Sokcet類的實例。然后通過這個Socket可以發送和接收字節流。
4. 動手實現自己的山寨版web server
????? 這個山寨web server由三個類組成:HttpServer、Request、Response。
????? HttpServer的main方法創建一個HttpServer實例并調用它的await方法,顧名思義,這個await方法一直等著請求到來,然后處理請求、發送響應信息到客戶端。它會一直等,直到程序終止或停機。
????? 這個山寨版的server目前只能發送靜態資源,它會在控制臺顯示HTTP請求的字節流,但不能發送任何響應頭,比如data、cookie之類的。
4.1 HTTPServer.java
?
1:
import java.io.File;
2:
import java.io.IOException;
3:
import java.io.InputStream;
4:
import java.io.OutputStream;
5:
import java.net.InetAddress;
6:
import java.net.ServerSocket;
7:
import java.net.Socket;
8:
?
9: public class HttpServer {
10:
?
11: /**
12: * WEB_ROOT is the directory where our HTML and other files reside. For this
13: * package, WEB_ROOT is the "webroot" directory under the working directory.
14: * The working directory is the location in the file system from where the
15: * java command was invoked.
16: */
17: public static final String WEB_ROOT = System.getProperty( "user.dir" ) + File.separator + "webroot" ;
18:
?
19: // 關機命令
20: private static final String SHUTDOWN_COMMAND = "/SHUTDOWN" ;
21:
?
22: // the shutdown command received
23: private boolean shutdown = false ;
24:
?
25: public static void main(String[] args) {
26: HttpServer server = new HttpServer();
27:
server.await();
28:
}
29:
?
30: public void await() {
31: ServerSocket serverSocket = null ;
32: int port = 8080;
33: try {
34: serverSocket = new ServerSocket(port, 1, InetAddress.getByName( "127.0.0.1" ));
35: } catch (IOException e) {
36:
e.printStackTrace();
37:
System.exit(1);
38:
}
39: // 輪詢是否有請求進來
40: while (!shutdown) {
41: Socket socket = null ;
42: InputStream input = null ;
43: OutputStream output = null ;
44: try {
45:
socket = serverSocket.accept();
46:
input = socket.getInputStream();
47:
output = socket.getOutputStream();
48: // create Request object and parse
49: Request request = new Request(input);
50:
request.parse();
51: // create Response object
52: Response response = new Response(output);
53:
response.setRequest(request);
54:
response.sendStaticResource();
55: // Close the socket
56:
socket.close();
57: // check if the previous URI is a shutdown command
58:
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
59: } catch (Exception e) {
60:
e.printStackTrace();
61: continue ;
62:
}
63:
}
64:
}
65:
}
4.2 Request.java
1:
package ex01.pyrmont;
2:
?
3:
import java.io.IOException;
4:
import java.io.InputStream;
5:
?
6: public class Request {
7:
?
8: private InputStream input;
9:
?
10: private String uri;
11:
?
12: public Request(InputStream input) {
13: this .input = input;
14:
}
15:
?
16: public void parse() {
17: // Read a set of characters from the socket
18: StringBuffer request = new StringBuffer(2048);
19: int i;
20: byte [] buffer = new byte [2048];
21: try {
22:
i = input.read(buffer);
23: } catch (IOException e) {
24:
e.printStackTrace();
25:
i = -1;
26:
}
27: for ( int j = 0; j < i; j++) {
28: request.append(( char )buffer[j]);
29:
}
30: System. out .print(request.toString());
31:
uri = parseUri(request.toString());
32:
}
33:
?
34: private String parseUri(String requestString) {
35: int index1, index2;
36: index1 = requestString.indexOf( ' ' );
37: if (index1 != -1) {
38: index2 = requestString.indexOf( ' ' , index1 + 1);
39: if (index2 > index1)
40: return requestString.substring(index1 + 1, index2);
41:
}
42: return null ;
43:
}
44:
?
45: public String getUri() {
46: return uri;
47:
}
48:
}
49:
?
4.3 Response.java
1:
package ex01.pyrmont;
2:
?
3:
import java.io.File;
4:
import java.io.FileInputStream;
5:
import java.io.IOException;
6:
import java.io.OutputStream;
7:
?
8: /*
9: * HTTP Response = Status-Line (( general-header | response-header |
10: * entity-header ) CRLF) CRLF [ message-body ] Status-Line = HTTP-Version SP
11: * Status-Code SP Reason-Phrase CRLF
12: */
13: public class Response {
14:
?
15: private static final int BUFFER_SIZE = 1024;
16:
?
17:
Request request;
18:
?
19:
OutputStream output;
20:
?
21: public Response(OutputStream output) {
22: this .output = output;
23:
}
24:
?
25: public void setRequest(Request request) {
26: this .request = request;
27:
}
28:
?
29: public void sendStaticResource() throws IOException {
30: byte [] bytes = new byte [BUFFER_SIZE];
31: FileInputStream fis = null ;
32: try {
33: File file = new File(HttpServer.WEB_ROOT, request.getUri());
34: if (file.exists()) {
35: fis = new FileInputStream(file);
36: int ch = fis.read(bytes, 0, BUFFER_SIZE);
37: while (ch != -1) {
38:
output.write(bytes, 0, ch);
39:
ch = fis.read(bytes, 0, BUFFER_SIZE);
40:
}
41: } else {
42: // file not found
43: String errorMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>" ;
44:
output.write(errorMessage.getBytes());
45:
}
46: } catch (Exception e) {
47: // thrown if cannot instantiate a File object
48: System. out .println(e.toString());
49: } finally {
50: if (fis != null )
51:
fis.close();
52:
}
53:
}
54:
}
55:
?
5. 總結
???? 本文講解了web server的基本原理,同時代碼貼出來了一個粗糙山寨的web server。它只有三個類構成,當然不是全功能的,不過呢,畢竟剛開始,我們會不斷的逐步完善這個web server,到本系列結束時,基本上就有一個完整的web server了。
?
文檔信息
- 譯者:張大爺
- 原文網址: http://www.cnblogs.com/flance/
- 版權聲明:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
