服务器实现消息推送的几种方案

目录

一、概述

二、DWR

三、STOMP

四、小结

五、原生websocket

六、Netty实现WebSocket

七、小结


一、概述

1. 本篇文章以聊天室程序为例,介绍实现消息推送给的几种方式。

2. 基于Spring Boot,其他框架或语言也类似,项目地址:https://github.com/yeta666/chat

3. 文中只提及主要代码,详细内容看项目。

 

二、DWR

1. 引入依赖


    org.directwebremoting
    dwr
    3.0.2-RELEASE

2. 创建提供服务的类

@RestController
@RemoteProxy
public class DWRController {

    /**
     * 根据请求参数type处理不同类型请求
     * @param reqMessage
     */
    @RemoteMethod
    public void chat(String reqMessage) {
        //请求
        CommonRequest request = JSON.parseObject(reqMessage, CommonRequest.class);
        Integer type = request.getType();
        //返回
        CommonResponse response;
        //所有会话
        WebContext webContext = WebContextFactory.get();
        Collection sessions = webContext.getAllScriptSessions();
        //构建发送所需的JS脚本
        ScriptBuffer scriptBuffer = new ScriptBuffer();
        if (type == 1) {        //群聊消息
            //调用客户端的JS脚本函数
            scriptBuffer.appendScript("chat(");
            //这个message可以被过滤处理一下,或者做其他的处理操作。这视需求而定。
            response = new CommonResponse(1000, 1, request.getMessage());
            scriptBuffer.appendData(JSON.toJSONString(response));
            scriptBuffer.appendScript(")");
            Util util = new Util(sessions);     //sessions,群发
            util.addScript(scriptBuffer);
        } else if (type == 2) {        //私聊消息
            for (ScriptSession session : sessions) {
                if (session.getId().equals(request.getTarget())) {
                    //调用客户端的JS脚本函数
                    scriptBuffer.appendScript("chat(");
                    //这个message可以被过滤处理一下,或者做其他的处理操作。这视需求而定。
                    response = new CommonResponse(1000, 2, request.getMessage());
                    scriptBuffer.appendData(JSON.toJSONString(response));
                    scriptBuffer.appendScript(")");
                    Util util = new Util(session);     //session,单独发
                    util.addScript(scriptBuffer);
                }
            }
        } else if (type == 3) {     //返回所有会话id,用于私聊
            List ids = new ArrayList();
            for (ScriptSession session : sessions) {
                ids.add(session.getId());
            }
            //调用客户端的JS脚本函数
            scriptBuffer.appendScript("addTarget(");
            //这个message可以被过滤处理一下,或者做其他的处理操作。这视需求而定。
            response = new CommonResponse(1000, 4, ids);
            scriptBuffer.appendData(JSON.toJSONString(response));
            scriptBuffer.appendScript(")");
            Util util = new Util(sessions);     //sessions,群发
            util.addScript(scriptBuffer);
        }
    }
}

3. 创建dwr配置文件




    

    

    

4. 配置dwrServlet

/**
 * 配置dwr Servlet
 * 重点注意取名
 * @return
 */
@Bean
public ServletRegistrationBean dwrServlet() {
    DwrSpringServlet servlet = new DwrSpringServlet();
    ServletRegistrationBean registrationBean = new ServletRegistrationBean(servlet, "/dwr/*");
    registrationBean.addInitParameter("debug", "true");
    //使用服务器反转Ajax
    registrationBean.addInitParameter("activeReverseAjaxEnabled", "true");
    //能够从其他域请求true:开启; false:关闭
    registrationBean.addInitParameter("crossDomainSessionSecurity", "false");
    //允许远程调用JS
    registrationBean.addInitParameter("allowScriptTagRemoting", "true");
    return registrationBean;
}

5. 导入dwr的配置文件

@ImportResource(locations = "classpath:config/dwrConfig.xml")

6. 引入dwr相关js,注意路径,文件由dwr自动生成,不会在项目中出现



7. 主要js代码

$(function() {
	//页面加载时进行反转的激活
	dwr.engine.setActiveReverseAjax(true);

	//点击建立连接按钮,加载发送消息目标
	$("#connectBtn").click(function() {
		$("#chatroomBox").css("display", "block");
		sendMessage(3);
	});

	//点击群聊按钮
	$("#topicBtn").click(function() {
		sendMessage(1);
	});

	//点击私聊按钮
	$("#queueBtn").click(function() {
		$target = $("#target").val().trim();
		sendMessage(2, $target);
	});
})

//发送消息方法
function sendMessage(type, target) {
	var request = {
		type: type,
		message: $('#message').val().trim(),
		target: target
	};
	DWRController.chat(JSON.stringify(request));
	$('#message').val("");
}

