<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
HTTP請求
首行: [方法] + [url] + [版本]
Header: 請求的屬性, 冒號分割的鍵值對;每組屬性之間使用n分隔;遇到空行表示Header部 分結束 Body: 空行後面的內容都是Body.
Body允許為空字串. 如果Body存在, 則在Header中會有 一個Content-Length屬性來標識Body的長度
HTTP響應
首行: [版本號] + [狀態碼] + [狀態碼解釋]
Header: 請求的屬性, 冒號分割的鍵值對;每組屬性之間使用n分隔;遇到空行表示Header部 分結束
Body: 空行後面的內容都是Body. Body允許為空字串. 如果Body存在, 則在Header中會有 一個Content-Length屬性來標識Body的長度; 如果伺服器返回了一個html頁面, 那麼html頁 面內容就是在body中
實現一個最簡單的 HTTP 伺服器.
在這個版本中, 我們只是簡單解析 GET 請求, 並根據請求的路徑來構造出不同的響應.
路徑為 /200, 返回一個 "歡迎頁面".
路徑為 /404, 返回一個 "沒有找到" 的頁面.
路徑為 /302, 重定向到其他的頁面
import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class HttpServerV1 { // HTTP 底層要基於 TCP 來實現. 需要按照 TCP 的基本格式來先進行開發. private ServerSocket serverSocket = null; public HttpServerV1(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("伺服器啟動"); ExecutorService executorService = Executors.newCachedThreadPool(); while (true) { // 1. 獲取連線 Socket clientSocket = serverSocket.accept(); // 2. 處理連線(使用短連線的方式實現) executorService.execute(new Runnable() { @Override public void run() { process(clientSocket); } }); } } private void process(Socket clientSocket) { // 由於 HTTP 協定是文字協定, 所以仍然使用字元流來處理. try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) { // 下面的操作都要嚴格按照 HTTP 協定來進行操作. // 1. 讀取請求並解析 // a) 解析首行, 三個部分使用空格切分 String firstLine = bufferedReader.readLine(); String[] firstLineTokens = firstLine.split(" "); String method = firstLineTokens[0]; String url = firstLineTokens[1]; String version = firstLineTokens[2]; // b) 解析 header, 按行讀取, 然後按照冒號空格來分割鍵值對 Map<String, String> headers = new HashMap<>(); String line = ""; // readLine 讀取的一行內容, 是會自動去掉換行符的. 對於空行來說, 去掉了換行符, 就變成空字串 while ((line = bufferedReader.readLine()) != null && line.length() != 0) { // 不能使用 : 來切分. 像 referer 欄位, 裡面的內容是可能包含 : . String[] headerTokens = line.split(": "); headers.put(headerTokens[0], headerTokens[1]); } // c) 解析 body (暫時先不考慮) // 請求解析完畢, 加上一個紀錄檔, 觀察請求的內容是否正確. System.out.printf("%s %s %sn", method, url, version); for (Map.Entry<String, String> entry : headers.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } System.out.println(); // 2. 根據請求計算響應 // 不管是啥樣的請求, 咱們都返回一個 hello 這樣的 html String resp = ""; if (url.equals("/200")) { bufferedWriter.write(version + " 200 OKn"); resp = "<h1>hello</h1>"; } else if (url.equals("/404")) { bufferedWriter.write(version + " 404 Not Foundn"); resp = "<h1>not found</h1>"; } else if (url.equals("/302")) { bufferedWriter.write(version + " 303 Foundn"); bufferedWriter.write("Location: http://www.sogou.comn"); resp = ""; } else { bufferedWriter.write(version + " 200 OKn"); resp = "<h1>default</h1>"; } // 3. 把響應寫回到使用者端 bufferedWriter.write("Content-Type: text/htmln"); bufferedWriter.write("Content-Length: " + resp.getBytes().length + "n"); // 此處的長度, 不能寫成 resp.length(), 得到的是字元的數目, 而不是位元組的數目 bufferedWriter.write("n"); bufferedWriter.write(resp); // 此處這個 flush 就算沒有, 問題也不大. 緊接著 // bufferedWriter 物件就要被關閉了. close 時就會自動觸發重新整理操作. bufferedWriter.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) throws IOException { HttpServerV1 server = new HttpServerV1(9090); server.start(); } }
在這個版本中, 我們只是簡單解析 GET 請求, 並根據請求的路徑來構造出不同的響應.
在版本1 的基礎上, 我們做出一些改進:
把解析請求和構造響應的程式碼提取成單獨的類.
能夠把 URL 中的 query string 解析成鍵值對.
能夠給瀏覽器返回 Cookie.
對照著 HTTP 請求的格式, 建立屬性: method, url, version, headers.
建立 patameters, 用於存放 query string 的解析結果.
建立一個靜態方法 build, 用來完成解析 HTTP 請求的過程.
從 socket 中讀取資料的時候注意設定字元編碼方式
建立一系列 getter 方法獲取到請求中的屬性.
單獨寫一個方法 parseKV 用來解析 query string
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; // 表示一個 HTTP 請求, 並負責解析. public class HttpRequest { private String method; // /index.html?a=10&b=20 private String url; private String version; private Map<String, String> headers = new HashMap<>(); private Map<String, String> parameters = new HashMap<>(); // 請求的構造邏輯, 也使用工廠模式來構造. // 此處的引數, 就是從 socket 中獲取到的 InputStream 物件 // 這個過程本質上就是在 "反序列化" public static HttpRequest build(InputStream inputStream) throws IOException { HttpRequest request = new HttpRequest(); // 此處的邏輯中, 不能把 bufferedReader 寫到 try ( ) 中. // 一旦寫進去之後意味著 bufferReader 就會被關閉, 會影響到 clientSocket 的狀態. // 等到最後整個請求處理完了, 再統一關閉 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); // 此處的 build 的過程就是解析請求的過程. // 1. 解析首行 String firstLine = bufferedReader.readLine(); String[] firstLineTokens = firstLine.split(" "); request.method = firstLineTokens[0]; request.url = firstLineTokens[1]; request.version = firstLineTokens[2]; // 2. 解析 url 中的引數 int pos = request.url.indexOf("?"); if (pos != -1) { // 看看 url 中是否有 ? . 如果沒有, 就說明不帶引數, 也就不必解析了 // 此處的 parameters 是希望包含整個 引數 部分的內容 // pos 表示 ? 的下標 // /index.html?a=10&b=20 // parameters 的結果就相當於是 a=10&b=20 String parameters = request.url.substring(pos + 1); // 切分的最終結果, key a, value 10; key b, value 20; parseKV(parameters, request.parameters); } // 3. 解析 header String line = ""; while ((line = bufferedReader.readLine()) != null && line.length() != 0) { String[] headerTokens = line.split(": "); request.headers.put(headerTokens[0], headerTokens[1]); } // 4. 解析 body (暫時先不考慮) return request; } private static void parseKV(String input, Map<String, String> output) { // 1. 先按照 & 切分成若干組鍵值對 String[] kvTokens = input.split("&"); // 2. 針對切分結果再分別進行按照 = 切分, 就得到了鍵和值 for (String kv : kvTokens) { String[] result = kv.split("="); output.put(result[0], result[1]); } } // 給這個類構造一些 getter 方法. (不要搞 setter). // 請求物件的內容應該是從網路上解析來的. 使用者不應該修改. public String getMethod() { return method; } public String getUrl() { return url; } public String getVersion() { return version; } // 此處的 getter 手動寫, 自動生成的版本是直接得到整個 hash 表. // 而我們需要的是根據 key 來獲取值. public String getHeader(String key) { return headers.get(key); } public String getParameter(String key) { return parameters.get(key); } @Override public String toString() { return "HttpRequest{" + "method='" + method + ''' + ", url='" + url + ''' + ", version='" + version + ''' + ", headers=" + headers + ", parameters=" + parameters + '}'; } }
根據 HTTP 響應, 建立屬性: version, status, message, headers, body
另外建立一個 OutputStream, 用來關聯到 Socket 的 OutputStream.
往 socket 中寫入資料的時候注意指定字元編碼方式.
建立一個靜態方法 build, 用來構造 HttpResponse 物件.
建立一系列 setter 方法, 用來設定 HttpResponse 的屬性.
建立一個 flush 方法, 用於最終把資料寫入 OutputStream
import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.HashMap; import java.util.Map; // 表示一個 HTTP 響應, 負責構造 // 進行序列化操作 public class HttpResponse { private String version = "HTTP/1.1"; private int status; // 狀態碼 private String message; // 狀態碼的描述資訊 private Map<String, String> headers = new HashMap<>(); private StringBuilder body = new StringBuilder(); // 方便一會進行拼接. // 當程式碼需要把響應寫回給使用者端的時候, 就往這個 OutputStream 中寫就好了. private OutputStream outputStream = null; public static HttpResponse build(OutputStream outputStream) { HttpResponse response = new HttpResponse(); response.outputStream = outputStream; // 除了 outputStream 之外, 其他的屬性的內容, 暫時都無法確定. 要根據程式碼的具體業務邏輯 // 來確定. (伺服器的 "根據請求並計算響應" 階段來進行設定的) return response; } public void setVersion(String version) { this.version = version; } public void setStatus(int status) { this.status = status; } public void setMessage(String message) { this.message = message; } public void setHeader(String key, String value) { headers.put(key, value); } public void writeBody(String content) { body.append(content); } // 以上的設定屬性的操作都是在記憶體中倒騰. // 還需要一個專門的方法, 把這些屬性 按照 HTTP 協定 都寫到 socket 中. public void flush() throws IOException { BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream)); bufferedWriter.write(version + " " + status + " " + message + "n"); headers.put("Content-Length", body.toString().getBytes().length + ""); for (Map.Entry<String, String> entry : headers.entrySet()) { bufferedWriter.write(entry.getKey() + ": " + entry.getValue() + "n"); } bufferedWriter.write("n"); bufferedWriter.write(body.toString()); bufferedWriter.flush(); } }
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class HttpServerV2 { private ServerSocket serverSocket = null; public HttpServerV2(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("伺服器啟動"); ExecutorService executorService = Executors.newCachedThreadPool(); while (true) { Socket clientSocket = serverSocket.accept(); executorService.execute(new Runnable() { @Override public void run() { process(clientSocket); } }); } } public void process(Socket clientSocket) { try { // 1. 讀取並解析請求 HttpRequest request = HttpRequest.build(clientSocket.getInputStream()); System.out.println("request: " + request); HttpResponse response = HttpResponse.build(clientSocket.getOutputStream()); response.setHeader("Content-Type", "text/html"); // 2. 根據請求計算響應 if (request.getUrl().startsWith("/200")) { response.setStatus(200); response.setMessage("OK"); response.writeBody("<h1>hello</h1>"); } else if (request.getUrl().startsWith("/add")) { // 這個邏輯要根據引數的內容進行計算 // 先獲取到 a 和 b 兩個引數的值 String aStr = request.getParameter("a"); String bStr = request.getParameter("b"); // System.out.println("a: " + aStr + ", b: " + bStr); int a = Integer.parseInt(aStr); int b = Integer.parseInt(bStr); int result = a + b; response.setStatus(200); response.setMessage("OK"); response.writeBody("<h1> result = " + result + "</h1>"); } else if (request.getUrl().startsWith("/cookieUser")) { response.setStatus(200); response.setMessage("OK"); // HTTP 的 header 中允許有多個 Set-Cookie 欄位. 但是 // 此處 response 中使用 HashMap 來表示 header 的. 此時相同的 key 就覆蓋 response.setHeader("Set-Cookie", "user=zhangsan"); response.writeBody("<h1>set cookieUser</h1>"); } else if (request.getUrl().startsWith("/cookieTime")) { response.setStatus(200); response.setMessage("OK"); // HTTP 的 header 中允許有多個 Set-Cookie 欄位. 但是 // 此處 response 中使用 HashMap 來表示 header 的. 此時相同的 key 就覆蓋 response.setHeader("Set-Cookie", "time=" + (System.currentTimeMillis() / 1000)); response.writeBody("<h1>set cookieTime</h1>"); } else { response.setStatus(200); response.setMessage("OK"); response.writeBody("<h1>default</h1>"); } // 3. 把響應寫回到使用者端 response.flush(); } catch (IOException | NullPointerException e) { e.printStackTrace(); } finally { try { // 這個操作會同時關閉 getInputStream 和 getOutputStream 物件 clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) throws IOException { HttpServerV2 server = new HttpServerV2(9090); server.start(); } }
強化理解Cookie
Cookie就是一個字串(裡面的內容由程式設計師自己決定)
Cookie從伺服器來,伺服器會在header中引入一個Set-Cookie欄位,對應的值就會儲存在瀏覽器中
Cookie按照域名/地址來存,每個域名/地址有自己的Cookie
Cookei在後續存取相同的域名/地址的請求,就會自動帶上Cookie,伺服器感知到這個Cookie之後就可以在伺服器端進行一些邏輯處理
在版本 2 的基礎上, 再做出進一步的改進.
解析請求中的 Cookie, 解析成鍵值對
解析請求中的 body, 按照 x-www-form-urlencoded 的方式解析.
根據請求方法, 分別呼叫 doGet / doPost
能夠返回指定的靜態頁面. 實現簡單的對談機制.
屬性中新增了 cookies 和 body
新增一個方法 parseCookie, 在解析 header 完成後解析
cookie 新增瞭解析 body 的流程.
import javax.print.attribute.standard.RequestingUserName; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; public class HttpRequest { private String method; private String url; private String version; private Map<String, String> headers = new HashMap<>(); // url 中的引數和 body 中的引數都放到這個 parameters hash 表中. private Map<String, String> parameters = new HashMap<>(); private Map<String, String> cookies = new HashMap<>(); private String body; public static HttpRequest build(InputStream inputStream) throws IOException { HttpRequest request = new HttpRequest(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); // 1. 處理首行 String firstLine = bufferedReader.readLine(); String[] firstLineTokens = firstLine.split(" "); request.method = firstLineTokens[0]; request.url = firstLineTokens[1]; request.version = firstLineTokens[2]; // 2. 解析 url int pos = request.url.indexOf("?"); if (pos != -1) { String queryString = request.url.substring(pos + 1); parseKV(queryString, request.parameters); } // 3. 迴圈處理 header 部分 String line = ""; while ((line = bufferedReader.readLine()) != null && line.length() != 0) { String[] headerTokens = line.split(": "); request.headers.put(headerTokens[0], headerTokens[1]); } // 4. 解析 cookie String cookie = request.headers.get("Cookie"); if (cookie != null) { // 把 cookie 進行解析 parseCookie(cookie, request.cookies); } // 5. 解析 body if ("POST".equalsIgnoreCase(request.method) || "PUT".equalsIgnoreCase(request.method)) { // 這兩個方法需要處理 body, 其他方法暫時不考慮 // 需要把 body 讀取出來. // 需要先知道 body 的長度. Content-Length 就是幹這個的. // 此處的長度單位是 "位元組" int contentLength = Integer.parseInt(request.headers.get("Content-Length")); // 注意體會此處的含義~~ // 例如 contentLength 為 100 , body 中有 100 個位元組. // 下面建立的緩衝區長度是 100 個 char (相當於是 200 個位元組) // 緩衝區不怕長. 就怕不夠用. 這樣建立的緩衝區才能保證長度管夠~~ char[] buffer = new char[contentLength]; int len = bufferedReader.read(buffer); request.body = new String(buffer, 0, len); // body 中的格式形如: username=tanglaoshi&password=123 parseKV(request.body, request.parameters); } return request; } private static void parseCookie(String cookie, Map<String, String> cookies) { // 1. 按照 分號空格 拆分成多個鍵值對 String[] kvTokens = cookie.split("; "); // 2. 按照 = 拆分每個鍵和值 for (String kv : kvTokens) { String[] result = kv.split("="); cookies.put(result[0], result[1]); } } private static void parseKV(String queryString, Map<String, String> parameters) { // 1. 按照 & 拆分成多個鍵值對 String[] kvTokens = queryString.split("&"); // 2. 按照 = 拆分每個鍵和值 for (String kv : kvTokens) { String[] result = kv.split("="); parameters.put(result[0], result[1]); } } public String getMethod() { return method; } public String getUrl() { return url; } public String getVersion() { return version; } public String getBody() { return body; } public String getParameter(String key) { return parameters.get(key); } public String getHeader(String key) { return headers.get(key); } public String getCookie(String key) { return cookies.get(key); } }
import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.HashMap; import java.util.Map; public class HttpResponse { private String version = "HTTP/1.1"; private int status; private String message; private Map<String, String> headers = new HashMap<>(); private StringBuilder body = new StringBuilder(); private OutputStream outputStream = null; public static HttpResponse build(OutputStream outputStream) { HttpResponse response = new HttpResponse(); response.outputStream = outputStream; return response; } public void setVersion(String version) { this.version = version; } public void setStatus(int status) { this.status = status; } public void setMessage(String message) { this.message = message; } public void setHeader(String key, String value) { headers.put(key, value); } public void writeBody(String content) { body.append(content); } public void flush() throws IOException { BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream)); bufferedWriter.write(version + " " + status + " " + message + "n"); headers.put("Content-Length", body.toString().getBytes().length + ""); for (Map.Entry<String, String> entry : headers.entrySet()) { bufferedWriter.write(entry.getKey() + ": " + entry.getValue() + "n"); } bufferedWriter.write("n"); bufferedWriter.write(body.toString()); bufferedWriter.flush(); } }
import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class HttpServerV3 { static class User { // 儲存使用者的相關資訊 public String userName; public int age; public String school; } private ServerSocket serverSocket = null; // session 對談. 指的就是同一個使用者的一組存取伺服器的操作, 歸類到一起, 就是一個對談. // 記者來採訪你, 記者問的問題就是一個請求, 你回答的內容, 就是一個響應. 一次採訪過程中 // 涉及到很多問題和回答(請求和響應), 這一組問題和回答, 就可以稱為是一個 "對談" (整個採訪的過程) // sessions 中就包含很多對談. (每個鍵值對就是一個對談) private HashMap<String, User> sessions = new HashMap<>(); public HttpServerV3(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("伺服器啟動"); ExecutorService executorService = Executors.newCachedThreadPool(); while (true) { Socket clientSocket = serverSocket.accept(); executorService.execute(new Runnable() { @Override public void run() { process(clientSocket); } }); } } public void process(Socket clientSocket) { // 處理核心邏輯 try { // 1. 讀取請求並解析 HttpRequest request = HttpRequest.build(clientSocket.getInputStream()); HttpResponse response = HttpResponse.build(clientSocket.getOutputStream()); // 2. 根據請求計算響應 // 此處按照不同的 HTTP 方法, 拆分成多個不同的邏輯 if ("GET".equalsIgnoreCase(request.getMethod())) { doGet(request, response); } else if ("POST".equalsIgnoreCase(request.getMethod())) { doPost(request, response); } else { // 其他方法, 返回一個 405 這樣的狀態碼 response.setStatus(405); response.setMessage("Method Not Allowed"); } // 3. 把響應寫回到使用者端 response.flush(); } catch (IOException | NullPointerException e) { e.printStackTrace(); } finally { try { clientSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } private void doGet(HttpRequest request, HttpResponse response) throws IOException { // 1. 能夠支援返回一個 html 檔案. if (request.getUrl().startsWith("/index.html")) { String sessionId = request.getCookie("sessionId"); User user = sessions.get(sessionId); if (sessionId == null || user == null) { // 說明當前使用者尚未登陸, 就返回一個登陸頁面即可. // 這種情況下, 就讓程式碼讀取一個 index.html 這樣的檔案. // 要想讀檔案, 需要先知道檔案路徑. 而現在只知道一個 檔名 index.html // 此時這個 html 檔案所屬的路徑, 可以自己來約定(約定某個 d:/...) 專門放 html . // 把檔案內容寫入到響應的 body 中 response.setStatus(200); response.setMessage("OK"); response.setHeader("Content-Type", "text/html; charset=utf-8"); InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html"); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); // 按行讀取內容, 把資料寫入到 response 中 String line = null; while ((line = bufferedReader.readLine()) != null) { response.writeBody(line + "n"); } bufferedReader.close(); } else { // 使用者已經登陸, 無需再登陸了. response.setStatus(200); response.setMessage("OK"); response.setHeader("Content-Type", "text/html; charset=utf-8"); response.writeBody("<html>"); response.writeBody("<div>" + "您已經登陸了! 無需再次登陸! 使用者名稱: " + user.userName + "</div>"); response.writeBody(+ user.age + "</div>"); response.writeBody("<div>" + user.school + "</div>"); response.writeBody("</html>"); } } } private void doPost(HttpRequest request, HttpResponse response) { // 2. 實現 /login 的處理 if (request.getUrl().startsWith("/login")) { // 讀取使用者提交的使用者名稱和密碼 String userName = request.getParameter("username"); String password = request.getParameter("password"); // System.out.println("userName: " + userName); // System.out.println("password: " + password); // 登陸邏輯就需要驗證使用者名稱密碼是否正確. // 此處為了簡單, 咱們把使用者名稱和密碼在程式碼中寫死了. // 更科學的處理方式, 應該是從資料庫中讀取使用者名稱對應的密碼, 校驗密碼是否一致. if ("zhangsan".equals(userName) && "123".equals(password)) { // 登陸成功 response.setStatus(200); response.setMessage("OK"); response.setHeader("Content-Type", "text/html; charset=utf-8"); // 原來登陸成功, 是給瀏覽器寫了一個 cookie, cookie 中儲存的是使用者的使用者名稱. // response.setHeader("Set-Cookie", "userName=" + userName); // 現有的對於登陸成功的處理. 給這次登陸的使用者分配了一個 session // (在 hash 中新增了一個鍵值對), key 是隨機生成的. value 就是使用者的身份資訊 // 身份資訊儲存在伺服器中, 此時也就不再有洩露的問題了 // 給瀏覽器返回的 Cookie 中只需要包含 sessionId 即可 String sessionId = UUID.randomUUID().toString(); User user = new User(); user.userName = "zhangsan"; user.age = 20; user.school = "北京大學"; sessions.put(sessionId, user); response.setHeader("Set-Cookie", "sessionId=" + sessionId); response.writeBody("<html>"); response.writeBody("<div>歡迎您! " + userName + "</div>"); response.writeBody("</html>"); } else { // 登陸失敗 response.setStatus(403); response.setMessage("Forbidden"); response.setHeader("Content-Type", "text/html; charset=utf-8"); response.writeBody("<html>"); response.writeBody("<div>登陸失敗</div>"); response.writeBody("</html>"); } } } public static void main(String[] args) throws IOException { HttpServerV3 serverV3 = new HttpServerV3(9090); serverV3.start(); } }
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>登入頁面</title> </head> <body> <form method="post" action="/login"> <div style="margin-bottom: 5px"> <input type="text" name="username" placeholder="請輸入名字"> </div> <div style="margin-bottom: 5px"> <input type="password" name="password" placeholder="請輸入密碼"> </div> <div> <input type="submit" value="登入"> </div> </form> </body> </html>
請求中沒有Cookie
響應中帶有Cookie欄位,此時瀏覽器就會帶有Cookie
到此這篇關於Java模擬實現HTTP伺服器專案實戰的文章就介紹到這了,更多相關Java HTTP伺服器內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45