跳转至

实验报告

实验报告这次不占什么分数,主要是实现的效果以及验收,所以本次报告我写的比较简略

完整代码压缩包

1题目要求

使用Java语言开发一个Web服务器,它能够处理HTTP请求,具体而言:

1.当一个客户端(浏览器)联系时创建一个连接socket

2.从这个连接socket接受HTTP请求

3.解释该请求以确定所请求的特定文件

4.从文件系统中获得请求的文件

5.创建一个由请求的文件组成的HTTP响应报文

6.经TCP连接向请求的浏览器返回响应

在功能上它需要:

1.使用ServerSocket和Socket进行代码实现

2.使用多线程接管连接

3.浏览器输入localhost:8081/index.html能显示自己的学号信息

4.浏览器输入localhost:8081其他无效路径,浏览器显示404 Not Found

5.浏览器输入localhost:8081/shutdown关闭服务器

6.使用postman进行测试,测试GET和POST两种请求方法

在Web 服务器基础之上实现Web代理服务器,让浏览器请求经过代理服务器来请求Web对象,具体而言:

1.从代理服务器从一个浏览器接收到对某个对象的HTTP请求时,它生成相同对象的一个新的HTTP请求并向初始服务器发送

2.当代理服务器从初始服务器接收到该对象的HTTP响应时,它生成一个包括该对象的新的HTTP响应,并发送给该客户

在功能上它需要:

1.使用ServerSocket和Socket进行代码实现

2.使用浏览器和postman分别进行测试

3.使用JMeter进行压测,在保证功能完整的前提下测试每秒响应的请求数

4.分析当前能支持同时连接的最大数,使用学习过的NIO修改代码使服务器能同时支持并发的1000个连接

2功能实现情况

2.1实验准备

在本次实验后期,我使用了Netty框架来优化,在实验初,要从Maven导入必要的库:

io.netty:netty-codec-http:4.1.77.Final和io.netty:netty-handler:4.1.77.Final

至于具体的导入流程,就是在“项目结构”的“库”模块中的新建项目库,从Maven导入,然后连接网络下载即可,这里就不赘述了。

2.2WebServer的实现

Web server的主要运行思路是这样的:有一个主线程,这个线程运行了一个ServerSocket,来监听是否有客户端连接;当一个连接到来时,即ServerSocket可以accept时,新建一个线程来完成解析报文、处理请求、回复报文三件事。

mport java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
// 采用BIO方式实现Web Server
public class BIOServer implements Runnable{
    static ServerSocket serverSocket;
    Socket socket;
    static int isShut;
    BIOServer(Socket s) {
        socket = s;
    }
    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 getURL(String message) {
        int idx1, idx2;
        idx1 = message.indexOf(' ');
        if (idx1 != -1) {
            idx2 = message.indexOf(' ', idx1 + 1);
            if (idx2 != -1) return message.substring(idx1 + 1, idx2);
        }
        return null;
    }

    @Override
    public void run() {
        try {
            System.out.println("Client:" + socket.getInetAddress().getLocalHost() + " has connected to the Server");
            // 准备工作
            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 message = br.readLine();
            System.out.println("Client: " + message);
            String url = getURL(message);
            if (url == null || url.equals("/")) url = "/index.html";
            System.out.println("URL :" + url);
            if (url.equals("/shutdown")) {
                System.out.println("shutting down the server...");
                socket.close();
                this.isShut = 1;
                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{
                    file = new File("src/404.html");
                    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(404, page));
                }
            }
            bw.close();
        } catch (IOException e) {
            System.out.println("shutting down the Thread...");
        }
    }
    public static void main(String[] args) throws IOException {
        serverSocket = new ServerSocket(8081);
        // 多线程的频繁创建和销毁很花费时间
        // 利用线程池我们可以回收那些本应该被销毁的线程,在需要的时候继续使用,这样可以提升运行效率
        int poolSize = 20;
        isShut = 0;
        ThreadPoolExecutor pool = new ThreadPoolExecutor(poolSize, poolSize, 120L, TimeUnit.SECONDS,
                new ArrayBlockingQueue(poolSize),
                new ThreadPoolExecutor.DiscardPolicy());

        while(isShut == 0) {
            System.out.println("Waiting for Threads' connection...");
            Socket s = serverSocket.accept();
            if(isShut == 1){
                s.close();
                continue;
            }
            pool.execute(new BIOServer(s));
        }
        serverSocket.close();
        pool.shutdown();
    }
}

