Java Nio(四)Java Nio实现HTTP请求

HTTP相比于HTTPS来说要简单的多,完整代码在github上 https://github.com/cxsummer/net-nio,我先说原理。

在文章开始我先抛出一个问题。HTTP的GET和POS请求方法区别在哪呢?

答案是除了名字没区别。因为我们是按照HTTP规范来发送的数据,而 HTTP 规范并未规定说 GET 就不能发送 body 数据,GET也能在报文体负载数据,只看服务器那边是不是给解析body并暴露出来罢了,如果服务器解析来GET报文体的内容,那么GET跟POST有什么区别呢,换句话说,POST请求就不能在url上加querystring吗?当然不是。网上大部分说GET和POST区别主要是按照http大众的约定来说的。但从功能上来说给GET加上request body,给POST带上url参数,技术上是完全行的通的。

HTTP协议的四个步骤:

(1)客户端与服务器建立连接。浏览器首先向Web服务器发出建立连接请求,建立TCP连接,打开一个称为socket的虚拟文件,此文件的建立标志着连接建立成功。

(2)客户端向服务器提出请求。客户端发出数据请求包,通过socket向Web服务器提交请求,在此浏览器是将请求的对象的统一资源定位符传给服务器,HTTP的请求一般是GET或POST命令(POST用于FORM参数的传递)。GET命令的格式为:GET路径/文件名HTTP/1.0

(3)服务器接受请求,并根据请求返回相应的文件作为回应,每个服务器上都运行着一个侦听50端口的进程等待来自客户端HTTP请求。当服务器接收到请求命令后根据命令作出响应,将HTTP头和酷虎段所请求的URL数据返回给客户端。

(4)客户端与服务器关闭连接。在数据返回完成之后服务器立即发出关闭这个TCP连接的命令,客户端响应这个命令关闭连接,一次连接完成了。

因为一次请求响应就断开了连接,所以http请求是无状态无连接的,因为第二次请求来了我并不知道和上次请求是用一个客户端,那怎样才让他有状态呢,那就是session,这个比较简单,相信大家都会我就不介绍了。http请求主要就是通过socket也就是tcp传输http格式的数据,并获得响应数据,之后断开连接即可。所以说只要了解了http格式就能用socket做http客户端了。

一、URI结构

HTTP使用统一资源标识符(URI)来传输数据和建立连接。URL(统一资源定位符)是一种特殊种类的URI,包含了用于查找的资源的足够的信息,我们一般常用的就是URL,而一个完整的URL包含下面几部分:

http://www.fishbay.cn:80/mix/76.html?name=kelvin&password=123456#first

1.协议部分

URL的协议部分为http:,表示网页用的是HTTP协议,后面的//为分隔符

2.域名部分

域名是www.fishbay.cn,发送请求时,需要向DNS服务器解析IP。如果为了优化请求,可以直接用IP作为域名部分使用

3.端口部分

域名后面的80表示端口,和域名之间用:分隔,端口不是一个URL的必须的部分。如果端口是80,也可以省略不写

4.虚拟目录部分

从域名的第一个/开始到最后一个/为止,是虚拟目录的部分。其中,虚拟目录也不是URL必须的部分,本例中的虚拟目录是/mix/

5.文件名部分

从域名最后一个/开始到?为止,是文件名部分;如果没有?,则是从域名最后一个/开始到#为止,是文件名部分;如果没有?#,那么就从域名的最后一个/从开始到结束,都是文件名部分。本例中的文件名是76.html,文件名也不是一个URL的必须部分,如果没有文件名,则使用默认文件名

6.锚部分

#开始到最后,都是锚部分。本部分的锚部分是first,锚也不是一个URL必须的部分

7.参数部分

?开始到#为止之间的部分是参数部分,又称为搜索部分、查询部分。本例中的参数是name=kelvin&password=123456,如果有多个参数,各个参数之间用&作为分隔符。

当然url是有大小限制的,一般是2K,具体还是看浏览器和服务器的限制情况而定。