//后台回调方法,加载发送消息目标
function addTarget(data) {
	var message = JSON.parse(data).message;
	//清空
	$("#target").html("");
	//加载
	for(var i = 0; i < message.length; i++) {
		$("#target").append($(''));
	}
}

//后台回调
function chat(data) {
	var message = JSON.parse(data).message;
	$("#messages").val($("#messages").val() + "\n" + message);
}

三、STOMP

1. 点对点推送需要指定用户名,这里引入spring security


    org.springframework.boot
    spring-boot-starter-websocket



    org.springframework.boot
    spring-boot-starter-security

2. 创建spring security配置类

@Configuration
@EnableWebSecurity
public class CommonWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Autowired
    private CommonUserDetailsService commonUserDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //内存中分配用户,spring security自己验证
        /*auth.inMemoryAuthentication()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("yeta1").password(new BCryptPasswordEncoder().encode("yeta1")).roles("USER")
                .and()
                .withUser("yeta2").password(new BCryptPasswordEncoder().encode("yeta2")).roles("USER");*/
        //自定义登陆验证,可以通过数据库来验证
        auth.userDetailsService(commonUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //设置/resources/static目录下的静态资源不拦截
        web.ignoring().antMatchers("/resources/static/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated()       //所有请求需要认证
                .and()
                .formLogin()
                .defaultSuccessUrl("/slogin", true)      //登陆验证成功路径
                .failureUrl("/flogin")       //登陆验证失败路径
                .permitAll();
        http.csrf()
                .disable();
    }
}

3. 由于需要在内存中保存登陆的所有用户信息,以便实现点对点推送,这里采用自定义登陆验证方式

@Service
public class CommonUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user;
        if (username.equals("yeta1")) {
            user = new User("A01", "yeta1", new BCryptPasswordEncoder().encode("yeta1"), "USER");
        } else if (username.equals("yeta2")) {
            user = new User("A02", "yeta2", new BCryptPasswordEncoder().encode("yeta2"), "USER");
        } else {
            user = new User();
        }
        return user;
    }
}

4. 创建WebSocket配置类

@Configuration
@EnableWebSocketMessageBroker   //开启STOMP协议来传输基于代理(message broker)的消息,这时控制器支持@MessageMapping
public class CommonWebSocketMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer {

    /**
     * 注册STOMP协议的节点(endpoint),并映射指定的URL
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //指定使用SocketJS协议
        registry.addEndpoint("/endpoint").withSockJS();
    }

    /**
     * 配置消息代理
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry){
        //广播式应配置一个/topic消息代理
        //点对点式应配置一个/queue消息代理
        registry.enableSimpleBroker("/topic", "/queue");
    }
}

5. 创建推送业务处理类

@RestController
public class PushController {

    //保存所有登陆的用户
    public static ConcurrentSkipListSet users = new ConcurrentSkipListSet();

    @Autowired
    private SimpMessagingTemplate smt;    //通过该对象向浏览器发送消息

    /**
     * 群发方法
     * @param request
     * @return
     */
    @MessageMapping(value = "/topic/pushAll/receive")       //服务器接收浏览器消息的地址
    @SendTo(value = "/topic/pushAll")       //浏览器订阅服务器消息的地址,服务器将会把消息推送到订阅了该地址的浏览器
    public CommonResponse pushAll(CommonRequest request) {
        return new CommonResponse(1000, 1, request.getMessage());
    }

    /**
     * 群发方法,同上面的方法效果一样
     * @param request
     * @return
     */
    @MessageMapping(value = "/topic/pushAll/receive1")
    public void pushAll1(CommonRequest request) {
        smt.convertAndSend("/topic/pushAll1", new CommonResponse(1000, 1, request.getMessage()));
    }

    /**
     * 推送在线用户
     * @return
     */
    @MessageMapping(value = "/topic/pushUsers")
    @SendTo(value = "/topic/pushAll")
    public CommonResponse pushUsers(Principal principal, CommonRequest request) {
        if (request.getType() == 5) {
            users.add(principal.getName());
        } else if (request.getType() == 6) {
            users.remove(principal.getName());
        }
        return new CommonResponse(1000, 4, users);
    }

    /**
     * 只推送自己方法
     * @param request
     * @return
     */
    @MessageMapping(value = "/queue/pushMyself/receive")
    @SendToUser(value = "/queue/pushMyself", broadcast = false)     //只推送给自己,broadcast=false表示如果一个账户登陆多个浏览器,只将消息推送给发出请求的浏览器
    public CommonResponse pushMyself(CommonRequest request) {
        return new CommonResponse(1000, 1, request.getMessage());
    }

