计网Final project-Web Server

实验目的

img

img

img

实验任务

img

img

实验过程

用java语言开发一个简单的web服务器,仅能处理一个请求,通过ServerSocket和Socket进行代码实现

实现思路

ServerSocket通过accept()方法阻塞等待请求,每次收到一个请求将socket传递给一个新的线程进行接管。由于accept(),read(),write()方法都是阻塞的,可以采用多线程接管连接来提高并行效率。服务器需要从输入流中解析出url,读取url对应的文件内容,将文件内容和http首部打包后写入输出流,当url为”/shutdown”时,关闭serversocket来关闭服务器。

实现代码

package IO;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

public class Server implements Runnable{
    static ServerSocket serverSocket;
    static String messageWrapper(int statusCode, String message) {
        return "HTTP/1.1 " + statusCode + "\r\n" +
                "Content-Type:text/html;charset=utf-8" +"\r\n" +
                "\r\n" +
                message;
    }
    static String parseURL(String mess) {
        int idx1, idx2;
        idx1 = mess.indexOf(' ');
        if (idx1 != -1) {
            idx2 = mess.indexOf(' ', idx1 + 1);
            if (idx2 != -1) return mess.substring(idx1 + 1, idx2);
        }
        return null;
    }
    Socket socket;
    Server(Socket socket_) {
        socket = socket_;
    }
    public void run() {
        try {
            System.out.println("客户端:" + socket.getInetAddress().getLocalHost() + "已连接到服务器");
            InputStream is = socket.getInputStream();
            OutputStream os = socket.getOutputStream();
            InputStreamReader isr = new InputStreamReader(is);
            OutputStreamWriter osw = new OutputStreamWriter(os);
            BufferedReader br = new BufferedReader(isr);
            BufferedWriter bw = new BufferedWriter(osw);
            //读取客户端发送来的消息
            String mess = br.readLine();
            System.out.println("客户端:" + mess);
            String url = parseURL(mess);
            if (url == null || url.equals("/")) url = "/null";
            System.out.println("URL :" + url);
            if (url.equals("/shutdown")) {
                System.out.println("关闭服务器...");
                socket.close();
                return;
            } else {
                File file = new File("src" + url);
                if (file.exists()) {
                    Long fileLength = file.length();
                    byte[] fileContent = new byte[fileLength.intValue()];
                    FileInputStream fis = new FileInputStream(file);
                    fis.read(fileContent);
                    fis.close();
                    String page = new String(fileContent);
                    bw.write(messageWrapper(200, page));
                } else{
                    bw.write(messageWrapper(404, "404 Not Found."));
                }
            }
            bw.close();
        } catch (IOException e) {
            // e.printStackTrace();
            System.out.println("线程关闭...");
        }
    }
    public static void main(String[] args) throws IOException {
        serverSocket = new ServerSocket(8081);
        int poolSize = 10;
        ThreadPoolExecutor pool = new ThreadPoolExecutor(poolSize, poolSize,
                60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue(poolSize),
                new ThreadPoolExecutor.DiscardPolicy());

        while (true) {
            System.out.println("线程等待连接...");
            Socket s = serverSocket.accept();
            pool.execute(new Server(s));
        }
    }
}

测试

访问/index.html

image-20210619123448917

访问无效地址:

img

访问/shutdown:

img

Postman测试:

Get方法:

img

Post方法:

img

用java语言实现一个web代理服务器

实现思路

代理服务器既作为服务器接收浏览器的请求,也作为客户端向web服务器发送请求。在转发时,代理服务器不会将接收到的所有内容发送给服务器,而是只发送包含请求url的第一行内容,减少web服务器处理压力。类似于web服务器,由于阻塞方法较多,采用多线程方式提高并发效率,并用线程池管理线程,线程池拒绝策略为DiscardPolicy(),即对于超过线程上限的请求直接丢弃。

实现代码

package IO;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

