24 changed files with 357 additions and 168 deletions
@ -1,8 +0,0 @@ |
|||||
package com.bx.imserver.constant; |
|
||||
|
|
||||
public class Constant { |
|
||||
|
|
||||
// public static String LOCAL_SERVER_ID = UUID.randomUUID().toString();
|
|
||||
|
|
||||
|
|
||||
} |
|
||||
@ -0,0 +1,10 @@ |
|||||
|
package com.bx.imserver.netty; |
||||
|
|
||||
|
public interface IMServer { |
||||
|
|
||||
|
boolean isReady(); |
||||
|
|
||||
|
void start(); |
||||
|
|
||||
|
void stop(); |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
package com.bx.imserver.netty; |
||||
|
|
||||
|
import com.bx.imcommon.contant.RedisKey; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.boot.CommandLineRunner; |
||||
|
import org.springframework.data.redis.core.RedisTemplate; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import javax.annotation.PreDestroy; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Component |
||||
|
public class IMServerMap implements CommandLineRunner { |
||||
|
|
||||
|
public static volatile long serverId = 0; |
||||
|
|
||||
|
@Autowired |
||||
|
RedisTemplate<String,Object> redisTemplate; |
||||
|
|
||||
|
@Autowired |
||||
|
private List<IMServer> imServers; |
||||
|
|
||||
|
@Override |
||||
|
public void run(String... args) throws Exception { |
||||
|
// 初始化SERVER_ID
|
||||
|
String key = RedisKey.IM_MAX_SERVER_ID; |
||||
|
serverId = redisTemplate.opsForValue().increment(key,1); |
||||
|
// 启动服务
|
||||
|
for(IMServer imServer:imServers){ |
||||
|
imServer.start(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
public void destroy(){ |
||||
|
// 停止服务
|
||||
|
for(IMServer imServer:imServers){ |
||||
|
imServer.stop(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
package com.bx.imserver.processor; |
package com.bx.imserver.netty.processor; |
||||
|
|
||||
|
|
||||
import io.netty.channel.ChannelHandlerContext; |
import io.netty.channel.ChannelHandlerContext; |
||||
@ -1,4 +1,4 @@ |
|||||
package com.bx.imserver.processor; |
package com.bx.imserver.netty.processor; |
||||
|
|
||||
import com.bx.imcommon.enums.IMCmdType; |
import com.bx.imcommon.enums.IMCmdType; |
||||
import com.bx.imserver.util.SpringContextHolder; |
import com.bx.imserver.util.SpringContextHolder; |
||||
@ -0,0 +1,94 @@ |
|||||
|
package com.bx.imserver.netty.tcp; |
||||
|
|
||||
|
import com.bx.imserver.netty.IMChannelHandler; |
||||
|
import com.bx.imserver.netty.IMServer; |
||||
|
import com.bx.imserver.netty.tcp.endecode.MessageProtocolDecoder; |
||||
|
import com.bx.imserver.netty.tcp.endecode.MessageProtocolEncoder; |
||||
|
import io.netty.bootstrap.ServerBootstrap; |
||||
|
import io.netty.channel.*; |
||||
|
import io.netty.channel.nio.NioEventLoopGroup; |
||||
|
import io.netty.channel.socket.nio.NioServerSocketChannel; |
||||
|
import io.netty.handler.timeout.IdleStateHandler; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* TCP服务器,用于连接非网页的客户端,协议格式: 4字节内容的长度+IMSendInfo的JSON序列化 |
||||
|
* |
||||
|
* @author Blue |
||||
|
* @date 2022-11-20 |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@Component |
||||
|
@ConditionalOnProperty(prefix = "tcpsocket", value = "enable", havingValue = "true",matchIfMissing = true) |
||||
|
public class TcpSocketServer implements IMServer { |
||||
|
|
||||
|
|
||||
|
private volatile boolean ready = false; |
||||
|
|
||||
|
@Value("${tcpsocket.port}") |
||||
|
private int port; |
||||
|
|
||||
|
private ServerBootstrap bootstrap = new ServerBootstrap(); |
||||
|
private EventLoopGroup bossGroup = new NioEventLoopGroup(); |
||||
|
private EventLoopGroup workGroup = new NioEventLoopGroup(); |
||||
|
|
||||
|
@Override |
||||
|
public boolean isReady() { |
||||
|
return ready; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void start() { |
||||
|
// 设置为主从线程模型
|
||||
|
bootstrap.group(bossGroup, workGroup) |
||||
|
// 设置服务端NIO通信类型
|
||||
|
.channel(NioServerSocketChannel.class) |
||||
|
// 设置ChannelPipeline,也就是业务职责链,由处理的Handler串联而成,由从线程池处理
|
||||
|
.childHandler(new ChannelInitializer<Channel>() { |
||||
|
// 添加处理的Handler,通常包括消息编解码、业务处理,也可以是日志、权限、过滤等
|
||||
|
@Override |
||||
|
protected void initChannel(Channel ch) throws Exception { |
||||
|
// 获取职责链
|
||||
|
ChannelPipeline pipeline = ch.pipeline(); |
||||
|
pipeline.addLast(new IdleStateHandler(15, 0, 0, TimeUnit.SECONDS)); |
||||
|
pipeline.addLast("encode",new MessageProtocolEncoder()); |
||||
|
pipeline.addLast("decode",new MessageProtocolDecoder()); |
||||
|
pipeline.addLast("handler", new IMChannelHandler()); |
||||
|
} |
||||
|
}) |
||||
|
// bootstrap 还可以设置TCP参数,根据需要可以分别设置主线程池和从线程池参数,来优化服务端性能。
|
||||
|
// 其中主线程池使用option方法来设置,从线程池使用childOption方法设置。
|
||||
|
// backlog表示主线程池中在套接口排队的最大数量,队列由未连接队列(三次握手未完成的)和已连接队列
|
||||
|
.option(ChannelOption.SO_BACKLOG, 5) |
||||
|
// 表示连接保活,相当于心跳机制,默认为7200s
|
||||
|
.childOption(ChannelOption.SO_KEEPALIVE, true); |
||||
|
|
||||
|
try { |
||||
|
// 绑定端口,启动select线程,轮询监听channel事件,监听到事件之后就会交给从线程池处理
|
||||
|
Channel channel = bootstrap.bind(port).sync().channel(); |
||||
|
// 就绪标志
|
||||
|
this.ready = true; |
||||
|
log.info("tcp server 初始化完成,端口:{}",port); |
||||
|
// 等待服务端口关闭
|
||||
|
//channel.closeFuture().sync();
|
||||
|
} catch (InterruptedException e) { |
||||
|
log.info("tcp server 初始化异常",e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void stop(){ |
||||
|
log.info("tcp server 停止"); |
||||
|
bossGroup.shutdownGracefully(); |
||||
|
workGroup.shutdownGracefully(); |
||||
|
this.ready = false; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
package com.bx.imserver.netty.tcp.endecode; |
||||
|
|
||||
|
import com.bx.imcommon.model.IMSendInfo; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import io.netty.buffer.ByteBuf; |
||||
|
import io.netty.channel.ChannelHandlerContext; |
||||
|
import io.netty.handler.codec.ReplayingDecoder; |
||||
|
import io.netty.util.CharsetUtil; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@Slf4j |
||||
|
public class MessageProtocolDecoder extends ReplayingDecoder { |
||||
|
|
||||
|
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { |
||||
|
if(byteBuf.readableBytes()< 4){ |
||||
|
return; |
||||
|
} |
||||
|
// 获取到包的长度
|
||||
|
long length=byteBuf.readLong(); |
||||
|
// 转成IMSendInfo
|
||||
|
ByteBuf contentBuf = byteBuf.readBytes((int)length); |
||||
|
String content = contentBuf.toString(CharsetUtil.UTF_8); |
||||
|
ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
IMSendInfo sendInfo = objectMapper.readValue(content, IMSendInfo.class); |
||||
|
list.add(sendInfo); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
package com.bx.imserver.netty.tcp.endecode; |
||||
|
|
||||
|
import com.bx.imcommon.model.IMSendInfo; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import io.netty.buffer.ByteBuf; |
||||
|
import io.netty.channel.ChannelHandlerContext; |
||||
|
import io.netty.handler.codec.MessageToByteEncoder; |
||||
|
|
||||
|
public class MessageProtocolEncoder extends MessageToByteEncoder<IMSendInfo> { |
||||
|
|
||||
|
@Override |
||||
|
protected void encode(ChannelHandlerContext channelHandlerContext, IMSendInfo sendInfo, ByteBuf byteBuf) throws Exception { |
||||
|
ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
String content = objectMapper.writeValueAsString(sendInfo); |
||||
|
byte[] bytes = content.getBytes("UTF-8"); |
||||
|
// 写入长度
|
||||
|
byteBuf.writeLong(bytes.length); |
||||
|
// 写入命令体
|
||||
|
byteBuf.writeBytes(bytes); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,102 @@ |
|||||
|
package com.bx.imserver.netty.ws; |
||||
|
|
||||
|
import com.bx.imserver.netty.IMChannelHandler; |
||||
|
import com.bx.imserver.netty.IMServer; |
||||
|
import com.bx.imserver.netty.ws.endecode.MessageProtocolDecoder; |
||||
|
import com.bx.imserver.netty.ws.endecode.MessageProtocolEncoder; |
||||
|
import io.netty.bootstrap.ServerBootstrap; |
||||
|
import io.netty.channel.*; |
||||
|
import io.netty.channel.nio.NioEventLoopGroup; |
||||
|
import io.netty.channel.socket.nio.NioServerSocketChannel; |
||||
|
import io.netty.handler.codec.http.HttpObjectAggregator; |
||||
|
import io.netty.handler.codec.http.HttpServerCodec; |
||||
|
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; |
||||
|
import io.netty.handler.stream.ChunkedWriteHandler; |
||||
|
import io.netty.handler.timeout.IdleStateHandler; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* WS服务器,用于连接网页的客户端,协议格式: 直接IMSendInfo的JSON序列化 |
||||
|
* |
||||
|
* @author Blue |
||||
|
* @date 2022-11-20 |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@Component |
||||
|
@ConditionalOnProperty(prefix = "websocket", value = "enable", havingValue = "true",matchIfMissing = true) |
||||
|
public class WebSocketServer implements IMServer { |
||||
|
|
||||
|
@Value("${websocket.port}") |
||||
|
private int port; |
||||
|
|
||||
|
private volatile boolean ready = false; |
||||
|
|
||||
|
private ServerBootstrap bootstrap = new ServerBootstrap(); |
||||
|
private EventLoopGroup bossGroup = new NioEventLoopGroup(); |
||||
|
private EventLoopGroup workGroup = new NioEventLoopGroup(); |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public boolean isReady(){ |
||||
|
return ready; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void start() { |
||||
|
// 设置为主从线程模型
|
||||
|
bootstrap.group(bossGroup, workGroup) |
||||
|
// 设置服务端NIO通信类型
|
||||
|
.channel(NioServerSocketChannel.class) |
||||
|
// 设置ChannelPipeline,也就是业务职责链,由处理的Handler串联而成,由从线程池处理
|
||||
|
.childHandler(new ChannelInitializer<Channel>() { |
||||
|
// 添加处理的Handler,通常包括消息编解码、业务处理,也可以是日志、权限、过滤等
|
||||
|
@Override |
||||
|
protected void initChannel(Channel ch) throws Exception { |
||||
|
// 获取职责链
|
||||
|
ChannelPipeline pipeline = ch.pipeline(); |
||||
|
pipeline.addLast(new IdleStateHandler(15, 0, 0, TimeUnit.SECONDS)); |
||||
|
pipeline.addLast("http-codec", new HttpServerCodec()); |
||||
|
pipeline.addLast("aggregator", new HttpObjectAggregator(65535)); |
||||
|
pipeline.addLast("http-chunked", new ChunkedWriteHandler()); |
||||
|
pipeline.addLast(new WebSocketServerProtocolHandler("/im")); |
||||
|
pipeline.addLast("encode",new MessageProtocolEncoder()); |
||||
|
pipeline.addLast("decode",new MessageProtocolDecoder()); |
||||
|
pipeline.addLast("handler", new IMChannelHandler()); |
||||
|
} |
||||
|
}) |
||||
|
// bootstrap 还可以设置TCP参数,根据需要可以分别设置主线程池和从线程池参数,来优化服务端性能。
|
||||
|
// 其中主线程池使用option方法来设置,从线程池使用childOption方法设置。
|
||||
|
// backlog表示主线程池中在套接口排队的最大数量,队列由未连接队列(三次握手未完成的)和已连接队列
|
||||
|
.option(ChannelOption.SO_BACKLOG, 5) |
||||
|
// 表示连接保活,相当于心跳机制,默认为7200s
|
||||
|
.childOption(ChannelOption.SO_KEEPALIVE, true); |
||||
|
|
||||
|
try { |
||||
|
// 绑定端口,启动select线程,轮询监听channel事件,监听到事件之后就会交给从线程池处理
|
||||
|
Channel channel = bootstrap.bind(port).sync().channel(); |
||||
|
// 就绪标志
|
||||
|
this.ready = true; |
||||
|
log.info("websocket server 初始化完成,端口:{}",port); |
||||
|
// 等待服务端口关闭
|
||||
|
//channel.closeFuture().sync();
|
||||
|
} catch (InterruptedException e) { |
||||
|
log.info("websocket server 初始化异常",e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void stop() { |
||||
|
log.info("websocket server 停止"); |
||||
|
bossGroup.shutdownGracefully(); |
||||
|
workGroup.shutdownGracefully(); |
||||
|
this.ready = false; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
package com.bx.imserver.ws.endecode; |
package com.bx.imserver.netty.ws.endecode; |
||||
|
|
||||
import com.bx.imcommon.model.IMSendInfo; |
import com.bx.imcommon.model.IMSendInfo; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
@ -1,4 +1,4 @@ |
|||||
package com.bx.imserver.ws.endecode; |
package com.bx.imserver.netty.ws.endecode; |
||||
|
|
||||
import com.bx.imcommon.model.IMSendInfo; |
import com.bx.imcommon.model.IMSendInfo; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
@ -1,101 +0,0 @@ |
|||||
package com.bx.imserver.ws; |
|
||||
|
|
||||
import com.bx.imcommon.contant.RedisKey; |
|
||||
import com.bx.imserver.ws.endecode.MessageProtocolDecoder; |
|
||||
import com.bx.imserver.ws.endecode.MessageProtocolEncoder; |
|
||||
import io.netty.bootstrap.ServerBootstrap; |
|
||||
import io.netty.channel.*; |
|
||||
import io.netty.channel.nio.NioEventLoopGroup; |
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel; |
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator; |
|
||||
import io.netty.handler.codec.http.HttpServerCodec; |
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; |
|
||||
import io.netty.handler.stream.ChunkedWriteHandler; |
|
||||
import io.netty.handler.timeout.IdleStateHandler; |
|
||||
import lombok.extern.slf4j.Slf4j; |
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
|
||||
import org.springframework.data.redis.core.RedisTemplate; |
|
||||
import org.springframework.stereotype.Component; |
|
||||
|
|
||||
import javax.annotation.PostConstruct; |
|
||||
import java.util.concurrent.TimeUnit; |
|
||||
|
|
||||
@Slf4j |
|
||||
@Component |
|
||||
public class WebsocketServer { |
|
||||
|
|
||||
public static long serverId = 0; |
|
||||
|
|
||||
@Autowired |
|
||||
RedisTemplate<String,Object> redisTemplate; |
|
||||
|
|
||||
private volatile boolean ready = false; |
|
||||
|
|
||||
|
|
||||
@PostConstruct |
|
||||
public void init(){ |
|
||||
// 初始化SERVER_ID
|
|
||||
String key = RedisKey.IM_MAX_SERVER_ID; |
|
||||
serverId = redisTemplate.opsForValue().increment(key,1); |
|
||||
} |
|
||||
|
|
||||
public boolean isReady(){ |
|
||||
return ready; |
|
||||
} |
|
||||
|
|
||||
public long getServerId(){ |
|
||||
return serverId; |
|
||||
} |
|
||||
|
|
||||
public void start(int port) { |
|
||||
// 服务端启动辅助类,用于设置TCP相关参数
|
|
||||
ServerBootstrap bootstrap = new ServerBootstrap(); |
|
||||
// 获取Reactor线程池
|
|
||||
EventLoopGroup bossGroup = new NioEventLoopGroup(); |
|
||||
EventLoopGroup workGroup = new NioEventLoopGroup(); |
|
||||
// 设置为主从线程模型
|
|
||||
bootstrap.group(bossGroup, workGroup) |
|
||||
// 设置服务端NIO通信类型
|
|
||||
.channel(NioServerSocketChannel.class) |
|
||||
// 设置ChannelPipeline,也就是业务职责链,由处理的Handler串联而成,由从线程池处理
|
|
||||
.childHandler(new ChannelInitializer<Channel>() { |
|
||||
// 添加处理的Handler,通常包括消息编解码、业务处理,也可以是日志、权限、过滤等
|
|
||||
@Override |
|
||||
protected void initChannel(Channel ch) throws Exception { |
|
||||
// 获取职责链
|
|
||||
ChannelPipeline pipeline = ch.pipeline(); |
|
||||
pipeline.addLast(new IdleStateHandler(15, 0, 0, TimeUnit.SECONDS)); |
|
||||
pipeline.addLast("http-codec", new HttpServerCodec()); |
|
||||
pipeline.addLast("aggregator", new HttpObjectAggregator(65535)); |
|
||||
pipeline.addLast("http-chunked", new ChunkedWriteHandler()); |
|
||||
pipeline.addLast(new WebSocketServerProtocolHandler("/im")); |
|
||||
pipeline.addLast("encode",new MessageProtocolEncoder()); |
|
||||
pipeline.addLast("decode",new MessageProtocolDecoder()); |
|
||||
pipeline.addLast("handler", new WebSocketHandler()); |
|
||||
} |
|
||||
}) |
|
||||
// bootstrap 还可以设置TCP参数,根据需要可以分别设置主线程池和从线程池参数,来优化服务端性能。
|
|
||||
// 其中主线程池使用option方法来设置,从线程池使用childOption方法设置。
|
|
||||
// backlog表示主线程池中在套接口排队的最大数量,队列由未连接队列(三次握手未完成的)和已连接队列
|
|
||||
.option(ChannelOption.SO_BACKLOG, 5) |
|
||||
// 表示连接保活,相当于心跳机制,默认为7200s
|
|
||||
.childOption(ChannelOption.SO_KEEPALIVE, true); |
|
||||
|
|
||||
try { |
|
||||
// 绑定端口,启动select线程,轮询监听channel事件,监听到事件之后就会交给从线程池处理
|
|
||||
Channel channel = bootstrap.bind(port).sync().channel(); |
|
||||
// 就绪标志
|
|
||||
this.ready = true; |
|
||||
log.info("websocket server 初始化完成...."); |
|
||||
// 等待服务端口关闭
|
|
||||
channel.closeFuture().sync(); |
|
||||
} catch (InterruptedException e) { |
|
||||
e.printStackTrace(); |
|
||||
} finally { |
|
||||
// 优雅退出,释放线程池资源
|
|
||||
bossGroup.shutdownGracefully(); |
|
||||
workGroup.shutdownGracefully(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
@ -1,11 +1,16 @@ |
|||||
server: |
server: |
||||
port: 8877 |
port: 8877 |
||||
|
|
||||
websocket: |
|
||||
port: 8878 |
|
||||
|
|
||||
spring: |
spring: |
||||
redis: |
redis: |
||||
host: 127.0.0.1 |
host: 127.0.0.1 |
||||
port: 6379 |
port: 6379 |
||||
database: 1 |
database: 1 |
||||
|
|
||||
|
websocket: |
||||
|
enable: true |
||||
|
port: 8878 |
||||
|
|
||||
|
tcpsocket: |
||||
|
enable: false # 暂时不开启 |
||||
|
port: 8879 |
||||
Loading…
Reference in new issue