欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-08 18:58:40
...

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部分组成,分别是:状态行、响应头、空行 和 响应体。

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

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
    java并发原理实战(14)--自己实现简易web服务器
    结果: 我们访问到了本地的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的资源:
java并发原理实战(14)--自己实现简易web服务器

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>

java并发原理实战(14)--自己实现简易web服务器
1.jpg图片如下:
java并发原理实战(14)--自己实现简易web服务器
现在修改代码,能够访问到图片,这样我们不能通过

 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为:
java并发原理实战(14)--自己实现简易web服务器
这其实就是http请求格式的第一行:
java并发原理实战(14)--自己实现简易web服务器
我们用空格分割,索引为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端口:
  • 客户端访问,可看到图片也访问到了
    java并发原理实战(14)--自己实现简易web服务器

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
java并发原理实战(14)--自己实现简易web服务器login.html

<!DOCTYPE html>
<html>
<head>
	<title></title>
</head>
<body>
    this is sign in html
    这是登陆页面奥
</body>
</html>
  • 测试下能否访问到
    java并发原理实战(14)--自己实现简易web服务器
  • 点击登陆链接

java并发原理实战(14)--自己实现简易web服务器
测试没有问题,可以访问到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主类
  • 测试
    java并发原理实战(14)--自己实现简易web服务器
    完美,结果没问题。

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


相关标签: 并发编程