测试结果如下所示

1.输入localhost:8081/index.html 显示自己的学号信息

image-20260427231305897

2.输入localhost:8081下其他无效路径,显示404:

image-20260427231322248

3.输入localhost:8081/shutdown关闭服务器:

image-20260427231338721

image-20260427231346054

​ 4.Postman中GET/POST测试:

图片 1

图片 2

图片 3

图片 4

2.3WebProxy的实现

其实代理服务器的I/O,控制逻辑与WebServer比较相似,只不过它既作为服务器接收客户端的请求,又作为客户端向真的服务器发送请求。如下图所示:

图片 5

但是在实际的实现过程中,我想到了一个问题,就是这个代理服务器是仅仅只能代理我们实现WebServer时那个监听localhost:8081端口的本地服务器吗?显然不是的,我们不仅要能代理它,还要代理其他的服务器,但是代理其他服务器的链接地址(比如www.baidu.com/search/error.html)和第一问的还不太一样,这就需要我们小心地来实现相应的链接解析函数。

由真的客户端发送请求给Proxy,然后Proxy处理这个请求报文,解析得到真的服务端的地址、端口、请求内容等信息,然后Proxy构建新的请求报文发给真的服务器;一段时间后,Proxy收到了来自真的服务端的响应报文,然后Proxy重构之后得到新的响应报文,将其发送给真的客户端。

mport java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
public class BIOProxy implements Runnable{
    // serverSocket is for true client.
    static ServerSocket serverSocket;
    Socket for_client; // 面向真的客户端
    BIOProxy(Socket s) {
        for_client = s;
    }
    @Override
    public void run() {
        try {
            // 代理作为服务器
            System.out.println("True client:" + for_client.getInetAddress().getLocalHost() + ":" + for_client.getPort() + " has connected.");
            InputStream is_for_client = for_client.getInputStream();
            OutputStream os_for_client = for_client.getOutputStream();
            InputStreamReader isr_for_client = new InputStreamReader(is_for_client);
            OutputStreamWriter osw_for_client = new OutputStreamWriter(os_for_client);
            BufferedReader br_for_client = new BufferedReader(isr_for_client);
            BufferedWriter bw_for_client = new BufferedWriter(osw_for_client);

            // 首先,应该读取客户端发送来的消息
            String mess_from_client = br_for_client.readLine();
            System.out.println(mess_from_client);

            // 使用正则表达式作为分隔符
            // \s表示匹配空白字符 +表示匹配连续 合在一起就是标识匹配1个/多个连续空白字符
            // split将mess_from_client拆分一下,结果放到result数组
            String[] result = mess_from_client.split("\\s+");
            // 以 GET http://www.baidu.com/search/error.html HTTP/1.1 为例
            String method = result[0],url = result[1],version = result[2];


            String[] ans = url.split("://"); // split之后为 http和www.baidu.com/search/error.html
            String tmp = ans[1].split("/")[0]; // 获得www.baidu.com

            String host,port;
            // 有冒号说明主机名和端口号都被提取了
            // 我默认使用80端口,即HTTP默认的端口
            if(tmp.contains(":")){
                host = tmp.split(":")[0];
                port = tmp.split(":")[1];
            }else{
                host = tmp;
                port = "80";
            }

            if(host.equals("localhost"))host = "127.0.0.1";
            // 代理作为客户端
            Socket for_server = new Socket(host, Integer.valueOf(port));
            System.out.println("True server:" + for_server.getInetAddress().getLocalHost() + ":" + for_server.getPort() + " has connected.");

            InputStream is_for_server = for_server.getInputStream();
            OutputStream os_for_server = for_server.getOutputStream();
            InputStreamReader isr_for_server = new InputStreamReader(is_for_server);
            OutputStreamWriter osw_for_server = new OutputStreamWriter(os_for_server);
            BufferedReader br_for_server = new BufferedReader(isr_for_server);
            BufferedWriter bw_for_server = new BufferedWriter(osw_for_server);

            // 然后,向服务端发送报文
            // IP地址是127.0.0.1,表示本地主机,这样就要按照我第一问写Server时的链接规则去特判
            if(host.equals("127.0.0.1")) {
                String[] tmpurl = url.split("/");
                url = "/" + tmpurl[tmpurl.length-1];
            }

            // 发送响应报文给真的服务器
            String message_to_server = method + " " + url + " " + version + "\r\n" + "\r\n";

            bw_for_server.write(message_to_server);
            System.out.println(message_to_server);
            // 将缓冲区中的数据全部发送走,不要等缓冲区满
            bw_for_server.flush();
            // 如果关闭了输出流,socket会被关闭,这样输入流也不能用了
            // bw_server.close();

            byte[] data = new byte[10240];
            int len;
            while ((len = is_for_server.read(data)) > 0) {  // read from true server
                os_for_client.write(data, 0, len);  // write to true client
            }
            os_for_client.flush();
            os_for_client.close();
        } catch (IOException e) {
            //e.printStackTrace();
        }
    }
    public static void main(String[] args) throws IOException {
        serverSocket = new ServerSocket(6666);//避免端口冲突
        int poolSize = 20;
        ThreadPoolExecutor pool = new ThreadPoolExecutor(poolSize, poolSize, 120L, TimeUnit.SECONDS,
                new ArrayBlockingQueue(poolSize),
                new ThreadPoolExecutor.DiscardPolicy());
        while (true) {
            System.out.println("Waiting for Threads' connection...");
            Socket s = serverSocket.accept();
            pool.execute(new BIOProxy(s));
        }
    }
}

