File类的一个对象,代表一个文件或一个文件目录(Directory)
File类声明在java.io
包下
File类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法**,并未涉及到写入或读取文件内容的操作。**如果需要读取或写入文件内容,必须使用IO流(Stream)来完成。
后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的终点。
// public File(String pathname)
File disc = new File("D:\\");
// public File(String parent, String child)
File c1 = new File("D:\\","child");
// public File(File parent, String child)
File c2 = new File(disc,"child");
// public File(URI uri)
File file = new File(URI.create("file:///d:/"));
由于java跨平台运行的特性,我们有的时候必须兼顾所有平台的路径分隔符
windows和DOS系统:\
UNIX和URL:/
File
类提供了静态变量separator
来根据操作系统动态提供路径分隔符
File.separator
public String getAbsolutePath()
public String getPath()
public String getName()
public String getParent()
public long length()
public long lastModified()
public String[] list()
public File[] listFiles()
public boolean renameTo(File dest)
public boolean isDirectory()
public boolean isFile()
public boolean exists()
public boolean canRead()
public boolean canWrite()
public boolean isHidden()
public boolean createNewFile()
public boolean mkdir()
public boolean mkdirs()
public boolean delete()
File file = new File("D:\\");
File[] files = file.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
// 过滤出所有的文件
return pathname.isFile();
}
});
System.out.println(Arrays.toString(files));
I:input ; O : output
I/O
即输入输出,是计算机与外界世界的一个接口。IO操作的实际主体是操作系统。在java编程中,一般使用流的方式来处理IO,所有的IO都被视作是单个字节的移动,通过stream对象一次移动一个字节。流IO负责把对象转换为字节,然后再转换为对象。
操作数据单位:字节流(8bit
)、字符流 (16bit
)
数据的流向:输入流(写入内存)、输出流(从内存写出)
流的角色:节点流(从一个特定的数据源读写数据)、处理流(连接在已存在的流之上,为程序提供更为强大的读写功能)
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputSreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
FilterInputStream | FilterOutputStream | FilterReader | FilterWriter | |
打印流 | PrintStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
如:InputStream、Reader、OutputStream、Writer
节点流与具体节点相连接,直接读写节点数据
.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt
.txt,.java,.c,.cpp
等语言的源代码。尤其注意.doc,.excel,.ppt
这些不是文本文件。InputStream(字节流) 和 Reader **(字符流)**是所有输入流的基类。
程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件 IO 资源(使用close方法)。
FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream 用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader
// 从输入流读取下一个字节,返回0到255的int字节值,如果到达流末尾则返回-1
int read()
// 从输入流中读取最多b.length个字节到byte[] b中,并返回读取字节的长度,如果到达流末尾则返回-1
int read(byte[] b)
// 从输入流中读取最多len个字节到byte[] b中,读到的第一个字节会被写到b[off]中并依次往后,并返回读取字节的长度,如果到达流末尾则返回-1
int read(byte[] b,int off,int len)
// 返回从该输入流中可读取的剩余字节数之和
int available()
// 关闭输入流并释放与该流有关的所有系统资源
public void close() throws IOException
// 从输入流读取单个字符,返回0到65535的int类型Unicode码值,如果到达流末尾则返回-1
int read()
// 从输入流读取最多cbuf.length个字符到char[] cbuf中,并返回读取字符的长度,如果到达流末尾则返回-1
int read(char[] cbuf)
// 从输入流中读取最多len个字符到char[] cbuf中,读到的第一个字符会被写到cbuf[off]中并依次往后,并返回读取字符的长度,如果到达流末尾则返回-1
int read(char[] cbuf,int off,int len)
// 关闭输入流并释放与该流有关的所有系统资源
public void close() throws IOException
**OutputStream(字节流)和Writer(字符流)**是所有输出流的基类。
在写入一个文件时,如果使用构造器FileOutputStream(file)
,则目录下有同名文件将被覆盖。
如果使用构造器FileOutputStream(file,true)
,则目录下的同名文件不会被覆盖, 在文件内容末尾追加内容。
因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组, 即以 String 对象作为参数
显式关闭IO资源,需要使用flush(防止数据丢失)和close方法
FileOutputStream 用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter
// 向输出流写入一个0-255范围的int类型的字节值
void write(int b)
// 将byte[] b中的所有字节写入输出流
void write(byte[] b)
// 将byte[] b数组从b[off]开始的len个字节写入输出流
void write(byte[] b,int off,int len)
// 刷新输出流,并强制将缓冲中的所有字节写出到输出流
public void flush()throws IOException
// 关闭输出流并释放与该流关联的系统资源
public void close()throws IOException
// 向输出流写入一个0-65535范围的int类型的Unicode码值
void write(int c)
// 将char[] cbuf中的所有字符写入输出流
void write(char[] cbuf)
// 将char[] cbuf从cbuf[off]开始的len个字符写入输出流
void write(char[] cbuf,int off,int len)
// 将字符串写入输出流
void write(String str)
// 将字符串从第off个字符开始写入len个字符到输出流
void write(String str,int off,int len)
// 刷新输出流,并强制将缓冲中的所有字节写出到输出流
public void flush()throws IOException
// 关闭输出流并释放与该流关联的系统资源
public void close()throws IOException
public static void copy(String copyBy,String copyTo) throws IOException {
//输入流
InputStream inputStream = new FileInputStream(new File(copyBy));
//输出流
OutputStream outputStream = new FileOutputStream(new File(copyTo));
//创建缓冲数组
byte[] bytes = new byte[1024];
int i;
//循环写入缓冲,然后从缓冲数组中输出到目标文件
while ((i = inputStream.read(bytes)) != -1){
outputStream.write(bytes,0,i);
}
//释放资源
outputStream.flush();
inputStream.close();
outputStream.flush();
}
BufferedInputStream 、BufferedOutputStream、BufferedReader、BufferedWriter
优点:提供流的读取、写入的速度
提高读写速度的原因:内部提供了一个缓冲区。默认情况下是8kb
File f = new File("D:\\test.txt");
FileInputStream inputStream = new FileInputStream(f);
// 创建缓冲流,并设置缓冲区大小
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream,1024);
byte[] bytes = new byte[bufferedInputStream.available()];
bufferedInputStream.read(bytes);
bufferedInputStream.close();
String s = new String(bytes);
System.out.println(s);
InputStreamReader:将一个字节的输入流转换为字符的输入流
File f = new File("D:\\test.txt");
InputStream inputStream = new FileInputStream(f);
// 将字节流转换为字符流
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("utf8"));
StringBuilder stringBuilder = new StringBuilder();
while (true){
int read = inputStreamReader.read();
if (read == -1){
break;
}
stringBuilder.append((char) read);
}
System.out.println(stringBuilder.toString());
inputStreamReader.close();
OutputStreamWriter:将一个字节的输出流转换为字符的输出流
File f = new File("D:\\test.txt");
OutputStream outputStream = new FileOutputStream(f);
// 将字节流转换为字符流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, Charset.forName("utf8"));
String target = "Hello Java IO";
outputStreamWriter.write(target);
outputStreamWriter.flush();
outputStreamWriter.close();
客户端/浏览器端——后台(Java,GO,Python)——数据库
要求前后使用的字符集都要统一:UTF-8
ObjectInputStream、OjbectOutputSteam
用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
java.io.Serializable
serialVersionUID
(序列化类新增属性时,请不要修改serialVersionUID字段,避免反序列失败),作用是验证版本一致性。Serializable
接口之外,还必须保证其内部所属性也必须是可序列化的。(默认情况下,基本数据类型可序列化;如果是引用数据类型,那么需要改类型是可序列化类)InvalidClassException
异常private static final long serialVersionUID = 771652260758459933L;
可序列化类中的版本标识,JVM用这个字段来确定是否能够反序列化出对象。换句话说,只有对象序列化后的二进制数据中的serialVersionUID
与当前对象的serialVersionUID
相同,反序列化才能成功,否则就会失败。
即使自己不显式的声明serialVersionUID
,在序列化时,JVM也会生成一个。但是,最好还是自己手动声明一个,避免后续程序执行出现问题。
然后在实现java.io.Serializable
接口的类名上,ctrl + enter
选择add 'serialVersionUID' field
即可
Java关键字
对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。
在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。
import java.io.Serializable;
public class Student implements Serializable {
private static final long serialVersionUID = 771652260758459933L;
private String id;
private String name;
private Integer age;
// transient 修饰的属性不会被序列化
private transient String tempNum;
public Student(String id, String name, Integer age, String tempNum) {
this.id = id;
this.name = name;
this.age = age;
this.tempNum = tempNum;
}
@Override
public String toString() {
return "Student{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", age=" + age +
", tempNum='" + tempNum + '\'' +
'}';
}
}
序列化
Student student = new Student("s1", "tom", 18,"t1");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./s1"));
objectOutputStream.writeObject(student);
objectOutputStream.flush();
objectOutputStream.close();
反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("./s1"));
Student student = (Student)objectInputStream.readObject();
System.out.println(student);
objectInputStream.close();
简单的话,可以在实现Serializable
接口的类中,编写private void writeObject(ObjectOutputStream out)
(序列化时会调用此方法)和private void readObject(ObjectInputStream in)
(反序列化时会调用此方法)两个方法来实现,但是性能欠佳
可以实现java.io.Externalizable
接口,并重写writeExternal
和readExternal
方法来实现,性能不错
import java.io.*;
public class Student implements Externalizable {
private static final long serialVersionUID = 771652260758459933L;
private String id;
private String name;
private Integer age;
// transient 修饰的属性不会被序列化
private transient String tempNum;
// 实现Externalizable反序列化必须要有无参构造
public Student() {
}
public Student(String id, String name, Integer age, String tempNum) {
this.id = id;
this.name = name;
this.age = age;
this.tempNum = tempNum;
}
@Override
public String toString() {
return "Student{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", age=" + age +
", tempNum='" + tempNum + '\'' +
'}';
}
// 定义序列化方法
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(this.id);
out.writeUTF(this.name);
out.writeInt(this.age);
}
// 定义反序列化逻辑
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 读写顺序一定要一致
this.id = in.readUTF();
this.name = in.readUTF();
this.age = in.readInt();
}
}
序列化
Student student = new Student("s1", "tom", 18,"t1");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./s1"));
objectOutputStream.writeObject(student);
objectOutputStream.flush();
objectOutputStream.close();
反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("./s1"));
Student student = (Student)objectInputStream.readObject();
System.out.println(student);
objectInputStream.close();
很多技术框架都使用NIO技术,学习和掌握Java NIO技术对于高性能、高并发网络的应用是非常关键的。
NIO (New lO)也有人称之为Java non-blocking lO
是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()
时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
java.nio
包及子包下,并且对原java.io
包中的很多类进行改写。BIO全称是Blocking IO
,同步阻塞式IO,是JDK1.4
之前的传统IO模型,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如下图所示
虽然此时服务器具备了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃,NIO可以一定程度解决这个问题。
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
一个线程中就可以调用多路复用接口(java中是select)阻塞同时监听来自多个客户端的IO请求,一旦有收到IO请求就调用对应函数处理,NIO擅长1
个线程管理多条连接,节约系统资源。
关系图的说明:
flip()
切换读写模式,而BIO是单向的,要么输入流要么输出流。Buffer本质上就是一块可以重复进行读写的内存空间(底层就是个数组),重要的五个概念如下
capacity - 1
。当从buffer中读取数据时,position重置回0,记录下一个要读取数据的位置。操作停止的最终位置 + 1
,用于限制程序可以写入或者读取的数据量,通常为limit <= capacity
mark()
方法 指定Buffer中一个特定的position,之后可以通过调用reset()
方法恢复到这个 position0 <= mark <= position <= limit <= capacity
// 创建非直接缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 创建直接缓冲区(只有ByteBuffer有这个方法)
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
只有ByteBuffer可以区分创建直接缓冲区和非直接缓冲区
ByteBuffer.allocateDirect()
本地IO-->直接内存-->本地IO
ByteBuffer.allocate()
本地IO-->直接内存-->非直接内存-->直接内存-->本地IO
Buffer clear()
:清空缓冲区并返回对缓冲区的引用,并且设置position=0
,limit=capcity
Buffer flip()
:为将缓冲区的界限设置为当前位置, 并将当前位置重置为0int capacity()
:返回 Buffer 的 capacity 大小boolean hasRemaining()
: 判断缓冲区中是否还有元素int limit()
:返回 Buffer 的界限(limit) 的位置Buffer limit(int n)
:将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象Buffer mark()
:对缓冲区设置标记int position()
:返回缓冲区的当前位置 positionBuffer position(int n)
:将设置缓冲区的当前位置为 n, 并返回修改后的 Buffer 对象int remaining()
:返回 position 和 limit 之间的元素个数Buffer reset()
:将位置 position 转到以前设置的mark 所在的位置Buffer rewind()
:将位置设为为 0, 取消设置的 markget()
:读取单个字节get(byte[] dst)
:批量读取多个字节到 dst 中get(int index)
:读取指定索引位置的字节(不会移动 position)放到入数据到Buffer中put(byte b)
:将给定单个字节写入缓冲区的当前位置put(byte[] src)
:将 src 中的字节写入缓冲区的当前位置put(int index, byte b)
:将指定字节写入缓冲区的索引 位置(不会移动 position)flip()
方法,转换为读取模式buffer.clear()
方法或者buffer.compact()
方法清除缓冲区1、创建一个容量capacity
为5
的buffer,此时缓冲区默认为写模式
ByteBuffer buffer = ByteBuffer.allocate(10);
2、写入数据到Buffer中,写入三个字节
channel.read(buffer)
3、切换为读模式,将position指向0位置,并将limit指向上一步position的位置3(最后有效数据的后一位)
buffer.flip();
4、从Buffer中逐个字节读取,直到position = limit
位置
StringBuilder stringBuilder = new StringBuilder();
while (buffer.position() < buffer.limit()){
stringBuilder.append((char) buffer.get());
}
5、清空Buffer,还原为写模式,准备下一次读取,循环至第一步
buffer.clear();
Channel 是 NIO 的核心概念,它表示 IO 源与目标打开的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序。 Channel 类似于传统的“流”,只不过 Channel本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
getChannel()
方法
newByteChannel()
获取字节通道open()
打开并返回指定通道Java NIO FileChannel是连接文件的通道。使用FileChannel,可以从文件中读取数据和将数据写入文件。
FileChannel 的优点包括:
FileChannel可以通过静态方法public static FileChannel open(Path path, OpenOption... options)
打开
package java.nio.file;
public enum StandardOpenOption implements OpenOption {
READ, // 读
WRITE, // 写
APPEND, // 追加写
TRUNCATE_EXISTING, // 如果文件存在并且以WRITE的方式连接时就会把文件内容清空,如果文件只以READ连接时,该选项会被忽略
CREATE, // 如果文件不存在则创建
CREATE_NEW, // 创建文件如果存在则抛异常
DELETE_ON_CLOSE, // Channel关闭时删除文件
SPARSE, // 创建稀疏文件,与CREATE_NEW选项配合使用
SYNC, // 要求每次写入要把内容和元数据刷到存储设备上
DSYNC; // 要求每次写入要把内容刷到存储设备上
}
int read(ByteBuffer dst)
:从Channel 到 中读取数据到 ByteBufferlong read(ByteBuffer[] dsts)
: 将Channel中的数据“分散”到 ByteBuffer[]int write(ByteBuffer src)
:将 ByteBuffer中的数据写入到 Channellong write(ByteBuffer[] srcs)
:将 ByteBuffer[] 到 中的数据“聚集”到 Channellong position()
:返回此通道的文件位置FileChannel position(long p)
:设置此通道的文件位置long size()
:返回此通道的文件的当前大小FileChannel truncate(long s)
:将此通道的文件截取为给定大小void force(boolean metaData)
:强制将所有对此通道的文件更新写入到存储设备中零拷贝是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。
long transferTo(long position, long count, WritableByteChannel target)
long transferFrom(ReadableByteChannel src, long position, long count)
这两个方法底层调用了Linux和UNIX系统的sendfile()
系统调用实现了零拷贝
// 源数据通道
FileChannel sChan = FileChannel.open(Paths.get("D:\\test.jpg"), StandardOpenOption.READ);
// 目标数据通道
FileChannel tChan = FileChannel.open(Paths.get("E:\\test_copy.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
// 进行拷贝
sChan.transferTo(0,sChan.size(),tChan);
sChan.close();
tChan.close();
// 1、创建通道
FileChannel channel = FileChannel.open(Paths.get("D:\\test.txt"), StandardOpenOption.READ);
// 2、创建字节缓冲区,大小10
ByteBuffer buffer = ByteBuffer.allocate(5);
// 3、读数据
StringBuilder stringBuilder = new StringBuilder();
// 4、每次读取字节数量 > 0,并且 <= buffer.capacity()
while (channel.read(buffer) > 0){
// 5、切换为读模式
buffer.flip();
// 6、将Buffer中的字节挨个读取
while (buffer.position() < buffer.limit()){
stringBuilder.append((char) buffer.get());
}
// 7、清空Buffer切换为写模式
buffer.clear();
}
// 8、关闭通道
channel.close();
System.out.println(stringBuilder.toString());
// 1、创建通道
FileChannel channel = FileChannel.open(Paths.get("D:\\test.txt"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
// 2、创建字节缓冲区,大小1024
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3、读数据
String target = "Hello Java Nio!";
byte[] bytes = target.getBytes(StandardCharsets.UTF_8);
// 4、依次写入字节数据
for (int i = 0; i < bytes.length ; i++){
buffer.put(bytes[i]);
if (buffer.position() == buffer.limit() || i == bytes.length - 1){
// 5、切换写模式
buffer.flip();
// 6、缓冲中的数据写入到通道
channel.write(buffer);
// 7、清空缓冲
buffer.clear();
}
}
// 8、将通道中的数据强制刷出到磁盘
channel.force(false);
// 9、关闭通道
channel.close();
新的socket通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。
DatagramChannel 和 SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身不传输数据。
(DatagramChannel、SocketChannel、ServerSocketChannel
)在被实例化时都会创建一个对等 socket 对象: java.net
包中的(DatagramSocket
、Socket
、ServerSocket
),可以通过Channel的socket()
方法获取,此外,这三个 java.net
类现在都有getChannel()
方法。
configureBlocking()
(参数为false:非阻塞,true:阻塞)的作用:对于ServerSocketChannel来说,非阻塞意味着accept()
方法为非阻塞,即时没有客户端连接,也会立即返回null
;对于SocketChannel来说,read()
方法为非阻塞,即时没有读到数据,也会继续执行下面的逻辑。
// 1、开启ServerSocketChannel
ServerSocketChannel channel = ServerSocketChannel.open();
// 2、设置监听的端口
channel.bind(new InetSocketAddress(8080));
// 3、设置为非阻塞
channel.configureBlocking(false);
SocketChannel socketChannel;
while (true){
// 4、接受客户端连接
socketChannel = channel.accept();
// 判断是否获取到连接
if (socketChannel != null){
// 5、设置客户端连接通道为非阻塞
socketChannel.configureBlocking(false);
// 6、获取客户端信息
Socket socket = socketChannel.socket();
System.out.println("客户端连接:" + socket.getRemoteSocketAddress());
// 7、创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 8、轮询读取客户端发来的消息
while (true){
// 判断是否有消息
if (socketChannel.read(buffer) > 0){
buffer.flip();
byte[] b = new byte[buffer.limit()];
buffer.get(b);
System.out.println("客户端说:" + new String(b, StandardCharsets.UTF_8));
buffer.clear();
}
}
}
}
// 1、建立和服务端的连接通道
SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8080));
// 2、设置通道为非阻塞
channel.configureBlocking(false);
// 3、创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4、向服务器发送消息
while (true){
String s = new Scanner(System.in).nextLine();
if ("exit".equals(s)){
break;
}
buffer.put(s.getBytes(StandardCharsets.UTF_8));
buffer.flip();
channel.write(buffer);
buffer.clear();
}
选择器(Selector)是SelectableChannle对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心。
注意:注册到Selector的Channel必须是非阻塞的以及可选择的(继承实现SelectableChannel抽象类),FileChannel不能注册到Selector,因为FileChannel不能切换为非阻塞模式,SelectableChannel抽象类有一个configureBlocking()
方法,SocketChannel
, ServerSocketChannel
, DatagramChannel
都是直接继承了 AbstractSelectableChannel(SelectableChannel的子类)抽象类。
Selector.open()
:创建选择器
SelectableChannel.register(Selector sel, int ops)
:将一个可选择的通道注册到选择器sel
上,第二个参数是选择选择器监听Channel关注什么事件
SelectionKey.OP_CONNECT
:连接就绪,一个客户端成功连接到另一个服务器时触发
SelectionKey.OP_ACCEPT
:接收就绪,当服务端收到客户端的一个连接请求时触发
SelectionKey.OP_READ
:读就绪,有一个数据可读的通道时触发
SelectionKey.OP_WRITE
:写就绪,有一个等待写数据的通道时触发
int select()
:阻塞到至少有一个通道在注册的事件上就绪了
select(long timeout)
:和select()
方法一样,可以设置最长阻塞时间(毫秒)
selectNow()
:不会阻塞,即时没有通道事件也会返回0
selectedKeys()
:一旦调用了select()
方法,并且返回值表明有一个或更多个通道就绪了,然后可以调用该方法获取所有就绪通道。
keys()
:返回当前所有注册在selector中channel的selectionKey,和selectedKeys()
的区别就是,这个方法返回的是所有已注册的通道,而不是当前已就绪的通道。
服务端
// 创建选择器
Selector selector = Selector.open();
// 创建可选择通道ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
// 注册到选择器,并且关注通道的连接事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
// select()为阻塞方法,当获取到新事件后,会返回事件的数量,并将就绪事件放入SelectionKey集合中
while (selector.select() > 0){
// 获取事件集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 处理每一个事件
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
// 如果是连接事件(有新的客户端连接)
if (selectionKey.isAcceptable()){
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
System.out.println("有新的客户端连接:" + socketChannel.socket().getRemoteSocketAddress());
// 将客户端通道注册到选择器,并且关注通道的可读事件
socketChannel.register(selector,SelectionKey.OP_READ);
// 如果是可读取事件
}else if (selectionKey.isReadable()){
// 创建buffer接数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 获取事件对应的通道
SocketChannel channel = (SocketChannel)selectionKey.channel();
channel.read(buffer);
buffer.flip();
System.out.println("收到客户端信息:" + new String(buffer.array()));
}
// 处理完事件,切记要移除事件
iterator.remove();
}
}
客户端
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8080));
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true){
System.out.print("input:");
String a = new Scanner(System.in).nextLine();
if ("quit".equals(a)){
break;
}
byte[] bytes = a.getBytes(StandardCharsets.UTF_8);
for (int i = 0 ;i < bytes.length;i ++){
buffer.put(bytes[i]);
if (buffer.position() == buffer.limit() || i == bytes.length - 1){
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
}
}