java nio入门_Java NIO 入门

最近接手了一个java项目,里面用到了nio。自己以前是做.net的,底子不足,所以花时间研究了一下。

要点简述  nio有三个最关键的概念,通道(Channel)、选择器(Selector)和缓冲器(ByteBuffer)。

通道(Channel):

这玩意说白点就是个管子,外面的数据传过来会到这个管子里,你向外发送数据的时候也得塞到这个管子里。与tcp编程相关的主要是两种通道:

1. 服务器管道(ServerSocketChannel):提供一个服务器端的监听器。

2. 普通管道(SocketChannel):提供一个一般通道,客户端和服务端互相通讯的时候会用到。

一般的客户端和服务端程序结构如下:

581f9e3e120dc45a4550a42abc5b905d.png

服务器先建立一个ServerSocketChannel监听,某个客户端创建一个SocketChannel(比如SocketChannel A)去连接服务器。服务器接收到客户端连接请求后创建一个新SocketChannel(比如SocketChannel 1),负责和客户端通讯。

ServerSocketChannel的创建比较简单,主要工作是要绑定一个端口监听客户端请求,下面的代码演示了一个简单的创建过程:

1 //创建服务通道

2 ServerSocketChannel serverChannel =ServerSocketChannel.open();3 serverChannel.configureBlocking(false);4 int port = 8888;5 InetSocketAddress address = newInetSocketAddress(port);6 serverChannel.socket().bind(address);

第三行配置了阻塞模式,这里设置成非阻塞。使用非阻塞模式时,通道进行读写或accept操作时,将立即返回,不会阻塞等待,返回值确定了调用是否成功。比如调用serverChannel的accept方法时,如果无客户端连接,则立即返回null。读写操作如果失败直接返回0,这个在SocketChannel时会用到。

SocketChannel有两种情况,服务器端是在调用serverChannel的accept方法动态创建的;客户端则要主动创建,然后调用connect方法连接到服务器。下面是个简单例子:

1 //create socket channel

2 SocketChannel clientChannel =SocketChannel.open();3 clientChannel.configureBlocking(false);4 if(!clientChannel.connect(new InetSocketAddress("127.0.0.1", 8888))) {5 while(!clientChannel.finishConnect()) {6 Thread.sleep(1000);7 }8 }

第三行和ServerSocketChannel是一样的,不废话了,关键是第4-7行。因为客户的channel被设置成非阻塞,所以connect方法调用完会立即返回,但是这时候客户端不一定已经完成了连接的创建,所以我们需要用一个循环通过调用finnishConnect方法来检测连接是否完成。

选择器(Selector):

这个可以说是nio里面最重要的一个概念,它是整个nio架子的基础。选择器帮助程序员用事件模型来处理channel间的通讯,我们可以通过轮询调用它的select方法实现一个类似windows消息循环的东西。这样既可以及时处理客户请求,又避免了在服务器端创建大量的线程。在nio之前,服务器客户端编程一般都是为每个客户端创建一个线程,这种方法会创建很多线程,在客户数比较大的时候,服务器压力会很大,线程的创建和上下文切换会吃掉很多服务器资源,服务器要么累死,要么奔溃。在nio中,如果我们用非阻塞轮询select的方法,则只要一个线程就可以同时响应多个客户端。当然如果也可以利用线程池来处理具体的数据计算,主线程只用来轮询派发事件,这样能充分利用服务器的多核或多cpu优势。由于选择器的主要作用是优化服务器性能和增加并发处理数,所以一般情况下客户端并不使用选择器。下面的代码演示了使用选择器的一般步骤:

1 //创建选择器

2 Selector selector =Selector.open();3

4 //注册相关通道

5 serverChannel.register(selector, SelectionKey.OP_ACCEPT);6

7 //轮询选择

8 while(true) {9 int keyCnt =selector.select(SELECT_TIME_OUT);10 //nothing to handle, just continue polling.

11 if(keyCnt == 0) continue;12 Iterator itr =selector.selectedKeys().iterator();13 while(itr.hasNext()) {14

15 //TODO do some thing here16

17 //remove the key

18 itr.remove();19 }20 }

第5行注册了一个服务器通道,这里也可以注册多个通道。不过一般情况下都是服务器通道accept到客户端后将动态创建的SocketChannel注册到选择器中。

第9行调用select方法选择出需要处理的key,返回的是需要处理的key的个数。参数是一个超时时间,当没有key需要处理时会等待响应时间。下面的代码则是迭代需要处理的key,然后根据key的事件类型做响应处理。需要注意的是第18行,每个key处理完需要移除,否则下次select调用仍然会选择到。

