netty 搭建服务端客户端 实现长连接 发送心跳

netty 搭建服务端客户端 实现长连接 发送心跳

  • 开始
    • 1.新建项目
    • 2.pom.xml
    • 3.数据模型
    • 4.编码器解码器
    • 5. ChannelInboundHandlerAdapter 实现接受发送消息
    • 6. server
    • 7. Client
    • 8. 测试

开始

netty 是一个高性能的长连接 api 也比较简单,网上的资料也很多,我尝试搭建了几次,都是搭建好以后ping不通,现在我根据我的总结把自己搭建的贴出来。

1.新建项目

netty 搭建服务端客户端 实现长连接 发送心跳_第1张图片
这个是我的项目结构,我把client 跟server 放到了一个项目里面,当然也可以把他们分开放,分开的时候注意,发送消息的Model 他们的传递介质一定要一样,否则另一方收到消息后转换失败就没有办法收到消息了,或者说收到消息后转换字段不会映射成功,然后就会出问题 了。

2.pom.xml

		<!-- 解码and编码器 -->
        <!-- https://mvnrepository.com/artifact/org.msgpack/msgpack -->
        <dependency>
            <groupId>org.msgpack</groupId>
            <artifactId>msgpack</artifactId>
            <version>0.6.12</version>
        </dependency>
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.16.Final</version>
		</dependency>

3.数据模型

所有的数据传递都是通过数据模型进行转换的,所以首先定义一个发送数据的消息


import java.io.Serializable;
import org.msgpack.annotation.Message;
/**
 * 消息类型分离器
 * @author Administrator
 *
 */
@Message
public class Model implements Serializable{
	
	public Model() {}
	public Model(int type) {
		this.type = type;
	}

    private static final long serialVersionUID = 1L;

    //类型
    private int type;

    //内容
    private Object body;

    public Object getBody() {
        return body;
    }

    public void setBody(Object body) {
        this.body = body;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return "Model [type=" + type + ", body=" + body + "]";
    }
}

并且为了方便消息的类型判断,我定义了一个枚举


/**
 * 配置项
 * @author Administrator
 *
 */
public interface TypeData {

	byte PING = 5; 
    
	byte PONG = 6;
	
	byte CONTENT = 1; // 消息
}

4.编码器解码器

下面就是,处理字符串的编码器解码器,用于json传递


import org.msgpack.MessagePack;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

/**
 * 编码器
 * @author Administrator
 *
 */
public class MsgPckEncode extends MessageToByteEncoder<Object>{

    @Override
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf buf)throws Exception {
        // TODO Auto-generated method stub
        MessagePack pack = new MessagePack();

        byte[] write = pack.write(msg);

        buf.writeBytes(write);

    }
}


import java.util.List;
import org.msgpack.MessagePack;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import netty.demo.entity.Model;

/**
 * 解码器
 * @author Administrator
 *
 */
public class MsgPckDecode extends MessageToMessageDecoder<ByteBuf>{

	@Override
	protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
		// TODO Auto-generated method stub
		
		final  byte[] array;

        final int length = msg.readableBytes();

        array = new byte[length];

        msg.getBytes(msg.readerIndex(), array, 0, length);

        MessagePack pack = new MessagePack();

        out.add(pack.read(array, Model.class));
		
	}
}

可以传递对象的编码器解码器

传递Object 类型的body 的时候使用这个解码器

import java.io.ByteArrayInputStream;
import java.util.List;

import com.alibaba.fastjson.util.IOUtils;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.KryoException;
import com.esotericsoftware.kryo.io.Input;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import netty.demo.entity.Model;

/**
 * 自定义Decoder
 * @author Ricky
 *
 */
public class KyroMsgDecoder extends ByteToMessageDecoder {

    public static final int HEAD_LENGTH = 4;

