设计思路
java BIO进行通信有个弊端就是读取或者写入是阻塞的,Server端必须依次的处理完每个请求。要与多个用户同时通信,那么我们必须对每个请求都开启线程。当并发量大时,线程切换的成本也很大,所以并发量大的应用不适用这种技术。所以我们采用NIO来进行通信。Java NIO是jdk1.4新推出的,它是一种同步非阻塞的I/O模型,也是I/O多路复用模式。它的同步指的是读取或者写入的时候依然是同步的,需要等待程序处理完毕才能返回,另外Selector获取哪些Channel有消息时也是同步等待的。非阻塞是指从网络到内存是非阻塞的,这部分被系统托管,我们通过Selector循环遍历来得到哪些Channel有接收到数据,有数据则立即处理。Netty也是采用NIO,所以我们也用NIO。
五种IO模型参考
IO模型理解参考
NIO参考
BIO学习
socket 通信是基于TCP/IP 网络层上的一种传送方式,我们通常把TCP和UDP称为传输层。socket是基于应用服务与TCP/IP通信之间的一个抽象,他将TCP/IP协议里面复杂的通信逻辑进行分装。
传统Socket被称为阻塞式I/O,使用Socket与SocketServer两个类实现,SocketServer接收来自客户端的请求时是阻塞的,需等待上一个连接处理完毕,所以我们一般使用线程池来处理新连接。BIO的弊端就是当线程太多的时候,线程间切换的成本太大。
在Java的Socket编程中,若使用阻塞式(BIO),则往往通过ServerSocket的accept()方法获取到客户端Socket之后,再使用客户端Socket的InputStream和OutputStream进行读写。Socket.getInputstream.read(byte[] b)和Socket.getOutputStream.write(byte[] b)的方法中的参数都是字节数组。这种阻塞式的Socket编程显然已经远远不能满足目前的并发式访问需求
NIO学习
NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。NIO统一管理来自客户端的连接,统一注册到Selector中,然后轮询,观察这些连接中是否有就绪的。一般我们需要关注的就是接收请求、连接成功、读、写等事件。Netty框架就是使用的NIO。我们的项目就是模仿Netty的模式使用NIO实现。
在Java NIO中,主要有三大基本的组件:Buffer、Channel和Selector。Selector是中央控制器,Buffer是承载数据的容器,而Channel可以说是最基础的门面,它是本地I/O设备、网络I/O的通信桥梁,只有搭建了这座桥梁,数据才能被写入Buffer,连接才能被Selector控制。
Java原生NIO,需要通过ServerSocketChannel的accept()方法获取到客户端的SocketChannel,再使用客户端SocketChannel直接进行读写。但SocketChannel.read(ByteBuffer dst)和SocketChannel.write(ByteBuffer src)的方法中的参数则都变为了java.nio.ByteBuffer,该类型就是JavaNIO对byte数组的一种封装,其中包括了很多基本的操作。在NIO中ByteBuffer就相当于一个中转站或者中转容器,我们需要反复去socketChannel中获取数据,最后汇总起来。直接使用SocketChannel与ByteBuffer,其效果与BIO相同。结合了Selector后才算是多路复用、同步非阻塞IO。
AIO学习
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。AIO的所有代码基本都是要添加一个回调函数来处理,是CompletionHandler的实现类。AIO是完全实现异步的,但是其代码比较繁复,另外其底层与NIO一样(epoll),也就是并没有达到最优效。
Reactor模式
要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将事件通知工作线程(逻辑单元)数据的读写,接受新的连接以及处理客户请求均在工作线程中完成;除此之外,逻辑线程不作任何工作。对应为NIO。
1.主线程往epoll内核事件表中注册socket上的读就绪事件
2.主线程调用epoll_wait等待socket上有数据可读
3.当socket上有数据可读时,epoll_wait通知主线程,主线程则将socket可读事件放入请求队列。
4.睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件。
5.主线程调用epoll_wait等待socket可写
6.当socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列
7.睡眠在请求队列上的某个工作线程(工作线程从请求队列读取事件后,根据事件的类型来决定如何处理它,没有必要区分读工作线程和写工作线程)被唤醒,它往socket上写入服务器处理客户请求的结果
Proactor模式
Proactor将所有I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。对应为AIO,使用异步I/O模型(aio_read和aio_write)来实现Proactor模式的工作流程是:
1.主线程调用aio_read向内核注册socket上的读完成事件,并告诉内核用户缓冲区的位置,以及读操作完成时如何通知应用程序(可以用信号)
2.主线程继续处理其他逻辑
3.当socket上的读数据被读入用户缓冲区后,内核向应用进程发送一个信号,已通知应用程序数据已经可用
4.应用进程预先定义好的信号处理函数选择一个工作线程来处理处理客户请求,工作线程处理完客户请求之后,调用aio_write向内核注册socket的完成写事件,并告诉内核用户写缓冲区的位置,以及操作完成时如何通知应用程序(可以用信号)
5.主线程继续处理其他逻辑
6.当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,已通知应用程序数据已经发送完毕
7.应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket。
第一步
因为Reactor需要在后台运行,所以在客户端我们创建了一个AbstractProcess虚拟类,主要就是继承了Runable接口,另外规定了几个要实现的方法。
public abstract class AbstractProcess implements Runnable{
protected ByteBuffer buffer;
protected Selector selector;
public AbstractProcess() throws IOException {
selector=Selector.open();
buffer=ByteBuffer.allocate(1024);
}
public AbstractProcess(int buffSize) throws IOException{
selector=Selector.open();
buffer=ByteBuffer.allocate(buffSize);
}
public abstract boolean sendMsg(ByteBuffer msg);
public abstract void handle(SelectionKey key) throws IOException;//接收事件
public abstract void selector();
public void run() {
selector();
}
}
当我们开启线程运行Reactor 后,我们的Selector就会开始无限循环,等待处理数据。另外当我们连接不上服务器时,我们会再次调用连接函数,尝试重连。AppSession 类主要是存储是用户信息以及客户端聊天界面的聊天记录以及所有在线用户,另外就是一下操作方法。
public class AppSession {
private Map<String, ObservableList<Message>> msgMap=new HashMap<>();//存储所有用户的聊天记录
private ObservableList<ChatSession> chatSession= FXCollections.observableList(new LinkedList<ChatSession>());
private String currentUsername;
private User currentUser;
...
}
public class Reactor extends AbstractProcess{
private volatile boolean isShutdown=false;
private int PORT = 9000;
private String HOSTNAME = "localhost";
private int TIMEOUT = 3000;
private AppSession session;
private SocketChannel sc = null;
private Main main;
private HertBeator hertBeator;
public Reactor(Main main, AppSession session) throws IOException {
super();
this.main=main;
this.session=session;
connect();
}
public Reactor(int BUF_SIZE, int TIMEOUT,Main main) throws IOException {
super(BUF_SIZE);
this.TIMEOUT = TIMEOUT;
this.main=main;
connect();
}
public void connect() throws IOException {
sc = SocketChannel.open();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_CONNECT);
sc.connect(new InetSocketAddress(HOSTNAME, PORT));
}
public boolean hertBeatorConnect() throws InterruptedException {
//重连
reconnect();
//判断是否重连成功,不成功则休眠
while (!sc.isConnected()) {
Thread.sleep(500);
}
//发送登录信息
main.sendMsg(msgUtils.getloginMsg(session.getCurrentUsername()));
return true;
}
public void reconnect() {
//先关闭连接
try{
if(sc!=null){
sc.close();
}
}catch(IOException e1){
e1.printStackTrace();
}
//重连
if(!sc.isConnected()) {
try {
sc = SocketChannel.open();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_CONNECT);
sc.connect(new InetSocketAddress(HOSTNAME, PORT));
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public boolean sendMsg(ByteBuffer msg) {
try {
sc.write(msg);
//更新心跳连接的操作时间
if(hertBeator!=null)
hertBeator.updateActionTime();
return true;
} catch (IOException e) {
return false;
}
}
public void stop(){
isShutdown=true;
Thread.currentThread().