Java NIO 实现服务端和客户端的通信示例
温馨提示:阅读本示例前首先需要对 Java NIO 的三大核心有一定了解
- channel (通道
- buffer (缓冲区
- selector(选择器
可以先看看 Java NIO Tutorial
服务端
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
public class NIOServer {
static final Logger LOGGER = LoggerFactory.getLogger(NIOServer.class);
public static void main(String[] args) throws IOException {
int port = 9090;
try {
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//设置为非阻塞才能注册到选择器。FileChannel不能切换到非阻塞模式
ssChannel.configureBlocking(false);
//2. 绑定端口
ssChannel.bind(new InetSocketAddress(port));
//4. 将通道注册到选择器上
Selector selector = Selector.open();
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
LOGGER.info("NIO服务已启动,绑定端口号为:" + port);
//5. 通过轮询的方式,获取准备就绪的事件
while (selector.select() > 0) { //该方法会一直阻塞,直到至少有一个 SelectionKey 准备就绪
//6. 获取当前选择器中所有注册的 SelectionKey
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
handleAccept(key);
} else if (key.isReadable()) {
handleRead(key);
}
//当SelectionKey 任务完成后需要移除,否则会一直执行这个key。
iterator.remove();
}
}
ssChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
static void handleAccept(SelectionKey key) {
try {
LOGGER.info("接收就绪!");
//7. 获取接受状态准备就绪的 selectionKey
//调用accept 方法获取通道
SocketChannel sChannel = ((ServerSocketChannel) key.channel()).accept();
Selector selector = key.selector();
//8. 将 sChannel 设置为非阻塞的
sChannel.configureBlocking(false);
//9. 将该通道注册到选择器上,让选择器能够监听这个通道
sChannel.register(selector, SelectionKey.OP_READ);
LOGGER.info("当前接收的连接: " + sChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
static void handleRead(SelectionKey key) throws IOException {
LOGGER.info("读取就绪!");
//10. 获取 读状态 准备就绪的 selectionKey
SocketChannel sChannel = (SocketChannel) key.channel();
//创建缓冲区
ByteBuffer buf = ByteBuffer.allocate(1032);
byte[] bytes = new byte[1024];
int read = 0;
StringBuilder receiveMessage = new StringBuilder();
int temp;
boolean statusSent = false;
//从通道中循环读取数据写入缓冲区,直到达到流的末尾,也就是客户端主动关闭输出流(SocketChannel.shutdownOutput()),或者关闭通道。但由于稍后客户端还需要从服务端接收数据,因此客户端还不能关闭通道。
while ((read = sChannel.read(buf)) != -1) {
//该方法会让缓冲区切换到读取状态,即①ByteBuff.limit = ByteBuff.position; ②ByteBuff.position = 0;
buf.flip();
receiveMessage.append(StandardCharsets.UTF_8.newDecoder().decode(buf));
//该方法让缓冲区清空,并准备下一次写入,即①ByteBuff.limit = ByteBuff.capacity; ②ByteBuff.position = 0;
buf.clear();
}
//执行至此时,地址接收完毕
String tpUrl = receiveMessage.toString();
LOGGER.info("长度:[{}],完整url: [{}]", tpUrl.length(), tpUrl);
URL url = new URL(tpUrl);
InputStream inputStream = null;
int responseCode = -1;
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setConnectTimeout(10000);
try {
inputStream = con.getInputStream();
responseCode = con.getResponseCode();
} catch (IOException e) {
String format = String.format("图片资源不存在或网络不通!图片地址:%s,异常信息:%s,状态码:%d", tpUrl, e.getMessage(), responseCode);
e.printStackTrace();
LOGGER.error(format);
//返回异常信息。我在这里自定义了服务器返回的第一个Int类型数据标识着当前请求成功与否1:成功,0:失败。
buf.putInt(0);
buf.put(format.getBytes(StandardCharsets.UTF_8));
buf.flip();
sChannel.write(buf);
buf.clear();
if (inputStream != null) {
inputStream.close();
}
//关闭通道,告知客户端写入已达到末尾,让它不再需要等待服务端。
sChannel.close();
return;
}
while ((temp = inputStream.read(bytes)) != -1) {
if (!statusSent) {
//发送成功标识
buf.putInt(1);
statusSent = true;
}
//写入图片数据到缓冲区
buf.put(bytes, 0, temp);
//反转一下,准备读取
buf.flip();
sChannel.write(buf);
//清空缓冲区,准备下一次写入
buf.clear();
}
LOGGER.info("返回结束.");
if (inputStream != null) {
inputStream.close();
}
//关闭通道
sChannel.close();
LOGGER.info("通道已关闭");
return;
}
}
客户端
import java.io.*;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class NIOClient {
public static void main(String[] args) throws FileNotFoundException {
nioClient();
}
public static void nioClient() {
try {
String[] sends = {
"https://pic.cnblogs.com/avatar/1345194/20180308181944.png",
"https://pic.cnblogs.com/avatar/1345194/20180308181944.png"
};
for (int i = 0; i < sends.length; i++) {
File file = new File("D:\\test\\IOnNio\\" + i + ".png");
FileOutputStream outputStream = new FileOutputStream(file);
output(outputStream, sends[i]);
}
} catch (IOException e) {
e.printStackTrace();
}
}
static void output(OutputStream out, String url) throws IOException {
//1. 获取通道
SocketChannel sChannel = SocketChannel.open();
//设置 10s 超时时间
sChannel.socket().connect(new InetSocketAddress("127.0.0.1", 9090), 10000);
//1.2 将阻塞的套接字 变为 非阻塞 的
sChannel.configureBlocking(false);
//2. 创建指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("发送:" + url);
sChannel.write(ByteBuffer.wrap(url.getBytes()));
// 主动关闭输出但不关闭通道,通知服务端当前发送请求已经完毕,等待服务端响应
sChannel.shutdownOutput();
int len = 0;
int count = 0;
int anInt = -1;
boolean statusRead = false;
//循环读取服务端返回到通道的数据,直到达到末尾
while ((len = sChannel.read(buf)) != -1) {
buf.flip();
if (!statusRead && buf.limit() > 0) {
//读取第一位标识成功或失败的 Int 值
anInt = buf.getInt();
statusRead = true;
}
if (anInt == 1) {
if (len > 0) {
count += buf.limit() - buf.position();
}
//输出到本地磁盘
out.write(buf.array(), buf.position(), buf.limit() - buf.position());
} else if (anInt == 0) {
//失败
System.out.println("服务端请求图片资源失败!");
System.out.println(new String(buf.array(), buf.position(), buf.limit() - buf.position(), StandardCharsets.UTF_8));
}
buf.clear();
}
out.close();
System.out.println("返回最终大小:" + count);
//关闭通道
sChannel.close();
}
}
附
日志依赖与配置
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
#log4j.properties
log4j.rootLogger=DEBUG, f, console
log4j.appender.f=org.apache.log4j.DailyRollingFileAppender
log4j.appender.f.File =logs/pic-download.log
log4j.appender.f.Append = true
log4j.appender.f.DatePattern='.'yyyy-MM-dd
log4j.appender.f.layout=org.apache.log4j.PatternLayout
log4j.appender.f.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p [%c{1}]: %m%n
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.out
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p [%c{1}]: %m%n