缓冲器(ByteBuffer):这个概念也很重要,其主要作用吧是在channel读写时提供缓冲。说白点就是channel读数据时将把数据塞到一个ByteBuffer里面,向channel里面写数据时也要先在一个ByteBuffer里面准备好数据,然后传递给channel。nio里面增加此类主要是提供更为底层和灵活的数据读写方式。在以前的socket编程时只能用流来读写,封装的太深,很多地方难以优化,内存也难以控制。另外流是阻塞的,这也和nio的非阻塞用法相斥。

ByteBuffer说白点就是对一个定长数组的包装。里面有几个指针控制读写的位置和范围。数组的长度是capacity,读写的当前位置是position,读写范围的结束地点为limit。position的位置会随着读写向后移动,当到达limit位置时会报出响应的异常。比如读的范围超过了limit,会报BufferUnderflowException,如果写的时候超过了limit,会报BufferOverflowException。position和limit都提供了相应方法设置和获取,使用起来相当灵活。下面的图演示了ByteBuffer的主要结构。

5587b6b1a9a5ef9f6f964496e43db6d0.png

一般情况下,我们往channel里面写入数据时,会先调用ByteBuffer的clear方法,将position设置成0,limit设置成capacity,然后开始调用ByteBuffer的各种put方法来向里面塞入数据,然后调用ByteBuffer的flip方法将position重置为0、limit设置为老的position,然后调用channel的write写入数据。

读数据也一样,直接调用ByteBuffer的clear方法清空buffer,然后调用channel的read方法读入数据,然后调用flip方法重置指针,再调用ByteBuffer的各种get方法来从buffer中提取数据。

有时候我们需要准确的读写某个范围的数据,这时候get和put方法里面可以传入读写开始位置(index)。get和put方法有各种支持index的版本。

另外一种情况是读写的都是同一个buffer,而且要同时使用,这时候读完了可以调用ByteBuffer的compact方法,这个方法将会将已经读过的区域后面的数据移动到buffer开头,这样可以腾出更多区域写。不过这个方法频繁使用效率不咋地。

还有一点需要说明,调用ByteBuffer的wrap或者allocate方法创建的buffer都在jvm堆上面,优点是对buffer操作比较快,但channel读写buffer时需要将ByteBuffer里面的数据复制到内核中的buffer或者从内核中的buffer复制到ByteBuffer,如果读写很频繁,而且数据量比较大,这种复制将很吃资源。解决方法是调用ByteBuffer的allocateDirect方法直接将ByteBuffer创建在内核里,这样channel读写效率将提高,同时节省内存,不过对ByteBuffer的put和get操作将更加耗时,因为java代码需要与内核通讯。

废话说的有点多,其实nio写代码不是很好些,毕竟是非阻塞模式。最近同时在研究netty,这个功能强大,而且用起来也简单,性能也不错,还支持WebSocket。以后有时间多研究一下。

简单远程调用的例子

下面是个简答的例子,实现了一个简单的远程调,类似java的RMI。调用和简单,客户端像服务器发送一个操作(字符串),服务器返回一个结果(字符串)。服务器也只用了单线程。如果改进的话,可以在请求返回时用json,服务器也可以创建一个线程池处理具体逻辑,select只负责轮询。

总共三个类,Consts--公用常量,RPCClient--客户端,RPCServer--服务器。

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.pngConsts.java

1 packageorg.alala.nio;2

3 public final classConsts {4

5 /**

6 * 远程传输字符串编码7 */

8 public static final String CODE = "utf-8";9

10 /**

11 * int的字节数12 */

13 public static final int INT_BYTES = Integer.SIZE / 8;14

15 /**

16 * 简单回显17 */

18 public static final String ECHO = "echo";19

20 /**

21 * 请求服务器时间22 */

23 public static final String SERVER_TIME = "server_time";24 }

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.pngRPCClient.java

1 packageorg.alala.nio;2

3 importjava.io.IOException;4 importjava.net.InetSocketAddress;5 importjava.nio.ByteBuffer;6 importjava.nio.channels.SocketChannel;7

