文章目录
-
- 网络IO(13)
-
- IO/BIO
- NIO
- 文件拷贝(22)
-
- BIO
- NIO
- Files.copy
- 拷贝性能
网络IO(13)
IO/BIO
1、Socket简单实现客户端和服务端通信
1-服务端:建立ServerSocket,等待客户端连接,然后处理数据。
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;public class DemoSocketServer extends Thread{
private ServerSocket serverSocket;//获取端口号public int getPort(){
return serverSocket.getLocalPort();}public void run(){
try {
//serverSocket相当于大门,绑定一个门牌号(端口)serverSocket=new ServerSocket(0);while (true){
//创建一个socket,相当于在大门口迎接的服务员,//没有客户来,就一直等着(阻塞)//当有客户端来链接时,就为其服务System.out.println("等待连接....");Socket socket=serverSocket.accept();System.out.println("已连接");// 处理客户端(新建一个线程)RequestHandler requestHandler = new RequestHandler(socket);requestHandler.start();}}catch (IOException e){
e.printStackTrace();}finally {
//任何情况下都要关闭连接if(serverSocket!=null){
try {
serverSocket.close();}catch (IOException e){
e.printStackTrace();}}}}public static class RequestHandler extends Thread{
private Socket mSocket;RequestHandler(Socket socket){
mSocket = socket;}@Overridepublic void run() {
try {
// 1、Socket的输出流来创建printWriterPrintWriter printWriter = new PrintWriter(mSocket.getOutputStream());// 2、写入数据printWriter.println("Hello World!");//清空输出流printWriter.flush();} catch (IOException e) {
e.printStackTrace();}}}
}
2-客户端(简单的打印数据):借助try-with-resources,用Reader去读取数据。
public class client {
public static void main(String[] args) throws IOException {
DemoSocketServer server = new DemoSocketServer();server.start();// 1、Socket客户端,绑定Server端Host地址,和Server端的端口。(这边是本机)try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
// 2、通过客户端的inpustream,创建ReaderBufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));// 3、从Reader中读取到数据,并且打印。String s=null;while ((s=bufferedReader.readLine())!=null)System.out.println(s);}}
}
执行结果:
2、线程池改进服务端
需要减少线程频繁创建和销毁的开销
// 线程池
private Executor mExecutor;
@Override
public void run() {
try {
serverSocket = new ServerSocket(0);// 1、创建线程池:只有核心线程数,没有非核心线程数。任务队列无限。空闲线程会立即停止mExecutor = Executors.newFixedThreadPool(8);while (true){
Socket socket = serverSocket.accept();RequestHandler requestHandler = new RequestHandler(socket);// 2、线程池进行处理mExecutor.execute(requestHandler);}} catch (IOException e) {
e.printStackTrace();}finally {
// 任何情况下都要保障Socket资源关闭。if(serverSocket != null){
try {
serverSocket.close();} catch (IOException e) {
e.printStackTrace();}}}
}
3、服务端采用线程池来提供服务的典型工作模式图
4、服务端采用线程池处理客户端连接的缺点?
- 连接数几百时,这种模式没有问题。但是在高并发,客户端及其多的情况下,就会出现问题。
- 线程上下文切换的开销会在高并发时非常明显。
- 这就是同步阻塞方式的低扩展性的体现
NIO
5、NIO优化服务端连接问题的实例
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;public class NIOServer extends Thread {
public void run(){
try {
//1、创建selector,调度员Selector selector=Selector.open();//2、ServerSocketChannel,相当于IO中的SocketServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 1. 绑定IP和端口serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));// 2. 非阻塞模式。因为阻塞模式下是不允许注册的。serverSocketChannel.configureBlocking(false);/**-------------------------* 3、向Selector进行注册。通过OP_ACCEPT,表明关注的是新的连接请求* 相当于IO中 serverSocket向Socket注册*---------------------------*/System.out.println("等待连接");serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while(true){
// 4、Selector调度员,阻塞在select操作。当有Channel有接入请求时,会被唤醒selector.select();System.out.println("已连接");// 5、被唤醒,获取到事件已经被捕获的SelectionKey的集合Set<SelectionKey> selectionKeys = selector.selectedKeys();System.out.println("已连接"+selectionKeys.size()+"个客户端");Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();// 6、从SelectionKey中获取到对应的ChannelhandleRequest((ServerSocketChannel) selectionKey.channel());iterator.remove();}}}catch (IOException e){
e.printStackTrace();}}// 处理客户端的请求private void handleRequest(ServerSocketChannel socketChannel){
// 1、获取到连接到该Channel Socket的连接try(SocketChannel client = socketChannel.accept()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 2、从客户端读取数据client.read(byteBuffer);// 3、翻转,用于读取显示和发送给服务器byteBuffer.flip();System.out.println("receive msg from NIOClient: "+ Charset.defaultCharset().decode(byteBuffer.asReadOnlyBuffer()).toString());byteBuffer.clear();byteBuffer=Charset.defaultCharset().encode("I got it!");// 4、向客户端中写数据client.write(byteBuffer);} catch (IOException e) {
e.printStackTrace();}}
}
测试
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;public class NIOClient {
public static void main(String[] args)throws IOException {
//开启服务器NIOServer server=new NIOServer();server.start();//1.创建客户端try (Socket client=new Socket(InetAddress.getLocalHost(),8888)){
//2.写入数据BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));bufferedWriter.write("I'm Client!");bufferedWriter.flush();// 3.读取数据BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));System.out.println("receive msg from server: "+bufferedReader.readLine());}}
}
执行结果:
6、NIO在Socket编程上的优势
- 使用非阻塞的IO方式
- 支持IO多路复用
- 这些特性改变了传统网络编程中一个线程只能管理一个链接的情况,现在可以采用一个线程管理多个链接。
7、NIO为什么比IO同步阻塞模式要更好?
- 同步阻塞模式需要多线程来处理多任务。
- NIO利用了单线程轮询事件的机制,高效定位就绪的Channel。
- 仅仅是select阶段是阻塞的,可以避免大量客户端连接时,频繁切换线程带来的问题。
8、NIO实现网络通信的工作模式图
9、NIO能解决什么问题?
- 服务端多线程并发处理任务,即使使用线程池,高并发处理依然会因为上下文切换,导致性能问题。
- NIO是利用单线程轮询事件的机制,高效的去选择来请求连接的Channel仅提供服务。
10、NIO的请求接收和处理都是在一个线程处理,如果有多个请求的处理顺序是什么?
- 多个请求会按照顺序处理
- 如果一个处理具有耗时操作,会阻塞后续操作。
11、NIO是否应该在服务端开启多线程进行处理?
我觉得是可以的(转载作者说的)
我觉得因为CPU的时间片轮转机制,一个线程只有有限的时间来使用cpu,利用多线程可以更高效的榨取CPU性能(我说的)
12、NIO遇到大量耗时操作该怎么办?
如果有大量耗时操作,那么整个NIO模型就不适用于这种场景。??感觉可以开多线程。 过多的耗时操作,可以采用传统的IO方式。
13、selector在单线程下的处理监听任务会成为性能瓶颈?
- 是的。单线程中需要依次处理监听。会导致性能问题。
- 在并发数数万、数十万的情况下,会导致性能问题。
- Doug Lea推荐使用多个selector,在多个线程中并发监听Socket事件
文件拷贝(22)
1、Java有几种文件拷贝方式?哪一种效率最高?
- 利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建一个FileOutputStream,完成写入工作。
public static void copyFileByStream(File source, File dest) throwsIOException {
try (InputStream is = new FileInputStream(source);OutputStream os = new FileOutputStream(dest);){
byte[] buffer = new byte[1024];int length;while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);}}}
- 利用 java.nio 类库提供的 transferTo 或 transferFrom 方法实现。
public static void copyFileByChannel(File source, File dest) throwsIOException {
try (FileChannel sourceChannel = new FileInputStream(source).getChannel();FileChannel targetChannel = new FileOutputStream(dest).getChannel();){
for (long count = sourceChannel.size() ;count>0 ;) {
long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel); sourceChannel.position(sourceChannel.position() + transferred);count -= transferred;}}}
3.Java 标准类库本身已经提供了几种 Files.copy 的实现。
对于 Copy 的效率,这个其实与操作系统和配置等情况相关,总体上来说,NIO transferTo/From的方式可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。
2、不同的文件复制方式,底层机制有什么不同?
- 当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。写入操作也是类似,仅仅是步骤相反,你可以参考下面这张图。
所以,这种方式会带来一定的额外开销,可能会降低 IO 效率。
- 而基于 NIO transferTo 的实现方式,在 Linux 和 Unix
上,则会使用到零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。注意,transferTo
不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行 Socket
发送,同样可以享受这种机制带来的性能和扩展性提高。transferTo 的传输过程是:
2,3问题回答来自极客时间
3、用户态空间是什么?
用户态空间-User Space 用户态空间是普通应用和服务所使用的
4、内核态空间是什么?
- 内核态空间-Kernel Space
- 操作系统内核、硬件驱动等都运行在内核态空间
5、读写操作时的上下文切换是什么?
- 就是内核态和用户态之间的切换。
- 使用输入流、输出流进行读写操作时,就在进行用户态和内核态之间的上下文切换。
- 当读取数据时,会切换至内核态将数据从磁盘读取到内核缓存。然后再切换到用户态,将数据从内核缓存读取到用户缓存。
- 写入操作类似, 仅仅是方向相反。
6、读写操作时的性能问题是什么?如何去解决?
- 读写操作时,进行用户态和内核态的上下文切换,会带来额外的开销,从而降低IO效率。
- 基于NIO transferTo的实现方式,在Linux和Unix上,会用到零拷贝技术。
- 数据传输不再需要用户态参与,节省了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。
7、为什么零拷贝(zero-copy)可能有性能优势?
- 不再需要用户态和内核态的切换
- 减少了从内核缓存拷贝到用户缓存的这些不必要的内存拷贝。
- 原来是4次拷贝: 磁盘->内核缓存,内核缓存->用户缓存,用户缓存->内核缓存,内核缓存->磁盘
- 零拷贝只有2次拷贝: 磁盘->内核缓存,内核缓存->磁盘
8、NIO transferTo的实现方式
- 会采用零拷贝技术
- transferTo不仅仅用在文件拷贝中,也能用于读取磁盘文件、然后Socket进行发送。同样性能有很大优势。
9、为什么copy要设计成需要进行上下文切换的方式?为什么不和nio的transfer一样设计为不需要用户态切换的开销?
- 大部分工作需要用户态。
- transfer是特定场景而不是通用场景
BIO
10、利用java.io的InputStream和OutputStream进行文件拷贝
1-实现文件拷贝
/**========================================* java.io: 实现文件复制* @param src 源文件* @param dst 目标文件*=======================================*/
public static void copyFileByIO(File src, File dst){
try(InputStream inputStream = new FileInputStream(src);OutputStream outputStream = new FileOutputStream(dst)){
byte[] buffer = new byte[1024];int length;// 读取数据到byte数组中,然后输出到OutStream中while((length = inputStream.read(buffer)) > 0){
outputStream.write(buffer, 0, length);}} catch (FileNotFoundException e) {
e.printStackTrace();} catch (IOException e) {
e.printStackTrace();}
}
2-测试程序
File src = new File("D:\\src.txt");
File dst = new File("D:\\dst.txt");
copyFileByIO(src, dst);
NIO
11、利用NIO实现文件的拷贝
/**===================================* java.nio: 实现文件复制* @param src 源文件* @param dst 目标文件*===============================*/
public static void copyFileByChannel(File src, File dst){
// 1、获取到源文件和目标文件的FileChanneltry(FileChannel srcFileChannel = new FileInputStream(src).getChannel();FileChannel dstFileChannel = new FileOutputStream(dst).getChannel()){
// 2、当前FileChannel的大小long count = srcFileChannel.size();while(count > 0){
/**=============================================================* 3、从源文件的FileChannel中将bytes写入到目标FileChannel中* 1. srcFileChannel.position(): 源文件中开始的位置,不能为负* 2. count: 转移的最大字节数,不能为负* 3. dstFileChannel: 目标文件*==============================================================*/long transferred = srcFileChannel.transferTo(srcFileChannel.position(),count, dstFileChannel);// 4、传输完成后,更改源文件的position到新位置srcFileChannel.position(srcFileChannel.position() + transferred);// 5、计算出剩下多少byte需要传输count -= transferred;}} catch (FileNotFoundException e) {
e.printStackTrace();} catch (IOException e) {
e.printStackTrace();}
}
测试
File src = new File("D:\\src.txt");
File dst = new File("D:\\dst.txt");
// nio拷贝
copyFileByChannel(src, dst);
12、Nio的transferTo一定会快于BIO吗?
Files.copy
13、利用Files.copy()进行文件拷贝
Path srcPath = Paths.get("D:\\src.txt");
Path dstPath = Paths.get("D:\\dst.txt");
try {
// 进行拷贝,CopyOption参数可以没有Files.copy(srcPath, dstPath);
} catch (IOException e) {
e.printStackTrace();
}
14、Files.copy()有哪4种方法?
1-文件间进行copy
public static Path copy(Path source, Path target, CopyOption... options);
2-从输入流中copy到文件中
public static long copy(InputStream in, Path target, CopyOption... options);
3-从文件中copy到输出流
public static long copy(Path source, OutputStream out);
4-从输入流copy到输出流中
private static long copy(InputStream source, OutputStream sink);
15、Path copy(Path, Path, CopyOption…)源码分析
public static Path copy(Path source, Path target, CopyOption... options)
{
FileSystemProvider provider = provider(source);if (provider(target) == provider) {
// same provider-同种文件系统拷贝provider.copy(source, target, options);} else {
// different providers-不同文件系统拷贝CopyMoveHelper.copyToForeignTarget(source, target, options);}return target;
}
从文件系统中最终会定位到UnixCopyFile.c 根据内部实现的说明,这只是简单的用户态空间拷贝。 因此这个copy(path,path)并不是利用transferTo,而是本地技术实现的用户态拷贝
16、FileSystemProvider是什么?如何提供文件系统的?
- 文件系统的服务的提供者
- 是一个抽象类
- 内部通过ServiceLoader机制加载一系列文件系统,然后提供服务。
- 文件系统的实际逻辑是在JDK内部实现的,可以在JDK源码中搜索FileSystemProvider、nio,可以定位到sun/nio/fs,因为NIO底层和操作系统紧密相连,所以每个平台都有自己部分特有的文件系统。
- Unix平台上会定位到UnixFileSystemProvider->UnixCopyFile.Transfer->UnixCopyFile.c
// FileSystemProvider.java
private static List<FileSystemProvider> loadInstalledProviders() {
//加载所有已经安装的文件系统服务的提供者List<FileSystemProvider> list = new ArrayList<FileSystemProvider>();ServiceLoader<FileSystemProvider> sl = ServiceLoader.load(FileSystemProvider.class, ClassLoader.getSystemClassLoader());//xxx
}
17、平台特有的文件系统服务提供者: FileSystemProvider
18、copy(InputStream, OutputStream)源码分析
直接进行inoputstream和outputstream的read和write操作,本质和一般IO操作是一样的。
也就是用户态的读写
private static long copy(InputStream source, OutputStream sink)
{
long nread = 0L;byte[] buf = new byte[BUFFER_SIZE];int n;while ((n = source.read(buf)) > 0) {
sink.write(buf, 0, n);nread += n;}return nread;
}
19、copy(InputStream, Path, CopyOption…)源码分析
本质调用的copy(InputStream, OutputStream)方法
public static long copy(InputStream in, Path target, CopyOption... options)
{
// xx省略代码xxObjects.requireNonNull(in);OutputStream ostream;// 1、通过target的Path获取到OutputStreamostream = newOutputStream(target, StandardOpenOption.CREATE_NEW,StandardOpenOption.WRITE);// 2、然后还是直接调用copy(InputStream, OutpuStream)的方法进行数据拷贝。try (OutputStream out = ostream) {
return copy(in, out);}
}
20、copy(Path, OutputStream)源码分析
本质调用的copy(InputStream, OutputStream)方法
public static long copy(Path source, OutputStream out) throws IOException {
//1、输出流不为NullObjects.requireNonNull(out);//2、本质调用的copy(InputStream, OutputStream)方法try (InputStream in = newInputStream(source)) {
return copy(in, out);}
}
21、JDK10中Files.copy()实现的轻微改变:InputStream.transferTo
- copy(Path, Path, CopyOption…)内部机制没有变化
- 剩余copy()方法,将输入流、输出流的读写封装到了方法中:InputStream.transferTo(),也就是处于用户态的读写
public long transferTo(OutputStream out) throws IOException {
Objects.requireNonNull(out, "out");long transferred = 0;byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];int read;//IO读写操作while ((read = this.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
out.write(buffer, 0, read);transferred += read;}return transferred;
}
拷贝性能
22、如何提升类似拷贝等IO操作的性能?
- 合理使用缓存等机制,合理减少IO次数
- 使用transferTo等机制,减少上下文切换和额外的IO操作
- 减少不必要的转换过程, 如:
- 编解码
- 对象序列化和反序列化
- 操作文本文件或者网络通信,如果不是要使用文本信息,可以直接传输二进制信息。而不是传输字符串。