在前面的一篇文章nio演变历程的简单介绍,介绍了bio到nio的发展过程,那么本文就详细介绍下在单线程模式下如何编写一个简单的网络编程,主要步骤如下
- 服务端开启端口监听,并注册到selector中,告诉操作系统,需要监听客户端的连接请求事件
- selector进行监控,当监控的channel上应用程序感兴趣的事件处于ready状态时,便告知应用程序
- 应用程序通过selector拿到这些处于ready状态的channel
- 循环处理每个channel,根据channel上的不同事件类型做不同的逻辑处理,一般有accept、read、write事件
- 处理完channel后,需要将其移除,避免下次循环重复处理
public static void main(String[] args) throws Exception {
//创建多路复用器,用于监听channel上事件的发生
Selector selector = Selector.open();
//服务器开启端口监听,
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8899));
serverSocketChannel.configureBlocking(false);
//服务器监听的channel注册到selector,SelectionKey表示channel与selector的一个上下文关系
SelectionKey selectionKey = serverSocketChannel.register(selector, 0);
//告知selector,应用程序感兴趣的事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
for (;;) {
//selector进行监听,当有channel上感兴趣的事件发生时,此方法便会返回,返回值为channel数量
int select = selector.select();
if (select == 0) {
continue;
}
//感兴趣事件处于ready状态的selectionKey集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
handleAccept(key);
} else if (key.isReadable()) {
handleRead(key);
} else if (key.isWritable()) {
handleWrite(key);
}
//移除,避免下次循环重复处理
iterator.remove();
}
}
}
服务器端的编程模式一般都是这个套路,只需要分别实现连接、读、写三种类型的事件处理逻辑即可。
处理连接事件时,通常是接受连接,并将channel注册到selector,监听读事件
private static void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(key.selector(), 0);
scKey.interestOps(SelectionKey.OP_READ);
}
处理读事件时,需要注意到三个方面的问题,第一个是连接被迫断开,此时读事件会抛出异常,第二个是客户端主动关闭连接,最后是拆包与粘包问题,这个后面再专门来介绍下
private static void handleRead(SelectionKey key) {
SocketChannel sc = (SocketChannel) key.channel();
try {
ByteBuffer buffer = ByteBuffer.allocate(100);
int readCnt = sc.read(buffer);
if (readCnt == -1) {
key.cancel();
} else if (readCnt > 0) {
buffer.flip();
String string = Charset.defaultCharset().decode(buffer).toString();
System.out.println("收到客户端的请求数据 " + string);
}
//读取完数据后,返回数据给客户端
String writeContent = "helloworldhelloworldhelloworldhelloworldhelloworldhelloworldhelloworld";
ByteBuffer writeBuffer = ByteBuffer.wrap(writeContent.getBytes());
sc.write(writeBuffer);
if (writeBuffer.hasRemaining()) {
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
key.attach(writeBuffer);
}
} catch (Exception ex) {
ex.printStackTrace();
//取消监控
key.cancel();
}
}
处理写事件,通常在连接对应的发送缓冲区有剩余空间可写入时,多路复用器便会触发写事件,因此在应用程序中,若是写完数据了,需要及时将写事件取消,不然每次select,都会返回channel,但实际上又没数据可写。
private static void handleWrite(SelectionKey key) throws IOException {
SocketChannel sc = (SocketChannel) key.channel();
Object attachment = key.attachment();
if (attachment != null) {
ByteBuffer buffer = (ByteBuffer) attachment;
int write = sc.write(buffer);
//没数据可写了,取消写事件的监控,不然每次还会返回
if (!buffer.hasRemaining()) {
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}
}
}
网友评论