1.IO模型的基本说明
IO模型就是用什么样的通道或者说是通信模式和架构进行数据的传输和接收,很大程度上决定了程序通信的性能
(1)BIO
同步并阻塞(传统阻塞型),服务器实现模式为一个客户端连接一个线程,即客户端有连接请求时服务器端就需要
启动一个线程进行处理。当客户端不传送数据时,这个线程也需要等待,那么就会造成不必要的线程开销。
一般适用于客户端数量较少并且固定的架构,对服务器资源的要求比较高
![](https://img-blog.csdnimg.cn/46aa0c06df0c42d5bdaf4e863ca0e137.png)
(2)NIO
同步非阻塞,服务器实现模式为一个线程处理多个请求,即客户端发送的连接请求都会注册到多路复用器上,
多路复用器轮询到连接有IO请求就进行处理
适用于连接数目多且连接比较短的架构
![](https://img-blog.csdnimg.cn/bba3cf6b134046968ef229c7d4994c8d.png)
(3)AIO
异步非阻塞,服务器实现模式为:一个有效请求一个线程,客户端的IO请求都是由OS先完成了再通知服务器
应用去启动线程进行处理
一般适用于连接数较多且连接时间较长的应用
2.BIO
![](https://img-blog.csdnimg.cn/b2393d60d4ff4fcd8657abb191b13647.png)
(1)同步阻塞演示(客户端发送数据,服务端接收数据)
服务端会一直等待客户端的消息,如果客户端没有进行消息的发送,服务端将一直进入阻塞状态
public class Server {
public static void main(String[] args) {
try {
System.out.println("=======服务端启动=======");
ServerSocket ss = new ServerSocket(9999);
Socket socket=ss.accept();
InputStream is = socket.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(is));
String msg;
if((msg=br.readLine())!=null){
System.out.println("服务端接收到:"+msg);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class Client {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1",9999);
OutputStream os = socket.getOutputStream();
PrintStream ps=new PrintStream(os);
ps.println("hello 你好");
ps.flush();
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
}
}
}
(2)多发多收机制
public class Server {
public static void main(String[] args) {
try {
System.out.println("=======服务端启动=======");
ServerSocket ss = new ServerSocket(9999);
Socket socket=ss.accept();
InputStream is = socket.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg=br.readLine())!=null){
System.out.println("服务端接收到:"+msg);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class Client {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1",9999);
OutputStream os = socket.getOutputStream();
PrintStream ps=new PrintStream(os);
Scanner sc=new Scanner(System.in);
while (true){
System.out.print("请输入:");
String msg=sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
(3)接收多个客户端
(1)每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能
(2)每个线程都会占用栈空间和CPU资源
(3)并不是每个socket都进行IO操作,无意义的线程处理
(4)客户端的并发访问增加时,服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,
线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务
public class ServerThreadReader extends Thread{
private Socket socket;
public ServerThreadReader(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg=br.readLine())!=null){
System.out.println("服务端接收到:"+msg);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class Server {
public static void main(String[] args) {
try {
System.out.println("=======服务端启动=======");
ServerSocket ss = new ServerSocket(9999);
while (true){
Socket socket=ss.accept();
new ServerThreadReader(socket).start();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class Client1 {
public static void main(String[] args) {
CreateClient createClient = new CreateClient();
createClient.client();
}
}
public class Client2 {
public static void main(String[] args) {
CreateClient createClient = new CreateClient();
createClient.client();
}
}
public class CreateClient {
public void client(){
try {
Socket socket = new Socket("127.0.0.1",9999);
PrintStream ps=new PrintStream(socket.getOutputStream());
Scanner sc=new Scanner(System.in);
while (true){
System.out.print("请输入要发送给服务端的数据: ");
String msg=sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
(4)伪异步IO
采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现Runnable线程
任务接口)交给后端的线程池中进行处理。由于线程池可以设置任务队列的大小和最大线程数,因此,它占用的
资源是可控的,无论多少个客户端并发访问,都不会导致资源地耗尽和宕机。
![](https://img-blog.csdnimg.cn/b5a65e0a8bbd449e91989b7cfda3be51.png)
public class Server {
public static void main(String[] args) {
try {
ServerSocket ss=new ServerSocket(9999);
HandlerSocketServerPool pool=new HandlerSocketServerPool(3,5);
while (true){
Socket socket=ss.accept();
Runnable target=new ServerRunnableTarget(socket);
pool.execute(target);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public class ServerRunnableTarget implements Runnable{
private Socket socket;
public ServerRunnableTarget(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg=br.readLine())!=null){
System.out.println("服务端接收到:"+msg);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class HandlerSocketServerPool {
private ExecutorService executorService;
public HandlerSocketServerPool(int maxThreadNums,int queueSize){
executorService=new ThreadPoolExecutor(3, maxThreadNums,
120, TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueSize));
}
public void execute(Runnable target){
executorService.execute(target);
}
}
public class Client {
public static void main(String[] args) {
CreateClient createClient = new CreateClient();
createClient.client();
}
}
public class CreateClient {
public void client(){
try {
Socket socket = new Socket("127.0.0.1",9999);
PrintStream ps=new PrintStream(socket.getOutputStream());
Scanner sc=new Scanner(System.in);
while (true){
System.out.print("请输入要发送给服务端的数据: ");
String msg=sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
(5)文件上传
public class Service {
public static void main(String[] args) {
try{
ServerSocket ss=new ServerSocket(9999);
while (true){
Socket socket=ss.accept();
new ServerReaderThread(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket=socket;
}
@Override
public void run() {
OutputStream os=null;
try {
DataInputStream dis=new DataInputStream(socket.getInputStream());
String suffix=dis.readUTF();
System.out.println("服务端已经接收到了文件类型");
os=new FileOutputStream("D:\\io\\111\\"+ UUID.randomUUID().toString()+suffix);
byte[] buffer=new byte[1024];
int len;
while ((len=dis.read(buffer))!=-1){
os.write(buffer,0,len);
}
System.out.println("服务端接收文件保存成功");
}catch (Exception e){
e.printStackTrace();
}finally {
try {
os.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
public class Client {
public static void main(String[] args) {
try(InputStream is=new FileInputStream("D:\\img\\1.jpg");)
{
Socket socket=new Socket("127.0.0.1",9999);
DataOutputStream dos=new DataOutputStream(socket.getOutputStream());
dos.writeUTF(".jpg");
byte[] buffer=new byte[1024];
int len;
while ((len=is.read(buffer))!=-1){
dos.write(buffer,0,len);
}
dos.flush();
socket.shutdownOutput();
}catch (Exception e){
e.printStackTrace();
}
}
}
(6)端口转发
服务端将一个客户端的消息发送给其它客户端
![](https://img-blog.csdnimg.cn/fb3c4c9101a540bcbd2cec4a29743fc5.png)
public class Server {
public static List<Socket> allSocketOnline=new ArrayList<>();
public static void main(String[] args) {
try {
ServerSocket ss=new ServerSocket(9999);
while (true){
Socket socket=ss.accept();
allSocketOnline.add(socket);
new ServerReaderThread(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try {
BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
while ((msg=br.readLine())!=null){
sendMsgToAllClient(msg);
}
}catch (Exception e){
System.out.println("当前有人下线");
Server.allSocketOnline.remove(socket);
}
}
private void sendMsgToAllClient(String msg) throws IOException {
for (Socket sock : Server.allSocketOnline) {
PrintStream ps=new PrintStream(sock.getOutputStream());
ps.println(msg);
ps.flush();
}
}
}
(7)即时通讯项目
解决客户端到客户端的通信
public class ServerChat {
public static Map<Socket,String> onLineSockets=new HashMap<>();
public static void main(String[] args) {
try {
ServerSocket ss=new ServerSocket(9999);
while (true){
Socket socket=ss.accept();
new ServerReader(socket).start();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public class ServerReader extends Thread{
private Socket socket;
public ServerReader(Socket socket){
this.socket=socket;
}
@Override
public void run() {
DataInputStream dis=null;
try {
dis=new DataInputStream(socket.getInputStream());
while (true){
int flag=dis.readInt();
if (flag==1){
String name= dis.readUTF();
System.out.println(name+"---->"+socket.getRemoteSocketAddress());
ServerChat.onLineSockets.put(socket,name);
}
writeMsg(flag,dis);
}
}catch (Exception e){
System.out.println("---有人下线了---");
ServerChat.onLineSockets.remove(socket);
try {
writeMsg(1,dis);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}finally {
try {
dis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void writeMsg(int flag, DataInputStream dis) throws IOException {
String msg=null;
if (flag==1){
StringBuilder rs=new StringBuilder();
Collection<String> onlineNames=ServerChat.onLineSockets.values();
if (onlineNames!=null&&onlineNames.size()>0){
for (String name:onlineNames){
rs.append(name+ Constants.SPILIT);
}
msg=rs.substring(0,rs.lastIndexOf(Constants.SPILIT));
sendMsgToAll(flag,msg);
}
} else if (flag==2||flag==3) {
String newMsg= dis.readUTF();
String sendName=ServerChat.onLineSockets.get(socket);
StringBuilder msgFinal=new StringBuilder();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE");
if (flag==2){
msgFinal.append(sendName).append(" ").append(sdf.format(System.currentTimeMillis()*2)).append("\r\n");
msgFinal.append(" ").append(newMsg).append("\r\n");
sendMsgToAll(flag,msgFinal.toString());
} else if (flag==3) {
msgFinal.append(sendName).append(" ").append(sdf.format(System.currentTimeMillis()*2)).append("对您私发\r\n");
msgFinal.append(" ").append(newMsg).append("\r\n");
String destName= dis.readUTF();
sendMsgToOne(destName,msgFinal.toString());
}
}
}
private void sendMsgToOne(String destName, String msg) throws IOException {
Set<Socket> allOnlineSockets=ServerChat.onLineSockets.keySet();
for (Socket sk:allOnlineSockets){
if (ServerChat.onLineSockets.get(sk).trim().equals(destName)){
DataOutputStream dos=new DataOutputStream(sk.getOutputStream());
dos.writeInt(2);
dos.writeUTF(msg);
dos.flush();
}
}
}
private void sendMsgToAll(int flag, String msg) throws IOException {
Set<Socket> allOnlineSockets=ServerChat.onLineSockets.keySet();
for (Socket sk:allOnlineSockets){
DataOutputStream dos=new DataOutputStream(sk.getOutputStream());
dos.writeInt(flag);
dos.writeUTF(msg);
dos.flush();
}
}
}
public class Constants {
public static final int PORT=7778;
public static final String SPILIT="$%&12&";
}
3.NIO
(1)NIO支持面向缓冲区的、基于通道的IO操作,以更加高效的方式进行文件的读写操作。
(2)可以理解为非阻塞IO
(3)以块的方式处理数据,效率高
(1)三大核心(通道、缓冲区、选择器)
(1)缓冲区:本质是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer
对象,并提供了一组方法,用来方便访问该块内存。
(2)通道:既可以从通道中读取数据,又可以写数据到通道。通道可以非阻塞读取和写入通道,通道可以
支持读取或写入缓冲区,也支持异步的读写
(3)选择器:可以检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。一个单独的
线程可以管理多个通道,从而管理多个网络连接,提高效率。
![](https://img-blog.csdnimg.cn/c33a2f1297264117a7b25dced3b9eccb.png)
(2)缓冲区(Buffer)
(1)保存基本数据类型
基本属性
(1)容量(capacity):作为一个内存块,Buffer具有一定的固定大小,也称为容量,缓冲区容量
不能为负,并且创建后不能更改
(2)限制(limit):表示缓冲区中可以操作数据的大小(limit后的数据不能进行读写)。缓冲区的限制
不能为负,并且不能大于其容量。写入模式:限制等于Buffer的容量;读取模式:limit等于写入
的数据量
(3)位置(position):下一个要读取或写入的数据的索引,位置不能为负并且不能大于其限制
(4)标记(mark)与重置(reset):标记是一个索引,通过Buffer中的mark()指定Buffer中一个
特定的position,之后可以通过调用reset()方法恢复到这个position
0 <= mark <= position <= limit <= capacity
常用API
(1)XxxBuffer.allocate(10):分配一个Xxx类型的缓冲区,容量设置为10
XxxBuffer可以是ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、
LongBuffer、 ShortBuffer、MappedByteBuffer
(1)clear():清空缓冲区并返回对缓冲区的引用
(2)flip():将缓冲区的界限设置为当前位置,并将当前位置设置为0
(3)capacity():返回缓冲区的大小
(4)hasRemaining():判断缓冲区中是否还有元素
(5)limit():返回缓冲区的界限位置
(6)mark():对缓冲区设置标记
(7)position():返回缓冲区的当前位置
(8)position(int n):设置缓冲区的当前位置为n,并返回修改后的Buffer对象
(9)remaining():返回position和limit之间的元素个数
(10)reset():将位置position转到以前设置mark所在位置
(11)rewind():将位置设为0,取消设置的mark
(12)get():获取缓冲区中的数据
get():读取单个字节
get(byte[] dst):批量读取多个字节到dst中
get(int index):读取指定索引位置的字节(不会移动position)
(13)put():放入数据到缓冲区中
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将src中的字节写入缓冲区的当前位置
put(int index,byte b):将指定字节写入缓冲区的索引位置(不会移动position)
直接与非直接缓冲区
(1)直接内存缓冲区:使用 XxxBuffer.allocateDirect(1024) 来分配的缓冲区就在直接内存
可以使用 isDirect() 来确定是否是直接缓冲区, 如果返回true的话就是直接缓冲区
(2)直接缓冲区:基于直接内存(非堆内存),JVM将在IO操作上具有更高的性能,因为它直接作用于
本地系统的IO操作。
本地IO -> 直接内存 -> 本地IO
(3)非直接缓冲区:基于堆内存,如果要做IO操作,会先从本进程内存复制到直接内存,再利用本地
IO处理
本地IO -> 直接内存 -> 堆内存 -> 直接内存 -> 本地IO
(4)在做IO处理时,比如网络发送大量数据时,直接内存具有更高的效率。但是它比申请普通的堆内存
需要耗费更高的性能。所以当有很大的数据需要缓存,并且生命周期又很长,或者需要频繁的IO操作
(比如网络并发场景)那么就比较适合使用直接内存
(3)通道(channel)
(1)channel本身不能直接访问数据,channel只能与Buffer进行交互
(2)通道可以同时进行读写,而流只能读或者只能写
(3)通道可以实现异步读写数据
(4)通道可以从缓冲区读数据,也可以写数据到缓冲
(5)channel在NIO中是一个接口,需要使用它的实现类
FileChannel:用于读取、写入、映射和操作文件的通道
DatagramChannel:通过UDP读写网络中的数据通道
SocketChannel:通过TCP读写网络中的数据
ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个
ServerSocketChannel
FileChannel的常用方法
(1)read(ByteBuffer dst):从channel中读取数据到ByteBuffer
(2)read(ByteBuffer[] dsts):将channel中的数据分散到ByteBuffer[]
(3)write(ByteBuffer src):将ByteBuffer 中的数据写入到channel中
(4)write(ByteBuffer[] srcs):将ByteBuffer[] 中的数据聚集到channel
(5)position():返回此通道的文件位置
(6)FileChannel position(long p):设置此通道的文件位置
(7)size():返回此通道的文件的当前大小
(8)FileChannel truncate(long s):将此通道的文件截取为给定大小
(9)force(boolean metaData):强制将所有对此通道的文件更新写入到存储设备中
(10)transferFrom(原通道,开始位置,大小):从哪个通道复制数据
transferTo(开始位置,大小,目标通道):将数据写入哪个通道
public class Test1 {
@Test
public void write(){
try {
FileOutputStream os=new FileOutputStream("hello.txt");
FileChannel channel = os.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello,welcome to China".getBytes());
buffer.flip();
channel.write(buffer);
channel.close();
System.out.println("写入成功");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test
public void read(){
try {
FileInputStream is=new FileInputStream("hello.txt");
FileChannel channel = is.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();
String s=new String(buffer.array(),0,buffer.remaining());
System.out.println(s);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test
public void copy() throws Exception {
File fileIn=new File("hello.txt");
File fileOut=new File("hi.txt");
FileInputStream is=new FileInputStream(fileIn);
FileOutputStream os=new FileOutputStream(fileOut);
FileChannel isChannel = is.getChannel();
FileChannel osChannel = os.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true){
int flag = isChannel.read(buffer);
if (flag==-1){
break;
}
}
buffer.flip();
osChannel.write(buffer);
isChannel.close();
osChannel.close();
System.out.println("复制成功");
}
@Test
public void more() throws Exception {
FileInputStream is=new FileInputStream("hello.txt");
FileOutputStream os=new FileOutputStream("hi.txt");
FileChannel isChannel = is.getChannel();
FileChannel osChannel = os.getChannel();
ByteBuffer buffer1 = ByteBuffer.allocate(4);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
ByteBuffer[] buffers={buffer1,buffer2};
isChannel.read(buffers);
for (ByteBuffer buffer:buffers){
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.remaining()));
}
osChannel.write(buffers);
isChannel.close();
osChannel.close();
System.out.println("success");
}
}
(4)选择器(Selector)
(1)选择器是SelectableChannel对象的多路复用器,Selector可以同时监控多个SelectableChannel
的IO状况,利用selector可使一个单独的线程管理多个通道,是非阻塞IO的核心
(2)只有在通道真正有读写事件发生时,才会进行读写,就大大的减少了系统开销,并且不必为每个
连接都创建一个线程,不用去维护这个线程
(3)避免了多线程之间的上下文切换导致的开销
选择器的应用
Selector selector=Selector.open();
ServerSocketChannel ssChannel=ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.bind(new InetSocketAddress(9898));
Selector selector=Selector.open();
ssChannel.register(selector,SelectionKey.OP_ACCEPT);
当调用register将通道注册到选择器上时,选择器对通道的监听事件需要第二个参数指定
可以监听的事件类型:
(1)读:SelectionKey.OP_READ
(2)写:SelectionKey.OP_WRITE
(3)连接:SelectionKey.OP_CONNECT
(4)接收:SelectionKey.OP_ACCEPT
若注册时不止监听一个事件,则可以使用"位或"操作符连接
4.AIO
(1)异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由OS先完成了再通知
服务器应用去启动线程进行处理
(2)与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可,这两种方法均为异步
的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区;对于写操作而言,
当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序