1.maven引入
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.8.Final</version>
</dependency>
2.服务启动类优化
@Slf4j
@Service
public class NettyServer implements Runnable {
private int port;
private Channel channel;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
public NettyServer() {
this.port = 8000;
}
public NettyServer(int port) {
this.port = port;
}
@PostConstruct
public void init(){
RedisUtil.deleteKeys(Global.KEY);
Map<String,String> uploadMap= PropertyUtil.getPropertyMap(Const.REDIS_CONFIG);
String nettyPort =uploadMap.get("netty.port");
if(BaseUtil.isNotEmpty(nettyPort)){
new Thread(new NettyServer(Integer.parseInt(nettyPort))).start();
}else{
new Thread(new NettyServer()).start();
}
}
@PreDestroy
public void destory() {
System.out.println("destroy server resources");
if (null == channel) {
System.out.println("server channel is null");
}else{
channel.closeFuture().syncUninterruptibly();
}
try{
bossGroup.shutdownGracefully();
bossGroup = null;
}catch (Exception e){
}
try{
workerGroup.shutdownGracefully();
workerGroup = null;
}catch (Exception e){
}
}
@Override
public void run() {
bossGroup = new NioEventLoopGroup();
workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b =
new ServerBootstrap();
b.option(ChannelOption.SO_REUSEADDR, true).option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_RCVBUF, 1024 * 128)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
b.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.SO_KEEPALIVE, true);
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel e) throws Exception {
//Socket 连接心跳检测
e.pipeline().addLast("idleStateHandler", new IdleStateHandler(60, 0, 0));
e.pipeline().addLast(new HttpServerCodec());
e.pipeline().addLast(new ChunkedWriteHandler());
// HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息
e.pipeline().addLast(new HttpObjectAggregator(65536));
// ChunkedWriteHandler:向客户端发送HTML5文件
e.pipeline().addLast(new ChunkedWriteHandler());
// 在管道中添加我们自己的接收数据实现方法
e.pipeline().addLast(new MyWebSocketServerHandler(port));
e.pipeline().addLast(new WebSocketServerProtocolHandler("/ws",null,true,65536*10));
}
});
// 服务器绑定端口监听
ChannelFuture f = b.bind(port).sync();
if(f.isSuccess()){
System.out.println("Netty 服务已启动"+port);
}
// 监听服务器关闭监听
channel = f.channel();
channel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
PropertyUtil读取的是配置文件,自行根据项目情况进行修改
public class Global {
//所有连接的用户-后续对接
public static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
//应用对应所有的链接
public static Map<Long,Map<String, ChannelGroup>> appUserMap = new HashMap<>();
public static Map<String, Channel> userMap = new HashMap<>();//id 对应 用户
public static final String KEY = "USER_KEY";
}
根据情况设计存储的map.方便随时通信
@Component
@Slf4j
@ChannelHandler.Sharable
public class MyWebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame > {
private int port;
public MyWebSocketServerHandler() {
}
public MyWebSocketServerHandler(int port) {
this.port = port;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " channelRead " );
Channel channel = ctx.channel();
//首次连接是FullHttpRequest,处理参数
if (null != msg && msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.getUri();
Map paramMap=getUrlParams(uri);
//如果url包含参数,需要处理
if(uri.contains("?")){
String newUri=uri.substring(0,uri.indexOf("?"));
request.setUri(newUri);
}
String userSig = (String) paramMap.get("userSig");
try{
//鉴权
JSONObject object = UserSignTokenUtil.validSign(userSig);
UserChannelUtil.handshake(object,channel);
}catch (NonePrintException e){
super.channelRead(ctx, msg);
CloseWebSocketFrame closeFrame = new CloseWebSocketFrame(1008, "服务端错误: " + e.getErrMsg());
ctx.writeAndFlush(closeFrame).addListener(ChannelFutureListener.CLOSE);
return;
}
}else if(msg instanceof TextWebSocketFrame){
//正常的TEXT消息类型
TextWebSocketFrame frame=(TextWebSocketFrame)msg;
System.out.println("客户端收到服务器数据:" +frame.text());
// sendAllMessage(frame.text());
}
super.channelRead(ctx, msg);
}
private void sendAllMessage(String message){
//收到信息后,群发给所有channel
Global.group.writeAndFlush( new TextWebSocketFrame(message));
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " channelRead0 " );
String jsonString = msg.text();
try{
SocketRequest socketRequest = JSON.parseObject(jsonString, SocketRequest.class);
if(BaseUtil.isEmpty(socketRequest.getM())){
return;
}
SocketUtil.handleMessage(socketRequest,ctx);
}catch (Exception e){
log.error("websocket执行错误:{}",e.getMessage());
}
//ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间" + LocalDateTime.now() + " " + msg.text()));
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " channelRegistered " );
System.out.println("与客户端建立连接,通道开启!");
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " channelUnregistered " );
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
UserChannelUtil.remove(channel);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " exceptionCaught :" + cause.getMessage() );
super.exceptionCaught(ctx, cause);
}
private static Map getUrlParams(String url){
Map<String,String> map = new HashMap<>();
url = url.replace("?",";");
if (!url.contains(";")){
return map;
}
if (url.split(";").length > 0){
String[] arr = url.split(";")[1].split("&");
for (String s : arr){
String key = s.split("=")[0];
String value = s.split("=")[1];
map.put(key,value);
}
return map;
}else{
return map;
}
}
}
鉴权逻辑不需要可以直接忽略,如果需要可私信
3.统一请求方式
@Data
public class SocketRequest {
//请求方法
private String m;
//请求参数
private String data;
}
4.统一返回格式
@Data
public class SocketResult {
//方法名称
private String m;
//错误信息
private String errMsg;
//错误code 0 正常
private Integer errCode = 0;
//数据
private Object data;
}
public class SocketResultUtil {
/**
* 成功返回数据
* @param o
* @param m
* @return
*/
public static SocketResult success(Object o, String m) {
SocketResult result = new SocketResult();
result.setM(m);
result.setErrMsg("");
result.setErrCode(0);
result.setData(o);
return result;
}
/**
* 失败返回数据
* @param code
* @param desc
* @param m 请求方法
* @return
*/
public static SocketResult fail(Integer code, String desc,String m,Object o) {
SocketResult result = new SocketResult();
result.setM(m);
result.setErrMsg(desc);
result.setErrCode(code);
result.setData(o);
return result;
}
public static ChannelFuture sendFail(ChannelHandlerContext ctx, SocketError socketError, String m) {
return sendFail(ctx,socketError.getCode(),socketError.getDesc(),m,null);
}
public static ChannelFuture sendFail(ChannelHandlerContext ctx,Integer code, String desc, String m,Object o) {
ChannelFuture future = ctx.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(fail(code,desc,m,o))));
return future;
}
public static void sendParamFail(ChannelHandlerContext ctx,String desc, String m,Object o) {
ctx.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(fail(SocketError.PARAM_ERROR.getCode(),SocketError.PARAM_ERROR.getDesc()+desc, m,o))));
}
}
5.消息处理demo
/**
* 消息处理
* @param socketRequest
* @param ctx
*/
public static void handleMessage(SocketRequest socketRequest, ChannelHandlerContext ctx) {
if(!SocketParamCheck.check(socketRequest)){
SocketResultUtil.sendFail(ctx, SocketError.PARAM_ERROR,"");
return;
}
switch (socketRequest.getM()){
//心跳
case "ping":
MessageSender.send(ctx.channel(),CmdEnums.heartbeat, TimeUtil.getUnixTime());
break;
//根据不同方法处理消息结果返回
case "sendMessage":
//这里处理消息
break;
default:
SocketResultUtil.sendFail(ctx, SocketError.UN_KNOW_M,socketRequest.getM());
break;
}
}
6.发送方法
public static void send(Channel channel, String m,Object o) {
channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(SocketResultUtil.success(o,m))));
}
/**
* 发送消息到用户
* @param userId
* @param cmdEnums
* @param object
*/
public static void sendToUser(String userId, CmdEnums cmdEnums, Object object) {
Channel channel = UserChannelUtil.getUserChannel(userId);
if(UserChannelUtil.checkChannel(channel)){
send(channel,cmdEnums,object);
}
}
注意:在分布式环境中,Global
类中的连接管理需替换为Redis等分布式存储,可通过Channel.attr()
绑定用户信息实现集群管理