使用Netty编写一个多人聊天程序(下)-客户端实现和验收
引言
上篇文章中,详细阐述了服务端核心功能的设计思路和实现逻辑。与服务端相比起来,客户端的实现就比较简单了。Netty为开发者尽最大可能保证了编程模型的一致性。服务端和客户端的区别仅仅只是在于Channel具体实现类和BootStrap引导类的区别。
客户端的代码托管于:https://gitee.com/eric_ds/learnNetty/tree/master/client
命令编码
和服务端一样,客户端需要发送消息,首要考虑的问题也是对命令对象Command的二进制编码问题。每个命令对象本身内部最清晰自身结构,因此这里采用和服务端编码Receive对象一样的思路,在Command接口上增加writeToBuf方法,将编码的部分职责下放到具体的Command对象中,因此,命令编码的handler可以编写为如下形式。
public class CommandEncoder extends ChannelOutboundHandlerAdapter
{
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception
{
if (msg instanceof Command)
{
Command command = (Command) msg;
ByteBuf buffer = ctx.alloc().buffer();
buffer.writerIndex(4);
command.writeToBuf(buffer);
int writerIndex = buffer.writerIndex();
//消息协议前四个字节是整型变量,需要计算报文体长度
buffer.writerIndex(0).writeInt(writerIndex - 4).writerIndex(writerIndex);
ctx.write(buffer, promise);
}
else
{
throw new IllegalArgumentException();
}
}
}
响应解码
客户端的响应解码的职能和服务端对命令的解码职能很接近。在服务端的处理方式中,采用了一个命令解码器,在这个类中根据协议类型字段而对后续的字节进行解析处理构建Command对象。这种写法的缺点主要在于需要硬编码所有的命令类型,在新增或者修改命令对象的时候不太方便。参考命令编码的思路,我们可以将响应解码的具体内容放在具体的Receive对象中进行处理。也就是为Receive接口新增方法readFromBuf。在这个基础上,还需要解决的问题就是如何根据协议头的消息类型构建正确的Receive实现类。解决办法倒也简单,可以创建一个EnumMap,放入ReceiveType和对应Receive实现类的class对象,通过反射来构建。而这EnumMap可以在初始化的时候被传入。这样,后续消息格式变更或者消息新增,可以简单添加元素到EnumMap中实现,解码器则无需更改。
按照上面的思路,解码器的代码可以写为
public class ReceiveDecoder extends ChannelInboundHandlerAdapter
{
private EnumMap<ReceiveType, Class<? extends Receive>> enumMap;
public ReceiveDecoder(EnumMap<ReceiveType, Class<? extends Receive>> enumMap)
{
this.enumMap = enumMap;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
{
ByteBuf buffer = (ByteBuf) msg;
byte b = buffer.readByte();
ReceiveType receiveType = ReceiveType.value(b);
Class<? extends Receive> aClass = enumMap.get(receiveType);
Receive receive = aClass.newInstance();
receive.readFromBuf(buffer);
ctx.fireChannelRead(receive);
}
}
除了响应解码器外,在响应解码之前,首先是进行报文的拆包处理。这个拆包的思路和服务端中对命令的报文拆包思路是相同的,都是使用LengthFieldBasedFrameDecoder,从报文头读取长度,进而确定报文体的内容。
注册和登录示例
在完成命令编码和响应解码的基础上,我们就可以先完成一个注册和登录的小例子了。首先来看下服务端的引导应用,如下
public class Server
{
public static void main(String[] args) throws InterruptedException
{
DAOFactory daoFactory = new MemDAOFactory();
final Router router = new RouterImpl(daoFactory.getGroupDAO(), daoFactory.getRelationDAO());
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1), new NioEventLoopGroup());
//handler方法传入的处理器工作在服务端监听链接上
serverBootstrap.handler(new ChannelInboundHandlerAdapter()
{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
{
System.out.println("msg对象是一个SocketChannel:" + (msg instanceof SocketChannel));
ctx.fireChannelRead(msg);
}
});
final EnumMap<CommandType, CommandHandler.CommandProcessor> enumMap = new EnumMap<CommandType, CommandHandler.CommandProcessor>(CommandType.class);
enumMap.put(CommandType.LOGIN, new LoginProcessor(daoFactory.getClientDAO(), router));
enumMap.put(CommandType.REGISTER, new RegisterProcessor(daoFactory.getClientDAO(), router));
enumMap.put(CommandType.CREATE_GROUP, new CreateGroupProcessor(daoFactory.getGroupDAO(), daoFactory.getRelationDAO(), ro
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> 通过本专刊的学习,对网络开发所需掌握的基础理论知识会更加牢固,对网络应用涉及的线程模型,设计模式,高性能架构等更加明确。通过对Netty的源码深入讲解,使得读者对Netty达到“知其然更之所以然”的程度。在遇到一些线上的问题时,具备了扎实理论功底的情况,可以有的放矢而不会显得盲目。 本专刊购买后即可解锁所有章节,故不可以退换哦~ </p> <p> <br /> </p>
