并发编程—自定义实现web服务器


手写网站服务器案例

**需求:模拟基于Http协议的网站服务器,使用浏览器访问自己编写的服务端程序。**然后压测看一看

案例分析

  • 准备测试页面及图片,存放在web文件夹
  • 模拟服务器端(ServerSocket)使用浏览器访问,查看页面效果
  • 本案例涉及并发编程与网络编程,我们先来观察并发编程部分

01-服务器实现:单线程版本

不足:

  • 一次只能处理一个HTTP请求,处理完服务就停止了,不支持多任务【既可以响应html网页,也可以响应图片】
  • 因为是单线程程序,完全没有性能可言
  • 端口硬编码
/**
 * 单线程版本
 */
public class BsServer01 {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器 启动.....  ");
        System.out.println("开启端口 : 9999.....  ");
        // 1. 创建服务端ServerSocket
        ServerSocket serverSocket = new ServerSocket(9999);
        // 2. 循环接收,建立连接
        Socket accept = serverSocket.accept();
        /*
         *3. socket对象进行读写操作
         */
        //转换流,读取浏览器请求第一行
        BufferedReader readWb = new BufferedReader(new InputStreamReader(accept.getInputStream()));
        String requst = readWb.readLine();//包含URL地址
        //取出请求资源的路径
        String[] strArr = requst.split(" ");
        System.out.println(Arrays.toString(strArr));
        String path = strArr[1].substring(1);//截取请求path
        System.out.println(path);

        //----前提请求的Path与文件相对路径的Path是相同的
        FileInputStream fis = new FileInputStream(path);
        System.out.println(fis);
        byte[] bytes= new byte[1024];
        int len = 0 ;

        //向浏览器 回写数据
        OutputStream out = accept.getOutputStream();
        out.write("HTTP/1.1 200 OK\r\n".getBytes());
        out.write("Content-Type:text/html\r\n".getBytes());
        out.write("\r\n".getBytes());
        while((len = fis.read(bytes))!=-1){
            out.write(bytes,0,len);
        }

        fis.close();
        out.close();
        readWb.close();
        accept.close();

    }
}

02-服务器实现:多线程版本

文件上传的案例中,服务器只能为客户端服务器一次,之后服务器端程序就会结束。而我们必须做到让服务器程序不能结束,时时刻刻都要为客户端服务。而且同时可以为多个客户端提供服务器,要做到一个客户端请求就要开启一个新线程。

不足:

  • 频繁的创建线程会增加系统资源开销。为每个请求建立新的线程,线程无法复用这种操作代价过高。
  • 端口硬编码
/**
 * 多线程版本
 */
