手把手教你擼一個Web服務器(一)


聲明:本文大概需要30分鍾,如果只依據本文不看源碼就能寫出Web服務器就算學會了~如有錯誤歡迎指正~

首先我們要知道web服務器是什么?
  一般指網站服務器,是指駐留於因特網上某種類型計算機的程序
服務器有什么作用:
  1.放置網站文件,讓別人瀏覽
  2.可以放置數據文件,供別人下載
服務器分類:
  1.Apache(例如TomCat)
  2.Nginx
  3.IIS
Web服務器的工作原理,分四步:
  1.連接過程
  2.請求過程
  3.應答過程
  4.關閉連接

手擼web服務器就是根據web服務器的工作原理去手寫代碼以實現例如Tomcat的部分核心功能

根據這兩天的學習可以分為基礎和進階版本:
  基礎就是簡單實現,靈活度不高
  進階就是把部分固定功能的代碼封裝,再做一些動態的方法以供調用

下面我總結一下手擼Web服務器的業務邏輯

 

邏輯

 1 /**
 2  * 極其簡易的版本
 3  * @author shaking
 4  *
 5  */
 6 public class WebServer {
 7     
 8     public static void main(String[] args) {
 9         WebServer webServer = new WebServer();
10         webServer.start();
11     }
12 
13     private ServerSocket server;
14     
15     public WebServer() {
16         try {
17             server = new ServerSocket(8080);
18         } catch (IOException e) {
19             e.printStackTrace();
20         }
21     }
22     
23     public void start() {
24         try {
25             while(true) {
26                 Socket socket = server.accept();
27                 OutputStream outputStream = socket.getOutputStream();
28                 outputStream.write("abcaaa".getBytes());
29                 outputStream.flush();
30                 socket.close();
31             }
32         } catch (IOException e) {
33             e.printStackTrace();
34         }
35         
36     }
37     
38 }

基礎實現:
一、創建服務器類即WebServer類(連接過程)
  ·1聲明ServerSocket類,代表服務器;ServerSocket作用是監聽特定端口,例如:8080,8086等;端口號總共用65535個
    綁定端口方法:ServerSocket server = new ServerSocket(8080);
    利用構造方法初始化ServerSocket;
    端口可以傳入0,表示操作來為服務器分配一個任意可用的端口,也稱為匿名端口,但不推薦使用;
    如果端口被占用會拋出BindException異常
    解決辦法:win + r進入運行,輸入CMD進入命令行模式
    輸入 netstat -ano查看所有被占用端口 找到想要關閉的端口對應的Listening后的值
    輸入 taskkill -f -pid (Listening后的值)


  ·2創建開始方法start()(請求和應答過程)
    (請求)調用ServerSocket的accept()方法監聽並接受套接字(socket)的連接,返回值是Socket對象
    因為服務器是被動程序,需要等有請求的時候才會響應,所以accept()方法應該是持續運行的;所以要用到while(true)
    (應答)接收到請求之后根據請求的不同應該回應不同的信息,此處基礎實現回應相同的信息
    ·調用socket的getOutputStream()返回這個套接字的輸出流(OutputStream)
    ·然后調用OutputStream的方法write(byte[] b)輸入想寫內容;因為我們輸入的是字符串,而write方法要求傳入字節數組,
    所以調用字符串的getBytes()方法返回字符數組
    ·然后調用OutputStream的方法flush()刷新流並強制寫出所有緩沖的輸出字節
  ·3關閉連接
    調用Socket的close()方法關閉連接

  ·4測試該基礎服務器能否成功運行
    利用HttpWatch監聽該連接過程,查看請求和響應;如果響應的是你在write中寫的內容,該服務器即創建成功!
    但是其中頁面一直處於加載過程,原因是你的響應不符合Http協議,瀏覽器一直在等待想要的內容(即Http的標准響應格式)

  ·5修改程序不當的響應方式
    ·這個時候就要修改write以符合Http的標准響應格式
    ·調用PrintStream對象,該對象繼承了FilterOutputStream,而FilterOutputStream繼承了OutputStream;
    ·該對象與其他輸出流不同的是永遠不會拋出IOException,並且會自動調用flush()方法(一般在執行print,println,write時自動執行),在需要寫入字符的時候推薦使用;因為IO流是基於裝飾者模式,所以使用該對象必須兩種類型(File或OutputStream)參數傳入一種,此處傳入的是OutputStream;而OutputStream可以通過Socket的getOutputStream()方法得到
    ·調用PrintStream的println()方法拼接出標准的Http協議響應格式(狀態行,響應頭,空行,響應內容)如果在調試過程中沒有響應,可以嘗試以下方法,查看方法調用順序是否有問題、重新啟動瀏覽器、換一個瀏覽器、重啟Eclipse

 1 /**
 2  * 修改符合HTTP協議的版本
 3  * @author shaking
 4  *
 5  */
 6 public class WebServer {
 7     
 8     public static void main(String[] args) {
 9         WebServer webServer = new WebServer();
10         webServer.start();
11     }
12 
13     private ServerSocket server;
14     
15     public WebServer() {
16         try {
17             server = new ServerSocket(8080);
18         } catch (IOException e) {
19             e.printStackTrace();
20         }
21     }
22     
23     public void start() {
24         try {
25             while(true) {
26                 Socket socket = server.accept();
27                 PrintStream ps = new PrintStream(socket.getOutputStream());
28                 ps.println("HTTP/1.1 200 OK");
29                 ps.println("Content-Type:text/html");
30                 String s = "server is running->->->->";
31                 ps.println("Content-Length:" + s.length());
32                 
33                 ps.println("");
34                 
35                 ps.write(s.getBytes());
36                 socket.close();
37             }
38         } catch (IOException e) {
39             e.printStackTrace();
40         }
41         
42     }
43     
44 }

