SocketChannel
(1)新的Socket通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。再也没有必要为每个socket连接使用一个线程,也避免了管理大量线程所需的上下文切换的开销。借助新的NIO类,一个或几个线程就可以管理成百上千的活动socket连接了,并且只有很少甚至可能没有性能损失。所有的socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都继承了位于java.nio.channels.spi包中的AbstractSelectableChannel。这意味着我们可以用一个Selector对象来执行socket通道的就绪选择。
(2)请注意DatagramChannel和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。
(3)socket和socket通道之间的关系。通道是一个连接I/O服务导管并提供与该服务交互的方法。就某个socket而言,它不会再次实现与之对应的socket通道类中的socket协议API,而java.net中已经存在的socket通道都可以被大多数协议操作重复使用。
全部socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对等socket对象。这些我们所熟悉的来自java.net的类(Socket、ServerSocket和DatagramSocket),它们已经被更新以识别通道。对等socket可以通过调用socket()方法从一个通道上获取。此外,这三个java.net类现在都有getChannel方法。
(4)要把一个socket通道置于非阻塞模式,我们要依赖所有socket通道类的公有超级类:SelectableChannel。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞I/O和可选择性是紧密相连的,那也正是管理阻塞模式的API代码要在SelectableChannel超级类中定义的原因。
设置或重新设置一个通道的阻塞模式是简单的,只要调用configureBlocking()方法即可,传递参数值为true则设为阻塞模式,参数值为false值设为非阻塞模式。可以通过调用isBlocking()方法来判断某个socket通道当前处于那种模式。
非阻塞模式socket通常被认为是服务端模式使用的,因为它们使同时管理很多socket通道变得非常容易。但是,在客户端使用一个或几个非阻塞模式的socket通道也是有益处的,例如,借助非阻塞socket通道,GUI程序可以专注于用户请求并且同时维护与一个或多个服务器的会话。在很多程序上,非阻塞模式都是有用的。
偶尔地,我们也需要防止socket通道的阻塞模式被更改。API中有一个blockingLock()方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式。
ServerSocketChannel
ServerSocketChannel是一个基于通道的socket监听器。它同我们熟悉的java.net.ServerSocket执行相同的任务,不过它增加了通道的语义,因此能够在非阻塞模式下运行。
由于ServerSocketChannel没有bind()方法,因此有必要取出对等的socket并使用它来绑定到一个端口以开始监听连接。我们也是使用对等ServerSocket的API来根据需要设置其他的socket选项。
同java.net.ServerSocket一样,ServerSocketChannel也有accept()方法。一旦创建了一个ServerSocketChannel并用对等socket绑定了它,然后您就可以在其中一个上调用accept()。如果您选择在ServerSocket上调用accept()方法,那么它会同任何其他的ServerSocket表现一样的行为:总是阻塞并返回一个java.net.Socket对象。如果您选择在ServerSocketChannel上调用accept()方法则会返回SocketChannel类型的对象,返回的对象能够在非阻塞模式下运行。
换句话说,ServerSocketChannel的accept()方法会返回SocketChannel类型对象,SocketChannel可以在非阻塞模式下运行。
其他Socket的accept()方法会阻塞返回一个socket对象。如果ServerSocketChannel以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept()会立即返回null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册ServerSocketChannel对象以实现新连接到达时自动通知的功能。
以下代码演示了如何使用一个非阻塞的accept()方法:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class FileChannelAccept {
public static void main(String[] argv) throws IOException, InterruptedException {
int port = 8888;
ByteBuffer buf = ByteBuffer.wrap("Hello java nio.".getBytes());
ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket socket = ssc.socket();
socket.bind(new InetSocketAddress(port));
//设置非阻塞模式
ssc.configureBlocking(false);
while (true) {
System.out.println("waiting for connections");
SocketChannel sc = ssc.accept();
if (sc == null) { //没有连接传入
System.out.println("null");
Thread.sleep(2000);
} else {
System.out.println("Incoming connection from: "+sc.socket().getRemoteSocketAddress());
buf.rewind(); //指针0
sc.write(buf);
sc.close();
}
}
}
}
SocketChannel
SocketChannel介绍
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。
SocketChannel是一种面向流连接sockets套接字的可选择通道。从这里可以看出:
- SocketChannel是用来连接Socket套接字。
- SocketChannel主要用途用来处理网络I/O的通道。
- SocketChannel是基于TCP连接传输。
- SocketChannel实现了可选择通道,可以被多路复用的。
SocketChannel特征:
(1)对于已经存在的socket不能创建SocketChannel。
(2)SocketChannel中提供的open接口创建的Channel并没有网络连接,需要使用connect接口连接到指定地址。
(3)未进行连接的SocketChannel执行I/O操作时,会抛出NotYetConnectedException。
(4)SocketChannel支持两种I/O模式:阻塞式和非阻塞式。
(5)SocketChannel支持异步关闭。如果SocketChannel在一个线程上read阻塞,另一个线程对该SocketChannel调用shutdownInput,则读阻塞的线程将返回-1表示没有读取任何数据;如果SocketChannel在一个线程上write阻塞,另一个线程对该SocketChannel调用shutdownWrite,则写阻塞的线程将抛出AsynchronousCloseException。
(6)SocketChannel支持设定参数:
- SO_SNDBUF 套接字发送缓冲区大小
- SO_RCVBUF 套接字接收缓冲区大小
- SO_KEEPALIVE 保活链接
- SO_REUSEADDR 复用地址
- SO_LINGER 有数据传输时延缓关闭Channel(只有在非阻塞模式下有用)
- TCP_NODELAY 禁用Nagle算法
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class SocketChannelDemo {
public static void main(String[] args) throws IOException {
//创建SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com",80));
// SocketChannel socketChannel = SocketChannel.open();
// socketChannel.connect(new InetSocketAddress("www.baidu.com",80));
//连接校验
socketChannel.isOpen(); // 测试SocketChannel是否为open状态
socketChannel.isConnected(); //测试SocketChannel是否已经被连接
socketChannel.isConnectionPending(); //测试SocketChannel是否正在进行连接
socketChannel.finishConnect(); //校验正在进行套接字连接的SocketChannel是否已经完成连接
//读写模式
socketChannel.configureBlocking(false);
//设置和获取参数
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE,Boolean.TRUE);
//读写
ByteBuffer buf = ByteBuffer.allocate(48);
socketChannel.read(buf);
socketChannel.close();
System.out.println("read over!");
}
}
DatagramChannel
正如SocketChannel对应Socket,ServerSocketChannel对应ServerSocket,每一个DatagramChannel对象也有一个关联的DatagramSocket对象。正如SocketChannel模拟连接导向的流协议(如TCP/IP),DatagramChannel则模拟包导向的无连接协议(如UDP/IP)。DatagramChannel是无连接的,每个数据报(datagram)都是一个自包含的实体,拥有它自己的地址及不依赖其他数据报的数据负载。与面向流的socket不同,DatagramChannel可以发送单独的数据报给不同的目的地址。同样,DatagramChannel对象也可以接收来自任意地址的数据包。每个到达的数据报都包含有关于它来自何处的信息(源地址)。
基本使用:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
public class DatagramChannelDemo {
public static void main(String[] args) throws IOException {
//打开DatagramChannel
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.socket().bind(new InetSocketAddress(8888));
//接收数据
ByteBuffer buf = ByteBuffer.allocate(64);
buf.clear();
SocketAddress socketAddress = datagramChannel.receive(buf);
//发送数据
ByteBuffer buffer = ByteBuffer.wrap("data...".getBytes());
datagramChannel.send(buffer,new InetSocketAddress("127.0.0.1",8888));
//连接
datagramChannel.connect(new InetSocketAddress("127.0.0.1",8888));
int readSize = datagramChannel.read(buffer);
datagramChannel.write(buffer);
}
}
示例:
发送端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.StandardCharsets;
public class DatagramChannelDemo1 {
//发送
public static void main(String[] args) throws IOException, InterruptedException {
//打开DatagramChannel
DatagramChannel datagramChannel = DatagramChannel.open();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
//发送
while (true) {
ByteBuffer buffer = ByteBuffer.wrap("send data".getBytes(StandardCharsets.UTF_8));
datagramChannel.send(buffer,address);
System.out.println("finished!");
Thread.sleep(1000);
}
}
}
接收端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.Charset;
public class DatagramChannelDemo2 {
//接收
public static void main(String[] args) throws IOException {
//打开
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.bind(new InetSocketAddress("127.0.0.1",8888));
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
SocketAddress receive = datagramChannel.receive(buffer);
buffer.flip();
System.out.println(receive.toString());
System.out.println(Charset.forName("UTF-8").decode(buffer));
}
}
}
Scatter/Gather
Java NIO开始支持scatter/gather,scatter/gather用于描述从Channel中读取或者写入到Channel的操作。
分散(scatter):从Channel中读取是指读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散scatter”到多个buffer中。
聚集(gather):写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel将多个Buffer中的数据“聚集(gather)”后发送到Channel。
scatter/gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。
Scattering Reads
Scattering Reads是指数据从一个channel读取到多个buffer中。如图:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header,body};
channel.read(bufferArray);
注意buffer首先被插入到数组,然后再将数组作为channel.read()的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧跟着向另一个buffer中写。
Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充。Scattering Reads才能正常工作。
Gathering Writes
Gathering Writes是指数据从多个buffer写入到同一个channel。如图:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header,body};
channel.write(bufferArray);
buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与scattering reads相反,gathering writes能较好的处理动态消息。