    private Kryo kryo = new Kryo();

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        if (in.readableBytes() < HEAD_LENGTH) {  //这个HEAD_LENGTH是我们用于表示头长度的字节数。  由于Encoder中我们传的是一个int类型的值,所以这里HEAD_LENGTH的值为4.
            return;
        }
        in.markReaderIndex();                  //我们标记一下当前的readIndex的位置
        int dataLength = in.readInt();       // 读取传送过来的消息的长度。ByteBuf 的readInt()方法会让他的readIndex增加4
        if (dataLength < 0) { // 我们读到的消息体长度为0,这是不应该出现的情况,这里出现这情况,关闭连接。
            ctx.close();
        }

        if (in.readableBytes() < dataLength) { //读到的消息体长度如果小于我们传送过来的消息长度,则resetReaderIndex. 这个配合markReaderIndex使用的。把readIndex重置到mark的地方
            in.resetReaderIndex();
            return;
        }

        byte[] body = new byte[dataLength];  //传输正常
        in.readBytes(body);
        Object o = convertToObject(body);  //将byte数据转化为我们需要的对象
        out.add(o);
    }

    private Object convertToObject(byte[] body) {

        Input input = null;
        ByteArrayInputStream bais = null;
        try {
            bais = new ByteArrayInputStream(body);
            input = new Input(bais);

            return kryo.readObject(input, Model.class);
        } catch (KryoException e) {
            e.printStackTrace();
        }finally{
            IOUtils.close(input);
            IOUtils.close(bais);
        }

        return null;
    }

}
import java.io.ByteArrayOutputStream;

import com.alibaba.fastjson.util.IOUtils;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.KryoException;
import com.esotericsoftware.kryo.io.Output;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import netty.demo.entity.Model;

public class KyroMsgEncoder extends MessageToByteEncoder<Model>{
	
	private Kryo kryo = new Kryo();

    @Override
    protected void encode(ChannelHandlerContext ctx, Model msg, ByteBuf out) throws Exception {

        byte[] body = convertToBytes(msg);  //将对象转换为byte
        int dataLength = body.length;  //读取消息的长度
        out.writeInt(dataLength);  //先将消息长度写入,也就是消息头
        out.writeBytes(body);  //消息体中包含我们要发送的数据
    }

    private byte[] convertToBytes(Model car) {

        ByteArrayOutputStream bos = null;
        Output output = null;
        try {
            bos = new ByteArrayOutputStream();
            output = new Output(bos);
            kryo.writeObject(output, car);
            output.flush();

            return bos.toByteArray();
        } catch (KryoException e) {
            e.printStackTrace();
        }finally{
            IOUtils.close(output);
            IOUtils.close(bos);
        }
        return null;
    }
	
}

5. ChannelInboundHandlerAdapter 实现接受发送消息

由于这个项目我写的是客户端服务端放到了一个项目里,所以我定义了一个公共的类去实现了这个 适配器 的接受消息发送消息
Middleware


import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
import netty.demo.entity.Model;
import netty.demo.entity.TypeData;
import netty.demo.service.controller.ConnectHandler;

/**
 *  定义一个枢纽站,client 跟 server  公用的功能
 * @author Administrator
 *
 */
public abstract class Middleware extends ChannelInboundHandlerAdapter{

    protected String name;
    //记录次数
    private int heartbeatCount = 0;

    //获取server and client 传入的值
    public Middleware(String name) {
        this.name = name;
    }
    /**
     *继承ChannelInboundHandlerAdapter实现了channelRead就会监听到通道里面的消息
     *收到消息以后判断消息内容
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        Model m = (Model) msg; 
        int type = m.getType();
        switch (type) {
        case TypeData.PING:
        	// 收到客户端的ping  消息,回复 pong 表示收到
            sendPongMsg(ctx);
            break;
        case TypeData.PONG:
            System.out.println(name + " 收到回复  pong  消息来自 " + ctx.channel().remoteAddress());
            break;
        default:
        	handlerData(ctx,msg,type);
            break;
        }
    }
    /**
     * 收到消息回复第消息
     * @param ctx
     * @param msg
     */
    protected abstract void handlerData(ChannelHandlerContext ctx,Object msg,Integer type);
    /**
     * 客户端发送ping 消息使用
     * @param ctx
     */
    protected void sendPingMsg(ChannelHandlerContext ctx){
        Model model = new Model();

        model.setType(TypeData.PING);

        ctx.channel().writeAndFlush(model);

        heartbeatCount++;

        System.out.println(name + " 发送 ping 消息 to " + ctx.channel().remoteAddress() + "count :" + heartbeatCount);
    }
    /**
     * 服务端  回复 pong 消息使用
     * @param ctx
     */
    private void sendPongMsg(ChannelHandlerContext ctx) {
    	ConnectHandler.sendPongMsg(ctx, name);
    }
    
