压力测试
项目启动使用步骤
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));
}
}