目录
1:写在文章之前
上一张的IO了解了IO流的字节流和字符流,主要操作读写文件。
我们在网络通信中经常会用到BIO和NIO,主要处理网络数据,以下就是BIO和NIO的特征。
BIO | NIO |
面向流 | 面向缓冲 |
阻塞IO | 非阻塞IO |
无 | 选择器 |
2:什么是BIO(Block IO-阻塞IO)
BIO:同步阻塞IO
同步:客户端跟服务器经过TCP链接后。客户端发数据后服务端立马能接受数据。
阻塞:1个客户端线程都会对应1个服务端线程。客户端线程发数据服务端线程接收数据。如果客户端线程还没有发送数据呢,服务端的对应的接受线程会阻塞。适合连接数目较小的框架。
结构如下:
2.1:代码案例(多个客户端对1个服务端)
客户端可以启动多个,但是服务端只有一个线程。只会处理连接到的第一个客户端,其他的客户端发送的数据服务端不会接受。
/**
* @date :8/30/21 4:16 PM
* 服务端只有一个线程提供服务
* 只能处理一个客户端
* 服务端代码
*/
public class Service1 {
public static void main(String[] args) {
try {
System.out.println("服务端启动");
//补充:端口范围0-65535,0-1023为系统占用端口
//服务端指定端口
//1:建立连接
ServerSocket serverSocket = new ServerSocket(9998);
//2:监听客户端
Socket socket = serverSocket.accept();
//3:获取IO流,通过scoket获取IO流
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int a=0;
//4:输出数据
while ((a = inputStream.read(bytes)) != -1) {
String string = new String(bytes, 0, a, "UTF-8");
System.out.println("服务端获取信息:" + string);
}
System.out.println("结束");
} catch (IOException e) {
e.printStackTrace();
}finally {
//此处不关闭
}
}
}
=====================以下是客户端代码===================
/**
* @author :huyiju
* @date :8/30/21 4:12 PM
* 客户端代码 发送数据
*/
public class BIO2 {
public static void main(String[] args) throws IOException {
System.out.println("客户端多发启动13");
String host = "127.0.0.1";
int port = 9998;
Socket socket = new Socket(host, port);
OutputStream outputStream = socket.getOutputStream();
Scanner scanner=new Scanner(System.in);
while (true){
System.out.print("请说:");
String msg=scanner.nextLine();
outputStream.write(msg.getBytes("UTF-8"));
outputStream.flush();//此处刷新流 便于下次重新写数据
//socket.close();//不要关闭 模拟多次请求
}
}
}
结果分析:客户端1和2都发送数据,但是服务端只能接收到第一个客户端的数据。验证了阻塞同步是BIO的特性
2.2:代码案例(多个客户端对应多个服务端)
在上边的案例。知道了BIO的同步。我们要是处理多个客户端的话,服务端可以自己手写,也可以使用线程池。此处手写代码,不是用线程池。
/**
* @author :huyiju
* @date :8/30/21 4:16 PM
* 服务端代码
* 实现多对多
* 线程的1对1
* 缺点客户端有多少请求,服务端就会有多少线程的响应
* 通信密集的时候 线程太多会崩的
*/
public class ServiceDuo {
public static void main(String[] args) {
try {
System.out.println("多个服务端启动");
//补充:端口范围0-65535,0-1023为系统占用端口
//服务端指定端口
//1:建立连接,接受多个客户端信息。
ServerSocket serverSocket = new ServerSocket(9998);
//2:监听客户端,多个客户端就有多个服务端
while (true){
Socket socket = serverSocket.accept();
new ScoketThreadRun(socket).start();//多线程传递scoket
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ScoketThreadRun extends Thread {
Socket socket;
public ScoketThreadRun(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//3:获取IO流
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int a;
//4:输出数据
while ((a = inputStream.read(bytes)) != -1) {
String string = new String(bytes, 0, a, "UTF-8");
System.out.println("服务端获取信息:" + string);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
=====================以下是客户端代码===================
/**
* @author :huyiju
* @date :8/30/21 4:12 PM
* 客户端代码 发送数据
*/
public class BIO2 {
public static void main(String[] args) throws IOException {
System.out.println("客户端多发启动13");
String host = "127.0.0.1";
int port = 9998;
Socket socket = new Socket(host, port);
OutputStream outputStream = socket.getOutputStream();
Scanner scanner=new Scanner(System.in);
while (true){
System.out.print("请说:");
String msg=scanner.nextLine();
outputStream.write(msg.getBytes("UTF-8"));
outputStream.flush();//此处刷新流 便于下次重新写数据
//socket.close();//不要关闭 模拟多次请求
}
}
}
结果展示可以看到服务端通过Socket socket = serverSocket.accept();创建多个socket,然后交给不同的线程处理。
3:什么是NIO(New IO-非阻塞IO)
NIO:同步非阻塞IO
同步:客户端跟服务器经过TCP链接后。客户端发数据后服务端立马能接受数据。
阻塞:多个个客户端线程都会对应1个服务端选择器(selector)。客户端可以有很多通过缓冲区(buffer)发送数据到通道(channel)。说人话就是客户端不管多少个线程,数据都是先用缓冲区封装,通过指定的通道发送到选择器。如果选择器看到有数据,才会创建服务端线程处理数据。没有数据线程不阻塞,只要一个人看着就行。这个人就是选择器。有数据的时候。选择器摇人。
Java NIO 由以下几个核心部分组成:
Channels(客户端通过通道给服务器传递数据,相当于高速公路)
Buffers(缓冲区是货车,负责按容量运送数据)
Selectors(收费站,看看那个高速公路的有货车发送数据,就指定线程处理数据)
结构如下
3.1:Buffers(缓冲区)
作用:相当于货车按照指定的容量装数据,并且能够读取数据。
以下是Java NIO里关键的Buffer实现:
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
这些Buffer覆盖了你能通过IO发送的基本数据类型:byte, short, int, long, float, double 和 char。
代码案例:
public class Buffer1 {
@Test
public void ByteBUffer1(){
//1:创建缓冲区byte 长度是10
ByteBuffer byteBuffered=ByteBuffer.allocate(10);
System.out.println(byteBuffered.put("123456".getBytes()));
System.out.println(byteBuffered.put("b".getBytes()));
System.out.println(byteBuffered.put("a".getBytes()));
System.out.println("position位置:"+byteBuffered.position());//随着读取数据 数据下标
System.out.println("界限:"+byteBuffered.limit());//数据实际长度不变 position<=limit<=capacity
System.out.println("容量:"+byteBuffered.capacity());
System.out.println("没有flip获取第一个数据会乱码:"+(char) byteBuffered.get());
System.out.println("----------------flip让数据可读------------------------");
byteBuffered.flip();
System.out.println("位置:"+byteBuffered.position());
System.out.println("界限:"+byteBuffered.limit());
System.out.println("容量:"+byteBuffered.capacity());
System.out.println("flip获取第一个数据:"+(char) byteBuffered.get());
System.out.println("flip获取第一个数据:"+(char) byteBuffered.get());
System.out.println("flip获取第一个数据:"+(char) byteBuffered.get());
System.out.println("position位置:"+byteBuffered.position());//随着读取数据 数据下标
System.out.println("界限:"+byteBuffered.limit());//数据实际长度不变 position<=limit<=capacity
System.out.println("容量:"+byteBuffered.capacity());
System.out.println("---------------清除数据-----------------");
System.out.println(byteBuffered.clear());
System.out.println(byteBuffered.put("jj".getBytes()));
System.out.println(byteBuffered.position());//2
System.out.println(byteBuffered.limit());//10
System.out.println(byteBuffered.capacity());//10
byteBuffered.flip();
System.out.println("位置:"+byteBuffered.position());//0
System.out.println("界限:"+byteBuffered.limit());//2
System.out.println("容量:"+byteBuffered.capacity());//10
}
@Test
public void ByteBUffer2(){
//1:创建缓冲区byte 长度是10
ByteBuffer byteBuffered=ByteBuffer.allocate(10);
System.out.println(byteBuffered.isDirect());
ByteBuffer byteBuffered1=ByteBuffer.allocateDirect(10);
System.out.println(byteBuffered1.isDirect());
}
}
3.2:Channels(通道)
作用:将数据放到channel。然后通过buffer运送数据。
Channel和Buffer有好几种类型。下面是JAVA NIO中的一些主要Channel的实现:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
代码案例
/**
* @author :huyiju
* @date :8/30/21 9:38 PM
* 通过channel
* 读文件 写文件 等操作
*/
public class Channel1 {
public static void main(String[] args) {
}
/**
* 用文件缓冲区把数据写入文件
*/
@Test
public void write() {
try {
int size = 2;
//1:输出流 写入文件
FileOutputStream outputStream = new FileOutputStream("/Users/huyiju/Desktop/log/huyiju.txt");
//2:字节流得到通道
FileChannel channel = outputStream.getChannel();
//3:分配缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(size);
//4:讲述数据写入缓冲区
String msg = "1234567890";
byte[] b = msg.getBytes();
int num = 0;
System.out.println("数据长度" + b.length + " 缓冲区长度:" + size);
if (b.length < size) {
byteBuffer.put(b);
byteBuffer.flip();
//将数据写入缓冲区
channel.write(byteBuffer);
System.out.println("写入结束1");
} else {
while (num < b.length) {
System.out.println("起始位置" + num + " 数据长度:" + size);
byteBuffer.clear();
if (b.length-num<size){
byteBuffer.put(b, num, (b.length-num));
byteBuffer.flip();
//将数据写入缓冲区
channel.write(byteBuffer);
System.out.println("写入结束22");
break;
}else {
byteBuffer.put(b, num, size);
num = num + 2;
byteBuffer.flip();
//将数据写入缓冲区
channel.write(byteBuffer);
System.out.println("写入结束2");
}
}
}
channel.close();
//System.out.println(byteBuffer.put("hello word".getBytes()));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 从文件读取数据
*/
@Test
public void read() {
try {
//1:输出流 写入文件
FileInputStream fileInputStream = new FileInputStream("/Users/huyiju/Desktop/log/huyiju.txt");
//2:字节流得到通道
FileChannel channel = fileInputStream.getChannel();
//3:分配缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
//4:讲述数据写入缓冲区
//将数据写入缓冲区
while (true) {
byteBuffer.clear();//先清空缓冲区
int flag = channel.read(byteBuffer);
if (flag == -1) {
break;
}
byteBuffer.flip();
String string = new String(byteBuffer.array(), 0, byteBuffer.limit());
System.out.println(string);
}
channel.close();
System.out.println("读取结束");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 从文件读取数据 然后写入文件
*/
@Test
public void readAndWrite() {
try {
//1:输出流 写入文件
FileInputStream fileInputStream = new FileInputStream("/Users/huyiju/Desktop/log/huyiju.txt");
FileOutputStream fileOutputStream = new FileOutputStream("/Users/huyiju/Desktop/log/huyiju_new.txt");
//2:字节输入数据流得到不同的通道
FileChannel channelin = fileInputStream.getChannel();
FileChannel channelout = fileOutputStream.getChannel();
//3:分配缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(4);
//4:讲述数据写入缓冲区
//将数据写入缓冲区
while (true) {
byteBuffer.clear();//先清空缓冲区
int flag = channelin.read(byteBuffer);//数据量大 按照缓冲区读 然后写
if (flag == -1) {
break;
}
byteBuffer.flip();
channelout.write(byteBuffer);
}
channelin.close();
channelout.close();
System.out.println("先读数据 在写数据");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 从文件读取数据 然后写入文件
*/
@Test
public void transTo() {
try {
//1:原通道
FileInputStream fileInputStream = new FileInputStream("/Users/huyiju/Desktop/log/huyiju.txt");
FileChannel fileChannelIn = fileInputStream.getChannel();
//2:复制通道
FileOutputStream fileOutputStream = new FileOutputStream("/Users/huyiju/Desktop/log/huyiju_new1.txt");
FileChannel fileChannelOut = fileOutputStream.getChannel();
//3:复制通道
FileOutputStream fileOutputStream2 = new FileOutputStream("/Users/huyiju/Desktop/log/huyiju_new2.txt");
FileChannel fileChannelOut2 = fileOutputStream2.getChannel();
//3:复制数据 transferFrom(来源通道)
//fileChannelOut.transferFrom(fileChannelIn,fileChannelIn.position(),fileChannelIn.size());
//3.1:复制数据 transferTo(来源通道,目标通道)
fileChannelIn.transferTo(fileChannelIn.position(), fileChannelIn.size(), fileChannelOut2);
fileChannelIn.close();
fileChannelOut.close();
fileChannelOut2.close();
System.out.println("复制数据");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.3:Selector(选择器)
作用:Selector可以根据不同的事件选择不同的选择器,要使用selector必须将channel注册到选择器上。采用多路复用。
此处代码采用,bytebuffer(缓冲区)、ServerSocketChannel(网络通道)、Selector实现网络通信。
此处代码采用多个客户端启动,服务端一个,然后查看运行结果
/**
* @date :8/31/21 12:09 AM
* NIO 非阻塞通信 服务端开发
* 可以应对多客户端,非阻塞模式
*/
public class ServiceHu {
public static void main(String[] args) throws IOException {
//1:获取服务端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2:切换非阻塞模式
serverSocketChannel.configureBlocking(false);
//3:绑定端口
serverSocketChannel.bind(new InetSocketAddress(9997));
//4:获取选择器
Selector selector = Selector.open();
//5:将服务端通道注册到选择器
//OP_READ = 1 << 0; 读
//OP_WRITE = 1 << 2; 写
//OP_CONNECT = 1 << 3;链接
//OP_ACCEPT = 1 << 4; 接收
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("1:服务端启动");
//6:使用selector 轮循环事件>0
while (selector.select() > 0) {
System.out.println("2:客户端启动会进入选择器");
//迭代所有的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
System.out.println("进入迭代事件");
//7:提取当期事件
SelectionKey selectionKey = iterator.next();
//8:判断事件是否是接收事件
if (selectionKey.isAcceptable()) {
System.out.println("3:客户端启动会进入接受事件");
//9:获取客户端通道
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
//10:将客户端注入选择器 读事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
System.out.println("4:进入可读事件");
//11:如果是读事件 获取客户端通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//12:读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len;
while ((len = socketChannel.read(byteBuffer)) > 0) {
byteBuffer.flip();
System.out.println("服务端获取数据输出:" + new String(byteBuffer.array(), 0, len));
byteBuffer.clear();
}
} else if (selectionKey.isWritable()) {
}
iterator.remove();//处理完毕 移除当前事件
}
}
}
}
======================以下是客户端代码==================
/**
* @date :8/30/21 11:56 PM
* 客户端发送数据
*/
public class Client1 {
public static void main(String[] args) throws IOException {
//1:获取客户端通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9997));
//2:设置通道是否堵塞false 非阻塞
socketChannel.configureBlocking(false);
//3:设置缓冲区容量
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
//4:得到输入
Scanner scanner=new Scanner(System.in);
//5:循环
while (true){
System.out.println("输入要发送数据");
String msg=scanner.nextLine();
byteBuffer.put(msg.getBytes());
byteBuffer.flip();//通道可读
socketChannel.write(byteBuffer);
byteBuffer.clear();//重置缓冲区
}
}
}
运行结果省略