    /**
     * 用户心跳,客户端
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
            throws Exception {
        IdleStateEvent stateEvent = (IdleStateEvent) evt;

        switch (stateEvent.state()) {
        case READER_IDLE:
            handlerReaderIdle(ctx);
            break;
        case WRITER_IDLE:
            handlerWriterIdle(ctx);
            break;
        case ALL_IDLE:
            handlerAllIdle(ctx);
            break;  
        default:
            break;
        }
    }
    
    protected void handlerAllIdle(ChannelHandlerContext ctx) {
        System.err.println("---其他消息---");       
    }

    protected void handlerWriterIdle(ChannelHandlerContext ctx) {
        System.err.println("---发送消息---");        
    }


    protected void handlerReaderIdle(ChannelHandlerContext ctx) {
        ConnectHandler.handlerReaderIdle(ctx);   
    }
    /**
     *  有连接进入的时候触发
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ConnectHandler.channelActive(ctx);
    }
    /**
     * 有连接退出的时候触发
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    	ConnectHandler.channelInactive(ctx);
    }
}

6. server

Server3Handler

import io.netty.channel.ChannelHandlerContext;
import netty.demo.Middleware;
import netty.demo.entity.Model;
import netty.demo.entity.TypeData;
import netty.demo.service.controller.ReadHalder;

public class Server3Handler extends Middleware {

	public Server3Handler() {
		super("server");
		// TODO Auto-generated constructor stub
	}

	@Override
	protected void handlerData(ChannelHandlerContext ctx, Object msg, Integer type) {
		// TODO Auto-generated method stub
		Model model = (Model) msg;
		System.out.println(model.toString());
		ctx.channel().writeAndFlush(model);
	}

	@Override
	protected void handlerReaderIdle(ChannelHandlerContext ctx) {
		// TODO Auto-generated method stub
		super.handlerReaderIdle(ctx);
		ctx.close();
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		System.err.println(name + "  exception" + cause.toString());
	}
}

**Server **

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import netty.demo.decode.KyroMsgDecoder;
import netty.demo.decode.KyroMsgEncoder;
import netty.demo.decode.MsgPckDecode;
import netty.demo.decode.MsgPckEncode;

public class Server {
	private static final Integer PORT = 8081;
	
    public static void main(String[] args) {
    	
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);

        EventLoopGroup workerGroup = new NioEventLoopGroup(4);
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .localAddress(PORT)
            .childHandler(new ChannelInitializer<Channel>() {

                @Override
                protected void initChannel(Channel ch) throws Exception {
                    // TODO Auto-generated method stub
                    ChannelPipeline pipeline = ch.pipeline();
                    pipeline.addLast(new IdleStateHandler(10,0,0)); // 设置读 机制为 十秒读一次
                    //pipeline.addLast(new MsgPckDecode());
                    //pipeline.addLast(new MsgPckEncode());
                    pipeline.addLast(new KyroMsgDecoder());
                    pipeline.addLast(new KyroMsgEncoder());
                    pipeline.addLast(new Server3Handler()); 
                }
            });         
            System.out.println("服务启动端口为: "+PORT+" --");
            ChannelFuture sync = serverBootstrap.bind().sync();
            sync.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            //优雅的关闭资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

**ConnectHandler ** 存连接的类

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.alibaba.fastjson.JSON;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import netty.demo.entity.Model;
import netty.demo.entity.TypeData;

public class ConnectHandler {
	
	private static final Integer errorCount = 5; // 允许超时的次数
	
	// 存放当前的连接
	private static Map<Integer,Channel> channels = new ConcurrentHashMap<Integer, Channel>();
	// 消息超时的时候记录  超过一定次数就断开客户端
	private static Map<Integer,Integer> errors = new ConcurrentHashMap<Integer,Integer>();
	
	/**
	 * 有连接进入的时候调用
	 * @param ctx
	 * @throws Exception
	 */
	public static void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.err.println(" ---"+ctx.channel().remoteAddress() +"----连接进入" );
        ConnectHandler.putChannel(ctx);
    }
	
	public static void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // TODO Auto-generated method stub
        System.err.println(" ---"+ctx.channel().remoteAddress() +"----连接退出");
        ConnectHandler.removeChannel(ctx.hashCode());
    }
	/**
	 * 消息超时,认为已经断开  ,关闭连接
	 * @param ctx
	 */
	public static void handlerReaderIdle(ChannelHandlerContext ctx) {
        System.out.println("一次收不到消息");
        if(ConnectHandler.addErrors(ctx.hashCode()) >= errorCount) {
        	ctx.close();
            ConnectHandler.removeChannel(ctx.hashCode());
            System.err.println(" ---- client "+ ctx.channel().remoteAddress().toString() + " reader timeOut, --- close it");
        }
    } 
	/**
	 * 异常
	 * @param ctx
	 * @param cause
	 * @throws Exception
	 */
	public static void exceptionCaught(ChannelHandlerContext ctx, Throwable cause,String name)
            throws Exception {      
        System.out.println(name + "exception :"+ cause.toString());
    }
	/**
	 * 回复pong  内容
	 * @param ctx
	 */
	public static void sendPongMsg(ChannelHandlerContext ctx,String name) {

        Model model = new Model();

        model.setType(TypeData.PONG);

        ctx.channel().writeAndFlush(model);
        
        // 收到ping 消息  回复的时候清空 超时次数
        ConnectHandler.restErroes(ctx.hashCode());
        
        System.out.println(name +" 发送 pong 消息 to "+ctx.channel().remoteAddress() /*+" , count :" + heartbeatCount*/);
    }
	
	/**
	 * 连接成功,重置错误次数
	 * @param hashCode
	 */
	private static void restErroes(Integer hashCode) {
		errors.put(hashCode, 0);
	}
	/**
	 * 超时,计数加一
	 * @param hashCode
	 */
	private static Integer addErrors(Integer hashCode) {
		Integer hearCount;
		if(errors.containsKey(hashCode)) {
			hearCount = errors.get(hashCode);
			hearCount++;
		}else {
			hearCount = 1;
		}
		errors.put(hashCode,hearCount);
		return hearCount;
	}
	// 判断 连接是否在线
	public static boolean containsKey(Integer hashCode) {
		return channels.containsKey(hashCode);
	}
	
	/**
	 * 添加连接
	 */
	private static synchronized void putChannel(ChannelHandlerContext ctx) {
		Integer hashCode = ctx.hashCode();
		// 存入连接
        channels.put(hashCode, ctx.channel());
        // 存放ping 的次数
        errors.put(hashCode, 0);
	}
	/**
	 * 断开连接
	 * @param hashCode
	 */
	private static synchronized void removeChannel(Integer hashCode) {
		if(channels.containsKey(hashCode)) {
			channels.remove(hashCode);
			errors.remove(hashCode);
		}
	}
}

