N/O模型
一、介绍
Java共支持3种网络编程模型I/O模式:BIO、NIO、AIO
简单来说就是:用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
二、BIO 同步并阻塞模型
1.介绍BIO
服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就会需要启动一个线程进行处理,会造成不必要的线程开销。
适用场景:连接数目较小,且固定的架构。
2.BIO编程流程
- 服务器端启动一个ServerSocket
- 客户端启动Socket对服务器端进行通讯,默认情况下服务器端需要对每个客户简历一个线程与之通讯。
- 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
- 如果有响应,客户端线程会等待请求结束后,在继续执行。
3.代码实现
package com.lsh;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author :LiuShihao
* @date :Created in 2020/9/1 4:58 下午
* @desc :BIO 同步阻塞模型
*/
public class bio {
public static void main(String[] args) throws IOException {
/**
* 思路:
* 1.创建一个线程池
* 2.如果有客户端,就创建一个线程,与之通讯
*
*/
ExecutorService newexecutorService = Executors.newCachedThreadPool();
//创建一个ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器已经启动了");
System.out.println("等待连接....");
while(true){
//监听,等待客户端连接
final Socket accept = serverSocket.accept();
System.out.println("连接到一个客户端");
//创建一个线程
newexecutorService.execute(new Runnable() {
public void run() {
handler(accept);
}
});
}
}
public static void handler(Socket socket){
try{
byte[] bytes = new byte[1024];
//通过socket获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while(true){
//将数据读取到bytes字节数组中去
System.out.println("read....");
int read = inputStream.read(bytes);
if (read != -1){
//输出客户端发送的数据 将bytes字节数组中的数据从0开始到read(数据长度)读取到字符串中去
System.out.println("当前线程:"+ Thread.currentThread().getId()+"-"+Thread.currentThread().getName());
System.out.println(new String(bytes,0,read));
}else {
break;
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
socket.close();
System.out.println("关闭client连接");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
三、NIO 同步非阻塞模型
1.介绍NIO
服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上
,多路复用器轮询到连接有I/O请求就进行处理。
适用场景:连接数目较多且连接时间较短的架构,比如聊天服务器、弹幕系统等。
1).Selector选择器
- Java的NIO用的非阻塞的IO方式,可以用一个线程,处理多个客户端的连接,就会使用到Selector选择器。
- Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,这样就可以只用一个线程去管理多个通道,也就是管理多个连接和请求。
- 只有在连接真正有读写事件发生时,才会进行读写,就大大减少了系统开销,并且不必为每一个连接都创建一个线程,就不用维护多个线程。
- 避免了多线程之间的上下文切换导致的开销。
如下图:
2).SelectionKey
SelectionKey表示Selector和网络通道的注册关系,共四种:
OP_ACCEPT:表示有新的网络连接 值为16
OP_CONNECT:表示连接已经建立 值为8
OP_READ:代表读操作 值为1
OP_WRITE:代表写操作,值为4
3).ServerSocketChannel
ServerSocketChannel在服务器端监听新的客户端Scoket连接。
# 得到一个ServerScoketChannel通道
public static ServerSocketChannel open() {
}
# 设置服务器端端口号
public abstract ServerSocketChannel bind(SocketAddress local, int backlog){
}
# 设置阻塞或非阻塞模式 false为非阻塞 NIO编程需要设置为false
public final SelectableChannel configureBlocking(boolean block){
}
# 接收一个连接 返回这个连接的通道对象
public abstract SocketChannel accept(){
}
# 注册一个选择器 并设置监听事件
public final SelectionKey register(Selector sel, int ops,
Object att){
}
4).ScoketChannel
ScoketChannel 网络IO通道,具体负责进行读写操作,NIO把缓冲区的数据写到通道,或者吧通道中的数据写入到缓冲区。
# 往通道中写数据
public abstract int write(ByteBuffer src) {
}
# 从通道中读数据
public abstract int read(ByteBuffer dst) {
}
2.NIO和BIO的比较
- BIO 以流的方式处理数据,而NIO 以块的方式处理数据,块I/0的效率比流I/0高很
多 - BIO 是阻塞的,NIO 则是非阻塞的
- BIO基于 字节流和字符流进行操作,而NIO 基于Channel(通道)和Buffer(缓冲区)进
行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
3.NIO应用实例-群聊系统
服务器端:
客户端:
需求:
1)编写一个NIO群聊系统,实现服务器
端和客户端之间的数据简单通讯(非
阻塞)
2)实现多人群聊
3) 服务器端:可以监测用户上线,离线,
并实现消息转发功能
4)客户端:通过channel可以无阻塞发送
消息给其它所有用户,同时可以接受
其它用户发送的消息(有服务器转发得
到)
5) 目的:进一步理解NIO非阻塞网络编程
机制
服务器代码:
package com.lsh.wechat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* @author :LiuShihao
* @date :Created in 2020/9/17 10:45 上午
* @desc :
*/
public class WeChatServer {
//定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;
//初始化 构造器
public WeChatServer(){
try{
//得到选择器
selector = Selector.open();
//获得ServerSocketChannel
listenChannel = ServerSocketChannel.open();
//绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞
listenChannel.configureBlocking(false);
//将ServerSocketChannel注册到selector
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
}catch(IOException e){
e.printStackTrace();
}
}
//监听
public void listen(){
try{
//循环处理
while(true){
// int count = selector.select(2000);
int count = selector.select();
if (count>0){
//有事件要处理
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
//监听accept事件
if (key.isAcceptable()){
SocketChannel accept = listenChannel.accept();
//设置非阻塞
accept.configureBlocking(false);
//将accept注册到Selector
accept.register(selector,SelectionKey.OP_READ);
// 上线提示
System.out.println(accept.getRemoteAddress()+" 上线了");
}
if (key.isReadable()){
//通道发生 READ事件
//从通道中读取数据到BUffer中
readData(key);
}
//当前的key删除,防止重复处理
iterator.remove();
}
}else {
System.out.println("等待中...");
}
}
}catch (IOException e){
e.printStackTrace();
}
}
private void readData (SelectionKey key) {
//定义一个SocketChannel
//得到关联的channel
SocketChannel channel= (SocketChannel)key.channel();
//创建Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
int count = channel.read(byteBuffer);
if (count>0){
String msg = new String(byteBuffer.array());
System.out.println("客户端消息: "+msg);
//向其他客户端转发消息
sendInfoOther(msg,channel);
}
} catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress()+" 离线了...");
//取消注册
key.cancel();
//关闭通道
channel.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
private void sendInfoOther(String msg,SocketChannel self) throws IOException {
System.out.println("服务器转发消息");
//遍历所有注册到selectorng上的SocketorChannel 并排除自己
for (SelectionKey key : selector.keys()) {
//通过key 取出对应的SocketChannel
Channel targetChannel = key.channel();
//排除自己
if (targetChannel instanceof SocketChannel && targetChannel!= self){
SocketChannel dest = (SocketChannel)targetChannel;
//将msg存储到buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将buffer中的数据写入到通道中
dest.write(buffer);
}
}
}
public static void main(String[] args) {
WeChatServer weChatServer = new WeChatServer();
weChatServer.listen();
}
}
客户端代码:
package com.lsh.wechat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
/**
* @author :LiuShihao
* @date :Created in 2020/9/17 2:02 下午
* @desc :
*/
public class WeChatClient1 {
private static final String HOST ="127.0.0.1";
private static final int PORT = 6667;
private SocketChannel socketChannel;
private Selector selector;
private String username;
public WeChatClient1() throws IOException {
selector = Selector.open();
socketChannel = socketChannel.open(new InetSocketAddress(HOST,PORT));
socketChannel.configureBlocking(false);
//将channle注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username+"is OK...");
}
/**
* 向服务器发送消息
*/
public void snedInfo(String info){
info = username + "说:"+info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
}catch (Exception e){
e.printStackTrace();
}
}
public void readInfo(){
try {
int select = selector.select(2000);
if (select>0){
//有可以用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
if (key.isReadable()){
//得到相关通道
SocketChannel channel = (SocketChannel)key.channel();
//得到一个buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将通道中的数据读取到缓冲区
channel.read(buffer);
//把读到缓冲区的数据 转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
iterator.remove();
}
}else {
// System.out.println("没有可用的通道");
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
WeChatClient1 weChatClient = new WeChatClient1();
new Thread(()->{
while (true){
weChatClient.readInfo();
try {
Thread.currentThread(). sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s = scanner.nextLine();
weChatClient.snedInfo(s.trim());
}
}
}
Bug:
这里发生了一个bug,暂时还未解决,就是客户端下线的时候,服务器端并没有打印下线提醒。
3.NIO零拷贝
零拷贝是网络编程的关键,很多性能优化都离不开,在Java中常用的零拷贝技术有mmap(内存映射)和sendFile。
零拷贝从操作系统角度,是没有CPU拷贝。
DMA拷贝:直接内存拷贝不使用CPU
四、AIO异步非阻塞模型
1.介绍AIO
AIO引入异步通道的概念,采用了Rroactor模式,简化了程序编写,对于有效的请求才启动线程,他的特点是先由操作系统完成后才通知服务端启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
(目前应用不广泛)
使用场景:连接数目较多且连接时间较长的架构。