计网Final project-Web Server
实验目的
实验任务
实验过程
用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
访问无效地址:
访问/shutdown:
Postman测试:
Get方法:
Post方法:
用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));
}
}
}
测试
浏览器:
Postman:
附加:分析现有能支持同时连接的最大数,修改代码使服务器使其能支持一千个连接
实现思路
使用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:
请求数5000:
请求数10000:
请求数20000:
- NIO实现
请求数1000:
请求数5000:
请求数10000:
请求数20000:
总结
可以发现,当吞吐量<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之类的发包软件用于测试。
在高并发压力测试中,由于并发数较大,一些调试信息被反复输出也会导致对测试结果产生影响,应注释掉大多数调式信息来提高测试准确度。