一 Java 源生api的核心概念
1.1 Channel
Channel:通道,BIO模型中使用流来传输数据,在NIO中使用Channel来传输数据,它是双向的,一个Channel即可以读也可以写(BIO中流是单向的,所以分了InputStream和OutputStream)。
网络编程中用到的Channel只有ServerSocketChannel和SocketChannel,可以类比于ServerSocket和Socket,一个在服务端使用,一个在客户端使用。
1.2 Buffer
在BIO中,可以将数据直接写入到流当中,但是在NIO中,数据只能写入到缓冲区中,Buffer就是缓冲区。
一个Buffer其实就是一个字节数组,最常用的Buffer是ByteBuffer。
1.3 Selector
在文章Java NIO(一)select 和 epoll底层实现原理中,我们提到select/epoll函数可以同时监听多个socket,在socket准备就绪后会返回。
Java中使用Selector对象完成select/epoll函数的功能,Selector可以监控多个Channel,当某一个Channel有数据可以读取时,Selector会把它选取出来,然后交给线程处理。
二 使用NIO构建一个简单的服务器
基本的步骤如下:
- 创建一个ServerSocketChannel对象,绑定端口并配置成非阻塞模式。
- 创建一个Selector,并把第一步创建的ServerSocketChannel交给Selector监听。
- 不停的从Selector获取准备就绪的Channel,当有客户端连接时,ServerSocketChannel就会被选取出来。
- 从ServerSocketChannel获取SocketChannel对象,这个对象代表了客户端,需要使用它来与客户端读写数据。
- 把SocketChannel也交给Selector监听,当SocketChannel可以读取数据时,也会被选取出来
- SocketChannel被选取出来后,从中读取数据解析出请求,并写入返回数据。
代码如下:
public class NioServerTest {
public static void main(String[] args) throws Exception{
//创建一个ServerSocketChannel对象,绑定端口并配置成非阻塞模式。
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888), 1024);
//下面这句必需要,否则ServerSocketChannel会使用阻塞的模式,那就不是NIO了
serverSocketChannel.configureBlocking(false);
//把ServerSocketChannel交给Selector监听
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环,不断的从Selector中获取准备就绪的Channel,最开始的时候Selector只监听了一个ServerSocketChannel
//但是后续有客户端连接时,会把客户端对应的Channel也交给Selector对象
while (true) {
//这一步会阻塞,当有Channel准备就绪时或者超过1000秒后会返回。
selector.select(1000);
//获取所有的准备就绪的Channel,SelectionKey中包含中Channel信息
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
//遍历,每个Channel都可处理
for (SelectionKey selectionKey : selectionKeySet) {
//如果Channel已经无效了,则跳过(如Channel已经关闭了)
if(!selectionKey.isValid()) {
continue;
}
//判断Channel具体的就绪事件,如果是有客户端连接,则建立连接
if (selectionKey.isAcceptable()) {
acceptConnection(selectionKey, selector);
}
//如果有客户端可以读取请求了,则读取请求然后返回数据
if (selectionKey.isReadable()) {
System.out.println(readFromSelectionKey(selectionKey));
}
}
//处理完成后把返回的Set清空,如果不清空下次还会再返回这些Key,导致重复处理
selectionKeySet.clear();
}
}
//客户端建立连接的方法
private static void acceptConnection(SelectionKey selectionKey, Selector selector) throws Exception{
System.err.println("accept connection...");
//SelectionKey中包含选取出来的Channel的信息,我们可以从中获取,对于建立连接来说,只会有ServerSocketChannel可能触发,
//因此这里可以把它转成ServerSocketChannel对象
ServerSocketChannel ssc = ((ServerSocketChannel) selectionKey.channel());
//获取客户端对应的SocketChannel,也需要配置成非阻塞模式
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
//把客户端的Channel交给Selector监控,之后如果有数据可以读取时,会被select出来
socketChannel.register(selector, SelectionKey.OP_READ);
}
//从客户端读取数据的庐江
private static String readFromSelectionKey(SelectionKey selectionKey) throws Exception{
//从SelectionKey中包含选取出来的Channel的信息把Channel获取出来
SocketChannel socketChannel = ((SocketChannel) selectionKey.channel());
//读取数据到ByteBuffer中
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(byteBuffer);
//如果读到-1,说明数据已经传输完成了,可以并闭
if (len < 0) {
socketChannel.close();
selectionKey.cancel();
return "";
} else if(len == 0) { //什么都没读到
return "";
}
byteBuffer.flip();
doWrite(selectionKey, "Hello Nio");
return new String(byteBuffer.array(), 0, len);
}
private static void doWrite(SelectionKey selectionKey, String responseMessage) throws Exception{
System.err.println("Output message...");
SocketChannel socketChannel = ((SocketChannel) selectionKey.channel());
ByteBuffer byteBuffer = ByteBuffer.allocate(responseMessage.getBytes().length);
byteBuffer.put(responseMessage.getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
}
}
咋一看,有点多,其实很简单,两分钟就能看完了。另外,这只是个简单的示例代码,没有处理异常,关闭Channel和连接等操作,也没有考虑并发问题。
使用浏览器访问http://localhost:8888/,可以在控制台上看到请求头信息就说明服务器搭建成功了(浏览器中是不会看到信息的,因为我们返回的数据就不是一个符合HTTP协议的数据)