二、Request

HTTP的请求包括:请求行(request line)、请求头部(header)、空行 和 请求数据 四个部分组成。

Http请求消息结构

抓包的request结构如下:

GET /mix/76.html?name=kelvin&password=123456 HTTP/1.1
Host: www.fishbay.cn
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

1.请求行

GET为请求类型,/mix/76.html?name=kelvin&password=123456为要访问的资源,HTTP/1.1是协议版本

2.请求头部

从第二行起为请求头部,Host指出请求的目的地(主机域名);User-Agent是客户端的信息,它是检测浏览器类型的重要信息,由浏览器定义,并且在每个请求中自动发送。

3.空行

请求头后面必须有一个空行

4.请求数据

请求的数据也叫请求体,可以添加任意的其它数据。这个例子的请求体为空。

Response

一般情况下,服务器收到客户端的请求后,就会有一个HTTP的响应消息,HTTP响应也由4部分组成,分别是:状态行、响应头、空行 和 响应体。

http响应消息格式

抓包的数据如下:

HTTP/1.1 200 OK
Server: nginx
Date: Mon, 20 Feb 2017 09:13:59 GMT
Content-Type: text/plain;charset=UTF-8
Vary: Accept-Encoding
Cache-Control: no-store
Pragrma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache
Content-Encoding: gzip
Transfer-Encoding: chunked
Proxy-Connection: Keep-alive

{"code":200,"notice":0,"follow":0,"forward":0,"msg":0,"comment":0,"pushMsg":null,"friend":{"snsCount":0,"count":0,"celebrityCount":0},"lastPrivateMsg":null,"event":0,"newProgramCount":0,"createDJRadioCount":0,"newTheme":true}

1.状态行

状态行由协议版本号、状态码、状态消息组成

2.响应头

响应头是客户端可以使用的一些信息,如:Date(生成响应的日期)、Content-Type(MIME类型及编码格式)、Connection(默认是长连接)等等

3.空行

响应头和响应体之间必须有一个空行

4.响应体

响应正文,本例中是键值对信息

三、状态码

HTTP协议的状态码由3位数字组成,第一个数字定义了响应的类别,共有5中类别:

1.1xx: 指示信息--表示请求已接收,继续处理

2.2xx: 成功--表示请求已被成功接收、理解、接受

3.3xx: 重定向--要完成请求必须进行更进一步的操作

4.4xx: 客户端错误--请求有语法错误或请求无法实现

5.5xx: 服务器端错误--服务器未能实现合法的请求

其中,常用的状态码如下:

200 OK                        //客户端请求成功
400 Bad Request               //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized              //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用 
403 Forbidden                 //服务器收到请求,但是拒绝提供服务
404 Not Found                 //请求资源不存在,eg:输入了错误的URL
500 Internal Server Error     //服务器发生不可预期的错误
503 Server Unavailable        //服务器当前不能处理客户端的请求,一段时间后可能恢复正常

如需了解更多的状态码,请参考这个网址:HTTP状态码

四、请求方法

HTTP定义了多种请求方法,来满足各种需求。HTTP/1.0定义了三种请求方法:GETPOST 和 HEAD,到了HTTP/1.1,新增了五种请求方法:OPTIONSPUTDELETETRACE 和 CONNECT。各个请求方法的具体功能如下:

GET         请求指定的页面信息,并返回实体主体。
HEAD        类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
POST        向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
PUT         从客户端向服务器传送的数据取代指定的文档的内容。
DELETE      请求服务器删除指定的页面。
CONNECT     HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
OPTIONS     允许客户端查看服务器的性能。
TRACE       回显服务器收到的请求,主要用于测试或诊断。

五、TCP为什么三次握手和四次挥手

