无意间看到“优锐课”一节讲NIO的课程,老师讲得特别好,认真听完后收获颇多,继续找资料学习并写下这篇文章分享给大家。
通过这篇文章,你可以了解有关Java的创建套接字的阻塞和非阻塞替代方法的更多信息。
介绍
套接字使用TCP/IP传输协议,是两个主机之间的最后网络通信。通常不需要处理它们,因为在它们之上构建了诸如HTTP或FTP的协议。但是,了解它们的工作原理很重要。
TCP:这是一种可靠的数据传输协议,可确保发送的数据完整且正确,并且需要建立连接。
Java提供了一种阻塞和非阻塞的替代方法来创建套接字,根据你的要求,你可以考虑使用其中一种。
Java阻塞IO
Java阻塞IO API包含在JDK中的java.net包下,通常最简单地使用。
该API基于字节流和可读取或写入的字符流的流。没有索引可用于来回移动,就像在数组中一样,它只是连续的数据流。
客户端每次请求与服务器的连接时,都会阻塞线程。因此,如果我们希望同时进行许多连接,则必须创建足够大的线程池。
ServerSocket serverSocket = new ServerSocket(PORT_NUMBER);
while (true) {
Socket client = serverSocket.accept();
try {
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream())
);
OutputStream out = client.getOutputStream();
in.lines().forEach(line -> {
try {
out.write(line.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
});
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
- 使用给定的端口创建一个ServerSocket进行监听。
- 服务器将在调用accept( )时阻塞,并开始侦听客户端连接。
- 如果客户端请求连接,则accept( )返回一个Socket。
- 现在,我们可以从客户端(InputStream)读取数据并将数据发送回客户端(OutputStream)。
如果要允许多个连接,则必须创建一个线程池:
ExecutorService threadPool = Executors.newFixedThreadPool(100);
threadPool.execute(() -> {
// SOCKET CREATION
});
如你所见,此API有一些限制。我们将无法接受比计算机中可用线程更多的连接。因此,如果你希望有许多连接,则需要替代方法。
Java NIO
Java.nio 是用于套接字连接的非阻塞API,这意味着你对可用线程数的要求并不严格。使用此库,一个线程可以一次处理多个连接。
主要内容:
• 通道(Channel): 通道是输入和输出流的组合,因此它们允许你进行读取和写入,并且它们使用缓冲区来执行这些操作。
• 缓冲区(Buffer): 它是用于从通道读取并写入通道的内存块。 当你想从缓冲区读取数据时,需要调用flip(),以便将pos设置为0。
int read = socketChannel.read(buffer); // pos = n & lim = 1024
while (read != -1) {
buffer.flip(); // set buffer in read mode - pos = 0 & lim = n
while(buffer.hasRemaining()){
System.out.print((char) buffer.get()); // read 1 byte at a time
}
buffer.clear(); // make buffer ready for writing - pos = 0 & lim = 1024
read = socketChannel.read(buffer); // set to -1
}
- 在第1行,pos将等于写入缓冲区的字节数。
- 在第3行,调用flip( )将position设置为0,并限制为先前写入的字节数。
- 在第5行,它一次从Buffer读取一个字节,直到达到限制。
- 最后,在第7行,清除缓冲区。
• 选择器(Selector): 选择器可以注册多个通道,并将检查哪些通道已准备好接受新连接。与阻塞IO的accept()方法类似,当调用select()时,它将阻塞应用程序,直到Channel准备好进行操作为止。由于选择器可以注册多个通道,因此只需要一个线程即可处理多个连接。
• 选择键(Selection Key): 它包含特定频道的属性(兴趣集,就绪集,选择器/频道和可选的附加对象)。选择键主要用于了解通道的当前兴趣(isAcceptable(),isReadable(),isWritable()),获取通道并对该通道进行操作。
例如
我们将使用 Echo Socket Channel 服务器来显示NIO的工作方式。
var serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
var selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
var keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
var selectionKey = (SelectionKey) keys.next();
if (selectionKey.isAcceptable()) {
createChannel(serverSocketChannel, selectionKey);
} else if (selectionKey.isReadable()) {
doRead(selectionKey);
} else if (selectionKey.isWritable()) {
doWrite(selectionKey);
}
keys.remove();
}
}
- 从第1行到第3行,创建一个ServerSocketChannel,你必须将其显式设置为非阻塞模式。套接字还配置为侦听端口8080。
- 在第5和第6行上,将创建一个Selector,并在Selector上注册ServerSocketChannel,其中的SelectionKey指向ACCEPT操作。
- 为了使应用程序始终保持侦听状态,阻塞方法select()位于无限while循环内,并且至少在选择了一个通道时,将调用select()调用wakeup()或中断线程。
- 然后,在第10行上,从选择器返回一组键,我们将迭代它们以执行就绪通道。
private static void createChannel(ServerSocketChannel serverSocketChannel, SelectionKey selectionKey) throws IOException {
var socketChannel = serverSocketChannel.accept();
LOGGER.info("Accepted connection from " + socketChannel);
socketChannel.configureBlocking(false);
socketChannel.write(ByteBuffer.wrap(("Welcome: " + socketChannel.getRemoteAddress() +
"\nThe thread assigned to you is: " + Thread.currentThread().getId() + "\n").getBytes()));
dataMap.put(socketChannel, new LinkedList<>()); // store socket connection
LOGGER.info("Total clients connected: " + dataMap.size());
socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ); // selector pointing to READ operation
}
- 每次创建新连接时,isAcceptable()将为true,并且新通道将被注册到选择器中。
- 为了跟踪每个通道的数据,将其放入Map中,以套接字通道为键,并列出ByteBuffer。
- 然后,选择器将指向READ操作。
private static void doRead(SelectionKey selectionKey) throws IOException {
LOGGER.info("Reading...");
var socketChannel = (SocketChannel) selectionKey.channel();
var byteBuffer = ByteBuffer.allocate(1024); // pos=0 & lim=1024
int read = socketChannel.read(byteBuffer); // pos=numberOfBytes & lim=1024
if (read == -1) { // if connection is closed by the client
doClose(socketChannel);
} else {
byteBuffer.flip(); // put buffer in read mode by setting pos=0 and lim=numberOfBytes
dataMap.get(socketChannel).add(byteBuffer); // find socket channel and add new byteBuffer queue
selectionKey.interestOps(SelectionKey.OP_WRITE); // set mode to WRITE to send data
}
}
- 在读取块中,将检索通道并将传入的数据写入ByteBuffer。
- 在第6行,我们检查连接是否已关闭。
- 在第9行和第10行,使用flip()将缓冲区设置为读取模式,并将其添加到Map中。
- 然后,调用interestOps()指向WRITE操作。
private static void doWrite(SelectionKey selectionKey) throws IOException {
LOGGER.info("Writing...");
var socketChannel = (SocketChannel) selectionKey.channel();
var pendingData = dataMap.get(socketChannel); // find channel
while (!pendingData.isEmpty()) { // start sending to client from queue
var buf = pendingData.poll();
socketChannel.write(buf);
}
selectionKey.interestOps(SelectionKey.OP_READ); // change the key to READ
}
- 再次检索通道,以便将保存在地图中的数据写入其中。
- 然后,将选择器设置为READ操作。
private static void doClose(SocketChannel socketChannel) throws IOException {
dataMap.remove(socketChannel);
var socket = socketChannel.socket();
var remoteSocketAddress = socket.getRemoteSocketAddress();
LOGGER.info("Connection closed by client: " + remoteSocketAddress);
socketChannel.close(); // closes channel and cancels selection key
}
- 如果连接已关闭,则将通道从Map中移除,然后我们关闭该通道。
Java IO 与NIO
在IO和NIO之间进行选择将取决于用例。对于较少的连接和简单的解决方案,IO可能更适合你。而如果你想要更高效的东西同时处理数千个连接,则NIO可能是更好的选择,但请记住,这会带来很多代码复杂性。但是,有一些像Netty或Apache MINA这样的框架是在NIO之上构建的,隐藏了编程的复杂性。
感谢你的阅读!
感谢优锐课老师的讲解!
有什么问题可以加下Java学习资料交流qq群:907135806进群探讨。也可以添加vx:ddmsiqi,有更多JVM、Mysql、Tomcat、Spring Boot、Spring Cloud、Zookeeper、Kafka、RabbitMQ、RockerMQ、Redis、ELK、Git等Java学习资料和视频课程干货!