這個時候瀏覽器上應該有write()里的內容:“server is running->->->->”;如果出錯了檢查是不是沒有加空行

 

進階實現:
具體的Web服務器結構
·cn.itlou----core 核心包: WebServer ,ClientHandler
       |
       ---http 封裝Http協議相關內容:HttpRequest ,HttpResponse
       |
       ---common 參數配置信息:ServletContext ,HttpContext

config配置文件:web.xml

一、基礎實現有許多許多的不足,單線程不能同時接收過多的請求,所以我們加入多線程技術
  具體的服務器架構不用變,增加線程池對象的引用並初始化線程池;
  ExecutorService threadPool = Executors.newFixedThreadPool(int a);線程池的創建方法,記得數字不要給的過高,可能電腦不行導致程序出錯;
  在start()方法中加入線程的應用,調用execute(Runnable command)方法;傳入實現Runnable的對象ClientHandler
  而該對象應該包含所有我們希望通過利用多線程提高性能的方法(應答,關閉連接)
  我們把該對象命名為ClientHandler它實現了Runnable接口,重寫run方法並寫入應答,關閉連接的代碼;
  ·1聲明一個代表客戶端的對象Socket,並將該對象傳入ClientHandler構造方法;
  ·2提取響應代碼寫入run方法中
  ·3利用線程池執行寫好的ClientHandler類
  注意:重寫時寫入網頁數據應該用PrintStream的write方法,而不是println()方法,使用println方法會輸出地址

 1 /**
 2  * 進階利用多線程的版本
 3  * @author shaking
 4  *
 5  */
 6 public class WebServer {
 7     
 8     public static void main(String[] args) {
 9         WebServer webServer = new WebServer();
10         webServer.start();
11     }
12 
13     private ServerSocket server;
14     private ExecutorService threadPool;
15     
16     public WebServer() {
17         try {
18             server = new ServerSocket(8080);
19             threadPool = Executors.newFixedThreadPool(100);
20         } catch (IOException e) {
21             e.printStackTrace();
22         }
23     }
24     
25     public void start() {
26         try {
27             while(true) {
28                 Socket socket = server.accept();
29                 threadPool.execute(new ClientHandler(socket));
30             }
31         } catch (IOException e) {
32             e.printStackTrace();
33         }
34         
35     }
36     
37 }
 1 /**
 2  * 多線程部分代碼
 3  * @author shaking
 4  *
 5  */
 6 public class ClientHandler implements Runnable{
 7     
 8     private Socket socket;
 9     
10     public ClientHandler(Socket socket) {
11         this.socket = socket;
12     }
13 
14     public void run() {
15         try {
16             PrintStream ps = new PrintStream(socket.getOutputStream());
17             ps.println("HTTP/1.1 200 OK");
18             ps.println("Content-Type:text/html");
19             String s = "server is running------->>>";
20             ps.println("Content-Length:" + s.length());
21             
22             ps.println("");
23             
24             ps.write(s.getBytes());
25             socket.close();
26         } catch (IOException e) {
27             e.printStackTrace();
28         }
29         
30     }
31     
32 }

這個時候瀏覽器上應該有write()里的內容:“server is running------->>>”;如果出錯了檢查是不是線程池加入過多線程數;

