目录
- 1.目的
- 2.Netty是什么
-
- 2.1.Netty和Tomcat的区别
- 2.2.Netty为什么流行
-
- 2.2.1并发高
- 2.2.2.传输快
- 2.2.3.封装好
- 3.基于Netty实现WebSocket聊天室
-
- 3.1创建simple_webchat项目
- 3.2编写代码
- 4.结果
1.目的
2.Netty是什么
- Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
2.1.Netty和Tomcat的区别
- Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的。Netty能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能。
2.2.Netty为什么流行
2.2.1并发高
- 对比于BIO(Blocking I/O,阻塞IO),Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架。当一个连接建立之后,NIO有两个步骤要做(接收完客户端发过来的全部数据、处理完请求业务之后返回response给客户端),NIO和BIO的区别主要是在第一步。在BIO中,等待客户端发数据这个过程是阻塞的,一个线程只能处理一个请求而最大线程数是有限的,BIO不能支持高并发。
- 而NIO中,当一个Socket建立好之后,Thread并不会阻塞去接收这个Socket,而是将这个请求交给Selector,Selector会不断的去遍历所有的Socket,一旦有一个Socket建立完成,他会通知Thread,然后Thread处理完数据再返回给客户端——这个过程是不阻塞的。
2.2.2.传输快
- 依赖NIO的一个特性——零拷贝。Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点。针对这种情况,当Netty需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。
2.2.3.封装好
3.基于Netty实现WebSocket聊天室
3.1创建simple_webchat项目
3.2编写代码
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.23</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
</dependencies>
public interface Server {
void start();
void shutdown();
}
public abstract class BaseServer implements Server{
protected Logger logger = LoggerFactory.getLogger(getClass());
protected String host = "localhost";
protected int port = 8099;
protected DefaultEventLoopGroup defLoopGroup;
protected NioEventLoopGroup bossGroup;
protected NioEventLoopGroup workGroup;
protected NioServerSocketChannel ssch;
protected ChannelFuture cf;
protected ServerBootstrap b;
public void init(){
defLoopGroup = new DefaultEventLoopGroup(8, new ThreadFactory() {
private AtomicInteger index = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "DEFAULTEVENTLOOPGROUP_" + index.incrementAndGet());
}
});
bossGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
private AtomicInteger index = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "BOSS_" + index.incrementAndGet());
}
});
workGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() * 10, new ThreadFactory() {
private AtomicInteger index = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "WORK_" + index.incrementAndGet());
}
});
b = new ServerBootstrap();
}
@Override
public void shutdown() {
if (defLoopGroup != null) {
defLoopGroup.shutdownGracefully();
}
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public class UserInfo {
private static AtomicInteger uidGener = new AtomicInteger(1000);
private boolean isAuth = false;
private long time = 0;
private int userId;
private String nick;
private String addr;
private Channel channel;
public void setUserId() {
this.userId = uidGener.incrementAndGet();
}
public class MessageHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static final Logger logger = LoggerFactory.getLogger(MessageHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame)
throws Exception {
UserInfo userInfo = UserInfoManager.getUserInfo(ctx.channel());
if (userInfo != null && userInfo.isAuth()) {
JSONObject json = JSONObject.parseObject(frame.text());
UserInfoManager.broadcastMess(userInfo.getUserId(), userInfo.getNick(), json.getString("mess"));
}
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
UserInfoManager.removeChannel(ctx.channel());
UserInfoManager.broadCastInfo(ChatCode.SYS_USER_COUNT,UserInfoManager.getAuthUserCount());
super.channelUnregistered(ctx);
}
}
public class UserAuthHandler extends SimpleChannelInboundHandler<Object> {
private static final Logger logger = LoggerFactory.getLogger(UserAuthHandler.class);
private WebSocketServerHandshaker handshaker;
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) msg);
} else if (msg instanceof WebSocketFrame) {
handleWebSocket(ctx, (WebSocketFrame) msg);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent evnet = (IdleStateEvent) evt;
if (evnet.state().equals(IdleState.READER_IDLE)) {
final String remoteAddress = NettyUtil.parseChannelRemoteAddr(ctx.channel());
logger.warn("NETTY SERVER PIPELINE: IDLE exception [{}]", remoteAddress);
UserInfoManager.removeChannel(ctx.channel());
UserInfoManager.broadCastInfo(ChatCode.SYS_USER_COUNT,UserInfoManager.getAuthUserCount());
}
}
ctx.fireUserEventTriggered(evt);
}
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
if (!request.decoderResult().isSuccess() || !"websocket".equals(request.headers().get("Upgrade"))) {
logger.warn("protobuf don't support websocket");
ctx.channel().close();
return;
}
WebSocketServerHandshakerFactory handshakerFactory = new WebSocketServerHandshakerFactory(
Constants.WEBSOCKET_URL, null, true);
handshaker = handshakerFactory.newHandshaker(request);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), request);
UserInfo userInfo = new UserInfo();
userInfo.setAddr(NettyUtil.parseChannelRemoteAddr(ctx.channel()));
UserInfoManager.addChannel(ctx.channel());
}
}
private void handleWebSocket(ChannelHandlerContext ctx, WebSocketFrame frame) {
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
UserInfoManager.removeChannel(ctx.channel());
return;
}
if (frame instanceof PingWebSocketFrame) {
logger.info("ping message:{}", frame.content().retain());
ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
return;
}
if (frame instanceof PongWebSocketFrame) {
logger.info("pong message:{}", frame.content().retain());
ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
return;
}
if (!(frame instanceof TextWebSocketFrame)) {
throw new UnsupportedOperationException(frame.getClass().getName() + " frame type not supported");
}
String message = ((TextWebSocketFrame) frame).text();
JSONObject json = JSONObject.parseObject(message);
int code = json.getInteger("code");
Channel channel = ctx.channel();
switch (code) {
case ChatCode.PING_CODE:
case ChatCode.PONG_CODE:
UserInfoManager.updateUserTime(channel);
logger.info("receive pong message, address: {}", NettyUtil.parseChannelRemoteAddr(channel));
return;
case ChatCode.AUTH_CODE:
boolean isSuccess = UserInfoManager.saveUser(channel, json.getString("nick"));
UserInfoManager.sendInfo(channel,ChatCode.SYS_AUTH_STATE,isSuccess);
if (isSuccess) {
UserInfoManager.broadCastInfo(ChatCode.SYS_USER_COUNT,UserInfoManager.getAuthUserCount());
}
return;
case ChatCode.MESS_CODE:
break;
default:
logger.warn("The code [{}] can't be auth!!!", code);
return;
}
ctx.fireChannelRead(frame.retain());
}
}
public class UserInfoManager {
private static final Logger logger = LoggerFactory.getLogger(UserInfoManager.class);
private static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true);
private static ConcurrentMap<Channel, UserInfo> userInfos = new ConcurrentHashMap<>();
private static AtomicInteger userCount = new AtomicInteger(0);
public static void addChannel(Channel channel) {
String remoteAddr = NettyUtil.parseChannelRemoteAddr(channel);
if (!channel.isActive()) {
logger.error("channel is not active, address: {}", remoteAddr);
}
UserInfo userInfo = new UserInfo();
userInfo.setAddr(remoteAddr);
userInfo.setChannel(channel);
userInfo.setTime(System.currentTimeMillis());
userInfos.put(channel, userInfo);
}
public static boolean saveUser(Channel channel, String nick) {
UserInfo userInfo = userInfos.get(channel);
if (userInfo == null) {
return false;
}
if (!channel.isActive()) {
logger.error("channel is not active, address: {}, nick: {}", userInfo.getAddr(), nick);
return false;
}
userCount.incrementAndGet();
userInfo.setNick(nick);
userInfo.setAuth(true);
userInfo.setUserId();
userInfo.setTime(System.currentTimeMillis());
return true;
}
public static void removeChannel(Channel channel) {
try {
logger.warn("channel will be remove, address is :{}", NettyUtil.parseChannelRemoteAddr(channel));
rwLock.writeLock().lock();
channel.close();
UserInfo userInfo = userInfos.get(channel);
if (userInfo != null) {
UserInfo tmp = userInfos.remove(channel);
if (tmp != null && tmp.isAuth()) {
userCount.decrementAndGet();
}
}
} finally {
rwLock.writeLock().unlock();
}
}
public static void broadcastMess(int uid, String nick, String message) {
if (!BlankUtil.isBlank(message)) {
try {
rwLock.readLock().lock();
Set<Channel> keySet = userInfos.keySet();
for (Channel ch : keySet) {
UserInfo userInfo = userInfos.get(ch);
if (userInfo == null || !userInfo.isAuth()) continue;
ch.writeAndFlush(new TextWebSocketFrame(ChatProto.buildMessProto(uid, nick, message)));
}
} finally {
rwLock.readLock().unlock();
}
}
}
public static void broadCastInfo(int code, Object mess) {
try {
rwLock.readLock().lock();
Set<Channel> keySet = userInfos.keySet();
for (Channel ch : keySet) {
UserInfo userInfo = userInfos.get(ch);
if (userInfo == null || !userInfo.isAuth()) continue;
ch.writeAndFlush(new TextWebSocketFrame(ChatProto.buildSystProto(code, mess)));
}
} finally {
rwLock.readLock().unlock();
}
}
public static void broadCastPing() {
try {
rwLock.readLock().lock();
logger.info("broadCastPing userCount: {}", userCount.intValue());
Set<Channel> keySet = userInfos.keySet();
for (Channel ch : keySet) {
UserInfo userInfo = userInfos.get(ch);
if (userInfo == null || !userInfo.isAuth()) continue;
ch.writeAndFlush(new TextWebSocketFrame(ChatProto.buildPingProto()));
}
} finally {
rwLock.readLock().unlock();
}
}
public static void sendInfo(Channel channel, int code, Object mess) {
channel.writeAndFlush(new TextWebSocketFrame(ChatProto.buildSystProto(code, mess)));
}
public static void sendPong(Channel channel) {
channel.writeAndFlush(new TextWebSocketFrame(ChatProto.buildPongProto()));
}
public static void scanNotActiveChannel() {
Set<Channel> keySet = userInfos.keySet();
for (Channel ch : keySet) {
UserInfo userInfo = userInfos.get(ch);
if (userInfo == null) continue;
if (!ch.isOpen() || !ch.isActive() || (!userInfo.isAuth() &&
(System.currentTimeMillis() - userInfo.getTime()) > 10000)) {
removeChannel(ch);
}
}
}
public static UserInfo getUserInfo(Channel channel) {
return userInfos.get(channel);
}
public static ConcurrentMap<Channel, UserInfo> getUserInfos() {
return userInfos;
}
public static int getAuthUserCount() {
return userCount.get();
}
public static void updateUserTime(Channel channel) {
UserInfo userInfo = getUserInfo(channel);
if (userInfo != null) {
userInfo.setTime(System.currentTimeMillis());
}
}
}
public class ChatCode {
public static final int PING_CODE = 10015;
public static final int PONG_CODE = 10016;
public static final int AUTH_CODE = 10000;
public static final int MESS_CODE = 10086;
public static final int SYS_USER_COUNT = 20001;
public static final int SYS_AUTH_STATE = 20002;
public static final int SYS_OTHER_INFO = 20003;
}
public class ChatProto {
public static final int PING_PROTO = 1 << 8 | 220;
public static final int PONG_PROTO = 2 << 8 | 220;
public static final int SYST_PROTO = 3 << 8 | 220;
public static final int EROR_PROTO = 4 << 8 | 220;
public static final int AUTH_PROTO = 5 << 8 | 220;
public static final int MESS_PROTO = 6 << 8 | 220;
private int version = 1;
private int uri;
private String body;
private Map<String,Object> extend = new HashMap<>();
public ChatProto(int head, String body) {
this.uri = head;
this.body = body;
}
public static String buildPingProto() {
return buildProto(PING_PROTO, null);
}
public static String buildPongProto() {
return buildProto(PONG_PROTO, null);
}
public static String buildSystProto(int code, Object mess) {
ChatProto chatProto = new ChatProto(SYST_PROTO, null);
chatProto.extend.put("code", code);
chatProto.extend.put("mess", mess);
return JSONObject.toJSONString(chatProto);
}
public static String buildAuthProto(boolean isSuccess) {
ChatProto chatProto = new ChatProto(AUTH_PROTO, null);
chatProto.extend.put("isSuccess", isSuccess);
return JSONObject.toJSONString(chatProto);
}
public static String buildErorProto(int code,String mess) {
ChatProto chatProto = new ChatProto(EROR_PROTO, null);
chatProto.extend.put("code", code);
chatProto.extend.put("mess", mess);
return JSONObject.toJSONString(chatProto);
}
public static String buildMessProto(int uid,String nick, String mess) {
ChatProto chatProto = new ChatProto(MESS_PROTO, mess);
chatProto.extend.put("uid", uid);
chatProto.extend.put("nick", nick);
chatProto.extend.put("time", DateTimeUtil.getCurrentTime());
return JSONObject.toJSONString(chatProto);
}
public static String buildProto(int head, String body) {
ChatProto chatProto = new ChatProto(head, body);
return JSONObject.toJSONString(chatProto);
}
public int getUri() {
return uri;
}
public void setUri(int uri) {
this.uri = uri;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public Map<String, Object> getExtend() {
return extend;
}
public void setExtend(Map<String, Object> extend) {
this.extend = extend;
}
}
public class SimpleWebChatServer extends BaseServer {
private ScheduledExecutorService executorService;
public SimpleWebChatServer(int port) {
this.port = port;
executorService = Executors.newScheduledThreadPool(2);
}
@Override
public void start() {
b.group(bossGroup, workGroup);
b.channel(NioServerSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true);
b.option(ChannelOption.TCP_NODELAY, true);
b.option(ChannelOption.SO_BACKLOG, 1024);
b.localAddress(new InetSocketAddress(port));
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(defLoopGroup,
new HttpServerCodec(),
new HttpObjectAggregator(65536),
new ChunkedWriteHandler(),
new IdleStateHandler(60, 0, 0),
new UserAuthHandler(),
new MessageHandler()
);
}
});
try {
cf = b.bind().sync();
InetSocketAddress addr = (InetSocketAddress) cf.channel().localAddress();
logger.info("WebSocketServer start success, port is:{}", addr.getPort());
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
logger.info("scanNotActiveChannel --------");
UserInfoManager.scanNotActiveChannel();
}
}, 3, 60, TimeUnit.SECONDS);
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
UserInfoManager.broadCastPing();
}
}, 3, 50, TimeUnit.SECONDS);
} catch (InterruptedException e) {
logger.error("WebSocketServer start fail,", e);
}
}
@Override
public void shutdown() {
if (executorService != null) {
executorService.shutdown();
}
super.shutdown();
}
}
public class BlankUtil {
public static boolean isBlank(final String str) {
return (str == null) || (str.trim().length() <= 0);
}
public static boolean isBlank(final Character cha) {
return (cha == null) || cha.equals(' ');
}
public static boolean isBlank(final Object obj) {
return (obj == null);
}
public static boolean isBlank(final Object[] objs) {
return (objs == null) || (objs.length <= 0);
}
public static boolean isBlank(final Collection<?> obj) {
return (obj == null) || (obj.size() <= 0);
}
public static boolean isBlank(final Set<?> obj) {
return (obj == null) || (obj.size() <= 0);
}
public static boolean isBlank(Integer i) {
return i == null || i < 1;
}
public static boolean isBlank(final Serializable obj) {
return obj == null;
}
public static boolean isBlank(final Map<?, ?> obj) {
return (obj == null) || (obj.size() <= 0);
}
}
public class Constants {
public static String DEFAULT_HOST = "localhost";
public static int DEFAULT_PORT = 9688;
public static String WEBSOCKET_URL = "ws://localhost:8099/websocket";
}
public class DateTimeUtil {
private static final Logger logger = LoggerFactory.getLogger(DateTimeUtil.class);
private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";
private static final String DEFAULT_TIME_PATTERN = "HH:mm:ss";
public static String getTodayStr(){
SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd");
return sdf.format(new Date());
}
public static String getTodayStr2(){
SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd");
return sdf.format(new Date());
}
public static String getCurrentTime(){
return getCurrentTime(DEFAULT_TIME_PATTERN);
}
public static String getCurrentDateTime(){
return getCurrentTime(DEFAULT_DATE_PATTERN);
}
public static String getCurrentTime(String format){
SimpleDateFormat sdf = new SimpleDateFormat(format);
Timestamp timestamp = new Timestamp(System.currentTimeMillis());
return sdf.format(timestamp);
}
public static String getCurrentMonth(){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM");
return sdf.format(new Date());
}
public static int compareTime(String time1,String time2) throws ParseException{
SimpleDateFormat sdf = new SimpleDateFormat(DEFAULT_DATE_PATTERN);
Date date1 = sdf.parse(time1);
Date date2 = sdf.parse(time2);
long result = date1.getTime() - date2.getTime();
if(result > 0){
return 1;
}else if(result==0){
return 0;
}else{
return -1;
}
}
public static Date convertStrToDate(String dateStr,String format){
if(!BlankUtil.isBlank(dateStr)&&!BlankUtil.isBlank(format)){
try{
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.parse(dateStr);
}catch (Exception e) {
logger.warn("convertDate fail, date is "+ dateStr, e);
}
}
return null;
}
public static String convertDate(String dateStr,String format,String otherFormat){
try{
Date date = convertStrToDate(dateStr, format);
SimpleDateFormat sdf = new SimpleDateFormat(otherFormat);
return sdf.format(date);
}catch (Exception e) {
logger.warn("convertDate fail, date is "+ dateStr, e);
}
return null;
}
public static String convertDate(String dateStr,String format){
return convertDate(dateStr, DEFAULT_DATE_PATTERN,format);
}
public class NettyUtil {
public static String parseChannelRemoteAddr(final Channel channel) {
if (null == channel) {
return "";
}
SocketAddress remote = channel.remoteAddress();
final String addr = remote != null ? remote.toString() : "";
if (addr.length() > 0) {
int index = addr.lastIndexOf("/");
if (index >= 0) {
return addr.substring(index + 1);
}
return addr;
}
return "";
}
}
public class SimpleWebChatApplication {
private static final Logger logger = LoggerFactory.getLogger(SimpleWebChatApplication.class);
public static void main(String[] args) {
final SimpleWebChatServer server = new SimpleWebChatServer(Constants.DEFAULT_PORT);
server.init();
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run(){
server.shutdown();
logger.warn(">>>>>>>>>> jvm shutdown");
System.exit(0);
}
});
}
}
4.结果