主要参考
https://blog.csdn.net/coder_py/article/details/73441043
有小改动(因使用的是netty4的包 netty-all-4.1.25.Final.jar)
通讯信息的基类,需要实现序列化,定义了信息的类型和客户端ID,方便进行管理
消息基类:
public abstract class BaseMsg implements Serializable{
private static final long serialVersionUID = 1L;
private MsgType msgType;
private String clientID;
public BaseMsg() {
this.clientID = Constants.getClientID();
}
public MsgType getMsgType() {
return msgType;
}
public void setMsgType(MsgType msgType) {
this.msgType = msgType;
}
public String getClientID() {
return clientID;
}
public void setClientID(String clientID) {
this.clientID = clientID;
}
}
心跳请求消息类:
public class AskMsg extends BaseMsg{
AskParams params;
public AskMsg() {
super();
setMsgType(MsgType.ASK);
}
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;
}
}
常量类,存放客户端ID:
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{
String username;
String password;
public LoginMsg() {
super();
setMsgType(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;
}
}
枚举类,主要包含消息协议类型的定义:
public enum MsgType {
PING,ASK,REPLY,LOGIN;
}
服务端Channel管理类,封装在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());
}
}
}
}
心跳包传输数据类型:
public class PingMsg extends BaseMsg{
public PingMsg() {
super();
setMsgType(MsgType.PING);
}
}
消息回应类:
public class ReplyMsg extends BaseMsg {
public ReplyMsg() {
super();
setMsgType(MsgType.REPLY);
}
private ReplyBody body;
public ReplyBody getBody() {
return body;
}
public void setBody(ReplyBody body) {
this.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;
}
}
客户端的处理类,主要是对于消息类型的判断处理和心跳处理机制的实现
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 channelRead0(ChannelHandlerContext channelHandlerContext, BaseMsg baseMsg)
throws Exception {
MsgType msgType=baseMsg.getMsgType();
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:{
System.out.println("receive push from server----------");
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 server msg: "+replyServerBody.getServerInfo());
}
default:break;
}
ReferenceCountUtil.release(msgType);
}
}
服务端处理类,消息类型处理以及对于断开连接的客户端的移除:
public class NettyServerHandler extends SimpleChannelInboundHandler<BaseMsg>{
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//channel失效,从Map中移除
NettyChannelMap.remove((SocketChannel)ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, BaseMsg baseMsg)
throws Exception {
if(MsgType.LOGIN.equals(baseMsg.getMsgType())){
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.getMsgType()){
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);
}
}
客户端启动类
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(10,5,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(9999,"localhost");
//登录
LoginMsg loginMsg=new LoginMsg();
loginMsg.setPassword("yao");
loginMsg.setUsername("robin");
bootstrap.socketChannel.writeAndFlush(loginMsg);
while (true){
//发送心跳包
TimeUnit.SECONDS.sleep(3);
PingMsg pingMsg = new PingMsg();
bootstrap.socketChannel.writeAndFlush(pingMsg);
}
}
}
服务端启动类:
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(9999);
PushThread pushThread = new PushThread();
pushThread.start();
}
}
推送消息线程类:
public class PushThread extends Thread {
@Override
public void run() {
while(true){
try {
SocketChannel channel=(SocketChannel)NettyChannelMap.get("001");
if(channel!=null){
AskMsg askMsg = new AskMsg();
channel.writeAndFlush(askMsg);
}
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
分析:
1.这里客户端向发送登录请求LoginMsg给服务端,服务端验证通过后,将当前客户端的channel通道放入通道管理map中。
2.然后客户端每隔3秒钟向服务端发送心跳包PingMsg,服务端收到客户端发送的心跳包后也给客户端回应心跳包PingMsg。
3.同时服务端启动的时候会启动一个后台线程负责定时给客户端推送消息,这个线程会从管理通道map中获取通道号为001的通道号,没3秒推送一次数据,这样001对应的客户端就收到服务端推送过来的数据。