二、修改程序使其能輸出具體的網頁
這時候只需要修改響應部分的部分代碼;即修改ClientHandler類中run方法的部分內容
  ·1自制一個簡單網頁或者已有的網頁,例如index.html,把其放入WebContent文件夾下
  思考一下在網頁中怎么輸出的字符串,網頁同理(傳入文件,寫出文件內容)
  ·2    -1)傳入文件,用File類,new File(); 在其中傳入一個String類型的pathName,然后使用BufferedInputStream字節緩沖流,使用該類需要傳入一個FileInputStream類型的對象,這里我們使用FileInputStream傳入我們的File對象即想要輸出的網頁,將字節數組byte[] bs = new byte[(int)file.length()],傳入BufferedInputStream的read(byte[])方法將文件讀入
         -2)寫出文件內容,調用PrintStream的write方法,write要求傳入字符數組,調用PrintStream的write(byte[])方法傳入已經創建好的字符數組,關閉BufferedInputStream流
  ·3關閉socket連接

 1 /**
 2  * 輸出具體頁面的代碼
 3  * @author shaking
 4  *
 5  */
 6 public class ClientHandler implements Runnable{
 7     
 8     private Socket socket;
 9     
10     public ClientHandler(Socket socket) {
11         this.socket = socket;
12     }
13 
14     public void run() {
15         try {
16             PrintStream ps = new PrintStream(socket.getOutputStream());
17             ps.println("HTTP/1.1 200 OK");
18             ps.println("Content-Type:text/html");
19             String pathName = "WebContent/index.html";
20             File file = new File(pathName);
21             ps.println("Content-Length:" + file.length());
22             
23             ps.println("");
24             
25             BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
26             
27             byte[] bs = new byte[(int) file.length()];
28             
29             bis.read(bs);
30             ps.write(bs);
31             bis.close();
32             socket.close();
33         } catch (IOException e) {
34             e.printStackTrace();
35         }
36         
37     }
38     
39 }

三、我們這個服務器還是很低端,只能顯示一個固定的網頁,我們希望服務器能夠動態的響應輸入的所有網頁,有的就顯示,沒有就404
  根據地址欄輸入的網址不同,請求行也會相應的改變
  ·1獲取請求行的部分內容,例如: GET /abc.html HTTP/1.1
    這里我們使用字符輸入流BufferedReader,傳入新的對象InputStreamReader並在新的對象中傳入socket的輸入流;
    調用BufferedReader的方法readline()讀取這一行數據,如果需要完整的HTTP請求可以寫死循環
    我們利用字符串的split()方法切割請求行,以" "為目標切割成3份再用String類型數組接收,其中索引為1的數組就是我們想要的/abc.html
  ·2修改pathName的值使其可以動態的變化
  ·3設置index默認頁面和404錯誤頁面
    判斷split切割后的地址,決定如何顯示頁面;如果為空顯示index,如果沒有對應的網頁顯示404
    index需要判斷切割后的地址是否為空
    404需要判斷切割后的地址對應的文件是否存在
  注意:如果控制台報錯:FileNotFoundException:WebContent\favicon.ico在WebCont下放入一個后綴名為ico的圖片文件即可

 1 /**
 2  * 輸出具體頁面的代碼
 3  * 包括默認首頁與404頁面
 4  * @author shaking
 5  *
 6  */
 7 public class ClientHandler implements Runnable{
 8     
 9     private Socket socket;
10     
11     public ClientHandler(Socket socket) {
12         this.socket = socket;
13     }
14 
15     public void run() {
16         try {
17             BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
18             String line = reader.readLine();
19             String[] s = line.split(" ");
20             String uri = s[1];
21             
22             if(uri.equals("/")) {
23                 uri = "/index.html";
24             }
25             
26             PrintStream ps = new PrintStream(socket.getOutputStream());
27             ps.println("HTTP/1.1 200 OK");
28             ps.println("Content-Type:text/html");
29             String pathName = "WebContent" + uri;
30             File file = new File(pathName);
31             
32             if(!file.exists()) {
33                 uri = "/404.html";
34                 file = new File("WebContent" + uri);
35             }
36             
37             ps.println("Content-Length:" + file.length());
38             
39             ps.println("");
40             
41             BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
42             
43             byte[] bs = new byte[(int) file.length()];
44             
45             bis.read(bs);
46             ps.write(bs);
47             bis.close();
48             socket.close();
49         } catch (IOException e) {
50             e.printStackTrace();
51         }
52         
53     }
54     
55 }

                                                                    轉載請注明出處:http://www.cnblogs.com/shak1ng/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM