1.分布式通信方式
因为分布式服务框架大部门是针对一个大型的业务系统,那么通信是尤为关键的。一般通信方式采用的是长连接和短连接两种。
1.1 长连接和短连接
长连接:当面对大型应用服务时,本地API需要经常调用远程接口服务,那么本地大量的方法都会成为被远程调用的对象,影响跨服务调用最大的因素就是网络延迟,那么如何应对这个问题?这里举个例子,如果连接两地的马路,需要构建那么是不是需要资源去搭建,当你不需要这条马路,需要拆除,等下次用的时候又需要建,那么资源上的浪费可想而知。回到现实,这个问题也是大型应用无法接受的。这里就需要心跳和业务消息来维持链路,从而实现多消息复用一条链路的目的。
其应用场景:实时通信、聊天室、游戏等
短连接:客户端和服务器之间进行一次通信就关闭,一般常用子啊请求-回应的场景。像浏览器浏览页面时,经常发生变化的。这里就能体现短连接的优势:可以及时释放服务器资源,避免长期占用资源,导致服务器压力过大。当然了,频繁的创建和删除也是很浪费系统通信资源的。
所以无论是使用长连接还是短连接都需要根据自己实际业务出发,选择一个合适自己需求的通信方式。
在这里我们介绍的是分布式服务框架,而该框架主要是采用长连接方式实现通信。
1.2 IO模型
上一面讲了通信方式,这里我们介绍下IO模型
1.2.1 BIO模型
在JAVA4之前,开发JAVA的所以Socket通信都是采用同步阻塞模式,同步阻塞模式顾名思义就是请求和回应都在一个通道中进行,这里举个例子方便理解:顾客A来餐厅点菜,我们作为服务员,首先获取A的点菜单,A点菜慢,我们就在这一直等着,等菜点完了我们再把菜单交给厨师长,顾客B同理。 但是这样系统就存在很大的性能瓶颈,如果顾客数量多的情况下,那么餐厅服务员明显是不够的,并且在有限数量是忙不过来的。唯一的解决方式就是增加服务员数量。当然了这种方式虽然能解决高并发和低时效的问题,但是对餐厅的支出成本就不太友好了,并且这么多的服务员也会导致餐厅管理起来十分不方便。
package com.huangjialun;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author :***
* @date : 2024/1/23
* @description :
*/
public class BIOServer {
public static void main(String[] args) throws IOException {
// 线程池机制
// 1.创建一个线程池
// 2.如果客户端连接,就创建一个线程,与之通信
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
// 创建ServerSocket
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器启动");
while (true){
// 监听,等待客户端连接
final Socket socket = serverSocket.accept();
System.out.println("连接一个客户端");
// 创建一个线程
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
// 可以和客户端通信
try {
handler(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
// 编写一个handler方法,与客户端通讯
public static void handler(Socket socket) throws IOException {
try{
System.out.println("连接客户端++++客户端IP:"+socket.getLocalAddress()+"端口:" +socket.getLocalPort());
byte[] bytes = new byte[1024];
// 通过Socket获取输入流
InputStream inputStream = socket.getInputStream();
while(true){
int read = inputStream.read(bytes);
if(read != -1){
System.out.println(new String(bytes,0,read)); // 输出客户端发送的数据
}else{
break;
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("关闭和client的连接");
socket.close();
}
}
}
1.2.2 NIO模型
如果采用采用BIO模型的服务端,就需要一个独立的Acceptor线程来负责监听客户端的连接,当接收到客户端请求的时候会建立线程来处理该请求,当业务处理完毕后,会通过输出流返回给客户端,然后销毁线程。这样的弊端就是我上面例子所说,线程是JAVA虚拟机宝贵的资源,当线程数激增,不仅是系统性能下架,当并发量到达一个顶点的时候,还会出现线程溢出,创建线程失败等一系列问题。
为了解决这系列问题,JAVA在1.4推出了NIO,俗称同步非阻塞。在IO编程中,如果需要处理多个客户端请求时,可以利用多线程或者IO多路复用技术,IO多路复用技术就是通过吧多个IO的阻塞复用到同一个Select等阻塞中,从而实现了一个线程能同时处理多个客户端请求。于多线程相比,多路复用技术的优势就在于开销更小,系统不需要创建新的线程,也不需要额外去维护这些线程的运行,降低了系统的维护工作量,节省了系统资源。
NIO采用多路复用技术,一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以没有了最大连接句柄的限制。
2. Netty
如果是自己去实现IO,不仅对技术的要求很高以外,还存在Selector空轮询等一些原生bug。
所以Netty就成了目前流行的通信框架。
2.1 Netty介绍
Netty是一个开源的、异步事件驱动的网络应用框架,主要用于Java编程语言。Netty提供了一组易于使用的API和抽象,抽象了网络编程的复杂性。
Netty使用基于Java的NIO模型,可以使用少量线程处理许多并发连接,这使得它非常适合构建需要处理大量同时连接的的高性能网络应用,例如Web服务器、聊天服务器、游戏服务器等。
2.2 Netty特点
- 高性能:Netty以其高性能、低延迟的能力而闻名。它通过高效的内存管理,事件驱动的架构和非阻塞IO实现这一点。
- 可扩展性:Netty可以使用少量线程处理千万个并发连接,具有很高的可扩展性。
- 协议支持:Netty提供了对各种网络协议的内置支持 ,如HTTP、WebSocket、SSL/TLS、UDP等,以及可以实现自定义协议。
- 丰富社区资源:得益于netty优秀的能力,其社区也是十分活跃的。
2.3 Netty服务端实现
2.3.1 导入依赖
第一步通过Maven导入Netty依赖,
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>版本号</version>
</dependency>
2.3.2 创建服务器引导类
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyServer {
private int port;
public NettyServer(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new YourRequestHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port = 8080;
NettyServer server = new NettyServer(port);
server.run();
}
}
BossGroup主要用于接受客户端连接的线程池,其构造方法与处理I/O读写的线程池相同(workGroup),都是通过new NioEventLoopGroup创建,bossGroup一般线程数设置为1,因为主要负责接受客户端的连接,不做任何复杂的逻辑处理。
2.3.3 创建请求处理器
创建一个请求处理器类,用于处理接受到的请求,根据实际业务需求进行编辑
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class YourRequestHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 处理接收到的请求
ByteBuf byteBuf = (ByteBuf) msg;
// 向客户端发送响应
ByteBuf response = ctx.alloc().buffer();
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 处理异常
cause.printStackTrace();
ctx.close();
}
}
2.3.4启动服务器
在main方法中去实例化NettyServer类。并调用run方法来启动服务器
public static void main(String[] args) throws Exception {
int port = 8080;
NettyServer server = new NettyServer(port);
server.run();
}
2.4 Netty客户端实现
2.4.1 导入依赖
通过Maven导入Netty依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>版本号</version>
</dependency>
2.4.2 创建客户端引导类
创建一个客户端引导类,用于配置和启动Netty客户端,在该类中,需要设置服务器的地址和端口号,以及对应的处理器主要用来接收响应。
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClient {
private String host;
private int port;
public NettyClient(String host, int port) {
this.host = host;
this.port = port;
}
public void run() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new YourResponseHandler());
}
})
.option(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = bootstrap.connect(host, port).sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
String host = "服务器地址";
int port = 服务器端口号;
NettyClient client = new NettyClient(host, port);
client.run();
}
}
2.4.3 创建响应处理器
创建一个处理器类,用来处理接收到的响应,可以根据业务需求进一步实现处理逻辑。
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class YourResponseHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 在连接建立时发送请求数据
ByteBuf request = ctx.alloc().buffer();
ctx.writeAndFlush(request);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 处理接收到的响应
ByteBuf response = (ByteBuf) msg;
response.release();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 处理异常
cause.printStackTrace();
ctx.close();
}
}
2.4.4 启动客户端
public static void main(String[] args) throws Exception {
String host = "服务器地址";
int port = 服务器端口号;
NettyClient client = new NettyClient(host, port);
client.run();
}
2.5 Netty 模型
Netty主要是基于主从Reactor多线程模型
Netty原理:
- Netty抽象出两组线程池BossGroup专门负责接收客户端的连接,WorkGroup专门负责网络的读写
- BossGroup和WorkerGroup类型都是NIOEventLoopGroup,BossGroup主要接收传入连接,一旦boss接受连接,就会处理已接受连接的流量,并将已接受的连接注册到worker,使用多少个线程以及它们如何映射到创建的channel线程取决于EventLoopgroup实现,甚至也可以通过构造函数去进行配置。
- NIOEventLoopGroup相当于事件循环组,这个组中含有多个事件循环,每个事件循环是NioEventLoop
- NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个Selector,用于监听绑定在其Socket的网络通讯
- NIOEventLoopGroup可以有多个线程,既可以含有多个NioEventLoop
- 每个BossNioEventLoop执行的步骤有三步:
1.轮训accept事件
2.处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某一个workerNioEventLoop上的Selector
3.处理任务队列的任务,即runAllTasks
7.每个workerNioEventLoop循环执行的步骤
1.轮训read,write事件
2.处理i/o事件,即read,write事件,在对应的NioSocketChannel处理
3.处理任务队列的任务,即runAllTasks
8.每个WorkerNioEvent处理业务时,都会使用到pipeline(管道),pipeline中包含了channel,即通过piepeline可以获取到对应通道,管道维护了很多的处理器