springboot+netty+websokcet 高并发推送,聊天等业务通知,单独拆出来的项目

压力测试

项目启动使用步骤

1.连接websocket    ws://127.0.0.1:1235

2.链接成功后发送注册消息    ws://127.0.0.1:1235 {"userId":"1","type":"REGISTER"}                   ws://127.0.0.1:1235 {"userId":"2","type":"REGISTER"}

3 用户2向指定用户1发送消息   {"fromUserId":"2","toUserId":"1","content":"消息来了","type":"SINGLE_SENDING"}                 

package com.zgyanglao.saas.common.netty.config;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.zgyanglao.saas.common.netty.handler.NettyWebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.*;

/**
 * @Author YangJiaBin
 * @Date  2019/11/15
 */
@Component
@Scope("singleton")
@Slf4j
public class ApplicationContext
{

    private final NettyWebSocketServer webSocketServer;
    /**
     * newFixedThreadPool和newSingleThreadExecutor:  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
     * newCachedThreadPool和newScheduledThreadPool:  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
     */
    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-netty_chat-runner-%d").build();
    ExecutorService executor = new ThreadPoolExecutor(Constant.THEAD_SIZE,Constant.THEAD_SIZE,0L,TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),namedThreadFactory);
    public ApplicationContext(NettyWebSocketServer webSocketServer) {
        this.webSocketServer = webSocketServer;
    }

    @PostConstruct
    public void init() {
        executor.submit(webSocketServer);
        log.info("netty线程已经开启");
    }


    /**
     * 描述:Tomcat服务器关闭前需要手动关闭Netty Websocket相关资源,否则会造成内存泄漏。
     *      1. 释放Netty Websocket相关连接;
     *      2. 关闭Netty Websocket服务器线程。(强行关闭,是否有必要?)
     */
    @SuppressWarnings("deprecation")
    @PreDestroy
    public void close() {
        log.info("正在释放Netty Websocket相关连接...");
        executor.shutdown();
    }

}

package com.zgyanglao.saas.common.netty.config;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;

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

/**
 * @Author: yangjiabin
 * @Date: 2020/11/17 15:41
 */
public class Constant implements Serializable {


    public static final String ID = "id";

    public static final Integer THEAD_SIZE = 10;

    public static Map<String, WebSocketServerHandshaker> webSocketHandshakerMap =
            new ConcurrentHashMap<String, WebSocketServerHandshaker>();


    public static Map<String, ChannelHandlerContext> onlineUserMap =
            new ConcurrentHashMap<>();

    public static void removeUser(ChannelHandlerContext channel) {
        onlineUserMap.entrySet().stream().filter(entry -> entry.getValue() == channel).forEach(entry -> onlineUserMap.remove(entry.getKey()));
    }

    public static void remove(ChannelHandlerContext channel) {
        for(Map.Entry<String,WebSocketServerHandshaker> entry:webSocketHandshakerMap.entrySet()){
            if(channel.channel().id().asLongText().equals(entry.getKey())){
                webSocketHandshakerMap.remove(entry.getKey());
            }
        }
    }

}
package com.zgyanglao.saas.common.netty.config;

import lombok.Builder;
import org.springframework.http.HttpStatus;

import java.util.HashMap;

/**
 * @Author YangJiaBin
 * @Date  2019/11/15
 */
@Builder
public class ResponseResult extends HashMap<String, Object> {

    private static final Integer ERROR_STATUS = -1;

    private static final Integer SUCCESS_STATUS = 200;

    private static final String SUCCESS_MSG = "ok";

    public ResponseResult() {
        super();
    }

    public ResponseResult(int code) {
        super();
        setStatus(code);
    }

    public ResponseResult(HttpStatus status) {
        super();
        setStatus(status.value());
        setMsg(status.getReasonPhrase());
    }

    public ResponseResult success() {
        put("msg", SUCCESS_MSG);
        put("status", SUCCESS_STATUS);
        return this;
    }

    public ResponseResult success(String msg) {
        put("msg", msg);
        put("status", SUCCESS_STATUS);
        return this;
    }

