Java模拟实现HTTP服务器项目实战
作者:/少司命
一,HTTP 协议的工作过程
二、HTTP协议格式
1,抓包分析搜狗主页
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中
2,协议格式总结
三、版本V1
实现一个最简单的 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 %s\n", 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 OK\n"); resp = "<h1>hello</h1>"; } else if (url.equals("/404")) { bufferedWriter.write(version + " 404 Not Found\n"); resp = "<h1>not found</h1>"; } else if (url.equals("/302")) { bufferedWriter.write(version + " 303 Found\n"); bufferedWriter.write("Location: http://www.sogou.com\n"); resp = ""; } else { bufferedWriter.write(version + " 200 OK\n"); resp = "<h1>default</h1>"; } // 3. 把响应写回到客户端 bufferedWriter.write("Content-Type: text/html\n"); 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(); } }
四、版本V2
在这个版本中, 我们只是简单解析 GET 请求, 并根据请求的路径来构造出不同的响应.
在版本1 的基础上, 我们做出一些改进:
把解析请求和构造响应的代码提取成单独的类.
能够把 URL 中的 query string 解析成键值对.
能够给浏览器返回 Cookie.
1,创建 HttpRequest 类
对照着 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 + '}'; } }
2,创建 HttpResponse 类
根据 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(); } }
3,创建 HttpServer 类
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之后就可以在服务器端进行一些逻辑处理
五、版本V3
在版本 2 的基础上, 再做出进一步的改进.
解析请求中的 Cookie, 解析成键值对
解析请求中的 body, 按照 x-www-form-urlencoded 的方式解析.
根据请求方法, 分别调用 doGet / doPost
能够返回指定的静态页面. 实现简单的会话机制.
1. 创建 HttpRequest 类
属性中新增了 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); } }
2,创建 HttpResponse 类
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(); } }
3,创建 HttpServer 类
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(); } }
4,insex.html
<!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服务器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!