day20-WebServer(十二)

Readme:

  HTTP協議要求,以地址欄形式傳遞數據時,是不能含有中文的(請求行、消息頭中的內容都不能有中文)。當我們希望從客戶端傳遞中文時,常見的方法是先將中文對應的字符集轉爲2進制,然後再將每個字節的2進制內容以2位16進制形式表示,前面加一個%,這樣每個字節的內容就可以以字符串"%XX"的格式表示一個字節的內容。這種形式避免了傳遞中文的問題。
  那麼在服務端接收到這樣的數據後,我們還要進行一個反向操作,將%XX的內容還原爲2進制,再按照指定字符集轉換爲對應的文字。Java提供了API:URLDecoder,可以很方便的解決這個問題。
  將HttpRequest中的進一步解析url的操作改一下,在獲取到queryString後使用URLDecoder對內容進行轉碼。

WebServer:

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 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();
	}
}

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.getRequestURI();
			/*
			 * 判斷是否請求業務
			 * 1;先根據用戶請求獲取對應的Servlet名字
			 * 2:若得到的名字不爲null,說明對應的是業務
			 */
			String servletName = ServletContext.getServletName(url);
			if(servletName != null) {
				//加載該Servlet
				Class cls = Class.forName(servletName);
				System.out.println("請求"+url+",正在實例化對應的:"+servletName);
				//實例化
				HttpServlet servlet = (HttpServlet)cls.newInstance();
				//調用service方法處理業務
				servlet.service(request, response);
			}else {
				File file = new File("webapps"+url);
				if(file.exists()){
					System.out.println("資源已找到!");
					/*
					 * 以一個標準的HTTP響應格式回覆客戶端該資源
					 */				
					response.setStatusCode(200);
					response.setEntity(file);						
				}else{
					System.out.println("資源未找到!");
					file = new File("webapps/myweb/404.html");
					response.setStatusCode(404);			
					response.setEntity(file);
				}
			}
			//3響應客戶端
			response.flush();
		}catch(EmptyRequestException e) {
			System.out.println("空請求!");
		} catch (Exception e) {
			e.printStackTrace();
		} finally{
			//響應後與客戶端斷開連接
			try {
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

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);
	}
	
}

ServletContext

package com.senbao.webserver.core;

/**
 * 服務端環境信息
 */
public class ServletContext {
	/**
	 * 請求與Servlet映射
	 * key:請求路徑
	 * value:對應Servlet的名字
	 */
	private static Map<String,String> servletMapping = new HashMap<>();
	static {
		initServletMapping();
	}
	/**
	 * 初始化請求與Servlet映射信息
	 * @throws DocumentException 
	 * @throws IOException 
	 */
	private static void initServletMapping() {
		/*
		 * 讀取conf/server.xml文件
		 * 將所有<servlet>標籤解析出來,用其中的url屬性作爲key,className屬性的值作爲value
		 * 存入到initServletMapping中
		 */
		try {
			SAXReader reader = new SAXReader();
			Document doc = reader.read(new File("conf/server.xml"));
			Element root = doc.getRootElement();
			//獲取servlets標籤
			Element servlets = root.element("servlets");
			//獲取所有servlet標籤
			List<Element> servletList= servlets.elements();
			//遍歷每個servlet標籤
			for(Element servletEle : servletList) {
				servletMapping.put(servletEle.attributeValue("url"),servletEle.attributeValue("className"));
				System.out.println(servletEle.asXML());
			}
		}catch (Exception e) {
			e.printStackTrace();
		}
	}
	/**
	 * 根據請求獲取對應的Servlet名字
	 * @param url
	 * @return
	 */
	public static String getServletName(String url) {
		return servletMapping.get(url);
	}
	
