java并发原理实战(14)--自己实现简易web服务器

简介: java并发原理实战(14)--自己实现简易web服务器

0.web服务器知识储备


web服务器知识储备,了解网络编程,其实就是java的socket。如果实现网络请求,也就是服务端能够接收客户端的请求,然后服务端再对客户端进行响应。响应的话需要按照http的请求格式,浏览器才能识别显示。


http的格式推荐文章

HTTP的请求包括:请求行(request line)、请求头部(header)、空行 和 请求数据 四个部分组成。


Http请求消息结构


抓包的request结构如下:


GET /mix/76.html?name=kelvin&password=123456 HTTP/1.1
Host: www.fishbay.cn
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6


1.请求行


GET为请求类型,/mix/76.html?name=kelvin&password=123456为要访问的资源,HTTP/1.1是协议版本


2.请求头部


从第二行起为请求头部,Host指出请求的目的地(主机域名);User-Agent是客户端的信息,它是检测浏览器类型的重要信息,由浏览器定义,并且在每个请求中自动发送。


3.空行


请求头后面必须有一个空行


4.请求数据


请求的数据也叫请求体,可以添加任意的其它数据。这个例子的请求体为空


Response


一般情况下,服务器收到客户端的请求后,就会有一个HTTP的响应消息,HTTP响应也由4部分组成,分别是:状态行、响应头、空行 和 响应体。


1dc618a0ed9580ce8bfa6facb208c08f.png


1.简易web服务器-版本1


之前的javaweb项目中,一般把项目放在tomcat的webapps目录或者直接放在ROOT根目录下,我们在浏览器输入服务器的地址和tomcat的端口号等信息就可以访问到服务器上的资源了。


现在我们要实现类似tomcat的web服务器功能:


假设我们本地就是服务器,现在就实现在浏览器输入个地址,访问到本地的某个盘符的资源的功能:


首先


在e盘建个webroot目录,我们的资源都放在这个目录下。


在这个文件下写个index.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>index.html</title>
</head>
<body>
  hello world
</body>
</html>


写web服务端代码:


import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
public class HttpServer {
    public static void main(String[] args) throws IOException {
        //启动服务器,监听8888端口
        ServerSocket server = new ServerSocket(8888);
        while (!Thread.interrupted()) {
            //不停的接收客户端请求
            Socket client = server.accept();
            //获取输入流
            InputStream ins = client.getInputStream();
            OutputStream out = client.getOutputStream();
//            int len = 0;
//            byte[] b = new byte[1024];
//            while ((len = ins.read(b)) != -1) {
//                System.out.println(new String(b, 0, len));
//            }
            BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
            String line = reader.readLine();
            System.out.println(line);
            //给用户(客户端)响应
            FileInputStream i = new FileInputStream("e:\\webroot\\index.html");
            PrintWriter pw = new PrintWriter(out);
            BufferedReader fr = new BufferedReader(new InputStreamReader(i, "UTF-8"));
            String c = null;
            pw.println("HTTP/1.1 200 OK");
            pw.println("Content-Type: text/html;charset=utf-8");
            pw.println("Content-Length: " + i.available());
            pw.println("Server: hello");
            pw.println("Date: " + new Date());
            pw.println("");
            pw.flush();
            while ((c = fr.readLine()) != null) {
                pw.println(c);
            }
            pw.flush();
            fr.close();
            pw.close();
            reader.close();
            client.close();
        }
        server.close();
    }
}


关键的代码:需要按照http格式响应,客户端才能识别。


pw.println("HTTP/1.1 200 OK");
  pw.println("Content-Type: text/html;charset=utf-8");
  pw.println("Content-Length: " + i.available());
  pw.println("Server: hello");
  pw.println("Date: " + new Date());
  pw.println("");
  pw.flush();


运行代码,这样我们就在服务端开放了8888端口,客户端请求去就可以了

在浏览器输入 http://localhost:8888/ 查看能否访问到index.html

5d4c6812c8535adbb050f4ddf2e1bce8.png

结果: 我们访问到了本地的index.html资源。

但是这里有个问题,只能请求一次,所以我们要进行优化,变成多线程版本。


2.简易web服务器版本2-多线程版


这里我们修改服务器端的代码


抽出一个单独响应客户端的线程类

