Netty开发在线聊天


)

技术选型

由于公司业务的发展,在线聊天成了一个迫切的需求。而这样的一个任务落在了我的头上,当时我首先想到的就是之前在另一个项目中用到的Netty,用这个东西开发再合适不过。
网络通信协议方面放弃了之前项目中的java自带的二进制格式,开始采用Google旗下的ProtoBuf,ProtoBuf无论是在效率和兼容性方面都特别的强。
至于基础的项目架构采用的是,SpringBoot的架构,简单方便,以后想要加入各种最组件也比较方便。
文章底部有源码下载链接,欢迎各位不吝赐教。

ProtoBuf简单使用

去年也查过一些使用教程,感觉使用还是比较困难的,之后就放弃了。后来才发现还有更简单的使用方式。

idea安装ProtoBuf

protobuf安装说明

引入protobuf支持

主要是引入protobuf的jar包以及插件支持。

 <properties>
      
        <protobuf-java.version>3.5.1</protobuf-java.version>
        <protobuf-javanano.version>3.1.0</protobuf-javanano.version>
    </properties>
 <dependencies>
		        <dependency>
		            <groupId>com.google.protobuf</groupId>
		            <artifactId>protobuf-java</artifactId>
		            <version>${protobuf-java.version}</version>
		        </dependency>
		        <dependency>
		            <groupId>com.google.protobuf.nano</groupId>
		            <artifactId>protobuf-javanano</artifactId>
		            <version>${protobuf-javanano.version}</version>
		        </dependency>
   </dependencies>
 <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.6.2</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <pluginId>java</pluginId>
                    <protocArtifact>com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}</protocArtifact>
                    <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
                    <outputDirectory>${project.build.sourceDirectory}</outputDirectory>
                    <clearOutputDirectory>false</clearOutputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

编写ProtoBuf文件

简单的编写了一个例子,大致如下

syntax = "proto3";
option java_package = "com.jihite";
option java_outer_classname = "PersonModel";

message Person {
    int32 id = 1;
    string name = 2;
    string email = 3;
}

然后打开Idea的Maven视图,选择Plugins>protobuf>protobuf:compile-javanano的图标,点击即可生成protobuf代码。完成后代码自动引入到src目录下。
在这里插入图片描述
下图为生成的java代码的部分贴图。
在这里插入图片描述

对应实体类的编写

public class Person {

    private int id;
    private String name;
    private String email;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

ProtoBuf工具类的编写

mport com.google.protobuf.nano.MessageNano;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
public class ProtobuffUtil {
    public static <T extends MessageNano> byte[] toBytes(T nano){
        if(nano == null){
            return new byte[0];
        }
        return MessageNano.toByteArray(nano);
    }

    public static <T extends MessageNano> T fromBytes(byte[] bytes, Class<T> nanoClazz){
        if(bytes == null || bytes.length <= 0){
            return null;
        }
        try{
            T t = nanoClazz.newInstance();
            return MessageNano.mergeFrom(t, bytes);
        }catch(Exception e){
            throw new RuntimeException("反序列化失败", e);
        }
    }

    public static <R, T extends MessageNano> R toBean(T nano, Class<R> beanClazz){
        try{
            R bean = beanClazz.newInstance();
            BeanInfo bi = Introspector.getBeanInfo(beanClazz);
            PropertyDescriptor[] properties = bi.getPropertyDescriptors();
            for(PropertyDescriptor pd : properties){
                String fieldName = pd.getName();
                if(fieldName.equalsIgnoreCase("class")){
                    continue;
                }
                pd.getWriteMethod().invoke(bean, getFieldValue(nano, fieldName));
            }
            return bean;
        }catch(Exception e){
            throw new RuntimeException("nano转化成bean异常", e);
        }
    }

    public static <R extends MessageNano, T> R toNano(T bean, Class<R> nanoClass){
        try{
            R nano = nanoClass.newInstance();
            BeanInfo bi = Introspector.getBeanInfo(bean.getClass());
            PropertyDescriptor[] properties = bi.getPropertyDescriptors();
            for(PropertyDescriptor pd : properties){
                String fieldName = pd.getName();
                if(fieldName.equalsIgnoreCase("class")){
                    continue;
                }
                Object fieldValue = pd.getReadMethod().invoke(bean, null);
                setFieldValue(nano, fieldName, fieldValue);
            }
            return nano;
        }catch(Exception e){
            throw new RuntimeException("bean转化成nano异常", e);
        }
    }

    private static <T extends MessageNano> Object getFieldValue(T nano, String name)throws Exception {
        Field field = nano.getClass().getField(name);
        if(field == null){
            return null;
        }
        field.setAccessible(true);
        return field.get(nano);
    }