1.使用postman设置代理,启动第二题的proxy,使用postman测试是否能成功访问www.baidu.com/search/error.html

图片 6

我们先要在postman软件中设置代理

图片 7

接下来再给localhost:8081/index.html发送请求:

图片 8

可以看到代理服务器功能上是没什么问题的。

​ 2.使用系统提供的代理,启动第二题的proxyserver,使用浏览器测试是否能成功访问考察时给出的地址

​ 这里我还是选择了www.baidu.com/search/error.html作为地址:

图片 9

图片 10

图片 11

3性能测试情况

3.1 基本压力测试

Number of Threads (users)=1000 Ramp-up period=10时,要求吞吐为100/sec左右 ,错误率0~10%:

图片 12

3.2 困难压力测试

​ 参数为Number of Threads (users)=5000, Ramp-up period=5时,增加集合点设置 Number of Simulated Users to Group by=1000,Timeout in milliseconds=0,吞吐为500/sec左右及以上,错误率0~10%: ​ 这个需要先加一个集合点设置:

图片 13

如果不加修改地直接去测试,结果很差:

图片 14

虽然吞吐量达到了要求,但是错误率却很高,在面对更大的压力时,服务器处理能力并没有自己预期地那么好。采用阻塞方式BIO实现的服务器显然是无法达到所需,结合我们之前所学的知识,这里换成非阻塞的NIO模式来实现应该是可以提升服务器的性能的。

NIO 即 New IO(Non-blocking IO),NIO 和 IO 有相同的作用以及目的,但 NIO 和 IO 实现方式不同,NIO 主要用到的是块,效率要比 IO 高很多,其主要差异如下:

1.Java IO 和 NIO 之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的。Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。Java NIO 的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。 而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

2.Java IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO 为非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入, 这个线程同时可以去做别的事情。线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作, 所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

3.Java NIO 的选择器允许一个单独的线程来监视多个输入通道,我们可以注册多个通道使用一个选择器, 然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

