基于netty的网络聊天室(一)——基础框架搭建

最近在学习Netty框架,使用的学习教材是李林锋著的《Netty权威指南》。国内关于netty的书籍几乎没有,这本书算是比较好的入门资源了。

我始终觉得,学习一个新的框架,除了研究框架的源代码之外,还应该使用该框架开发一个实际的小应用。为此,我选择Netty作为通信框架,开发一个模仿QQ的聊天室。

基本框架是这样设计的,使用Netty作为通信网关,使用JavaFX开发客户端界面,使用Spring作为IOC容器,使用MyBatics支持持久化。本文将着重介绍Netty的私有协议栈开发,使用的Netty版本是最新的5.0.0.Alpha2版本。

服务端程序代码:

流程步骤:

1.启动Reactor线程组监听客户端链路的连接与IO网络读写。


[java] view plain copy



  1. package com.kingston.netty;  
  2.   
  3. import io.netty.bootstrap.ServerBootstrap;  
  4. import io.netty.channel.ChannelFuture;  
  5. import io.netty.channel.ChannelInitializer;  
  6. import io.netty.channel.ChannelOption;  
  7. import io.netty.channel.ChannelPipeline;  
  8. import io.netty.channel.EventLoopGroup;  
  9. import io.netty.channel.nio.NioEventLoopGroup;  
  10. import io.netty.channel.socket.SocketChannel;  
  11. import io.netty.channel.socket.nio.NioServerSocketChannel;  
  12. import io.netty.handler.codec.LengthFieldBasedFrameDecoder;  
  13. import io.netty.handler.codec.LengthFieldPrepender;  
  14. import io.netty.handler.timeout.IdleStateHandler;  
  15.   
  16. import java.io.IOException;  
  17. import java.net.InetSocketAddress;  
  18.   
  19. import com.kingston.net.codec.PacketDecoder;  
  20. import com.kingston.net.codec.PacketEncoder;  
  21.   
  22. public class NettyChatServer {  
  23.   
  24.     public void bind(int port) throws IOException{  
  25.         EventLoopGroup bossGroup = new NioEventLoopGroup();  
  26.         EventLoopGroup workerGroup = new NioEventLoopGroup();  
  27.         System.err.println("服务端已启动,正在监听用户的请求......");  
  28.         try{  
  29.             ServerBootstrap b = new ServerBootstrap();  
  30.             b.group(bossGroup,workerGroup)  
  31.             .channel(NioServerSocketChannel.class)  
  32.             .option(ChannelOption.SO_BACKLOG, 1024)  
  33.             .childHandler(new ChildChannelHandler());  
  34.               
  35.             ChannelFuture f = b.bind(new InetSocketAddress(port))  
  36.                     .sync();  
  37.             f.channel().closeFuture().sync();  
  38.         }catch(Exception e){  
  39.             e.printStackTrace();  
  40.         }finally{  
  41.             bossGroup.shutdownGracefully();  
  42.             workerGroup.shutdownGracefully();  
  43.         }  
  44.     }  
  45.       
  46.     private class ChildChannelHandler extends ChannelInitializer<SocketChannel>{  
  47.   
  48.         @Override  
  49.         protected void initChannel(SocketChannel arg0) throws Exception {  
  50.             ChannelPipeline pipeline = arg0.pipeline();  
  51.             pipeline.addLast(new PacketDecoder(1024*10,2,0,2));  
  52.             pipeline.addLast(new LengthFieldPrepender(2));  
  53.             pipeline.addLast(new PacketEncoder());  
  54.             pipeline.addLast("idleStateHandler"new IdleStateHandler(1000));   
  55.             pipeline.addLast(new ChatServerHandler());  
  56.         }  
  57.     }  
  58.       
  59. }  




2.私有协议栈的设计。私有协议栈主要用于跨进程的数据通信,只能用于企业内部,协议设计比较灵巧方便。


