我們基本每天都在通過WEB瀏覽器,去瀏覽一些新聞,看看視頻之類的。
衆所周知,這就是所謂的B/S結構(Browser/Server,瀏覽器/服務器模式),是WEB興起後的一種網絡結構模式,WEB瀏覽器是客戶端最主要的應用軟件。
那順道就來簡單的看一下,所謂的Web服務器(例如知名的Tomcat)與瀏覽器,基本的實現原理是什麼樣的呢?
首先可以明確的就是,例如我們所做的通過瀏覽器輸入一個地址,訪問一個網頁的操作。
實際對應的底層操作簡單來說就是:客戶端(瀏覽器)面向於WEB服務器的網絡通信。
那麼,既然是網絡通信。對應於Java當中來說,就自然離不開Socket與IO流。其實這也正是Web服務器與瀏覽器的基礎實現原理。
當然,想要開發一套完善的WEB服務器或瀏覽器,需要做的工作是很複雜的。但這裏,我們想要了解的,只是其原理。
我們知道,將開發的web項目部署到tomcat服務器之後,就可以通過瀏覽器對服務器上的資源進行訪問。
但重要的一點是,存在多種不同廠商開發的不同瀏覽器。但各個類型的WEB瀏覽器,都可以正常的訪問tomcat服務器上的資源。
對此,我們可以這樣理解:我開發了一個WEB服務器,並且能夠保證其他人開發的客戶端都能夠與我的服務器正常通信。
能夠實現這樣的目的的前提自然就是,你要制定一個規範,並讓想要與你開發的服務器正常進行通信的客戶端都遵循這個規範來實現。
這個規範,也就是所謂的協議。
所以,正如在網絡通信中,數據的傳輸可以遵循TCP/IP或UDP協議一樣。
WEB服務器與WEB瀏覽器之間,也通過一種雙方都熟悉的語言進行通信。
這種協議即是:超文本傳輸協議,也就是HTTP協議。
不同的是,TCP/IP與UDP議是傳輸層當中的通信協議,而HTTP協議是應用層當中的協議。
所以,當我們想要使用Java語言實現所謂的WEB通信,自然也應當遵循HTTP協議。
Java中已經爲我們提供了這樣的一種實現規範,也就是廣爲人知的:Servlet接口。
而我們開發web項目時,最常用到的HttpServlet類,就是基於此接口實現的具體子類。
該類封裝和提供了,針對基於Http協議通信的內容進行訪問和操作的常用方法。
說了這麼多,我們通過一些小的實例,方便進行更形象的理解。
首先,我們通過一段簡單的Servlet代碼來看一下,基於HTTP協議進行WEB通信的請求信息:
public class ServletTest extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
for (Enumeration e = request.getHeaderNames(); e.hasMoreElements();) {
String header = (String) e.nextElement();
if (header != null)
System.out.println((new StringBuilder(String.valueOf(header)))
.append(":").append(request.getHeader(header))
.toString());
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
}
}
上面的代碼中,我們的目的是通過HttpSerlvetRequest當中的方法,
來打印web瀏覽器基於http協議發起的請求當中,封裝的HTTP請求詳情。程序輸出的結果如下:
一個HTTP協議的請求中,通常主要包含三個部分:
- 方法/統一資源標示符(URI)/協議/版本
- 請求標頭
- 實體主體
其中方法也就是所謂的get/post之類的請求方法,統一資源標示符也就是要訪問的目標資源的路徑,包括協議及協議版本,這些信息被放在請求的第一行。
隨後,緊接着的便是請求標頭;請求標頭通常包含了與客戶端環境及請求實體主體相關的有用信息。
最後,在標頭與實體主體之間是一個空行。它對於HTTP請求格式是很重要的,空行告訴HTTP服務器,實體主體從這裏開始。
前面已經說過了,我們這裏想要研究的,是WEB服務器的基本實現原理。
那麼我們自然想要自己來實現一下所謂的WEB服務器,我們已經知道了:
所謂的B/S結構,實際上就是客戶端與服務器之間基於HTTP協議的網絡通信。
那麼,肯定是離不開socket與io的,所以我們可以簡單的模擬一個最簡易功能的山寨瀏覽器:
public class MyTomcat {
public static void main(String[] args) {
try {
ServerSocket tomcat = new ServerSocket(9090);
System.out.println("服務器啓動");
//
Socket s = tomcat.accept();
//
byte[] buf = new byte[1024];
InputStream in = s.getInputStream();
//
int length = in.read(buf);
String request = new String(buf,0,length);
//
System.out.println(request);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
這次我們在通過對應的URL在瀏覽器中對我們的山寨服務器進行訪問,得到的輸出結果是:
通過成果我們看到,我們已經成功的簡單山寨了一下tomcat。
不過這裏需要注意的是,我們自己山寨的tomcat服務器當中,之所以也成功的輸出了Http協議的請求體,是因爲:
我們是通過web瀏覽器進行訪問的,如果通過普通的socket進行對serversocket的連接訪問,是沒有這些請求信息的。
因爲我們前面已經說過了,web瀏覽器與服務器之間的通信必須遵循Http協議。
所以,我們日常生活中使用的web瀏覽器,會自動的爲我們的請求進行基於http協議的包裝。
但是,因爲我們已經瞭解了原理,所以我們也可以自己模擬一下瀏覽器過過癮:
//山寨瀏覽器
public class MyBrowser {
public static void main(String[] args) {
try {
Socket browser = new Socket("192.168.1.102", 9090);
PrintWriter pw = new PrintWriter(browser.getOutputStream(),true);
// 封裝請求第一行
pw.println("GET/ HTTP/1.1");
// 封裝請求頭
pw.println("User-Agent: Java/1.6.0_13");
pw.println("Host: 192.168.1.102:9090");
pw
.println("Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2");
pw.println("Connection: keep-alive");
// 空行
pw.println();
// 封裝實體主體
pw.println("UserName=zhangsan&Age=17");
// 寫入完畢
browser.shutdownOutput();
// 接受服務器返回信息,
InputStream in = browser.getInputStream();
//
int length = 0;
StringBuffer request = new StringBuffer();
byte[] buf = new byte[1024];
//
while ((length = in.read(buf)) != -1) {
String line = new String(buf, 0, length);
request.append(line);
}
System.out.println(request);
//browser.close();
} catch (IOException e) {
System.out.println("異常了,操!");
}finally{
}
}
}
//修改後的山寨tomcat服務器
public class MyTomcat {
public static void main(String[] args) {
try {
ServerSocket tomcat = new ServerSocket(9090);
System.out.println("服務器啓動");
//
Socket s = tomcat.accept();
//
byte[] buf = new byte[1024];
InputStream in = s.getInputStream();
//
int length = 0;
StringBuffer request = new StringBuffer();
while ((length = in.read(buf)) != -1) {
String line = new String(buf, 0, length);
request.append(line);
}
//
System.out.println("request:"+request);
PrintWriter pw = new PrintWriter(s.getOutputStream(),true);
pw.println("<html>");
pw.println("<head>");
pw.println("<title>LiveSession List</title>");
pw.println("</head>");
pw.println("<body>");
pw.println("<p style=\"font-weight: bold;color: red;\">welcome to MyTomcat</p>");
pw.println("</body>");
s.close();
tomcat.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
我們先啓動服務器,然後運行瀏覽器模擬網頁瀏覽的過程,首先看到服務器端收到的請求信息:
緊接着,服務器收到請求進行處理後,返回資源給瀏覽器,於是得到輸出信息:
可以看到,我們在山寨瀏覽器當中得到的返回信息,實際上就是一個HTML文件的源碼,
之所以我們的山寨瀏覽器中,這些信息僅僅是以純文本形式顯示,是因爲我們的山寨瀏覽器不具備解析HTML語言的能力。
所以說,瀏覽器另外一個重要的功能其實就是:可以對超文本標記語言進行解析。而實際上,這也是瀏覽器開發的難點和重點。
上面這樣的輸出結果看上去顯然不爽,所以說山寨貨畢竟還是坑爹!
我們還是通過正規的WEB瀏覽器,來試着訪問一下我們的山寨服務器,結果發現,效果帥多了:
而順帶一提的是,既然當瀏覽器向WEB服務器發起訪問請求時,會封裝有對應的HTTP請求體。
那麼,對應的,當WEB服務器處理完瀏覽器請求,返回數據時,也會有對應的封裝,就是所謂的HTTP響應體。
舉例來說,假如我們將我們的山寨瀏覽器的代碼進行修改,去連接真正的tomcat服務器:
public class MyBrowser {
public static void main(String[] args) {
try {
Socket browser = new Socket("192.168.1.102", 8080);
PrintWriter pw = new PrintWriter(browser.getOutputStream(),true);
// 封裝請求第一行
pw.println("GET / HTTP/1.1");
// 封裝請求頭
pw.println("User-Agent: Java/1.6.0_13");
pw.println("Host: 192.168.1.102:8080");
pw
.println("Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2");
pw.println("Connection: keep-alive");
// 空行
pw.println();
// 封裝實體主體
//pw.println("UserName=zhangsan&Age=17");
// 寫入完畢
browser.shutdownOutput();
// 接受服務器返回信息,
InputStream in = browser.getInputStream();
//
int length = 0;
StringBuffer request = new StringBuffer();
byte[] buf = new byte[1024];
//
while ((length = in.read(buf)) != -1) {
String line = new String(buf, 0, length);
request.append(line);
}
System.out.println(request);
//browser.close();
} catch (IOException e) {
System.out.println("異常了,操!");
}finally{
}
}
}
運行程序,你將會發下如下的輸出信息:與HTTP請求類似,通常一個HTTP響應也包含三個部分:
- 協議/響應碼/狀態描述:協議也就是指HTTP協議的信息,響應碼是指代表該次請求的處理結果的碼(例如常見的200、404、500),其實就是該次請求處理的響應描述
- 響應標頭:響應標頭也包含與HTTP請求中的標頭類似的有用信息。
- 響應實體:通常也就是指響應本身的HTML內容。
與HTTP請求一樣,響應表頭與響應實體之間,也會使用一個空行進行分割,方便解讀。
同時我們也可以發現,其實真正被解析顯示在瀏覽器網頁上的內容,其實只是響應實體的部分。
響應行和響應標頭當中,實際上是負責將相關的一些有用信息返回給我們,但這部分是不需要在瀏覽器中所展示的。
也就是說,我們的瀏覽器除了應當具備獲取一個完整的HTTP響應的能力之外,還應該具備解析HTTP協議響應的能力。
事實上,Java也爲我們提供了這樣的對象,那就是URL及URLConnection對象。
如果我們在我們的山寨瀏覽器中,植入這樣的對象,來進行與服務器之間的HTTP通信,那麼:
public class MyBrowser2 {
public static void main(String[] args) {
try {
URL url = new URL("http://192.168.1.102:8080");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
InputStream in = conn.getInputStream();
byte[] buf = new byte[1024];
int length = 0;
StringBuffer text = new StringBuffer();
String line = null;
while ((length = in.read(buf)) != -1) {
line = new String(buf, 0, length);
text.append(line);
}
System.out.println(text);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
這次當我們再運行程序,查看輸出信息,發現我們從URLConnection對象獲取到的輸入流當中,
讀取的響應信息,就如我們所願的,只剩下了需要被解析顯示在頁面的響應實體的內容。
實際上這也就是Java爲我們提供的對象,將對HTTP協議內容的解析功能進行了封裝。
而究其根本來說,我們基本可以想象到,URLConnection = Socket + HTTP協議解析器。
也就是說,該對象的底層除了通過Socket連接到WEB服務器之外,還封裝了對HTTP協議內容的解析功能。
於是到此,我們已經簡單的瞭解了關於WEB服務器與瀏覽器的基本實現原理。