WebServer(七)
Readme:
~該版本改動:
支持所有介質類型。tomcat中的conf目錄裏有一個web.xml文件,該文件中配置着所有資源後綴與對應的Content-Type的值。我們將該文件拷貝到我們的項目中,並在HttpContex的initMimeMapping方法中解析該文件來初始化介質映射。
~步驟:
- 在項目中新建一個conf目錄。
- 將tomcat中的web.xml文件拷貝到conf目錄中。
- 導入dom4j的jar包。
- 修改HttpContext初始化介質類型的映射方法:initMimeMapping。
WebServer:
HttpContext
package com.senbao.webserver.http;
/*
該類定義了HTTP協議相關信息
*/
public class HttpContext{
/*
狀態代碼與對應狀態描述的映射關係
key:狀態代碼
value:狀態描述
*/
private static Map<Integer,String> STATUS_REASON_MAPPING = new HashMap<Integer,String>();
/*
資源後綴與Content-Type之間的映射關係
key:資源的後綴名
value:該資源對應的Content-Type的值
注:不同的資源對應的Content-Type的值在w3c上都有定義,可前往w3c官網查詢MIME定義
*/
private static Map<String,String> MIME_MAPPING = new HashMap<>();
static{
initStatusReasonMapping();
initMIMEMAPPING();
}
/*
讀取conf/web.xml文件,將根元素下所有的名爲<mime-mapping>的子元素讀取出來,然後將每個<mime-mapping>元素中的子元素<extension>之間的文本作爲key,將子元素<mime-type>中間的文本作爲value,存入到MIME_MAPPING中,完成初始化
*/
private static void initMIMEMAPPING(){
try {
SAXReader reader = new SAXReader();
Doucument doc = reader.read(new File("conf/web.xml"));
Element root = doc.getRooyElement();
List<Element> mimeList = root.elements("mime-mapping");
for(Element e : mimeList){
MIME_MAPPING.put(e.element("extension").getText(),e.element("mime-type").getText());
}
}catch(Exception e){
e.printStckTrace();
}
}
/*
初始化狀態代碼與描述的映射MAP
*/
private static void initMIMEMAPPING(){
STATUS_REASON_MAPPING.put(200, "OK");
STATUS_REASON_MAPPING.put(302, "Move Temporaily");
STATUS_REASON_MAPPING.put(404, "Not Found");
STATUS_REASON_MAPPING.put(500, "Internal Server Error");
}
/**
* 根據給定的狀態代碼獲取對應的狀態描述
* @param statusCode
* @return
*/
public static String getStatusReason(int statusCode){
return STATUS_REASON_MAPPING.get(statusCode);
}
/**
* 根據資源後綴名獲取對應的Content-Type的值
* @param ext
* @return
*/
public static String getMimeType(String ext) {
return MIME_MAPPING.get(ext);
}
public static void main(String[] args) {
String reason = getStatusReason(200);
System.out.println(reason);
//介質
String type = getMimeType("css");
System.out.println(type);
}
}
WebServer
package com.senbao.webserver.core;
/*
WebServer主類
*/
public class WebServer{
private ServerSocket server;
public WebServer(){
try{
//tomcat默認開啓的端口號是8080
server = new ServerSocket(8080);
}catch(Exception e){
e.printStackTrace();
}
}
public void start(){
try{
while(true){
System.out.println("等待客戶端連接...");
Socket socket = server.accept();
System.out.println("一個客戶端連接了!");
//啓動一個線程處理客戶端請求
ClientHandler handler = new CilentHandler(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();
}
}
EmptyRequestException
package com.senbao.webserver.core;
/*
空求情異常
當客戶端發送連接後發生空請求時,HttpRequest的構造方法會拋出該異常
*/
public class EmptyRequestException extends Exception{
private static final long serialVersionUID = 1L;
public EmptyRequestException() {
super();
}
public EmptyRequestException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public EmptyRequestException(String message, Throwable cause) {
super(message, cause);
}
public EmptyRequestException(String message) {
super(message);
}
public EmptyRequestException(Throwable cause) {
super(cause);
}
}
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:解析請求,創建HttpRequest
創建響應對象HttpResponse
2:處理請求
3:給予響應
*/
try{
//1.解析請求,生成HttpRequest
HttpRequest request = new HttpRequest(socket);
HttpResponse response = new HttpResponse(socket);
//2.處理請求
/*
通過request獲取請求的資源路徑,從webapps中尋找資源
*/
String url = request.getUrl();
File file = new File("webaps" + url);
if(file.exists()){
System.out.println("資源已找到!");
/*
以一個標準的TTP響應格式回覆給客戶端
*/
response.setStatusCode(200);
response.setEntity(file);
}else{
System.out.println("資源未找到!");
File file = new File("webapps/myweb/404.html");
response.setStatusCode(404);
response.setEntity(file);
}
//響應客戶端
response.flush();
}catch(EmptyRequestExceptione){
System.out.println("空請求!");
}catch(Exception e){
e.printStackTrace();
}filnally{
try{
socket.close();
}catsh(IOException e){
e.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> headers = new HashMap<String,String>();
public HttpRequest(Socket socket) throws EmptyRequestException{
System.out.println("HttpRequest:開始解析請求");
try{
this.socket = socket;
this.in = socket.getInputStream();
/*
* 1:解析請求行
* 2:解析消息頭
* 3:解析消息正文
*/
//1
parseRequestLine();
//2
parseHeaders();
//3
parseContent();
}catch(EmptyRequestException e){
//將空請求拋給ClientHandler
throw e;
}catch(Exception e){
e.printStackTrace();
}
}
/*
解析請求行
*/
private void parseRequestLine throws EmptyRequestException{
System.out.println("解析請求行...");
/*
大致流程:
1:通過輸入流讀取第一行字符串
2:將請求行按照空格拆分爲三項
3:將拆分的三項分別放置到method、url、protocol
解析請求行時,在獲取拆分後的數組元素時可能會引發數組小標越界,這是由於HTTP協議允許客戶端發送一個空請求過來導致的。
*/
String line = readLine();
String[] data = line.split("\\s");
//判斷拆分請求行內容是否能達到三項
if(data.length < 3){
throw new EmptyRequestException();
}
this.method = data[0];
this.url = data[1];
this.protocol = data[2];
System.out.println("method:"+method);// GET
System.out.println("url:"+url);// /index.html
System.out.println("protocol:"+protocol);// HTTP/1.1
System.out.println("請求行解析完畢");
}
/*
解析請求頭
*/
private void parseHeaders(){
System.out.println("解析消息頭...");
/*
大致步驟:
1:繼續使用readLine方法讀取若干行內容,每一行都應該是一個消息頭
2:當readLine方法返回值爲空字符的時候則停止循環(單獨讀到了CRLF時readLine方法返回值應該當爲空字符串)
3:每當讀取一個消息頭信息時都應當按照“: ”拆分爲兩項,第一項爲消息頭名字,第二項爲消息頭對應的值,將名字作爲key,將對應的值作爲value存入到headers中
*/
while(true){
String line = readLine();
//判斷是否單獨讀到了CRLF
if("".equals(line)){
break;
}
String[] data = line.split(":\\s");
headers.put(data[0],data[1]);
}
System.out.println("headers:"+headers);
System.out.println("消息頭解析完畢");
}
/**
* 解析消息正文
*/
private void parseContent(){
System.out.println("解析消息正文...");
System.out.println("消息正文解析完畢");
}
/*
通過給定的輸入流讀取一行字符串(以CRLF結尾)
*/
private String readLine(){
try{
StringBuilder builder = new StringBuiler();
//c1表示上次讀到的字符,c2表示本次讀到的字符
char c1 = 'a',c2 = 'a';
int d = -1;
while((d = in.read()) != -1){
c2 = (char)d;
if(c1 = 13 && c2 = 10){
break;
}
builder.append(c2);
c1 = c2;
}
return builder.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 headers.get(name);
}
}
HttpResponse
package com.senbao.webserver.http;
/*
響應對象
該類的每個實例用於表示一個服務端發送給客戶端的響應內容
*/
public class HttpResponse {
private Socket socket;
private OutputStream out;
/*
狀態行相關信息定義
*/
//狀態代碼
private int statusCode;
/*
響應頭相關信息定義
*/
private Map<String,String> headers = new HashMap<>();
/*
響應正文相關信息定義
*/
//要響應的實體文件
private File entity;
public HttpResponse(Socket socket){
try{
this.socket = socket;
this.out = socket.getOutputStream();
}catch(Exception e){
e.printStackTrace();
}
}
/*
將響應的內容按照Http協議格式發送給客戶端
*/
public void flush(){
/*
響應客戶端做三件事
1:發送狀態行
2:發送響應頭
3:發送響應正文
*/
sendStatusLine();
sendHeaders();
sendContent();
}
/*
發送狀態行
*/
private void sendStatusLine(){
try{
String line = "HTTP/1.1" + " " + statusCode + " " + HttpContext.getStatusReason(statusCode);
println(line);
}catch (Exception e) {
e.printStackTrace();
}
}
/*
發送響應頭
*/
private void sendHeaders(){
try{
Set<Entry<String,String>> set = header.entrySet();
for(Entry<String,String> header : set){
String name = header.getKey();
String value = header.getValue();
String line = name + " " + value;
println(line);
}
//表示響應頭部分發送完畢
println("");
}catch (Exception e) {
e.printStackTrace();
}
}
/*
發送響應正文
*/
private void sendContent(){
try(
FileInputStream fis = new FileInputStream(entity);
){
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();
}
}
private void println(String line){
try{
out.write(line.getBytes("ISO8859-1"));
out.write(13);
out.write(10);
}catch (Exception e) {
e.printStackTrace();
}
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public File getEntity() {
return entity;
}
/*
設置響應的實體文件數據,該方法會自動添加對應的兩個響應頭
Cotent-Type Content-Length
*/
public void setEntity(File entity){
this.entity = entity;
/*
添加響應頭Content-Length
*/
headers.put("Content-Length",entity.length()+"");
/*
添加響應頭Content-Type
1:先通過Entity獲取該文件的名字
2:獲取該文件名的後綴名
3:通過HttpContext根據後綴名獲取到對應的Content-Type的值
4:向headers中設置該響應頭的信息
*/
String name = entity.getName();
String ext = name.subString(name.lastIndexOf(".")+1);
String type = HttpContext.getMimeType(ext);
this.headers.put("Content-Type",type);
}
/*
添加一個響應頭
name:響應頭的名字
value:響應頭的值
*/
public void putHeders(String name,String value){
this.headers.put(name,value);
}
}