    /**
     * 私发方法
     * @param principal
     * @param request
     * @return
     */
    @MessageMapping(value = "/queue/pushSomeone/receive")
    public CommonResponse pushSomeone(Principal principal, CommonRequest request) {
        String target = request.getTarget();
        for (String username : users) {
            if (username.equals(target)) {
                CommonResponse response = new CommonResponse(1000, 1, principal.getName() + " say: " + request.getMessage());
                smt.convertAndSendToUser(target, "/queue/pushSomeone", response);
            }
        }
        return new CommonResponse(1001, 2, "error");
    }
}

6. 主要js代码

//连接SockJS
var socket = new SockJS(SOCKJS_URI);
//使用STOMP子协议的WebSocket客户端
var stompClient = Stomp.over(socket);
//连接WebSocket客户端
stompClient.connect({}, function(frame) {
	//订阅服务器群发消息
	stompClient.subscribe(PUSH_ALL_URL, function(data) {
		var response = JSON.parse(data.body);
		var message = response.message;
		if(response.type == 4) {
			$("#target").html("");
			for(var i = 0; i < message.length; i++) {
				$("#target").append($(''));
			}
		} else {
			$("#messages").val($("#messages").val() + "\n" + message);
		}
	});
	//订阅服务器私发自己消息
	stompClient.subscribe(PUSH_MYSELF_URL, function(data) {
		var response = JSON.parse(data.body);
		var message = response.message;
		$("#messages").val($("#messages").val() + "\n" + message);
	});
	//订阅服务器私发某人消息
	stompClient.subscribe(PUSH_SOMEONE_URL, function(data) {
		var response = JSON.parse(data.body);
		var message = response.message;
		$("#messages").val($("#messages").val() + "\n" + message);
	});

	//显示聊天室
	$("#chatroomBox").css("display", "block");
	//断开连接按钮可用
	$("#disconnectBtn").attr("disabled", false);

	//更新在线用户
	sendMessage("/topic/pushUsers", 5);
});

//点击群发按钮
$("#topicBtn").click(function() {
	sendMessage(PUSH_ALL_RECEIVE_URL, 1);
});

//点击点对点发按钮
$("#queueBtn").click(function() {
	sendMessage(PUSH_SOMEONE_RECEIVE_URL, 2, $("#target").val());
});

//发送消息方法
function sendMessage(url, type, target) {
	var request = {
		type: type,
		target: target,
		message: $("#message").val().trim()
	};
	stompClient.send(url, {}, JSON.stringify(request));
	$('#message').val("");
}

//监听窗口关闭
window.onbeforeunload = function() {
	//更新在线用户
	sendMessage("/topic/pushUsers", 6);
	//客户端断开连接
	stompClient.disconnect();
	socket.close();
};

四、小结

在dwr和stomp的使用过程中,我发现在服务器端不能监听它们的连接建立和关闭,实现不了如下场景:当有一个新连接建立,服务器主动推送给所有连接当前在线的用户,当有一个连接断开,服务器也主动推送给所有连接当前在线的用户。

当然,也可能只是我没有研究到位。

其实stomp可以这样实现:

1. stompClient.connect回调方法中,stompClient.subscribe订阅之后,浏览器主动向服务器发送一个消息,表示自己要和服务器建立连接;

2. 服务器将该连接加入在线用户列表,并向所有连接推送最新的在线用户;

3. 浏览器监听用户准备关闭连接的时候(比如说监听关闭浏览器或者有一个主动断开连接的按钮),先向服务器发送一个消息,表示自己要和服务器断开连接;

4. 服务器将该连接移除在线用户列表,并向所有连接推送最新的在线用户;

五、原生websocket

1. 引入WebSocket依赖


    org.springframework.boot
    spring-boot-starter-websocket

2. 创建WebSocket配置类

@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3. 创建WebSocket服务类

@ServerEndpoint("/webSocket")
@Component
public class WebSocketServer {

    //保存所有的会话
    private static ConcurrentHashMap sessions = new ConcurrentHashMap();

    /**
     * 建立连接
     * @param session
     * @throws IOException
     */
    @OnOpen
    public void onOpen(Session session) throws IOException {
        //保存会话
        if (sessions.get(session.getId()) == null) {
            sessions.put(session.getId(), session);
        }
        //服务器主动推送所有会话id
        for (Session s : sessions.values()) {
            String ids = sessions.keySet().toString();
            CommonResponse response = new CommonResponse(1000, 4, ids);
            s.getBasicRemote().sendText(JSON.toJSONString(response));
        }
    }

