SocketChannel
Java NIO中的SocketChannel
是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel
:
- 打开一个
SocketChannel
并连接到互联网上的某台服务器。 - 一个新连接到达
ServerSocketChannel
时,会创建一个SocketChannel
。打开 SocketChannel
下面是SocketChannel
的打开方式:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
关闭 SocketChannel
当用完SocketChannel
之后调用SocketChannel.close()
关闭SocketChannel
:
socketChannel.close();
从 SocketChannel 读取数据
要从SocketChannel
中读取数据,调用一个read()
的方法之一。以下是例子:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
首先,分配一个Buffer
。从SocketChannel
读取到的数据将会放到这个Buffer
中。
然后,调用SocketChannel.read()
。该方法将数据从SocketChannel
读到Buffer
中。read()
方法返回的int
值表示读了多少字节进Buffer
里。如果返回的是-1,表示已经读到了流的末尾(连接关闭了)。
写入 SocketChannel
写数据到SocketChannel
用的是SocketChannel.write()
方法,该方法以一个Buffer
作为参数。示例如下:
String newData = "New String to write to file..." + System.currentTimeMillis();
//生成Buffer,并向Buffer中写数据
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
//切换buffer为读模式
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
注意SocketChannel.write()
方法的调用是在一个while
循环中的。write()
方法无法保证能写多少字节到SocketChannel
。所以,我们重复调用write()
直到Buffer
没有要写的字节为止。
非阻塞模式
可以设置 SocketChannel
为非阻塞模式(non-blocking mode
).设置之后,就可以在异步模式下调用connect()
, read()
和write()
了。
connect()
如果SocketChannel
在非阻塞模式下,此时调用connect()
,该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()
的方法。像这样:
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
while(! socketChannel.finishConnect() ){
//wait, or do something else...
}
write()
非阻塞模式下,write()
方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()
。前面已经有例子了,这里就不赘述了。
read()
非阻塞模式下,read()
方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int
返回值,它会告诉你读取了多少字节。
非阻塞模式与选择器
非阻塞模式与选择器搭配会工作的更好,通过将一或多个SocketChannel
注册到Selector
,可以询问选择器哪个通道已经准备好了读取,写入等。Selector
与SocketChannel
的搭配使用会在后面详讲。
ServerSockerChannel
Java NIO中的 ServerSocketChannel
是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket
一样。ServerSocketChannel
类在 java.nio.channels
包中。
这里有个例子:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//使用socketChannel做一些工作...
}
打开 ServerSocketChannel
通过调用 ServerSocketChannel.open()
方法来打开ServerSocketChannel
.如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
关闭 ServerSocketChannel
通过调用ServerSocketChannel.close()
方法来关闭ServerSocketChannel
. 如:
serverSocketChannel.close();
监听新进来的连接
通过 ServerSocketChannel.accept()
方法监听新进来的连接。当 accept()
方法返回的时候,它返回一个包含新进来的连接的 SocketChannel
。因此, accept()
方法会一直阻塞到有新连接到达。
通常不会仅仅只监听一个连接,在while
循环中调用 accept()
方法. 如下面的例子:
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//使用socketChannel做一些工作...
}
当然,也可以在while
循环中使用除了true
以外的其它退出准则。
非阻塞模式
ServerSocketChannel
可以设置成非阻塞模式。在非阻塞模式下,accept()
方法会立刻返回,如果还没有新进来的连接,返回的将是null
。 因此,需要检查返回的SocketChannel
是否是null
.如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
if(socketChannel != null){
//使用socketChannel做一些工作...
}
}
Java NIO中的DatagramChannel
是一个能收发UDP包的通道。因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包。
示例
//客户端
@Test
void client() throws IOException {
//1.打开SocketChannel
SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
channel.configureBlocking(false);//设置为非阻塞通道
//2.创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//3.读取键盘输入 并发送数据
Scanner scanner = new Scanner(System.in);
System.out.println("请输入信息:");
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
//写入缓冲区
buffer.put((LocalDateTime.now() + " " + line).getBytes());
buffer.flip();
//将缓冲区放入通道
channel.write(buffer);
buffer.clear();
}
//4.关闭通道
channel.close();
}
//服务端
@Test
void server() throws IOException {
//1.打开ServerSocketChannel 并绑定端口 获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);//切换到非阻塞模式
serverSocketChannel.bind(new InetSocketAddress(9999));
//2.获取选择器
Selector selector = Selector.open();
//3.将通道注册到选择器上 指定监听"准备就绪"事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//4.循环获取选择器上"准备就绪"的事件
while (selector.select() > 0) {
//5.获取当前选择器上所有注册的"选择键(监听准备就绪事件)"
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
System.out.println("接收就绪");
//6.接收就绪后,获取客户端的连接
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
socketChannel.configureBlocking(false)//切换为非阻塞模式
.register(selector, SelectionKey.OP_READ);//注册到选择器上
}
} else if (key.isConnectable()) {
System.out.println("连接就绪");
} else if (key.isReadable()) {
System.out.println("读就绪");
//7.获取选择器上读就绪的通道
SocketChannel socketChannel = (SocketChannel) key.channel();
//8.创建缓冲区 读数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
buffer.flip();
System.out.println("服务端收到: " + new String(buffer.array()));
buffer.clear();
} else if (key.isWritable()) {
System.out.println("写就绪");
} else {
//取消选择键
iterator.remove();
}
}
}
}
DatagramChannel
打开 DatagramChannel
下面是 DatagramChannel
的打开方式:
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
这个例子打开的 DatagramChannel
可以在UDP端口9999上接收数据包。
接收数据
通过receive()
方法从DatagramChannel
接收数据,如:
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
receive()
方法会将接收到的数据包内容复制到指定的Buffer
. 如果Buffer
容不下收到的数据,多出的数据将被丢弃。
发送数据
通过send()
方法从DatagramChannel
发送数据,如:
String newData = "New String to write to file..."
+ System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
这个例子发送一串字符到”jenkov.com”服务器的UDP端口80。 因为服务端并没有监控这个端口,所以什么也不会发生。也不会通知你发出的数据包是否已收到,因为UDP在数据传送方面没有任何保证。
连接到特定的地址
可以将DatagramChannel
“连接”到网络中的特定地址的。由于UDP是无连接的,连接到特定地址并不会像TCP通道那样创建一个真正的连接。而是锁住DatagramChannel
,让其只能从特定地址收发数据。
这里有个例子:
channel.connect(new InetSocketAddress("jenkov.com", 80));
当连接后,也可以使用read()
和write()
方法,就像在用传统的通道一样。只是在数据传送方面没有任何保证。这里有几个例子:
int bytesRead = channel.read(buf);
int bytesWritten = channel.write(buf);
示例
//udp协议
@Test
public void send() throws IOException {
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
System.out.println("请输入信息:");
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
buffer.put((LocalDateTime.now() + " " + line).getBytes());
buffer.flip();
channel.send(buffer, new InetSocketAddress("127.0.0.1", 9898));
buffer.clear();
}
channel.close();
}
@Test
public void receive() throws IOException {
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);
while (selector.select() > 0) {
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey sk = it.next();
if (sk.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.receive(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
}
}
it.remove();
}
}