    private static <T extends MessageNano> void setFieldValue(T nano, String name, Object value)throws Exception {
        Field field = nano.getClass().getField(name);
        if(field == null){
            return;
        }
        field.setAccessible(true);
        field.set(nano, value);
    }
}

ProtBuf工具的测试

public static void main(String[] args) {
        Person p = new Person();
        p.setName("张三");
        p.setId(30);
        p.setEmail("ssss@163.com");
        byte[] bytes = ProtobuffUtil.toBytes(ProtobuffUtil.toNano(p, PersonModel.Person.class));
        System.out.println("ProtoBuf 方式 bytes.length:"+bytes.length);//10

        String pJson= JSON.toJSONString(p);

        System.out.println("Json 方式转bytes.length:"+pJson.getBytes().length);

        p = ProtobuffUtil.toBean(ProtobuffUtil.fromBytes(bytes, PersonModel.Person.class), Person.class);
        System.out.println(p.getName()+"------"+p.getEmail());

        System.out.println();
    }

以上代码片段的运行结果:

ProtoBuf 方式 bytes.length:29
Json 方式转bytes.length:53

由此可以看出ProtoBuf的长度是Java方式的一般左右,表达同样的信息ProtoBuf所占的字节数目也会更小。更利于性能的发挥,难怪很多大神都用Protobuf最为网络传输的协议。

Netty聊天

netty是一个非常优秀的非阻塞架构,现在市面上不少流行的rpc框架都是采用的netty作为核心技术组件,比如说Dubbo、GRPC等等。不少的IM在线聊天采用的也是Netty框架作为基础组件。

NettyServer端

Netty启动类

import com.zouni.chat.service.ChatService;
import com.zouni.netty.NettyServerBootstrap;
import com.zouni.netty.base.model.ChatMessage;
import com.zouni.netty.util.SpringApplicationContextUtil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.StringRedisTemplate;

@SpringBootApplication(scanBasePackages = "com.zouni.*")
public class NettySocketApplication {


    public static void main(String[] args) throws InterruptedException {
        NettyServerBootstrap bootstrap=new NettyServerBootstrap( 6789 );
        ApplicationContext run =SpringApplication.run(NettySocketApplication.class,args);
        SpringApplicationContextUtil.setApplicationContext(run);

       
       // chat.register(new ChatMessage());
    }
}

NettySocket类

public class NettyServerBootstrap {

    EventExecutorGroup execGroup = new DefaultEventExecutorGroup(100);


    private static  int port=0;
    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 pipeline = socketChannel.pipeline();
                pipeline.addLast(new SocketDecoder());
                pipeline.addLast(new SocketEncoder());
                pipeline.addLast(  new IdleStateHandler(25, 0, 0, TimeUnit.SECONDS) );
                pipeline.addLast(execGroup,new EqmServerHandler());
            }
        });
        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(port);

    }
}

Netty编码解码

解决粘包,拆包的问题

import com.zouni.netty.util.ByteUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

public class SocketDecoder extends ByteToMessageDecoder {
    private static final Logger logger = LoggerFactory.getLogger( SocketDecoder.class);

    /**
     * 编码插件,接受到的数据在此处进行过滤,做拆包粘包处理等操作
     */
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        byte[] orignalBytes = new byte[in.readableBytes()];
        in.getBytes(in.readerIndex(), orignalBytes);
        if (in.readableBytes() <= 0) {
            return;
        }

        /**消息完整性校验开始**/
        int length= ByteUtil.byteArrayToInt(ByteUtil.subByte(orignalBytes,0,4));
        int realLength=orignalBytes.length;
        logger.info( "消息实际长度---"+realLength+"-----------消息声明长度------"+length );
        if(realLength<length){
            logger.warn("数据包缺失");
            return;
        }

        logger.warn("服务端接收程序:" );

        // TODO 做粘包处理

        byte[] bytes = new byte[in.readableBytes()];

        in.readBytes(bytes, 0, bytes.length);
        out.add(bytes);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        logger.error("unexpected exception", cause);
        // ctx.close();
    }
}
public class SocketEncoder extends ByteArrayEncoder
{
	 private static final Logger logger = LoggerFactory.getLogger( SocketEncoder.class.getName());

	@Override
	protected void encode(ChannelHandlerContext ctx, byte[] msg, List<Object> out) throws Exception
	{
		//logger.warn("服务器发送数据:" + FrameUtils.toString(msg));
		ChannelHandlerContextUtils.writeAndFlush(ctx, msg);
	}
}

业务处理模块

import com.zouni.netty.attribute.SocketContext;
import com.zouni.netty.base.model.ChatMessage;
import com.zouni.netty.protobuf.nano.ProtoMsg;
import com.zouni.netty.util.FrameUtils;
import com.zouni.netty.util.ProtobuffUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AttributeKey;
import io.netty.util.internal.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.UUID;

public class EqmServerHandler extends SimpleChannelInboundHandler<byte[]> {




