手写HTTP服务器
首先给一个需求:用手写的http服务器实现登入、修改密码、校验用户是否登录和登出功能。
在手写http服务器之前,我们要知道http服务器是用来监听浏览器(客户端)发出的请求,然后在处理请求后将结果响应给浏览器的。
所以手写一个简易的http服务器要做的就是:
1、监听服务器请求
2、处理这个请求
3、将处理结果响应给浏览器
1、写出相关的HTML页面。
ajax-demo (给出登录界面)
登入
使用Ajax,让浏览器向服务端发送一个路径为addUser2,body中包含userName和passWord(JSON格式)的POST请求。根据addUser2处理结束后所响应的值来判断账号密码是否正确。
如果正确则显示“密码账号正确”,并跳转到一个路径为ajax-demo3的页面。
如果错误则显示“密码账号错误”。
ajax-demo2 (给出修改密码的界面)
修改密码
使用Ajax,让浏览器一旦访问这个页面,会自动的向服务端发送一个路径为addUser4的POST请求,根据addUser4处理结果的响应值来判断用户是否登入。
如果已经登入则显示“您已经登入”,点击提交按钮,浏览器将会向服务端发送一个路径为addUser3,body中包含passWord(为JSON格式)的POST请求。根据addUser3处理结束后所响应的值来判断密码是否重置成功。
如果重置失败会显示“没有相关账号”,并跳转至默认路径;重置成功则显示“密码重置成功”,并跳转至路径为ajax-demo3的页面。
如果没有登入,则会显示“您尚未登入”,并跳转至默认路径。
ajax-demo3 (给出跳转到修改密码的按钮和登出的按钮)
Title
欢迎登入
使用Ajax,让浏览器一旦访问这个页面,会自动的向服务端发送一个路径为addUser4的POST请求,根据addUser4处理结果的响应值来判断用户是否登入。
如果已经登入则显示“您已经登入”,点击登出按钮,浏览器就会向服务端发送一个地址为addUser5的POST请求,并且跳转至路径为ajax-demo的页面;点击修改密码按钮则会跳转至路径为ajax-demo2的页面。
如果没有登入,则会显示“您尚未登入”,并跳转至默认路径。
2、手写http服务器来接收来自浏览器的请求,处理请求,将处理结果响应给浏览器
1、给出一个端口号
public class Server {
public static void main(String[] args) throws IOException {
new Server(12345);
}
}
2、启动监听端口
public class Server {
/*
* 启动监听端口
* */
public Server(int port) throws IOException {
if (port < 1 || port >65535){
throw new DemoApplication("端口错误");
}
ServerSocket serverSocket = new ServerSocket(port);
ExecutorService pool = Executors.newFixedThreadPool(50);
System.out.println("已经启动,开始监听端口"+port);
while (true){
Socket clientSocket = serverSocket.accept();
if (clientSocket != null && !clientSocket.isClosed()){
//首先服务端输出内容到客户端的输入流
Runnable r = () -> {
try {
acceptToClient(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
};
pool.submit(r);
}
}
}
public static void main(String[] args) throws IOException {
new Server(12345);
}
}
3、抛出未定义的异常
public class Server {
//抛出异常
private static class DemoApplication extends RuntimeException{
public DemoApplication() {
super();
}
public DemoApplication(String message) {
super(message);
}
public DemoApplication(String message, Throwable cause) {
super(message, cause);
}
public DemoApplication(Throwable cause) {
super(cause);
}
protected DemoApplication(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
/*
* 启动监听端口
* */
public Server(int port) throws IOException {
if (port < 1 || port >65535){
throw new DemoApplication("端口错误");
}
ServerSocket serverSocket = new ServerSocket(port);
ExecutorService pool = Executors.newFixedThreadPool(50);
System.out.println("已经启动,开始监听端口"+port);
while (true){
Socket clientSocket = serverSocket.accept();
if (clientSocket != null && !clientSocket.isClosed()){
//首先服务端输出内容到客户端的输入流
Runnable r = () -> {
try {
acceptToClient(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
};
pool.submit(r);
}
}
}
public static void main(String[] args) throws IOException {
new Server(12345);
}
}
4、封装响应报头(报文)
public class Server {
//抛出异常
private static class DemoApplication extends RuntimeException{
public DemoApplication() {
super();
}
public DemoApplication(String message) {
super(message);
}
public DemoApplication(String message, Throwable cause) {
super(message, cause);
}
public DemoApplication(Throwable cause) {
super(cause);
}
protected DemoApplication(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
/*
* 启动监听端口
* */
public Server(int port) throws IOException {
if (port < 1 || port >65535){
throw new DemoApplication("端口错误");
}
ServerSocket serverSocket = new ServerSocket(port);
ExecutorService pool = Executors.newFixedThreadPool(50);
System.out.println("已经启动,开始监听端口"+port);
while (true){
Socket clientSocket = serverSocket.accept();
if (clientSocket != null && !clientSocket.isClosed()){
//首先服务端输出内容到客户端的输入流
Runnable r = () -> {
try {
acceptToClient(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
};
pool.submit(r);
}
}
}
/*
* 封装响应报头(报文)
* */
private void writeToClient(OutputStream clientOut, String ContentType ,String cookieName,String cookieValue,String cookieTime, int responseCode, String responseDes, String content) throws IOException {
clientOut.write(("HTTP/1.1 "+responseCode+ " " +responseDes+ "\r\n").getBytes());
clientOut.write(("Date: " + (new Date()).toString() + "\r\n").getBytes());
clientOut.write(("Content-Type: "+ ContentType +"; charset=UTF-8\r\n").getBytes());
clientOut.write(("Set-Cookie: "+cookieName+"="+cookieValue+";"+"max-age="+cookieTime+"\r\n").getBytes(StandardCharsets.UTF_8));
clientOut.write("\r\n".getBytes());//空行
clientOut.write(content.getBytes());
clientOut.flush();
clientOut.close();
}
public static void main(String[] args) throws IOException {
new Server(12345);
}
}
5、处理请求
如何让浏览器记住我已经登入了呢?HTTP是一个无状态协议,即自身不对请求和响应之间的通讯状态进行保存。这里就得引用Cookie技术来实现保存了。
我们在网页的输入框中输入用户名点击登入按钮,浏览器就会向服务器提交一个POST请求(数据存放在body中)。服务端监听到POST请求后,通过响应的逻辑处理获取浏览器提交的用户名。服务端存储这个cookie,再通过Set-Cookie 响应报头,告诉浏览器来储存这个(携带用户名的)cookie。从此,浏览器的每一次请求(在cookie的生命周期中)都会携带这cookie进行访问。所以只要对浏览器是否携带(存储用户名的)cookie进行判断就能得知用户是否登入。
登出也是这个道理。点击登出按钮后,浏览器向服务端发送一个POST请求。服务端获取到这个请求后开始相应的逻辑判断,将cookie中的max-age(cookie的生命周期)设置成0,就会让这个cookie过期,再通过Set-Cookie 响应报头告诉浏览器这个cookie过期了(发送新的cookie)。自此浏览器在接下来的请求中就不会携带这个cookie了。完成了登出操作。
其他的操作比如:如何设置默认路径、如何读取请求、如何获取请求报文的URL等等的逻辑判断,我已经在代码中加了注释来解析。
public class Server {
/*
* 抛出异常
* */
private static class DemoApplication extends RuntimeException{
public DemoApplication() {
super();
}
public DemoApplication(String message) {
super(message);
}
public DemoApplication(String message, Throwable cause) {
super(message, cause);
}
public DemoApplication(Throwable cause) {
super(cause);
}
protected DemoApplication(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
/*
* 启动监听端口
* */
public Server(int port) throws IOException {
if (port < 1 || port >65535){
throw new DemoApplication("端口错误");
}
ServerSocket serverSocket = new ServerSocket(port);
ExecutorService pool = Executors.newFixedThreadPool(50);
System.out.println("已经启动,开始监听端口"+port);
while (true){
Socket clientSocket = serverSocket.accept();
if (clientSocket != null && !clientSocket.isClosed()){
//首先服务端输出内容到客户端的输入流
Runnable r = () -> {
try {
acceptToClient(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
};
pool.submit(r);
}
}
}
private void acceptToClient(Socket clientSocket) throws IOException {
//使用 InputStream 来读取浏览器的请求报文
InputStream clientIn = clientSocket.getInputStream();
//使用 BufferedReader 将InputStream强转成 BufferedReader。(InputStream读取到的是字节,BufferedReader读取到的是字符,我们需要的是字符,所以强转成BufferedReader)
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientIn,"UTF-8"));
OutputStream clientOut = clientSocket.getOutputStream();
String content = "";
String cookieName = "";
String cookieValue = "";
String cookieTime = "";
//如果获取到的有效长度为0
if (clientIn.available()==0){
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,200,"OK","OK
");
return;
}
/*
* 读取第一行要进行特殊处理,从中获取到请求报文中的 方法 和 URL
* */
String firstLine = bufferedReader.readLine();
System.out.println("读取到的第一行是"+firstLine);
String requestUri = firstLine.split(" ")[1];
System.out.println("读取数组中的URL"+requestUri);
String method = firstLine.split(" ")[0];
System.out.println("这次请求的方法是"+method);
String str = "";
String result = "";
/*
* 判断路径是否是addUser2,如果是则进入这个处理逻辑。
* */
if(requestUri.equals("/addUser2")){
System.out.println("进入addUser2");
if(method.equals("POST")){
String d = "";
int e = 0;
//将读取到的请求报文按行读取
while ((str=bufferedReader.readLine())!= null){
d = str.split(" ")[0];
//将Content-Length中的长度提取出来
if(d.equals("Content-Length:")){
e = Integer.parseInt(str.split(" ")[1]);
System.out.println("e"+e);
}
System.out.println(str);
//读取到空行说明请求报文中首部字段已经读取完毕(请求首部字段和内容实体中间隔了一个空行),即将进入内容实体(body)
if ((str = bufferedReader.readLine()).equals("")){
System.out.println("读取到空行");
break;
}
}
char[] f = new char[e];
int g = 0;
int h = 0;
int j = e;
//read(暂存区,偏移量,读取的最大长度) 是读取字符放入f缓冲区
/*使用read方法而不是readLine是应为readLean在这里会出现堵塞,会一直卡在这里。
所以使用read方法将剩余的body长度(Content-Length中的长度)读取出来*/
while ((g = bufferedReader.read(f,h,e))!=-1){
h = h +g;
System.out.println(h);
break;
}
//将f暂存区的内容一一输出
for(int i = 0 ; i < e ; i++){
result += f[i];
}
System.out.println("读取到的body是"+result);
}
System.out.println("进入addUser2逻辑处理");
//获取body中的JSON数据
ObjectMapper mapper = new ObjectMapper();
People people = mapper.readValue(result,People.class);
System.out.println(people.userName);
System.out.println(people.passWord);
String userName = people.userName;
String passWord = people.passWord;
//查询数据库
//2.获取SqlSession对象
Sql sql = new Sql();
SqlSession sqlSession = sql.Sql().openSession();
//3.获取Mapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//4.执行方法,将数据库中的账号密码与获取到的账号密码进行比对
User users =userMapper.selectAll(userName, passWord);
System.out.println(users);
//释放资源
sqlSession.close();
//即将响应给浏览器的处理结果
if (users != null) {
//将用户名密码通过Set-Cookie的报头告诉浏览器储存这个cookie值
cookieName = "userName";
cookieValue = userName;
content = "true";
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,200,"OK",content);
}else {
content ="false";
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,200,"OK",content);
}
}
/*
* 判断是否是addUser3,如果是则进入
* */
System.out.println(cookieName);
System.out.println(cookieValue);
if(requestUri.equals("/addUser3")){
System.out.println("进入addUser3方法");
String k = "";
String d = "";
String l = "";
int e = 0;
if(method.equals("POST")){
while ((str=bufferedReader.readLine())!= null){
d = str.split(" ")[0];
//获取Cookie中的值,用于后期判断
if (d.equals("Cookie:")) {
k = str.split(" ")[2];
System.out.println("k" + k);
}
if(d.equals("Content-Length:")){
e = Integer.parseInt(str.split(" ")[1]);
System.out.println("e"+e);
}
System.out.println(str);
if ((str = bufferedReader.readLine()).equals("")){
System.out.println("读取到空行");
break;
}
}
char[] f = new char[e];
int g = 0;
int h = 0;
int j = e;
//read 是读取字符放入f缓冲区
while ((g = bufferedReader.read(f,h,e))!=-1){
h = h +g;
System.out.println(h);
break;
}
for(int i = 0 ; i < e ; i++){
result += f[i];
}
System.out.println("读取到的body是"+result);
}
l = k.split("=")[0];
k = k.split("=")[1];
System.out.println("l"+l);
System.out.println("k"+k);
//将获取到的cookie值进行比对,如果一致则进入逻辑
if ("userName".equals(l)) {
System.out.println("成功进入");
String value = k;
System.out.println(l + ":" + value);
//提取JSON中的值
ObjectMapper mapper = new ObjectMapper();
People people = mapper.readValue(result,People.class);
String userName = value;
String passWord = people.passWord;
System.out.println("passWord"+passWord);
//修改密码
User user = new User();
user.setUserName(userName);
user.setPassWord(passWord);
//2.获取SqlSession对象
Sql sql = new Sql();
SqlSession sqlSession = sql.Sql().openSession();
//3.获取Mapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//4.执行方法,将数据库中的值进行更改
int update = userMapper.update(user);
System.out.println(update);
//提交事务
sqlSession.commit();
//释放资源
sqlSession.close();
System.out.println(result);
//即将响应给浏览器的内容
if (update !=0) {
content = "true";
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,200,"OK",content);
} else {
content = "false";
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,200,"OK",content);
}
}
}
/*
* 判断是否是addUser4,如果是则进入
* */
if(requestUri.equals("/addUser4")){
System.out.println("进入addUser4方法");
String d = "";
String e = "";
if(method.equals("POST")) {
while ((str = bufferedReader.readLine()) != null) {
d = str.split(" ")[0];
if (d.equals("Cookie:")) {
e = str.split(" ")[2];
System.out.println("e" + e);
break;
}
}
}
//从请求首部字段中获取cookie值。
d = e.split("=")[0];
//e = e.split("=")[1];
System.out.println("cookieName="+d);
//如果浏览器中已经存放了这个cookie值
if ("userName".equals(d)) {
content = "true";
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,200,"OK",content);
}else {
content = "false";
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,200,"OK",content);
}
}
/*
* 判断是否是addUser5,如果是则进入
* */
if(requestUri.equals("/addUser5")){
System.out.println("进入addUser5");
String d = "";
String e = "";
if(method.equals("POST")) {
while ((str = bufferedReader.readLine()) != null) {
d = str.split(" ")[0];
if (d.equals("Cookie:")) {
e = str.split(" ")[2];
System.out.println("e" + e);
break;
}
}
}
d = e.split("=")[0];
e = e.split("=")[1];
System.out.println("cookieName="+d);
System.out.println("cookieValue="+e);
cookieName = d;
cookieValue = e;
//将cookie中的生命周期(Max-age)设置成0,让这个cookie值过期。
cookieTime ="0";
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,200,"OK",content);
}
if(requestUri.equals("/favicon.ico")){
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,200,"OK","favicon.ico");
return;
}
System.out.println("=========================================>>>>>>>>>>>");
//判断是否带路径搜索,没带路径的默认搜索ajax-demo.html
String resourcePath = requestUri.equals("/") ? "ajax-demo" : requestUri.substring(1);
System.out.println(resourcePath);
//打印获取的html
String a = "";
String htmlStr = "";
try {
BufferedReader in = new BufferedReader(new FileReader("src/webapp/"+resourcePath +".html"));
while ((a = in.readLine()) != null) {
htmlStr =htmlStr + "\n" +a;
}
} catch (IOException e) {
System.out.print("错误");
}
//读取资源的内容
System.out.println(htmlStr);
//找不到资源直接返回404
if (htmlStr == null){
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,404,"Not Found","404 FILE NOT FOUND
");
return;
}
content = htmlStr;
writeToClient(clientOut,"text/html",cookieName,cookieValue,cookieTime,200,"OK",content);
}
/*
* 封装响应报头(报文)
* */
private void writeToClient(OutputStream clientOut, String ContentType ,String cookieName,String cookieValue,String cookieTime, int responseCode, String responseDes, String content) throws IOException {
clientOut.write(("HTTP/1.1 "+responseCode+ " " +responseDes+ "\r\n").getBytes());
clientOut.write(("Date: " + (new Date()).toString() + "\r\n").getBytes());
clientOut.write(("Content-Type: "+ ContentType +"; charset=UTF-8\r\n").getBytes());
clientOut.write(("Set-Cookie: "+cookieName+"="+cookieValue+";"+"max-age="+cookieTime+"\r\n").getBytes(StandardCharsets.UTF_8));
clientOut.write("\r\n".getBytes());//空行
clientOut.write(content.getBytes());
clientOut.flush();
clientOut.close();
}
public static void main(String[] args) throws IOException {
new Server(12345);
}
}
3、查收,是否符合预期
自此,这个手写的http服务器就已经完成了,来看看效果如何。
首先直接访问路径为ajax-demo2和ajax-demo3会发生什么。
符合预期。
登入后呢?
可以看到,此时请求中Cookie报头已经携带了userName的信息,这时在路径为ajax-demo2的页面点击登出会发生什么。
可以看到响应报文中的Set-Cookie中的max-age被设置成了0,让cookie(username)失效。
可以看到,自此浏览器的请求报文中已经不再携带username这个cookie。
符合预期,结束!