这段时间学习了java的nio。
所以,自己练手,写了一个基于nio的java服务器。
可以说是一个简单的服务器吧。总共就是10多KB而已。
代码已经放到Github上。
目前只实现了基本的访问静态资源的功能。
1、基本原理
先上图:
其实原理很简单。
a、开启一个selector,然后不断用selector来select。selector.select()。可以获取发生的io时间。然后生成一个AnalyseHandle。扔到线程池。
b、AnalyseHandle会读取浏览器发过来的http请求信息,然后生成一个StaticHandle,扔到线程池。
c、StaticHandle根据http请求信息,读取文件内容,然后按照http协议,把内容返回。
整一个过程是比较简单的,同时每次请求,都会生成一个session对象,里面保存了请求的一些信息,每个handle里,都会有这个对象,这样子就能保证每个handle能获取这次请求的信息。
2、代码分析
随便画了个类图帮助大家理解。
Server类:
package me.idashu.server;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;/**
-
描述:服务端
-
@author: dashu
-
@since: 13-2-27
*/
public class Server implements Runnable{private int port = 80;
private Selector selector;public Server() {
}
@Override
public void run() {try { selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); ssc.socket().bind(new InetSocketAddress(port)); ssc.register(selector, SelectionKey.OP_ACCEPT); System.out.println("start server"); while(true){ int n = selector.select(); Set<SelectionKey> set = selector.selectedKeys(); Iterator<SelectionKey> it = set.iterator(); while (it.hasNext()) { SelectionKey key = it.next(); it.remove(); if (key.isAcceptable()) { Session session = new Session(key); System.out.println("access"); } else if (key.isReadable()) { key.interestOps(0); Session session = (Session) key.attachment(); AnalyseHandle analyseHandle = new AnalyseHandle(session); ThreadPool.getInstance().execute(analyseHandle); } } } } catch (IOException e) { e.printStackTrace(); }
}
public static void main(String[] args){
new Thread(new Server()).start();
}
}
这是程序的入口,在main函数里面,生成了一个Server类,然后运行里面的run方法。
在run方法里面,生成了一个Selector。然后一直循环调用selector.select()。
这方法可以获取一个key,首先一般都是先这样子if (key.isAcceptable()) {...}
如果这个事件是socket连接事件,就生成一个session。
session里面会根据key,获取一个ServerSocketChannel对象,然后在把对象绑定在selector。而且只关注read方法。
然后继续循环,让发生了else if (key.isReadable()) {...},就是这个key可以读的时候。
把刚才生成的session拿出来,生成一个AnalyseHandle对象,放到线程池。取消key关注的事件,key.interestOps(0)。
Handle抽象类:
package me.idashu.server;/**
-
描述:处理事务的类<br>
-
实现了 Runnable 接口,抽象类,必须继承
-
@author: dashu
-
@since: 13-3-4
*/
public abstract class Handle implements Runnable{protected Session session;
public Handle(Session session) {
this.session = session;
}
}
这个抽象类,就是实现了Runnable接口,然后这个类里面有一个Session对象。
在我们服务器里面,每一个处理事务的线程,都得继承这个类。
ThreadPool线程池:
package me.idashu.server;import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;/**
-
描述:线程池<br>
-
实现单例
-
@author: dashu
-
@since: 13-3-1
*/
public class ThreadPool extends ThreadPoolExecutor{private static ThreadPool threadPool;
private ThreadPool(){
super(5,10, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
}public synchronized static ThreadPool getInstance() {
if (threadPool == null) {
threadPool = new ThreadPool();
}
return threadPool;
}
}
其实就是一个实现了单例的线程池。
Session类:
package me.idashu.server;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.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;/**
- 描述:会话
- @author: dashu
- @since: 13-2-27
*/
public class Session {
/**
* 请求Key
*/
private SelectionKey key;
/**
* 通道
*/
private SocketChannel channel;
/**
* 应答类型
*/
private String mime;
/**
* 请求路径
*/
private String requestPath;
/**
* 请求方法
*/
private String method;
/**
* 请求头信息
*/
private Map<String, String> heads = new HashMap<String, String>();
public Session(SelectionKey key) throws IOException {
channel = ((ServerSocketChannel) key.channel()).accept();
channel.configureBlocking(false);
key = channel.register(key.selector(), SelectionKey.OP_READ, this);
key.selector().wakeup();
this.key = key;
}
/**
* 放入头信息
* @param key
* @param value
*/
public void head(String key, String value){
heads.put(key, value);
}
/**
* 取出头信息
* @param key
* @return
*/
public String head(String key){
return heads.get(key);
}
/**
* 关闭通道
*/
public void close(){
if (channel != null && channel.isOpen())
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public SelectionKey getKey() {
return key;
}
public void setKey(SelectionKey key) {
this.key = key;
}
public SocketChannel getChannel() {
return channel;
}
public void setChannel(SocketChannel channel) {
this.channel = channel;
}
public String getMime() {
if (mime == null) {
int index = requestPath.indexOf(".");
String m = requestPath.substring(index+1);
mime = Mime.get(m);
}
return mime;
}
public void setMime(String mime) {
this.mime = mime;
}
public String getRequestPath() {
return requestPath;
}
public void setRequestPath(String requestPath) {
this.requestPath = requestPath;
}
public Map<String, String> getHeads() {
return heads;
}
public void setHeads(Map<String, String> heads) {
this.heads = heads;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
}
每一次请求,都会生成一个session对象。然后session对象会传到handle里面。
handle里面就是我们处理事务的线程Runnable。
这样子每一次有关于请求的数据都被保存到session里面,或者从session里面读取。
AnalyseHandle类:
package me.idashu.server;import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.StringTokenizer;/**
-
描述:处理请求<br>
-
判断请求类型
-
@author: dashu
-
@since: 13-3-4
*/
public class AnalyseHandle extends Handle {public AnalyseHandle(Session session) {
super(session);
}@Override
public void run() {
SocketChannel sc = (SocketChannel) session.getKey().channel();
session.setChannel(sc);
// 请求信息
StringBuffer headSB = new StringBuffer();
ByteBuffer bb = ByteBuffer.allocate(1024);
int count = 0;try { // 读取请求头信息 while (true) { bb.clear(); count = sc.read(bb); if (count == 0) { break; } if (count < 0) { // 连接已经中断 session.close(); return; } bb.flip(); byte[] data = new byte[count]; bb.get(data, 0, count); headSB.append(new String(data)); }
// System.out.println("head:\n"+headSB.toString().trim());
// 解析请求
StringTokenizer st = new StringTokenizer(headSB.toString(), Sysconst.CRLF);
String line = st.nextToken();
// 请求方法路径等
StringTokenizer http = new StringTokenizer(line, " ");
session.setMethod(http.nextToken()); // 方法
String get = http.nextToken();
int index = get.indexOf('?');
if (index > 0) {
session.setRequestPath(get.substring(0, index)); // 路径
} else {
session.setRequestPath(get);
}
line = st.nextToken();
while (line != null && !line.equals("")) {
int colon = line.indexOf(":");
if (colon > -1) {
String key = line.substring(0, colon).toLowerCase();
String value = line.substring(colon + 1).trim();
session.head(key, value);
}
if (st.hasMoreElements()) {
line = st.nextToken();
} else {
break;
}
}
StaticHandle sh = new StaticHandle(session);
ThreadPool.getInstance().execute(sh);
} catch (IOException e) {
e.printStackTrace();
}
}
}
刚才上面的时候已经提到过,这个类继承了Handle这个类,这个类在Server里面被new出来,放到线程池里。
其实这个类很简单,就是从session获取key,然后从key里获取channel。
得到channel,就可以读取浏览器传过来的http请求信息。
再进行适当的解析,保存会session里面。
在AnalyseHandle的后面,因为我们只处理静态文件,所以就直接生成了一个StaticHandle类,扔线程池里。
StaticHandle类:
package me.idashu.server;import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;/**
-
描述:
-
@author: dashu
-
@since: 13-3-4
*/
public class StaticHandle extends Handle {/**
- 静态路径
*/
private String staticPath;
public StaticHandle(Session session) {
super(session);
}@Override
public void run() {// 获取channel SocketChannel channel = session.getChannel(); if (!channel.isOpen()) return; File file = new File(Sysconst.ROOTPATH, session.getRequestPath()); FileChannel from = null; try { from = new FileInputStream(file).getChannel(); ByteBuffer bb = ByteBuffer.allocate(1024); bb.clear(); // 返回头信息 StringBuffer sb = new StringBuffer(); sb.append("HTTP/1.1 200 OK").append(Sysconst.CRLF); sb.append("Content-Type: ").append(session.getMime()).append(Sysconst.CRLF); sb.append("Content-Length: ").append(file.length()).append(Sysconst.CRLF); sb.append(Sysconst.CRLF); System.out.println(sb.toString()); bb.put(sb.toString().getBytes()); bb.flip(); channel.write(bb); bb = from.map(FileChannel.MapMode.READ_ONLY, 0, from.size()); channel.write(bb); } catch (FileNotFoundException e) { // 404 ByteBuffer bb = ByteBuffer.allocate(1024); bb.clear(); StringBuffer sb = new StringBuffer(); sb.append("HTTP/1.1 404 Not Found").append(Sysconst.CRLF); sb.append("Content-Type: ").append("text/html").append(Sysconst.CRLF); sb.append(Sysconst.CRLF); sb.append("404"); bb.put(sb.toString().getBytes()); bb.flip(); try { channel.write(bb); } catch (IOException e1) { e1.printStackTrace(); } sb.append(Sysconst.CRLF); } catch (IOException e) { e.printStackTrace(); } finally { session.close(); }
}
public String getStaticPath() {
return staticPath;
}public void setStaticPath(String staticPath) {
this.staticPath = staticPath;
}
} - 静态路径
StaticHandle类其实就是一个处理静态文件,返回信息的类而已。
StaticHandle根据AnalyzeHandle类读取的http请求数据,找到对应的静态文件。
如果文件存在,就写回去。不存在,就返回404。
返回了信息之后,调用session.close()方法,关掉通道,整一个http请求结束。
其实这里还有几个其他的类的,Mime就是处理http返回类型的,Sysconst就是保存系统常量的。但是在这里就不贴代码了,有需要自己去Github上面去取吧。dashuserver。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于