    private static final Logger logger = LoggerFactory.getLogger(EqmServerHandler.class);


    private DeviceController deviceController;

    /**
     * 通道上下文信息
     */
    private AttributeKey<SocketContext> attrDEqmContext;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception{
        logger.warn("channelActive被触发,已经有设备连接上采集软件");
        attrDEqmContext = AttributeKey.valueOf(String.valueOf( UUID.randomUUID()));
        deviceController=new DeviceController(attrDEqmContext);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, byte[] msg) throws Exception {
        //logger.warn("channelRead0进入了");
        ChatMessage chatMessage= FrameUtils.getMessageInfo(msg);
        lossConnectCount=0;
        //System.out.println(chatMessage.getFrom()+"------"+chatMessage.getFromNick());
        if(chatMessage.getMsgType()==ChatMessage.registerMsg){
            deviceController.saveRegisterInfo(channelHandlerContext,chatMessage);
        }else if(chatMessage.getMsgType()==ChatMessage.heartMsg){
            deviceController.heartbeat( channelHandlerContext,chatMessage );
        }else{
            deviceController.otherOperate( channelHandlerContext,chatMessage );
        }
    }


    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        logger.info("断开连接");
        if (StringUtil.isNullOrEmpty(ctx.channel().attr(attrDEqmContext).get().getDeviceNo())) {
            logger.warn("设备注册失败!");
        } else {
            String deviceNo=ctx.channel().attr(attrDEqmContext).get().getDeviceNo();
            SocketContextMap.getInstance().remove(deviceNo);
            logger.warn(ctx.channel().attr(attrDEqmContext).get().getDeviceNo() + " : 编号设备主动断开连接!");
        }
    }
    private int lossConnectCount = 0;

    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{
        if (evt instanceof IdleStateEvent){
            IdleStateEvent event = (IdleStateEvent)evt;
            if (event.state()== IdleState.READER_IDLE){
                lossConnectCount++;
                if (lossConnectCount>2){
                    if(ctx!=null&&ctx.channel()!=null&&ctx.channel().attr( attrDEqmContext )!=null&&ctx.channel().attr(attrDEqmContext).get()!=null){
                        String deviceNo=ctx.channel().attr(attrDEqmContext).get().getDeviceNo();
                        SocketContextMap.getInstance().remove(deviceNo);
                        ctx.channel().close();
                    }

                }
            }
        }else {
            super.userEventTriggered(ctx,evt);
        }

    }


}

Netty客户端

import com.zouni.netty.codec.SocketDecoder;
import com.zouni.netty.codec.SocketEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.SneakyThrows;

public class HelloClient {

    public void connect(String host, int port) throws Exception {

        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup).channel(NioSocketChannel.class).option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {

                            ch.pipeline().addLast(new SocketEncoder()).addLast(new SocketDecoder()).addLast(new ClientServerHandler());
                        }
                    });

            // Start the client.
            ChannelFuture f = b.connect(host, port).sync();

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        for (int i=0;i<100;i++){
            new Thread(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    HelloClient client = new HelloClient();
                    client.connect("127.0.0.1", 6789);
                }
            }).start();
        }
    }
}

客户端业务处理

public class ClientServerHandler extends SimpleChannelInboundHandler<byte[]> {




    private static final Logger logger = LoggerFactory.getLogger(ClientServerHandler.class);






    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception{
        logger.warn("channelActive被触发,已经有设备连接上采集软件");
        logger.info("HelloClientIntHandler.channelActive");
        ChatMessage p = new ChatMessage();
        p.setContent("张三");
        p.setMsgId(30L);
        p.setFrom("ssss@163.com");
        p.setTo("kkk");
        p.setFromNick("--");
        p.setUrl("");
        p.setProperty("");
        p.setJson("");
        p.setMsgType(0);
        p.setTime(Calendar.getInstance().getTimeInMillis());

        byte[] bytes = ProtobuffUtil.toBytes(ProtobuffUtil.toNano(p, ProtoMsg.MessageRequest.class));
        System.out.println(bytes.length);
        byte [] length= ByteUtil.intByteArray(bytes.length);
        byte [] msgByte=ByteUtil.addBytes(length,bytes);
        ctx.write(msgByte);
        Thread.sleep(100);

    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, byte[] msg) throws Exception {
        //logger.warn("channelRead0进入了");
        System.out.println("-----channelRead0进入了--------");
        ChatMessage chatMessage= FrameUtils.getMessageInfo(msg);
        lossConnectCount=0;
        System.out.println(chatMessage.getFrom());

    }


    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        logger.info("断开连接");

    }
    private int lossConnectCount = 0;

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception{


    }


}

运行效果

在这里插入图片描述

完整代码下载链接:https://github.com/xuhang0310/netty-socket.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值