08 netty的简单应用
Toy Example:聊天应用
0 应用功能划分
client包:客户端代码
message包:基于实际消息类型抽象出的java对象
protocol包:自定义协议的编解码器
server.service包: 模拟实际业务中的用户管理服务(保存数据库)
server.session包:客户端和服务器的会话管理
玩具需实现的聊天命令
客户端控制台输入命令格式 | 命令作用 |
---|---|
send [username] [content] | 指定username发送content |
gsend [group name] [content] | 指定group name发送content |
gcreate [group name] [m1,m2,m3...] | 指定group name建立群聊,初始化包含用户m1,m2,m3,... |
gmembers [group name] | 显示group name包含的用户 |
gjoin [group name] | 加入名称为group name的群聊 |
gquit [group name] | 退出名称为group name的群聊 |
quit | 客户端退出 |
- 上面不同的命令可以定义为不同的消息类型,针对每种消息类型,服务端根据消息类型定义不同的handler放入到pipeline中进行处理,比如上述命令可以定义出以下的消息类型。
知识点:channel中常用的事件:
io.netty.channel.ChannelInitializer:是一个特殊的ChannelInboundHandler,初始化注册到eventLoop的channel对象。
通用的ChannelInboundHandlerAdapte包含以下方法:
@Skip
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelRegistered();
}
@Skip
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelUnregistered();
}
@Skip
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
}
@Skip
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelInactive();
}
@Skip
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
@Skip
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelReadComplete();
}
@Skip
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
ctx.fireUserEventTriggered(evt);
}
方法 | 说明 |
---|---|
channelRegistered | channel注册时会调用该方法 |
channelActive | channel建立时会调用该方法 |
channelInactive | channel关闭时会调用该方法 |
channelUnregistered | channel取消注册时会调用该方法 |
注意:一个channel的声明周期中必然会按照顺序发生registerd-active-inactive-unregistered这四种类型的事件。
方法 | 说明 |
---|---|
channelRead | 读取事件发生会调用该方法 |
userEventTriggered | 用户自定义事件发生会调用该方法 |
1 登录流程的实现
需求要点:
1)客户端在建立连接时接受用户输入的用户名和密码并发送“封装登录消息的实例对象”给服务端,并接受服务端返回的”登录消息的应答实例对象“。
a) 通过重写ChannelInitializer.channelActive实现登录消息发送
b) 用户名和密码的输入需要另外使用一个线程接受,不能放入NIO线程中。
2)服务端接受“封装登录消息的实例对象”后进行处理并返回”登录消息的应答实例对象“。
a) 登录成功\失败的返回消息不同
b) 通过重写抽象类SimpleChannelInboundHandler<I>的channelRead0方法来处理特定类型的消息。
服务端对登录消息处理的handler
package application.chatRoom.server.handler;
import application.chatRoom.server.service.UserService;
import application.chatRoom.server.service.UserServiceFactory;
import application.chatRoom.server.session.SessionFactory;
import application.chatRoom.message.*;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
// 服务端: 处理客户端登录消息请求的入站handler
@ChannelHandler.Sharable
public class LoginRequestMessageHandler extends SimpleChannelInboundHandler<LoginRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRequestMessage msg) throws Exception {
// 此处service通过基于内存实现,实际开发中可以基于数据的service进行校验,
UserService userService = UserServiceFactory.getUserService();
LoginResponseMessage rs = null;
boolean flag = userService.login(msg.getUsername(),msg.getPassword());
if(flag){
// 保存 用户名=>服务器映射关系 的channel,这里的默认实现也是保存在内存中
SessionFactory.getSession().bind(ctx.channel(),msg.getUsername());
rs = new LoginResponseMessage(true,"登录成功");
}else{
rs = new LoginResponseMessage(false,"登录失败,用户名或密码错误");
}
ctx.writeAndFlush(rs); // 发送出站消息
}
}
2 在线单聊流程实现
需求:两个用户都在线的情况下,用户A在客户端控制台输入类似"send [用户B] [content]"命令,服务端将用户A发送的消息转发给对应的用户B。
服务端单聊消息的handler
基本思路:服务端需要维护用户名和channel的绑定关系从而快速实现转发
Netty:发送用户在客户端发送含有内容的ChatRequestMessage,服务端的入站handler()处理此类型消息
package application.chatRoom.server.handler;
import application.chatRoom.message.ChatRequestMessage;
import application.chatRoom.message.ChatResponseMessage;
import application.chatRoom.server.session.SessionFactory;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
//import application.protocol_design.message.*;
// 服务端: 处理客户端登录消息请求的入站handler
@ChannelHandler.Sharable
public class ChatRequestMessageHandler extends SimpleChannelInboundHandler<ChatRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ChatRequestMessage msg){
// 此处service通过基于内存实现,实际开发中可以基于数据的service进行校验,
try{
String toName = msg.getTo();
Channel toChannel = SessionFactory.getSession().getChannel(toName);
ChatResponseMessage r_msg = null;
if(toChannel != null){
toChannel.writeAndFlush(new ChatResponseMessage(msg.getFrom(),msg.getContent()));
// 问题:自定义的协议栈无法接受String类型的对象?,这里服务端发送成功,但客户端接受不到
// toChannel.writeAndFlush(msg.getContent());
toChannel.writeAndFlush(new ChatResponseMessage(msg.getFrom(),msg.getContent()));
System.out.printf("%s发送消息给%s\n",msg.getFrom(),msg.getTo());
r_msg = new ChatResponseMessage(true,"发送成功");
}else{
System.out.printf("%s发送消息给%s失败\n",msg.getFrom(),msg.getTo());
r_msg = new ChatResponseMessage(false,"发送失败");
}
ctx.writeAndFlush(r_msg);
}catch (Exception e){
e.printStackTrace();
}
}
}
- 单聊的基本逻辑是根据用户名检索channel,然后服务端转发发送者的信息。
3 在线群聊流程实现
需求:
- 用户在客户端控制台输入类似"gcreate [group name] [m1,m2,m3...]"命令,服务端将用户发送的消息中多个建立建立群组转发给对应的用户。
- 用户在客户端控制台输入类似"gsend [group name] [content]"命令,服务端将用户发送的消息转发给群组中所有用户。
服务端创建群组命令处理的handler
package application.chatRoom.server.handler;
import application.chatRoom.message.*;
import application.chatRoom.server.session.Group;
import application.chatRoom.server.session.GroupSession;
import application.chatRoom.server.session.GroupSessionFactory;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.util.List;
import java.util.Set;
// 根据命令,创建群组关系,本质上维护了一个 (群组名,用户-channel集合)。
@ChannelHandler.Sharable
public class GroupCreateRequestMessageHandler extends SimpleChannelInboundHandler<GroupCreateRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, GroupCreateRequestMessage msg) {
try{
String gName = msg.getGroupName();
Set<String> gMembers = msg.getMembers();
GroupSession gSession = GroupSessionFactory.getGroupSession();
Group g = gSession.createGroup(gName,gMembers);
GroupCreateResponseMessage rs = null;
if(g != null){
rs = new GroupCreateResponseMessage(false,"创建失败,群组已存在");
}else{
rs = new GroupCreateResponseMessage(true,gName+"创建成功");
List<Channel> list = gSession.getMembersChannel(gName);
for(Channel c:list) c.writeAndFlush(new GroupCreateResponseMessage(true,"您已加入群聊"+gName));
}
ctx.writeAndFlush(rs);
}catch (Exception e){
e.printStackTrace();
}
}
}
服务端群聊消息发送处理的handler
package application.chatRoom.server.handler;
import application.chatRoom.message.GroupChatRequestMessage;
import application.chatRoom.message.GroupChatResponseMessage;
import application.chatRoom.server.session.*;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.util.List;
/*
实现群内用户A的消息发送给群内其他人。
"gsend [group name] [content]"
*/
@ChannelHandler.Sharable
public class GroupChatRequestMessageHandler extends SimpleChannelInboundHandler<GroupChatRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, GroupChatRequestMessage msg){
try{
String gName = msg.getGroupName();
String content = msg.getContent();
GroupSession gSession = GroupSessionFactory.getGroupSession();
List<Channel> list = gSession.getMembersChannel(gName);
if(list == null) return;
for(Channel c:list){
c.writeAndFlush(new GroupChatResponseMessage(msg.getFrom(),content));
}
}catch (Exception e){
e.printStackTrace();
}
}
}
上面代码中在线群聊消息发送的基本逻辑:根据群聊名称获取与服务端连接的所有在线用户的channel,然后转发消息到每个channel。
4 服务端连接释放处理策略
服务端连接的释放分为两种类型:
- 正常释放:客户端遵循业务逻辑或者程序执行时抛出异常,主动关闭连接通道,此时会触发netty的inactive事件,
- 异常释放: 客户端由于不可抗力,比如断电或者手动强行关闭,此时服务端会抛出异常,
目标:上面两种情况都要求我们能够对该客户端的相关信息做好善后工作。
- netty中对于上面两种情况对应两种事件类型,可以通过重写对应方法做好善后工作
服务端异常处理的handler
package application.chatRoom.server.handler;
import application.chatRoom.server.session.SessionFactory;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
@ChannelHandler.Sharable
@Slf4j
public class QuitHandler extends ChannelInboundHandlerAdapter {
// 客户端与服务端的channel断开时,客户端的工作
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
SessionFactory.getSession().unbind(ctx.channel());
log.debug("服务端断开{}",ctx.channel());
}
// 发生异常时,客户端的的工作
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
try{
SessionFactory.getSession().unbind(ctx.channel());
log.debug("发生异常{},断开连接{}",cause.getMessage(), ctx.channel());
}catch (Exception e){
e.printStackTrace();
}
}
}
5 服务端假死连接的检测策略
关于TCP/IOCP构架中出现的Socket假死连接解决方案
假死连接:服务端存在的无效连接
产生原因:
1)网络设备出现故障,例如网卡,机房等,底层的 TCP 连接已经断开了,但应用程序没有感知到,仍然占用着资源。
2)公网网络不稳定,出现丢包。如果连续出现丢包,这时现象就是客户端数据发不出去,服务端也一直收不到数据,就这么一直耗着
3)应用程序线程阻塞,无法进行数据读写
带来的问题:1)占用的资源不能自动释放,造车服务器的”越来越慢“ 2)向假死的连接发送数据,得到的反馈是发送超时
网上的通用解决策略:TCP设置keepLive + 应用层启动定时线程发送心跳包检测假死连接并及时释放资源
Netty的策略:提供IdleStateHandler检测当前channel的读/写空闲时间并触发事件。开发者可以捕获事件
进行建立心跳机制。
比如:客户端每3s写空闲就发送一个心跳包,服务端每5s读空闲就关闭当前channel,这样能够过滤掉假死连接。
netty提供的检测读写空闲的handler,当检测到空闲事件满足要求会触发事件:
new IdleStateHandler(读空闲,写空闲,读/写空闲)
服务端实例
- 下面代码中服务端检测到某个channel超过5s没有读取活动会触发状态为IdleState.READER_IDLE的用户事件,此时可以判定其为假死连接,将该channel关闭
ch.pipeline().addLast(new IdleStateHandler(5,0,0));
ch.pipeline().addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent)evt;
if(event.state() == IdleState.READER_IDLE) {
log.debug("当前channel,5s没有读取数据");
}
}
});
客户端
- 下面代码中客户端检测到某个channel超过3s没有写入活动会触发状态为IdleState.WRITER_IDLE的用户事件,此时可以发送心跳包给服务端
ch.pipeline().addLast(new IdleStateHandler(0,3,0));
ch.pipeline().addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent)evt;
if(event.state() == IdleState.WRITER_IDLE) {
log.debug("当前channel,3s写入数据,向服务端发送心跳包保活");
ctx.writeAndFlush(new PingMessage());
}
}
});
总结:netty中通过检测读写空闲事件触发相应事件,然后事件处理时发送心跳包,上面代码中实现了
客户端每3s发送ping包,服务端每5s检测channel是否有读取,由于3s < 5s,因此只要客户端连接正常,服务端一定不会触发读空闲事件从而关闭channel。
6 整体代码
客户端代码
package application.chatRoom.client;
import application.chatRoom.protocol.MessageCodecSharable;
import application.chatRoom.protocol.ProtocolFrameDecoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import application.chatRoom.message.*;
//import application.protocol_design.message.*;
@Slf4j
public class ChatClient {
public static void main(String[] args) {
TreeMap<Integer,Integer> map = new TreeMap<>();
NioEventLoopGroup group = new NioEventLoopGroup();
// 日志handler
LoggingHandler LOGIN_HANDLER = new LoggingHandler();
// 自定义协议编解码器
MessageCodecSharable PROTOCOL = new MessageCodecSharable();
// 基于AQS实现的工具类用于同步NIO线程和其他线程
CountDownLatch WAIT_FOR_LOG = new CountDownLatch(1);
AtomicBoolean LOG_FLAG = new AtomicBoolean();
try{
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ProtocolFrameDecoder()); // 通过长度字段处理粘包和半包
ch.pipeline().addLast(LOGIN_HANDLER);
ch.pipeline().addLast(PROTOCOL);
ch.pipeline().addLast(new IdleStateHandler(0,3,0));
ch.pipeline().addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent)evt;
if(event.state() == IdleState.WRITER_IDLE) {
log.debug("当前channel,3s写入数据,向服务端发送心跳包保活");
ctx.writeAndFlush(new PingMessage());
}
}
});
// 客户端添加入站处理器
ch.pipeline().addLast("client login handler",new ChannelInboundHandlerAdapter(){
// 在channel建立时会出发active事件向服务器发送登录消息
@Override
public void channelActive(ChannelHandlerContext ctx) {
try{
LoginOperate op = new LoginOperate(ctx,WAIT_FOR_LOG,LOG_FLAG);
new Thread(op,"Thread:system in").start();
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
System.out.println("客户端通道关闭");
}
// 读取服务端返回的消息,为什么无法接受String类型的信息?(待解决)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg){
try{
log.debug("入站消息:{}",msg);
if(msg instanceof LoginResponseMessage){
LoginResponseMessage tmp = (LoginResponseMessage)msg;
LOG_FLAG.set(tmp.isSuccess()); // 这里只是两个线程,因此直接set,没有进行compare and swap
}
WAIT_FOR_LOG.countDown();
}catch (Exception e){
e.printStackTrace();
}
}
});
}
});
Channel channel = bootstrap.connect("localhost", 8080).sync().channel();
channel.closeFuture().sync();
}catch (Exception e){
log.debug("client error",e);
}finally {
group.shutdownGracefully();
}
}
}
// 用户交互线程,接受控制台输入并打印结果
class LoginOperate implements Runnable{
private ChannelHandlerContext ctx;
// 线程间同步机制
private CountDownLatch latch;
private AtomicBoolean flag;
Scanner sc;
public LoginOperate(ChannelHandlerContext ctx,CountDownLatch latch,AtomicBoolean flag){
this.ctx = ctx;
this.latch = latch;
this.flag = flag;
sc = new Scanner(System.in);
}
@Override
public void run() {
System.out.println("请输入用户名:");
String name = sc.nextLine();
System.out.println("请输入密码:");
String passWord = sc.nextLine();
LoginRequestMessage msg = new LoginRequestMessage(name,passWord);
// 客户端传递登录消息给服务端
ctx.writeAndFlush(msg);
System.out.println("等待....");
try{
latch.await();
}catch (Exception e){
e.printStackTrace();
}
if(flag.get()){
while(true){
System.out.println("==================================");
System.out.println("send [username] [content]");
System.out.println("gsend [group name] [content]");
System.out.println("gcreate [group name] [m1,m2,m3...]");
System.out.println("gmembers [group name]");
System.out.println("gjoin [group name]");
System.out.println("gquit [group name]");
System.out.println("quit");
System.out.println("==================================");
String[] cmd = sc.nextLine().split(" ");
switch (cmd[0]){
case "send":
ctx.writeAndFlush(new ChatRequestMessage(name,cmd[1],cmd[2]));
break;
case "gsend":
ctx.writeAndFlush(new GroupChatRequestMessage(name,cmd[1],cmd[2]));
break;
case "gcreate":
Set<String> set = new HashSet<>(Arrays.asList(cmd[2].split(",")));
set.add(name); // 加入自己
ctx.writeAndFlush(new GroupCreateRequestMessage(cmd[1], set));
break;
case "gmembers":
ctx.writeAndFlush(new GroupMembersRequestMessage(cmd[1]));
break;
case "gjoin":
ctx.writeAndFlush(new GroupJoinRequestMessage(name, cmd[1]));
break;
case "gquit":
ctx.writeAndFlush(new GroupQuitRequestMessage(name, cmd[1]));
break;
case "quit":
ctx.channel().close();
return;
}
}
}else{
System.out.println("登录失败,关闭连接");
ctx.channel().close();
return;
}
}
}
服务端代码
package application.chatRoom.server;
import application.chatRoom.protocol.MessageCodecSharable;
import application.chatRoom.protocol.ProtocolFrameDecoder;
import application.chatRoom.server.handler.*;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ChatServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
// 日志handler
LoggingHandler LOG_HANDLER = new LoggingHandler();
// 自定义协议编解码器
MessageCodecSharable PROTOCOL = new MessageCodecSharable();
// 处理客户端的登录消息类型的handler
LoginRequestMessageHandler LOGIN_HANDLER = new LoginRequestMessageHandler();
// 处理客户端单聊消息类型的handler
ChatRequestMessageHandler CHAT_REQUEST_HANDLER = new ChatRequestMessageHandler();
// 处理客户端创建群聊的消息类型的handler
GroupCreateRequestMessageHandler GROUP_CREATE_HANDLER = new GroupCreateRequestMessageHandler();
// 对客户端连接断开处理进行善后工作
QuitHandler QUIT_HANDLER = new QuitHandler();
GroupChatRequestMessageHandler GROUP_REQUEST_HANDLER = new GroupChatRequestMessageHandler();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 官方netty的handler,通过读空闲连接时间和写空闲时间是否过长判断是否是空闲
ch.pipeline().addLast(new ProtocolFrameDecoder()); // 通过长度字段处理粘包和半包
ch.pipeline().addLast(new IdleStateHandler(5,0,0));
ch.pipeline().addLast(new ChannelDuplexHandler(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent)evt;
if(event.state() == IdleState.READER_IDLE) {
log.debug("当前channel,5s没有读取数据");
}
}
});
ch.pipeline().addLast(LOG_HANDLER);
ch.pipeline().addLast(PROTOCOL);
ch.pipeline().addLast(LOGIN_HANDLER);
ch.pipeline().addLast(CHAT_REQUEST_HANDLER);
ch.pipeline().addLast(GROUP_CREATE_HANDLER);
ch.pipeline().addLast(GROUP_REQUEST_HANDLER);
ch.pipeline().addLast(QUIT_HANDLER);
}
});
Channel channel = serverBootstrap.bind(8080).sync().channel();
channel.closeFuture().sync();
} catch (Exception e) {
log.error("server error", e);
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
Toy Example:简洁RPC框架
RPC的Wikipedia定义:
In distributed computing, a remote procedure call (RPC) is when a computer program causes a procedure (subroutine) to execute in a different address space (commonly on another computer on a shared network), which is coded as if it were a normal (local) procedure call, without the programmer explicitly coding the details for the remote interaction. That is, the programmer writes essentially the same code whether the subroutine is local to the executing program, or remote. This is a form of client–server interaction (caller is client, executor is server), typically implemented via a request–response message-passing system. In the object-oriented programming paradigm, RPCs are represented by remote method invocation (RMI). The RPC model implies a level of location transparency, namely that calling procedures are largely the same whether they are local or remote, but usually they are not identical, so local calls can be distinguished from remote calls. Remote calls are usually orders of magnitude slower and less reliable than local calls, so distinguishing them is important.
RPCs are a form of inter-process communication (IPC), in that different processes have different address spaces: if on the same host machine, they have distinct virtual address spaces, even though the physical address space is the same; while if they are on different hosts, the physical address space is different. Many different (often incompatible) technologies have been used to implement the concept.
背景:远程调用(远程方法调用)通常是建立在网络通信的基础上。属于进程间的网络通信。netty是网络通信框架,因此可以用于实现RPC
需求明确:
1)抽象出RPC请求和返回的消息对象。
2)实现两个handler分别用于处理请求和返回消息对象
这里使用Toy Example:聊天应用中的自定义协议
step1:消息对象定义
RPC的请求/响应的消息对象定义如下:
RPC request message class 包含属性:
接口名称 | 方法名称 | 方法返回类型 | 方法参数类型数组 | 方法参数值数组 |
---|
// RPC请求消息class
package extension.chatRoom.message;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString(callSuper = true)
public class RpcRequestMessage extends Message {
private String interfaceName; // 接口全限定名
private String methodName; // 接口方法名
private Class<?> returnType; // 方法返回类型
private Class[] parameterTypes; // 方法参数类型数组
private Object[] parameterValue; // 方法参数值数组
public RpcRequestMessage(int sequenceId, String interfaceName, String methodName, Class<?> returnType, Class[] parameterTypes, Object[] parameterValue) {
super.setSequenceId(sequenceId);
this.interfaceName = interfaceName;
this.methodName = methodName;
this.returnType = returnType;
this.parameterTypes = parameterTypes;
this.parameterValue = parameterValue;
}
@Override
public int getMessageType() {
return RPC_MESSAGE_TYPE_REQUEST;
}
}
// RPC响应消息class
package extension.chatRoom.message;
import lombok.Data;
import lombok.ToString;
@Data
@ToString(callSuper = true)
public class RpcResponseMessage extends Message {
private Object returnValue;
private Exception exceptionValue;
@Override
public int getMessageType() {
return RPC_MESSAGE_TYPE_RESPONSE;
}
}
RPC response message class 包含属性: 返回值对象(调用成功)和异常值对象(调用失败)
step2:handler的基本实现
- RPC请求消息的handler(被调用方)
基本思想:服务端根据客户端发送消息中的信息通过Java反射调用方法获取执行结果,并将结果封装到RPC Response Message发送给客户端
package extension.chatRoom.server.handler;
import extension.chatRoom.message.RpcRequestMessage;
import extension.chatRoom.message.RpcResponseMessage;
import extension.chatRoom.server.service.HelloService;
import extension.chatRoom.server.service.HelloServiceImpl;
import extension.chatRoom.server.service.ServicesFactory;
import extension.chatRoom.server.service.UserServiceFactory;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage message) throws Exception {
RpcResponseMessage rpcResponseMessage = new RpcResponseMessage();
rpcResponseMessage.setSequenceId(message.getSequenceId()); // 设置消息ID
try{
HelloService service = (HelloService) ServicesFactory.getService(Class.forName(message.getInterfaceName()));
// 根据远程调用方发送的信息通过反射调用本地方法
Method method = service.getClass().getMethod(message.getMethodName(),message.getParameterTypes());
rpcResponseMessage.setReturnValue(method.invoke(service,message.getParameterValue()));
}catch (Exception e){
rpcResponseMessage.setExceptionValue(e);
}
ctx.writeAndFlush(rpcResponseMessage);
}
}
- RPC响应消息的handler(调用方)
package extension.chatRoom.server.handler;
import extension.chatRoom.message.RpcRequestMessage;
import extension.chatRoom.message.RpcResponseMessage;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {
log.debug("{}",msg);
}
}
step3:客户端实现(动态代理应用)
需求:通过动态代理为远程调用的接口生成代理类进行方法执行
package extension.chatRoom.client;
import extension.chatRoom.message.RpcRequestMessage;
import extension.chatRoom.protocol.MessageCodecSharable;
import extension.chatRoom.protocol.ProtocolFrameDecoder;
import extension.chatRoom.protocol.SequenceIdGenerator;
import extension.chatRoom.server.handler.RpcResponseMessageHandler;
import extension.chatRoom.server.service.HelloService;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.concurrent.DefaultPromise;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Proxy;
/*
支持远程调用的RPC的Client实现
创建单例channel对象(为何channel的创建采用单例模式?)
如果不采用单例模式,每次执行对应代码都会创建一个新的channel,有何影响
*/
@Slf4j
public class RpcClientManager {
private volatile static Channel channel = null;
public static void main(String[] args) {
// 方式1:远程调用服务端HelloService.sayHello方法
// Channel channel = getChannel();
// RpcRequestMessage message = new RpcRequestMessage(
// 1,
// "extension.chatRoom.server.service.HelloService",
// "sayHello",
// String.class,
// new Class[]{String.class},
// new Object[]{"dog"}
// );
// System.out.println(channel.writeAndFlush(message));
// 方式2:通过代理对象封装方式1实现远程调用
HelloService service = getProxyService(HelloService.class); // 获取HelloService的代理类
// 对sayHello方法进行了三次调用, 这里sayHello的调用方式是同步远程调用,
// 成熟的RPC框架会提供异步调用的方式
System.out.println(service.sayHello("dog"));
// System.out.println(service.sayHello("cat"));
// System.out.println(service.sayHello("mouse"));
}
// 通过动态代理对远程调用进行封装
public static <T> T getProxyService(Class<T> serviceClass){
// JDK提供的动态代理方法(类加载器,类实现的接口,invocationHandler接口的实现类)
// 由于invocationHandler是单个方法的接口,因此可以采用lambda表达式简化
Class<?>[] interfaces = new Class[]{serviceClass}; // 这里由于传入的Class是接口,因此需要自己构造接口数组
Object o = Proxy.newProxyInstance(serviceClass.getClassLoader(),interfaces, (proxy, method, args) -> {
RpcRequestMessage message = new RpcRequestMessage(
SequenceIdGenerator.nextId(), // 单机版本的ID生成器
serviceClass.getName(), // 接口名称的字符串表示
method.getName(),
method.getReturnType(),
method.getParameterTypes(),
args
);
ChannelFuture future = getChannel().writeAndFlush(message); // 发送RPC请求消息进行远程调用
// 添加回调函数,检测client本地执行时是否有异常发生,比如序列化发生的异常可以通过该函调函数检测到
future.addListener(promise->{
if(!promise.isSuccess()){
Throwable cause = promise.cause();
log.error("客户端write消息失败{}",cause);
}
});
// 当前线程中writeAndFlush调用后,需要从NIO线程获取执行结果,如何获取?
// 策略:采用Promise容器获取NIO线程的执行结果
DefaultPromise<Object> promise = new DefaultPromise<>(getChannel().eventLoop());
RpcResponseMessageHandler.promises.put(message.getSequenceId(),promise);
promise.await(); //同步等待promise获取结果
// 调用成功返回结果,调用失败则返回异常
if(promise.isSuccess()) return promise.getNow();
else{
throw new RuntimeException("调用失败",promise.cause());
}
});
return (T) o;
}
// DCL获取单例channel对象
// 多次调用确保获取的channel实例对象唯一(采用DCL确保唯一单例)
private static Channel getChannel() {
if(channel != null) return channel;
synchronized (RpcClientManager.class) {
if(channel != null) return channel;
channel = initClient();
return channel;
}
}
private static Channel initClient() {
NioEventLoopGroup group = new NioEventLoopGroup();
// 日志handler
LoggingHandler LOGIN_HANDLER = new LoggingHandler();
// 自定义协议编解码器
MessageCodecSharable PROTOCOL = new MessageCodecSharable();
RpcResponseMessageHandler RPC_HANDLER = new RpcResponseMessageHandler();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ProtocolFrameDecoder()); // 通过长度字段处理粘包和半包
ch.pipeline().addLast(LOGIN_HANDLER);
ch.pipeline().addLast(PROTOCOL);
ch.pipeline().addLast(RPC_HANDLER);
}
});
channel = bootstrap.connect("localhost", 8080).sync().channel();
// 提供channel关闭时的回调函数,将NIO线程池同步关闭
channel.closeFuture().addListener(future1 -> {
group.shutdownGracefully();
});
} catch (Exception e) {
log.error("client error",e);
}
return channel;
}
}
RpcResponseMessageHandler的改造
package extension.chatRoom.server.handler;
import extension.chatRoom.message.RpcRequestMessage;
import extension.chatRoom.message.RpcResponseMessage;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.concurrent.Promise;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
// 该handler虽然是有状态的,但concurrent hashmap 保证了线程安全性
@Slf4j
@ChannelHandler.Sharable
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {
// 定义promises集合用于存放结果,其他线程可以静态属性的promise容器集合获取NIO线程的执行结果
public static ConcurrentHashMap<Integer, Promise<Object>> promises = new ConcurrentHashMap<>();
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) {
try {
log.debug("{}", msg);
if (promises.containsKey(msg.getSequenceId())) {
Promise<Object> p = promises.remove(msg.getSequenceId()); // 获取后并清除
Object v = msg.getReturnValue();
Exception e = msg.getExceptionValue();
// 返回的异常为null表示调用成功
if (e == null) p.setSuccess(v);
else p.setFailure(e);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
基本思想:通过JDK的动态代理为远程调用接口生成对应的代理类,在代理类中实现将RpcRequestMessage写入channel,由于远程调用的结果是由于NIO线程从channel获取(RpcResponseMessageHandler),因此调用线程需要与NIO线程进行通信从而获取远程调用的结果,这里采用Netty提供的Promise容器实现调用线程和NIO线程的通信。
// 通过动态代理对远程调用进行封装
public static <T> T getProxyService(Class<T> serviceClass){
// JDK提供的动态代理方法(类加载器,类实现的接口,invocationHandler接口的实现类)
// 由于invocationHandler是单个方法的接口,因此可以采用lambda表达式简化
Class<?>[] interfaces = new Class[]{serviceClass}; // 这里由于传入的Class是接口,因此需要自己构造接口数组
Object o = Proxy.newProxyInstance(serviceClass.getClassLoader(),interfaces, (proxy, method, args) -> {
RpcRequestMessage message = new RpcRequestMessage(
SequenceIdGenerator.nextId(), // 单机版本的ID生成器
serviceClass.getName(), // 接口名称的字符串表示
method.getName(),
method.getReturnType(),
method.getParameterTypes(),
args
);
ChannelFuture future = getChannel().writeAndFlush(message); // 发送RPC请求消息进行远程调用
// 添加回调函数,检测client本地执行时是否有异常发生,比如序列化发生的异常可以通过该函调函数检测到
future.addListener(promise->{
if(!promise.isSuccess()){
Throwable cause = promise.cause();
log.error("客户端write消息失败{}",cause);
}
});
// 当前线程中writeAndFlush调用后,需要从NIO线程获取执行结果,如何获取?
// 策略:采用Promise容器获取NIO线程的执行结果
DefaultPromise<Object> promise = new DefaultPromise<>(getChannel().eventLoop());
RpcResponseMessageHandler.promises.put(message.getSequenceId(),promise);
promise.await(); //同步等待promise获取结果
// 调用成功返回结果,调用失败则返回异常
if(promise.isSuccess()) return promise.getNow();
else{
throw new RuntimeException("调用失败",promise.cause());
}
});
return (T) o;
}
线程间的交互流程
- 代理类方法调用线程
1)构造RPC请求消息对象并写入channel
2)在map中放入promise供入站handler存放结果,RpcResponseMessageHandler.promises.put(message.getSequenceId(),promise);
3)调用await阻塞线程等待结果
4)获得结果并返回
- 入站消息处理的NIO线程(RpcResponseMessageHandler)
从map中检查key是否存在并设置结果
上述线程间的通信本质上是保护性暂停模式的体现,该模式的实现通过两个线程关联共享的promise容器实现
注意点
1)上述代码中通过ID区分promise容器,这种方法要确保ID生成的唯一性
2)上述实现从RPC框架的使用者角度来看,是同步远程调用(用到了promise.await),成熟的框架中应该支持异步调用,以及带有超时时间的远程调用。
3)上述实现中采用的自定义协议并基于长度字段控制消息长度,由于消息帧长度是写死的,应该在消息发送前对长度字段进行检查并确保消息不超过固定长度,比如远程调用失败返回异常信息,不需要返回所有堆栈信息,这样做会导致消息过长从而超出限制。
4)上述实现中采用JDK动态代理并且通过配置文件指定远程调用接口的实现类,实际上也可以通过spring框架实现上述功能。
完整代码
服务端
- handler代码
package extension.chatRoom.server.handler;
import extension.chatRoom.message.RpcRequestMessage;
import extension.chatRoom.message.RpcResponseMessage;
import extension.chatRoom.server.service.HelloService;
import extension.chatRoom.server.service.HelloServiceImpl;
import extension.chatRoom.server.service.ServicesFactory;
import extension.chatRoom.server.service.UserServiceFactory;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage message) throws Exception {
RpcResponseMessage rpcResponseMessage = new RpcResponseMessage();
rpcResponseMessage.setSequenceId(message.getSequenceId()); // 设置消息ID
try{
HelloService service = (HelloService) ServicesFactory.getService(Class.forName(message.getInterfaceName()));
// 根据远程调用方发送的信息通过反射调用本地方法
Method method = service.getClass().getMethod(message.getMethodName(),message.getParameterTypes());
rpcResponseMessage.setReturnValue(method.invoke(service,message.getParameterValue()));
}catch (Exception e){
// 服务端远程调用出现异常后不需要将所有堆栈消息返回,由于自定义协议采用的FieldBased1024字节的数据帧大小
// 返回所有的异常堆栈消息可能会导致消息长度超出1024字节。
e.printStackTrace();
String msg = e.getCause().getMessage();
rpcResponseMessage.setExceptionValue(new RuntimeException("服务端执行远程调用出现异常"+msg));
}
ctx.writeAndFlush(rpcResponseMessage);
}
}
- 启动代码
package extension.chatRoom.server;
import extension.chatRoom.protocol.MessageCodecSharable;
import extension.chatRoom.protocol.ProtocolFrameDecoder;
import extension.chatRoom.server.handler.QuitHandler;
import extension.chatRoom.server.handler.RpcRequestMessageHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RpcServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();
RpcRequestMessageHandler RPC_HANDLER = new RpcRequestMessageHandler();
// 对客户端连接断开处理进行善后工作
QuitHandler QUIT_HANDLER = new QuitHandler();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ProtocolFrameDecoder());
ch.pipeline().addLast(LOGGING_HANDLER);
ch.pipeline().addLast(MESSAGE_CODEC);
ch.pipeline().addLast(RPC_HANDLER);
ch.pipeline().addLast(QUIT_HANDLER);
}
});
Channel channel = serverBootstrap.bind(8080).sync().channel();
channel.closeFuture().sync();
} catch (InterruptedException e) {
log.error("server error", e);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
客户端
- handler代码
package extension.chatRoom.server.handler;
import extension.chatRoom.message.RpcRequestMessage;
import extension.chatRoom.message.RpcResponseMessage;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.concurrent.Promise;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
// 该handler虽然是有状态的,但concurrent hashmap 保证了线程安全性
@Slf4j
@ChannelHandler.Sharable
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {
// 定义promises集合用于存放结果,其他线程可以静态属性的promise容器集合获取NIO线程的执行结果
public static ConcurrentHashMap<Integer, Promise<Object>> promises = new ConcurrentHashMap<>();
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) {
try {
log.debug("{}", msg);
if (promises.containsKey(msg.getSequenceId())) {
Promise<Object> p = promises.remove(msg.getSequenceId()); // 获取后并清除
Object v = msg.getReturnValue();
Exception e = msg.getExceptionValue();
// 返回的异常为null表示调用成功
if (e == null) p.setSuccess(v);
else p.setFailure(e);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
- 启动代码
package extension.chatRoom.client;
import extension.chatRoom.message.RpcRequestMessage;
import extension.chatRoom.protocol.MessageCodecSharable;
import extension.chatRoom.protocol.ProtocolFrameDecoder;
import extension.chatRoom.protocol.SequenceIdGenerator;
import extension.chatRoom.server.handler.RpcResponseMessageHandler;
import extension.chatRoom.server.service.HelloService;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.concurrent.DefaultPromise;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Proxy;
/*
支持远程调用的RPC的Client实现
1)创建单例channel对象(为何channel的创建采用单例模式?)
如果不采用单例模式,每次执行对应代码都会创建一个新的channel,有何影响
*/
@Slf4j
public class RpcClientManager {
private volatile static Channel channel = null;
public static void main(String[] args) {
// 方式1:远程调用服务端HelloService.sayHello方法
// Channel channel = getChannel();
// RpcRequestMessage message = new RpcRequestMessage(
// 1,
// "extension.chatRoom.server.service.HelloService",
// "sayHello",
// String.class,
// new Class[]{String.class},
// new Object[]{"dog"}
// );
// System.out.println(channel.writeAndFlush(message));
// 方式2:通过代理对象封装方式1实现远程调用
HelloService service = getProxyService(HelloService.class); // 获取HelloService的代理类
// 对sayHello方法进行了三次调用, 这里sayHello的调用方式是同步远程调用,
// 成熟的RPC框架会提供异步调用的方式
System.out.println(service.sayHello("dog"));
// System.out.println(service.sayHello("cat"));
// System.out.println(service.sayHello("mouse"));
}
// 通过动态代理对远程调用进行封装
public static <T> T getProxyService(Class<T> serviceClass){
// JDK提供的动态代理方法(类加载器,类实现的接口,invocationHandler接口的实现类)
// 由于invocationHandler是单个方法的接口,因此可以采用lambda表达式简化
Class<?>[] interfaces = new Class[]{serviceClass}; // 这里由于传入的Class是接口,因此需要自己构造接口数组
Object o = Proxy.newProxyInstance(serviceClass.getClassLoader(),interfaces, (proxy, method, args) -> {
RpcRequestMessage message = new RpcRequestMessage(
SequenceIdGenerator.nextId(), // 单机版本的ID生成器
serviceClass.getName(), // 接口名称的字符串表示
method.getName(),
method.getReturnType(),
method.getParameterTypes(),
args
);
ChannelFuture future = getChannel().writeAndFlush(message); // 发送RPC请求消息进行远程调用
// 添加回调函数,检测client本地执行时是否有异常发生,比如序列化发生的异常可以通过该函调函数检测到
future.addListener(promise->{
if(!promise.isSuccess()){
Throwable cause = promise.cause();
log.error("客户端write消息失败{}",cause);
}
});
// 当前线程中writeAndFlush调用后,需要从NIO线程获取执行结果,如何获取?
// 策略:采用Promise容器获取NIO线程的执行结果
DefaultPromise<Object> promise = new DefaultPromise<>(getChannel().eventLoop());
RpcResponseMessageHandler.promises.put(message.getSequenceId(),promise);
promise.await(); //同步等待promise获取结果
// 调用成功返回结果,调用失败则返回异常
if(promise.isSuccess()) return promise.getNow();
else{
throw new RuntimeException("调用失败",promise.cause());
}
});
return (T) o;
}
// DCL获取单例channel对象
// 多次调用确保获取的channel实例对象唯一(采用DCL确保唯一单例)
private static Channel getChannel() {
if(channel != null) return channel;
synchronized (RpcClientManager.class) {
if(channel != null) return channel;
channel = initClient();
return channel;
}
}
private static Channel initClient() {
NioEventLoopGroup group = new NioEventLoopGroup();
// 日志handler
LoggingHandler LOGIN_HANDLER = new LoggingHandler();
// 自定义协议编解码器
MessageCodecSharable PROTOCOL = new MessageCodecSharable();
RpcResponseMessageHandler RPC_HANDLER = new RpcResponseMessageHandler();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ProtocolFrameDecoder()); // 通过长度字段处理粘包和半包
ch.pipeline().addLast(LOGIN_HANDLER);
ch.pipeline().addLast(PROTOCOL);
ch.pipeline().addLast(RPC_HANDLER);
}
});
channel = bootstrap.connect("localhost", 8080).sync().channel();
// 提供channel关闭时的回调函数,将NIO线程池同步关闭
channel.closeFuture().addListener(future1 -> {
group.shutdownGracefully();
});
} catch (Exception e) {
log.error("client error",e);
}
return channel;
}
}
总结
采用netty进行基础的网络应用开发通常需要把握以下关键点:
1.根据业务需求确定底层通信机制包括应用层协议(自定义协议/现有协议如http),序列化算法等
- 实现应用层(对象->字节数组->对象)的可靠双向传输。
2.根据选定的协议和业务需求抽象出消息类型定义为类
- 比如聊天应用中包含 单聊请求/响应消息类,群聊请求/响应消息类,入聊请求/响应消息类,退聊请求/响应消息类,创建群聊请求/响应消息类
- 比如RPC框架中定义了RPC request/response message class
3.根据业务需求对每种消息类型实现客户端/服务端的出入站handler
- 难点在于线程间的通信,异步/同步调用的处理,各种异常处理
- 超时处理,假死连接处理等可靠性通常都需要考虑
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
2021-04-12 04 G1垃圾回收器的介绍以及垃圾回收调优的基础知识和简单案例
2021-04-12 03 JVM中垃圾回收算法以及典型的垃圾回收器
2021-04-12 02 Java的引用类型以及应用场景
2021-04-12 15 Java线程安全的类以及hashmap与并发的hashmap的介绍
2021-04-12 14 JUC的Semaphore,CountDownLatch,Cyclicbarrier的应用与原理