实验报告¶
实验报告这次不占什么分数,主要是实现的效果以及验收,所以本次报告我写的比较简略
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 显示自己的学号信息

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

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


4.Postman中GET/POST测试:




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

但是在实际的实现过程中,我想到了一个问题,就是这个代理服务器是仅仅只能代理我们实现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

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

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

可以看到代理服务器功能上是没什么问题的。
2.使用系统提供的代理,启动第二题的proxyserver,使用浏览器测试是否能成功访问考察时给出的地址
这里我还是选择了www.baidu.com/search/error.html作为地址:



3性能测试情况¶
3.1 基本压力测试¶
Number of Threads (users)=1000 Ramp-up period=10时,要求吞吐为100/sec左右 ,错误率0~10%:

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%: 这个需要先加一个集合点设置:

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

虽然吞吐量达到了要求,但是错误率却很高,在面对更大的压力时,服务器处理能力并没有自己预期地那么好。采用阻塞方式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库后完成的服务器结果并不好,也没有达到要求:

面对更差的结果(error率更高),屡次调试无果之后,结合我查阅的资料,我发现了一条小路——Netty.
Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能协议服务器和客户端。 Netty 底层使用了 JAVA 的 NIO 技术,并在其基础上进行了性能的优化,虽然Netty不是单纯的 JAVA nio,但是 Netty 的底层还是基于的是 nio 技术。

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)再去以此循环处理任务队列中的下一个事件

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()&¬_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实现了服务器,并用之去测试了一下,结果很好,全方位达到了要求:

总结¶
这是《计算机网络》这门课程最后一个实验,也是最富有挑战性的实验,我在这个实验上花费了很多时间,也有极大的收获。首先,为了完成本实验基本的功能,我复习了之前实验课上有关Socket编程、BIO/NIO的相关内容;经过漫长的调试,在基础的功能之上,我发现自己面对浩繁的Java NIO有些无能为力,然后自学了Netty框架的基本内容,最终凭借这个封装且有优化的工具实现了本实验的所有要求。
回顾一学期来自己学习计网的经历,我从最开始对Java编程一窍不通到现在稍微有了点感觉,这其中离不开面对每次的实验不断自学与钻研,更离不开助教学长、学姐,老师的悉心帮助,在此万分感谢你们🌹🌹🌹。