一: 原理
服务器接受http请求并解析为java对象,如果是静态资源请求则进行处理,是其他请求转发到指定的服务器
二: 代码实现
1.使用nio创建服务器接受http请求
public class HttpServer {
private ServerSocketChannel server;
private Selector selector;
private boolean running = false;
private ExecutorService executor = Executors.newFixedThreadPool(10);
public void init(int port) throws IOException {
this.server = ServerSocketChannel.open();
this.selector = Selector.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
server.register(selector, SelectionKey.OP_ACCEPT);
}
public void start() {
if (server == null || selector == null) {
throw new RuntimeException("服务器未初始化");
}
if (running) {
throw new RuntimeException("重复启动服务器");
}
this.running = true;
Runnable main = () ->{
while (running) {
try {
selector.select(); //没有事件可选择时会阻塞,它仅在选择了至少一个通道后才会返回
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey current = iterator.next();
if (current.isAcceptable()) {
System.out.println("有可连接事件");
SocketChannel channel = server.accept();
//channel.socket().setTcpNoDelay(true);
System.out.println("连接成功");
channel.configureBlocking(false);
channel.register(selector,SelectionKey.OP_READ);
}
if (current.isReadable()) {
//System.out.println("有可读事件");
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel channel = (SocketChannel) current.channel();
//System.out.println(channel);
int read = 0;
StringBuilder builder = new StringBuilder();
while ((read = channel.read(byteBuffer)) > 0){
byteBuffer.flip();
byte[] array = byteBuffer.array();
builder.append(new String(array,0,read, StandardCharsets.UTF_8));
byteBuffer.clear();
}
String requestStr = builder.toString();
HttpRequestResolver httpRequestResolver = new HttpRequestResolver();
httpRequestResolver.resolver(requestStr);
if (httpRequestResolver.isLegal()){
System.out.println("请求: \n " + requestStr);
RequestTask task = new RequestTask(channel, requestStr, httpRequestResolver);
System.out.println("已提交处理");
executor.execute(task);
}
//重新注册为读事件
//channel.register(selector,SelectionKey.OP_READ);
}
iterator.remove();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
executor.execute(main);
}
public void stop() {
running = false;
close();
}
private void close() {
try {
server.close();
selector.close();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static void main(String[] args) { HttpServer server1 = new HttpServer();
try {
server1.init(8080);
server1.start();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
2. 处理解析http请求
public class HttpRequestResolver {
private String method;
private String url;
private String version;
private Map<String,String> headers = new HashMap<>();
private String body;
private boolean isLegal = false;
public void resolver(String request){
if (request.trim().equals("")){
return;
}
String[] lines = request.split("\n");
//request line
String[] item = lines[0].split(" ");
if (item.length != 3){
throw new RuntimeException("bad request \n" + request);
}
method = item[0].trim();
url = item[1].trim();
version = item[2].trim();
//header
for (int i = 1; i < lines.length; i++){
if (lines[i] == null || lines[i].trim().equals("")){
break;
}
String[] header = lines[i].split(": ");
if (header.length != 2){
throw new RuntimeException("bad request");
}
headers.put(header[0],header[1].trim());
}
//body
String last = lines[lines.length - 1];
body = last.trim().equals("") ? null : last;
isLegal = true;
}
public String getMethod(){
return method;
}
public String getUrl(){
return url;
}
public String getVersion(){
return version;
}
public String getBody(){
return body;
}
public String getHeader(String header){
return headers.get(header);
}
public boolean isLegal(){
return this.isLegal;
}
}
3. 处理http请求
/**
* 处理请求任务
*
*/
public class RequestTask implements Runnable {
private static final String errorPath;
private static final String classPath;
private static final ExecutorService executorService;
private final SocketChannel socketChannel;
private final String requestStr;
private final HttpRequestResolver requestResolver;
static {
classPath = Thread.currentThread().getContextClassLoader().getResource("").getPath().substring(1);
errorPath = classPath + "html/404.html";
executorService = Executors.newCachedThreadPool();
}
public RequestTask(SocketChannel socketChannel, String requestStr, HttpRequestResolver requestResolver) {
this.socketChannel = socketChannel;
this.requestStr = requestStr;
this.requestResolver = requestResolver;
}
@Override
public void run() {
synchronized (socketChannel){
System.out.println("开始处理请求");
requestResolver.resolver(requestStr);
String url = requestResolver.getUrl();
String extension = getExtension(url);
if (extension == null) {
//转发请求
TranspondTask transpondTask = new TranspondTask(socketChannel, requestStr);
executorService.execute(transpondTask);
} else {
String mimeType;
if ((mimeType = ContentType.getMimeType(extension)) != null) {
System.out.println("响应数据");
response(url, mimeType);
} else {
response404();
}
}
}
}
private void response(String url, String mimeType) {
try {
String realityUrl = classPath + "html" + url;
HttpResponseBuilder httpResponseBuilder = new HttpResponseBuilder();
byte[] content = content(realityUrl);
System.out.println(content.length);
String response = httpResponseBuilder.version("HTTP/2")
.contentType(mimeType)
.contentLength(content.length)
.build();
send(response.getBytes());
send(content);
responseFinished();
} catch (IOException e) {
response404();
throw new RuntimeException(e);
}
}
/**
* 通过文件路径获取文件内容
*
* @param path 文件绝对路径
* @return 文件内容
* @throws FileNotFoundException fnt
*/
private byte[] content(String path) throws IOException {
System.out.println(path);
byte[] resource = Resource.getResource(path);
if (resource == null){
throw new FileNotFoundException();
}
return resource;
}
/**
* 发送报文到客户端
*
* @param content 报文内容
* @throws IOException ioe
*/
private void send(String content) throws IOException {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
buffer.put(bytes);
buffer.flip();
socketChannel.write(buffer);
}
/**
* 发送请求体到客户端
*
* @param body 响应内容
* @throws IOException ioe
*/
private void send(byte[] body) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocate(body.length);
byteBuffer.put(body);
byteBuffer.flip();
socketChannel.write(byteBuffer);
responseFinished();
}
/**
* 响应错误信息
*/
private void response404() {
try {
HttpResponseBuilder httpResponseBuilder = new HttpResponseBuilder();
byte[] content = content(errorPath);
String response = httpResponseBuilder
.statesCode(404)
.contentType("text/html")
.contentLength(content.length)
.build();
send(response);
send(content);
responseFinished();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
/**
* 响应完成后调用关闭socketChannel
* @throws IOException ioe
*/
private void responseFinished() throws IOException {
//强制刷新
socketChannel.socket().getOutputStream().flush();
}
private String getExtension(String url) {
int i = url.lastIndexOf(".");
if (i == -1) return null;
else return url.substring(i);
}
}
4. 将静态资源加载到内存
public class Resource {
private static final String classPath;
private static final Map<String,byte[]> resourceMap;
static {
classPath = Thread.currentThread().getContextClassLoader().getResource("").getPath().substring(1);
resourceMap = new HashMap<>();
String resourcePath = classPath + "/html";
File file = new File(resourcePath);
loadResource(file);
}
private static void loadResource(File file){
File[] files = file.listFiles();
if (files == null) return;
for(File item : files){
if (item.isFile()){
String path = item.getPath();
System.out.println(path);
try {
byte[] bytes = Files.readAllBytes(Paths.get(path));
resourceMap.put(path,bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}else {
loadResource(item);
}
}
}
public static byte[] getResource(String filePath){
filePath = filePath.replaceAll("/", Matcher.quoteReplacement(File.separator));
return resourceMap.get(filePath);
}
}
5. 构建响应字符串
public class HttpResponseBuilder {
private String version = "HTTP/2";
private int statesCode = 200;
private String states = "ok";
private Map<String,String> headers = new HashMap<>();
private String body;
public HttpResponseBuilder version(String version){
this.version = version;
return this;
}
public HttpResponseBuilder statesCode(int statesCode){
this.statesCode = statesCode;
return this;
}
public HttpResponseBuilder states(String states){
this.states = states;
return this;
}
public HttpResponseBuilder addHeader(String key, String value){
headers.put(key,value);
return this;
}
public HttpResponseBuilder body(String body){
this.body = body;
return this;
}
public HttpResponseBuilder contentType(String mimeType){
headers.put("content-type",mimeType);
return this;
}
public HttpResponseBuilder contentLength(Integer length){
headers.put("content-length",length.toString());
return this;
}
public String build(){
StringBuilder builder = new StringBuilder();
builder.append(version)
.append(" ")
.append(statesCode)
.append(" ")
.append(states)
.append("\n");
for (Map.Entry<String,String> item : headers.entrySet()){
builder.append(item.getKey())
.append(": ")
.append(item.getValue())
.append("\n");
}
builder.append("\n");
if (body != null) builder.append(body);
return builder.toString();
}
}
7. 执行转发请求的任务
/**
* 转发请求任务
*/
public class TranspondTask implements Runnable {
private final SocketChannel socketChannel;
private final String requestStr;
public TranspondTask (SocketChannel socketChannel,String requestStr){
this.socketChannel = socketChannel;
this.requestStr = requestStr;
}
@Override
public void run() {
try {
//目标服务器地址
SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8081));
channel.configureBlocking(false);
byte[] bytes = requestStr.getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bytes.length);
byteBuffer.put(bytes);
byteBuffer.flip();
channel.write(byteBuffer);
channel.shutdownOutput();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len =0;
while ((len = channel.read(buffer)) != -1){
buffer.flip();
String s = new String(buffer.array(), 0, len);
socketChannel.write(buffer);
buffer.clear();
}
channel.close();
socketChannel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
8.文件后缀名与content-type对应关系类
public class ContentType {
private static final Map<String,String> contentType;
static {
contentType = new HashMap<>();
contentType.put(".html" , "text/html");
contentType.put(".css" , "text/css");
contentType.put(".js" , "text/javascript");
//............
}
public static String getMimeType(String extension){
return contentType.get(extension);
}
}