Java nio 建立HTTP Server

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》

猜你喜欢

转载自blog.csdn.net/qq_34911465/article/details/79251076
今日推荐