public class Proxy implements Runnable{
    static ServerSocket ss_client;
    static String messageWrapper(int statusCode, String message) {
        return "HTTP/1.1 " + statusCode + "\r\n" +
                "Content-Type:text/html;charset=utf-8" +"\r\n" +
                "\r\n" +
                message;
    }
    static String parseURL(String mess) {
        int idx1, idx2;
        idx1 = mess.indexOf(' ');
        if (idx1 != -1) {
            idx2 = mess.indexOf(' ', idx1 + 1);
            if (idx2 != -1) return mess.substring(idx1 + 1, idx2);
        }
        return null;
    }
    Socket s_client;
    Proxy(Socket s_) {
        s_client = s_;
    }
    public void run() {
        try {
            Socket s_server = new Socket("127.0.0.1", 8081);
            // System.out.println("客户端:" + s_client.getInetAddress().getLocalHost() + ":" +  s_client.getPort() + "已连接到服务器");
            // System.out.println("服务端:" + s_server.getInetAddress().getLocalHost() + ":" +  s_server.getPort() + "已连接到服务器");

            InputStream is_client = s_client.getInputStream();
            OutputStream os_client = s_client.getOutputStream();
            InputStream is_server = s_server.getInputStream();
            OutputStream os_server = s_server.getOutputStream();
            InputStreamReader isr_client = new InputStreamReader(is_client);
            OutputStreamWriter osw_client = new OutputStreamWriter(os_client);
            InputStreamReader isr_server = new InputStreamReader(is_server);
            OutputStreamWriter osw_server = new OutputStreamWriter(os_server);

            BufferedReader br_client = new BufferedReader(isr_client);
            BufferedWriter bw_client = new BufferedWriter(osw_client);
            BufferedReader br_server = new BufferedReader(isr_server);
            BufferedWriter bw_server = new BufferedWriter(osw_server);

            //读取客户端发送来的消息
            String mess_client = br_client.readLine();
            // System.out.println(mess_client);
            //bw_server.write(mess_client);
            bw_server.write(mess_client + '\n');
            // bw_server.flush();
            bw_server.flush();
            // bw_server.close(); // 不能关 输出流关闭会造成socket被关闭 输入输出流都不可用
            String mess_server;
            String mess = "";
            while((mess_server = br_server.readLine()) != null) {
                mess += mess_server +'\n';
            }
            bw_client.write(mess);

            bw_client.close();
        } catch (IOException e) {
            // e.printStackTrace();
        }
    }
    public static void main(String[] args) throws IOException {
        ss_client = new ServerSocket(8082);
        int poolSize = 30;
        ThreadPoolExecutor pool = new ThreadPoolExecutor(poolSize, poolSize,
                60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue(poolSize),
                new ThreadPoolExecutor.DiscardPolicy());

        while (true) {
            // System.out.println("线程等待连接...");
            Socket s = ss_client.accept();
            pool.execute(new Proxy(s));
        }
    }
}

测试

浏览器:

image-20210619123435771

Postman:

img

附加:分析现有能支持同时连接的最大数,修改代码使服务器使其能支持一千个连接

实现思路

使用NIO实现web服务器,通过非阻塞的方式实现IO。NIO实现中,阻塞方法只有selector.select() 一种,因此不需要显式的多线程就能达到较高的并发效率,而是通过selector管理多线程。

实现代码

Server.java

package NIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;


public class Server implements Runnable{
    static ServerSocketChannel serverSocketChannel;
    static Selector selector;
    static final int PORT = 8081;
    static final String HOST = "127.0.0.1";

    public static void main(String[] args) throws IOException {
        Thread thread = new Thread(new Server());
        thread.start();
    }
    public void run() {
        try {
            createServer();
            while(serverSocketChannel.isOpen()) {
                selector.select();
                Set<SelectionKey> sets = selector.selectedKeys();
                Iterator<SelectionKey> iterator = sets.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    doHandleInteresting(selectionKey);
                    iterator.remove();
                }
            }
        }  catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
    private void doHandleInteresting(SelectionKey selectionKey) throws Exception {
        if (selectionKey.isAcceptable()) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
        } else if (selectionKey.isReadable()) {
            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
            Request request = new Request(socketChannel);
            request.handle();
            Response response = new Response(request, socketChannel, serverSocketChannel);
            response.handle();
            socketChannel.close();
        }
    }
    static void createServer() throws IOException {
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(HOST, PORT));
        serverSocketChannel.configureBlocking(false);
        createSelector();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }
    static void createSelector() throws IOException {
        selector=Selector.open();
    }
}