    /**
     * 关闭连接
     * @param session
     * @throws IOException
     */
    @OnClose
    public void onClose(Session session) throws IOException {
        sessions.remove(session.getId(), session);
        //服务器主动推送所有会话id
        for (Session s : sessions.values()) {
            String ids = sessions.keySet().toString();
            CommonResponse response = new CommonResponse(1000, 4, ids);
            s.getBasicRemote().sendText(JSON.toJSONString(response));
        }
    }

    /**
     * 收到消息
     * @param session
     * @param sRequest
     * @throws IOException
     */
    @OnMessage
    public void onMessage(Session session, String sRequest) throws IOException {
        CommonRequest request = JSON.parseObject(sRequest, CommonRequest.class);
        String message = request.getMessage();
        CommonResponse response;
        if (request.getType() == 1) {       //群聊消息
            for (Session s : sessions.values()) {
                response = new CommonResponse(1000, 1, message);
                s.getBasicRemote().sendText(JSON.toJSONString(response));
            }
        } else if (request.getType() == 2) {        //私聊消息
            String target = request.getTarget();
            for (Session s : sessions.values()) {
                if (s.getId().equals(target)) {
                    response = new CommonResponse(1000, 1, message);
                    s.getBasicRemote().sendText(JSON.toJSONString(response));
                }
            }
        } else if (request.getType() == 3) {        //浏览器自己请求资源
            response = new CommonResponse(1000, 3, message);
            session.getBasicRemote().sendText(JSON.toJSONString(response));
        }
    }

    /**
     * 出现错误
     * @param session
     * @param e
     * @throws IOException
     */
    @OnError
    public void onError(Session session, Throwable e) throws IOException {
        CommonResponse response = new CommonResponse(1001, 3, e.getMessage());
        session.getBasicRemote().sendText(JSON.toJSONString(response));
        e.printStackTrace();
    }
}

4. 主要js代码

//创建WebSocket对象
var webSocket = new WebSocket("ws://localhost:8080/chat/webSocket");

//连接成功调用方法
webSocket.onopen = function(e) {
	console.log("onopen");
	//显示聊天室
	$("#chatroomBox").css("display", "block");
	//断开连接按钮可用
	$("#disconnectBtn").attr("disabled", false);
};

//浏览器收到服务器消息调用方法
webSocket.onmessage = function(e) {
	console.log("onmessage");
	var response = JSON.parse(e.data);
	if(response.code == 1000) {
		if(response.type == 4) { //服务器主动推送消息
			//清空
			$("#target").html("");
			//加载
			var message = response.message.substring(1, response.message.length - 1).split(",");
			for(var i = 0; i < message.length; i++) {
				$("#target").append($(''));
			}
		} else { //群聊消息或私聊消息
			$("#messages").val($("#messages").val() + "\n" + response.message);
		}
	}
};

//出现错误调用方法
webSocket.onerror = function(e) {
	alert(e.type);
};

//连接关闭调用方法
webSocket.onclose = function(e) {
	alert(e.type);
};

//点击群聊按钮
$("#topicBtn").click(function() {
	sendMessage(1);
});

//点击私聊按钮
$("#queueBtn").click(function() {
	//判断目标
	var $target = $("#target").val().trim();
	sendMessage(2, $target);
});

//发送消息方法
function sendMessage(type, target) {
	if(webSocket.readyState == WebSocket.OPEN) {
		var request = {
			type: type,
			message: $('#message').val().trim(),
			target: target
		};
		webSocket.send(JSON.stringify(request));
		$('#message').val("");
	}
}

//监听窗口关闭
window.onbeforeunload = function() {
	webSocket.close();
};

六、Netty实现WebSocket

1. 引入Netty依赖


    io.netty
    netty-all
    5.0.0.Alpha1

2. 创建Netty配置类

public class NettyConfig {

    //存储每一个浏览器接入进来时的channel对象
    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    //IP地址
    public static final String IP = "localhost";

    //端口号
    public static final int PORT = 8081;

    //路径
    public static final String URL = "/webSocket";
}

3. 创建业务处理类

public class CommonSimpleChannelInboundHandler extends SimpleChannelInboundHandler {

    //日志
    private static final Logger LOG = LoggerFactory.getLogger(CommonSimpleChannelInboundHandler.class);

    private WebSocketServerHandshaker handshaker;

