【雜談】一個簡易的靜態網頁服務器 【雜談】一個回車下去,瀏覽器作了什麼?

前言

  上一篇隨筆【雜談】一個回車下去,瀏覽器作了什麼?講了瀏覽器的處理,這裏再用一個例子講解一下,也不算講解,算是梳理一下服務端處理瀏覽器請求的過程。固然實際過程要比這複雜多了。下文的例子,其實就是《How Tomcat Works》這本書的第一個例子,感興趣的能夠去看這本書。不過書上的例子有問題,我下文中會提到。html

注:此項目不須要用tomcat,純Java底層代碼寫就能夠了。java

概述

程序有三個類HttpServer,Request,Response。web

HttpServer  => 負責監聽socket鏈接,建立Request、Response對象apache

Request => 用於獲取請求信息的URI(利用Socket的InputStream),這裏URI就是靜態網頁文件的相對路徑數組

Response => 用於發送響應數據報(利用Request獲取請求信息,利用OutputStream寫出數據)瀏覽器

程序包圖tomcat

完整代碼

因爲貼完整代碼都會使篇幅略顯過長,因此下面都摺疊起來了,看客能夠逐個展開查看。安全

HttpServer.java服務器

package com.wze.ex01.pyrmont;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class HttpServer {
    public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";

    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

    private boolean shutdown = false;

    public static void main(String[] args) {
        System.out.println(WEB_ROOT);
        HttpServer server = new HttpServer();
        server.await();
    }

    public void await() {
        ServerSocket serverSocket = null;
        int port = 8080;
        try {
            //之因此要綁定監聽的IP地址,是由於一個電腦可能有多個網卡
            serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
        } catch (IOException e) {
            e.printStackTrace();
            //若是綁定失敗,那麼這個程序也就沒有運行下去的必要了。
            System.exit(1);
        }
        while(!shutdown) {
            Socket socket = null;
            InputStream input  = null;
            OutputStream output = null;
            try {
                //接收一個請求,處理完畢後關閉鏈接
                socket = serverSocket.accept();
                input = socket.getInputStream();
                output = socket.getOutputStream();
                Request request = new Request(input);
                request.parse();
                Response response = new Response(output);
                response.setRequest(request);
                response.sendStaticResource();
                socket.close();
                shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }

}
View Code

Request.java網絡

package com.wze.ex01.pyrmont;

import java.io.IOException;
import java.io.InputStream;

public class Request {
    private InputStream input;
    private String uri;

    public Request(InputStream input) {
        this.input = input;
    }

    public void parse() {
        //之因此是大小是2048,是由於請求行的大小通常就是2048
        StringBuffer request = new StringBuffer(2048);
        int i;
        byte[] buffer = new byte[2048];
        try {
            i = input.read(buffer); //讀入數據到buffer,並返回請求行的實際長度
        } catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }
        for(int j = 0; j < i; j++) {
            request.append((char)buffer[j]);
        }
        System.out.println(request.toString());
        uri = parseUri(request.toString()); //從請求行中把uri取出來
        System.out.println(uri);
    }

    /**
     * 獲取請求行中的uri
     *
     * 請求行格式:Method URI Version
     * 用空格作分隔符
     * @param requestString
     * @return
     */
    private String parseUri(String requestString) {
        int index1, index2;
        index1 = requestString.indexOf(' ');
        if(index1 != -1) {
            index2 = requestString.indexOf(' ', index1+1);
            System.out.println(index1 + " " + index2);
            if(index2 > index1)
                return requestString.substring(index1 + 1, index2);
        }
        return null;
    }

    public String getUri() {
        return uri;
    }
}
View Code

Response.java

package com.wze.ex01.pyrmont;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

public class Response {
    private static final int BUFFER_SIZE = 1024;
    Request request;
    OutputStream output;

    public Response(OutputStream output) {
        this.output = output;
    }

    public void setRequest(Request request) {
        this.request = request;
    }

    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputStream fis = null;
        try {
            //獲取用戶請求文件的實際路徑
            File file = new File(HttpServer.WEB_ROOT + request.getUri());
            System.out.println(file);
            if(file.exists()) { //若是文件存在,則讀取到緩衝數組,再利用socket的outputstream寫出數據
                long contentLength = file.length();
                String successMessage = "HTTP/1.1 200 success\r\n" +
                        "Content-Type:text/html\r\n" +
                        "Content-Length:"+contentLength +"\r\n" +
                        "\r\n";
                output.write(successMessage.getBytes());
                fis = new FileInputStream(file);
                //每次最多讀寫1024字節,直到所有讀完
                int ch = fis.read(bytes, 0, BUFFER_SIZE);
                System.out.println(ch);
                while(ch != -1) {
                    output.write(bytes, 0, ch);
                    ch = fis.read(bytes, 0, BUFFER_SIZE);
                }
            } else {
                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>";
                output.write(errorMessage.getBytes());
            }
        } catch (Exception e) {
            System.out.println(e.toString());
        } finally {
            if(fis != null)
                fis.close();
        }

    }
}
View Code

運行效果

運行HttpServer的主方法,而後在瀏覽器地址欄鍵入localhost:8080/index.html,你就能夠在瀏覽器看見網頁內容了。到這一步就至關於實現了一個apache服務器。

注意:index.html是你本身建立的,你隨便寫點內容。我是隻在body裏面寫了hello。

代碼解析

Request對象中緩衝大小爲何是2048?

由於大多數瀏覽器請求行最大長度就是2048字節,因此讀取2048字節,裏面必然徹底包含了請求行的數據。這也是parameter傳參長度限制的緣由,由於parameter在URI中,而URI又是組成請求行的元素之一。

注:HTTP請求報文的請求行由三部分組成,請求方法,URI,協議版本,且這三個參數用空格隔開。

前面說的例子有問題在哪裏?

上面的例子是正常的,不過書本里面少了一部分,那就是響應頭的編寫,若是沒有發送響應頭給瀏覽器,它沒法識別發送給它的數據是什麼。

Content-Length在上文中起什麼做用?

細心的朋友會發現,我在響應頭中添加了Content-Length的頭信息,指明瞭文件的長度,也就是字節數。有了這個頭信息,瀏覽器就能夠知道何時數據接收完成。這跟瀏覽器的加載提示有關。

怎麼讓別人也能訪問到這個網頁?

若是你的電腦有公網IP的話,那你要作的只是把程序跑起來掛着,而後開放端口。開放端口是什麼意思?默認狀況下,防火牆會爲了安全,其餘電腦是不能隨便訪問本機的端口(例外,80端口是默認開啓的)。開啓的方法就是進入防火牆設置進站規則,開放8080端口。

感悟

  其實涉及到網絡通訊,底層傳遞的就是一堆字節,而"協議"從一個角度來講,其實就是雙方共同遵照的數據格式,它指明從哪裏到哪裏的字節數據表示的是什麼,應用程序根據這些進行處理。想來,其實這些東西在上《計算機網絡》的時候都講到了,只是當時沒有如今這種感受吧。

相關文章
相關標籤/搜索