把Channel交给Selector对象的代码中,有一个参数:
socketChannel.register(selector, SelectionKey.OP_READ);
这个参数表明了关注的内容,比如ServerSocketChannel,我们关注它什么时候需要建立连接,而SocketChannel我们关注它什么时候可以读写数据。
三 构建NIO的简单客户端
创建客户端的方式与服务端类似,这里不再重复,把步骤放在代码的注释中了。
public class NioClient {
public static void main(String[] args) throws Exception{
//创建一个SocketChannel对象,配置成非阻塞模式
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
//创建一个选择器,并把SocketChannel交给selector对象
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
//发起建立连接的请求,这里会立即返回,当连接建立完成后,SocketChannel就会被选取出来
socketChannel.connect(new InetSocketAddress("localhost", 8888));
//遍历,不段的从Selector中选取出已经就绪的Channel,在这个例子中,Selector只监控了一个SocketChannel
while (true) {
selector.select(1000);
Set<SelectionKey> selectionKeySet = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeySet) {
if(!selectionKey.isValid()) {
continue;
}
//连接建立完成后的操作:直接发送请求数据
if (selectionKey.isConnectable()) {
if(socketChannel.finishConnect()) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWriteRequest(((SocketChannel) selectionKey.channel()));
}
}
//如果当前已经可以读数据了,说明服务端已经响应完了,读取数据
if (selectionKey.isReadable()) {
doRead(selectionKey);
}
}
//最后同样要清除所有的Key
selectionKeySet.removeAll(selectionKeySet);
}
}
//发送请求
private static void doWriteRequest(SocketChannel socketChannel) throws Exception{
System.err.println("start connect...");
//创建ByteBuffer对象,会放入数据
ByteBuffer byteBuffer = ByteBuffer.allocate("Hello Server!".getBytes().length);
byteBuffer.put("Hello Server!".getBytes());
byteBuffer.flip();
//写数据
socketChannel.write(byteBuffer);
if(!byteBuffer.hasRemaining()) {
System.err.println("Send request success...");
}
}
//读取服务端的响应
private static void doRead(SelectionKey selectionKey) throws Exception{
SocketChannel socketChannel = ((SocketChannel) selectionKey.channel());
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = socketChannel.read(byteBuffer);
System.out.println("Recv:" + new String(byteBuffer.array(), 0 ,len));
}
}
先启动服务端,再启动客户端,可以在客户端看到服务端返回的数据:

也可以在服务端看到客户端的请求数据:

网友评论