    public ResponseResult error(String msg) {
        put("msg", msg);
        put("status", ERROR_STATUS);
        return this;
    }

    public ResponseResult setData(String key, Object obj) {
        @SuppressWarnings("unchecked")
        HashMap<String, Object> data = (HashMap<String, Object>) get("data");
        if (data == null) {
            data = new HashMap<>();
            put("data", data);
        }
        data.put(key, obj);
        return this;
    }

    public ResponseResult setStatus(int status) {
        put("status", status);
        return this;
    }

    public ResponseResult setMsg(String msg) {
        put("msg", msg);
        return this;
    }

    public ResponseResult setValue(String key, Object val) {
        put(key, val);
        return this;
    }





}

package com.zgyanglao.saas.common.netty.config;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @Author YangJiaBin
 * @Date  2019/11/15
 */
@AllArgsConstructor
@Getter
public enum TypeEnums {
    REGISTER("注册"),
    SINGLE_SENDING("给单个人发送消息"),
	order_notice("通知成功消息");
    private String type;
}

package com.zgyanglao.saas.common.netty.handler;

import com.zgyanglao.saas.common.netty.config.Constant;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @Author YangJiaBin
 * @Date  2019/11/15
 * @Param 处理Http升级为WebSocket协议
 * @return
 **/
@Component
@Sharable
@Slf4j
public class NettyHttpRequestHandler extends SimpleChannelInboundHandler<Object>
{


