java网络通信 本笔记来源
黑马C++网络编程
黑马java Nio bio aio基础讲解
网络基础 1.mac地址 网卡->物理地址 通常不会重复 虚拟网卡除外 48位 6字节
00:00:00:00:00:00
2.ip地址 标识主机Id 为虚拟的 有ipv4(32位) ipv6(64位)
分为子网ID 和主机 ID 其中的计算需要子网掩码搭配 netmask
ip中连续的1覆盖的位 子网ip
连续0覆盖的位 主机id
ip:(二进制)10.1.1.2 ->0000 1010 0000 0001 0000 0001 0000 0010
netmask->255.255.255.0->1111 1111 1111 1111 1111 1111 0000 0000
10.1.1 子网id 主机id2
网段地址:10.1.1.0
广播地址:10.1.1.255
可设置范围
1-254
3.ping 127.0.0.1->254 都是属于对本机进行联通
5.ip netmask设置 linux中
1 sudo ifconfig ens33 192.168 .26 .33 netmask 255.255 .255 .0
6.桥接和nat的区别
桥接(Bridge)是用于连接不同网络段的设备,主要在数据链路层工作,转发数据帧并实现不同网络之间的通信;
NAT(网络地址转换,Network Address Translation) 是一种在网络层使用的技术,主要用于将私有网络地址转换为公网地址,或者将公网地址映射到多个私有网络地址,从而实现内外网的通信。NAT通常用于路由器或防火墙设备,帮助多个设备共享一个公网IP地址,解决IPv4地址不足的问题。
7.端口 每个程序都有一个/多个端口 作用:用来标识应用程序
port:2字节 0-65535 其中0-1023知名端口 自定义端口1024-65535
端口类似于进程号,同一时刻只能只能标志一个,可以重复使用
8.网络分层模型
物理层:双绞线的接口类型 光纤的传输速度 数据链路层:mac 负责收发数据 网络层:ip给两台主机 提供路径选择 传输层:port 区分数据递送到哪一个应用层 会话层:建立链接 表示层:解码 应用层:拿到数据 物 数 网 传 会 话 层
开发中 四层 TCP/IP模型
8.协议 规定数据传输的方式和格式
应用层协议 ftp:文件传输协议 http:超文本传输协议 省 传输层协议 tcp:传输控制协议 udp:用户数据报协议
网络层 ip:因特网互联协议 icmp:因特网控制报文协议 ping命令 ugmp:因特网管理协议】链路层 arp:地址解析协议 通过Ip找mac rarp:反向地址解析协议 通过mac找ip
ttl->最大在网络出游走数
TCP/IP中的TTL 原创 TTL是IP协议包中的一个值,它告诉网络,数据包在网络中的时间是否太长而应被丢弃 。
防止在路由器内循环
9.组包过程 组包。简单的说就是tcp协议把过大的数据包分成了几个小的包传输,接收方要把同一组的数据包重新组合成一个完整的数据包。
圆圈内通过
以前笔记的图片丢失想不起来了
https://blog.csdn.net/Chaman1378/article/details/107160327
10.arp通信 地址解析协议 ip找mac地址
a->广播发送信息 传给b b返回mac(局域网)
11.网络模式 B/S 网页/服务器 开发周期段
CS 客户端/服务器->客户端计算 容易开挂 开发时间长
12.socket通信流程 tcp服务器通信步骤 1.创建套接字 2.绑定 3.监听 4.提取 5.读写 6.关闭
服务器创建api
给套接字绑定固定的端口和ip
Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
在网络通信中,套接字一定是成对出现的。 一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
13.粘包 对服务器客户端等函数进行报错处理 以及简化处理
缓冲区内 由于没发送完毕另外一个包就来了直接挤占前一个包的缓冲区(被信号打断的情况
解决方式 1.约定 一次发送固定字节数 2.数据结尾要\n 3.头部加上数据的大小
14三次握手
三次握手 我们如果要设计一个通信软件 如和设计 设计 连接性 1.创建套接字 ->让套接字中包含服务器的ip和端口进行链接 链接实现
1 发送链接请求 2 收到链接请求 并且链接客户端 3 测试是否发送成功 设计处三次请求
如果丢包了呢 TCP 第一次握手的 SYN 丢包了,会发生了什么? 场景 客户端链接服务器 但是服务器被D死了 重复发送 第一次超时1s 第2次超时 4s 一次增加 当超过最大重传次数后,客户端不再发送 SYN 包。
内核中定义超时请求次数 5次 那么就发送5次 时间会叠加
TCP 第二次握手的 SYN、ACK 丢包了,会发生什么? 场景 客户端链接服务器 服务器防火墙禁止数据链接
客户端:无法收到syn ack包 超时重传 达到内核设置次数 就终止 服务器:服务器能收但是不能传 服务器回syn ack 但是客户端收不到 服务器就等待客户端回第三次握手的ack 超时重传
TCP 第三次握手的 ACK 包丢了,会发生什么?
场景 客户端链接服务器 但是服务器被D死了
客户端状态:已完成 TCP 连接建立,处于 ESTABLISHED 状态-> 手法发送数据 ——>到达不了 自动断开 依靠保活机制
服务端状态:处于 SYN_RECV 如果一直收不到ack包则断开链接 ->重发syn ack包 到达次数就断开链接
15四次挥手 1 2 3 4 5 6 7 8 9 10 11 12 13 第一次挥手(Active Close ): 客户端发送一个FIN (Finish )报文给服务器,表示客户端不再发送数据。客户端进入FIN_WAIT _ 1 状态,等待服务器的确认。 第二次挥手(Passive Close ): 服务器收到客户端的FIN 报文后,发送一个ACK (Acknowledge )报文作为确认,表示已经收到了客户端的关闭请求。服务器进入CLOSE_WAIT 状态。此时,服务器可能还有未发送完的数据,因此仍可以发送数据给客户端。 第三次挥手(Passive Close ): 服务器发送一个FIN 报文给客户端,表示服务器也准备关闭连接。同时,服务器通知客户端:我已经没有数据要发送了。服务器进入LAST_ACK 状态。 第四次挥手(Active Close ): 客户端收到服务器的FIN 报文后,发送一个ACK 报文作为确认。客户端进入TIME_WAIT 状态,并等待一段时间(两个最大报文段生存时间的时间),以确保服务器接收到了ACK 报文。 在等待时间结束后,客户端关闭了连接,进入CLOSED 状态。服务器接收到ACK 报文后,也关闭了连接,进入CLOSED 状态。
服务器在发送最后一个FIN报文后,并不会立即断开连接,而是进入LAST_ACK状态。在LAST_ACK状态下,服务器等待客户端发送确认的ACK报文。
在接收到客户端的确认ACK报文后,服务器才会关闭连接并进入CLOSED状态。这个过程中,服务器也需要等待一段时间以确保客户端接收到ACK报文,避免出现网络延迟或丢包导致的问题。
因此,可以说服务器在发送最后一个FIN报文后并不立即断开连接,而是等待客户端的确认ACK报文后才关闭连接。这样做是为了确保连接的正常关闭,并保证数据的可靠传输。
如果服务器最后发送的FIN报文没有收到客户端的ACK报文,那么服务器就无法确认连接已经被关闭,因此不能立即关闭连接。在这种情况下,服务器会等待一段时间(通常是2倍的最大报文段生存时间)以确认ACK报文是否丢失或者延迟到达。
如果在等待时间结束后服务器仍然没有收到客户端的ACK报文,那么服务器就会强制关闭连接。在强制关闭连接后,服务器可能会向上层应用程序或者其他系统发送错误报告或日志,以提醒管理员发现和修复问题。
需要注意的是,在正常情况下,四次挥手过程中服务器不会强制关闭连接,除非发送的FIN报文丢失或者延迟到达导致ACK报文没有及时到达。四次挥手过程中的等待时间是为了保证数据传输的可靠性和完整性,并尽可能避免数据丢失或损坏。
16 滑动窗口 mms
mss 出现三次握手前两次 告知对方发送数据最大长度 MTU 跟网卡有关系 一帧最大传输单元
17.tcp浏览控制 TCP报文窗口尺寸 发送报文者的最大缓冲区
三次握手第2次告诉 发送流程
18.tcp状态 概念图
简化图
简化图未考虑特殊情况
netstat 命令(了解)
19 什么是套接字 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 1 .客户端与服务端 如何靠sock进行通信 服务器端的返回的套接字是不是客户端的套接字 服务器端 创建监听套接字socket 绑定bind 监听 listen 提取accept 客户端 创建链接套接字 链接socket 是用来实现网络传输功能的,它负责不同主机进程之间的网络通信连接1 .寻找 结构体 ip+端口 实现进程通信2 .协议选择 创建多个数据结构继承sock 例如 负责udp协议 udp_sock3 .sock sock进行网络传输,对网卡进行操作 需在内核中 我们将各类sock封装成文件 并返回一个文件句柄 fd 使得应用层可以靠文件句柄进行操作 接口 send bind listen connect recv 处在用户态的程序通过 socket 提供的接口,将网络传输的这部分工作外包给了 Linux 内核4 .总结sock是什么 sock是处于内核的一种数据结构 用来实现网络传输 因为协议不同 衍生了各类sock 同时sock会在linux中生成为文件 提高接口 应用层 创建sock 绑定文件描述符 让应用层可以调用接口
20.总结 一年前学习的网络基础,同时大部分笔记图片消失,Io复用基于的cpp不进行探究
但是一年前讲cpp的老师也是选择讲java的一个人
Java 数据交换 I/O 模型:就是用什么样的通道或者说是通信模式和架构进行数据的传输和接收,很大程度上决定了程序通信的性能,Java 共支持 3 种网络编程的/IO 模型:BIO、NIO、AIO 实际通信需求下,要根据不同的业务场景和性能需求决定选择不同的I/O模型
1.bio 同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器 端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销 【简单示意图
2.nio Java NIO : 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注 册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理 【简单示意图】
3.aio Java AIO(NIO.2) : 异步 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较 多且连接时间较长的应用
4.总结
BIO Bio介绍
Java BIO 就是传统的 java io 编程,其相关的类和接口在 java.io
BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需 要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).
bio工作机制
对 对 BIO 编程流程的梳理
服务器端启动一个 ServerSocket ,注册端口,调用accpet方法监听客户端的Socket连接。
绑定 监听 提取 读写
客户端启动 Socket 对服务器进行通信,默认情况下服务器端需要对每个客户 建立一个线程与之通讯
链接 读写
bio编程实例回顾 网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑定IP地址和端口) ,客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。
传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。 基于BIO模式下的通信,客户端 - 服务端是完全同步,完全耦合的 。
1.单行发送 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class ser { public static void main (String[] args) { System.out.println("ser" ); try { ServerSocket sersocket=new ServerSocket (9999 ); Socket soccket = sersocket.accept(); System.out.println("链接成功" ); InputStream is=soccket.getInputStream(); BufferedReader br = new BufferedReader (new InputStreamReader (is)); String msg; while ((msg=br.readLine())!=null ){ System.out.println("客户端说:" +msg); } } catch (IOException e) { throw new RuntimeException (e); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class cli { public static void main (String[] args) { System.out.println("cli" ); try { Socket socket = new Socket ("127.0.0.1" , 9999 ); System.out.println("链接成功" ); OutputStream os = socket.getOutputStream(); PrintStream ps=new PrintStream (os); ps.println("你好" ); ps.flush(); } catch (UnknownHostException e) { throw new RuntimeException (e); } catch (IOException e) { throw new RuntimeException (e); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Exception in thread "main" java.lang .RuntimeException : java.net .SocketException : Connection reset at oenday.ser .main (ser.java :33 ) Caused by: java.net .SocketException : Connection reset at java.net .SocketInputStream .read (SocketInputStream.java :209 ) at java.net .SocketInputStream .read (SocketInputStream.java :141 ) at sun.nio .cs .StreamDecoder .readBytes (StreamDecoder.java :284 ) at sun.nio .cs .StreamDecoder .implRead (StreamDecoder.java :326 ) at sun.nio .cs .StreamDecoder .read (StreamDecoder.java :178 ) at java.io .InputStreamReader .read (InputStreamReader.java :184 ) at java.io .BufferedReader .fill (BufferedReader.java :161 ) at java.io .BufferedReader .readLine (BufferedReader.java :324 ) at java.io .BufferedReader .readLine (BufferedReader.java :389 ) at oenday.ser .main (ser.java :26 )
双方没有规范退出
2.多行发送 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class cli { public static void main (String[] args) { System.out.println("cli" ); try { Socket socket = new Socket ("127.0.0.1" , 9999 ); System.out.println("链接成功" ); 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 (UnknownHostException e) { throw new RuntimeException (e); } catch (IOException e) { throw new RuntimeException (e); } } }
3.接收多个客户端-多线程 改造思路
写法1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class ser { public static void main (String[] args) { System.out.println("ser" ); try { ServerSocket sersocket=new ServerSocket (9999 ); while (true ) { Socket soccket = sersocket.accept(); System.out.println("链接成功" +soccket.getInetAddress()); new Thread (){ private Socket socketInstance = soccket; public void run () { try { InputStream is=soccket.getInputStream(); BufferedReader br = new BufferedReader (new InputStreamReader (is)); String msg; while ((msg=br.readLine())!=null ){ System.out.println("客户端说:" +msg); } } catch (IOException e) { throw new RuntimeException (e); } } }.start(); } } catch (IOException e) { throw new RuntimeException (e); } } }
写法2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class ServerReadThread extends Thread { private Socket socket; ServerReadThread (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 (IOException e) { throw new RuntimeException (e); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 public class ServerDemo { public static void main (String[] args) throws Exception { System.out.println("==服务器的启动==" ); ServerSocket serverSocket = new ServerSocket (7777 ); while (true ){ Socket socket = serverSocket.accept(); new ServerReadThread (socket).start(); System.out.println(socket.getRemoteSocketAddress()+"上线了!" ); } } }
问题
1.每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;
2.每个线程都会占用栈空间和CPU资源;
3.并不是每个socket都进行IO操作,无意义的线程处理;
4.客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
4.伪异步IO 在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
接下来我们采用一个伪异步I/O的通信框架,
采用线程池和任务队列实现
当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。
JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
图示如下:
线程池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class HandlerSocketThreadPool { private ExecutorService executor; HandlerSocketThreadPool (int maxPoolSize,int queueSize) { this .executor=new ThreadPoolExecutor ( maxPoolSize, maxPoolSize, 120L , TimeUnit.SECONDS, new ArrayBlockingQueue <Runnable>(queueSize) ); } public void execute (Runnable r) { this .executor.execute (r); } }
服务端编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class ReaderClientRunnable implements Runnable { private Socket socket; ReaderClientRunnable(Socket socket) { this .socket=socket; } @Override public void run() { InputStream is = null ; try { is = socket.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is )); String msg; while ((msg=br.readLine())!=null ){ System.out.println("客户端说:" +msg); } } catch (IOException e) { throw new RuntimeException(e); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class ser { public static void main (String[] args ) { System.out .println("ser" ); try { ServerSocket sersocket=new ServerSocket(9999 ); HandlerSocketThreadPool handlerSocketThreadPool = new HandlerSocketThreadPool(3 , 1000 ); while (true ) { Socket soccket = sersocket.accept(); System.out .println("链接成功" +soccket.getInetAddress()); handlerSocketThreadPool.execute(new ReaderClientRunnable(soccket)); } } catch (IOException e) { throw new RuntimeException(e); } } }
伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续socket的i/o消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。
5.bio文件传递 cli
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class cli { public static void main (String [] args) { try { Socket socket = new Socket ("127.0.0.1" , 9999 ); InputStream is=new FileInputStream ("D:\\调试信息.txt" ); DataOutputStream dos = new DataOutputStream (socket.getOutputStream ()); dos.writeUTF (".txt" ); byte [] buffer = new byte [1024 ]; int len; while ((len=is.read (buffer))>0 ) { dos.write (buffer,0 ,len); } dos.flush (); Thread.sleep (1000 ); } catch (IOException | InterruptedException e) { throw new RuntimeException (e); } } }
ser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class ServerReaderThread extends Thread { private Socket socket; ServerReaderThread(Socket socket) { this .socket = socket; } @Override public void run () { try { DataInputStream dis = new DataInputStream(socket.getInputStream()); String suffx=dis.readUTF(); System.out .println("服务端已经成功接收到了文件类型:" + suffx); OutputStream out =new FileOutputStream("I:\\mp3" ); byte [] buffer = new byte [1024 ]; int len; while ((len=dis.read(buffer))>0 ) { out .write(buffer,0 ,len); } out .close(); System.out .println("服务端接收文件保存成功!" ); } catch (IOException e) { throw new RuntimeException(e); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ser { public static void main (String[] args) { try { ServerSocket serverSocket = new ServerSocket (9999 ); while (true ) { Socket clientSocket = serverSocket.accept(); new ServerReaderThread (clientSocket).run(); } } catch (IOException e) { throw new RuntimeException (e); } } }
6.端口转发思想 需求:需要实现一个客户端的消息可以发送给所有的客户端去接收 。(群聊实现)
客户端接收文件
服务端
1 List<Socket> sockets = new ArrayList<>()
1 2 3 4 5 6 7 8 while (true ) { Socket clientSocket = serverSocket.accept(); sockets.add (clientSocket); new ServerReaderThread(sockets,clientSocket).run(); }
NIO 基本介绍 NIO支持面向缓冲区 的、基于通道 的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO
NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
NIO 有三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)
Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况,可以分配20 或者 80个线程来处理。不像之前的阻塞 IO 那样,非得分配 1000 个。
*
BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
BIO 是阻塞的,NIO 则是非阻塞的
BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道 读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
Nio三大核心 1.Buffer缓冲区 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API更加容易操作和管理。
2.Channel Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。 通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。
3.Selector Selector是 一个Java NIO组件,可以能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率
每个 channel 都会对应一个 Buffer
一个线程对应Selector , 一个Selector对应多个 channel(连接)
程序切换到哪个 channel 是由事件决定的
Selector 会根据不同的事件,在各个通道上切换
Buffer 就是一个内存块 , 底层是一个数组
数据的读取写入是通过 Buffer完成的 , BIO 中要么是输入流,或者是输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写。
Java NIO系统的核心在于:通道(Channel)和缓冲区 (Buffer)。
通道表示打开到 IO 设备(例如:文件、 套接字)的连接。
若需要使用 NIO 系统,需要获取 用于连接 IO 设备的通道以及用于容纳数据的缓冲 区。
然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据
Nio-Buffer 1.介绍 一个用于特定基本数据类 型的容器。由 java.nio 包定义的,所有缓冲区 都是 Buffer 抽象类的子类.。Java NIO 中的 Buffer 主要用于与 NIO 通道进行 交互,数据是从通道读入缓冲区,从缓冲区写入通道中的
2.子类及本身 Buffer 就像一个数组,可以保存多个相同类型的数据。根 据数据类型不同 ,有以下 Buffer 常用子类:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
上述 Buffer 类 他们都采用相似的方法进行管理数据,只是各自 管理的数据类型不同而已。都是通过如下方法获取一个 Buffer 对象:
1 static XxxBuffer allocate (int capacity) : 创建一个容量为capacity 的 XxxBuffer 对象
3.基本属性 Buffer 中的重要概念:
容量 (capacity) :作为一个内存块,Buffer具有一定的固定大小,也称为”容量”,缓冲区容量不能为负,并且创建后不能更改。
限制 (limit):表示缓冲区中可以操作数据的大小 (limit 后数据不能进行读写) 。缓冲区的限制不能为负,并且不能大于其容量。 写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量 。
位置 (position): 下一个要读取或写入的数据的索引 。缓冲区的位置不能为 负,并且不能大于其限制
**标记 (mark)与重置 (reset)**:标记是一个索引,通过 Buffer 中的 mark() 方法 指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这 个 position.
标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
4.常见方法 1 2 3 4 5 6 7 8 9 10 11 12 Buffer clear() 清空缓冲区并返回对缓冲区的引用 Buffer flip() 为 将缓冲区的界限设置为当前位置,并将当前位置充值为 0 int capacity() 返回 Buffer 的 capacity 大小 boolean hasRemaining() 判断缓冲区中是否还有元素 int limit() 返回 Buffer 的界限(limit) 的位置 Buffer limit(int n) 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象 Buffer mark() 对缓冲区设置标记 int position() 返回缓冲区的当前位置 position Buffer position(int n) 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象 int remaining() 返回 position 和 limit 之间的元素个数 Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置 Buffer rewind() 将位置设为为 0, 取消设置的 mark
5.缓冲区数据操作 1 2 3 4 5 6 7 8 9 10 Buffer 所有子类提供了两个用于数据操作的方法:get ()put () 方法 取获取 Buffer中的数据get () :读取单个字节get (byte[] dst) :批量读取多个字节到 dst 中get (int index) :读取指定索引位置的字节(不会移动 position ) 放到 入数据到 Buffer 中 中put (byte b) :将给定单个字节写入缓冲区的当前位置put (byte[] src) :将 src 中的字节写入缓冲区的当前位置put (int index, byte b) :将指定字节写入缓冲区的索引位置(不会移动 position )
使用Buffer读写数据一般遵循以下四个步骤:
1.写入数据到Buffer
2.调用flip()方法,转换为读取模式 ///为 将缓冲区的界限设置为当前位置,并将当前位置充值为 0
3.从Buffer中读取数据
4.调用buffer.clear()方法或者buffer.compact()方法清除缓冲区
6.案例演示 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 package nio1;import org.junit.Test;import java.nio.ByteBuffer;public class TestBuffer { @Test public void test3 () { ByteBuffer buf = ByteBuffer.allocateDirect(1024 ); System.out.println(buf.isDirect()); } @Test public void test2 () { String str = "itheima" ; ByteBuffer buf = ByteBuffer.allocate(1024 ); buf.put(str.getBytes()); buf.flip(); byte [] dst = new byte [buf.limit()]; buf.get(dst, 0 , 2 ); System.out.println(new String (dst, 0 , 2 )); System.out.println(buf.position()); buf.mark(); buf.get(dst, 2 , 2 ); System.out.println(new String (dst, 2 , 2 )); System.out.println(buf.position()); buf.reset(); System.out.println(buf.position()); if (buf.hasRemaining()){ System.out.println(buf.remaining()); } } @Test public void test1 () { String str = "itheima" ; ByteBuffer buf = ByteBuffer.allocate(1024 ); System.out.println("-----------------allocate()----------------" ); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); buf.put(str.getBytes()); System.out.println("-----------------put()----------------" ); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); buf.flip(); System.out.println("-----------------flip()----------------" ); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); byte [] dst = new byte [buf.limit()]; buf.get(dst); System.out.println(new String (dst, 0 , dst.length)); System.out.println("-----------------get()----------------" ); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); buf.rewind(); System.out.println("-----------------rewind()----------------" ); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); buf.clear(); System.out.println("-----------------clear()----------------" ); System.out.println(buf.position()); System.out.println(buf.limit()); System.out.println(buf.capacity()); System.out.println((char )buf.get()); } }
7.直接与非直接缓冲区 什么是直接内存与非直接内存
根据官方文档的描述:
byte byffer可以是两种类型,一种是基于直接内存(也就是非堆内存) ;
另一种是非直接内存(也就是堆内存)。
对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理。
从数据流的角度,非直接内存是下面这样的作用链:
而直接内存是:
很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。直接内存使用allocateDirect 创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。所以呢,当你有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。
使用场景
1 有很大的数据需要存储,它的生命周期又很长
2 适合频繁的IO操作,比如网络并发场景
Nio-channel 1.概述 通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
1、 NIO 的通道类似于流,但有些区别如下:
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
通道可以从缓冲读数据,也可以写数据到缓冲:
2、BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel) 是双向的,可以读操作,也可以写操作。
3、Channel 在 NIO 中是一个接口
1 public interface Channel extends Closeable {}
2.channel实现类
FileChannel :用于读取、写入、映射和操作文件的通道。
DatagramChannel :通过 UDP 读写网络中的数据通道。
SocketChannel :通过 TCP 读写网络中的数据。
ServerSocketChannel :可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel 。 【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】
3.FileChannel类 获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket 获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道
4.常用方法 1 2 3 4 5 6 7 8 9 int read (ByteBuffer dst) 从 从 Channel 到 中读取数据到 ByteBufferlong read (ByteBuffer[] dsts) 将 将 Channel 到 中的数据“分散”到 ByteBuffer[]int write (ByteBuffer src) 将 将 ByteBuffer 到 中的数据写入到 Channellong write (ByteBuffer[] srcs) 将 将 ByteBuffer[] 到 中的数据“聚集”到 Channellong position () 返回此通道的文件位置 FileChannel position (long p) 设置此通道的文件位置long size () 返回此通道的文件的当前大小 FileChannel truncate (long s) 将此通道的文件截取为给定大小void force (boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中
5.本地读写文件 需求:使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 “hello,黑马Java程序员!” 写入到 data.txt 中.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ChannelTest { @Test public void write () { try { FileOutputStream fos = new FileOutputStream ("data01.txt" ); FileChannel channel = fos.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024 ); buffer.put("hello,黑马Java程序员!" .getBytes()); buffer.flip(); channel.write(buffer); channel.close(); System.out.println("写数据到文件中!" ); } catch (Exception e) { e.printStackTrace(); } } }
本地读
1.获取输入流 获取通道
2.开辟缓存区-将通道数据传到缓冲区
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void read () throws Exception { FileInputStream is = new FileInputStream ("data01.txt" ); FileChannel channel = is.getChannel (); ByteBuffer buffer = ByteBuffer.allocate (1024 ); channel.read (buffer); buffer.flip (); String rs = new String (buffer.array (),0 ,buffer.remaining ()); System.out.println (rs); }
6.文件拷贝完成 使用 FileChannel(通道) ,完成文件的拷贝。
1.打开文件=>获取输入流=>获取通道
2.打开文件 =>获取输出流=>获取通道
1 2 3 4 5 6 7 8 9 10 File srcFile = new File ("C:\\ Users\\ dlei\\ Desktop\\ BIO,NIO,AIO\\ 文件\\ 壁纸.jpg" ); File destFile = new File ("C:\\ Users\\ dlei\\ Desktop\\ BIO,NIO,AIO\\ 文件\\ 壁纸new.jpg" ); FileInputStream fis = new FileInputStream (srcFile); FileOutputStream fos = new FileOutputStream (destFile); FileChannel isChannel = fis.getChannel(); FileChannel osChannel = fos.getChannel();
3.开辟缓冲区=>1往缓冲区赛值 =>2往缓冲区放值-写入
1 2 // 分配缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 while (true ){ buffer.clear (); int flag = isChannel.read (buffer); if (flag == -1 ){ break ; } buffer.flip (); osChannel.write (buffer); } isChannel.close (); osChannel.close (); System.out.println ("复制完成!" );
7.分散和聚合 分散读取(Scatter ):是指把Channel通道 的数据读入到多个缓冲区 中去
聚集写入(Gathering )是指将多个 Buffer 中的数据“聚集”到 Channel 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Test public void test () throws IOException { RandomAccessFile raf1 = new RandomAccessFile ("1.txt" , "rw" ); FileChannel channel1 = raf1.getChannel(); ByteBuffer buf1 = ByteBuffer.allocate(100 ); ByteBuffer buf2 = ByteBuffer.allocate(1024 ); ByteBuffer[] bufs = {buf1, buf2}; channel1.read(bufs); for (ByteBuffer byteBuffer : bufs) { byteBuffer.flip(); } System.out.println(new String (bufs[0 ].array(), 0 , bufs[0 ].limit())); System.out.println("-----------------" ); System.out.println(new String (bufs[1 ].array(), 0 , bufs[1 ].limit())); RandomAccessFile raf2 = new RandomAccessFile ("2.txt" , "rw" ); FileChannel channel2 = raf2.getChannel(); channel2.write(bufs); }
1 2 3 我毒药是谁我毒药是谁我毒药是谁我毒药是谁我毒药是谁我毒药是谁我毒药� ----------------- ��谁我毒药是谁我毒药是谁我毒药是谁我毒药是谁我毒药是谁我毒药是谁我毒药是谁我毒药是谁我毒药是谁我毒药是谁我毒药是谁
8.transferFrom() 从目标通道中去复制原通道数据
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test02 () throws Exception { FileInputStream is = new FileInputStream ("data01.txt" ); FileChannel isChannel = is.getChannel(); FileOutputStream fos = new FileOutputStream ("data03.txt" ); FileChannel osChannel = fos.getChannel(); osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size()); isChannel.close(); osChannel.close(); }
9.transferTo() 把原通道数据复制到目标通道
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test02 () throws Exception { FileInputStream is = new FileInputStream ("data01.txt" ); FileChannel isChannel = is.getChannel(); FileOutputStream fos = new FileOutputStream ("data04.txt" ); FileChannel osChannel = fos.getChannel(); isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel); isChannel.close(); osChannel.close(); }
Nio-选择器 Selector 1.选择器介绍 选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心
Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector (选择器)
Selector 能够检测多个注册的通道上是否有事件发生 (注意:多个 Channel 以事件的方式可以注册到同一个 Selector ),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管 理多个通道,也就是管理多个连接和请求。
只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都 创建一个线程,不用去维护多个线程
避免了多线程之间的上下文切换导致的开销
2.选择器应用 创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。
1 Selector selector = Selector.open();
向选择器注册通道:SelectableChannel.register(Selector sel, int ops)
1 2 3 4 5 6 7 8 9 10 ServerSocketChannel ssChannel = ServerSocketChannel.open(); ssChannel.configureBlocking(false ); ssChannel.bind(new InetSocketAddress (9898 ));Selector selector = Selector.open(); ssChannel.register(selector, SelectionKey.OP_ACCEPT);
当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):
读 : SelectionKey.OP_READ (1)
写 : SelectionKey.OP_WRITE (4)
连接 : SelectionKey.OP_CONNECT (8)
接收 : SelectionKey.OP_ACCEPT (16)
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
nio网络通信解析 Selector 示意图和特点说明
Selector可以实现: 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
服务端流程
当客户端连接服务端时,服务端会通过 ServerSocketChannel 得到 SocketChannel:
获取通道
1 ServerSocketChannel ssChannel = ServerSocketChannel.open()
2、切换非阻塞模式
1 ssChannel.configureBlocking(false)
3、绑定连接
1 ssChannel.bind(new InetSocketAddress(9999 ))
4.获取选择器
1 Selector selector = Selector .open();
5、 将通道注册到选择器上, 并且指定“监听接收事件”
1 ssChannel.register (selector, SelectionKey.OP_ACCEPT);
6.轮询式的获取选择器上已经“准备就绪”的事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 while (selector.select () > 0 ) { System.out .println("轮一轮" ); Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey sk = it.next(); if (sk.isAcceptable()) { SocketChannel sChannel = ssChannel.accept(); sChannel.configureBlocking(false ); sChannel.register(selector, SelectionKey.OP_READ); } else if (sk.isReadable()) { SocketChannel sChannel = (SocketChannel) sk.channel(); ByteBuffer buf = ByteBuffer.allocate(1024 ); int len = 0 ; while ((len = sChannel.read(buf)) > 0 ) { buf.flip(); System.out .println(new String(buf.array(), 0 , len)); buf.clear(); } } it.remove (); } } }
客户端
1.获取通道
1 SocketChannel sChannel = SocketChannel.open (new InetSocketAddress("127.0.0.1" , 9999 ))
2.切换非阻塞模式
1 sChannel.configureBlocking(false)
3.分配指定大小的缓冲区
1 ByteBuffer buf = ByteBuffer.allocate(1024 )
4.发送数据给服务端
1 2 3 4 5 6 7 8 9 10 11 Scanner scan = new Scanner(System .in );while (scan.hasNext()){ String str = scan.nextLine(); buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System .currentTimeMillis()) + "\n" + str).getBytes()); buf.flip(); sChannel.write (buf); buf.clear(); } //关闭通道 sChannel.close ();
nio网络通信案例代码 ser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class nioser { public static void main (String [] args) { try { ServerSocketChannel ssChannel= ServerSocketChannel.open (); ssChannel.bind (new InetSocketAddress (9999 )); ssChannel.configureBlocking (false ); Selector selector = Selector.open (); ssChannel.register (selector, SelectionKey.OP_ACCEPT); while (selector.select ()>0 ) { Iterator<SelectionKey> key = selector.selectedKeys ().iterator (); while (key.hasNext ()) { SelectionKey selKey = key.next (); if (selKey.isAcceptable ()) { SocketChannel sChannel = ssChannel.accept (); sChannel.configureBlocking (false ); sChannel.register (selector,SelectionKey.OP_READ); System.out.println ("有新链接上线了" ); }else if (selKey.isReadable ()) { SocketChannel sChannel = (SocketChannel) selKey.channel (); ByteBuffer buffer = ByteBuffer.allocate (1024 ); int len = 0 ; while ((len=sChannel.read (buffer))>0 ) { buffer.flip (); System.out.println (new String (buffer.array (),0 ,len)); buffer.clear (); } } key.remove (); } } } catch (IOException e) { throw new RuntimeException (e); } } }
cli
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class niocli { public static void main(String[] args) throws Exception{ SocketChannel sch=SocketChannel.open(new InetSocketAddress("127.0.0.1" , 9999 )); sch.configureBlocking(false); ByteBuffer buf=ByteBuffer.allocate(1024); Scanner scan = new Scanner(System.in); while (scan.hasNext()){ String str=scan.nextLine(); buf.put(str.getBytes()); buf.flip(); sch.write(buf); buf.clear(); } sch.close(); } }
nio群聊系统
编写一个 NIO 群聊系统,实现客户端与客户端的通信需求(非阻塞)
服务器端:可以监测用户上线,离线,并实现消息转发功能
客户端:通过 channel 可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.*;import java.util.Iterator;public class Server { private Selector selector; private ServerSocketChannel ssChannel; private static final int PORT = 9999 ; Server(){ try { ssChannel=ServerSocketChannel.open(); ssChannel.configureBlocking(false ); ssChannel.bind(new java .net.InetSocketAddress(PORT)); selector=Selector.open(); ssChannel.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException (e); } } public void listen () { System.out.println("监听线程: " + Thread.currentThread().getName()); try { while (selector.select() > 0 ) { System.out.println("开始一轮事件处理~~~" ); Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey selectionKey= it.next(); if (selectionKey.isAcceptable()) { SocketChannel schannel = ssChannel.accept(); schannel.configureBlocking(false ); System.out.println(schannel.getRemoteAddress() + " 上线 " ); schannel.register(selector , SelectionKey.OP_READ); }else if (selectionKey.isReadable()) { readData(selectionKey); } } } } catch (IOException e) { e.printStackTrace(); } } private void readData (SelectionKey sk) throws IOException { SocketChannel channel = null ; try { channel= (SocketChannel) sk.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024 ); int count = channel.read(buffer); if (count > 0 ) { String msg = new String (buffer.array()); System.out.println("form 客户端: " + msg); sendInfoToOtherClients(msg, channel); } }catch (Exception e) { System.out.println(channel.getRemoteAddress() + " 离线了.." ); e.printStackTrace(); sk.cancel(); channel.close(); } } private void sendInfoToOtherClients (String msg, SocketChannel channel) throws IOException { System.out.println("服务器转发消息中..." ); System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName()); for (SelectionKey key: selector.keys()) { Channel targetChannel = key.channel(); if (targetChannel instanceof SocketChannel && targetChannel != channel) { SocketChannel dest = (SocketChannel)targetChannel; ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); dest.write(buffer); } } } public static void main (String[] args) { Server groupChatServer = new Server (); groupChatServer.listen(); } }
cli
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 public class Client { private final String HOST = "127.0.0.1" ; private final int PORT = 9999 ; private Selector selector; private SocketChannel socketChannel; private String username; public Client () throws IOException { selector = Selector.open(); socketChannel = socketChannel.open(new InetSocketAddress ("127.0.0.1" , PORT)); socketChannel.configureBlocking(false ); socketChannel.register(selector, SelectionKey.OP_READ); 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 (IOException 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(); ByteBuffer buffer = ByteBuffer.allocate(1024 ); sc.read(buffer); String msg = new String (buffer.array()); System.out.println(msg.trim()); } } iterator.remove(); } else { } }catch (Exception e) { e.printStackTrace(); } } public static void main (String[] args) throws Exception { Client chatClient = new Client (); new Thread () { public void run () { while (true ) { chatClient.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(); chatClient.sendInfo(s); } } }
Aio Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
1 2 3 4 5 AIO 异步非阻塞,基于NIO 的,可以称之为NIO2 .0 BIO NIO AIO Socket SocketChannel AsynchronousSocketChannel ServerSocket ServerSocketChannel AsynchronousServerSocketChannel
1 2 3 与NIO不同,当进行读写操作时,只须直接调用API的read 或write 方法即可, 这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read 方法的缓冲区,对于写操作而言,当操作系统将write 方法传递的流写入完毕时,操作系统主动通知应用程序 即可以理解为,read /write 方法都是异步的,完成后会主动调用回调函数。在JDK1.7 中,这部分内容被称作NIO.2 ,主要在Java.nio.channels包下增加了下面四个异步通道:
1 2 3 4 AsynchronousSocketChannel AsynchronousServerSocketChannel AsynchronousFileChannel AsynchronousDatagramChannel
总结 BIO、NIO、AIO:
Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
BIO、NIO、AIO适用场景分析:
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。Netty!