SokectChannel 特征
(1)对于已经存在的 socket 不能创建 SocketChannel
(2)SocketChannel 中提供的 open 接口创建的 Channel 并没有尽享网络级联,需要使用 conect 接口连接到指定地址
(3)未进行连接的 SocketChannel 执行 I/O 操作时,会抛出 NotYesConnectedException
(4)SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式
(5)SocketChannel 支持异步关闭。如果 SocketChannel 一个线程上 read 阻塞,另一个线程对该 SocketChannel 调用 shutdownInput. 则读阻塞的线程将返回 -1, 表示没有任何数据;如果 SocketChannel 在一个线程上 write 阻塞,另一个线程对该 SocketChannel 调用 shutdownwrite , 则写阻塞的线程将抛出
AsynchronousCloseException.
(6)SocketChannel 支持设定参数:
SO_SNDBUF 套接字发送缓冲区大小
SO_RCVBUF 套接字接受缓冲区大小
SO_KEEPALIVE 保活连接
O_REUSEADDR 复用地址
SO_LINGER 有数据传输时延迟关闭 Channel (只有在非阻塞模式下有用)
TCP_NODELAY 禁用 Nagle 算法
SocketChannel 使用
(1)创建 SokcetChannel
方式一
// 创建 socketchannel SocketChannel socketChannel = SocketChannel.open( new InetSocketAddress("www.baidu.com", 80));
方式二
SocketChannel socketChannel1 = SocketChannel.open(); socketChannel1.connect(new InetSocketAddress("www.baidu.com", 80));
直接使用有参 open api 或者使用无惨 open api , 但是在无参 open 只是创建了一个 SocketChannel 对象,并没有进行实质性的 tcp 连接
(2)链接校验
// 测试 SocketChannel 是否 open 状态 socketChannel.isOpen(); // 测试 SocketChannel 是否已经被链接 socketChannel.isConnected(); // 测试 SocketChannel 是否正在链接状态 socketChannel.isConnectionPending(); // 校验正在进行套接字连接的 SocketChannel 是否已经完成链接 socketChannel.finishConnect();
(3)读写模式
前面提到 SocketChannel 支持阻塞和非阻塞两种模式
socketChannel.configureBlocking(false);
通过以上方法设置 SocketChannel 的读写模式。 false 表示非阻塞,true 表示阻塞。
(4)读写
SocketChannel socketChannel0 = SocketChannel.open( new InetSocketAddress("www.baidu.com", 80)); ByteBuffer byteBuffer = ByteBuffer.allocate(16); socketChannel0.read(byteBuffer); socketChannel0.close(); System.out.println("read over");
以上为阻塞式读,当执行到 read 处,线程将阻塞,控制台将无法打印 “read over”.
SocketChannel socketChannel10 = SocketChannel.open( new InetSocketAddress("www.baidu.com", 80)); socketChannel10.configureBlocking(false) ByteBuffer byteBuffer1 = ByteBuffer.allocate(16); socketChannel10.read(byteBuffer1); socketChannel10.close(); System.out.println("read over");
以上为非阻塞读,控制台将打印 read over
读写都是面向缓冲区,这个读写方式与前文中的 FileChannel 相同。
(5)设置和获取参数
socketChannel5.setOption(StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE) .setOption(StandardSocketOptions.TCP_NODELAY, Boolean.TRUE);
通过 setOption 方法可以设置 socket 套接字相关的参数
socketChannel5.getOption(StandardSocketOptions.SO_KEEPALIVE); socketChannel5.getOption(StandardSocketOptions.SO_RCVBUF);
可以通过 getOption 获取相关的参数值,如果默认接受缓冲区大小式 8192 byte
SocketChannel 还支持多路复用,多路复用后面内容会详细描述。
DatagramChannel
正如 SocketChannel 对应 Socket,ServerSocketChannel 对应 ServerSocket ,每一个 DatagramChannel 对象也有一个关联的 DatagramSocket 对象。正如 SocketChannel 模拟连接导向的流协议 (如:TCP/IP),Datagram Channel 则模拟包导向的无连接协议(如:UDP/IP)。DatagramChannel 是无连接的,每个数据报(datagram) 都是一个自包含的实体,拥有它自己的目的地址以及不依赖其他数据报的数据负载。与相面流的 socket 不同, DatagramChannel 对象也可以接受来自任意地址的数据包。每个到达的数据都包含有关于它来自何处的信息(源地址)。
打开 DatagramChannel
DatagramChannel server = DatagramChannel.open(); server.socket().bind(new InetSocketAddress(25000));
这个例子是打开 25000 端口接受 UDP 数据包。
接受数据
通过 receive() 接受 UDP 包
ByteBuffer receiveBuffer = ByteBuffer.allocate(64); receiveBuffer.clear(); SocketAddress receiveAddr = server.receive(receiveBuffer);
SocketAddress 可以获取发包的 ip 、端口等信息,通过 toString 查看,格式如下:
/127.0.0.1:57126
发送数据
通过 send() 发送 UDP 包
DatagramChannel server1 = DatagramChannel.open(); ByteBuffer sendBuffer = ByteBuffer.wrap("client hello!".getBytes()); server1.send(sendBuffer, new InetSocketAddress("127.0.0.1", 25000));
连接
UDP 不存在整整意义上的连接,这里的连接是向特定地址用 read 和 write 接受发送数据包。
client.connect(new InetSocketAddress("127.0.0.1", 25000)); int readSize = client.read(sendBuffer); server.write(sendBuffer);
read() 和 write() 自由 connect() 后才能使用,不能会抛出 NotYetConnectedException 异常。用 read() 接受时, 如果没有接受到包, 会抛出 PortNnreachableException 异常。
DatagramChannel 实例
客户端发送,服务端接受的例子:
// 发送客户端 public class DatagramChannelClient { public static void main(String[] args) throws IOException, InterruptedException { DatagramChannel sendChannel = DatagramChannel.open(); InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 25000); while (true) { ByteBuffer buffer = ByteBuffer.wrap("发送 DataGram 测试数据".getBytes(StandardCharsets.UTF_8)); sendChannel.send(buffer, socketAddress); System.out.println("send success!"); TimeUnit.SECONDS.sleep(2); } } } // 接受服务端 public class DatagramChannelServer { public static void main(String[] args) throws IOException { DatagramChannel server = DatagramChannel.open(); server.socket().bind(new InetSocketAddress(25000)); ByteBuffer receiveBuffer = ByteBuffer.allocate(1024); while (true) { receiveBuffer.clear(); SocketAddress receiveAddress = server.receive(receiveBuffer); receiveBuffer.flip(); System.out.println(receiveAddress.toString()); System.out.println(StandardCharsets.UTF_8.decode(receiveBuffer)); } } }
服务端打印:
客户端打印:
连接测试代码
public class DatagramChannelClient2 { public static void main(String[] args) throws IOException, InterruptedException { DatagramChannel channel = DatagramChannel.open(); InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 25000); channel.bind(socketAddress); channel.connect(socketAddress); ByteBuffer buffer = ByteBuffer.wrap("发送 DataGram 测试数据".getBytes(StandardCharsets.UTF_8)); channel.write(buffer); System.out.println("write success!"); // buffer ByteBuffer readBuffer = ByteBuffer.allocate(1024); while (true) { readBuffer.clear(); channel.read(readBuffer); readBuffer.flip(); System.out.println(StandardCharsets.UTF_8.decode(readBuffer)); TimeUnit.SECONDS.sleep(2); } } }
数据打印
Scatter/Gather
Java NIO 开始支持 scatter/gather , scatter/gather 用于描述从 channel 中读取或者写入到 channel 操作。
分散(scatter) 从channel 中读取是指在读操作时会将读取的数据写入多个 buffer 中,因此,channel 将从 channel z中读取的数据 “分散(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 = [heander, body]; channel.read(bufferArray);
注意 buffer 首先被插入到数组,然后再将数组作为 channel.read() 的输入参数。
read() 方法按照 buffer 在数组中的顺序从 channel 中读取的数据写入到 buffer , 当一个 buffer 被写满后,channel 紧接着向另外一个 buffer 中写。
Scattering Reads 在移动下一个 buffer 之前,必须填满当前的 buffer , 这就意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息同和消息体,消息头必须完成填充(例如 128 byte),Scattering Reads 才能正常工作。
Gathing Writes
Gathering Writes 是指从多个 buffer 写入到同一个 channel。如下图描述:
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = [heander, body]; channel.write(bufferArray);
buffers 数组是 write 方法的入参,write() 方法会按照 buffer 数组中的顺序,将数据写入到 channel, 注意只有 position 和 limit 之间的数据才会被写入。因此,如果一个 buffer 的容量为 128byte, 但是仅仅包含了 58 byte 的数据将被写入到 channel 中。Scattering Reads 相反,Gathering Writes 能较好的处理动态消息。