在这里,消息定义将消息头和消息体融为一体。将消息的第一个short数据视为消息的类型,服务端将根据消息类型处理不同的业务逻辑。定义Packet抽象类,抽象方法

 readFromBuff(ByteBuf buf) 和  writePacketMsg(ByteBuf buf) 作为读写数据的抽象行为,而具体的读写方式由相应的子类去实现。代码如下:


[java] view plain copy



  1. package com.kingston.net;  
  2. import io.netty.buffer.ByteBuf;  
  3.   
  4. import java.io.UnsupportedEncodingException;  
  5. public abstract  class Packet {  
  6.   
  7. //  protected String userId;  
  8.       
  9.     public void writeToBuff(ByteBuf buf){  
  10.         buf.writeShort(getPacketType().getType());  
  11.         writePacketMsg(buf);  
  12.     }  
  13.       
  14.     abstract public void  writePacketMsg(ByteBuf buf);  
  15.       
  16.     abstract public void  readFromBuff(ByteBuf buf);  
  17.       
  18.     abstract public PacketType  getPacketType();  
  19.       
  20.     abstract public void execPacket();  
  21.       
  22.     protected  String readUTF8(ByteBuf buf){  
  23.         int strSize = buf.readInt();  
  24.         byte[] content = new byte[strSize];  
  25.         buf.readBytes(content);  
  26.         try {  
  27.             return new String(content,"UTF-8");  
  28.         } catch (UnsupportedEncodingException e) {  
  29.             e.printStackTrace();  
  30.             return "";  
  31.         }  
  32.           
  33.     }  
  34.       
  35.     protected  void writeUTF8(ByteBuf buf,String msg){  
  36.         byte[] content ;  
  37.         try {  
  38.             content = msg.getBytes("UTF-8");  
  39.             buf.writeInt(content.length);  
  40.             buf.writeBytes(content);  
  41.         } catch (UnsupportedEncodingException e) {  
  42.             e.printStackTrace();  
  43.         }  
  44.     }  
  45.       
  46. }  

在这里需要注意的是,由于Netty通信本质上传送的是byte数据,无法直接传送String字段串,需要先经过简单的编解码成字节数组才能传送。


3.POJO对象的编码与解码

数据发送方发送载体为ByteBuf,因此在发包时,需要将POJO对象进行编码。本项目使用Netty自带的编码器MessageToByteEncoder,实现自定义的编码方式。代码如下:


[java] view plain copy



  1. package com.kingston.net;  
  2.   
  3. import io.netty.buffer.ByteBuf;  
  4. import io.netty.channel.ChannelHandlerContext;  
  5. import io.netty.handler.codec.MessageToByteEncoder;  
  6.   
  7. public class PacketEncoder extends MessageToByteEncoder<Packet> {  
  8.   
  9.     @Override  
  10.     protected void encode(ChannelHandlerContext ctx, Packet msg, ByteBuf out)  
  11.             throws Exception {  
  12.         msg.writeToBuff(out);  
  13.     }  
  14.   
  15. }  

接收方实际接收ByteBuf数据,需要将其解码成对应的POJO对象,才能处理对应的逻辑。本项目使用Netty自带的解码器ByteToMessageDecoder(LengthFieldBasedFrameDecoder继承自ByteToMessageDecoder,其作用见下文),实现自定义的解码方式。代码如下:


