第一部分-RPC框架
1.Socket网络编程
1.1 Socket概述
Socket,套接字就是两台主机之间逻辑连接的端点。TCP/IP协议是传输层协议,主要解决数据如何
在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。Socket是通信的基石,是支持TCP/IP协
议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信
息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议
端口。
1.2 Socket整体流程
在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536,但是0到1024是为特权服务保留的端口号,可以选择任意一个当前没有被其他进程使用的端口。
客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。
1.3 代码实现
-
服务器端
package com.tongc.io.socket; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.*; /** * @author tongc * @date 2021/4/27 22:01 */ public class SocketServerDemo { public static void main(String[] args) { //1.创建一个线程池,如果有客户端连接就创建一个线程, 与之通信 ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), new ThreadPoolExecutor.AbortPolicy()); try { ServerSocket serverSocket = new ServerSocket(9999); Socket socket = serverSocket.accept(); while (true){ pool.execute(() -> { handle(socket); }); } } catch (IOException e) { e.printStackTrace(); } finally { } } public static void handle(Socket socket){ try { InputStream ins = socket.getInputStream(); byte [] bytes = new byte[1024]; ins.read(bytes); System.out.println("服务端收到" + new String(bytes)); OutputStream ops = socket.getOutputStream(); ops.write("没钱".getBytes()); } catch (IOException e) { e.printStackTrace(); } finally { try { //关闭连接 socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
-
客户端
package com.tongc.io.socket; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.Scanner; /** * @author tongc * @date 2021/4/27 22:01 */ public class SocketClientDemo { public static void main(String[] args) throws IOException { while (true) { 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(); } } }
2 IO模型
Java 共支持 3 种网络编程模型/IO 模式:BIO(同步并阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)
2.1 BIO(同步阻塞)
Java BIO就是传统的 socket编程.
BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器
端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程
池机制改善(实现多个客户连接服务器)。
工作机制
BIO问题分析
-
每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write
-
并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
-
连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
2.2 NIO(同步非阻塞)
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到
多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。
2.3 AIO(异步非阻塞)
AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的
特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长
的应用
Proactor 模式是一个消息异步通知的设计模式,Proactor 通知的不是就绪事件,而是操作完成事
件,这也就是操作系统异步 IO 的主要模型
2.4 场景分析
- BIO(同步并阻塞) 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,
并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解
- NIO(同步非阻塞) 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕
系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持
- AIO(异步非阻塞) 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分
调用 OS 参与并发操作, 编程比较复杂,JDK7 开始支持。
3 NIO编程
3.1 NIO介绍
全称 Java non-blocking IO,jdk 1.4 之后提供一系列的改进的IO,也叫 New IO ,是同步非阻塞的。
- NIO三块核心部分:Channel、Buffer、Selector
- 面向Buffer编程,数据读取到缓冲区,需要时从Buffer中前后移动,这增加了处理过程的灵活性,可以提供非阻塞式的高伸缩性网络
-
原理图
-
每个channel都会对应一个buffer
-
selector对应一个线程,一个线程对应多个channel
-
每个channel都要注册到selector上
-
selector不断轮训查看channel上的事件,事件是channel非常重要的概念
-
selector会根据不同的事件完成不同的操作
-
buffer就是一个内存块,底层是一个数组
-
buffer是双向的,可读可写,channel也是双向的
-
3.2 BIO和NIO的比较
NIO | BIO |
---|---|
以Buffer方式处理数据 | 以流的方式处理数据 |
基于 Channel(通道)和 Buffer(缓冲区)进行操作 | 基于字节流和字符流进行操作 |
Selector(选择器)用于监听多个通道的,使用单个线程就可以监听多个客户端通道 | 一个线程只能监听一个客户端 |
3.3 Buffer缓冲区
Channel 提供从网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
3.3.1 Buffer API
3.3.1.1Buffer类及子类
在 NIO 中,Buffer是一个顶层父类,它是一个抽象类, 类的层级关系图,常用的缓冲区分别对应
byte,short, int, long,float,double,char 7种。
3.3.1.2Buffer创建
方法名 | 说明 |
---|---|
static ByteBuffer allocate(长度) | 创建byte类型的指定长度的缓冲区 |
static ByteBuffer wrap(byte[] array) | 创建一个有内容的byte类型缓冲区 |
-
示例代码
public class BufferDemo { public static void main(String[] args) { //创建一个长度为5的缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(5); for (int i = 0; i < 5; i++) { //默认值均为0 System.out.println(byteBuffer.get()); } //创建一个有内容的缓冲区 ByteBuffer wrap = ByteBuffer.wrap("hello".getBytes()); for (int i = 0; i < 5; i++) { System.out.println(wrap.get()); } } }
3.3.1.3Buffer添加数据
方法 | 描述 |
---|---|
int position() | 获得当前要操作的索引 |
Buffer position(int newPosition) | 修改当前要操作的索引位置 |
int limit() | 最多能操作到哪个索引 |
Buffer limit(int newLimit) | 修改最多能操作的索引位置 |
int capacity() | 返回缓冲区的总长度 |
int remaining() | 还有多少能操作索引个数 |
boolean hasRemaining() | 是否还有能操作 |
put(byte b) | 添加一个字节 |
put(byte[] src) | 添加字节数组 |
-
示例代码
public class PutBufferDemo { public static void main(String[] args) { //创建一个长度为5的缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(5); //获取当前位置,初始值为0 System.out.println(byteBuffer.position()); //修改要操作的索引位置 System.out.println(byteBuffer.position(2).position()); ByteBuffer byteBuffer2 = ByteBuffer.allocate(5); //放入一个字节 byteBuffer2.put((byte) 99); //获取当前位置 1 System.out.println(byteBuffer2.position()); //缓冲区的总长度 5 System.out.println(byteBuffer2.capacity()); //最多能操作到的索引位置 5 System.out.println(byteBuffer2.limit()); //还有多少个能操作的 4 System.out.println(byteBuffer2.remaining()); ByteBuffer byteBuffer3 = ByteBuffer.allocate(5); //超出长度会报异常 BufferOverflowException byteBuffer3.put("abcdef".getBytes()); } }
3.3.1.4 Buffer读取数据
方法 | 介绍 |
---|---|
flip() | 写切换读模式,limit设置position位置, position设置0 |
get() | 读取一个字节 |
get(byte[] dst) | 读取多个字节 |
get(int index) | 读取指定索引字节 |
rewind() | 将position设置为0,可以重复读 |
clear() | 切换为写模式,position设置0,limit设置为capacity |
array() | 将缓冲区转换成字节数组返回 |
- 图解flip()
- 图解clear()
public class GetBufferDemo {
public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
//写入数据
byteBuffer.put("abcdef".getBytes());
//切换读模式
byteBuffer.flip();
for (int i = 0; i < byteBuffer.limit(); i++) {
System.out.println(byteBuffer.get());
}
// 切换写模式,覆盖之前索引所在位置的值
System.out.println("写模式--------------");
byteBuffer.clear();
byteBuffer.put("11".getBytes());
System.out.println(new String(byteBuffer.array()));
}
}
3.4 Channel通道
常用的Channel实现类类 有 :FileChannel , DatagramChannel ,ServerSocketChannel和SocketChannel 。FileChannel 用于文件的数据读写, DatagramChannel 用于 UDP 的数据读写, ServerSocketChannel 和SocketChannel 用于 TCP 的数据读写。
3.4.1 ServerSocketChannel
public class ServerSocketChannelDemo {
public static void main(String[] args) throws Exception{
//1.打开服务器通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2.绑定端口
serverSocketChannel.bind(new InetSocketAddress(9999));
//3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
System.out.println("服务端启动完成");
while (true){
//4.检查是否有客户端连接,有客户端连接会返回对应的channle,否则返回null
SocketChannel accept = serverSocketChannel.accept();
if(accept == null){
System.out.println("没有客户端连接...");
System.out.println("我去做其他事情了");
Thread.sleep(1000);
continue;
}
//5.定义一个存放数据的buffer
ByteBuffer allocate = ByteBuffer.allocate(1024);
//0:没有读到有效数据
//-1:读到了末尾
//正数:读到的有效字节数
int read = accept.read(allocate);
System.out.println("客户端消息: " + new String(allocate.array(),0,read, StandardCharsets.UTF_8));
//6.给客户端发消息
accept.write(ByteBuffer.wrap("再见".getBytes(StandardCharsets.UTF_8)));
//7.关闭连接
accept.close();
}
}
}
3.4.2 SocketChannel
public class SocketChannelClientDemo {
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 allocate = ByteBuffer.allocate(1024);
int read = socketChannel.read(allocate);
System.out.println("服务端返回: "+ new String(allocate.array(), 0 , read, StandardCharsets.UTF_8));
socketChannel.close();
}
}
3.5 Selector 选择器
可以用一个线程,处理多个的客户端连接,就会使用到NIO的Selector(选择器). Selector 能够检测多个注册的服务端通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
3.5.1 Selector API
方法 | 说明 |
---|---|
open() | 得到一个选择器对象 |
select() | 阻塞 监控所有注册的通道,当有对应的事件操作时, 会将SelectionKey放入集合内部并返回事件数量 |
selector.select(1000) | 阻塞 1000 毫秒,监控所有注册的通道,当有对应的事件操作时, 会将SelectionKey放入集合内部并返回 |
selectedKeys() | 返回存有SelectionKey的集合 |
SelectionKey.isAcceptable() | 是否是连接继续事件 |
SelectionKey.isConnectable() | 是否是连接就绪事件 |
SelectionKey.isReadable() | 是否是读就绪事件 |
SelectionKey.isWritable() | 是否是写就绪事件 |
-
SelectionKey中定义的4种事件:
- SelectionKey.OP_ACCEPT —— 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
- SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户端与服务器的连接已经建立成功
- SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
- SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)
-
Selector 编码
-
打开一个服务端通道
-
绑定对应的端口号
-
通道默认是阻塞的,需要设置为非阻塞
-
创建选择器
-
将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
-
检查选择器是否有事件
-
获取事件集合
-
判断事件是否是客户端连接事件SelectionKey.isAcceptable()
-
得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
-
判断是否是客户端读就绪事件SelectionKey.isReadable()
-
得到客户端通道,读取数据到缓冲区
-
给客户端回写数据
-
从集合中删除对应的事件, 因为防止二次处理.
-