7. Client

Client3Handler

import io.netty.channel.ChannelHandlerContext;
import netty.demo.Middleware;
import netty.demo.entity.Model;
/**
*继承我们自己编写的枢纽站
*/
public class Client3Handler extends Middleware {

    private Client client;

    public Client3Handler(Client client) {
        super("client");
        this.client = client;
    }

    @Override
    protected void handlerData(ChannelHandlerContext ctx, Object msg,Integer type) {
        // TODO Auto-generated method stub
        Model model = (Model) msg;
        System.out.println("client  收到数据: " + model.toString());
    }
    /**
     * 触发时间的时候第一时间发送一个ping  让ping 机制开始执行
     */
    @Override
    protected void handlerAllIdle(ChannelHandlerContext ctx) {
        // TODO Auto-generated method stub
        super.handlerAllIdle(ctx);
        sendPingMsg(ctx);
    }   
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // TODO Auto-generated method stub
        super.channelInactive(ctx);
        client.doConnect(); // 断开重连
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {      
        System.out.println(name + "exception :"+ cause.toString());
    }
}

Client

import java.util.Scanner;
import java.util.concurrent.TimeUnit;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import netty.demo.decode.KyroMsgDecoder;
import netty.demo.decode.KyroMsgEncoder;
import netty.demo.decode.MsgPckDecode;
import netty.demo.decode.MsgPckEncode;
import netty.demo.entity.Model;
import netty.demo.entity.TypeData;