[java] view plain copy



  1. package com.kingston.net.codec;  
  2.   
  3. import io.netty.buffer.ByteBuf;  
  4. import io.netty.channel.ChannelHandlerContext;  
  5. import io.netty.handler.codec.LengthFieldBasedFrameDecoder;  
  6.   
  7. import com.kingston.net.Packet;  
  8. import com.kingston.net.PacketManager;  
  9.   
  10. public class PacketDecoder extends LengthFieldBasedFrameDecoder{  
  11.   
  12.     public PacketDecoder(int maxFrameLength,  
  13.             int lengthFieldOffset, int lengthFieldLength,  
  14.             int lengthAdjustment, int initialBytesToStrip  
  15.             ) {  
  16.         super(maxFrameLength, lengthFieldOffset, lengthFieldLength,  
  17.                 lengthAdjustment, initialBytesToStrip);  
  18.     }  
  19.   
  20.     @Override  
  21.     public Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {  
  22.         ByteBuf frame = (ByteBuf)(super.decode(ctx, in));  
  23.         if(frame.readableBytes() <= 0return null ;  
  24.         short packetType = frame.readShort();  
  25.         Packet packet = PacketManager.createNewPacket(packetType);  
  26.         packet.readFromBuff(frame);  
  27.   
  28.         return packet;  
  29.     }  
  30.   
  31. }  



通信协议将包头的第一个short数据视为包类型,根据包类型反射拿到对应的包class定义,调用抽象读取方法完成消息体的读取。


4.消息协议的解析与执行

消息使用第一个short数据作为消息的类型。为了区分每一个消息协议包,需要有一个数据结构缓存各种协议的类型与对应的消息包定义。为此,使用枚举类定义所有的协议包。代码如下:

[java] view plain copy



  1. package com.kingston.net;  
  2.   
  3. import java.util.HashMap;  
  4. import java.util.HashSet;  
  5. import java.util.Map;  
  6. import java.util.Set;  
  7.   
  8. import com.kingston.service.login.ClientLogin;  
  9. import com.kingston.service.login.ServerHearBeat;  
  10. import com.kingston.service.login.ServerLogin;  
  11.   
  12. public enum PacketType {  
  13.     //业务上行数据包  
  14.     ServerLogin((short)0x0001,ServerLogin.class),  
  15.     ServerHearBeat((short)0x0002,ServerHearBeat.class),  
  16.   
  17.     //业务下行数据包  
  18.     ClientLogin((short)0x2000,ClientLogin.class),  
  19.     ;  
  20.   
  21.     private short type;  
  22.     private Class<? extends Packet> packetClass;  
  23.     private static Map<Short,Class<? extends Packet>> PACKET_CLASS_MAP = new HashMap<Short,Class<? extends Packet>>();  
  24.   
  25.     static{  
  26.         //使用Map数据结构,缓存包类型与对应的实体类的映射关系  
  27.         Set<Short> typeSet = new HashSet<Short>();  
  28.         for(PacketType p:PacketType.values()){  
  29.             Short type = p.getType();  
  30.             if(typeSet.contains(type)){  
  31.                 throw new IllegalStateException("packet type 协议类型重复"+type);  
  32.             }  
  33.             PACKET_CLASS_MAP.put(type,p.getPacketClass());  
  34.             typeSet.add(type);  
  35.         }  
  36.     }  
  37.     PacketType(short type,Class<? extends Packet> packetClass){  
  38.         this.setType(type);  
  39.         this.packetClass = packetClass;  
  40.     }  
  41.   
  42.     public short getType() {  
  43.         return type;  
  44.     }  
  45.   
  46.     public void setType(short type) {  
  47.         this.type = type;  
  48.     }  
  49.   
  50.     public Class<? extends Packet> getPacketClass() {  
  51.         return packetClass;  
  52.     }  
  53.   
  54.     public void setPacketClass(Class<? extends Packet> packetClass) {  
  55.         this.packetClass = packetClass;  
  56.     }  
  57.   
  58.   
  59.   
  60.     public static  Class<? extends Packet> getPacketClassBy(short packetType){  
  61.         return PACKET_CLASS_MAP.get(packetType);  
  62.     }  
  63.   
  64. //  public static void main(String[] args) {  
  65. //      for(PacketType p:PacketType.values()){  
  66. //          System.err.println(p.getPacketClass().getSimpleName());  
  67. //      }  
  68. //  }  
  69.   
  70. }  

  PacketType枚举类中有一段静态代码块,在初始化时缓存所有包类型与对应的实体类的映射关系。这样,就可以根据包类型,直接拿到对应的Packet子类。


经过解码反射得到完整的消息包定义后,就可以通过反射机制,调用相应的业务方法。该步骤由包执行器完成,代码如下:


[java] view plain copy



  1. package com.kingston.net;  
  2.   
  3. import java.lang.reflect.InvocationTargetException;  
  4. import java.lang.reflect.Method;  
  5.   
  6. public class PacketExecutor {  
  7.   
  8.     public static void execPacket(Packet pact){  
  9.         if(pact == nullreturn;  
  10.           
  11.         try {  
  12.             Method m = pact.getClass().getMethod("execPacket");  
  13.             m.invoke(pact, null);  
  14.         } catch (NoSuchMethodException | SecurityException e) {  
  15.             e.printStackTrace();  
  16.         } catch (IllegalAccessException e) {  
  17.             e.printStackTrace();  
  18.         } catch (IllegalArgumentException e) {  
  19.             e.printStackTrace();  
  20.         } catch (InvocationTargetException e) {  
  21.             e.printStackTrace();  
  22.         }  
  23.     }  
  24.       
  25. }  

包执行器其实是根据反射,调用对应子类消息包的业务处理方法。

到这里,读者应该可以感受抽象包Packet的定义是该通信机制的精华部分。正是有了abstract public void  readFromBuff(ByteBuf buf);abstract public void  writePacketMsg(ByteBuf buf);abstract public void execPacket()三个抽象方法,才能将各种消息包的读写、业务逻辑相互隔离。


写到这里,我不禁回想起大学期间做过的一个聊天室课程设计。当初,我采用Java作为服务器,flash作为客户端,基于socket进行通信。通信消息体只有一个长字符串,通信双方根据不同消息类型将字符串作多次分隔。如果当初协议类型再多几个的话,估计想死的心都有了。

5.半包读写解决之道

MessageToByteEncoder 和 ByteToMessageDecoder两个类只是解决POJO的编解码,并没有处理粘包,拆包的异常情况。在本例中,使用LengthFieldBasedFrameDecoder和LengthFieldPrepender两个工具类,就可以轻松解决半包读写异常。

6.服务端与客户端数据通信方式

客户端tcp链路建立后,服务端必须缓存对应的ChannelHandlerContext对象。这样,服务端就可以向所有连接的用户发送数据了。发送数据基础服务类代码如下:


[java] view plain copy



  1. package com.kingston.base;  
  2.   
  3. import io.netty.channel.ChannelHandlerContext;  
  4.   
  5. import java.util.Map;  
  6. import java.util.concurrent.ConcurrentHashMap;  
  7.   
  8. import com.kingston.net.Packet;  
  9. import com.kingston.util.StringUtil;  
  10.   
  11. public class ServerManager {  
  12.   
  13.     //缓存所有登录用户对应的通信上下文环境(主要用于业务数据处理)  
  14.     private static Map<Integer,ChannelHandlerContext> USER_CHANNEL_MAP  = new ConcurrentHashMap<>();  
  15.     //缓存通信上下文环境对应的登录用户(主要用于服务)  
  16.     private static Map<ChannelHandlerContext,Integer> CHANNEL_USER_MAP  = new ConcurrentHashMap<>();  
  17.       
  18.     public static void sendPacketTo(Packet pact,String userId){  
  19.         if(pact == null || StringUtil.isEmpty(userId)) return;  
  20.           
  21.         Map<Integer,ChannelHandlerContext> contextMap  = USER_CHANNEL_MAP;  
  22.         if(StringUtil.isEmpty(contextMap)) return;  
  23.           
  24.         ChannelHandlerContext targetContext = contextMap.get(userId);  
  25.         if(targetContext == nullreturn;  
  26.           
  27.         targetContext.writeAndFlush(pact);  
  28.     }  
  29.       
  30.     /** 
  31.      *  向所有在线用户发送数据包 
  32.      */  
  33.     public static void sendPacketToAllUsers(Packet pact){  
  34.         if(pact == null ) return;  
  35.         Map<Integer,ChannelHandlerContext> contextMap  = USER_CHANNEL_MAP;  
  36.         if(StringUtil.isEmpty(contextMap)) return;  
  37.           
  38.         contextMap.values().forEach( (ctx) -> ctx.writeAndFlush(pact));  
  39.           
  40.     }  
  41.       
  42.     /** 
  43.      *  向单一在线用户发送数据包 
  44.      */  
  45.     public static void sendPacketTo(Packet pact,ChannelHandlerContext targetContext ){  
  46.         if(pact == null || targetContext == nullreturn;  
  47.         targetContext.writeAndFlush(pact);  
  48.     }  
  49.       
  50.     public static ChannelHandlerContext getOnlineContextBy(String userId){  
  51.         return USER_CHANNEL_MAP.get(userId);  
  52.     }  
  53.       
  54.     public static void addOnlineContext(Integer userId,ChannelHandlerContext context){  
  55.         if(context == null){  
  56.             throw new NullPointerException();  
  57.         }  
  58.         USER_CHANNEL_MAP.put(userId,context);  
  59.         CHANNEL_USER_MAP.put(context, userId);  
  60.     }  
  61.       
  62.     /** 
  63.      *  注销用户通信渠道 
  64.      */  
  65.     public static void ungisterUserContext(ChannelHandlerContext context ){  
  66.         if(context  != null){  
  67.             int userId = CHANNEL_USER_MAP.getOrDefault(context,0);  
  68.             CHANNEL_USER_MAP.remove(context);  
  69.             USER_CHANNEL_MAP.remove(userId);  
  70.             context.close();  
  71.         }  
  72.     }  
  73.       
  74. }  



7.服务端验证用户登录的简单demo


demo流程为客户端发送一个以Server开头命名的上行包到服务端,服务端接受数据后,直接发送一个以Client开头命名的响应包到客户端。

上行包ServerLogin代码如下:


[java] view plain copy



  1. package com.kingston.service.login;  
  2.   
  3. import io.netty.channel.ChannelHandlerContext;  
  4.   
  5. import com.kingston.base.ServerManager;  
  6.   
  7. public class LoginManagerImpl implements LoginManager{  
  8.   
  9. //  @Autowired  
  10. //  private UserDao userDao;  
  11.       
  12.     @Override  
  13.     public void validateLogin(ChannelHandlerContext context,Integer userId, String password) {  
  14.         boolean isValid = validate(userId, password);  
  15.         ClientLogin resp = new ClientLogin();  
  16.         resp.setAlertMsg("成功登录");  
  17.         if(isValid){  
  18.             resp.setIsValid((byte)1);  
  19.             ServerManager.addOnlineContext(userId, context);  
  20.         }  
  21.         ServerManager.sendPacketTo(resp, context);  
  22.     }  
  23.       
  24.     /** 
  25.      *  验证帐号密码是否一致 
  26.      */  
  27.     private boolean validate(Integer userId, String password){  
  28. //      userDao = (UserDao) ServerDataPool.SPRING_BEAN_FACTORY .getBean(User.class);  
  29. //      User user = userDao.findById(userId);  
  30. //      if(user == null) return false;  
  31. //        
  32. //      return user.getPassword().equals(password);  
  33.           
  34.         return true;  
  35.     }  
  36.   
  37. }  

下行包ClientLogin代码如下:



[java] view plain copy



  1. package com.kingston.service.login;  
  2.   
  3. import io.netty.buffer.ByteBuf;  
  4.   
  5. import com.kingston.net.Packet;  
  6. import com.kingston.net.PacketType;  
  7.   
  8. public class ClientLogin extends Packet{  
  9.   
  10.     private String alertMsg;  
  11.     private byte isValid;  
  12.       
  13.     @Override  
  14.     public void writePacketMsg(ByteBuf buf) {  
  15.         writeUTF8(buf, alertMsg);  
  16.         buf.writeByte(isValid);  
  17.     }  
  18.   
  19.     @Override  
  20.     public void readFromBuff(ByteBuf buf) {  
  21.         this.alertMsg = readUTF8(buf);  
  22.         this.isValid = buf.readByte();  
  23.     }  
  24.   
  25.     @Override  
  26.     public PacketType getPacketType() {  
  27.         return PacketType.ClientLogin;  
  28.     }  
  29.   
  30.     @Override  
  31.     public void execPacket() {  
  32.         System.err.println("收到服务端的验证消息,"+alertMsg);  
  33.     }  
  34.   
  35.     public String getAlertMsg() {  
  36.         return alertMsg;  
  37.     }  
  38.   
  39.     public void setAlertMsg(String alertMsg) {  
  40.         this.alertMsg = alertMsg;  
  41.     }  
  42.   
  43.     public byte getIsValid() {  
  44.         return isValid;  
  45.     }  
  46.   
  47.     public void setIsValid(byte isValid) {  
  48.         this.isValid = isValid;  
  49.     }  
  50.   
  51. }  

处理登录逻辑的管理类代码如下:



[java] view plain copy



  1. package com.kingston.service.login;  
  2.   
  3. import io.netty.channel.ChannelHandlerContext;  
  4.   
  5. import com.kingston.base.ServerManager;  
  6.   
  7. public class LoginManagerImpl implements LoginManager{  
  8.   
  9. //  @Autowired  
  10. //  private UserDao userDao;  
  11.       
  12.     @Override  
  13.     public void validateLogin(ChannelHandlerContext context,Integer userId, String password) {  
  14.         boolean isValid = validate(userId, password);  
  15.         ClientLogin resp = new ClientLogin();  
  16.         resp.setAlertMsg("成功登录");  
  17.         if(isValid){  
  18.             resp.setIsValid((byte)1);  
  19.             ServerManager.addOnlineContext(userId, context);  
  20.         }  
  21.         ServerManager.sendPacketTo(resp, context);  
  22.     }  
  23.       
  24.     /** 
  25.      *  验证帐号密码是否一致 
  26.      */  
  27.     private boolean validate(Integer userId, String password){  
  28. //      userDao = (UserDao) ServerDataPool.SPRING_BEAN_FACTORY .getBean(User.class);  
  29. //      User user = userDao.findById(userId);  
  30. //      if(user == null) return false;  
  31. //        
  32. //      return user.getPassword().equals(password);  
  33.           
  34.         return true;  
  35.     }  
  36.   
  37. }  

至此,服务端主要通信逻辑基本完成。


客户端程序代码:

客户端私有协议跟编解码方式跟服务端完全一致。客户端主要关注数据界面的展示。下面只给出启动应用程序的代码,以及测试通信的示例代码。
1.启动Reactor线程组建立与服务端的的连接,以及处理IO网络读写。

程序启动方式跟服务端类似,具体代码如下:


[java] view plain copy



  1. package com.kingston.netty;  
  2.   
  3. import com.kingston.net.PacketDecoder;  
  4. import com.kingston.net.PacketEncoder;  
  5.   
  6. import io.netty.bootstrap.Bootstrap;  
  7. import io.netty.channel.ChannelFuture;  
  8. import io.netty.channel.ChannelInitializer;  
  9. import io.netty.channel.EventLoopGroup;  
  10. import io.netty.channel.nio.NioEventLoopGroup;  
  11. import io.netty.channel.socket.SocketChannel;  
  12. import io.netty.channel.socket.nio.NioSocketChannel;  
  13. import io.netty.handler.codec.LengthFieldBasedFrameDecoder;  
  14. import io.netty.handler.codec.LengthFieldPrepender;  
  15.   
  16.   
  17. public class NettyChatClient {  
  18.   
  19.     public void connect(int port,String host) throws Exception{  
  20.         EventLoopGroup group = new NioEventLoopGroup();  
  21.         try{  
  22.             Bootstrap b  = new Bootstrap();  
  23.             b.group(group).channel(NioSocketChannel.class)  
  24.             .handler(new ChannelInitializer<SocketChannel>(){  
  25.   
  26.                 @Override  
  27.                 protected void initChannel(SocketChannel arg0)  
  28.                         throws Exception {  
  29.               
  30.                     arg0.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024*10,2,0,2));  
  31.                     arg0.pipeline().addLast(new PacketDecoder());  
  32.                     arg0.pipeline().addLast(new LengthFieldPrepender(2));  
  33.                     arg0.pipeline().addLast(new PacketEncoder());  
  34.                     arg0.pipeline().addLast(new NettyClientHandler());  
  35.                 }  
  36.                   
  37.             });  
  38.               
  39.             ChannelFuture f = b.connect(host,port).sync();  
  40.             f.channel().closeFuture().sync();  
  41.         }catch(Exception e){  
  42.             e.printStackTrace();  
  43.         }finally{  
  44.             group.shutdownGracefully();  
  45.         }  
  46.     }  
  47.       
  48.       
  49. }  

处理业务逻辑的ChannelHandler代码如下:



[java] view plain copy



  1. package com.kingston.netty;  
  2.   
  3. import io.netty.channel.Channel;  
  4. import io.netty.channel.ChannelHandlerAdapter;  
  5. import io.netty.channel.ChannelHandlerContext;  
  6. import io.netty.channel.ChannelPromise;  
  7.   
  8. import com.kingston.net.Packet;  
  9. import com.kingston.net.PacketExecutor;  
  10. import com.kingston.service.login.ServerLogin;  
  11.   
  12. public class NettyClientHandler extends ChannelHandlerAdapter{  
  13.   
  14.   
  15.     public NettyClientHandler(){  
  16.   
  17.     }  
  18.   
  19.     @Override  
  20.     public void channelActive(ChannelHandlerContext ctx){  
  21.         ServerLogin loginPact = new ServerLogin();  
  22.         loginPact.setUserName("Netty爱好者");  
  23.         loginPact.setUserPwd("world");  
  24.         ctx.writeAndFlush(loginPact);  
  25.         System.err.println("向服务端发送登录请求");  
  26. //      StartApp.channelContext = ctx;  
  27.     }  
  28.   
  29.     @Override  
  30.     public void channelRead(ChannelHandlerContext ctx, Object msg)  
  31.             throws Exception{  
  32.         Packet  packet = (Packet)msg;  
  33.   
  34.         PacketExecutor.execPacket(packet);  
  35.     }  
  36.   
  37.     @Override  
  38.     public void close(ChannelHandlerContext ctx,ChannelPromise promise){  
  39.         System.err.println("TCP closed...");  
  40.         ctx.close(promise);  
  41.     }  
  42.   
  43.     @Override  
  44.     public void channelInactive(ChannelHandlerContext ctx) throws Exception {  
  45.         System.err.println("客户端关闭1");  
  46.     }  
  47.   
  48.     @Override  
  49.     public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {  
  50.         ctx.disconnect(promise);  
  51.         System.err.println("客户端关闭2");  
  52.     }  
  53.   
  54.     @Override  
  55.     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
  56.         System.err.println("客户端关闭3");  
  57.         //          ctx.fireExceptionCaught(cause);  
  58.         Channel channel = ctx.channel();  
  59.         cause.printStackTrace();  
  60.         if(channel.isActive()){  
  61.             System.err.println("simpleclient"+channel.remoteAddress()+"异常");  
  62.             //              ctx.close();  
  63.         }  
  64.     }  
  65. }  


先启动服务器,再启动客户端,即可看到客户端的打印输出




至此,聊天室的登录流程基本完成。限于篇幅,此demo例子并没有出现spring,mybatic,javafx相关代码,但是私有协议通信方式代码已全部给出。有了一个用户登录的例子,相信构建其他得业务逻辑也不会太困难。

最后,说下写代码的历程。这个demo是我春节宅家期间,利用零碎时间做的,平均一天一个小时。很多开发人员应该有这样的经历,看书的时候往往觉得都能理解,但实际上自己动手就会遇到各种卡思路。在做这个demo时,我更多时间是花在查资料上。

我也会继续往这个项目添加功能,让它看起来越来越“炫”。(^-^)


全部代码已在github上托管(代码经过多次重构,与博客上的代码略有不同)

(服务端git地址,go->)

(客户端git地址,go->)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值