疯狂创客圈 Java 高并发【 亿级流量聊天室实战】实战系列 【博客园总入口 】
架构师成长+面试必备之 高并发基础书籍 【Netty Zookeeper Redis 高并发实战 】
Netty是由Jboss提供的一款著名的开源框架,常用于搭建 RPC中的TCP服务器、Websocket服务器,甚至是类似Tomcat的Web服务器,反正就是各种网络服务器,在处理高并发的项目中,有奇用!功能丰富且性能良好,基于Java中NIO的二次封装,具有比原生NIO更好更稳健的体验。
关于Netty 原理,请参见 《Netty Zookeeper Redis 高并发实战》 一书
很多项目,都需要基于 Websocket 协议做在线客服、在线推送、在线聊天,虽然 Tomcat 内置支持 Websocket 协议,但是由于 Tomcat 的吞吐量、连接数都很低,作为测试是可以的。在生产环境,一定需要使用高吞吐量、高连接数的 Netty 服务器进行替代。
之所以 Netty 性能高,因为其使用的是 Reactor 反应器模式。关于反应器模式原理,请参见 《Netty Zookeeper Redis 高并发实战》 一书。
聊天过程gif 演示:
Netty搭建的服务器基本上都是差不多的写法:
绑定主线程组和工作线程组,这部分对应架构图中的事件循环组。其原理,,请参见 《Netty Zookeeper Redis 高并发实战》 一书。
重点就是ChannelInitializer的配置,以异步的方式启动,最后是结束的时候关闭线程组。
/**
* 启动即时通讯服务器
*/
public void start()
{
final WebSocketServer webSocketServer = new WebSocketServer();
ChannelFuture channelFuture = null;
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new ChatServerInitializer());
InetSocketAddress address = new InetSocketAddress(9999);
channelFuture = bootstrap.bind(address);
channelFuture.syncUninterruptibly();
channel = channelFuture.channel();
// 返回与当前Java应用程序关联的运行时对象
Runtime.getRuntime().addShutdownHook(new Thread()
{
@Override
public void run()
{
webSocketServer.stop();
}
});
channelFuture.channel().closeFuture().syncUninterruptibly();
}
/**
* 内部类
*/
class ChatServerInitializer extends ChannelInitializer
{
private static final int READ_IDLE_TIME_OUT = 60; // 读超时 s
private static final int WRITE_IDLE_TIME_OUT = 0;// 写超时
private static final int ALL_IDLE_TIME_OUT = 0; // 所有超时
@Override
protected void initChannel(Channel ch) throws Exception
{
ChannelPipeline pipeline = ch.pipeline();
// HTTP请求的解码和编码
pipeline.addLast(new HttpServerCodec());
// 主要用于处理大数据流,比如一个1G大小的文件如果你直接传输肯定会撑暴jvm内存的; 增加之后就不用考虑这个问题了
pipeline.addLast(new ChunkedWriteHandler());
// 把多个消息转换为一个单一的FullHttpRequest或是FullHttpResponse,
// 原因是HTTP解码器会在每个HTTP消息中生成多个消息对象HttpRequest/HttpResponse,HttpContent,LastHttpContent
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
// WebSocket数据压缩
pipeline.addLast(new WebSocketServerCompressionHandler());
// 协议包长度限制
pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null, true, 10 * 1024));
// //当连接在60秒内没有接收到消息时,进会触发一个 IdleStateEvent 事件,被 HeartbeatHandler 的 userEventTriggered 方法处理
pipeline.addLast(new IdleStateHandler(READ_IDLE_TIME_OUT, WRITE_IDLE_TIME_OUT, ALL_IDLE_TIME_OUT, TimeUnit.SECONDS));
pipeline.addLast(new TextWebSocketFrameHandler());
}
}
/**
* Created by 尼恩 @ 疯狂创客圈
*
* WebSocket 帧:WebSocket 以帧的方式传输数据,每一帧代表消息的一部分。一个完整的消息可能会包含许多帧
*/
@Slf4j
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler
{
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception
{
//增加消息的引用计数(保留消息),并将他写到 ChannelGroup 中所有已经连接的客户端
ServerSession session = ServerSession.getSession(ctx);
Map result = ChatProcesser.inst().onMessage(msg.text(), session);
if (result != null && null!=result.get("type"))
{
switch (result.get("type"))
{
case "msg":
SessionMap.inst().sendToOthers(result, session);
break;
case "init":
SessionMap.inst().addSession(result, session);
break;
}
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception
{
//是否握手成功,升级为 Websocket 协议
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE)
{
// 握手成功,移除 HttpRequestHandler,因此将不会接收到任何消息
// 并把握手成功的 Channel 加入到 ChannelGroup 中
ServerSession session = new ServerSession(ctx.channel());
String echo = ChatProcesser.inst().onOpen(session);
SessionMap.inst().sendMsg(ctx, echo);
} else if (evt instanceof IdleStateEvent)
{
IdleStateEvent stateEvent = (IdleStateEvent) evt;
if (stateEvent.state() == IdleState.READER_IDLE)
{
ServerSession session = ServerSession.getSession(ctx);
SessionMap.inst().remove(session);
session.processError(null);
}
} else
{
super.userEventTriggered(ctx, evt);
}
}
}
下面是用websocket做聊天室的逻辑:
使用 Json 传递实体消息;
ServerSession 存储了每个会话,保存对 Channel和 User,使用User 表示连接上来用户
前端要求填入用户和房间(群组)后,模拟登录,并返回用户列表。进入后可以发送群组消息。
package com.crazymaker.websocket.processer;
import com.crazymaker.websocket.Model.User;
import com.crazymaker.websocket.session.ServerSession;
import com.crazymaker.websocket.session.SessionMap;
import com.crazymaker.websocket.util.JsonUtil;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 业务处理器
* Created by 尼恩 @ 疯狂创客圈
*/
@Slf4j
public class ChatProcesser
{
private static final Logger logger = LoggerFactory.getLogger(ChatProcesser.class);
/**
* 单例
*/
private static ChatProcesser singleInstance = new ChatProcesser();
public static ChatProcesser inst()
{
return singleInstance;
}
/**
* 连接建立成功调用的方法
*
* @param s 会话
*/
public String onOpen(ServerSession s) throws IOException
{
Map result = new HashMap<>();
result.put("type", "bing");
result.put("sendUser", "系统消息");
result.put("id", s.getId());
String json = JsonUtil.pojoToJson(result);
return json;
}
/**
* 连接关闭调用的方法
*/
public String onClose(ServerSession s)
{
User user = s.getUser();
if (user != null)
{
String nick = user.getNickname();
Map result = new HashMap<>();
result.put("type", "init");
result.put("msg", nick + "离开房间");
result.put("sendUser", "系统消息");
String json = JsonUtil.pojoToJson(result);
return json;
}
return null;
}
/**
* 收到客户端消息后调用的方法
*
* @param message 消息内容
* @param session 会哈
*/
public Map onMessage(String message, ServerSession session)
{
TypeToken typeToken = new TypeToken>()
{
};
Map map = JsonUtil.jsonToPojo(message, typeToken);
Map result = new HashMap<>();
User user = null;
switch (map.get("type"))
{
case "msg":
user = session.getUser();
result.put("type", "msg");
result.put("msg", map.get("msg"));
result.put("sendUser", user.getNickname());
break;
case "init":
String room = map.get("room");
session.setGroup(room);
String nick = map.get("nick");
user = new User(session.getId(), nick);
session.setUser(user);
result.put("type", "init");
result.put("msg", nick + "成功加入房间");
result.put("sendUser", "系统消息");
break;
case "ping":
break;
}
return result;
}
/**
* 连接发生错误时的调用方法
*
* @param session 会话
* @param error 异常
*/
public String onError(ServerSession session, Throwable error)
{
//捕捉异常信息
if (null != error)
{
log.error(error.getMessage());
}
User user = session.getUser();
if (user == null)
{
return null;
}
String nick = user.getNickname();
Map result = new HashMap<>();
result.put("type", "init");
result.put("msg", nick + "离开房间");
result.put("sendUser", "系统消息");
String json = JsonUtil.pojoToJson(result);
return json;
}
}
源码网址: Java 高并发研习社群 【博客园 总入口 】
疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备