三次握手是有两个原因

  1. 防止重复连接,如果两次握手,那么就服务器而言,请求到了就建立连接,如果客户端第一次请求因为网络原因没有还没到达服务器,就进行重发了,那么第一次的请求在某刻到达了服务器,服务器就会建立连接等待客服端数据,发给客服端第二次握手,但是因为两次握手,接收方不能回应服务器,只能选择接受请求或者拒绝接受请求,因为是过期连接客服端就拒绝这次请求,但是服务器并不知道还在等待数据。这便导致服务器白白耗费了很多资源。如果TCP 是三次握手的话,那么客户端在接收到服务器端 SEQ+1 的消息之后,就可以判断当前的连接是否为历史连接,如果判断为历史连接的话就会发送终止报文(RST)给服务器端终止连接;如果判断当前连接不是历史连接的话就会发送指令给服务器端来建立连接。并且如果第三次握手也是因为网络原因很久之后才到服务器,那么因为服务器知道他是第三次握手,会知道他和第一次握手的时间差,如果太长的话就直接拒绝接受请求了。
  2. 保证双方都能知道对方的读写都是正常的。客户端第一次握手到服务器,服务器收到后便会知道客户端的写和服务器的读是正常的,服务器第二次握手到客户端,客户端就知道服务器的读写和客服端的读写都是正常的,第三次握手到服务器,服务器就知道客户端的读和服务器的写是正常的。

四次挥手

  • 仅仅为了是确保双方的读和写能正常关闭,确保数据的完整性,和提早关闭读写的维护以节省资源。比如,客服端发送完数据那么会第一次挥手告诉服务器我要关了写,服务器收到会发送第二次挥手告诉客服端可以关,并且我关了读,这时客户端就可以关了写。等服务器将响应完全发送完,会发送第三次挥手告诉客户端我要关了写。客户端收到会关了读,并发送第四次挥手告诉服务器收到,并且我关了读,这时服务器就可以关了写。双方读和写都关闭了,至此连接就可以断开了。

Java NIO发送HTTP请求

按照上面说的HTTP发送数据格式来说,发送HTTP请求,我们只需将数据组装成HTTP格式的字节数组,再通过socket发送过去即可。

请求方法(GET) 请求path(/mix/76.html?name=kelvin&password=123456) 协议(HTTP/1.1)\r\n
请求头key: 请求头value\r\n
请求头key: 请求头value\r\n
......
\r\n报文体

ip/域名和端口是通过socket来进行绑定和连接。废话不多说先上段代码

public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("caibaojian.com", 80));
        socketChannel.configureBlocking(false);
        StringBuilder stringBuilder = new StringBuilder("GET /t/json/ HTTP/1.1 \r\n");
        stringBuilder.append("Host: caibaojian.com\r\n");
        stringBuilder.append("\r\n");
        ByteBuffer byteBuffer = ByteBuffer.wrap(stringBuilder.toString().getBytes());
        while (byteBuffer.hasRemaining()) {
            socketChannel.write(byteBuffer);
        }
        ByteBuffer respByteBuffer = ByteBuffer.allocate(1024 * 10);
        while (socketChannel.read(respByteBuffer) > -1) {
            respByteBuffer.flip();
            byte[] b = new byte[respByteBuffer.limit()];
            respByteBuffer.get(b);
            System.out.print(new String(b));
            respByteBuffer.clear();
        }
    }

这样你会发现打印出上述响应格式的字符串

这就是一个简单的http请求了,当然这还不完整,毕竟我们是通过socketChannel.read(respByteBuffer) = -1来判断是否读完,但事实上返回-1的情况是对方主动断开或超时断开,这样需要的时间太长,有时候还会出现Connection reset by peer的报错,所以我们应该自己判断是否读完,并主动关闭连接。那么怎么判断是否读完呢,答案在响应头里面,当然我们发送报文体的时候也要加上Content-Length或者Transfer-Encoding,报文体的格式也要依此改变,这样服务端才能解析,不然服务器会认为没有报文体(不知道在哪结束)不予解析。我现在知道的有两种:

  • Content-Length。报文体的长度就是Content-Length的值即从head(第一个\r\n\r\n)后的字节数
  • Transfer-Encoding等于chunked。报文体是分段返回的,从head(第一个\r\n\r\n)后开始,每一段的开始是  当前段长度(16进制)\r\n 结束是 \r\n 中间的字节就是段内容,其字节数等于当前段长度,最后一个分段是长度值为0,读到这就代表读取完毕了。如下图所示,红框是16进制,是下面绿框的长度,一个红框和一个绿框是一个组合,直到红框是0.