    /**
     * 服务器处理浏览器WebSocket请求的核心方法
     * @param channelHandlerContext
     * @param o
     * @throws Exception
     */
    @Override
    protected void messageReceived(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
        //处理浏览器向服务器发起HTTP握手请求
        if (o instanceof FullHttpRequest) {
            handleHttpRequest(channelHandlerContext, (FullHttpRequest) o);
        } else if (o instanceof WebSocketFrame) {       //处理WebSocket连接
            handleWebSocketFrame(channelHandlerContext, (WebSocketFrame) o);
        }
    }

    /**
     * 处理浏览器向服务器发起HTTP握手请求
     * @param ctx
     * @param req
     */
    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
        if (!req.getDecoderResult().isSuccess() || !("websocket").equals(req.headers().get("Upgrade"))) {
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }

        WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(
                "ws://" + NettyConfig.IP + ":" + NettyConfig.PORT  + NettyConfig.URL,
                null,
                false);
        handshaker = factory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
        } else {
            handshaker.handshake(ctx.channel(), req);
        }
    }

    /**
     * 服务器向浏览器发送消息
     * @param ctx
     * @param request
     * @param res
     */
    private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, DefaultFullHttpResponse res) {
        if (res.getStatus().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
        }
        //发送数据
        ChannelFuture future = ctx.channel().writeAndFlush(res);
        if (res.getStatus().code() != 200) {
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }

    /**
     * 处理WebSocket连接
     * @param ctx
     * @param frame
     */
    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        //判断是否关闭WebSocket消息
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), ((CloseWebSocketFrame) frame).retain());
        }
        //判断是否是ping消息
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        //判断是否是二进制消息
        if (!(frame instanceof TextWebSocketFrame)) {
            LOG.info("收到二进制消息");
            throw new RuntimeException(this.getClass().getName() + " 不支持二进制消息");
        }
        //获取浏览器向服务器发送的消息
        String reqMessage = ((TextWebSocketFrame) frame).text();
        CommonRequest request = JSON.parseObject(reqMessage, CommonRequest.class);
        Integer type = request.getType();
        CommonResponse response;
        TextWebSocketFrame twsf;
        if (type == 1) {       //群聊消息
            response = new CommonResponse(1000, 1, request.getMessage());
            twsf = new TextWebSocketFrame(JSON.toJSONString(response));
            NettyConfig.channels.writeAndFlush(twsf);
        } else if (type == 2) {        //私聊消息
            for (Channel channel : NettyConfig.channels) {
                if (channel.id().toString().equals(request.getTarget())) {
                    response = new CommonResponse(1000, 2, request.getMessage());
                    twsf = new TextWebSocketFrame(JSON.toJSONString(response));
                    channel.writeAndFlush(twsf);
                }
            }
        } else if (type == 3) {        //浏览器自己请求资源
            sendAllChannelIds();
        }
    }

    /**
     * 服务器主动推送所有channel id
     */
    public void sendAllChannelIds() {
        List ids = new ArrayList();
        for (Channel channel : NettyConfig.channels) {
            ids.add(channel.id().toString());
        }
        CommonResponse response = new CommonResponse(1000, 4, ids);
        TextWebSocketFrame twsf = new TextWebSocketFrame(JSON.toJSONString(response));
        NettyConfig.channels.writeAndFlush(twsf);
    }

    /**
     * 出现异常时调用
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 浏览器与服务器创建连接时调用
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        NettyConfig.channels.add(ctx.channel());
        sendAllChannelIds();
    }

    /**
     * 浏览器与服务器断开连接时调用
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        NettyConfig.channels.remove(ctx.channel());
        sendAllChannelIds();
    }

    /**
     * 服务器接收浏览器发送过来的数据结束之后调用
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
} 
  

3. 创建初始化类

public class CommonChannelInitializer extends ChannelInitializer {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        //HTTP消息编码解码
        socketChannel.pipeline().addLast("http-codec", new HttpServerCodec());
        //HTTP消息组装
        socketChannel.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
        //WebSocket通信支持
        socketChannel.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
        //
        socketChannel.pipeline().addLast("handler", new CommonSimpleChannelInboundHandler());
    }
}

4. 创建Netty启动类

@Configuration
public class NettyService implements CommandLineRunner {

    //日志
    private static final Logger LOG = LoggerFactory.getLogger(NettyConfig.class);

    @Override
    public void run(String... args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap sb = new ServerBootstrap();
            sb.group(bossGroup, workGroup);
            sb.channel(NioServerSocketChannel.class);
            sb.childHandler(new CommonChannelInitializer());
            LOG.info("等待连接...");
            Channel channel = sb.bind(NettyConfig.PORT).sync().channel();
            channel.closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

七、小结

原生websocket和netty实现websocket都可以监听到连接建立和关闭,所以可以轻易实现上面说到的功能。

你可能感兴趣的:(Java,服务器推送)