三大组件
Chinnel
- FileChannel
一般就是文件的数据传输通道
- DatageamChannel
UDP网络编程时用到的数据传输通道
- SocketChannel
TCP时的数据传输通道(客户端服务器都可用)
- ServerSocketChannel
TCP时的数据传输通道(服务器专用)
Buffer
Selector
**改进后**
必须等一个socket任务执行完成才能执行别的sokcket任务
我们来看看 **Selector**
是怎么解决的selector
会监听channel
中客户端的需求,一旦客户端有需求,selector
就会把需求分发给Thread
执行
ByteBuffer
使用Channle和FileBuffer读取文件
public static void main(String[] args) {
// FileChannel
// 输入输出流
try(FileChannel channel = new FileInputStream("./NIOBasic/data.txt").getChannel()) {
// 准备缓冲区
ByteBuffer buffer = ByteBuffer.allocate(8196);
// 读取数据写入缓冲区
int len=0;
while (true){
int read = channel.read(buffer);
if (read==-1){
break;
}
// 打印buffer的内容
buffer.flip(); // 切换至读模式(将读取位置设置为0)
// 获取数据
while (buffer.hasRemaining()){ // hasRemaining是否还有剩余
byte b = buffer.get(); // 一次读一个
System.out.println((char)b);
}
// 清空缓冲区,进入写模式,否则buffer再次执行读取的时候将读不到数据,永远读的前几个
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
ByteBuffer结构
ByteBuffer常见方法
分配空间方法
ByteBuffer allocate = ByteBuffer.allocate(16); // -java堆内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(16); // -直接内存
堆内存:
读写效率低,java内部内存,会收到gc
影响
直接内存:
读写效率高,少一次拷贝,不会受到gc
影响,分配速度慢因为需要调用系统函数,而且使用不当容易造成内存泄漏
向buffer写入数据
int read = channel.read(buffer); // 从channel读取数据存入buffer
buffer.put(byte[]) // 直接向buffer写入数据
从buffer读取数据
byte b = buffer.get(); // 从buffer读取
channel.write(buffer); // 向channel输出时传入buffer
读写之前记得调用
buffer.get_(1);// 按照索引读取,并且不改变position指针_
字符串和buffer互转
Scattering Read(分散读)
GetherubgWrites(集中写入)
将buffer进行组合,进行集中写入,减少拷贝次数
粘包和半包的问题
粘包
因为数据传输时为了提升效率,经常将数据组合发送,所以产生了粘包
半包
因为服务器缓冲区大小限制,导致在某一些数据传输中必须分批发送数据,导致出现半包问题
文件编程
基本APi
两个Channel进行传输
把数据从Channel传输到另一个Channel
public void toChannel() {
try (
FileChannel from = new FileInputStream("NIOBasic\\data.txt").getChannel();
FileChannel to = new FileOutputStream("NIOBasic\\toData.txt").getChannel()
){
// 效率高,经过了0拷贝优化
from.transferTo(0,from.size(),to); // 从from发起传输,从0开始,结束位置是from的最大长度也是就全部,到传输到to的channel去
} catch (IOException e) {
e.printStackTrace();
}
}
// 传输大于2gb的文件
public void toChannel2() {
try (
FileChannel from = new FileInputStream("NIOBasic\\data.txt").getChannel();
FileChannel to = new FileOutputStream("NIOBasic\\toData.txt").getChannel()
){
// 这里有一点需要注意,这个传输方法一次最大传输2gb的文件,多出的文件将会消失,
// 我们可以分批传递
long size = from.size();
// left代表channel中还有多少
long left = size;
while (left>0){
long numberOfBytes = from.transferTo(size-left, left, to); // 返回的是实际传输了多少
left = size-numberOfBytes;
}
} catch (IOException e) {
e.printStackTrace();
}
}
Path
Files
需要手动删除所有文件以后才能删除文件夹
遍历文件夹案例
public static void main(String[] args) throws IOException {
Path start = Paths.get("D:\\Project\\TestProject\\Netty");
// 必须用这两个进行计数
// 因为匿名类里和外部变量不再一个工作空间,所以不能使用基本类型,
// 因为基本类型数据是保存在栈里的,就得用地址不可变的引用类型
// 即使Integer也不行因为值变了地址也变了,简而言之,匿名类如果要使用外部变量,则该变量地址不能发生改变;
final AtomicInteger dirCount = new AtomicInteger();
final AtomicInteger fileCount = new AtomicInteger();
SimpleFileVisitor<Path> fileVisitor = new SimpleFileVisitor<Path>() {
// 访问到目录时执行的方法
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOExcept
// 重写时我们加入自己的方法就可以了,不要去改动他的返回
System.out.println("进入文件夹:" + dir.getFileName());
dirCount.incrementAndGet();
return super.preVisitDirectory(dir, attrs);
}
// 访问到文件时执行的方法
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("找到文件:" + file.getFileName());
fileCount.incrementAndGet();
return super.visitFile(file, attrs);
}
};
// 遍历文件树
Files.walkFileTree(start,fileVisitor);
System.out.println("文件夹共计:"+dirCount+"个");
System.out.println("文件共计:"+fileCount+"个");
}
遍历删除
遍历删除中就是在遍历到文件的时候直接删除文件,然后删除文件夹
public static void main(String[] args) throws IOException {
Path start = Paths.get("D:\\Project\\TestProject\\Netty\\NIOBasic\\src\\main\\java\\cn\\netty");
SimpleFileVisitor<Path> fileVisitor = new SimpleFileVisitor<Path>() {
// 进入文件夹时
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("进入:"+dir.getFileName());
return super.preVisitDirectory(dir, attrs);
}
// 进入文件时
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("找到文件:"+file.getFileName());
return super.visitFile(file, attrs);
}
// 退出文件夹时
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("退出:"+dir.getFileName());
return super.postVisitDirectory(dir, exc);
}
};
Files.walkFileTree(start,fileVisitor);
}
所以我们只需要在找到文件时
删除所有文件,在退出文件夹时
删除文件夹即可
public static void main(String[] args) throws IOException {
Path start = Paths.get("C:\\Users\\1\\Desktop\\tempa");
SimpleFileVisitor<Path> fileVisitor = new SimpleFileVisitor<Path>() {
// 进入文件时
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("删除文件:"+file.getFileName());
Files.delete(file);
return super.visitFile(file, attrs);
}
// 退出文件夹时
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
System.out.println("删除文件夹:"+dir.getFileName());
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
};
Files.walkFileTree(start,fileVisitor);
}
文件夹对拷
public static void main(String[] args) throws IOException {
Path resource = Paths.get("C:\\Users\\1\\Desktop\\webSocketTest");
Path target = Paths.get("C:\\Users\\1\\Desktop\\channelTest");
// 使用返回Stream流的方法
Files.walk(resource).forEach(
path -> {
if (Files.isDirectory(path)) {
// 如果是文件夹就创建目录
// 生成路径
String targetPah = target.toString() + path.toString().replace(resource.toString(), "");
try {
Files.createDirectories(Paths.get(targetPah));
} catch (IOException ioException) {
ioException.printStackTrace();
System.out.println("文件夹创建错误");
}
}
if (Files.isRegularFile(path)) {
// 如果是文件就传输
// 路径
String targetPah = target.toString() + path.toString().replace(resource.toString(), "");
try {
// FileChannel in = new FileInputStream(path.toString()).getChannel();
// FileChannel out = new FileOutputStream(targetPah).getChannel();
// in.transferTo(0, in.size(), out);
// 或者使用
Files.copy(path,Paths.get(targetPah));
} catch (IOException e) {
e.printStackTrace();
}
}
}
);
}
网络编程
阻塞和非阻塞
阻塞
public class Server {
// 用于存放连接
public static LinkedList<SocketChannel> CHANNELS = new LinkedList<>();
public static ByteBuffer buf =ByteBuffer.allocate(8196);
public static void main(String[] args) throws IOException {
// 创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定监听的端口
ssc.bind(new InetSocketAddress(8080));
// 建立连接
while (true){
// 建立与客户端的连接,channel用来与客户端建立通讯
System.out.println("服务器启动完成..");
SocketChannel channel = ssc.accept(); // 阻塞方法,等待连接
System.out.println("请求到达..");
CHANNELS.add(channel);
for (SocketChannel c : CHANNELS) {
// 读取
c.read(buf);// 连接建立以后阻塞,客户端如果不发送数据则会阻塞,等待客户端发送数据
buf.flip();
while (buf.hasRemaining()){
byte b = buf.get();
System.out.println((char)b);
}
buf.clear();
}
}
}
}
/**
* 创建网络客户端
*/
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel clientSc = SocketChannel.open();
clientSc.connect(new InetSocketAddress("localhost",8080));
System.out.println("waiting....");
}
}
非阻塞
public class Server {
// 用于存放连接
public static LinkedList<SocketChannel> CHANNELS = new LinkedList<>();
public static ByteBuffer buf =ByteBuffer.allocate(8196);
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
// 服务器通道设置非阻塞模式。影响下方{ssc.accept()},服务器通道建立时非阻塞
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
while (true){
System.out.println("服务器启动完成..");
// 设置非阻塞以后到这不会等待,会继续向下执行,如果没有连接则SocketChannel为null
SocketChannel socketChannel = ssc.accept();
// 连接通道设置非阻塞,影响下方{channel.read(buf);},网络连接建立时非阻塞
socketChannel.configureBlocking(false);
System.out.println("请求到达..");
CHANNELS.add(socketChannel);
// 轮流遍历客户端,读取客户端发送的数据
for (SocketChannel channel : CHANNELS) {
channel.read(buf);
buf.flip();
while (buf.hasRemaining()){
byte b = buf.get();
System.out.println((char)b);
}
buf.clear();
}
}
}
}
Selector
事件类型
- accept : ServerSocket连接请求建立时触发
- connect : 客户端连接建立后触发的事件
- read : 客户端发送数据,数据到达时触发的事件
- write : 可写事件
示例代码
public class AddSelector {
public static void main(String[] args) throws IOException {
// 1.创建selector
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 2.把Channel注册到Select;;SelectionKey 事件发生时通过它可以知道是哪个channle发生的事件
SelectionKey sscKey = ssc.register(selector, 0, null);
// 3.设置专注类型,这里我们就关注连接建立事件,
// 由于SelectionKey不但serverSocketChannel可以使用,socketChannel也可以使用,所以这里也有他们的事件
sscKey.interestOps(SelectionKey.OP_ACCEPT); // 也可以在上面ssc.register时指定
ssc.bind(new InetSocketAddress(8080)); // 启动端口
System.out.println("服务器启动完成....");
System.out.println("key:"+sscKey);
while (true){
// 执行select方法,虽然select依然是阻塞的,但是只要有上面的事件发生就会让线程恢复执行
selector.select();
// 处理事件,拿到所有发生的可用事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 然后遍历处理,由于我们需要在执行后删除事件,所以我们需要用迭代器遍历,不能使用增强for
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()){
SelectionKey key = iter.next(); // 通过它就知道是哪个channle发生的事件
System.out.println(key);
ServerSocketChannel channel = (ServerSocketChannel)key.channel(); // 强转拿到ServerChannel
SocketChannel socketChannel = channel.accept(); // 建立连接,建立连接以后就会消除事件
}
}
}
}