解析出响应头

既然响应体依赖于响应头,那么我们先解析出响应头。由http格式来看,响应头是从第一个\r\n到第一个\r\n\r\n,那么我们改下代码,因为不知道响应头的大小,所以我们需要对数组进行动态扩容,话不多说上代码。

public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("caibaojian.com", 80));
        socketChannel.configureBlocking(false);
        StringBuilder stringBuilder = new StringBuilder("GET /t/json/ HTTP/1.1 \r\n");
        stringBuilder.append("Host: caibaojian.com\r\n");
        stringBuilder.append("\r\n");
        ByteBuffer byteBuffer = ByteBuffer.wrap(stringBuilder.toString().getBytes());
        while (byteBuffer.hasRemaining()) {
            socketChannel.write(byteBuffer);
        }
        int headerIndex = 0;
        byte[] originHeader = new byte[1024];
        LinkedHashMap<String, List<String>> head = null;
        ByteBuffer respByteBuffer = ByteBuffer.allocate(1024 * 10);
        while (socketChannel.read(respByteBuffer) > -1) {
            for (int i = 0; i < respByteBuffer.position(); i++) {
                byte b = respByteBuffer.get(i);
                originHeader[headerIndex++] = b;
                if (originHeader.length == headerIndex) {
                    originHeader = byteExpansion(originHeader, 1024);
                }
                if (originHeader[headerIndex - 1] == '\n' && originHeader[headerIndex - 2] == '\r' && originHeader[headerIndex - 3] == '\n' && originHeader[headerIndex - 4] == '\r') {
                    String headerStr = new String(originHeader);
                    String[] headerList = headerStr.split("\r\n");
                    head = Arrays.stream(headerList).skip(1).filter(h -> h.contains(":")).collect(Collectors.groupingBy(h -> h.split(":")[0].trim(), LinkedHashMap::new, Collectors.mapping(h -> h.split(":")[1].trim(), Collectors.toList())));
                    return;
                }
            }
            respByteBuffer.clear();
        }
    }

    /**
     * 扩容
     */
    public static byte[] byteExpansion(byte[] origin, int num) {
        return Optional.ofNullable(origin).map(o -> {
            byte[] temp = new byte[o.length + num];
            IntStream.range(0, o.length).forEach(i -> temp[i] = o[i]);
            return temp;
        }).orElseGet(() -> new byte[num]);
    }

这个head对象就是我们解析出来的请求头

Content-Length的报文体解析

