Netty开发在线聊天工具
)
技术选型
由于公司业务的发展,在线聊天成了一个迫切的需求。而这样的一个任务落在了我的头上,当时我首先想到的就是之前在另一个项目中用到的Netty,用这个东西开发再合适不过。
网络通信协议方面放弃了之前项目中的java自带的二进制格式,开始采用Google旗下的ProtoBuf,ProtoBuf无论是在效率和兼容性方面都特别的强。
至于基础的项目架构采用的是,SpringBoot的架构,简单方便,以后想要加入各种最组件也比较方便。
文章底部有源码下载链接,欢迎各位不吝赐教。
ProtoBuf简单使用
去年也查过一些使用教程,感觉使用还是比较困难的,之后就放弃了。后来才发现还有更简单的使用方式。
idea安装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