基本思路:netty服務端經過一個Map保存全部鏈接上來的客戶端SocketChannel,客戶端的Id做爲Map的key。每次服務器端若是要向某個客戶端發送消息,只需根據ClientId取出對應的SocketChannel,往裏面寫入message便可。心跳檢測經過IdleEvent 事件,定時向服務端放送Ping消息,檢測SocketChannel是否終斷。 環境JDK1.8 和netty5 如下是具體的代碼實現和介紹: 1公共的Share部分(主要包含消息協議類型的定義) 設計消息類型: public enum MsgType { PING,ASK,REPLY,LOGIN } Message基類: //必須實現序列,serialVersionUID 必定要有,否者在netty消息序列化反序列化會有問題,接收不到消息!!! public abstract class BaseMsg implements Serializable { private static final long serialVersionUID = 1L; private MsgType type; //必須惟一,否者會出現channel調用混亂 private String clientId; //初始化客戶端id public BaseMsg() { this.clientId = Constants.getClientId(); } public String getClientId() { return clientId; } public void setClientId(String clientId) { this.clientId = clientId; } public MsgType getType() { return type; } public void setType(MsgType type) { this.type = type; } } 常量設置: public class Constants { private static String clientId; public static String getClientId() { return clientId; } public static void setClientId(String clientId) { Constants.clientId = clientId; } } 登陸類型消息: public class LoginMsg extends BaseMsg { private String userName; private String password; public LoginMsg() { super(); setType(MsgType.LOGIN); } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } 心跳檢測Ping類型消息: public class PingMsg extends BaseMsg { public PingMsg() { super(); setType(MsgType.PING); } } 請求類型消息: public class AskMsg extends BaseMsg { public AskMsg() { super(); setType(MsgType.ASK); } private AskParams params; public AskParams getParams() { return params; } public void setParams(AskParams params) { this.params = params; } } //請求類型參數 //必須實現序列化接口 public class AskParams implements Serializable { private static final long serialVersionUID = 1L; private String auth; public String getAuth() { return auth; } public void setAuth(String auth) { this.auth = auth; } } 響應類型消息: public class ReplyMsg extends BaseMsg { public ReplyMsg() { super(); setType(MsgType.REPLY); } private ReplyBody body; public ReplyBody getBody() { return body; } public void setBody(ReplyBody body) { this.body = body; } } //相應類型body對像 public class ReplyBody implements Serializable { private static final long serialVersionUID = 1L; } public class ReplyClientBody extends ReplyBody { private String clientInfo; public ReplyClientBody(String clientInfo) { this.clientInfo = clientInfo; } public String getClientInfo() { return clientInfo; } public void setClientInfo(String clientInfo) { this.clientInfo = clientInfo; } } public class ReplyServerBody extends ReplyBody { private String serverInfo; public ReplyServerBody(String serverInfo) { this.serverInfo = serverInfo; } public String getServerInfo() { return serverInfo; } public void setServerInfo(String serverInfo) { this.serverInfo = serverInfo; } } 2 Server端:主要包含對SocketChannel引用的Map,ChannelHandler的實現和Bootstrap. Map: public class NettyChannelMap { private static Map<String,SocketChannel> map=new ConcurrentHashMap<String, SocketChannel>(); public static void add(String clientId,SocketChannel socketChannel){ map.put(clientId,socketChannel); } public static Channel get(String clientId){ return map.get(clientId); } public static void remove(SocketChannel socketChannel){ for (Map.Entry entry:map.entrySet()){ if (entry.getValue()==socketChannel){ map.remove(entry.getKey()); } } } } Handler public class NettyServerHandler extends SimpleChannelInboundHandler<BaseMsg> { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { //channel失效,從Map中移除 NettyChannelMap.remove((SocketChannel)ctx.channel()); } @Override protected void messageReceived(ChannelHandlerContext channelHandlerContext, BaseMsg baseMsg) throws Exception { if(MsgType.LOGIN.equals(baseMsg.getType())){ LoginMsg loginMsg=(LoginMsg)baseMsg; if("robin".equals(loginMsg.getUserName())&&"yao".equals(loginMsg.getPassword())){ //登陸成功,把channel存到服務端的map中 NettyChannelMap.add(loginMsg.getClientId(),(SocketChannel)channelHandlerContext.channel()); System.out.println("client"+loginMsg.getClientId()+" 登陸成功"); } }else{ if(NettyChannelMap.get(baseMsg.getClientId())==null){ //說明未登陸,或者鏈接斷了,服務器向客戶端發起登陸請求,讓客戶端從新登陸 LoginMsg loginMsg=new LoginMsg(); channelHandlerContext.channel().writeAndFlush(loginMsg); } } switch (baseMsg.getType()){ case PING:{ PingMsg pingMsg=(PingMsg)baseMsg; PingMsg replyPing=new PingMsg(); NettyChannelMap.get(pingMsg.getClientId()).writeAndFlush(replyPing); }break; case ASK:{ //收到客戶端的請求 AskMsg askMsg=(AskMsg)baseMsg; if("authToken".equals(askMsg.getParams().getAuth())){ ReplyServerBody replyBody=new ReplyServerBody("server info $$$$ !!!"); ReplyMsg replyMsg=new ReplyMsg(); replyMsg.setBody(replyBody); NettyChannelMap.get(askMsg.getClientId()).writeAndFlush(replyMsg); } }break; case REPLY:{ //收到客戶端 ReplyMsg replyMsg=(ReplyMsg)baseMsg; ReplyClientBody clientBody=(ReplyClientBody)replyMsg.getBody(); System.out.println("receive client msg: "+clientBody.getClientInfo()); }break; default:break; } ReferenceCountUtil.release(baseMsg); } } ServerBootstrap: public class NettyServerBootstrap { private int port; private SocketChannel socketChannel; public NettyServerBootstrap(int port) throws InterruptedException { this.port = port; bind(); } private void bind() throws InterruptedException { EventLoopGroup boss=new NioEventLoopGroup(); EventLoopGroup worker=new NioEventLoopGroup(); ServerBootstrap bootstrap=new ServerBootstrap(); bootstrap.group(boss,worker); bootstrap.channel(NioServerSocketChannel.class); bootstrap.option(ChannelOption.SO_BACKLOG, 128); //經過NoDelay禁用Nagle,使消息當即發出去,不用等待到必定的數據量才發出去 bootstrap.option(ChannelOption.TCP_NODELAY, true); //保持長鏈接狀態 bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); bootstrap.childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline p = socketChannel.pipeline(); p.addLast(new ObjectEncoder()); p.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null))); p.addLast(new NettyServerHandler()); } }); ChannelFuture f= bootstrap.bind(port).sync(); if(f.isSuccess()){ System.out.println("server start"); } } public static void main(String []args) throws InterruptedException { NettyServerBootstrap bootstrap=new NettyServerBootstrap(9); while (true){ SocketChannel channel=(SocketChannel)NettyChannelMap.get("001"); if(channel!=null){ AskMsg askMsg=new AskMsg(); channel.writeAndFlush(askMsg); } TimeUnit.SECONDS.sleep(5); } } } 3 Client端:包含發起登陸,發送心跳,及對應消息處理 handler public class NettyClientHandler extends SimpleChannelInboundHandler<BaseMsg> { //利用寫空閒發送心跳檢測消息 @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof IdleStateEvent) { IdleStateEvent e = (IdleStateEvent) evt; switch (e.state()) { case WRITER_IDLE: PingMsg pingMsg=new PingMsg(); ctx.writeAndFlush(pingMsg); System.out.println("send ping to server-"); break; default: break; } } } @Override protected void messageReceived(ChannelHandlerContext channelHandlerContext, BaseMsg baseMsg) throws Exception { MsgType msgType=baseMsg.getType(); switch (msgType){ case LOGIN:{ //向服務器發起登陸 LoginMsg loginMsg=new LoginMsg(); loginMsg.setPassword("yao"); loginMsg.setUserName("robin"); channelHandlerContext.writeAndFlush(loginMsg); }break; case PING:{ System.out.println("receive ping from server-"); }break; case ASK:{ ReplyClientBody replyClientBody=new ReplyClientBody("client info **** !!!"); ReplyMsg replyMsg=new ReplyMsg(); replyMsg.setBody(replyClientBody); channelHandlerContext.writeAndFlush(replyMsg); }break; case REPLY:{ ReplyMsg replyMsg=(ReplyMsg)baseMsg; ReplyServerBody replyServerBody=(ReplyServerBody)replyMsg.getBody(); System.out.println("receive client msg: "+replyServerBody.getServerInfo()); } default:break; } ReferenceCountUtil.release(msgType); } } bootstrap public class NettyClientBootstrap { private int port; private String host; private SocketChannel socketChannel; private static final EventExecutorGroup group = new DefaultEventExecutorGroup(20); public NettyClientBootstrap(int port, String host) throws InterruptedException { this.port = port; this.host = host; start(); } private void start() throws InterruptedException { EventLoopGroup eventLoopGroup=new NioEventLoopGroup(); Bootstrap bootstrap=new Bootstrap(); bootstrap.channel(NioSocketChannel.class); bootstrap.option(ChannelOption.SO_KEEPALIVE,true); bootstrap.group(eventLoopGroup); bootstrap.remoteAddress(host,port); bootstrap.handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new IdleStateHandler(20,10,0)); socketChannel.pipeline().addLast(new ObjectEncoder()); socketChannel.pipeline().addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null))); socketChannel.pipeline().addLast(new NettyClientHandler()); } }); ChannelFuture future =bootstrap.connect(host,port).sync(); if (future.isSuccess()) { socketChannel = (SocketChannel)future.channel(); System.out.println("connect server 成功"); } } public static void main(String[]args) throws InterruptedException { Constants.setClientId("001"); NettyClientBootstrap bootstrap=new NettyClientBootstrap(9,"localhost"); LoginMsg loginMsg=new LoginMsg(); loginMsg.setPassword("yao"); loginMsg.setUserName("robin"); bootstrap.socketChannel.writeAndFlush(loginMsg); while (true){ TimeUnit.SECONDS.sleep(3); AskMsg askMsg=new AskMsg(); AskParams askParams=new AskParams(); askParams.setAuth("authToken"); askMsg.setParams(askParams); bootstrap.socketChannel.writeAndFlush(askMsg); } } }