	public static void main(String[] args) {
		String servletName = getServletName("/myweb/login");
		System.out.println(servletName);
	}
}

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-Tpye的值
	 * 注:不同的資源對應的Content-Type的值在w3c上都有定義,可前往w3c官網查詢MIME定義
	 */
	private static Map<String,String> MIME_MAPPING = new HashMap<String,String>();
	static{
		initStatusReasonMapping();
		initMIMEMAPPING();
	}
	private static void initMIMEMAPPING(){
		/*
		 * 讀取conf/web.xml文件,將根元素下所有名爲<mime-mapping>的子元素讀取出來,然後將每個
		 * <mime-mapping>元素中的子元素<extension>之間的文本作爲key,將子元素<mime-type>中間的
		 * 文本作爲value,存入到MIME_MAPPING中,完成初始化
		 */
		try {
			SAXReader reader = new SAXReader();
			Document doc = reader.read(new FileInputStream("conf/web.xml"));
			Element root = doc.getRootElement();
			@SuppressWarnings("unchecked")
			List<Element> mimeList = root.elements("mime-mapping");
			for(Element e : mimeList) {
				if(e.element("extension") != null && e.element("mime-type") != null) {
					MIME_MAPPING.put(e.element("extension").getText(),e.element("mime-type").getText());
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	/**
	 * 初始化狀態代碼與描述的映射MAP
	 */
	private static void initStatusReasonMapping(){
		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);
	}
}

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;
	
//	url中的請求部分
	private String requestURI;
	
	//url中的參數部分
	private String queryString;
	
	//url中的所有參數 key是參數名 value是參數值
	private Map<String,String> parameters = new HashMap<>();
	
	/*
	 * 消息頭相關信息
	 */
	private Map<String,String> headers = new HashMap<String,String>();
	
	/**
	 * 實例化HttpRequest使用的構造方法,需要將對應
	 * 客戶端的Socket傳入,以便讀取該客戶端發送過來
	 * 的請求內容
	 * @param socket
	 * @throws EmptyRequestException 
	 */
	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();
		}
	}
	/**
	 * 解析請求行
	 * @throws EmptyRequestException 
	 */
	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];
		//進一步解析URL部分
		parseURL();
		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("請求行解析完畢");
	}
	
	/**
	 * 進一步對url進行解析
	 * 將url中的請求部分設置到屬性requestURI上
	 * 將url中的參數部分設置到屬性queryString上
	 * 再對參數部分進一步解析,將每個參數都存入到屬性parameters中
	 * 
	 * 若該url不含有參數部分,則直接將url的值賦值給requestURI,參數部分不做任何處理
	 */
	private void parseURL() {
		System.out.println("開始解析url:"+url);
		
		/**
		 * 思路:
		 * url是否含有參數,可以根據該url中是否含有?來決定。若有則按照?拆分爲兩部分
		 * 第一部分爲請求部分,第二部分爲參數部分,設置到對應屬性即可。
		 * 然後再對參數進行拆分,最終將每個參數的名字作爲key,值作爲value存到parameters中
		 * 若不含參數,則直接將url賦值給requestURI即可
		 * 
		 *  /myweb/reg?username=fan&password=123&....
		 */
		if(this.url.indexOf("?") != -1) {
			String[] data = url.split("\\?");
			this.requestURI = data[0];
			if(data.length>1) {
				this.queryString = data[1];
				/*
				 * 對參數部分進行轉碼操作,將所有%XX的內容按照對應字符集還原爲字符串
				 */
				System.out.println("開始對queryString進行轉碼:");
				System.out.println("轉碼前:"+queryString);
				try {
					queryString = URLDecoder.decode(queryString,"UTF-8");
				} catch (UnsupportedEncodingException e) {
					e.printStackTrace();
				}
				System.out.println("轉碼後:"+queryString);
				
				String[] paras = queryString.split("&");
				for (String paraStr : paras) {
					String[] paraDate = paraStr.split("=");
					if(paraDate.length>1) {
						this.parameters.put(paraDate[0], paraDate[1]);
					}else {
						this.parameters.put(paraDate[0], null);
					}
				}
			}
		}else {
			this.requestURI = this.url;
		}
		
		
		System.out.println("requestURI:"+requestURI);
		System.out.println("queryString:"+queryString);
		System.out.println("parameters:"+parameters);
		System.out.println("url解析完畢");
	}
	
	/**
	 * 解析消息頭
	 */
	private void parseHeaders(){
		System.out.println("解析消息頭...");
		/*
		 * 大致步驟:
		 * 1:繼續使用readLine方法讀取若干行內容
		 *   每一行應該都是一個消息頭
		 * 2:當readLine方法返回值爲空字符串時則
		 *   停止循環讀取工作(單獨讀取到了CRLF時
		 *   readLine方法返回值應當爲空字符串)
		 * 3:每當讀取一個消息頭信息時應當按照": "
		 *   拆分爲兩項,第一項爲消息頭名字,第二項
		 *   爲消息頭對應的值,將名字作爲key,將
		 *   值作爲value存入到屬性headers這個Map中。    
		 */
		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結尾)
	 * @param in
	 * @return
	 */
	private String readLine(){
		try {
			StringBuilder builder = new StringBuilder();
			int d = -1;
			//c1表示上次讀到的字符,c2表示本次讀到的字符
			char c1 ='a',c2 = 'a';
			while((d = in.read())!=-1){
				c2 = (char)d;
				/*
				 * 在ASC編碼中CR的編碼對應的數字爲13
				 * LF編碼對應的數字爲10
				 * 就好比字符a的編碼對應的數字爲97
				 */
				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);
	}
	public String getRequestURI() {
		return requestURI;
	}
	public String getQueryString() {
		return queryString;
	}
	/**
	 * 根據給定的參數名獲取對應的參數值
	 * @param name
	 * @return
	 */
	public String getParameter(String name) {
		return this.parameters.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 {
			//遍歷headers,將所有消息頭髮送給客戶端
			Set<Entry<String,String>> set = headers.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();
		}
	}
	
	/**
	 * 將給定字符串按行發送給客戶端(以CRLF結尾)
	 * @param line
	 */
	private void println(String line){
		try {
			out.write(line.getBytes("ISO8859-1"));
			out.write(13);//written CR
			out.write(10);//written LF
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public int getStatusCode() {
		return statusCode;
	}


	public void setStatusCode(int statusCode) {
		this.statusCode = statusCode;
	}

	public File getEntity() {
		return entity;
		
	}

	/**
	 * 設置響應的實體文件數據
	 * 該方法會自動添加對應的兩個響應頭:
	 * Content-Type   Content-Length
	 * @param entity
	 */
	public void setEntity(File entity) {
		this.entity = entity;
		/*
		 * 1:添加響應頭Content-Length
		 */
		headers.put("Content-Length", entity.length()+"");
		/*
		 * 2:添加響應頭Content-Type
		 * 	2.1:先通過Entity獲取該文件的名字
		 * 	2.2:獲取該文件名的後綴名
		 * 2.3:通過HttpContext根據該後綴名獲取到對應的Content-Type的值
		 * 2.4:想headers中設置該響應頭的信息
		 */
		//2.1例如
		String name  = entity.getName();
		//2.2 
		String ext  = name.substring(name.lastIndexOf(".")+1);
		//2.3
		String type = HttpContext.getMimeType(ext);
		this.headers.put("Content-Type", type);
	}
	
	/**
	 * 添加一個響應頭
	 * @param name 響應頭的名字
	 * @param value 響應頭對應的值
	 */
	public void putHeaders(String name,String value) {
		this.headers.put(name, value);
	}
	
}

HttpServlet

package com.senbao.webserver.servlet;
/**
 * 所有Servlet的超類,規定了Servlet的功能
 */
public abstract class HttpServlet {
	public abstract void service(HttpRequest request,HttpResponse response);
	
	/**
	 * 跳轉到指定路徑
	 * 在TOMCAT中實際上是定義在轉發器上的功能
	 * TOMCAT以鏈式的結構將各組件之間串聯在一起,進行跳轉調用
	 * @param url
	 * @param request
	 * @param response
	 */
	public void forward(String url,HttpRequest request,HttpResponse response) {
		response.setStatusCode(200);
		response.setEntity(new File("webapps"+url));
	}
}

LoginServlet

package com.senbao.webserver.servlet;

public class LoginServlet extends HttpServlet{
	public void service(HttpRequest request,HttpResponse response) {
		try {
			System.out.println("開始處理登錄業務");
			String username = request.getParameter("username");
			String password = request.getParameter("password");
			
			try (
					RandomAccessFile raf = new RandomAccessFile("user.dat", "r");
			){
				boolean check = false;
				for (int i = 0; i < raf.length()/100; i++) {
					raf.seek(i*100);
					byte[] data = new byte[32];
					raf.read(data);
					String name = new String(data,"UTF-8").trim();
					if(name.equals(username)) {
						raf.read(data);
						String psw = new String(data,"UTF-8").trim();
						if(psw.equals(password)) {
							check = true;
							forward("/myweb/login_success.html", request, response);
							break;
						}
					}
				}
				if(!check) {
					forward("/myweb/login_fail.html", request, response);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
}

RegServlet

package com.tedu.webserver.servlet;

/**
 * 處理註冊業務
 */
public class RegServlet extends HttpServlet{
	public void service(HttpRequest request,HttpResponse response) {
		try {
			System.out.println("開始處理註冊業務");
			String username = request.getParameter("username");
			String password = request.getParameter("password");
			String nickname = request.getParameter("nickname");
			int age = Integer.parseInt(request.getParameter("age"));
			
			try (
					RandomAccessFile raf = new RandomAccessFile("user.dat", "rw");
			){
				raf.seek(raf.length());
				
				//用戶名
				byte[] data = username.getBytes("UTF-8");
				data = Arrays.copyOf(data, 32);
				raf.write(data);
				
				//密碼
				data = password.getBytes("UTF-8");
				data = Arrays.copyOf(data, 32);
				raf.write(data);
				
				//暱稱
				data = nickname.getBytes("UTF-8");
				data = Arrays.copyOf(data, 32);
				raf.write(data);
				
				//年齡
				raf.writeInt(age);
				
				System.out.println("username:"+username);
				System.out.println("password:"+password);
				System.out.println("nickname:"+nickname);
				System.out.println("age:"+age);
			} catch (Exception e) {
				e.printStackTrace();
			}
			
			forward("/myweb/reg_success.html", request, response);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	 
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章