8 public classRPCClient {9

10 private static final String CODE = "utf-8";11

12 /**

13 *@paramargs14 */

15 public static voidmain(String[] args) {16

17 try{18

19 //create socket channel

20 SocketChannel clientChannel =SocketChannel.open();21 clientChannel.configureBlocking(false);22 if(!clientChannel.connect(new InetSocketAddress("127.0.0.1", 8888))) {23 while(!clientChannel.finishConnect()) {24 Thread.sleep(1000);25 }26 }27

28 //执行远程调用

29 String result =CallService(Consts.ECHO, clientChannel);30 System.out.println("Get ECHO result:" +result);31 result =CallService(Consts.SERVER_TIME, clientChannel);32 System.out.println("Get SERVER_TIME result:" +result);33

34 //关闭通道

35 clientChannel.close();36 } catch(Exception e) {37 //TODO Auto-generated catch block

38 e.printStackTrace();39 }40 }41

42 private static String CallService(String op, SocketChannel clientChannel) throwsIOException, InterruptedException {43 ClientData data = newClientData();44 byte[] bytes =op.getBytes(CODE);45 data.buf.putInt(bytes.length);46 data.buf.put(bytes);47 data.buf.flip();48 int readSize = 0;49 int resultDataSize = 0;50 boolean writable = true;//是否处于发送请求阶段

51

52 final int TIME_OUT = 60000;//超时时间一分钟

53 long timeStart =System.currentTimeMillis();54 while( System.currentTimeMillis() - timeStart

56 //写数据

57 if(writable &&data.buf.hasRemaining()) {58 int writeCnt =clientChannel.write(data.buf);59 if(writeCnt == -1) {60 System.err.println("There are write errors, close the channel.");61 clientChannel.close();62 }63 continue;64 } else{65 writable = false;66 data.buf.clear();67 }68

69 //读数据

70 int readCnt =clientChannel.read(data.buf);71 if(readCnt == -1) {72 System.err.println("There are read errors, close the channel.");73 clientChannel.close();74 } else if(readCnt > 0) {75 readSize +=readCnt;76 if(resultDataSize == 0 && readSize >=Consts.INT_BYTES) {77 //获得结果字符串大小

78 resultDataSize = data.buf.getInt(0);79 }80 if(resultDataSize > 0 && readSize >= Consts.INT_BYTES +resultDataSize) {81 //获得结果内容

82 byte[] resultBytes = new byte[resultDataSize];83 data.buf.limit(data.buf.position());84 data.buf.position(Consts.INT_BYTES);85 data.buf.get(resultBytes);86 //返回结果

87 return newString(resultBytes, CODE);88 }89 } else if(readCnt == 0) {90 Thread.sleep(500);91 }92 }93

94 System.out.println("Call service time out, the operation is " +op);95 return "";96 }97

98 static classClientData {99 public ByteBuffer buf = ByteBuffer.allocate(1024);100 }101

102 static classClientHandler {103

104 }105

106 }

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.pngRPCServer.java

1 packageorg.alala.nio;2

3 importjava.io.IOException;4 importjava.net.InetSocketAddress;5 importjava.nio.ByteBuffer;6 importjava.nio.channels.SelectionKey;7 importjava.nio.channels.Selector;8 importjava.nio.channels.ServerSocketChannel;9 importjava.nio.channels.SocketChannel;10 importjava.util.Date;11 importjava.util.Iterator;12 importjava.util.logging.Logger;13

