使用技术
Cookie、session、线程池、工厂模式、html
实现代码
首先需要在这里创建一下文件,在index.html中写入想从网页上返回的内容,这里是需要完成一个登录页面,因此html中的内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆页面</title>
</head>
<body>
<!-- /login服务器端还没有实现,一会儿再写 -->
<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 style="margin-bottom: 5px">
<input type="submit" value="登录">
</div>
</form>
</body>
</html>
这里也是分为三部分来写:
第一部分:HttpRequest
package day0318;
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写道try括号中,一旦写进去就意味着bufferedReader就会被关闭,会影响到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){
//pos表示问号的下标
//看看有没有带问号,如果没有带问号就不用解析了
//index.html?a=10&b=20
String queryString = request.url.substring(pos+1);//从i+1开始取
//切分的最终结果
parseKV(queryString,request.parameters);
}
//3.解析header
String line = "";
//readLine()读到的一行内容,时会自动去掉换行符的
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){
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为100,就说明body中有100个字节,下面创建的缓冲区为100个char,相当于是两百个字节
//缓冲区不怕长,就怕不够,这样创建的缓冲区才能保证管够
char[] buffer = new char[contentLength];
int length = bufferedReader.read(buffer);
request.body = new String(buffer,0,length);
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 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]);
}
}
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 geCookie(String key){
return cookies.get(key);
}
}
第二部分:HttpResponse
package day0318;
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();
//当代码需要把响应写回客户端的时候,就往这个OutputStream里面写就好了
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 setHeaders(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();
}
}
第三部分:HttpServerV3
package day0318;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
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;
/**
* 1.让服务器支持返回一个静态的html文件
* 2.解析处理cookie,把cookie处理成键值对保存好
* 3.解析body,把body中的数据成键值对结构
* 4.完成一个登录功能,session的简单实现
*/
public class HttpServerV3 {
static class User{
//保存用户的相关信息
public String userName;
public int age;
public String school;
}
private ServerSocket serverSocket = null;
//session 会话,指的是同一个用户的一组访问服务操作,归类到一起,就是一个会话
//每个键值对就是一个会话
private HashMap<String,User> sessions = new HashMap<String, User>();
public HttpServerV3(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();
executorService.execute(new Runnable() {
@Override
public void run() {
//2.处理连接(采用短连接)
try {
process(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
public void process(Socket clientSocket) throws IOException {
try {
//1.读取并解析请求
HttpRequest request = HttpRequest.build(clientSocket.getInputStream());
HttpResponse response = HttpResponse.build(clientSocket.getOutputStream());
response.setHeaders("Content-Type", "text.html");
//2.根据请求计算相应
//此处按照不同的HTTP方法拆分成多个不同的逻辑
if ("GET".equalsIgnoreCase(request.getMethod())){
doGet(request,response);
}else if ("POST".equalsIgnoreCase(request.getMethod())){
doPost(request,response);
}else {
response.setStatus(405);
response.setMessage("Method Not Allowed");
}
//3.把响应写回到客户端
response.flush();
} catch (IOException | NullPointerException e) {
e.printStackTrace();
}finally {
clientSocket.close();
}
}
private void doGet(HttpRequest request, HttpResponse response) throws IOException {
//1.能够支持返回一个HTML文件
if (request.getUrl().startsWith("/index.html")){
String sessionId = request.geCookie("sessionId");
User user = sessions.get(sessionId);
if (sessionId == null || user == null) {
//说明当前用户尚未登陆,就返回一个登陆页面
//这种情况下,就让代码读取一个index.html这样的文件
//此时可以自己来约定路径
//把这个文件内容写入到响应的body中
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("Content-Type","text/html; charset=utf-8");
InputStream inputStream = HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//HttpServerV3.class: 获取到一个类的类对象
//getClassLoader(): 获取到当前类的“类加载器”,
//一个类想要工作,需要先由JVM把这个类的.class文件找到,然后读取内容,加载到内存中(类加载过程)
//.getResourceAsStream("index.html"): 根据文件名,在Resources目录中找到对应的文件并打开
//返回这个文件的InputStream对象
//按行读取
String line = null;
while ((line = bufferedReader.readLine()) != null){
response.writeBody(line+"\n");
}
bufferedReader.close();
}else {
//已经登陆了,就不需要再登陆了
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("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(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 ("dqy".equals(userName) && "123".equals(password)){
//登陆成功
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("Content-Type","text/html; charset=utf-8");
//原来登陆成功是给浏览器写了一个cookie,cookie中保存的是用户名
// response.setHeaders("Set-Cookie","userName="+userName);
String sessionId = UUID.randomUUID().toString();//会生成一个随机字符串,能保证每次调用这个方法生成的字符串都不一样
//此时身份信息保存在服务器中,就不会再有泄露的问题了,
//给浏览器返回cookie中包含sessionId即可
User user = new User();
user.userName = "dqy";
user.age = 20;
user.school = "大学";
sessions.put(sessionId,user);
response.setHeaders("Set-Cookie","sessionId="+sessionId);
//在响应中添加一个cookie,浏览器就会自动存储这个cookie
response.writeBody("<html>");
response.writeBody("<div>欢迎您!"+userName+"</div>");
response.writeBody("</html>");
}else {
//登陆失败
response.setStatus(403);
response.setMessage("Forbidden");
response.setHeaders("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();
}
}
成果展示
启动服务器
就会看到登录页面
输入用户名和密码之后,能看到:
此时就登陆成功了
抓包并且可以看到session字段
在浏览器中可以看到多了一个Cookie