WebServer(四)
Readme:
~該版本改動:
- 將客戶端請求的頁面響應給客戶端。
- 該版本要了解HTTP協議中的響應(response)規則。要嚴格按照HTTP協議的響應格式給客戶端響應,這樣瀏覽器才能得到正確的結果。
- 步驟:
~在項目中添加一個新目錄webapps,使用該目錄保存不同網站所需要的素材等內容。
~在webapps目錄下新建子目錄myweb,作爲我們測試的網站資源目錄。
注:在tomcat中webapps目錄是存放所有web應用的,其每一個子目錄都是一個具體的web應用(一個網站內容、涵蓋頁面、圖片等素材以及處理業務邏輯的java代碼)。
~在myweb中添加頁面index.html
~瀏覽器的地址欄請求某個服務端資源時,是無法寫在服務端資源的絕對路徑的,只能寫相對路徑(沒有平臺差異性)。
eg:http://localhost:8080/index.html 協議部分 服務器地址信息 資源路徑部分(請求行url內容)
我們可以規定在資源路徑部分中的gen根爲我們定義的保存所有資源目錄webapps。若想找到webapps/myweb/index.html頁面的話,瀏覽器地址欄要輸入爲:http://localhost:8080/myweb/index.html
當該請求發送到服務端時,我們解析請求中請求行裏的url部分會得到/myweb/index.html,那我們就對應的從webapps目錄中找到對應資源:webapps/myweb/index.html並將該資源響應給客戶端(在ClientHandler中添加分支判斷,查找客戶端請求的資源是否存在)。
~用一個標準的http響應格式,將用戶請求的資源回覆給客戶端。
HTTP:
~響應:
- 是服務端發送給客戶端的內容。
- 組成:
~狀態行:protocol status_code status_reason(CRLF)
協議版本 狀態代碼 狀態描述
狀態代碼是一個三位數字,有5類:
1xx:消息,在HTTP1.0協議時爲保留部分,未使用
2xx:成功,表示客戶端請求成功
3xx:重定向,表示要求客戶端需要進一步操作才能完成請求
4xx:客戶端錯誤,表示客戶端的請求無效
5xx:服務端錯誤,表示請求被接收,但是服務端處理髮生了錯誤
常見狀態碼:
200:請求已接收,並正常響應客戶端
302:要求客戶端進一步請求服務端指定的路徑
404:客戶端請求的資源未找到
500:服務端發生錯誤
~響應頭:響應頭的格式和意義跟請求中的消息頭一樣。
~響應正文:是二進制數據,是服務端響應給客戶端的實體數據,通常就是客戶端所請求的資源。
一個響應中是否有響應正文可以通過響應頭中的兩個頭信息得知:
Content-Type:說明響應正文中的數據類型
Content-Length:說明響應正文的長度(字節量)
客戶端就是通過這兩個頭來讀取並理解響應正文內容的。 - 一個響應的內容大致爲:
HTTP/1.1 200 OK(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 14424(CRLF)(CRLF)
101001010100100101010...
WebServer:
ClientHandler
package com.senbao.webserver.core;
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
public void run() {
/*
* 處理該客戶端的請求的大致步驟
* 1.解析請求
* 2.處理請求
* 3.給予響應
*/
try {
//1.解析請求,生成HttpRequest對象
HttpRequest request = new HttpRequest(socket);
//2處理請求
/*
* 通過request獲取請求資源路徑,從webapps中尋找對應資源
* http://localhost:8080/myweb/index.html
*/
String url = request.getUrl();
File file = new File("webapps" + url);
if(file.exists()) {
System.out.println("資源已存在");
/*
* 一個標準的http響應格式回覆客戶該資源
*/
OutputStream out = socket.getOutputStream();
//發送狀態行
String line = "HTTP/1.1 200 OK";
out.write(line.getBytes("ISO8859-1"));
out.write(13);//written CR
out.write(10); //written LF
//發送響應頭
line = "Content-Type: text/html";
out.write(line.getBytes("ISO8859-1"));
out.write(13);//written CR
out.write(10); //written LF
line = "Content-Length:"+file.length();
out.write(line.getBytes("ISO8859-1"));
out.write(13);//written CR
out.write(10); //written LF
//表示響應頭髮送完畢
out.write(13);//written CR
out.write(10); //written LF
//發送響應正文
FileInputStream fis = new FileInputStream(file);
byte[] data = new byte[1024*10];
int len = -1;
while((len = fis.read(data)) != -1) {
out.write(data, 0, len);
}
}else{
System.out.println("資源未找到");
/*
* 一個標準的http響應格式回覆客戶該資源
*/
file = new File("webapps/myweb/404.html");
OutputStream out = socket.getOutputStream();
//發送狀態行
String line = "HTTP/1.1 400 ERROR";
out.write(line.getBytes("ISO8859-1"));
out.write(13);
out.write(10);
//發送響應頭
line = "Content-Type: text/html";
out.write(line.getBytes("ISO8859-1"));
out.write(13);
out.write(10);
line = "Content-Length: " + file.length();
out.write(line.getBytes("ISO8859-1"));
out.write(13);
out.write(10);
//響應頭髮送完畢
out.write(13);
out.write(10);
//發送消息正文
FileInputStream fis = new FileInputStream(file);
byte[] data = new byte[1024*10];
int len = -1;
while((len = fis.read(data)) != -1) {
out.write(data, 0, len);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//響應後與客戶端斷開連接
try {
socket.close();
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
}
HttpRequest
package com.senbao.webserver.http;
/*
* HttpRequest表示一個Http協議要求的請求信息
* 一個請求包含三部分:請求行 消息頭 消息正文
*/
public class HttpRequest {
//對應客戶端的socket
private Socket socket;
//通過socket獲取到的輸入流,用於讀取客戶端發送的請求
private InputStream in;
/*
* 請求行相關信息定義
*/
//請求方式
private String method;
//資源路徑
private String url;
//請求使用的協議版本
private String protocol;
/*
* 消息頭相關信息
*/
private Map<String,String> handlers = new HashMap<>();
/**
* 實例化HttpRequest使用的構造方法,需要將對應客戶端的socket傳入,
* 以便讀取客戶端發送過來的請求內容
* @param socket
*/
public HttpRequest(Socket socket) {
System.out.println("HttpRequest:開始解析請求");
try {
this.socket = socket;
this.in = socket.getInputStream();
/*
* 1.解析請求行
* 2.解析消息頭
* 3.解析消息正文
*/
//1
parseRequestLine();
//2
parseHeaders();
//3
parseContent();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 解析請求行
*/
private void parseRequestLine() {
System.out.println("解析請求行");
/*
* 大致流程:
* 1.通過輸入流讀取第一行字符串
* 2.將請求行按照空格拆分爲三項
* 3.將拆分的三項分別設置到method,url,protocol三個屬性上
*
* 解析請求行時,在獲取才分後的數組元素時可能會引發數組下標越界,這是由於HTTP協議
* 允許客戶端發送一個空請求過來導致的
* 我們後面解決
*/
String line = readLine();
String[] array = line.split("\\s");
method = array[0];
url = array[1];
protocol = array[2];
System.out.println("method:"+method);
System.out.println("url:"+url);
System.out.println("protocal:"+protocol);
System.out.println("解析完畢");
}
/**
* 解析消息頭
*/
private void parseHeaders(){
System.out.println("解析消息頭");
/*
* 大致步驟
* 1.繼續使用readLine方法讀取若干行內容,每一行都是一個消息頭
* 2.當readLine方法返回值爲空字符串時,停止循環(單獨讀到CRLF時,readLine返回空字符串)
* 3.每當讀取一個消息頭信息時應當按照“:”拆分爲兩項,第一項作爲消息頭名字,第二項作爲
* 消息頭對應的值,將名字作爲key,值作爲value存入到handlers這個map中
*/
while(true) {
String line = readLine();
//判斷是否單獨讀取到了CRLF
if("".equals(line)) {
break;
}
String[] data = line.split(":\\s");
handlers.put(data[0], data[1]);
}
System.out.println("handler:" + handlers);
System.out.println("解析完畢");
}
/**
* 解析消息正文
*/
private void parseContent() {
System.out.println("解析消息正文");
System.out.println("解析完畢");
}
/**
* 通過給定的輸入流讀取一行字符串(以CRLF結尾)
* @param in
* @return
*/
private String readLine() {
try {
StringBuilder bulder = new StringBuilder();
int d = -1;
char c1 = 'a',c2 = 'a';
while((d = in.read())!=-1) {
c2 = (char)d;
/*
* 在ASCII碼中CR的編碼對應的數字爲13, LF對應的編碼爲10
* 就好比字符a對應的編碼爲97
*/
if(c1 == 13 && c2 == 10) {
break;
}
bulder.append(c2);
c1 = c2;
}
return bulder.toString().trim();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getProtocol() {
return protocol;
}
public String getHeader(String name) {
return handlers.get(name);
}
}
WebServer
package com.tedu.webserver.core;
public class WebServer {
private ServerSocket server;
public WebServer(){
try {
//Tomcat默認開啓的端口就是8080
server = new ServerSocket(8080);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
try {
// while(true) {
System.out.println("等待客戶端連接……");
Socket socket = server.accept();
System.out.println("一個客戶端連接了!");
//啓動一個線程,處理客戶端請求
ClientHandler handler = new ClientHandler(socket);
Thread t = new Thread(handler);
t.start();
// }
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
WebServer server = new WebServer();
server.start();
}
}