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(){
@Override
public 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. 检查是否有客户端连接 有客户端连接会返回对应的通道 , 否则返回null
SocketChannel 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_ACCEPT
serverSocketChannel.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_READ
if (key.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端已连接......" + socketChannel);
//必须设置通道为非阻塞, 因为selector需要轮询监听每个通道的事件
socketChannel.configureBlocking(false);
//并指定监听事件为OP_READ
socketChannel.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();
}
}