public class Client {

	private final String ADDRESS = "localhost";
	private final Integer PORT = 8081;
	
    private NioEventLoopGroup worker = new NioEventLoopGroup();

    private Channel channel;

    private Bootstrap bootstrap;

    public static void main(String[] args) {
        Client  client = new Client();

        client.start();
        System.out.println("客户端启动成功");
        client.sendData();      
    }

    private void start() {
        bootstrap = new Bootstrap();        
        bootstrap.group(worker)
        .channel(NioSocketChannel.class)
        .option(ChannelOption.TCP_NODELAY, true)
        .handler(new ChannelInitializer<Channel>() {
            @Override
            protected void initChannel(Channel ch) throws Exception {
                // TODO Auto-generated method stub
                ChannelPipeline pipeline = ch.pipeline();

                pipeline.addLast(new IdleStateHandler(0,0,5)); // 设置客户端的机制为 5 秒触发一次ping

                //pipeline.addLast(new MsgPckDecode()); // 编码器

                //pipeline.addLast(new MsgPckEncode()); // 解码器
                pipeline.addLast(new KyroMsgDecoder());
                pipeline.addLast(new KyroMsgEncoder());

                pipeline.addLast(new Client3Handler(Client.this));   // 当前客户端的控制器            
            }           
        }); 
        doConnect();
    }

    /**
     * 连接服务端 and 重连
     */
    protected void doConnect() {

        if (channel != null && channel.isActive()){
            return;
        }       
        ChannelFuture connect = bootstrap.connect(ADDRESS, PORT);
        //实现监听通道连接的方法
        connect.addListener(new ChannelFutureListener() {

            public void operationComplete(ChannelFuture channelFuture) throws Exception {

                if(channelFuture.isSuccess()){
                    channel = channelFuture.channel();
                    System.out.println("连接成功");
                }else{
                    System.out.println("每隔2s重连....");
                    channelFuture.channel().eventLoop().schedule(new Runnable() {

                        public void run() {
                            // TODO Auto-generated method stub
                            doConnect();
                        }
                    },2,TimeUnit.SECONDS);
                }   
            }
        });     
    }   
    /**
     * 向服务端发送消息
     */
    private void sendData() {
        Scanner sc= new Scanner(System.in); 
        for (int i = 0; i < 1000; i++) {

            if(channel != null && channel.isActive()){              
                //获取一个键盘扫描器
            	System.err.println("请输入要发送的消息");
                String nextLine = sc.nextLine();
                Model model = new Model();

                model.setType(TypeData.CONTENT);

                model.setBody(nextLine);

                channel.writeAndFlush(model);
            }
        }
    }
}

8. 测试

netty 搭建服务端客户端 实现长连接 发送心跳_第2张图片
客户端发送 ping 请求服务端接受回复 pong
客户端发送其他数据 ,原封不动的返回出去。
netty 搭建服务端客户端 实现长连接 发送心跳_第3张图片
这个是服务端的控制台

你可能感兴趣的:(netty)