public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("caibaojian.com", 80));
        socketChannel.configureBlocking(false);
        StringBuilder stringBuilder = new StringBuilder("GET /t/json/ HTTP/1.1 \r\n");
        stringBuilder.append("Host: caibaojian.com\r\n");
        stringBuilder.append("\r\n");
        ByteBuffer byteBuffer = ByteBuffer.wrap(stringBuilder.toString().getBytes());
        while (byteBuffer.hasRemaining()) {
            socketChannel.write(byteBuffer);
        }
        int num;
        byte[] body = null;
        int bodyIndex = 0;
        int headerIndex = 0;
        Integer contentLength = null;
        byte[] originHeader = new byte[1024];
        LinkedHashMap<String, List<String>> head = null;
        ByteBuffer respByteBuffer = ByteBuffer.allocate(1024 * 10);
        while ((num = socketChannel.read(respByteBuffer)) > -2) {
            for (int i = 0; i < respByteBuffer.position(); i++) {
                byte b = respByteBuffer.get(i);
                if (head == null) {
                    originHeader[headerIndex++] = b;
                    if (originHeader.length == headerIndex) {
                        originHeader = byteExpansion(originHeader, 1024);
                    }
                    if (originHeader[headerIndex - 1] == '\n' && originHeader[headerIndex - 2] == '\r' && originHeader[headerIndex - 3] == '\n' && originHeader[headerIndex - 4] == '\r') {
                        String headerStr = new String(originHeader);
                        String[] headerList = headerStr.split("\r\n");
                        head = Arrays.stream(headerList).skip(1).filter(h -> h.contains(":")).collect(Collectors.groupingBy(h -> h.split(":")[0].trim(), LinkedHashMap::new, Collectors.mapping(h -> h.split(":")[1].trim(), Collectors.toList())));
                        contentLength = Optional.ofNullable(head.get("Content-Length")).map(c -> Integer.parseInt(c.get(0))).orElse(-1);
                    }
                } else {
                    Integer finalContentLength = contentLength;
                    body = Optional.ofNullable(body).orElseGet(() -> new byte[finalContentLength]);
                    body[bodyIndex++] = b;
                    if (bodyIndex == contentLength) {
                        num = -2;
                        break;
                    }
                }
            }
            if (num < 0) {
                socketChannel.close();
                System.out.println(new String(body));
                return;
            }
            respByteBuffer.clear();
        }
    }

Transfer-Encoding等于chunked的报文体解析

public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.tietuku.com", 80));
        socketChannel.configureBlocking(false);
        StringBuilder stringBuilder = new StringBuilder("GET /album/1735537-2 HTTP/1.1 \r\n");
        stringBuilder.append("Host: www.tietuku.com\r\n");
        stringBuilder.append("\r\n");
        ByteBuffer byteBuffer = ByteBuffer.wrap(stringBuilder.toString().getBytes());
        while (byteBuffer.hasRemaining()) {
            socketChannel.write(byteBuffer);
        }
        int num;
        byte[] body = null;
        int bodyIndex = 0;
        int headerIndex = 0;
        Integer chunkedNum = null;
        int chunkedInitIndex = 0;
        String chunked = "";
        byte[] originHeader = new byte[1024];
        LinkedHashMap<String, List<String>> head = null;
        ByteBuffer respByteBuffer = ByteBuffer.allocate(1024 * 10);
        while ((num = socketChannel.read(respByteBuffer)) > -2) {
            for (int i = 0; i < respByteBuffer.position(); i++) {
                byte b = respByteBuffer.get(i);
                if (head == null) {
                    originHeader[headerIndex++] = b;
                    if (originHeader.length == headerIndex) {
                        originHeader = byteExpansion(originHeader, 1024);
                    }
                    if (originHeader[headerIndex - 1] == '\n' && originHeader[headerIndex - 2] == '\r' && originHeader[headerIndex - 3] == '\n' && originHeader[headerIndex - 4] == '\r') {
                        String headerStr = new String(originHeader);
                        String[] headerList = headerStr.split("\r\n");
                        head = Arrays.stream(headerList).skip(1).filter(h -> h.contains(":")).collect(Collectors.groupingBy(h -> h.split(":")[0].trim(), LinkedHashMap::new, Collectors.mapping(h -> h.split(":")[1].trim(), Collectors.toList())));
                    }
                } else {
                    if (chunked.endsWith("\r\n")) {
                        if (chunkedNum == 0) {
                            num = -2;
                            break;
                        }
                        body[bodyIndex++] = b;
                        if (bodyIndex - chunkedInitIndex == chunkedNum) {
                            chunked = "";
                        }
                    } else if (!chunked.equals("") || (b != '\r' && b != '\n')) {
                        if (b == '\r') {
                            chunkedNum = Integer.parseInt(chunked, 16);
                            body = byteExpansion(body, chunkedNum);
                            chunkedInitIndex = bodyIndex;
                        }
                        chunked = chunked + new String(new byte[]{b});
                    }
                }
            }
            if (num < 0) {
                socketChannel.close();
                System.out.println(new String(body));
                return;
            }
            respByteBuffer.clear();
        }
    }