public class BsServer02 {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器 启动.....  ");
        System.out.println("开启端口 : 9999.....  ");
        // 1. 创建服务端ServerSocket
        ServerSocket serverSocket = new ServerSocket(9999);
        // 2. 循环接收,建立连接
        while (true) {
            Socket accept = serverSocket.accept();
            /*
            3. socket对象交给子线程处理,进行读写操作
            Runnable接口中,只有一个run方法,使用lambda表达式简化格式
            */
            new Thread(() -> {
                try{
                    /*
                     *socket对象进行读写操作
                     */
                    //转换流,读取浏览器请求第一行
                    BufferedReader readWb = new BufferedReader(new InputStreamReader(accept.getInputStream()));
                    String requst = readWb.readLine();
                    //取出请求资源的路径
                    String[] strArr = requst.split(" ");
                    System.out.println(Arrays.toString(strArr));
                    String path = strArr[1].substring(1);
                    System.out.println(path);

                    //----
                    FileInputStream fis = new FileInputStream(path);
                    System.out.println(fis);
                    byte[] bytes= new byte[1024];
                    int len = 0 ;

                    //向浏览器 回写数据
                    OutputStream out = accept.getOutputStream();
                    out.write("HTTP/1.1 200 OK\r\n".getBytes());
                    out.write("Content-Type:text/html\r\n".getBytes());
                    out.write("\r\n".getBytes());
                    while((len = fis.read(bytes))!=-1){
                        out.write(bytes,0,len);
                    }

                    fis.close();
                    out.close();
                    readWb.close();
                    accept.close();

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

03-服务器实现:线程池版本

频繁的创建线程会增加系统资源的开销,为降低资源开销,可以利用线程池复用线程进行优化。

不足:

  • 端口硬编码
/**
 * 线程池版本
 */
public class BsServer03 {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器 启动.....  ");
        System.out.println("开启端口 : 9999.....  ");
        // 创建服务端ServerSocket
        ServerSocket serverSocket = new ServerSocket(9999);
        //创建10个线程的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(200);

        while (true) {
            Socket accept = serverSocket.accept();
            //提交线程执行的任务
            executorService.submit(()->{
                try{
                    /*
                     *socket对象进行读写操作
                     */
                    //转换流,读取浏览器请求第一行
                    BufferedReader readWb = new BufferedReader(new InputStreamReader(accept.getInputStream()));
                    String requst = readWb.readLine();
                    //取出请求资源的路径
                    String[] strArr = requst.split(" ");
                    System.out.println(Arrays.toString(strArr));
                    String path = strArr[1].substring(1);
                    System.out.println(path);

                    //----
                    FileInputStream fis = new FileInputStream(path);
                    System.out.println(fis);
                    byte[] bytes= new byte[1024];
                    int len = 0 ;

                    //向浏览器 回写数据
                    OutputStream out = accept.getOutputStream();
                    out.write("HTTP/1.1 200 OK\r\n".getBytes());
                    out.write("Content-Type:text/html\r\n".getBytes());
                    out.write("\r\n".getBytes());
                    while((len = fis.read(bytes))!=-1){
                        out.write(bytes,0,len);
                    }

                    fis.close();
                    out.close();
                    readWb.close();
                    accept.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

04-服务器实现:配置文件设置端口号

上面案例中,接口存在硬编码,如果要更换端口号只能修改源代码,程序的维护性很差。

解决办法:将端口号写在配置文件(xml)中,需要更换端口号,可以直接修改配置文件。当然,线程池的配置信息也可以加入到配置文件中来,这里就不做实现了。

在src目录下创建文件server.xml:

<?xml version="1.0" encoding="utf-8"?>
<server>
    <port>8080</port>
</server>

读取xml中的端口号:

/**
 * 线程池 + 配置文件读取端口
 */
public class BsServer04 {
    public static void main(String[] args) throws DocumentException, IOException {
        //初始化端口
        //读取配置文件Server.xml中的端口号
        InputStream in = BsServer04.class.getClassLoader().getResourceAsStream("server.xml");
        //获取配置文件输入流
        SAXReader saxReader = new SAXReader();
        Document doc = saxReader.read(in);
        //使用SAXReader + XPath读取端口配置
        Element portEle = (Element) doc.selectSingleNode("//port");
        String port = portEle.getText();

        System.out.println("服务器 启动.....  ");
        System.out.println("开启端口 : " + Integer.valueOf(port));
        //启动web服务
        ServerSocket server = new ServerSocket(Integer.valueOf(port));
        //线程池
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        while(true){
            Socket socket = server.accept();
            //提交一个任务
            executorService.submit(()->{
                try{
                    //转换流,读取浏览器请求第一行
                    BufferedReader readWb = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    String requst = readWb.readLine();
                    //取出请求资源的路径
                    String[] strArr = requst.split(" ");
                    System.out.println(Arrays.toString(strArr));
                    String path = strArr[1].substring(1);
                    System.out.println(path);

                    FileInputStream fis = new FileInputStream(path);
                    System.out.println(fis);
                    byte[] bytes= new byte[1024];
                    int len = 0 ;

                    //向浏览器 回写数据
                    OutputStream out = socket.getOutputStream();
                    out.write("HTTP/1.1 200 OK\r\n".getBytes());
                    out.write("Content-Type:text/html\r\n".getBytes());
                    out.write("\r\n".getBytes());
                    while((len = fis.read(bytes))!=-1){
                        out.write(bytes,0,len);
                    }
                    fis.close();
                    out.close();
                    readWb.close();
                    socket.close();
                }catch(Exception ex){
                    ex.printStackTrace();
                }
            });
        }
    }
}

导入POM依赖

<dependency>
    <groupId>org.dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>2.1.3</version>
</dependency>
<dependency>
    <groupId>jaxen</groupId>
    <artifactId>jaxen</artifactId>
    <version>1.1.6</version>
</dependency>

文章作者: Gustavo
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Gustavo !
评论
  目录