此文中的代码都是在学习过程中感觉较有代表性,综合性强的例子,希望通过此文的总结,在后续的复习中可以及时的回顾起来。
一、《Netty In Action》中关于BIO的实现
1、实现概述
- 首先根据端口号相关信息注册一个Socket,在一个死循环中调用此Socket的accept方法,在没有接收到请求的时候该方法会一直阻塞
- 当接受到了请求accept返回一个client的Socket取名为clientSocket
- clientSocket就相当于是一个信息通道我们通过他来读取数据,这个读取以及业务逻辑的处理是需要放在一个单独的线程中进行处理
- 根据上面的流程每一Socket都会有一个线程,单量线程的创建以及线程的上下文切换将造成大量的资源浪费
2、代码实现
public class PlainOioServer {
public void startServer(int port) throws IOException {
//使用port创建一个socket
ServerSocket serverSocket = new ServerSocket(port);
//使用一个死循环不停的监听此Socket
for (;;){
//现在在运行的时候将会阻塞在这里
final Socket clientSocket = serverSocket.accept();
//通过创建一下线程来执行我们的连接操作
new Thread(new Runnable() {
@Override
public void run() {
OutputStream outputStream = null;
try {
outputStream = clientSocket.getOutputStream();
outputStream.write("Hello,client".getBytes("utf-8"));
outputStream.flush();
} catch (IOException ioException) {
ioException.printStackTrace();
}finally {
try {
outputStream.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}).start();
}
}
}
二、《Netty In Action》中关于NIO的实现
1、实现理念
- NIO相关实现的三大组件是Channel、Buffer、Selector;创建他们并将他们连接在一起
2、服务端的实现
public class PlainNioServer {
public void startServer(@NotNull int port) throws Exception {
//检验port是否合法
if (port<1024){
throw new Exception("端口号不合法");
}
//创建Channel并进行相应的绑定以及配置
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));
//创建用来监听Channel的Select
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("当前已经注册的Channel的数量为:"+selector.selectedKeys().size());
//创建死循环用来监听是否有新的事件连接进来
for (;;){
//等待有可以响应的时间,如果超过事件未响应那么就直接放回执行下面的逻辑
selector.select(10000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
System.out.println("当前一共有SelectionKey:"+selectionKeys.size());
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//对当前连接过来的事件的性质做判断,如果是连接的请求则将此channel注册到selector
//如果是读写事件的话则做相应的读写操作
if (selectionKey.isAcceptable()){
System.out.println("监听到了一个连接事件!");
//获取到与selectorKey绑定的Channel并将之注册
SocketChannel clientChannel = (SocketChannel) serverSocketChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector,SelectionKey.OP_READ,ByteBuffer.allocate(1024));
System.out.println("获取到了一个客户端的连接,连接的地址为:"+clientChannel.getRemoteAddress());
ByteBuffer returnMsgBuffer = ByteBuffer.wrap("2-Hello,Client!".getBytes());
clientChannel.write(returnMsgBuffer);
}
if (selectionKey.isReadable()){
//监听到的时间是可读事件,这里通过Buffer的形式接受来自网络的数据
System.out.println("连接到了一个可读事件");
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
ByteBuffer msg = (ByteBuffer) selectionKey.attachment();
clientChannel.read(msg);
System.out.println("接受到了来自客户端的信息:"+new String(msg.array()));
ByteBuffer returnMsgBuffer = ByteBuffer.wrap("1-Hello,Client!".getBytes());
clientChannel.write(returnMsgBuffer);
System.out.println("监听到的读事件后,完成向客户端数据的读写");
}
iterator.remove();
}
}
}
public static void main(String[] args) throws Exception {
PlainNioServer plainNioServer = new PlainNioServer();
plainNioServer.startServer(5566);
}
}
3、客户端的实现
public class PlainNioClient {
public void createClient(@NotNull String serverAddress,@NotNull int serverPort) throws Exception {
//对传递过来的参数进行校验
if (serverAddress==""||serverAddress.length()<8||serverPort<1024){
throw new Exception("参数初始化错误,请慎重设置");
}
//根据传递过来的服务器参数创建客户端的Channel
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
InetSocketAddress address = new InetSocketAddress(serverAddress, serverPort);
if (!clientChannel.connect(address)) {
while (!clientChannel.finishConnect()) {
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
}
}
//创建一个监听器并进行绑定
Selector clientSelector = Selector.open();
//第三个参数可以传递也可以不传递但是如果不传递的话在使用key.attachment()得到的回事一个空的
clientChannel.register(clientSelector, SelectionKey.OP_READ,ByteBuffer.allocate(1024));
System.out.println("---客户端成功完成对服务器的监听---");
//开始向服务端发送消息
ByteBuffer msgBuffer = ByteBuffer.wrap("Hello,Server".getBytes());
clientChannel.write(msgBuffer);
//创建一个死循环完成对完成对来自服务端数据的监听
for (;;){
clientSelector.select(1000);
Set<SelectionKey> selectionKeys = clientSelector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isReadable()){
ByteBuffer msg = (ByteBuffer) key.attachment();
SocketChannel serverChannel = (SocketChannel) key.channel();
serverChannel.read(msg);
System.out.println("成功接收到来自服务端的信息:"+new String(msg.array()));
}
iterator.remove();
}
}
}
public static void main(String[] args) throws Exception {
PlainNioClient plainNioClient = new PlainNioClient();
plainNioClient.createClient("127.0.0.1",5566);
}
}
三、非阻塞任务队列的实现
1、场景与实现思路概述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oKnKDfPO-1651676471726)(F:\typroa\aimages\image-20220425221824991.png)]
2、Server端的实现
/**
* @description:
* 一、实现两种方式的任务队列:
* 1、使用更加灵活地将耗时任务线程处理放于Handler的
* 2、在添加Handler的时候就直接绑定一个用来处理他的Group
* 二、除过验证上面说的耗时任务异步处理也设置多个出入站Handler,并对他们执行的先后顺序进行验证
* 同时要结合《Netty in action》中的相关章节对Handler不同类型以及类结构有一个了解并进行总结
* @function:
* @author: Liu Menglei
* @Date: 2022/4/22 0022 10:52
*/
public class NoBlockTaskServer {
private static final NioEventLoopGroup taskGroup2 = new NioEventLoopGroup(4);
/**
*@Description: 根据端口入参创建一个基于Netty的服务
*@date: 2022/4/22 0022
*/
public static void createServer(@NotNull int port) throws Exception {
//校验port是否合法
if (port<1024||port>65525){
throw new Exception("端口号不合法请重新传值");
}
//创建关键核心组件,并使用引导类进行
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap()
.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
//保持活动的连接状态
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//根据SocketChannel获取到PipeLine
ch.pipeline()
.addLast(new ServerInHandler1())
//针对Handler整体添加异步处理线程池
.addLast(taskGroup2,new ServerInHandler2());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(6677);
channelFuture.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if (future.isSuccess()) {
System.out.println("---端口绑定成功---");
}else{
System.out.println("---端口绑定失败尝试重连---");
}
}
});
channelFuture.channel().close().sync().addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if (future.isSuccess()){
System.out.println("---通道关闭失败——--");
}else{
System.out.println("---通道关闭成功---");
}
}
});
}catch (Exception e){
e.printStackTrace();
} finally{
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
createServer(6677);
}
}
/**
* @description:
* 1、handler之间的传递需要通过fire*的方法,这类方法将会调用handler链的下一个流向一直的handler的对应方法
* @function: 通过设置线程池来为耗时任务提供一个异步处理
* @author: Liu Menglei
* @Date: 2022/4/22 0022 11:26
*/
public class ServerInHandler1 extends ChannelInboundHandlerAdapter {
//创建一个用来执行耗时任务的线程池,本质上也是一个Group,当然Group的本质上也是线程池
static final EventLoopGroup taskGroup = new NioEventLoopGroup(16);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Active:"+"ServerInHandler1");
ctx.fireChannelActive();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("目前执行的是ServerInHandler1.channelRead,线程名称为:"+
Thread.currentThread().getName());
ByteBuf msgByf = (ByteBuf) msg;
System.out.println("接受到来自客户端的信息为:"+msgByf.toString(CharsetUtil.UTF_8));
taskGroup.submit(new Runnable() {
@Override
public void run() {
System.out.println("目前执行的是ServerInHandler1.channelRead中的异步耗时" +
"任务,线程名称为:"+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.writeAndFlush(Unpooled.copiedBuffer("服务端耗时任务回写的数据", CharsetUtil.UTF_8));
System.out.println("异步耗时任务执行完成,继续交由pipeline走handler链");
}
});
ctx.fireChannelRead("channelRead1生成的信息");
}
}
public class ServerInHandler2 extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Active:"+"ServerInHandler2");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("目前执行的是ServerInHandler2.channelRead,线程名称为:"+
Thread.currentThread().getName());
String byteBuf = (String) msg;
System.out.println("来自pipeline上游的信息为:"+byteBuf);
}
}
3、Client 实现
public class NoBlockTaskClient {
private final static String address = "127.0.0.1";
private final static int port = 6677;
/**
*@description: 根据传递过来的address以及port来创建客户端
*@params: 地址与端口号
*@returns:
*/
public static void createClient(String address,int port){
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup(1);
try {
//创建客户端启动对象
//注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap()
.group(group) //设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ClientInHandler1()); //加入自己的处理器
}
});
//启动客户端去连接服务器端
ChannelFuture connectFuture = bootstrap.connect("127.0.0.1", 6677).sync();
connectFuture.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if (future.isSuccess()) {
System.out.println("***Bootstrap成功连接***");
}else{
System.out.println("***Bootstrap连接失败***");
}
}
});
//给关闭通道进行监听,如果不添加这一步的话客户端回送过来的数据将会接受不到
ChannelFuture closeFuture = connectFuture.channel().closeFuture().sync();
closeFuture.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if (future.isSuccess()) {
System.out.println("---通道成功关闭---");
}else{
System.out.println("---通道关闭失败---");
}
}
});
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
createClient(address,port);
}
}
public class ClientInHandler1 extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("这里请求到了ClientInHandler1.chanelRead()");
ByteBuf buf = (ByteBuf) msg;
System.out.println("服务端回送过来的信息为:"+((ByteBuf) buf).toString(CharsetUtil.UTF_8));
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
}
}
四、链式调用先后顺序的验证
上面管理非阻塞任务队列的验证中已经验证了链式调用的先后顺序,同时在编解码器的章节中的实例也是可以看出的,现在回头来看这样的验证知识初级不了解Netty的时候需要的。
五、编码解码——业务逻辑中数据流程的装换
参见与编解码器章节即可
六、RPC的实现
1、实现前规划
- 提供一个统一接口的模块,此接口模块可以被客户端以及服务端引用
- 客户端需要提供一个根据接口Class文件生成对应代理的方法
- 在代理中解析接口的全路径以及入参返回值等信息,对这些信息进行封装,之后通过Netty请求服务端(这其中牵扯到的只是是序列化以及反序列化)
- 服务端在拿到这些信息之后回首先根据接口的相关信息通过反射生成一个真正的实现类,用于执行服务
- 将执行服务拿到的结果封装之后传递会客户端
上面的可以是实现RPC接口的一个基本步骤,细化的过程应该包括
- 写一个类似于注册中心的服务,里面通过Map的接口存储一些实例信息,一个接口可以对应多个实例,至于说执行的时候调用那个可以根据情况调用
- 客户端在通过工具获取代理的时候他会先查找一下注册中心是否包含有这个服务对应的实例,没有则返回报错信息;如果有多个的话根据一定的策略返回这个实例的全路径名或是其他的相关信息客户端拿到这个信息之后直接请求服务端,服务端根据这个实现类的全路径名生成代理服务
- 加入注册中心后还会有很多东西需要考虑,例如请求注册中心的时机、注册中心的发现与治理、心跳检测等;不过服务端有哪些服务实例可以使用以及如何注册到注册中心这是一个需要考虑的问题
化)
- 服务端在拿到这些信息之后回首先根据接口的相关信息通过反射生成一个真正的实现类,用于执行服务
- 将执行服务拿到的结果封装之后传递会客户端
上面的可以是实现RPC接口的一个基本步骤,细化的过程应该包括
- 写一个类似于注册中心的服务,里面通过Map的接口存储一些实例信息,一个接口可以对应多个实例,至于说执行的时候调用那个可以根据情况调用
- 客户端在通过工具获取代理的时候他会先查找一下注册中心是否包含有这个服务对应的实例,没有则返回报错信息;如果有多个的话根据一定的策略返回这个实例的全路径名或是其他的相关信息客户端拿到这个信息之后直接请求服务端,服务端根据这个实现类的全路径名生成代理服务
- 加入注册中心后还会有很多东西需要考虑,例如请求注册中心的时机、注册中心的发现与治理、心跳检测等;不过服务端有哪些服务实例可以使用以及如何注册到注册中心这是一个需要考虑的问题
RPC的完整实现确实有难度,牵扯到的知识面太广了,目前梳理思路先不做实现,后期有时间的话再做具体实现。