前言:
之前,我们写了两个版本的 Http 服务器,本篇,再继续实现第三个版本
V3 将满足:
1.支持返回一个静态的 html 文件
2.解析处理 cookie (把 cookie 处理成键值对结构)
3.解析处理 body (把 body 中的数据处理成键值对结构)
4.实现一个完整的登录功能 (session 的简单实现)
public class Request {
private String method;
private String url;
private String version;
private Map<String,String> headers = new HashMap<>();
// url 中的参数和 body中的参数都放在 parameters 哈希表里
private Map<String,String> parameters = new HashMap<>();
private Map<String,String> cookies = new HashMap<>();
private String body;
public static Request build(InputStream inputStream) throws IOException {
Request request = new Request();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//1.处理首行
String fistLine = bufferedReader.readLine();
String[] firstLineTokens = fistLine.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 读取出来
// 此处的长度单位是 "字节"
int contentLength = Integer.parseInt(request.headers.get("Content-Length"));
// contentLength 长度单位是字节
// 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=huahua&password=666
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);
}
}
public class Response {
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 Response build(OutputStream outputStream){
Response response = new Response();
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();
}
}
public class HttpServerV3 {
private ServerSocket serverSocket = null;
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.读取请求并解析
Request request = Request.build(clientSocket.getInputStream());
Response response = Response.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(Request request, Response response) throws IOException {
//1.能够支持返回一个 html 文件
if(request.getUrl().startsWith("/index.html")){
// 让代码读取一个 /index.html 这样的文件
// 要想读文件,需要先知道文件路径 (只知道文件名)
// 此时 html 文件所属的路径,可以自己约定
// 把文件内容写到响应的 body 中
response.setStatus(200);
response.setMessage("OK");
response.setHeader("Content-Type","text/html; charset=utf-8");
// HttpServerV3.class获取一个类对象
// getClassLoader() 获取当前类的类加载器
InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//按行读取内容,把数据写入到 response 中
String lint = " ";
while ((lint = bufferedReader.readLine()) != null){
response.writeBody(lint + "\n");
}
bufferedReader.close();
}
}
private void doPost(Request request, Response response) {
}
public static void main(String[] args) throws IOException {
HttpServerV3 httpServerV3 = new HttpServerV3(6060);
httpServerV3.start();
}
}
要想能够支持返回一个 html 文件,就需要让代码读取一个 类似 /index.html 这样的文件,要想读文件,需要先知道文件路径 (但我们只知道文件名)
解决方法:
new 一个 和 src 同级的 Directory
右击新创建的 xxx (Directory):Mark Directory as — xxx Root
然后 new 一个 file:index.html
<html>
<head>
<title>登录界面</title>
<meta charset="UTF-8">
</head>
<body>
<!-- /login操作服务器端还没有实现 -->
<form method="post" action="/login">
<div style="margin-bottom: 8px">
<input type="text" name="username" placeholder="请输入用户名">
</div>
<div style="margin-bottom: 8px">
<input type="password" name="password" placeholder="请输入密码">
</div>
<div>
<input type="submit" value="登录">
</div>
</form>
</body>
</html>
写到这里,服务器已经可以返回一个指定的静态页面了
这个页面中包含了一个 form 表单,借助表单来实现登录操作
此时,启动服务器
由于表单是把数据提交到 /login 这个 path 中
服务器紧接着就要实现 POST 请求下的 /login 的处理
doPost 方法:
private void doPost(Request request, Response 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);
}
}
再次重启服务器,使用 fiddler 抓包:
流程:
接着,请求到达服务器上,解析成 Request 对象
用户名和密码这种键值对,就保存到 body 中,同时会把 body 中的键值对解析保存到 parameters 表中
doPost 方法获取参数:
在这个基础上,在继续实现验证用户名密码是否正确
private void doPost(Request request, Response response) {
//2.实现 /login 的处理
if(request.getUrl().startsWith("/login")) {
//读取用户提交的用户名和密码
String userName = request.getParameter("username");
String password = request.getParameter("password");
//登录逻辑需要验证用户名密码是否正确
// 此处为了简单,把用户名和密码在代码中写死
// 更科学的处理方式: 从数据库中读取用户名对应密码,检验密码是否一致
if("hh".equals(userName) && "666".equals(password)){
// 登陆成功
response.setStatus(200);
response.setMessage("OK");
response.setHeader("Content-Type","text/html;charset=utf-8");
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>");
}
}
}
重启服务器,进入页面:
使用 fiddler 抓包验证:
对于页面来说,若登录成功之后,刷新页面,自己仍然处于登陆状态
访问该网站的其他页面,此时仍然处在登录状态
如何实现上述功能? 就需要使用 Cookie
在 doPost 中再加上:
response.setHeader("Set-Cookie","username=" + userName);
此时我们就在 Http 的响应中加上了 Set-Cookie 这样的字段,浏览器就会自动存储刚才的这个 Cookie 数据
重启服务器,观察抓包效果:
Http 响应报文中就多了 Set-Cookie 字段:
此时浏览器端就保存了一个 Cookie
接下来访问服务器的其他页面时,Cookie 就会自动带上
刷新页面,查看请求报文:
进而服务器就可以根据这个 Cookie 的内容来判断当前页面对应的登陆状态
上述 Cookie 内容是 username=666,此时再访问 index/html 就不需要再登录
修改代码,实现上述功能:
private void doGet(Request request, Response response) throws IOException {
//1.能够支持返回一个 html 文件
if(request.getUrl().startsWith("/index.html")){
String userName = request.getCookie("username");
// userName == null 表示当前用户尚未登陆
if(userName == null){
// 让代码读取一个 /index.html 这样的文件
// 要想读文件,需要先知道文件路径 (只知道文件名)
// 此时 html 文件所属的路径,可以自己约定
// 把文件内容写到响应的 body 中
response.setStatus(200);
response.setMessage("OK");
response.setHeader("Content-Type","text/html; charset=utf-8");
// HttpServerV3.class获取一个类对象
// getClassLoader() 获取当前类的类加载器
InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//按行读取内容,把数据写入到 response 中
String lint = " ";
while ((lint = bufferedReader.readLine()) != null){
response.writeBody(lint + "\n");
}
bufferedReader.close();
}
// 已经登陆了
else {
response.setStatus(200);
response.setMessage("OK");
response.setHeader("Content-Type","text/html; charset=utf-8");
response.writeBody("<html>");
response.writeBody("您已登录,无需再次登录!用户名: " + userName);
response.writeBody("</html>");
}
}
}
重启服务器,刷新页面:
刷新页面,此时的 http 请求中带有了 Cookie 信息,服务器分析了 Cookie 之后,认为已经登陆成功,此时就不需要再继续登录
借助 Cookie 已经实现了登录保持功能,但用户信息在 Cookie 中,每次数据传输都要把这个 Cookie 再发给服务器,那么 Cookie 中的信息就很容易泄露
应该把用户的敏感信息保存到服务器端,在服务器端登陆成功时,把用户信息保存到一个哈希表中(value),同时生成一个 key (保证唯一性的字符串) SessionId,最终只要把 SessionId 写回到 Cookie 中即可
后续用户访问页面时,Cookie 中带有的内容就是 SessionId,SessionId 就是一个没规律的字符串(不涉及敏感信息),但服务器可以通过 SessionId 进一步找到用户的信息
再次修改代码,实现上述功能:
//保存用户的相关信息
static class User {
public String username;
public int age;
public String school;
}
// sessions 会话 —— 指的是同一个用户的一组访问服务器的操作,归类到一起,就是一个会话
private HashMap<String,User> sessions = new HashMap<>();
重启服务器,删除浏览器 Cookie,刷新页面:
首次访问的时候,由于用户浏览器中没有 Cookie,此时看到的页面是一个未登录的状态
请求中没有 Cookie
输入用户名密码,提交表单,进行验证登录:
请求中也没有 Cookie
响应中带有 Set-Cookie字段
进而浏览器就会保存这个内容 (sessionId 内容)
接下来再次访问服务器时,此时浏览器就会带有 Cookie
服务器会根据此处的 sessionId 就能获取到当前用户的登陆状态
sessionId 在 Cookie 中存在,并且拿着 sessionId 也能在 sessions 中找到对应的 User 信息,从而就可以确认现在是登陆成功状态
用户信息都在服务器中,不涉及泄露问题~
但是: 若别人拿到 sessionId,短期内是可以冒充该用户的
因此 session 一般要搭配"过期机制",可以在 session 中保存一个该用户的 session 何时创建和何时过期;可以使用一个单独的线程不停的扫描 sessions,只要发现过期的 session 就自动删掉,后续再使用这个过期的 session 去访问,此时就会让你重新登陆
public class Request {
private String method;
private String url;
private String version;
private Map<String,String> headers = new HashMap<>();
// url 中的参数和 body中的参数都放在 parameters 哈希表里
private Map<String,String> parameters = new HashMap<>();
private Map<String,String> cookies = new HashMap<>();
private String body;
public static Request build(InputStream inputStream) throws IOException {
Request request = new Request();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//1.处理首行
String fistLine = bufferedReader.readLine();
String[] firstLineTokens = fistLine.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 读取出来
// 此处的长度单位是 "字节"
int contentLength = Integer.parseInt(request.headers.get("Content-Length"));
// contentLength 长度单位是字节
// 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=huahua&password=666
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);
}
}
public class Response {
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 Response build(OutputStream outputStream){
Response response = new Response();
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();
}
}
public class HttpServerV3 {
//保存用户的相关信息
static class User {
public String username;
public int age;
public String school;
}
private ServerSocket serverSocket = null;
// 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.读取请求并解析
Request request = Request.build(clientSocket.getInputStream());
Response response = Response.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(Request request, Response response) throws IOException {
//1.能够支持返回一个 html 文件
if(request.getUrl().startsWith("/index.html")){
String sessionId = request.getCookie("sessionId");
// userName == null 表示当前用户尚未登陆
User user = sessions.get(sessionId);
if(sessionId == null || user == null){
// 让代码读取一个 /index.html 这样的文件
// 要想读文件,需要先知道文件路径 (只知道文件名)
// 此时 html 文件所属的路径,可以自己约定
// 把文件内容写到响应的 body 中
response.setStatus(200);
response.setMessage("OK");
response.setHeader("Content-Type","text/html; charset=utf-8");
// HttpServerV3.class获取一个类对象
// getClassLoader() 获取当前类的类加载器
InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//按行读取内容,把数据写入到 response 中
String lint = " ";
while ((lint = bufferedReader.readLine()) != null){
response.writeBody(lint + "\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("<div>" + user.age + "</div>");
response.writeBody("<div>" + user.school + "</div>");
response.writeBody("</html>");
}
}
}
private void doPost(Request request, Response 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("hh".equals(userName) && "666".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 = "hh";
user.age = 3;
user.school = "Coding666";
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 httpServerV3 = new HttpServerV3(6060);
httpServerV3.start();
}
}
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_47988201/article/details/122267535
内容来源于网络,如有侵权,请联系作者删除!