1

手写HTTP服务器

首先给一个需求:用手写的http服务器实现登入、修改密码、校验用户是否登录和登出功能。
在手写http服务器之前,我们要知道http服务器是用来监听浏览器(客户端)发出的请求,然后在处理请求后将结果响应给浏览器的。
所以手写一个简易的http服务器要做的就是:
1、监听服务器请求
2、处理这个请求
3、将处理结果响应给浏览器


1、写出相关的HTML页面。

ajax-demo (给出登录界面)

<!DOCTYPE html>
<html>
    <head>
        <h1>登入</h1>
        <link rel="icon" href="data: ;base64,= ">
    </head>
    <body>
        <form id="form1" action="##"  method="post" accept-charset="UTF-8" onsubmit="return false">
            <p>校验</p>
            <input type="text" name="userName" id="txtUserName" placeholder="用户名"/>
            <input type="text" name="passWord" id="txtPassWord" placeholder="密码"/>
            <input type="submit"  value="提交" onclick="fun()"/>
        </form>
        <script>
            function fun(){
                let flag = "ajax-demo3"
                const back = new XMLHttpRequest();
                let name = {"userName":form1.userName.value,"passWord":form1.passWord.value}
                let userName = JSON.stringify(name);
                back.onload = function(){
                    //判断true 、false
                    if(this.responseText == "true"){
                        //密码账号正确
                        alert("密码账号正确");
                        window.location.href="http://localhost:12345/"+flag;
                    }else{
                        //密码账号错误
                         alert("密码账号错误");
                    }
                }
                alert(userName);
                back.open("POST","http://localhost:12345/addUser2");
                back.setRequestHeader("Content-type","application/json;charset=UTF-8");//可以发送json格式字符串
                back.send(userName);
            }
        </script>
    </body>
</html>

使用Ajax,让浏览器向服务端发送一个路径为addUser2,body中包含userName和passWord(JSON格式)的POST请求。根据addUser2处理结束后所响应的值来判断账号密码是否正确。

如果正确则显示“密码账号正确”,并跳转到一个路径为ajax-demo3的页面。

如果错误则显示“密码账号错误”。

ajax-demo2 (给出修改密码的界面)

<!DOCTYPE html>
<html>
<h1>修改密码</h1>
    <head>
        <link rel="icon" href="data: ;base64,= ">
    </head>
    <body>
        <form id="form2" action="##"  method="post" accept-charset="UTF-8" onsubmit="return false">
            <p>修改密码</p>
            <input type="text" name="passWord"  placeholder="密码"/>
            <input type="submit"  value="提交" onclick="asd()"/>
        </form>
        <button onclick="tz()" >返回</button>
        <script>
            function tz(){
                let flag = "ajax-demo3";
                back1.open("POST","http://localhost:12345");
                back1.send('flag='+flag);
                window.location.href="http://localhost:12345/"+flag;
            }
            function asd(){
                let name = {"passWord":form2.passWord.value};
                let userName = JSON.stringify(name);
                back1.onload = function(){
                    //判断true 、false
                    if(this.responseText == "true"){
                        //密码重置成功
                        alert("密码重置成功");
                        window.location.href="http://localhost:12345/"+"ajax-demo3";
                    }else{
                        //没有相关的账号
                         alert("没有相关账号");
                         window.location.href="http://localhost:12345";
                    }
                }
                alert(userName);
                back1.open("POST","http://localhost:12345/addUser3");
                back1.setRequestHeader("Content-type","application/json;charset=UTF-8");//可以发送json格式字符串
                back1.send(userName);
            }
            const back1 = new XMLHttpRequest();
            back1.onload = function(){
                if(this.responseText == "true"){
                    alert("您已经登入");
                }else{
                    alert("您尚未登入")
                    window.location.href="http://localhost:12345";
                }
            }
            back1.open("POST","http://localhost:12345/addUser4");
            back1.send();
        </script>
    </body>
</html>

使用Ajax,让浏览器一旦访问这个页面,会自动的向服务端发送一个路径为addUser4的POST请求,根据addUser4处理结果的响应值来判断用户是否登入。

如果已经登入则显示“您已经登入”,点击提交按钮,浏览器将会向服务端发送一个路径为addUser3,body中包含passWord(为JSON格式)的POST请求。根据addUser3处理结束后所响应的值来判断密码是否重置成功。
如果重置失败会显示“没有相关账号”,并跳转至默认路径;重置成功则显示“密码重置成功”,并跳转至路径为ajax-demo3的页面。

如果没有登入,则会显示“您尚未登入”,并跳转至默认路径。

ajax-demo3 (给出跳转到修改密码的按钮和登出的按钮)

<!DOCTYPE html>
<html lang="en">
  <head>
      <meta charset="UTF-8">
      <title>Title</title>
      <h1>欢迎登入</h1>
      <link rel="icon" href="data: ;base64,= ">
  </head>
  <body>
    <button onclick="dc()">登出</button>
    <button onclick="tz()">修改密码</button>
    <script>
        function tz(){
                const back1 = new XMLHttpRequest();
                let flag = "ajax-demo2";
                back1.open("POST","http://localhost:12345");
                back1.send('flag='+flag);
                window.location.href="http://localhost:12345/"+flag;
        }
        function dc(){
                const back1 = new XMLHttpRequest();
                let flag = "ajax-demo";
                back1.open("POST","http://localhost:12345/addUser5");
                back1.send();
                window.location.href="http://localhost:12345/"+flag;
        }
        const back1 = new XMLHttpRequest();
        back1.onload = function(){
            if(this.responseText == "true"){
                alert("登入成功");
            }else{
                alert("您尚未登入")
                window.location.href="http://localhost:12345";
            }
        }
        back1.open("POST","http://localhost:12345/addUser4");
        back1.send();
    </script>
  </body>
</html>

使用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","<h1>OK</h1>");
            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","<h1>404 FILE NOT FOUND</h1>");
            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。

符合预期,结束!


爱摇头的电风扇
7 声望2 粉丝