压缩报文体解压

当响应头Content-Encoding为gzip,那么就代表响应体是压缩过的,需要解压。或者我们在请求头添加Accept-Encoding为gzip,告诉服务器我们支持解压数据,请求服务器返回压缩数据。当然压缩算法很多,这里我只是拿gzip举例因为它用的比较广泛。

解压方法:

public static byte[] uncompress(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        try {
            GZIPInputStream gzip = new GZIPInputStream(in);
            byte[] buffer = new byte[256];
            int n;
            while ((n = gzip.read(buffer)) >= 0) {
                out.write(buffer, 0, n);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return out.toByteArray();
    }

只需要在全部读完后执行上述解压方法即可,示例

if (num < 0) {
    socketChannel.close();
    System.out.println(new String(uncompress(body)));
    return;
}

由此HTTP的请求就算是写完了,但是上面说的都是非阻塞IO实现的,前面的文章也说过了非阻塞IO的弊端。所以我们要将其改为IO多路复用,真正利用java NIO的优势。

IO多路复用发送HTTP请求

io多路复用就是让一个或者少量的线程来监听多个连接,在实现上就是用Selector。注意多路复用调用业务代码是用的回调,是异步的,如果我们收发数据的时候出错(没执行回调函数之前)这时候报错对于用户来说是未知的,毕竟是异步并且没执行到回调函数,导致用户不知道这次请求的情况,所以设计的时候要注意把框架异常通知到用户。完整的代码篇幅比较长,所以在这里我只写实现原理,完整代码可去github上查看。

  1. 初始化Selector对象
  2. 使用selector.selectNow()不断的遍历到可用的连接
  3. 提交请求注册到Selector上
  4. 设置连接为不让selector监听,即selectionKey.interestOps(0)。因为具体处理方法的线程和selector的线程不是用一个,避免再次监听到此连接可用,导致重复处理此连接。
  5. 将用的连接放到线程池中处理,处理完成后或报错,关闭连接。
  6. 如果socketChannel.write返回0,并且没有写完,便不再继续循环等待,而是将连接设置为写监听,即selectionKey.interestOps(SelectionKey.OP_WRITE),结束方法,线程可以去做别的事情,等Selector判断有可写空间后再提交线程池处理,这样便避免了等待时间,读同样如此。注意,因为有可能一个请求要写/读好几次,每次都是重新提交线程池处理,所以每次都不是用一个线程,我们要记录到上一次写/读的记录,每次操作要在上一次的记录处继续写/读。
关闭socketChannel,当再次执行selector.select()时,会将此socketChannel从selector中移除,所以读取完毕后需要执行socketChannel.close()。当调用SelectionKey的cancel()方法或关闭与SelectionKey关联的Channel或与SelectionKey关联的Selector被关闭。SelectionKey对象会失效,意味着Selector再也不会监控与它相关的事件。selector.selectedKeys就是获取可用连接,处理完selector.selectedKeys()后,要把它从selectedKeys中删除,因为selector不会主动删除,如果不删除的话,下次遍历的时候它还会在。selector.keys方法是获取所以注册到Selector上的连接。

为什么不用selector.select方法

因为select方法执行的是lockAndDoSelect方法,里面是用的synchronized锁住的SelectorImpl类的publicKeys变量 那么selector.select就不能阻塞,因为一旦selector阻塞,而根据锁的可重入性,当前线程可以直接执行但是别的线程需要等待锁的释放select先执行,如果selector.select查询不到可用的连接,那么就不会释放锁,而只有向selector注册连接才会有可用连接即socketChannel.register(selector),这个方法也需要获取publicKeys对象锁,又因为是其他线程,这样就会造成死锁。 所以要selector.selectNow方法,查询到或者查不到都会释放锁,不会在获取到锁后阻塞。

这里只讲了客服端发送请求,和接受响应。其实服务端和客户端的解析和发送方式一模一样,我就不重复讲了。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Java NIO(New IO)主要用于需要高性能、高可伸缩性、高并发处理能力的场景,例如: 1. 网络编程:Java NIO提供了非阻塞式的网络通信模型,可以使用单线程处理多个并发连接,从而实现高并发网络编程。 2. 大数据处理:Java NIO可以高效地处理大文件的读写操作,通过内存映射文件的方式,避免了频繁的用户空间和内核空间之间的数据传输,提高了大数据处理的效率。 3. 实时性要求高的应用:Java NIO提供了选择器(Selector)机制,可以实现非阻塞式的I/O操作,并通过轮询方式检测I/O事件,从而及时响应并处理事件,满足实时性要求高的应用场景。 4. 多线程编程:Java NIO提供了基于缓冲区(Buffer)的操作方式,可以避免多线程操作时的线程安全问题,提高多线程编程的效率和可靠性。 5. 分布式应用:Java NIO可以通过非阻塞式的网络通信模型和多路复用技术,实现高并发的分布式应用系统,提高了系统的可伸缩性和性能。 ### 回答2: Java NIO(New I/O)是Java编程语言的一种新的I/O API,用于处理非阻塞的I/O操作。相对于传统的I/O,Java NIO提供了更高效、更灵活的方式来处理输入和输出。 Java NIO适用于许多场景,包括以下几个方面: 1. 网络编程:Java NIO可以轻松处理高并发的网络连接。使用Java NIO的Selector和Channel,可以同时管理多个网络连接,非阻塞地进行读写操作,并实现更高效的网络通信。 2. 高性能服务器:Java NIO提供了更好的性能,特别适用于需要处理大量连接和并发请求的服务器应用程序。通过使用Selector,可以同时处理多个连接,而无需为每个连接创建一个线程,从而减小了系统开销。 3. 文件操作:Java NIO提供了更快速、更灵活的文件操作方式。通过使用FileChannel,可以直接在文件中进行读写操作,而不需要通过中间缓冲区来处理数据。 4. 客户端/服务器通信:Java NIO可以用于实现高效的客户端/服务器通信。通过使用Selector,可以同时处理多个客户端连接,并且可以通过非阻塞地读写操作,提高了通信的效率。 5. 多线程任务调度:Java NIO的Selector机制可以用于多线程任务的调度。通过Selector将多个Channel注册到一个Selector上,可以更好地管理和调度多个任务的执行。 总之,Java NIO可以用在许多需要高性能和高并发的场景中,特别适合网络编程和处理大量并发连接的服务器应用程序。它提供了更高效、更灵活的方式来处理输入和输出,是现代Java应用程序开发中的重要组件。 ### 回答3: Java NIO(New I/O)是在Java 1.4版本中引入的一种替代传统Java IO的新的I/O处理方式。相比传统的Java IO,Java NIO提供了更高效、更灵活的I/O操作,适用于以下几个场景: 1. 高性能网络编程:Java NIO的非阻塞I/O模型使得可以同时处理多个客户端连接,适用于构建高性能的服务器程序,比如聊天服务器、游戏服务器、Web服务器等。 2. 大规模数据传输:Java NIO的零拷贝特性可以在数据传输过程中避免不必要的数据复制,提高传输性能。适用于大规模文件传输、内容分发网络(CDN)等场景。 3. 多路复用器:Java NIO中的Selector可以同时监控多个通道的状态,当一个或多个通道就绪时,可以及时进行相应的操作,实现高效的事件驱动编程。适用于实现高并发、高吞吐量的服务器程序。 4. 文件操作:Java NIO提供了更强大、更灵活的文件操作方式,包括读写文件、文件锁、内存映射文件等操作。适用于处理大文件、日志文件、数据库事务日志等场景。 5. 数据库操作:Java NIO可以结合数据库连接池进行高效的数据库操作,提高数据库读写性能。适用于需要频繁与数据库进行交互的应用程序。 总之,Java NIO适用于需要高效、高并发的I/O处理场景,可以帮助开发者更好地利用系统资源,提高应用程序的性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值