mport java.io.*;
import java.net.Socket;
import java.util.Date;
public class ServerThread implements Runnable {
    private Socket client;
    private InputStream ins;
    private OutputStream out;
    private PrintWriter pw;
    private BufferedReader br;
    private void init() {
        try {
            ins = client.getInputStream();
            out = client.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public ServerThread(Socket client) {
        this.client = client;
        init();
    }
    @Override
    public void run() {
        try {
            go();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void go() throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(ins));
        String line = reader.readLine();
        System.out.println(line);
        //给用户响应
        FileInputStream i = new FileInputStream("e:\\webroot\\index.html");
        PrintWriter pw = new PrintWriter(out);
        BufferedReader fr = new BufferedReader(new InputStreamReader(i, "UTF-8"));
        String c = null;
        pw.println("HTTP/1.1 200 OK");
        pw.println("Content-Type: text/html;charset=utf-8");
        pw.println("Content-Length: " + i.available());
        pw.println("Server: hello");
        pw.println("Date: " + new Date());
        pw.println("");
        pw.flush();
        while ((c = fr.readLine()) != null) {
            pw.println(c);
        }
        pw.flush();
        fr.close();
        pw.close();
        reader.close();
        client.close();
    }
}


服务端的主类


import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class HttpServer2 {
    public static void main(String[] args) throws IOException {
        //启动服务器,监听8888端口
        ServerSocket server = new ServerSocket(8888);
        while (!Thread.interrupted()) {
            //不停的接收客户端请求
            Socket client = server.accept();
             new Thread(new ServerThread(client)).start();
        }
        server.close();
    }
}


这样多线程版已完成,测试

打开多个浏览器窗口,都可以访问到index.html的资源:

1dc618a0ed9580ce8bfa6facb208c08f.png


3.1简易web服务器版本3-访问图片资源


上面的index.html资源中,只有文字,如果我们修改下


<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>index.html</title>
</head>
<body>
  hello world
  <br>
  <img src="1.jpg">
</body>
</html>

1dc618a0ed9580ce8bfa6facb208c08f.png

1.jpg图片如下:

5d4c6812c8535adbb050f4ddf2e1bce8.png

现在修改代码,能够访问到图片,这样我们不能通过

PrintWriter pw = new PrintWriter(out);
 while ((c = fr.readLine()) != null) {
            pw.println(c);
        }


这段代码了,因为,这个只能读取文本,我们只能使用inputstream来读取二进制。同时,响应的时候content-type的响应头也不能固定为text/html了,要根据请求的资源动态修改,比如jpg的图片,后缀就应该是image/jpeg。

我们可以定义个缓存map进行动态赋值。


private static Map<String, String> contentMap = new HashMap<>();
    static {
        contentMap.put("html", "text/html;charset=utf-8");
        contentMap.put("jpg", "image/jpeg");
    }


浏览器访问的时候,可以看到打印的line为:

1dc618a0ed9580ce8bfa6facb208c08f.png

这其实就是http请求格式的第一行:

5d4c6812c8535adbb050f4ddf2e1bce8.png

我们用空格分割,索引为1的就是资源,我们可以得到资源的后缀,给content-type动态赋值,如果直接就是\。我们可以手动改成index.html:


BufferedReader reader = new BufferedReader(new InputStreamReader(ins));
        String line = reader.readLine().split(" ")[1].replace("/","\\");
        if (line.equals("\\")){
            line += "index.html";
        }
        System.out.println(line);


现在全部的修改如下

HttpServer2类代码不变,修改ServerThread

public class ServerThread implements Runnable {
    private static final String webroot = "e:\\webroot\\";
    private static Map<String, String> contentMap = new HashMap<>();
    static {
        contentMap.put("html", "text/html;charset=utf-8");
        contentMap.put("jpg", "image/jpeg");
    }
    private Socket client;
    private InputStream ins;
    private OutputStream out;
    private PrintWriter pw;
    private BufferedReader br;
    private void init() {
        try {
            ins = client.getInputStream();
            out = client.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public ServerThread(Socket client) {
        this.client = client;
        init();
    }
    @Override
    public void run() {
        try {
            go();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void go() throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(ins));
        String line = reader.readLine().split(" ")[1].replace("/","\\");
        if (line.equals("\\")){
            line += "index.html";
        }
        System.out.println(line);
        //给用户响应
        PrintWriter pw = new PrintWriter(out);
        FileInputStream i = new FileInputStream(webroot + line);
//        BufferedReader fr = new BufferedReader(new InputStreamReader(i, "UTF-8"));
        String c = null;
        pw.println("HTTP/1.1 200 OK");
        String s = line.substring(line.lastIndexOf(".")+1);
        System.out.println(s);
        pw.println("Content-Type: "+contentMap.get(s));
        pw.println("Content-Length: " + i.available());
        pw.println("Server: hello");
        pw.println("Date: " + new Date());
        pw.println("");
        pw.flush();
        byte[] buff = new byte[1024];
        int len = 0;
        while ((len = i.read(buff)) != -1) {
           out.write(buff, 0 , len);
        }
        pw.flush();
        i.close();
        pw.close();
        reader.close();
        client.close();
    }
}


启动HttpServer2,开放8888端口:

客户端访问,可看到图片也访问到了

1dc618a0ed9580ce8bfa6facb208c08f.png


3.2简易web服务器版本3-访问外链地址测试


版本3,其实也可以访问到外链地址,我们测试下:

修改index.html


<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>index.html</title>
</head>
<body>
  hello world
  <br>
  <img src="1.jpg">
  <a href="login.html">登陆</a>
</body>
</html>


我们在webroot下新建login.html

login.html

1dc618a0ed9580ce8bfa6facb208c08f.png

<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
    this is sign in html
    这是登陆页面奥
</body>
</html>


测试下能否访问到

1dc618a0ed9580ce8bfa6facb208c08f.png

点击登陆链接

5d4c6812c8535adbb050f4ddf2e1bce8.png

测试没有问题,可以访问到login.html


4.简易web服务器-版本4-连接池版


上面的代码有啥问题吗,其实每次都是重新开个线程,比较消耗资源,我们可以使用连接池进行,管理,这样对以后的线程的监控,管理等都可以有效控制。阿里代码规范中,也有说到,凡是创建线程,必须通过,线程池,现在我们进行优化,其实只修改HttpServer2主类即可。


原来的代码:


import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class HttpServer2 {
    public static void main(String[] args) throws IOException {
        //启动服务器,监听8888端口
        ServerSocket server = new ServerSocket(8888);
        while (!Thread.interrupted()) {
            //不停的接收客户端请求
            Socket client = server.accept();
             new Thread(new ServerThread(client)).start();
        }
        server.close();
    }
}


修改为:


public class HttpServer2 {
    public static void main(String[] args) throws IOException {
        ExecutorService pool = Executors.newCachedThreadPool();
        //启动服务器,监听8888端口
        ServerSocket server = new ServerSocket(8888);
        while (!Thread.interrupted()) {
            //不停的接收客户端请求
            Socket client = server.accept();
            pool.execute(new ServerThread(client));
        }
        server.close();
    }
}


启动httpserver2主类

测试

1dc618a0ed9580ce8bfa6facb208c08f.png

完美,结果没问题。

现在简易的web服务器已经实现了,其实还有需要可以优化的地方。比如httpservet的支持等,tomcat的源码中可以多多去研读。



相关文章
|
9天前
|
SQL Java
20:基于EL与JSTL的产品管理页-Java Web
20:基于EL与JSTL的产品管理页-Java Web
21 5
|
2天前
|
网络协议 Dubbo Java
【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器
【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器
9 0
|
3天前
|
前端开发 JavaScript Java
Java与Web开发的结合:JSP与Servlet
Java与Web开发的结合:JSP与Servlet
8 0
|
3天前
|
关系型数据库 MySQL Java
通过使用阿里云服务器,搭建Java程序的运行环境
通过使用阿里云服务器,搭建Java程序的运行环境
|
4天前
|
网络协议 Ubuntu Java
如何使用MCSM搭建我的世界Java版服务器并实现远程联机游戏
如何使用MCSM搭建我的世界Java版服务器并实现远程联机游戏
17 0
|
5天前
|
缓存 负载均衡 安全
深入探索Nginx高性能Web服务器配置与优化
【5月更文挑战第7天】本文深入探讨了Nginx的配置与优化,重点介绍了基础配置参数如`worker_processes`、`worker_connections`和`keepalive_timeout`,以及优化策略,包括使用epoll事件驱动模型、开启gzip压缩、启用缓存、负载均衡和安全配置。此外,还提到了性能调优工具,如ab、nginx-stats和nmon,以助于提升Nginx的性能和稳定性。
|
9天前
|
设计模式 前端开发 Java
19:Web开发模式与MVC设计模式-Java Web
19:Web开发模式与MVC设计模式-Java Web
20 4
|
9天前
|
设计模式 存储 前端开发
18:JavaBean简介及其在表单处理与DAO设计模式中的应用-Java Web
18:JavaBean简介及其在表单处理与DAO设计模式中的应用-Java Web
25 4
|
9天前
|
SQL Java 数据库连接
17:数据库连接池与Servlet整合-Java Web
17:数据库连接池与Servlet整合-Java Web
22 3
|
1天前
|
负载均衡 固态存储 Linux
阿里云轻量应用服务器、云服务器、gpu云服务器最新收费标准参考
轻量应用服务器、云服务器、gpu云服务器是阿里云服务器产品中,比较热门的云服务器产品类型,不同类型的云服务器产品收费模式与收费标准是不一样的,本文为大家展示这几个云服务器产品的最新收费标准情况,以供参考。
阿里云轻量应用服务器、云服务器、gpu云服务器最新收费标准参考