BIO、NIO、AIO 分别实现多人聊天
DNS就是一个域名系统,它本质上就是一个分布式数据库
io流简述
节点流
字节流是8位的,字符流是16位的。
CharArrayReader:从一个char数组中读取流;
StringReader:从一个字符串中读取流。
Writer也是同理。
这些流是运行在基本流之上的
InputStreamReader / OutputStreamWriter 是将字节流转换成字符流,然后再用于其他操作。
比如说BufferedReader,他是运行在FileReader之上的
FileWriter writer = new FileWriter(file);
BufferedWriter bufferedWriter = new BufferedWriter(writer);
InputStreamReader,就是将字节流转换成字符流,然后通过他的子类FileReader读取进来。
网络编程
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。
在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口,Socket负责发起连接操作,连接成功之后,双方通过输入和输出流进行同步阻塞式通信。
网络编程模型有:同步、异步、阻塞、非阻塞
同步阻塞:客户端向服务端发送消息,客户端再等待服务端的回应,服务端也正在处理客户端发来的信息,处理完毕后回应客户端;
同步非阻塞:客户端向服务端发送消息,客户端不需要等待服务端的回应,而服务端正在处理客户端发来的信息,处理完毕后回应客户端;
异步阻塞:客户端向服务端发送消息,客户端再等待服务端的回应,服务端不一定正在处理客户端发来的信息,而是处理手头上的信息;
异步非阻塞:客户端向服务端发送消息,客户端不需要等待服务端的回应,服务端不一定正在处理客户端发来的信息,而是处理手头上的信息;
网络通信中的线程池
为了避免多线程的浪费,节省内存空间资源,java提供了线程池,来实现线程的复用。
通过实现ExecutorService接口,来对线程池进行管理。当有一个任务希望使用一个已经被创建好的线程去执行,那么可以把这个任务交给ExecutorService,如果ExecutorService中有空闲下的线程,则会调用该线程去执行,执行结果是一个Future对象,我们可以调用Future的isDone()方法查询线程是否执行完毕,调用get()方法获取线程结果。
Socket
Socket是网络通信的端点,也是一种数据源。
当一个主机与其他主机进行交互时,或者进程间的交互,就通过Socket绑定本地的 IP 和 port,然后作为本地的网络通信的端点,进程间通过这个Socket网络通信端点来互相传输数据(IO流)。
简单Socket例子
服务端
1 import java.io.*; 2 import java.net.ServerSocket; 3 import java.net.Socket; 4 5 public class Server { 6 public static void main(String[] args) { 7 //定义本机端口号 8 final int DEFAULT_PORT = 8888; 9 ServerSocket serverSocket = null; 10 try { 11 //绑定监听端口 12 serverSocket = new ServerSocket(DEFAULT_PORT); 13 System.out.println("启动服务器,监听端口" + DEFAULT_PORT); 14 15 while(true){ 16 //serverSocket.accept()返回的是Socet类型 17 //建立本机通信端点,等待客户端连接,否则一直卡在这里 18 Socket socket =serverSocket.accept(); 19 System.out.println("客户端【" + socket.getPort() + "】已连接"); 20 //BufferedReader 是建立在流之上的 21 //InputStreamReader 也是建立流之上的 22 //socket.getInputStream()接收客户端发来的流数据 23 BufferedReader reader = new BufferedReader( 24 new InputStreamReader(socket.getInputStream()) 25 ); 26 //向客户端发送消息 27 BufferedWriter writer = new BufferedWriter( 28 new OutputStreamWriter(socket.getOutputStream()) 29 ); 30 //读取客户端发送的消息 31 String msg = reader.readLine(); 32 if(msg != null){ 33 System.out.println("客户端【" + socket.getPort() + "】:" + msg); 34 35 //回复客户端发送的消息 36 writer.write("服务器:" + msg + "\n"); 37 //刷新,确保Buffered缓冲区的内容都输出来 38 writer.flush(); 39 } 40 } 41 } catch (IOException e) { 42 e.printStackTrace(); 43 }finally { 44 if(serverSocket != null){ 45 try { 46 serverSocket.close(); 47 System.out.println("服务端关闭连接"); 48 } catch (IOException e) { 49 e.printStackTrace(); 50 } 51 } 52 } 53 } 54 }
客户端
import java.io.*; import java.net.Socket; public class Client { public static void main(String[] args) { //客户端ip地址 final String DEFAULT_SERVER_HOST = "127.0.0.1"; final int DEFAULT_SERVER_PORT = 8888; Socket socket = null; BufferedWriter writer = null; try { //创建本机通信端点 socket = new Socket(DEFAULT_SERVER_HOST,DEFAULT_SERVER_PORT); //读取用户输入信息 BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in)); //获取用户输入的信息 String input = consoleReader.readLine(); //向服务端发送信息 writer = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream()) ); //发送 writer.write(input + "\n"); writer.flush(); //接收服务端的信息 BufferedReader reader = new BufferedReader( new InputStreamReader(socket.getInputStream()) ); //读取 String msg = reader.readLine(); System.out.println(msg); } catch (IOException e) { e.printStackTrace(); }finally { try { //再关闭Buffered流的时候会自动调用flush() writer.close(); System.out.println("客户端关闭连接"); } catch (IOException e) { e.printStackTrace(); } } } }
在以上的基础上,添加一些功能:
1、客户端可以重复发送信息;
2、客户端要求关闭连接时,客户端和服务端都关闭连接。
服务端
import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class Server { public static void main(String[] args) { final String exit = "exit"; //定义本机端口号 final int DEFAULT_PORT = 8888; ServerSocket serverSocket = null; try { //绑定监听端口 serverSocket = new ServerSocket(DEFAULT_PORT); System.out.println("启动服务器,监听端口" + DEFAULT_PORT); //serverSocket.accept()返回的是Socet类型 //建立本机通信端点,等待客户端连接,否则一直卡在这里 Socket socket = serverSocket.accept(); System.out.println("客户端【" + socket.getPort() + "】已连接"); //BufferedReader 是建立在流之上的 //InputStreamReader 也是建立流之上的 //socket.getInputStream()接收客户端发来的流数据 BufferedReader reader = new BufferedReader( new InputStreamReader(socket.getInputStream()) ); //向客户端发送消息 BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream()) ); String msg = null; while ((msg = reader.readLine())!=null) { System.out.println("客户端【" + socket.getPort() + "】:" + msg); //回复客户端发送的消息 writer.write("服务器:" + msg + "\n"); //刷新,确保Buffered缓冲区的内容都输出来 writer.flush(); //查看客户端是否退出 if(exit.equals(msg)){ break; } } } catch (IOException e) { e.printStackTrace(); }finally { if(serverSocket != null){ try { serverSocket.close(); System.out.println("服务端关闭连接"); } catch (IOException e) { e.printStackTrace(); } } } } }
客户端
1 import java.io.*; 2 import java.net.Socket; 3 4 public class Client { 5 public static void main(String[] args) { 6 //定义客户端退出 7 final String exit = "exit"; 8 //客户端ip地址 9 final String DEFAULT_SERVER_HOST = "127.0.0.1"; 10 final int DEFAULT_SERVER_PORT = 8888; 11 Socket socket = null; 12 BufferedWriter writer = null; 13 try { 14 //创建本机通信端点 15 socket = new Socket(DEFAULT_SERVER_HOST,DEFAULT_SERVER_PORT); 16 17 //读取用户输入信息 18 BufferedReader consoleReader = 19 new BufferedReader(new InputStreamReader(System.in)); 20 //向服务端发送信息 21 writer = new BufferedWriter( 22 new OutputStreamWriter(socket.getOutputStream()) 23 ); 24 //接收服务端的信息 25 BufferedReader reader = new BufferedReader( 26 new InputStreamReader(socket.getInputStream()) 27 ); 28 System.out.println("已连接【" + socket.getPort() + "】"); 29 while(true) { 30 //获取用户输入的信息 31 String input = consoleReader.readLine(); 32 33 //发送 34 writer.write(input + "\n"); 35 writer.flush(); 36 37 //读取 38 String msg = reader.readLine(); 39 System.out.println(msg); 40 //用户是否退出 41 if(exit.equals(input)){ 42 break; 43 } 44 } 45 } catch (IOException e) { 46 e.printStackTrace(); 47 }finally { 48 try { 49 //再关闭Buffered流的时候会自动调用flush() 50 writer.close(); 51 System.out.println("客户端关闭连接"); 52 } catch (IOException e) { 53 e.printStackTrace(); 54 } 55 } 56 } 57 }
阻塞式BIO
BIO通信模型图
BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型。
2Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
BIO的阻塞
1、ServerSocket.accept():服务端等待客户端连接后才能接着往下运行。
2、InputStream.read(),OutputStream.write():当用户输入时,才会运行,否则一直等待用户输入。
3、无法在同一个线程里处理多个Stream I/O。
实现BIO多人聊天:
ChatServer
package server;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ChatServer {
private int port = 8888;
private final String exit = "exit";
private ServerSocket serverSocket;
//使用Map记录用户发送的消息,key为用户的端口号,value为该用户发出的消息
private Map<Integer, Writer> connectedClients;
//线程池
private ExecutorService executorService;
public ChatServer(){
//设置只有10个线程,超过10个线程,也就是第11个用户想要连接,需要等待其他用户下线
executorService = Executors.newFixedThreadPool(10);
//初始化
connectedClients = new HashMap<>();
}
//当用户上线时,将其端口及发送的消息加入到Map中
public synchronized void addClient(Socket socket) throws IOException {
if(socket != null){
int port = socket.getPort();
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
//存入用户的端口和信息
connectedClients.put(port,writer);
System.out.println("客户端【" + port + "】已连接到服务器");
}
}
//把下线的用户从Map中移除
public synchronized void removeClient(Socket socket) throws IOException {
if(socket != null){
int port = socket.getPort();
//判断Map中是否存在这个用户
if (connectedClients.containsKey(port)){
connectedClients.get(port).close();
System.out.println("客户端【" + port + "】已断开连接");
}
connectedClients.remove(port);
}
}
//消息转发
public synchronized void forwardMessage(Socket socket , String fwdMsg) throws IOException {
//向在线的用户发送消息
for(Integer id : connectedClients.keySet()){
//将用户发送的信息转发给其他用户,发送者本身不需要接收自己发送的消息
if(!id.equals(socket.getPort())){
Writer writer = connectedClients.get(id);
writer.write(fwdMsg);
writer.flush();
}
}
}
//启动服务端时,服务端要做的事情
public void start(){
//绑定监听端口
try {
serverSocket = new ServerSocket(port);
System.out.println("服务器已启动,监听端口:" + port + "...");
while(true){
//等待客户端连接
Socket socket = serverSocket.accept();
//创建ChatHandler线程
executorService.execute(new ChatHandler(this,socket));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
close();
}
}
//用户退出
public Boolean readyExit(String msg){
return msg.equals(exit);
}
public synchronized void close(){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ChatServer server = new ChatServer();
server.start();
}
}
ChatHandler
package server; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; public class ChatHandler implements Runnable{ private ChatServer server; private Socket socket; public ChatHandler(ChatServer server , Socket socket){ this.server = server; this.socket = socket; } @Override public void run() { try { //将新上线用户存入Map中 server.addClient(socket); //读取用户发送的信息 BufferedReader reader = new BufferedReader( new InputStreamReader(socket.getInputStream()) ); String msg = null; //获取换行前的信息 while((msg = reader.readLine()) != null){ String fwdMsg = "客户端【" + socket.getPort() + "】:" + msg; System.out.println(fwdMsg); //将消息转发给聊天室里在线的其他用户 //readLine是获取换行前的消息,所以发送消息后要添加换行符 server.forwardMessage(socket,fwdMsg + "\n"); //检查用户是否要退出 if(server.readyExit(msg)){ break; } } } catch (IOException e) { e.printStackTrace(); }finally { try { server.removeClient(socket); } catch (IOException e) { e.printStackTrace(); } } } }
ChatClient
1 package client; 2 3 import java.io.*; 4 import java.net.Socket; 5 6 public class ChatClient { 7 private final String ip = "127.0.0.1"; 8 private final int port = 8888; 9 private final String exit = "exit"; 10 11 private Socket socket; 12 private BufferedReader reader; 13 private BufferedWriter writer; 14 15 //发送消息给服务器 16 public void send(String msg) throws IOException { 17 if(!socket.isOutputShutdown()){ 18 writer.write(msg + "\n"); 19 writer.flush(); 20 } 21 } 22 //从服务器接收信息 23 public String receive() throws IOException { 24 String msg = null; 25 if(!socket.isInputShutdown()){ 26 msg=reader.readLine(); 27 return msg; 28 } 29 return msg; 30 } 31 //检查用户是否准备退出 32 public boolean readyExit(String msg){ 33 return msg.equals(exit); 34 } 35 //启动 36 public void start(){ 37 38 try { 39 //创建socket 40 socket = new Socket(ip,port); 41 //创建IO流 42 reader = new BufferedReader( 43 new InputStreamReader(socket.getInputStream()) 44 ); 45 writer = new BufferedWriter( 46 new OutputStreamWriter(socket.getOutputStream()) 47 ); 48 //处理用户的输入 49 new Thread(new UserInputHandler(this)).start(); 50 //读取服务器转发的信息 51 String msg = null; 52 while ((msg=receive())!=null){ 53 System.out.println(msg); 54 } 55 } catch (IOException e) { 56 e.printStackTrace(); 57 }finally{ 58 close(); 59 System.out.println("客户端已关闭连接"); 60 } 61 } 62 public void close(){ 63 if(writer != null){ 64 try { 65 writer.close(); 66 } catch (IOException e) { 67 e.printStackTrace(); 68 } 69 } 70 } 71 72 public static void main(String[] args) { 73 ChatClient chatClient = new ChatClient(); 74 chatClient.start(); 75 } 76 }
UserInputHandle
1 package client; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStreamReader; 6 7 public class UserInputHandler implements Runnable{ 8 9 private ChatClient chatClient; 10 11 public UserInputHandler(ChatClient chatClient){ 12 this.chatClient = chatClient; 13 } 14 15 @Override 16 public void run() { 17 try { 18 //等待用户输入信息 19 BufferedReader consoleReader = new BufferedReader( 20 new InputStreamReader(System.in) 21 ); 22 while(true){ 23 String input = consoleReader.readLine(); 24 //向服务器发送信息 25 chatClient.send(input); 26 //用户退出 27 if(chatClient.readyExit(input)){ 28 break; 29 } 30 } 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } 34 } 35 }
非阻塞式NIO
NIO,是一种同步非阻塞的I/O模型,相较于传统BIO,NIO使用了双向通道,通道不用来传输任何数据,数据通过Buffer的移动来进行IO操作。
Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
1、使用Channel代替Stream,不再使用Stream;
2、使用Selecyor监控多条Channel;
3、可以在一个线程里处理多个Channel I/O。
Channel与Buffer
Channel通道可以进行读写操作,其实是通过Buffer来完成的。向Channel写数据时候,是要先把数据写入到Buffer中,从Channel中读数据时候,Channel是先从Buffer里面读出数据再返回出来。所以我们对Channel的操作都离不开Buffer。
常见的通道有四种:
FileChannel 从文件中读写数据。
DatagramChannel 能通过UDP读写网络中的数据。
SocketChannel 能通过TCP读写网络中的数据。
ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
常用的Buffer是ByteBuffer
向Buffer写入数据
创建一个Buffer时候就要给他设定一个内存大小。
capacity指针表示整个缓存区的最大容量;
position指针表示目前所操作数据的位置,也就是我写入的时候,是把数据放到position所指向的地方;
limit指针表示切换模式后数据的状态;
写数据操作已经完毕后,要在Buffer中读取数据,此时不能直接读,首先要调用flip()方法,把Buffer的写模式切换成读模式。
写数据模式,数据会被写入到position指向的位置,最多只能写到capacity指针指向的位置。
调用filp()之前的写模式
切换为读模式后,position指针指向写入的第一个数据位置,limit指针指向写入的最后一个数据位置。读的时候,最多只能读到limit指向的位置。这样就可以保证我们读取的,就是从头到写入最后一个数据的位置。
调用filp()之后切换为读模式
读模式切换写模式有两种情况:
1、读完数据,可以调用clear()切换回写模式,此时position指针会指向头,limit指针指到末尾,但是Buffer中的数据不会被清除。调用clear()只是移动了指针。
调用了clear()切换回写模式
2、当第一次读数据时,没把数据读完就要切换为写模式,此时可以使用compact()切换为写模式。
使用compact()切换为写模式时,会把第一次读时候没读完的数据放在前面,然后position指针指向这些数据的后面,在进行写操作,下次读时,就会先把未读的数据先读出来,再接着往下读。
Channel之间可以互相读写数据
文件拷贝案例1(使用缓冲)
1 package io; 2 3 import java.io.*; 4 import java.nio.ByteBuffer; 5 import java.nio.channels.FileChannel; 6 7 public class Nio { 8 public static void main(String[] args) { 9 File file1 = new File("路径1"); 10 File file2 = new File("路径2"); 11 new Nio(file1,file2); 12 } 13 public Nio(File source,File target){ 14 FileChannel fin = null; 15 FileChannel fout = null; 16 17 try { 18 fin = new FileInputStream(source).getChannel(); 19 fout = new FileOutputStream(target).getChannel(); 20 //设置容量1024字节缓冲区 21 ByteBuffer buffer = ByteBuffer.allocate(1024); 22 //Channel的读操作就是将数据写入到Buffer中 23 while((fin.read(buffer))!=-1){ 24 //把写模式切换成读模式 25 buffer.flip(); 26 //buffer.hasRemaining()判断是否还有可读的数据,使用这种方法更加可靠 27 while(buffer.hasRemaining()) { 28 fout.write(buffer); 29 } 30 //切换为写模式 31 buffer.clear(); 32 } 33 } catch (FileNotFoundException e) { 34 e.printStackTrace(); 35 } catch (IOException e) { 36 e.printStackTrace(); 37 } 38 } 39 }
文件拷贝案例2(不适用缓冲)
1 import java.io.*; 2 import java.nio.channels.FileChannel; 3 4 public class Nio { 5 public static void main(String[] args) { 6 File file1 = new File("路径1"); 7 File file2 = new File("路径2"); 8 new Nio(file1,file2); 9 } 10 public Nio(File source,File target){ 11 FileChannel fin = null; 12 FileChannel fout = null; 13 14 try { 15 fin = new FileInputStream(source).getChannel(); 16 fout = new FileOutputStream(target).getChannel(); 17 18 long transferred = 0L; 19 long size = fin.size(); 20 while(transferred != size){ 21 //直接在这两个通道之间传输 22 //参数一:从0开始拷贝 23 //参数二:拷贝到fin.size()位置 24 //参数三:拷贝到fout通道 25 transferred += fin.transferTo(0,size,fout); 26 } 27 } catch (FileNotFoundException e) { 28 e.printStackTrace(); 29 } catch (IOException e) { 30 e.printStackTrace(); 31 } 32 } 33 34 }
案例三:当文件字节大于ByteBuffer指定的字节数时如何处理
import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; public class testNIO { private ByteBuffer byteBuffer; private FileChannel channel; private File file; private Charset charset; public void start() throws IOException { file = new File("E:\\百度云\\NIO.txt"); channel = new FileInputStream(file).getChannel(); while (true) { byteBuffer = ByteBuffer.allocate(23); byteBuffer.clear(); int i = channel.read(byteBuffer); byteBuffer.flip(); charset = Charset.forName("UTF-8"); if(byteBuffer.hasRemaining()){ String msg = String.valueOf(charset.decode(byteBuffer)); System.out.print(msg); }else if (i == -1){ break; } } } public static void main(String[] args) { testNIO t = new testNIO(); try { t.start(); } catch (IOException e) { e.printStackTrace(); } } }
放入循环中,分n次来读,知道read()返回-1,即以及那个读完了。
FileChannel:与文件进行数据传输通道;
ServerSocketChannel和SocketChannel:在网络编程中进行数据传输。
Selector与Channel
NIO提供有Selector类,Selector可以监听多个通道的状态是否处于可操作的状态。
首先需要把通道注册到Selector上,通过Selector监控通道的状态。
连接校验
socketChannel.isOpen(); // 测试SocketChannel是否为open状态 socketChannel.isConnected(); //测试SocketChannel是否已经被连接 socketChannel.isConnectionPending(); //测试SocketChannel是否正在进行连接 socketChannel.finishConnect(); //校验正在进行套接字连接的SocketChannel是否已经完成连接
多人聊天室:
服务端
1 package server;
2
3 import java.io.IOException;
4 import java.net.InetSocketAddress;
5 import java.nio.ByteBuffer;
6 import java.nio.channels.*;
7 import java.nio.charset.Charset;
8 import java.util.Set;
9
10 public class ChatServer {
11
12 private static int port = 8888;
13 private final String exit = "exit";
14 private ServerSocketChannel server;
15 private Selector selector;
16 private ByteBuffer rBuffer = ByteBuffer.allocate(1024);
17 private ByteBuffer wBuffer = ByteBuffer.allocate(1024);
18 private Charset charset = Charset.forName("UTF-8");
19 private int defaultport;
20
21 public ChatServer(){
22 this(port);
23 }
24 public ChatServer(int defaultport){
25 this.defaultport = defaultport;
26 }
27 public void start(){
28 try {
29 //创建通道
30 server = ServerSocketChannel.open();
31 //ServerSocketChannel支持阻塞模式也支持非阻塞模式,使用open()创建,默认就是阻塞状态
32 //设置为非阻塞状态
33 server.configureBlocking(false);
34 //把通道关联的serverSocket绑定监听端口
35 server.socket().bind(new InetSocketAddress(port));
36 //创建Selector
37 selector = Selector.open();
38 //在Selector注册通道,当通道与客户端建立起了连接,就触发Selector的OP_ACCEPT事件(连接事件)
39 server.register(selector, SelectionKey.OP_ACCEPT);
40 System.out.println("启动服务器,监听端口:" + defaultport + "....");
41
42 //select()是阻塞式的,当通道触发了事件(例如连接事件)才会返回int类型整数说明触发了多少个事件
43 //如果事件没有被触发,则会阻塞式的等待被触发,出发之后才返回
44 //它可以返回出一个通道或者多个通道触发了多少事件
45 while(true) {
46 selector.select();
47 //一个通道或多个通道触发事件(例如连接事件)后,会把通道的事件信息(例如连接信息)封装在SelectionKey中
48 Set<SelectionKey> selectedKeysSet = selector.selectedKeys();
49
50 for(SelectionKey key : selectedKeysSet){
51 //处理被触发的事件
52 handles(key);
53 }
54 //清空Set集合元素,进入下一次监听通道信息循环
55 selectedKeysSet.clear();
56 }
57 } catch (IOException e) {
58 e.printStackTrace();
59 } finally {
60 try {
61 selector.close();
62 } catch (IOException e) {
63 e.printStackTrace();
64 }
65 }
66 }
67 //用户退出
68 public Boolean readyExit(String msg){
69 return msg.equals(exit);
70 }
71 //处理事件
72 private void handles(SelectionKey key) throws IOException {
73 //ACCEPT事件 - 与客户端建立了连接
74 if(key.isAcceptable()){
75 //获取触发了事件的通道
76 ServerSocketChannel server = (ServerSocketChannel) key.channel();
77 //建立与客户端连接
78 SocketChannel client = server.accept();
79 //默认是阻塞模式,需要转换为非阻塞模式
80 client.configureBlocking(false);
81 //在Selector注册通道,当服务端接收到客户端的消息,就触发OP_READ事件
82 client.register(selector,SelectionKey.OP_READ);
83 System.out.println("客户端【" + client.socket().getPort() + "】已连接");
84 }
85 //READ事件 - 客户端发送了消息
86 else if(key.isReadable()){
87 //获取触发了事件的通道
88 SocketChannel client = (SocketChannel)key.channel();
89 //从通道里面读数据
90 String fwdMsg = receive(client);
91 //处理转发消息
92 if(fwdMsg.isEmpty()){
93 //客户端异常
94 //在SelectionKey中取消监听这个通道
95 key.cancel();
96 //告诉Selector不需要等待事件被触发之后才返回,直接让Selector返回
97 selector.wakeup();
98 } else {
99 forwardMessage(client,fwdMsg);
100 //检查用户是否要退出
101 if (readyExit(fwdMsg)) {
102 key.cancel();
103 selector.wakeup();
104 System.out.println("客户端【" + client.socket().getPort() + "】已断开");
105 }
106 }
107 }
108 }
109 //转发消息
110 private void forwardMessage(SocketChannel client,String fwdMsg) throws IOException {
111 // selector.keys() 返回目前所有已经注册在Selector上面的通道
112 //instanceof是Java中的二元运算符,左边是对象,右边是类;当对象是右边类或子类所创建对象时,返回true;否则,返回false。
113 for(SelectionKey key: selector.keys()){
114 Channel connectedClient = key.channel();
115 if (connectedClient instanceof ServerSocketChannel){
116 continue;
117 }
118 //key.isValid():该key所对应的channel是否被关闭
119 if(key.isValid() && !client.equals(connectedClient)){
120 wBuffer.clear();
121 wBuffer.put(charset.encode(client.socket().getPort() + ":" + fwdMsg));
122 wBuffer.flip();
123 while(wBuffer.hasRemaining()){ //hasRemaining()
用于判断当前的 position 是否小于 limit:如果 position 小于 limit,则返回 true,代表仍有待处理的数据。
124 ((SocketChannel)connectedClient).write(wBuffer);
125 }
126 }
127 }
128 }
129 //读数据
130 private String receive(SocketChannel client) throws IOException {
131 rBuffer.clear();
132 //把通道中的信息写入rBuffer中
133 while(client.read(rBuffer) > 0);
134 rBuffer.flip();
135 //解码一个rBuffer对象
136 return String.valueOf(charset.decode(rBuffer));
137 }
138
139 public static void main(String[] args) {
140 ChatServer server = new ChatServer();
141 server.start();
142 }
143 }
客户端:
1 package client; 2 3 import java.io.IOException; 4 import java.net.InetSocketAddress; 5 import java.nio.ByteBuffer; 6 import java.nio.channels.ClosedSelectorException; 7 import java.nio.channels.SelectionKey; 8 import java.nio.channels.Selector; 9 import java.nio.channels.SocketChannel; 10 import java.nio.charset.Charset; 11 import java.util.Set; 12 13 public class ChatClient { 14 private final static String ip = "127.0.0.1"; 15 private final static int DefaultPort = 8888; 16 private final String exit = "exit"; 17 18 private String host; 19 private int port; 20 private SocketChannel client; 21 private ByteBuffer rBuffer = ByteBuffer.allocate(1024); 22 private ByteBuffer wBuffer = ByteBuffer.allocate(1024); 23 private Selector selector; 24 private Charset charset = Charset.forName("UTF-8"); 25 26 public ChatClient(){ 27 this(ip,DefaultPort); 28 } 29 public ChatClient(String ip,int port){ 30 this.host = ip; 31 this.port = port; 32 } 33 public boolean readyExit(String msg){ 34 return exit.equals(msg); 35 } 36 private void start() { 37 try { 38 client = SocketChannel.open(); 39 //改为非阻塞模式 40 client.configureBlocking(false); 41 selector = Selector.open(); 42 //注册Selector,并监听连接事件 43 client.register(selector, SelectionKey.OP_CONNECT); 44 //向服务器发送连接请求 45 client.connect(new InetSocketAddress(host, port)); 46 while (true) { 47 selector.select(); 48 Set<SelectionKey> selectionKeys = selector.selectedKeys(); 49 for (SelectionKey key : selectionKeys) { 50 handles(key); 51 } 52 selectionKeys.clear(); 53 } 54 }catch (IOException e){ 55 e.printStackTrace(); 56 } catch (ClosedSelectorException e){ 57 //用户正常退出 58 }finally { 59 try { 60 if(selector != null) 61 selector.close(); 62 } catch (IOException e) { 63 e.printStackTrace(); 64 } 65 } 66 } 67 private void handles(SelectionKey key) throws IOException { 68 //CONNECT - 连接就绪事件 69 if(key.isConnectable()){ 70 SocketChannel channel = (SocketChannel)key.channel(); 71 if(channel.isConnectionPending()){ 72 //正式建立客户端和服务端之间的连接 73 channel.finishConnect(); 74 //处理用户的输入,用户的输入是阻塞的,因为用户随时都可以输入 75 new Thread(new UserInputHandler(this)).start(); 76 } 77 client.register(selector,SelectionKey.OP_READ); 78 } 79 //READ - 接收服务器转发消息 80 else if(key.isReadable()){ 81 SocketChannel channel = (SocketChannel)key.channel(); 82 String msg = receive(channel); 83 if(msg.isEmpty()){ 84 selector.close(); 85 }else{ 86 System.out.println(msg); 87 } 88 } 89 90 } 91 public void send(String msg) throws IOException { 92 if(msg.isEmpty()){ 93 return; 94 } 95 wBuffer.clear(); 96 wBuffer.put(charset.encode(msg)); 97 wBuffer.flip(); 98 //发送消息 99 while(wBuffer.hasRemaining()){ 100 client.write(wBuffer); 101 } 102 //用户退出 103 if(readyExit(msg)){ 104 selector.close(); 105 } 106 } 107 108 private String receive(SocketChannel channel) throws IOException { 109 rBuffer.clear(); 110 while(channel.read(rBuffer) >0); 111 rBuffer.flip(); 112 return String.valueOf(charset.decode(rBuffer)); 113 } 114 115 public static void main(String[] args) { 116 ChatClient chatClient = new ChatClient(); 117 chatClient.start(); 118 } 119 }
最后再注释掉ChatHandle类。
异步I/O NIO
AIO 介绍
AIO最大的一个特性就是异步能力,这种能力对socket与文件I/O都起作用。AIO其实是一种在读写操作结束之前允许进行其他操作的I/O处理。AIO是对JDK1.4中提出的同步非阻塞I/O(NIO)的进一步增强。
应用程序层面发起系统调用,询问内核是否准备好数据,当内核没准备好,则先返回无数据,等内核数据准备好后,在发送数据非应用程序。
实现AIO异步有两种方式:
1)通过Future:
Future模式可以这样来描述:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情。一段时间之后,我就便可以从Future那儿取出结果。
Future的类图结构
Future接口定义了主要的5个接口方法,有RunnableFuture和SchedualFuture继承这个接口,以及CompleteFuture和ForkJoinTask继承这个接口。
Future接口主要包括5个方法
get()方法可以当任务结束后返回一个结果,如果调用时,工作还没有结束,则会阻塞线程,直到任务执行完毕;
get(long timeout,TimeUnit unit)做多等待timeout的时间就会返回结果;
cancel(boolean mayInterruptIfRunning)方法可以用来停止一个任务,如果任务可以停止(通过mayInterruptIfRunning来进行判断),则可以返回true,如果任务已经完成或者已经停止,或者这个任务无法停止,则会返回false;
isDone()方法判断当前方法是否完成;
isCancel()方法判断当前方法是否取消。
2)通过CompletionHandler
实现CompletionHandler接口的两个方法:
(1)completed 回调函数成功时执行的方法;
(2)failed 回调函数失败时1执行的方法。
AIO多人聊天演示
服务端
1 package AIO; 2 3 import java.io.Closeable; 4 import java.io.IOException; 5 import java.net.InetSocketAddress; 6 import java.nio.ByteBuffer; 7 import java.nio.CharBuffer; 8 import java.nio.channels.AsynchronousChannelGroup; 9 import java.nio.channels.AsynchronousServerSocketChannel; 10 import java.nio.channels.AsynchronousSocketChannel; 11 import java.nio.channels.CompletionHandler; 12 import java.nio.charset.Charset; 13 import java.util.ArrayList; 14 import java.util.List; 15 import java.util.concurrent.ExecutorService; 16 import java.util.concurrent.Executors; 17 18 public class Server { 19 20 private static final String LOCALHOST = "localhost"; 21 private static final int DEFAULT_PORT = 8888; 22 private static final String QUIT = "quit"; 23 private static final int BUFFER = 1024; 24 private static final int THREADPOOL_SIZE = 8; 25 private AsynchronousChannelGroup channelGroup; 26 private AsynchronousServerSocketChannel serverSocketChannel; 27 private List<ClientHandler> connectedClients; 28 private Charset charset = Charset.forName("UTF-8"); 29 private int port; 30 31 public Server(){ 32 this(DEFAULT_PORT); 33 } 34 public Server(int port){ 35 this.port = port; 36 this.connectedClients = new ArrayList<>(); 37 } 38 39 /** 40 * 准备退出 41 * @param msg 42 * @return 43 */ 44 private boolean readyToQuit(String msg){ 45 return QUIT.equals(msg); 46 } 47 48 /** 49 * 关闭此流并释放与之相关联的任何系统资源,如果流已经关闭,则调用此方法将不起作用。 50 * @param closeable 51 */ 52 private void close(Closeable closeable){ 53 if(closeable != null){ 54 try { 55 closeable.close(); 56 } catch (IOException e) { 57 e.printStackTrace(); 58 } 59 } 60 } 61 62 /** 63 * 往客户端列表里添加一个新的客户端 64 * @param clientHandler 65 */ 66 private synchronized void addClient(ClientHandler clientHandler) { 67 connectedClients.add(clientHandler); 68 System.out.println(getClientName(clientHandler.clientChannel) + "已经连接到服务器"); 69 } 70 71 /** 72 * 将客户端链表里移除一个断开的客户端,同时关闭连接 73 * @param clientHandler 74 */ 75 private synchronized void removeClient(ClientHandler clientHandler) { 76 connectedClients.remove(clientHandler); 77 System.out.println(getClientName(clientHandler.clientChannel) + "已断开连接"); 78 close(clientHandler.clientChannel); 79 } 80 81 /** 82 * 接收消息,并解码 83 * @param byteBuffer 84 * @return 85 */ 86 private String receive(ByteBuffer byteBuffer){ 87 CharBuffer charBuffer = charset.decode(byteBuffer); 88 return String.valueOf(charBuffer); 89 } 90 91 /** 92 * 获取客户端的信息 93 * @param clientChannel 94 * @return 95 */ 96 private String getClientName(AsynchronousSocketChannel clientChannel) { 97 int clientPort = -1; 98 try { 99 InetSocketAddress inetSocketAddress = (InetSocketAddress) clientChannel.getRemoteAddress(); 100 clientPort = inetSocketAddress.getPort(); 101 } catch (IOException e) { 102 e.printStackTrace(); 103 } 104 return "客户端[" + clientPort + "]"; 105 } 106 107 /** 108 * 转发客户端的消息 109 * @param clientChannel 110 * @param fwdMsg 111 */ 112 private synchronized void forwardMessage(AsynchronousSocketChannel clientChannel, String fwdMsg) { 113 for (ClientHandler handler : connectedClients){ 114 if (!handler.clientChannel.equals(clientChannel)){ 115 try { // 防止发生意想不到的错误或异常 116 ByteBuffer byteBuffer = charset.encode(getClientName(handler.clientChannel) + ":" + fwdMsg); 117 handler.clientChannel.write(byteBuffer, null, handler); 118 } catch (Exception e) { 119 e.printStackTrace(); 120 } 121 } 122 } 123 } 124 125 /** 126 * 当客户端连接时,用于接收客户端请求的Handler 127 */ 128 private class AcceptHandler implements 129 CompletionHandler<AsynchronousSocketChannel, Object>{ 130 @Override 131 public void completed(AsynchronousSocketChannel clientChannel, Object attachment) { 132 if (serverSocketChannel.isOpen()){ //保证serverSocketChannel继续去监听 133 serverSocketChannel.accept(null, this); 134 } 135 if (clientChannel != null && clientChannel.isOpen()){ 136 ClientHandler clientHandler = new ClientHandler(clientChannel); 137 ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER); 138 // 将新用户添加到在线用户列表 139 addClient(clientHandler); 140 //启动异步读操作,以从该通道读取到给定缓冲器中的字节序列 141 clientChannel.read(byteBuffer, byteBuffer, clientHandler); 142 } 143 } 144 145 @Override 146 public void failed(Throwable exc, Object attachment) { 147 System.out.println("连接失败:" + exc); 148 } 149 } 150 151 /** 152 * 处理客户端事件 153 */ 154 private class ClientHandler implements 155 CompletionHandler<Integer, Object>{ 156 157 private AsynchronousSocketChannel clientChannel; 158 public ClientHandler(AsynchronousSocketChannel channel){ 159 this.clientChannel = channel; 160 } 161 @Override 162 public void completed(Integer result, Object attachment) { 163 ByteBuffer byteBuffer = (ByteBuffer)attachment; 164 if (byteBuffer != null){ // 如果发生了读事件 165 if (result <= 0){ 166 // 客户端通道发生了异常 167 // 将客户从在线客户列表中去除 168 removeClient(this); 169 }else { 170 byteBuffer.flip(); //转换为读操作 171 String fwdMsg = receive(byteBuffer); //读取buffer里的消息 172 System.out.println(getClientName(clientChannel) + ":" +fwdMsg); 173 forwardMessage(clientChannel, fwdMsg); //向其他用户转发消息 174 byteBuffer.clear();//清除缓冲区 175 176 // 检查用户是否退出 177 if (readyToQuit(fwdMsg)){ 178 // 将客户从在线客户列表中去除 179 removeClient(this); 180 }else { 181 clientChannel.read(byteBuffer,byteBuffer,this); 182 } 183 } 184 } 185 } 186 187 @Override 188 public void failed(Throwable exc, Object attachment) { 189 System.out.println("读写失败: " + exc); 190 } 191 } 192 193 /** 194 * 启动服务器 195 */ 196 private void start(){ 197 try { 198 ExecutorService executorService = Executors.newFixedThreadPool(THREADPOOL_SIZE);//初始化线程池 199 channelGroup = AsynchronousChannelGroup.withThreadPool(executorService);//将线程池加入到异步通道,进行资源共享 200 serverSocketChannel = AsynchronousServerSocketChannel.open(channelGroup); 201 serverSocketChannel.bind(new InetSocketAddress(LOCALHOST, port)); 202 System.out.println("启动服务器,监听端口:"+ port); 203 204 while (true){ 205 //参数一:附件 206 //参数二:实现CompletionHandler接口的类 207 serverSocketChannel.accept(null, new AcceptHandler()); 208 System.in.read();//阻塞调用,防止占用系统资源,一直调用accept()函数 209 } 210 } catch (IOException e) { 211 e.printStackTrace(); 212 }finally { 213 close(serverSocketChannel); 214 } 215 } 216 217 /** 218 * 主函数入口 219 * @param args 220 */ 221 public static void main(String[] args) { 222 Server server = new Server(7777); 223 server.start(); 224 } 225 }
客户端
1 package AIO; 2 3 import java.io.Closeable; 4 import java.io.IOException; 5 import java.net.InetSocketAddress; 6 import java.nio.ByteBuffer; 7 import java.nio.channels.AsynchronousSocketChannel; 8 import java.nio.charset.Charset; 9 import java.util.concurrent.ExecutionException; 10 import java.util.concurrent.Future; 11 12 public class Client { 13 private static final String LOCALHOST = "localhost"; 14 private static final int DEFAULT_PORT = 8888; 15 private static final String QUIT = "quit"; 16 private static final int BUFFER = 1024; 17 private String host; 18 private int port; 19 private AsynchronousSocketChannel clientChannel; 20 private Charset charset = Charset.forName("UTF-8"); 21 22 public Client(){ 23 this(LOCALHOST, DEFAULT_PORT); 24 } 25 public Client(String host, int port){ 26 this.host = host; 27 this.port = port; 28 } 29 30 /** 31 * 关闭此流并释放与之相关联的任何系统资源,如果流已经关闭,则调用此方法将不起作用。 32 * @param closeable 33 */ 34 private void close(Closeable closeable){ 35 if(closeable != null){ 36 try { 37 closeable.close(); 38 } catch (IOException e) { 39 e.printStackTrace(); 40 } 41 } 42 } 43 44 /** 45 * 判断客户端是否准备退出 46 * @param msg 47 * @return 48 */ 49 public boolean readyToQuit(String msg){ 50 return QUIT.equals(msg); 51 } 52 53 private void start(){ 54 55 try { 56 // 创建channel 57 clientChannel = AsynchronousSocketChannel.open(); 58 Future<Void> future = clientChannel.connect(new InetSocketAddress(host, port)); 59 future.get();// 阻塞式调用 60 61 //启动一个新的线程,处理用户的输入 62 new Thread(new UserInputHandler(this)).start(); 63 64 ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER); 65 // 读取服务器转发的消息 66 while(true){ 67 //启动异步读操作,以从该通道读取到给定的缓冲器字节序列 68 Future<Integer> readResult = clientChannel.read(byteBuffer); 69 //Future的get方法返回读取的字节数或-1如果没有字节可以读取,因为通道已经到达流终止。 70 int result = readResult.get(); 71 if(result <= 0){ 72 // 服务器异常 73 System.out.println("服务器断开"); 74 close(clientChannel); 75 // 0是正常退出,非0是不正常退出 76 System.exit(1); 77 }else { 78 byteBuffer.flip(); //准备读取 79 String msg = String.valueOf(charset.decode(byteBuffer)); 80 byteBuffer.clear(); 81 System.out.println(msg); 82 } 83 } 84 } catch (IOException e) { 85 e.printStackTrace(); 86 } catch (InterruptedException e) { 87 e.printStackTrace(); 88 } catch (ExecutionException e) { 89 e.printStackTrace(); 90 } 91 } 92 public void send(String msg){ 93 if (msg.isEmpty()){ 94 //没有必要向服务器发送空白的消息从而占用资源 95 return; 96 } 97 ByteBuffer byteBuffer = charset.encode(msg); 98 Future<Integer> writeResult = clientChannel.write(byteBuffer); 99 try { 100 writeResult.get(); 101 } catch (InterruptedException | ExecutionException e) { 102 System.out.println("消息发送失败"); 103 e.printStackTrace(); 104 } 105 } 106 107 public static void main(String[] args) { 108 Client chatClient = new Client("127.0.0.1", 7777); 109 chatClient.start(); 110 } 111 }
客户端发送消息
1 package AIO; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStreamReader; 6 7 public class UserInputHandler implements Runnable{ 8 private Client client; 9 public UserInputHandler(Client chatClient){ 10 this.client = chatClient; 11 } 12 /** 13 * 14 */ 15 @Override 16 public void run() { 17 try { 18 //等待用户输入的消息 19 BufferedReader consoleReader = new BufferedReader( 20 new InputStreamReader(System.in) 21 ); 22 while(true){ 23 String input = consoleReader.readLine(); 24 //向服务器发送消息 25 client.send(input); 26 //检查用户是否准备退出 27 if(client.readyToQuit(input)){ 28 break; 29 } 30 } 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } 34 } 35 }