package NIOServer;

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 NIOServer 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 NIOServer());
        thread.start();
    }
    @Override
    public void run() {
        try {
            createServer();
            while(serverSocketChannel.isOpen()) {
                selector.select();
                // SelectionKey 将 Selector 与 SelectableChannel 关联起来
                Set<SelectionKey> sets = selector.selectedKeys();
                Iterator<SelectionKey> iterator = sets.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    doHandleInteresting(selectionKey);
                    iterator.remove();
                }
            }
        }  catch (Exception e) {
        }
    }
    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();
    }
}
package NIOServer;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Request {
    private String requestContext;
    private SocketChannel socketChannel;
    private String url;
    Request(SocketChannel s) {
        socketChannel = s;
    }
    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(1024);
        StringBuilder stringBuilder = new StringBuilder();

        int length = socketChannel.read(byteBuffer);
        stringBuilder.append(new String(byteBuffer.array(), 0, length));
        requestContext = stringBuilder.toString();
    }
    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);
        }
    }
}
package NIOServer;

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("shutting down the server...");
            serverSocketChannel.close();
            return;
        }
        ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
        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());
            // 将 buffer 的内容写入对应的目标对象,都需要先调用 flip 方法
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            fileChannel.transferTo(0, fileChannel.size(), socketChannel);
            fileInputStream.close();
        } else {
            file = new File("src/404.html");
            FileInputStream fileInputStream = new FileInputStream(file);
            FileChannel fileChannel = fileInputStream.getChannel();
            String message = messageWrapper(404, "");
            byteBuffer.put(message.getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            fileChannel.transferTo(0, fileChannel.size(), socketChannel);
            fileInputStream.close();
        }
    }
}

也许是自己在实现过程中出现了一些错误,沿着我们之前实验课的思路,我调用java的nio库后完成的服务器结果并不好,也没有达到要求:

图片 15

面对更差的结果(error率更高),屡次调试无果之后,结合我查阅的资料,我发现了一条小路——Netty.

Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能协议服务器和客户端。 Netty 底层使用了 JAVA 的 NIO 技术,并在其基础上进行了性能的优化,虽然Netty不是单纯的 JAVA nio,但是 Netty 的底层还是基于的是 nio 技术。

图片 16

1)有两组线程池:BossGroup 和 WorkerGroup,BossGroup 中的线程(可以有多个,图中只画了一个)专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。

2)BossGroup 和 WorkerGroup 含有多个不断循环的执行事件处理的线程,每个线程都包含一个 Selector,用于监听注册在其上的 Channel。

3)每个 BossGroup 中的线程循环执行以下三个步骤:

3.1)轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)

3.2)处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到 WorkerGroup 中某个线程上的 Selector 上

3.3)再去以此循环处理任务队列中的下一个事件

4)每个 WorkerGroup 中的线程循环执行以下三个步骤:

4.1)轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)

4.2)在对应的 NioSocketChannel 上处理 read/write 事件

4.3)再去以此循环处理任务队列中的下一个事件

图片 17

1)Netty 抽象出两组线程池:BossGroup 和 WorkerGroup,也可以叫做 BossNioEventLoopGroup 和 WorkerNioEventLoopGroup。每个线程池中都有 NioEventLoop 线程。BossGroup 中的线程专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。BossGroup 和 WorkerGroup 的类型都是 NioEventLoopGroup。

2)NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环就是一个 NioEventLoop。

3)NioEventLoop 表示一个不断循环的执行事件处理的线程,每个 NioEventLoop 都包含一个 Selector,用于监听注册在其上的 Socket 网络连接(Channel)。

5)每个 BossNioEventLoop 中循环执行以下三个步骤:

5.1)select:轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)

5.2)processSelectedKeys:处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到某个 WorkerNioEventLoop 上的 Selector 上

5.3)runAllTasks:再去以此循环处理任务队列中的其他任务

6)每个 WorkerNioEventLoop 中循环执行以下三个步骤:

6.1)select:轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)

6.2)processSelectedKeys:在对应的 NioSocketChannel 上处理 read/write 事件

6.3)runAllTasks:再去以此循环处理任务队列中的其他任务

7)在以上两个processSelectedKeys步骤中,会使用 Pipeline(管道),Pipeline 中引用了 Channel,即通过 Pipeline 可以获取到对应的 Channel,Pipeline 中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)。这里暂时不详细展开讲解 Pipeline。