Response.java

package NIO;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class Response {
    private Request request;
    private SocketChannel socketChannel;
    private ServerSocketChannel serverSocketChannel;
    Response(Request request_, SocketChannel socketChannel_, ServerSocketChannel serverSocketChannel_) {
        request = request_;
        socketChannel = socketChannel_;
        serverSocketChannel = serverSocketChannel_;
    }
    static String messageWrapper(int statusCode, String message) {
        return "HTTP/1.1 " + statusCode + "\r\n" +
                "Content-Type:text/html;charset=utf-8" +"\r\n" +
                "\r\n" +
                message;
    }
    public void handle() throws IOException {
        if (request.getUrl().equals("/shutdown")) {
            System.out.println("服务器关闭...");
            serverSocketChannel.close();
            return;
        }
        ByteBuffer byteBuffer=ByteBuffer.allocate(64);
        File file = new File("src/", request.getUrl());
        if (file.exists()) {
            FileInputStream fileInputStream = new FileInputStream(file);
            FileChannel fileChannel = fileInputStream.getChannel();
            String message = messageWrapper(200, "");
            byteBuffer.put(message.getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            fileChannel.transferTo(0, fileChannel.size(), socketChannel);
            fileInputStream.close();
        } else {
            String message = messageWrapper(200, "404 NOT FOUND!");
            byteBuffer.put(message.getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
        }

    }
}

Request.java

package NIO;

import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Request {
    private String requestContext;
    private SocketChannel socketChannel;
    private String url;
    Request(SocketChannel socketChannel_) {
        socketChannel = socketChannel_;
    }
    public void handle() throws Exception{
        parseRequestContext();
        parseRequestUrl();
    }
    public String getContext() { return requestContext; }
    public String getUrl() { return url; }
    private void parseRequestContext() throws Exception {
        ByteBuffer byteBuffer = ByteBuffer.allocate(64);
        StringBuilder stringBuilder = new StringBuilder();
//        int length;
//        while ((length = socketChannel.read(byteBuffer)) > 0) {
//            stringBuilder.append(new String(byteBuffer.array(), 0, length));
//        }
        int length = socketChannel.read(byteBuffer);
        stringBuilder.append(new String(byteBuffer.array(), 0, length));
        requestContext = stringBuilder.toString();
        // System.out.println(requestContext + "?");
    }
    private void parseRequestUrl() {
        int idx1, idx2;
        idx1 = requestContext.indexOf(' ');
        url = "/null";
        if (idx1 != -1) {
            idx2 = requestContext.indexOf(' ', idx1 + 1);
            if (idx2 != -1) url = requestContext.substring(idx1 + 1, idx2);
        }
    }
}

并发测试

利用jmeter进行并发测试,时间为20,总请求数为1000、5000、10000、20000

  • IO实现:

请求数1000:

img

请求数5000:

img

请求数10000:

img

请求数20000:

img

  • NIO实现

请求数1000:

img

请求数5000:

img

请求数10000:

img

请求数20000:

img

总结

可以发现,当吞吐量<500/s时,IO实现的web服务器和NIO实现的web服务器都能响应所有请求,不会发生error,当吞吐量>=500/s时,开始发生error,NIO实现的web服务器error率明显低于IO实现的web服务器。NIO实现的web服务器可以处理1000/s吞吐量的服务,error率为0.07%。

对于IO实现的web服务器,由于IO方法是阻塞的,每个线程同时只能处理一个请求,于是同时处理的请求数取决于线程数的上限,而线程数很难达到一千个,NIO实现的web服务器是非阻塞的,因此可以同时处理大量请求。

实现过程中加深了对java网络编程的认识,并了解了哪些方法是阻塞的,哪些是非阻塞的,以及这些方法对并发处理效率的影响。对于web代理服务器采用了线程池管理线程,了解了线程池的创建和使用。也了解了如postman,jmeter之类的发包软件用于测试。

在高并发压力测试中,由于并发数较大,一些调试信息被反复输出也会导致对测试结果产生影响,应注释掉大多数调式信息来提高测试准确度。