1. socket知识与IO模型
1.1 Socket
1.1.1 概述
套接字,两台主机间的连接端点,TCP/IP协议是传输层协议,解决数据传输的问题,http是应用层协议,解决数据的包装问题。socket是支持TCP/IP的网络通信的基本操作单元。是网络通信中端点的抽象表示,包含通信必须的五种信息:
- 连接使用的协议
- 本地主机的IP
- 本地进程的端口
- 远程主机的IP
- 远程进程的端口
1.1.2 流程
两个端:客户端,服务端
首先在服务端创建ServerSocket,附加到端口上,服务器在该处监听连接。端口范围0 - 65536,但是0 - 1024是为特权服务保留的端口。
客户端根据服务器的域名或者IP地址,加上端口,打开一个套接字。当服务器接受连接后,服务器和客户端的通信就想输入输出流一样操作。
1.1.3 代码
1.1.3.1 服务端代码
import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ServerDemo {public static void main(String[] args) {// 1. 创建线程池,有客户端连接就创建一个线程ExecutorService executorService = Executors.newCachedThreadPool();// 2. 创建 ServerSocket对象ServerSocket serverSocket = new ServerSocket(9999);while(true){// 3. 监听客户端Socket socket = serverSocket.accept();// 4. 开启新的线程处理executorService.execute(new Runnable(){@Overridepublic void run() {handle(socket);}});}}public static void handle(Socket socket){try {// 从连接中取出输入流来接收消息InputStream is = socket.getInputStream();byte[] b = new byte[1024];int read = is.read(b);System.out.println("客户端: " + new String(b, 0, read));// 连接中取出输出流回话OutputStream os = socket.getOutputStream();os.write("没钱".getBytes());} catch (Exception e) {e.printStackTrace();} finally {try {// 关闭连接socket.close();} catch (IOException e) {e.printStackTrace();}}}}
1.1.3.1 客户端代码
package com.lagou.client;import java.io.InputStream;import java.io.OutputStream;import java.net.Socket;import java.util.Scanner;public class ClientDemo {public static void main(String[] args) throws Exception {while (true) {// 1.创建 Socket 对象Socket s = new Socket("127.0.0.1", 9999);// 2.从连接中取出输出流并发消息OutputStream os = s.getOutputStream();System.out.println("请输入:");Scanner sc = new Scanner(System.in);String msg = sc.nextLine();os.write(msg.getBytes());// 3.从连接中取出输入流并接收回话InputStream is = s.getInputStream();byte[] b = new byte[1024];int read = is.read(b);System.out.println("老板说:" + new String(b, 0, read).trim());// 4.关闭s.close();}}}
1.2 IO模型
1.2.1 说明
简单理解:用什么样的通道进行数据的发送和接收,决定了程序通信的性能
阻塞与非阻塞:
访问IO的线程是否会等待线程访问资源,该资源是否准备就绪的一种处理方式
同步和异步:
指数据的请求方式,是访问数据的一种机制
1.2.2 BIO(同步 - 阻塞)
传统的socket编程。
blocking io:
同步阻塞,服务器实现模式为一个连接,一个线程,如果连接不做任何事情会造成线程开销,可以使用线程池进行改善。
举例:
人在烧水,这个人一直在观察水烧开没有,中途无法做其它事情
问题分析:
1、每个请求都需要线程
2、并发数较大时,需要大量线程,系统资源占用较大
3、连接建立后,如果线程暂时没有数据可读,线程会阻塞在read操作上,造成资源浪费
1.2.3 NIO(同步 - 非阻塞)
服务器实现模式为一个线程处理多个请求,客户端发送的连接请求都会注册到多路复用器上,多路复用器会轮训到连接有IO请求就进行处理
举例:
烧水时,一个人轮训查看多个烧水壶的状态,而不再是,只专注于一个烧水壶
1.2.4 AIO(异步 - 非阻塞)
引入了异步通道的概念,采用了Proactor模式,简化程序编写,有效的请求才会启动线程,特点是先由操作系统完成后才通知服务端程序去处理,一般适用于连接数较多且连接时间较长的应用
Proactor模式是一个消息异步通知的模式,Proactor通知的不是就绪事件,而是操作完成事件,这也就是操作系统异步IO的主要模型
1.2.5 BIO、NIO、AIO 场景分析
1、BIO适用于连接数较小且固定的架构,对服务器资源要求较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解
2、NIO适用于连接数较多且连接比较短(轻应用)的架构,比如聊天服务器,弹幕系统,服务器间通讯。编程复杂,JDK1.4开始支持
3、AIO适用于连接数较多且连接比较长(重应用)的架构,比如相册服务器,充分调用OS参与并发操作,编程复杂,JDK1.7开始支持
2. NIO编程
2.1 NIO介绍
全程non-blocking IO,JDK1.4开始,java提供了一些列改进的输入输出的新特性,统称为NIO,即new IO
1、三大核心:Channel,Buffer,Selector
2、面向缓冲区编程,数据读取到缓冲区中,需要时可在缓冲区中移动,增加处理过程的灵活性,可以提供非阻塞的高伸缩性网络
3、非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不获取,而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1w个请求,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,必须1w个。
2.2 NIO和BIO的比较
1、BIO以流的方式处理数据,而NIO以缓冲区的方式处理数据,缓冲区IO的效率比流IO高很多
2、BIO是阻塞的,NIO是非阻塞的
3、BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件(例如:连接,数据到达等),因此使用单个线程就可以监听多个客户端通道。
2.3 NIO三大核心原理

1、每个Channel都会对应一个Buffer
2、Selector对应一个线程,一个线程对应多个Channel
3、每个Channel都注册到Selector选择器上
4、Selector不断轮询查看Channel上的事件,事件是Channel非常重要的概念
5、Selector会根据不同的事件,完成不同的处理操作
6、Buffer就是一个内存块,底层是一个数组
7、数据的读取写书通过Buffer完成,BIO通过输入输出流完成,不能双向,Buffer可以读写,channel是双向的
2.4 缓冲区(Buffer)
2.4.1 介绍
本质上是一个读写数据的内存块,这个对象提供了一些方法,更轻松的使用内存块,内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从网络读取数据的渠道,但是数据必须都经过Buffer。
2.4.2 常用API
- Buffer及其子类

在NIO中,Buffer是顶层父类,抽象类,常用缓冲区分别对应byte,short,int,long,float,double,char7种
缓冲区对象创建 | 方法名 | 说明 | | —- | —- | | static ByteBuffer allocate(长度) | 创建byte类型的指定长度的缓冲区 | | static ByteBuffer wrap(byte[] array) | 创建一个有内容的byte类型缓冲区 |
缓冲区对象添加数据 | 方法名 | 说明 | | —- | —- | | int position()/position(int newPosition) | 获得当前要操作的索引/修改当前要操作的索引位 置 | | int limit()/limit(int newLimit) | 最多能操作到哪个索引/修改最多能操作的索引位 置 | | int capacity() | 返回缓冲区的总长度 | | int remaining()/boolean hasRemaining() | 还有多少能操作索引个数/是否还有能操作 | | put(byte b)/put(byte[] src) | 添加一个字节/添加字节数组 |
图解:
- 缓冲区对象读取数据 | 方法名 | 介绍 | | —- | —- | | flip() | 写切换读模式 limit设置position位置, position设置0 | | get() | 读一个字节 | | get(byte[] dst) | 读多个字节 | | get(int index) | 读指定索引的字节 | | rewind() | 将position设置为0,可以重复读 | | clear() | 切换写模式 position设置为0 , limit 设置为 capacity | | array() | 将缓冲区转换成字节数组返回 |
图解:flip方法
图解:clear()方法
- 注意事项
- capacity:容量(长度)limit: 界限(最多能读/写到哪里)posotion:位置(读/写 哪个索引)
- 获取缓冲区里面数据之前,需要调用flip方法
- 再次写数据之前,需要调用clear方法,但是数据还未消失,等再次写入数据,被覆盖了 才会消失。
2.5 通道(Channel)
2.5.1 介绍
- 通道可以读也可以写,流基本是单向的
- 通道可以异步读写
-
2.5.2 Channel常用类介绍
Channel接口,实现类
- FileChannel,文件处理
- DatagramChannel,UDP数据处理
- ServerSocketChannel,TCP数据处理
- SocketChannel,TCP数据处理
- SocketChannel与ServerSocketChannel
- 类似Socket与ServerSocket,可以实现客户端与服务端的通信工作
2.5.3 ServerSocketChannel
步骤:
1、打开一个服务端通道
2、绑定端口号
3、通道默认阻塞,设置为非阻塞
4、检查是否有客户端连接,有连接返回对应通道
5、获取客户端传递过来的数据,把数据放入ByteBuffer中
6、给客户端返回数据
7、释放资源
代码: ```java package com.lagou.channel; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; /**
- 类似Socket与ServerSocket,可以实现客户端与服务端的通信工作
- 服务端
*/
public class NIOServer {
public static void main(String[] args) throws IOException, InterruptedException {
} } ```//1. 打开一个服务端通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//2. 绑定对应的端口号serverSocketChannel.bind(new InetSocketAddress(9999));//3. 通道默认是阻塞的,需要设置为非阻塞// true 为通道阻塞 false 为非阻塞serverSocketChannel.configureBlocking(false);System.out.println("服务端启动成功..........");while (true) {//4. 检查是否有客户端连接 有客户端连接会返回对应的通道 , 否则返回nullSocketChannel socketChannel = serverSocketChannel.accept();if (socketChannel == null) {System.out.println("没有客户端连接...我去做别的事情");Thread.sleep(2000);continue;}//5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//返回值://正数: 表示本次读到的有效字节个数.//0 : 表示本次没有读到有效字节.//-1 : 表示读到了末尾int read = socketChannel.read(byteBuffer);System.out.println("客户端消息:" +new String(byteBuffer.array(), 0, read,StandardCharsets.UTF_8));//6. 给客户端回写数据socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));//7. 释放资源socketChannel.close();}
2.5.4 SocketChannel
步骤:
1、打开通道
2、设置连接IP和端口
3、写出数据
4、读取服务器写回的数据
5、释放资源
代码: ```java package com.lagou.channel; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; /** - 客户端
*/
public class NIOClient {
public static void main(String[] args) throws IOException {
} } ```//1.打开通道SocketChannel socketChannel = SocketChannel.open();//2.设置连接IP和端口号socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));//3.写出数据socketChannel.write(ByteBuffer.wrap("老板, 该还钱拉!".getBytes(StandardCharsets.UTF_8)));//4.读取服务器写回的数据ByteBuffer readBuffer = ByteBuffer.allocate(1024);int read=socketChannel.read(readBuffer);System.out.println("服务端消息:" + new String(readBuffer.array(), 0, read,StandardCharsets.UTF_8));//5.释放资源socketChannel.close();
2.6 Selector(选择器)
2.6.1 介绍
使用一个线程处理多个客户端的连接,就会使用到NIO的Selector,Selector能够检测多个注册的服务端通道上是否有事件发生,如果有,便获取事件然后针对每个事件进行响应的处理。这样就可以使用一个线程去管理多个通道,也就是可以管理多个连接和请求
没有选择器时:
每个连接对应一个处理线程,但是连接并不能马上就会发送消息,所以还会产生资源浪费
有选择器时:
在通道有读写事件发生时,才会进行读写,就很大程度的减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免线程之间的上下文切换导致的开销。
2.6.2 常用API
- Selector抽象类
- Selector.open():得到一个选择器对象
- Selector.select():阻塞,监控所有注册的通道,当有事件发生时,会将SelectionKey放入集合内部并返回事件数量
- Selector.select(1000):阻塞1000ms
- Selector.selectedKeys():返回存有selectionKey的集合
- SelectionKey
- isAcceptable:是否连接继续事件
- isConnectable:是否连接就绪事件
- isReadable:是否读就绪事件
- isWriteable:是否写就绪事件
- SelectionKey中定义的4种事件
- OP_ACCEPT:连接继续,表示服务器监听到了客户连接,服务器可以接收这个连接
- OP_CONNECT:连接就绪,表示客户端与服务器的连接已经建立
- OP_READ:读就绪事件,表示通道中有可读的数据,可以执行读操作
- OP_WRITE:写就绪事件,表示可以向通道写数据
2.6.3 Selector编码
步骤:
1、打开服务端通道
2、绑定端口
3、默认阻塞,设置为非阻塞
4、创建选择器
5、将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
6、检查选择器是否有事件
7、获取事件集合
8、判断事件是否是客户端连接事件SelectionKey.isAcceptable()
9、得到客户端通道,并将通道注册到选择器上,并指定监听事件为OP_READ
10、判断是否是客户端读就绪事件SelectionKey.isReadable()
11、得到客户端通道,读取数据到缓冲区
12、给客户端回写数据
13、从集合中删除对应的事件,因为防止二次处理
代码: ```java package com.lagou.selector; 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.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.Set; /**
- 服务端-选择器
*/
public class NIOSelectorServer {
public static void main(String[] args) throws IOException, InterruptedException {
} } ```//1. 打开一个服务端通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//2. 绑定对应的端口号serverSocketChannel.bind(new InetSocketAddress(9999));//3. 通道默认是阻塞的,需要设置为非阻塞serverSocketChannel.configureBlocking(false);//4. 创建选择器Selector selector = Selector.open();//5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPTserverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("服务端启动成功...");while (true) {//6. 检查选择器是否有事件int select = selector.select(2000);if (select == 0) {continue;}//7. 获取事件集合Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {//8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()SelectionKey key = iterator.next();//9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READif (key.isAcceptable()) {SocketChannel socketChannel = serverSocketChannel.accept();System.out.println("客户端已连接......" + socketChannel);//必须设置通道为非阻塞, 因为selector需要轮询监听每个通道的事件socketChannel.configureBlocking(false);//并指定监听事件为OP_READsocketChannel.register(selector, SelectionKey.OP_READ);}//10. 判断是否是客户端读就绪事件SelectionKey.isReadable()if (key.isReadable()) {//11.得到客户端通道,读取数据到缓冲区SocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(1024);int read = socketChannel.read(byteBuffer);if (read > 0) {System.out.println("客户端消息:" + new String(byteBuffer.array(), 0, read,StandardCharsets.UTF_8));//12.给客户端回写数据socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));socketChannel.close();}}//13.从集合中删除对应的事件, 因为防止二次处理.iterator.remove();}}
