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