    /**
     * 主要处理逻辑方法
     * @param context 处理上下文
     * @param msg 接收对象
     */
    @Override
    protected void channelRead0(ChannelHandlerContext context, Object msg) {
        // 如果是Http请求 需要进行协议升级
        if (msg instanceof FullHttpRequest) {
            handleHttpRequest(context,(FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            context.fireChannelRead(((WebSocketFrame) msg).retain());
        }

    }

    /**
     * Http协议和转换
     * @param context 处理上下文
     * @param request 消息请求
     */
    private void handleHttpRequest(ChannelHandlerContext context, FullHttpRequest request) {

        if (!request.decoderResult().isSuccess()) {
            sendHttpResponse(context, request, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }
        // 协议升级
        WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory("ws:/" + context.channel() + "/websocket", null, false);
        WebSocketServerHandshaker handsShaker = factory.newHandshaker(request);
        // 存储握手信息
        Constant.webSocketHandshakerMap.put(context.channel().id().asLongText(), handsShaker);

        //打印当前的连接数量
        log.info(" 链接数:"+Constant.webSocketHandshakerMap.size());

        if (handsShaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(context.channel());
        }else {
            // 表示握手成功
            handsShaker.handshake(context.channel(), request);
            log.info("Http-websocket握手协议升级成功啦");
        }
    }

    /**
     * 消息处理失败 发送一个失败请求 应答客户端
     * @param context 处理上下文
     * @param request 请求
     * @param defaultFullHttpResponse 默认的Http响应
     */
    private void sendHttpResponse(ChannelHandlerContext context, FullHttpRequest request, DefaultFullHttpResponse defaultFullHttpResponse) {
        if (defaultFullHttpResponse.status().code() != HttpResponseStatus.OK.code()) {
            ByteBuf buf = Unpooled.copiedBuffer(defaultFullHttpResponse.status().toString(), CharsetUtil.UTF_8);
            defaultFullHttpResponse.content().writeBytes(buf);
            buf.release();
        }
        // 如果长连接好存在 关闭长连接
        boolean keepLive = HttpUtil.isKeepAlive(request);
        ChannelFuture future = context.channel().writeAndFlush(request);
        if (!keepLive) {
            future.addListener(ChannelFutureListener.CLOSE);
        }

    }


    /**
     * 异常处理
     * @param ctx 处理上下文
     * @param cause 抛出异常
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

package com.zgyanglao.saas.common.netty.handler;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.springframework.stereotype.Component;

/**
 * @Author YangJiaBin
 * @Date  2019/11/15
 * @description:初始化ChannelChildHandler连接
 */
@Component
public class NettyWebsocketChildHandlerInitializer extends ChannelInitializer<SocketChannel>
{

    private final NettyWebSocketServerHandler webSocketServerHandler;

    private final NettyHttpRequestHandler httpRequestHandler;

    public NettyWebsocketChildHandlerInitializer(NettyWebSocketServerHandler webSocketServerHandler, NettyHttpRequestHandler httpRequestHandler) {
        this.webSocketServerHandler = webSocketServerHandler;
        this.httpRequestHandler = httpRequestHandler;
    }

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        // HTTP编解码器
        socketChannel.pipeline().addLast("http-codec", new HttpServerCodec());
        // HTTP头和body拼接成完整请求体
        socketChannel.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
        // 大文件传输策略
        socketChannel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
        socketChannel.pipeline().addLast("http-handler", httpRequestHandler);
        socketChannel.pipeline().addLast("websocket-handler", webSocketServerHandler);

    }
}

package com.zgyanglao.saas.common.netty.handler;


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.util.concurrent.Future;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @Author YangJiaBin
 * @Date  2019/11/15
 * @Param 使用Netty作为通信基础的Websocket
 * @return
 **/
@Slf4j
@Service
public class NettyWebSocketServer implements Runnable {

    private EventLoopGroup bossGroup = new NioEventLoopGroup();
    private EventLoopGroup workerGroup = new NioEventLoopGroup();
    private ServerBootstrap serverBootstrap = new ServerBootstrap();

    private static final int RECVBYTE_ALLOCATOR_SIZE = 592048;
    private static final Integer PORT = 1235;        //端口
    @Autowired
    private NettyWebsocketChildHandlerInitializer childChannelHandler;
    private ChannelFuture serverChannelFuture;

    NettyWebSocketServer(){}
    @Override
    public void run() {
        build();
    }

    /**
     * 启动NettyWebSocket
     */
    private void build() {
        Long beginTime = System.currentTimeMillis();
        serverBootstrap.group(bossGroup,workerGroup)
                // 指定是Nio通信服务
                .channel(NioServerSocketChannel.class)
                // TCP参数配置 握手字符串长度设置
                .option(ChannelOption.SO_BACKLOG,1024)
                // 设置TCP NO_DELAY 算法 尽量发送大文件包
                .option(ChannelOption.TCP_NODELAY,true)
                // 开启心跳模式
                .childOption(ChannelOption.SO_KEEPALIVE,true)
                // 配置固定长度接收缓存内存分配
                .childOption(ChannelOption.RCVBUF_ALLOCATOR,new FixedRecvByteBufAllocator(RECVBYTE_ALLOCATOR_SIZE))
                .childHandler(childChannelHandler);
        try {
            serverChannelFuture = serverBootstrap.bind(PORT).sync();
            Long endTime = System.currentTimeMillis();
            log.info("服务器启动完成,耗时:[{}]毫秒,已经在端口:[{}]进行阻塞等待",endTime - beginTime,PORT);
        } catch (InterruptedException e) {
            log.error(e.getMessage());
            // 优雅关闭连接
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
            e.printStackTrace();
        }
    }


    public void close() {
        serverChannelFuture.channel().close();
        Future<?> bossGroupFuture = bossGroup.shutdownGracefully();
        Future<?> workerGroupFuture = workerGroup.shutdownGracefully();
        try {
            bossGroupFuture.await();
            workerGroupFuture.await();
        }catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }

    public ChannelHandler getChildChannelHandler() {
        return childChannelHandler;
    }


}
package com.zgyanglao.saas.common.netty.handler;

import com.alibaba.fastjson.JSONObject;
import com.zgyanglao.saas.common.netty.config.Constant;
import com.zgyanglao.saas.common.netty.config.ResponseResult;
import com.zgyanglao.saas.common.netty.service.NettyService;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @Author YangJiaBin
 * @Date  2019/11/15
 * @description: 实际进行处理消息的Handler 发送消息
 */
@Slf4j
@Component
@Sharable
public class NettyWebSocketServerHandler extends SimpleChannelInboundHandler<WebSocketFrame>
{

    private final NettyService chatService;

    public NettyWebSocketServerHandler(NettyService chatService) {
        this.chatService = chatService;
    }

    /**
     * 读取连接消息并对消息进行处理
     * @param channelHandlerContext 处理上下文
     * @param webSocketFrame WebSocket组件
     */
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, WebSocketFrame webSocketFrame) {
        doHandler(channelHandlerContext,webSocketFrame);
    }

    private void doHandler(ChannelHandlerContext ctx, WebSocketFrame frame) {
        if (frame instanceof CloseWebSocketFrame) {
            WebSocketServerHandshaker handsShaker = Constant.webSocketHandshakerMap.get(ctx.channel().id().asLongText());
            if (null == handsShaker) {
                sendErrorMessage(ctx,"该用户已经离线或者不存在该连接");
            }else {
                handsShaker.close(ctx.channel(), ((CloseWebSocketFrame) frame).retain());
            }
            return;
        }
        // ping请求
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        if (!(frame instanceof TextWebSocketFrame)) {
            	sendErrorMessage(ctx,"不支持二进制文件");
         }
        String request = ((TextWebSocketFrame) frame).text();
        JSONObject params = null;
        try {
            params = JSONObject.parseObject(request);
            //System.out.println("收到服务器消息:[{}]"+params);
        }catch (Exception e){
            sendErrorMessage(ctx, "JSON字符串转换出错!");
           // log.error("参数转换异常");
        }
        if (null == params) {
            sendErrorMessage(ctx, "参数为空!");
            //log.warn("参数为空");
            return;
        }

        String messageType = (String) params.get("type");
        switch (messageType) {
            // 注册
			case "REGISTER":
				chatService.register(params,ctx);
				break;
            // 发送消息给单个人
            case "SINGLE_SENDING":
                chatService.sendOne(params, ctx);
                break;
            default:
                 chatService.typeError(ctx);
                break;
        }


    }

    /**
     * 出现不可抗拒因素发送错误消息给客户端
     * @param context 处理上下文
     * @param message 消息文字
     */
    public void sendErrorMessage(ChannelHandlerContext context,String message){
        String result = new ResponseResult().error(message).toString();
        context.channel().writeAndFlush(new TextWebSocketFrame(result));
    }

    /**
     * 客户端断开连接之后触发
     * @param ctx 处理和上下文
     * @throws Exception 捕获异常
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
		log.info("断开连接:"+ctx);
        chatService.remove(ctx);
    }

    /**
     * 出现移异常后触发
     * @param ctx 处理上下文
     * @param cause 异常类
     * @throws Exception 异常
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
package com.zgyanglao.saas.common.netty.service;

import com.alibaba.fastjson.JSONObject;
import io.netty.channel.ChannelHandlerContext;

/**
 * @Author YangJiaBin
 * @Date  2019/11/15
 * @description:领域服务-聊天服务
 */
public interface NettyService {

    /**
     * 用户向websocket注册
     * @param param 参数类型
     * @param ctx 处理上下午文
     */
    void register(JSONObject param, ChannelHandlerContext ctx);

    /**
     * 用户-用户之间相互之间发送消息
     * @param param 参数类型
     * @param ctx 处理上下午文
     */
    void sendOne(JSONObject param, ChannelHandlerContext ctx);


	/**
	 * 发送订单通知消息
	 */
	void sendOrder(String outTradeNo,String content);


    /**
     * 下线移除
     * @param ctx 处理上下文
     */
    void remove(ChannelHandlerContext ctx);

    /**
     * 不存在该类型业务
     * @param ctx 处理上下文
     */
    void typeError(ChannelHandlerContext ctx);


}

package com.zgyanglao.saas.common.netty.service;

import com.alibaba.fastjson.JSONObject;
import com.zgyanglao.saas.common.netty.config.Constant;
import com.zgyanglao.saas.common.netty.config.ResponseResult;
import com.zgyanglao.saas.common.netty.config.TypeEnums;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.text.MessageFormat;
import java.util.Map;

/**
 * @Author YangJiaBin
 * @Date  2019/11/15
 * @description:领域服务-聊天实现类
 */
@Service
@Slf4j
public class NttyServiceImpl implements NettyService {

	/**
     * 用户注册登录
     * @param param 参数类型
     * @param ctx 处理上下午文
     */
    @Override
    public void register(JSONObject param, ChannelHandlerContext ctx) {
		String userId = String.valueOf(param.get("userId"));
		Constant.onlineUserMap.put(userId, ctx);

		if(userId.isEmpty()){
			String responseJson = JSONObject
				.toJSONString(new ResponseResult().error("注册信息不能为空"));
			sendMessageToClient(ctx,responseJson);
		}
        //System.out.println("用户:[{}]进行登录注册"+userId);
        String registerResult = JSONObject.toJSONString(new ResponseResult().success().setData("type", TypeEnums.REGISTER.name()));
        // 发送消息给客户端
        log.info("注册信息:[{}]",registerResult);
         sendMessageToClient(ctx,registerResult);

		log.info(" 链接数:"+Constant.webSocketHandshakerMap.size());

		for(Map.Entry<String, WebSocketServerHandshaker> a:Constant.webSocketHandshakerMap.entrySet()){
			log.info("用户是"+a.getKey());
		}

		//查询用户未读消息 推送等

	}



    /**
     * 用户-用户之间相互之间发送消息
     * @param param 参数类型
     * @param ctx 处理上下午文
	 * fromUserId发送着
	 * toUserId接收者
	 * content内容
     */
    @Override
    public void sendOne(JSONObject param, ChannelHandlerContext ctx) {

		String fromUserId = String.valueOf(param.get("fromUserId"));
		String toUserId = String.valueOf(param.get("toUserId"));
		String content = String.valueOf(param.get("content"));
		param.put("isRead", 0);
		ChannelHandlerContext toUserCtx = Constant.onlineUserMap.get(toUserId);
		if (toUserCtx == null) {
			String responseJson = JSONObject
					.toJSONString(new ResponseResult().error(MessageFormat.format("userId为 {0} 的用户没有登录!", toUserId)));
			sendMessageToClient(ctx, responseJson);
			//没登陆 处理存储未读消息逻辑  上线时推送
		} else {
			String responseResult = JSONObject.toJSONString(
					new ResponseResult().success().setData("fromUserId", fromUserId).setData("content", content)
							.setData("type", TypeEnums.SINGLE_SENDING));
			sendMessageToClient(toUserCtx, responseResult);
			log.info("发送消息给:[{}]" + toUserCtx + "    消息内容为:[{}]" + responseResult);
		}
	}

	/**
	 *  场景: 如支付成功通知。  1.前端用订单id链接 , 2.后台需要向订单号推送成功消息,调接口单向通知
	 * @param outTradeNo 订单号
	 * @param content 内容
	 */
	@Override public void sendOrder(String outTradeNo,String content)
	{
		ChannelHandlerContext toUserCtx = Constant.onlineUserMap.get(outTradeNo);
		if(toUserCtx!=null){
			String responseResult = JSONObject.toJSONString(
				new ResponseResult().success().setData("data", content)
					.setData("type", TypeEnums.order_notice).setMsg("success").setStatus(0));
			sendMessageToClient(toUserCtx, responseResult);
			//发送成功后 移除该订单通讯
			remove((toUserCtx));
		}

	}


	/**
     * 下线移除
     * @param ctx 处理上下文
     */
    @Override
    public void remove(ChannelHandlerContext ctx) {
		Constant.remove(ctx);//移除握手的连接信息
		Constant.removeUser(ctx);//移除注册用户的信息
	}

    /**
     * 不存在该类型业务
     * @param ctx 处理上下文
     */
    @Override
    public void typeError(ChannelHandlerContext ctx) {


    }

    //发送消息
    private void sendMessageToClient(ChannelHandlerContext context, String msg) {
        // 此处一定要注意 不是直接使用context.writeAndFlush()方法
        context.channel().writeAndFlush(new TextWebSocketFrame(msg));
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

q联:1120972968

感谢老铁资质

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值