游戏服务端核心是socket服务,而java提供了传统的bio与nio,而要满足服务端的高性能与高并发无疑要选择nio作为开发首选.
什么是nio
nio即non-blocking io,nio是基于事件驱动思想的,实现上通常采用Reactor模式.当程序发起io的读或者写操作时是非阻塞的,当socket有流可读入或者可写入时,操作系统会通知应用程序进行处理,处理完后再将流再写回缓冲区或者操作系统.对于网络io来说,主要是连接建立,流读取与流写入三种事件.需要注意的是,linux 2.6以后的版本采用epoll方式实现nio.
什么是aio
aio即async io,同样aio也是基于事件驱动思想的,但实现上通常采用Proactor模式.当程序发起io的读或者写操作时是异步的,只需要调用相应的read或者write方法.当socket有流可读入时,操作系统会将流传入read方法缓冲区,并通知应用程序,而当流有可写入时,操作系统会将流写入完毕后通知应用程序.相比nio,aio不仅简化了开发,而且省去了遍历Selector的代价.但aio只在java 7中提供,而我们目前还是先研究java 6中的nio.
核心类
- Channel
- Selector
- ByteBuffer
Channel表现了一个可以进行io操作的通道(比如通过FileChannel我们可以对文件进行读写操作),channel可处于打开或关闭状态,并且一般情况下通道对于多线程的访问是安全的.而我们在开发时,主要会用到ServerSocketChannel与SocketChannel.SocketChannel主要用于建立连接,监听事件以及读写操作,ServerSocketChannel主要用户监听端口与监听连接事件.
Selector从字面来说它其实表示一个选择器,如果从专业性上说Selector表示多路开关选择器,而一个选择器可以管理多个信道上的io操作.
在nio里,数据的读写操作都是屯缓冲区关联,Channel将数据流读入缓冲区,然后应用程序再从缓冲区访问数据.写数据时,则是将要发送的数据按顺序写入缓冲区.而这个缓冲区与数组类似,它里面的所有元素都是基本数据类型,并且此缓冲区是定长的,它不可扩展容量,并且满足0 <= 标记 <= 位置 <= 限制 <= 容量的不等式.目前我们使用最频繁的则是ByteBuffer,在mina中则是IoBuffer.
核心API用法
- Channel与Selector
- ByteBuffer
ServerSocketChannel
1 2 3 4 5 6 7 8 9 10 11 12 |
// 打开服务器套接字通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 服务器配置为非阻塞 serverSocketChannel.configureBlocking(false); // 检索与此通道关联的服务器套接字 ServerSocket serverSocket = serverSocketChannel.socket(); // 进行服务的绑定 serverSocket.bind(new InetSocketAddress(port)); // 注册感兴趣的事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); |
SocketChannel与Selector
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 打开socket通道 SocketChannel socketChannel = SocketChannel.open(); // 设置为非阻塞方式 socketChannel.configureBlocking(false); // 打开选择器 Selector selector = Selector.open(); // 注册连接服务端socket动作以及感兴趣的事件 socketChannel.register(selector, SelectionKey.OP_CONNECT); // 对于非阻塞模式立刻返回false,表示正在连接中 socketChannel.connect(SERVER_ADDRESS); // 阻塞至感兴趣的事件发生,TIME_OUT表示最长等待时间 int keys = selector.select(TIME_OUT); // 若keys大于0则有感兴趣的事件发生 if ( keys > 0 ) { Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 此处没有使用Iterator,并不代表这里for循环中没有线程安全问题,如果对集合进行操作,建议使用Iterator for ( SelectionKey key : selectionKeys ) { // 对感兴趣事件处理 ... } } |
1 2 3 4 5 6 7 8 9 10 11 |
/** * 分配空间 */ @Test public void testAllocate() { ByteBuffer byteBuffer = ByteBuffer.allocate(16); byte[] bytes = byteBuffer.array(); LOG.debug("buffer:{}", bytes); } |
输入结果如下,其实这里我们完成可以把ByteBuffer当做一个byte数组使用
1 2 3 |
buffer:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
1 2 3 4 5 6 7 8 9 10 |
/** * 缓冲区的容量 */ @Test public void testCapacity() { ByteBuffer byteBuffer = ByteBuffer.allocate(16); LOG.debug("buffer:{}, capacity:{}", byteBuffer.array(), byteBuffer.capacity()); } |
输入结果如下
1 2 3 |
buffer:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], capacity:16 |
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * 设置此缓冲区位置 */ @Test public void tesPosition() { ByteBuffer byteBuffer = ByteBuffer.allocate(16); byteBuffer.put((byte) 1); byteBuffer.put((byte) 2); LOG.debug("position1:{}, position2:{}", byteBuffer.position(), byteBuffer.position(15)); } |
输入结果如下,可以看到position方法能设置当前ByteBuffer的位置.
1 2 3 |
position1:2, position2:java.nio.HeapByteBuffer[pos=15 lim=16 cap=16] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * 缓冲区的限制 * 限制区前一位置可读 */ @Test public void teslimit() { ByteBuffer byteBuffer = ByteBuffer.allocate(16); // 填充buffer for (int i = 0; i < 16; i++) { byteBuffer.put((byte) i); } // 设置limit LOG.debug("set limit:{}, current limit:{}", byteBuffer.limit(10), byteBuffer.limit()); // 读取数据 LOG.debug("limit:{}", byteBuffer.get(9)); LOG.debug("limit:{}", byteBuffer.get(10)); } |
输入结果如下,可以看到limit方法能设置当前ByteBuffer的限制,但不能读取limit位置后面的内容.
1 2 3 4 5 6 7 8 9 |
set limit:java.nio.HeapByteBuffer[pos=10 lim=10 cap=16], current limit:10 limit:9 java.lang.IndexOutOfBoundsException at java.nio.Buffer.checkIndex(Buffer.java:512) at java.nio.HeapByteBuffer.get(HeapByteBuffer.java:121) ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * 缓冲区的标记,记录了将来可返回的位置 * 当调用reset方法时,则将position位置恢复成上mark方法后的值 */ @Test public void tesMark() { ByteBuffer byteBuffer = ByteBuffer.allocate(16); for (int i = 0; i < 16; i++) { byteBuffer.put((byte) i); if (i == 5) { byteBuffer.mark(); } } LOG.debug("position:{}", byteBuffer.position()); // 将缓冲区的位置重置为以前标记的位置 byteBuffer.reset(); LOG.debug("current position:{}", byteBuffer.position()); } |
输入结果如下,可以看到设置mark后使用reset方法能将缓冲区的位置重置为以前标记的位置.
1 2 3 4 |
position:16 current position:6 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * 清除此缓冲区,将位置设置为0,将限制设置为容量,并丢弃标记,但不会清除buffer中数据 */ @Test public void testClear() { ByteBuffer byteBuffer = ByteBuffer.allocate(16); for (int i = 0; i < 16; i++) { byteBuffer.put((byte) i); } byteBuffer.position(10); LOG.debug("position:{}", byteBuffer.position()); byteBuffer.clear(); LOG.debug("buffer:{}, data:{}", new Object[] { byteBuffer.position(), byteBuffer, byteBuffer.array() }); } |
输入结果如下,可以看到将位置设置为0,将限制设置为容量,并丢弃标记,但不会清除buffer中数据.
1 2 3 4 |
position:10 position:0, buffer:java.nio.HeapByteBuffer[pos=0 lim=16 cap=16], data:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * 反转缓冲区 */ @Test public void testFlip() throws IOException { ByteBuffer byteBuffer = ByteBuffer.allocate(16); for (int i = 0; i < 12; i++) { byteBuffer.put((byte) i); } LOG.debug("buffer:{}", byteBuffer); byteBuffer.flip(); LOG.debug("buffer:{}", byteBuffer); LOG.debug(byteBuffer.position() + "," + byteBuffer.limit()); } |
输入结果如下,可以看到flip后,首先将限制设置为当前位置,然后将position设置为0,如果已定义了标记,则丢弃该标记.
1 2 3 4 |
buffer:java.nio.HeapByteBuffer[pos=12 lim=16 cap=16] buffer:java.nio.HeapByteBuffer[pos=0 lim=12 cap=16] |