HTTP Server
基于HTTP 的服务器,常见的框架很多,例如说著名的Tomcat,Nginx等。是目前常见的应用的基本部分,也是C/S 架构 或者说B/S架构中重要的部分。
前面的几篇文章使用了BIO创建了HTTP Server,本文就来讲讲使用NIO来创建HTTP Server。
区别
BIO :
Java中常见的ServerSocket和Socket类,应用的十分之多了,但同时也存在一定的问题,比如多线程的问题,需要自己进行多线程管理,如果只是请求一个很小的文件却要创建一个线程就非常不值得了,尽管有线程池辅助处理,但依旧存在一定的问题。如果要在资源不足的服务器上使用,则问题多多。
NIO:
非阻塞的方式,相对而言处理上更加迅速,同时也减少了线程的数量,相对而言就提高了效率。简而言之,BIO是一个连接一个线程,NIO是一个请求一个线程。
nio基础
这里讲讲如何使用nio建立HTTP Server,在Android平台上创建局域网的服务器。
http协议的部分略过。
Buffer缓冲区
首先是缓冲区的概念,缓冲区是数据的来源或者目标,就是说在nio中,数据要么是来自于缓冲区的,要么是要放入缓冲区的。java nio中通过缓冲区和通道的协同合作,提高效率。
缓冲区中有四个概念:
容量(Capacity)
上界(Limit)
位置(Position)
标记(Mark)
对应的含义如下:
容量(Capacity)
这是缓冲区所能容纳的数据元素的最大数量,在缓冲区创建时被设定,永远不能改变。
上界(Limit)
缓冲区中最后一个数据元素的后一位。
位置(Position)
下一个要被读或者写的元素的索引。可以由get和put方法进行更新。get方法和put方法有多种形式,具体可以查查api文档。
标记(Mark)
调用mark方法可以设定标记为当前位置position,调用reset可以设置当前位置为标记的位置,标记在设定前是未定义的。
综上我们可以得出:
0 <= mark <= position <= limit <= capacity
另外在缓冲区中我们常用的还有clear方法和flip方法。使用clear方法可以清空缓冲区,将缓冲区设置为可以放入数据的状态,而如果需要读出数据,则需要调用flip方法,然后缓冲区就变成了可以读出数据的释放状态。
创建缓冲区
我们有两种方式可以创建缓冲区:
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
ByteBuffer buffer2 = ByteBuffer.wrap(bytes);// bytes是数组,这个函数还有其他形式,可以参考api文档
通道
通道就像一个导管,将字节缓冲区和通道另外一侧的实体进行连接,并有效地传输数据。另外一侧的实体通常是文件或者套接字。
在HTTP Server中,我们如下开启一个通道,并监听某个端口上的数据:
// 开启一个通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 设置非阻塞模式
ssc.configureBlocking(false);
// 获得通道的socket套接字
ServerSocket ss = ssc.socket();
// 将套接字绑定到端口
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address);
// 在选择器中进行注册
ssc.register(selector, SelectionKey.OP_ACCEPT);
选择器
选择器提供选择执行已经就绪的任务的能力,假如没有选择器,例如BIO中,我们需要遍历每一个通道并且按顺序进行检查,这种方式不很复杂,但代价却很高昂。因为这种检查不是原子性的,也就是说每个通道可能在检查之后就绪,但是直到下次被检查到为止,我们都不能确认这个通道是否就绪。因此选择器就非常必要了。
// 创建新的选择器
Selector selector = Selector.open();
// 在选择器中进行注册ssc通道
ssc.register(selector, SelectionKey.OP_ACCEPT);
// 查询选择器中就绪的通道
int num = selector.select();
建立HTTP Server
进入正题了,有些代码前面见过。
// 创建新的选择器
Selector selector = Selector.open();
// 在端口上建立监听,并注册
for (int port : ports) {
// 开启一个通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 设置非阻塞模式
ssc.configureBlocking(false);
// 获得通道的socket套接字
ServerSocket ss = ssc.socket();
// 将套接字绑定到端口
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address);
// 在选择器中进行注册
ssc.register(selector, SelectionKey.OP_ACCEPT);
}
接受连接部分
while (true) {
int num = selector.select();
if (num <= 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if ((key.readyOps() & SelectionKey.OP_ACCEPT)
== SelectionKey.OP_ACCEPT) {
// 接受新连接
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// 将新连接注册到选择器
sc.register(selector, SelectionKey.OP_READ);
it.remove();
} else if ((key.readyOps() & SelectionKey.OP_READ)
== SelectionKey.OP_READ) {
// 读取数据
try {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.clear();
sc.read(byteBuffer);
if (byteBuffer.position() > 0) {
// 线程池
ThreadPool.execute(new VueRunnable(mAssetManager, sc, byteBuffer));
}
it.remove();
}catch (Exception e){
Log.d("exception","" + e);
}
}
}
}
响应连接并返回数据:
public void response(SocketChannel socketChannel, String data,
Status status, String mimeType) throws IOException {
if (data != null) {
ByteBuffer dataBuffer = ByteBuffer.wrap(data.getBytes(mCharset));
ByteBuffer headerBuffer = ByteBuffer.wrap(responseHead(status, mimeType,
String.valueOf(dataBuffer.remaining())).getBytes(mCharset));
while (headerBuffer.hasRemaining()){
socketChannel.write(headerBuffer);
}
while (dataBuffer.hasRemaining()){
socketChannel.write(dataBuffer);
}
} else {
ByteBuffer headerBuffer = encode(responseHead(status, mimeType, "0"));
headerBuffer.flip();
socketChannel.write(headerBuffer);
}
}
public String responseHead(Status status, String mimeType, String contentLength) {
return "HTTP/1.1 " + status.getDescription() + "\r\n" +
"Server: NIO_SERVER_1.0\r\n" +
"Charset: UTF-8\r\n" +
"Content-Type: " + mimeType + "\r\n" +
"Cache-Control: no-cache\r\n" +
"Access-Control-Allow-Origin: *\r\n" +
"Content-Length: " + contentLength + "\r\n\r\n";
}
字符编码问题
字符编码是一个重要的问题,尽管底层的字符都是二进制数据,但也只有确认了字符编码才能为人所理解。
首先是创建字符类:
private Charset mCharset;
mCharset = Charset.forName("utf-8");
进行编码解码:
// 编码
private ByteBuffer encode(String str) {
return ByteBuffer.wrap(str.getBytes(mCharset));
}
// 解码
private String decode(ByteBuffer byteBuffer) {
return mCharset.decode(byteBuffer).toString();
}
有一个要注意的地方,经过测试发现如下这种写法的字符编码是错误的,不能正确的进行字符编码。测试中这种写法,要么编码后都是0,要么就是编码的结果后面多了几位字符,也是0,原因未知。
private ByteBuffer encode(String str) {
return mCharset.encode(str);
}
参考:
NIO 入门
《Java NIO》