package NettyServer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class NettyServer {
    private final static int port=8888;
    public static void main(String[]args) throws InterruptedException{
        // 时间循环组BossEventLoop负责接收客户端的连接
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // 将Socket交给WorkerEventLoopGroup进行IO处理
        EventLoopGroup workerGroup=new NioEventLoopGroup();
        try{
            ServerBootstrap serverBootstrap=new ServerBootstrap(); // 服务器端启动
            // 链式调用
            serverBootstrap.group(bossGroup,workerGroup)
                    // 使用NioServerSocketChannel作为服务器的通道
                    .channel(NioServerSocketChannel.class)
                    // 初始化服务端可连接队列
                    .option(ChannelOption.SO_BACKLOG,128)
                    .childOption(ChannelOption.TCP_NODELAY,true)
                    // 初始化对象
                    .childHandler(new NettyServerInitializer());

            // 通道处理器添加完毕后启动服务器
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();// 异步绑定端口

            //监听关闭
            channelFuture.channel().closeFuture().sync();
        }finally {
            // 释放资源
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

package NettyServer;

import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import java.io.File;
import java.io.RandomAccessFile;

public class NettyServerHandleAdapter extends SimpleChannelInboundHandler<FullHttpRequest> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest) throws Exception {
        String url = fullHttpRequest.uri();
        int not_found = 0;
        if(url.equalsIgnoreCase("/") || url.equalsIgnoreCase("/index.html")){
            url="/index.html"; //null
        }else if(url.equalsIgnoreCase("/shutdown")){
            System.exit(0);
        }else{
            url="/404.html";
            not_found = 1;
        }
        // 根据地址构建
        File file=new File("src"+url);
        // 构建http响应
        HttpResponse httpResponse=new DefaultHttpResponse(fullHttpRequest.protocolVersion(), HttpResponseStatus.OK);
        // 设置文件格式内容
        if(url.endsWith(".html")) httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");


        if(file.exists()&&not_found!=1){
            httpResponse.setStatus(HttpResponseStatus.OK);
        } else{
            httpResponse.setStatus(HttpResponseStatus.NOT_FOUND);
        }
        RandomAccessFile randomAccessFile=new RandomAccessFile(file,"r");

        // 设置HTTP头部信息
        httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, randomAccessFile.length());
        httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);

        channelHandlerContext.write(httpResponse);// 写回HTTP响应报文
        /*
            正常情况下,Java的内存有堆内存、栈内存和字符串常量池等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方。
            一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区。
            一个数据会被拷贝两次才能到达他的的终点,如果数据量大就会造成不必要的资源浪费。
            但是NIO免去了将文件的内容从文件系统移动到网络栈的的繁琐步骤,具有零拷贝特性
        */
        // 从FileInputStream创建一个DefaultFileRegion,然后将其写入Channel,利用零拷贝性质来传输一个文件
        channelHandlerContext.write(new DefaultFileRegion(randomAccessFile.getChannel(), 0, file.length()));// 写回文件

        // 写入文件尾部
        channelHandlerContext.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        randomAccessFile.close();// 关闭
    }
}

package NettyServer;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) {
        ChannelPipeline channelPipeline = socketChannel.pipeline();// pipeline里添加handler
        // 对http进行处理,将请求和应答消息编码或解码为HTTP消息
        channelPipeline.addLast(new HttpServerCodec());
        // 添加自定义处理器
        // 用POST方式请求服务器的时候,对应参数信息保存在message body中
        // 因为HttpServerCodec只能获取uri中的参数,如果用HttpServerCodec无法完全的解析Http POST请求
        // 所以这里需要加上HttpObjectAggregator类
        channelPipeline.addLast(new HttpObjectAggregator(65536)); // 64*1024

        // ChunkedWriteHandler进行大规模文件传输
        channelPipeline.addLast(new ChunkedWriteHandler());

        channelPipeline.addLast(new NettyServerHandleAdapter());
    }
}

利用这个封装好的框架,我不必去面对复杂的NIO API和类库,而且能利用到Netty对NIO的优化,也许我能够解决这个问题。抱着试一试的想法,参考网上的教程,我利用Netty实现了服务器,并用之去测试了一下,结果很好,全方位达到了要求:

图片 18

总结

这是《计算机网络》这门课程最后一个实验,也是最富有挑战性的实验,我在这个实验上花费了很多时间,也有极大的收获。首先,为了完成本实验基本的功能,我复习了之前实验课上有关Socket编程、BIO/NIO的相关内容;经过漫长的调试,在基础的功能之上,我发现自己面对浩繁的Java NIO有些无能为力,然后自学了Netty框架的基本内容,最终凭借这个封装且有优化的工具实现了本实验的所有要求。

回顾一学期来自己学习计网的经历,我从最开始对Java编程一窍不通到现在稍微有了点感觉,这其中离不开面对每次的实验不断自学与钻研,更离不开助教学长、学姐,老师的悉心帮助,在此万分感谢你们🌹🌹🌹。