14 public classRPCServer {15

16 private static final int SELECT_TIME_OUT = 1000;17 private static Logger log = Logger.getLogger("TCPServer");18

19 /**

20 *@paramargs21 */

22 public static voidmain(String[] args) {23 try{24 //创建服务通道

25 ServerSocketChannel serverChannel =ServerSocketChannel.open();26 serverChannel.configureBlocking(false);27 int port = 8888;28 InetSocketAddress address = newInetSocketAddress(port);29 serverChannel.socket().bind(address);30

31 //创建选择器并注册

32 Selector selector =Selector.open();33 serverChannel.register(selector, SelectionKey.OP_ACCEPT);34

35 //创建处理器

36 ServerHandler handler = newServerHandler();37

38 log.info("Server starts polling. Port:" +port);39

40 //轮询选择

41 while(true) {42 int keyCnt =selector.select(SELECT_TIME_OUT);43 log.info("One selecting, selected keys:" +keyCnt);44 //nothing to handle, just continue polling.

45 if(keyCnt == 0) continue;46

47 Iterator itr =selector.selectedKeys().iterator();48 while(itr.hasNext()) {49 //获取下一个key

50 SelectionKey key =itr.next();51

52 try{53 //:处理其他各种情况

54 if(key.isAcceptable()) {55 handler.handleAccept(key);56 }57 if(key.isReadable()) {58 handler.handleRead(key);59 }60 if(key.isWritable()) {61 handler.handleWrite(key);62 }63 } catch(Exception exp) {64 log.warning("There are errors:" +exp.getMessage() );65 try{66 key.channel().close();67 } catch(Exception e) {68 log.warning("There are errors when closing the channel." +e.getMessage());69 }70 }71

72

73 //处理过必须移除

74 itr.remove();75 }76 }77

78 } catch(IOException e) {79 //TODO Auto-generated catch block

80 e.printStackTrace();81 }82 }83

84 /**

85 * 客户端上下文86 *@authorshun.li87 *88 */

89 static classClientContext {90 //请求操作

91 publicString op;92 //返回结果

93 publicString result;94 public ByteBuffer buf = ByteBuffer.allocate(1024) ;95 public int readSize = 0;96 public int writeSize = 0;97 public int readDataLen = 0;//读取数据大小

98 public int writeDataLen = 0;//写入数据大小

99

100 public synchronized voidclearRead() {101 buf.clear();102 readSize = 0;103 readDataLen = 0;104 }105

106 public synchronized voidclearWrite() {107 buf.clear();108 writeSize = 0;109 writeDataLen = 0;110 }111 }112

113 /**

114 * 具体的处理器115 *@authorshun.li116 *117 */

118 static classServerHandler {119

120 private Logger log = Logger.getLogger("TCPHandler");121 private static final String CODE = "utf-8";122

123 public void handleAccept(SelectionKey key) throwsIOException {124 ServerSocketChannel serverChannel =(ServerSocketChannel)key.channel();125 SocketChannel clientChannel =serverChannel.accept();126 clientChannel.configureBlocking(false);127 log.info("A client is accepted.The address:"

128 +clientChannel.socket().getInetAddress().getHostAddress());129 clientChannel.register(key.selector(), SelectionKey.OP_READ, newClientContext());130 }131

132 public void handleRead(SelectionKey key) throwsIOException {133 SocketChannel socketChannel =(SocketChannel)key.channel();134 ClientContext client =(ClientContext)key.attachment();135 int readCnt =socketChannel.read(client.buf);136 log.info("Read data. Address:"

137 +socketChannel.socket().getInetAddress().getHostAddress()138 + ".Read count:" +readCnt);139 if(readCnt == -1) {140 //there are some errors

141 log.warning("A client read fail, the channel would close.The address:"

142 +socketChannel.socket().getInetAddress().getHostAddress());143 socketChannel.close();144 } else if(readCnt > 0) {145 //update the read size

146 client.readSize +=readCnt;147

148 if(client.readSize >= Consts.INT_BYTES && client.readDataLen == 0) {149 //read the data length

150 client.readDataLen = client.buf.getInt(0);151 }152 if (client.readDataLen > 0 && client.readSize >= Consts.INT_BYTES +client.readDataLen) {153 //read the real data

154 byte[] data = new byte[client.readDataLen];155 client.buf.limit(client.buf.position());156 client.buf.position(Consts.INT_BYTES);157 client.buf.get(data);158 client.op = newString(data, CODE);159

160 //clear read status

161 client.clearRead();162 //the read is finised, server can send the request data to the client.

163 key.interestOps(SelectionKey.OP_WRITE);164 return;165 }166 if(client.readDataLen > 0 && client.readSize < Consts.INT_BYTES +client.readDataLen) {167 //reading is not finnished, continue

168 key.interestOps(SelectionKey.OP_READ);169 }170 }171 }172

173 public void handleWrite(SelectionKey key) throwsIOException {174 ClientContext client =(ClientContext)key.attachment();175 SocketChannel socketChannel =(SocketChannel)key.channel();176

177 if(client.writeSize == 0) {178 //一开始写时根据命令计算返回结果

179 String op =client.op;180 if(op.equals(Consts.ECHO)) {181 String clientName =socketChannel.socket().getInetAddress().getHostAddress();182 client.result =clientName;183 } else if(op.equals(Consts.SERVER_TIME)) {184 String serverTime = newDate().toString();185 client.result =serverTime;186 }187 //构建写入数据

188 byte[] writeData =client.result.getBytes(CODE);189 client.buf.putInt(writeData.length);190 client.buf.put(writeData);191 client.buf.flip();192 client.writeDataLen =writeData.length;193 }194

195 int writeCnt =socketChannel.write(client.buf);196 log.info("Write data. Address:"

197 +socketChannel.socket().getInetAddress().getHostAddress()198 + ".Write count:" +writeCnt);199 if(writeCnt == -1) {200 //无法写入,处理错误

201 log.warning("Write fail, would close the channel. Client address:"

202 +socketChannel.socket().getInetAddress().getHostAddress());203 socketChannel.close();204 } else{205 client.writeSize +=writeCnt;206 if(client.writeSize >=client.writeDataLen) {207 client.clearWrite();208 //写入完毕,可以重新读取客户端请求

209 key.interestOps(SelectionKey.OP_READ);210 } else{211 //没写完,继续写

212 key.interestOps(SelectionKey.OP_WRITE);213 }214 }215 }216 }217

218 }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值