BIO:同步并阻塞,服务器的实现方式为一个连接一个线程,如果这个连接不做任何事情就会造成资源的浪费;连接数据较小却固定的架构的架构,这种方式对系统的性能要求高但是可贵之处在于程序容易理解
NIO:同步非阻塞模型;服务器实现模式为一个线程处理多个请求、客户端发送的请求都会到一个多路复用器(Selector),多路复用器轮询到连接有I/O请求就进行处理;适用于连接数多但是连接较短的的架构就像聊天之类的系统,他的编程比较复杂
AIO:异步非阻塞,引入异步通道的概念采用Proactor模式;处理过程的特点是先有操作系统完成后才通知服务端程序启动线程去处理,一般来说适用于连接数较多且连接时间较长的应用;连接数多且连接长
1、服务器端启动一个ServerSocket
2、客户端启动Socket对服务器进行通信,默认情况下服务器端要与每个客户端建立一个线程与之通讯
3、客户端发出请求后,先咨询服务器是否有线程相应,如果没有则会进行等待或是被拒绝
4、如果有相应客户端线程会等待请求结束再继续执行
public class MainTest {
public static void main(String[] args) throws Exception{
//1、创建一个线程池
//2、创建一个ServerSocket对端口进行监听,如果有链接就从线程池中
//得到一个线程去怼客户端的请求进行处理
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ServerSocket serverSocket = new ServerSocket(7777);
System.out.println("~~~服务器已经启动~~~");
//这里的true是一种比较暴力的实现实际上我们是可以通过设置标记位来实现
//在这里循环中程序一直在监听客户端发来的请求
while (true){
//监听等待客户端连接这里是阻塞的
final Socket socket = serverSocket.accept();
System.out.println("~~~接受到一个客户端~~~~");
cachedThreadPool.execute(()->{
handle(socket);
});
}
}
public static void handle(Socket socket){
//通过socket获取输入流
//通过byte数组的方式直接接受数据
try {
byte[] bytes = new byte[1024];
InputStream inputStream = socket.getInputStream();
//循环读取客户端发送过来的数据
while (true){
//如果没有数据也将会阻塞在这个位置
int read = inputStream.read(bytes);
if (read!=-1){
System.out.println(new String(bytes,0,read));
}
}
} catch (IOException e) {
System.out.println("关闭与Client的连接");
try {
socket.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
e.printStackTrace();
}
}
}
cmd调试命令:
telnet 127.0.0.1 7777
ctrl ]
send 内容
NIO有三大核心部分:Channel(数据——通道)、Buffer(缓冲区)、Selector(选择器)。他是面向缓冲区编程的,数据读取到一个他稍后处理的缓冲区,需要时可以在缓冲区中前后移动,这就增加了处理过程中的灵活性。**缓冲区是NIO实现非阻塞的一个重要前提保障。**通道发生的事件选择器是可以感知到的。
public class BasicBufferTest {
public static void main(String[] args) {
//创建一个buffer,大小为5,
IntBuffer intBuffer = IntBuffer.allocate(5);
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i+3);
}
//将Buffer读写切换,使得可以开始读数据,这个操作会引起Buffer中相关属性的装换
//IntBuffer继承自Buffer,Buffer根据数据类型的不同有很多实现这样做的目的是
//是为了提高操作的效率
intBuffer.flip();
while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
}
1、BIO以流的方式进行数据处理而NIO是以块的方式处理数据;这一点的思想与Mysql中页的存在不也是相似的吗
2、BIO是阻塞的,NIO是非阻塞的
3、BIO基于字节流和字符流进行操作,而NIO基于Channel与Buffer进行操作
1、程序切换到那个Channel是由事件决定的
2、Selector会根据不同的事件在各个通道上切换
3、Buffer就是一个内存块,底层是一个数组——回答了为什么Buffer分数据类型而提高效率,或者说是必须性的
4、BIO中要么是输入流要么是输出流是不能够兼而有之的;NIO中是兼而有之的但是需要通过flip方法切换
5、channel是双向的,可以放回底层操作系统的情况;eg:Linux底层操作系统通道就是双向的
boolen是没有对应的Buffer的,确实也不需要,很多都能表示他
缓冲区本质上可以理解为是一个容器对象,他内置了一些机制能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读物数据的通道但是读取或写入的数据都必须经过Buffer。hb数组,这就是buffer底层就是通过数组来进行元素存储的
public abstract class IntBuffer extends Buffer implements Comparable<IntBuffer>{
final int[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly;
}
Buffer类定义了所有缓冲区都有的四个属性:
//标记
private int mark = -1;
//位置,表示的是下一个被读或写的元素的索引,每次缓冲区数据读写时候都会改变,为下次的操作做准备
private int position = 0;
//表示缓冲区当前的终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
private int limit;
//容量
private int capacity;
limit的应用场景是什么?
limit限制的是读写指针不能超过的位置,就像下面这个函数的操作逻辑就是从写操作转换为读操作,读操作能不能超过capacity的前提下也不能够访问那些没有数据的存储位。
public final Buffer flip() {//写操作装换为读操作
limit = position;
position = 0;
mark = -1;
return this;
}
clear()方法知识重置了相关的一些属性但是并没有真正的清除原来的一些数据:
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
通道类似于流但是通道可以同时进行读写而流智能够进行读或是写
@Test //写数据
public void test01() throws Exception {
String str = "Hello,Zhang";
//创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("d:\\aa.text");
//通过fileOutputStream获取对应的fileChannel,他的真实类型是FileChannelImp
FileChannel fileChannel = fileOutputStream.getChannel();
//创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将str放入到byteBuffer
byteBuffer.put(str.getBytes());
//Buffer读写操作翻转
byteBuffer.flip();
//将byteBuffer数据写入到fileChannel
fileChannel.write(byteBuffer);
//关闭流,但是为什么只要关闭这最后的一个就行了呢
fileOutputStream.close();
}
@Test//读数据
public void test02() throws IOException {
//创建文件输入流
File file = new File("d:\\bb.text");
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel fileChannel = fileInputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//将通道中的数据读入到Buffer,这里需要理解Channel中有关write与read的使用方法
fileChannel.read(byteBuffer);
//将字节数据装换为String
//array()是将buffer底层的hb数组返回过来
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
如果不适用clear方法进行标记属性重置,当Buffer的大小小于文件的大小时就会造成文件内容读取不全,只会向目标文件中不断重复写入来源文件第一次写入缓冲区的数据。这个机制也比较容易理解,因为flip()方法将position与limit相等了,所以下一步的读写操作都不能够正常进行。至于说为什么会造成read为0这与read方法的机制相关。
@Test//一个Buffer进行读写数据
public void test03() throws IOException {
//这里面如果结合前面学习的流处理应该是可以发发挥较大的作用
FileInputStream fileInputStream = new FileInputStream("d:\\aa.text");
FileChannel fileChannelScource = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("target3.txt");
FileChannel fileChannelTarget = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(4);
int read = 0;
while (true){
//通过重置标记位将数据清零
byteBuffer.clear();
read = fileChannelScource.read(byteBuffer);
System.out.println("read-"+read);
if (read==-1){
break;
}
//将Buffer中的数据写入到target.txt
//buffer读写的过程中一定是需要进行读写转换的
//这一步的转换操作会使得limit等于position,如果没有上面的clear方法重置
//将会形成一个read=0 的死循环操作
byteBuffer.flip();
fileChannelTarget.write(byteBuffer);
}
//关闭相关的流
fileInputStream.close();
fileOutputStream.close();
}
调用的这个方法本质上来说也是采用了和上面类似的缓冲机制,只不过是这里程序作了相关的封装。
@Test
public void test04() throws Exception {
//创建相关的流
FileInputStream fileInputStream = new FileInputStream("d:\\aa.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("d:\\aa.jpg");
FileChannel fileChannelSource = fileInputStream.getChannel();
FileChannel fileChannelTarget = fileOutputStream.getChannel();
//调用接口完成装换
fileChannelTarget.transferFrom(fileChannelSource,0,fileChannelSource.size());
//关闭相关流
fileChannelSource.close();
fileChannelTarget.close();
fileInputStream.close();
fileOutputStream.close();
}
bit称为位即一个二进制数,我们称作为1bit,1byte = 8bit
长度 | 类型 | 备注 |
---|---|---|
1byte | byte | 英文一个,中文两个(取决于字符编码) |
2byte | short | |
4byte | int、float | |
8byte | long、double |
buffer的hb数组中存储的内容是有序的,自己猜测就像这几调用getInt(index)就是读取出了起始位置开始到这个类型长度范围内的内容,之后进行装换。
但是这个buffer的底层实现中红应该是采用了压缩不然的话一个int怎么之后两个byte呢
@Test//put、get测试
public void test07(){
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
byteBuffer.putChar('e');
byteBuffer.putChar('e');
byteBuffer.putInt(3);
byteBuffer.putInt(3);
//buffer进行读写翻转
byteBuffer.flip();
//实例测试-1
System.out.println(byteBuffer.getDouble());//9.34599603476989E-307
System.out.println(byteBuffer.getInt());//3
//实例测试-2
System.out.println(byteBuffer.getInt());//6619237
System.out.println(byteBuffer.getInt());//3
}
@Test//MappedByteBuffer
public void test05() throws Exception{
//MappedByteBuffer可以让文件直接在对外内存中修改,操作系统不需要再拷贝一次
RandomAccessFile randomAccessFile = new RandomAccessFile("d:\\aa.text","rw");
FileChannel fileChannel = randomAccessFile.getChannel();
//使用读写模式;可以直接修改的起始位置;映射到内存的大小(不是索引位置)
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5);
mappedByteBuffer.put(0, (byte) 'O');
randomAccessFile.close();
System.out.println("~~修改成功~~");
}
一个buffer不够使用的时候我们可以通过使用buffer数组的形式来完成缓冲
@Test//Scattering
public void test06() throws Exception{
//Scattering 将数据写入到buffer时候,可以采用buffer数组依次写入[分散]
//Gathering 从buffer读取数据时,可以采用buffer数组依次读入
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7001);
//绑定端口到socket
serverSocketChannel.socket().bind(inetSocketAddress);
//创建buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等待客户端连接(telent)
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8;//假定从客户端接收8个字节
while (true){
int byteRead = 0;
while (byteRead<messageLength){
long l = socketChannel.read(byteBuffers);
byteRead += 1;
System.out.println("ByteRead: "+byteRead);
Arrays.asList(byteBuffers).stream().map(buffer->{
return "postion="+buffer.position()+",limit="+buffer.limit();
}).forEach(System.out::println);
}
//将所有的buffer进行flip
Arrays.asList(byteBuffers).forEach(buffer->buffer.flip());
//将数据读出显示到客户端
long byteWrite = 0;
while (byteWrite<messageLength){
long l = socketChannel.write(byteBuffers);
byteWrite += 1;
}
//将所有的buffer进行clear
Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());
System.out.println("byteRead:="+byteRead+" byteWrite="+byteWrite+" messagelength"+messageLength);
}
}
Selector能够检测到多个注册的通道上是否有事件发生,这是因为多个Channel以事件的方式可以注册到同一个Selector。Selector的存在减缓了多线程之间切换的开销。读写操作都是非阻塞的,这可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起
public abstract class Selector implements Closeable {
protected Selector() { }
//得到一个选择器对象
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
public abstract boolean isOpen();
public abstract SelectorProvider provider();
public abstract Set<SelectionKey> keys();
//从内部集合中得到所有的SelectionKey
public abstract Set<SelectionKey> selectedKeys();
public abstract int selectNow() throws IOException;
//监控所有注册的通道但其中有IO操作可以进行时,将对应的SelectionKey加入到内部稽核中并返回,参数是用来设置超时时间的
public abstract int select(long timeout) throws IOException;
public abstract int select() throws IOException;
public abstract Selector wakeup();
public abstract void close() throws IOException;
}
1、当客户端连接时,会通过ServerSocketChannel得到SocketChannel
开始selector监听
2、将socketChannel注册到Selector上,register(Selector sel,int ops),一个selector上可以注册多个SocketChannel
3、注册后返回一个SelectionKey,会通过集合和改Selector关联
4、Selector进行监听select 方法,返回有事件发生通道的个数
5、进一步得到各个SelectionKey
6、通过SelectionKey获取到SocketChannel channel()
7、通过得到的channel完成业务处理
Server端代码
public class NIOServer {
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
//得到一个Selector对象
Selector selector = Selector.open();
//绑定端口6666,在服务端进行监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//把ServerSocketChannel注册到selector,关心事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while (true){
if (selector.select(1000)==0){
//没有事件发生
System.out.println("服务器等待了1s,没有连接");
continue;
}
//如果返回的大于0就获取selectionKeys集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//通过selectionKeys获取通道
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
//获取到SelectionKey
SelectionKey key = keyIterator.next();
//根据key发生的时间类型进行不同处理
if (key.isAcceptable()){
//有新的客户端进行连接
//给该客户端生成一个新的SocketChannel
//accept()是阻塞的但是在这里他会立即执行因为这里是已经有连接进来了
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
//将SocketChannel注册到selector,同时给SocketChannel关联一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()){//有通道发生了读事件,这里所得读写实际上相对于即将要发生的事情来说
//通过Key反向获取到对应的Channel
SocketChannel channel = (SocketChannel) key.channel();
//获取到该Channel关联的Buffer
ByteBuffer bufferAccept = (ByteBuffer) key.attachment();
//读取通道中的数据
channel.read(bufferAccept);
System.out.println("From 客户端"+new String(bufferAccept.array(),0,bufferAccept.position()));
}
//手动从集合中移除当前的selectionKey,防止多线程环境下的误操作
keyIterator.remove();
}
}
}
}
Client端代码
public class NIOClient {
public static void main(String[] args) throws Exception {
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//提供服务器端的ip与端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
System.out.println("因为连接需要时间,客户端不会阻塞,可以做一些其他的工作");
}
}
//连接成功,发送一些数据
String str = "mes";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//发送数据,将Buffer数据写入channel
socketChannel.write(buffer);
System.in.read();
}
}
在实现的过程中客户端与服务端都使用了Selector,用他们来管理接收与发送消息。消息发送的是指就是往特定的通道里写数据信息,在实际显现的过程中需要注意的是
1、将通道注册成为非阻塞的
2、及时释放selectionKey,否则的话会造成重复操作,这一点确实还不是很明白为什么会这样;
public class GroupChatClient {
//定义相关属性
private final String HOST = "127.0.0.1";
private final int PORT = 6667;
private Selector selector;
private SocketChannel socketChannel;
private String userName;
//构造器
public GroupChatClient() throws IOException {
selector = Selector.open();
//连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress(HOST,PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//将channel注册到selector
socketChannel.register(selector,SelectionKey.OP_READ);
//得到UserName
userName = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(userName+"is Ok~~~");
}
//向服务器发送消息
public void sendInfo(String info){
info = userName+"说:"+info;
try{
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
}catch (Exception e){
e.printStackTrace();
}
}
//读取从服务器端恢复的消息
public void readInfo(){
try{
int readChannels = selector.select();
if (readChannels>0){
//有可用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if (key.isReadable()){
//得到相关通道
SocketChannel sc = (SocketChannel) key.channel();
//得到一个buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取
sc.read(buffer);
//把读到缓冲区的数据装换为字符串
String mes = new String(buffer.array());
System.out.println(mes.trim());
}
//及时移除,避免重复操作,否则将造成消息无法读取
iterator.remove();
}
}
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
//启动客户端
GroupChatClient chatClient = new GroupChatClient();
//启动一个单线程,读取服务器发过来的数据
new Thread(){
public void run(){
while (true){
chatClient.readInfo();
try{
Thread.currentThread().sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
}
}
}.start();
//发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}
public class GroupChatServer {
//定义相关属性
private Selector selector;
private ServerSocketChannel listenChannel;//客户端监听
private static final int PORT = 6667;
//构造器,初始化工作
public GroupChatServer(){
try {
selector = Selector.open();
listenChannel = ServerSocketChannel.open();
//绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞模式
listenChannel.configureBlocking(false);
//将该listenChannel注册到Selector
listenChannel.register(selector,SelectionKey.OP_ACCEPT);
}catch (IOException ioException){
ioException.printStackTrace();
}
}
//监听方法
public void listen(){
try{
//循环处理
while (true){
int count = selector.select();
if(count>0){//表示有事件需要处理
//遍历得到SelectionKey
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
//取出selectionKey
SelectionKey key = iterator.next();
//监听到accept
if (key.isAcceptable()){
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
//将sc注册到selector
sc.register(selector,SelectionKey.OP_READ);
//提示
System.out.println(sc.getRemoteAddress()+"上线");
}
//监听到read
if(key.isReadable()){
//处理客户端的信息并实现转发,并且排除自己
readData(key);
}
//将当前的key从集合中删除防止重复操作
iterator.remove();
}
}else{
System.out.println("等待中~~~~~~");
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
}
}
//读取客户端消息
private void readData(SelectionKey key){
//定义一个SocketChannel
SocketChannel channel = null;
try{
//取到关联的channel
channel = (SocketChannel) key.channel();
//创建缓冲
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
//根据count的值对是否读取到数据进行逻辑处理
if(count>0){
//把缓冲区的数据转换成字符串
String mes = new String(buffer.array(),0,buffer.position());
//输出该消息
System.out.println("from 客户端:"+mes);
//向其他客户端发送消息
sendInfoToOtherClients(mes,channel);
}
}catch (IOException e){
try {
System.out.println(channel.getRemoteAddress()+"离线");
//取消注册
key.cancel();
//关闭通道
channel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
//转发消息给其他客户
private void sendInfoToOtherClients(String msg,SocketChannel self) throws IOException{
System.out.println("服务器转发消息中");
//遍历所有注册到selector上的SocketChannel,并排除self
for (SelectionKey key:selector.keys()){
//通过key取出对应的sockChannel
Channel targetChannel = key.channel();
//排除自己
if (targetChannel instanceof SocketChannel && targetChannel!=self){
//转型
SocketChannel dest = (SocketChannel)targetChannel;
//将mes,存储到Buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将buffer数据写入到通道
dest.write(buffer);
}
}
}
public static void main(String[] args) {
//创建一个服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
//通过key取出对应的sockChannel
Channel targetChannel = key.channel();
//排除自己
if (targetChannel instanceof SocketChannel && targetChannel!=self){
//转型
SocketChannel dest = (SocketChannel)targetChannel;
//将mes,存储到Buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将buffer数据写入到通道
dest.write(buffer);
}
}
}
public static void main(String[] args) {
//创建一个服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
### 10、NIO与零拷贝