写在前面:

最近有一个想法,做一个程序员师徒体系。由于在大学期间的我在学习java的时分十分地苍茫,找不到自己的方向,也没有一个社会上有经历的前辈去辅导,所以走了许多的弯路。后来工作了,想把自己的避坑经历共享给别人,可是发现身边都是有经历的开发者,也没有机会去共享自己的想法,所以富有同学就想做一个程序员专属的师徒体系,秉承着学徒能够有人指导少走弯路,师傅能桃李满天下的意图,所以开端做这个师徒体系,也会同步更新该体系所用到的技能,并且作为教程共享给咱们,期望咱们能够关注一波。

我是怎么实现开源系统的实时聊天功能的?

其实谈天功用最开端的时分咱们能够创立一个表,当人们发送的时分将音讯往表里面刺进,接纳的时分将音讯从表里面取出,然后定时去取出音讯,这样勉强能实现一个音讯谈天的功用,可是会大大的耗费服务器的功用,所以咱们用到了一项新技能:WebSocket。那么老规矩,WebSocket是什么呢?

我是怎么实现开源系统的实时聊天功能的?
在这儿富有同学用自己的话总结一次:websocket使得服务器能够主动得向客户端推送音讯,而且服务器和客户端树立衔接只要一次承认就能够了。 好了,那么怎样将这个功用集成到师徒办理体系中来呢?在这儿富有同学要感谢wensocket开源项目,由于富有同学在改项目上进行改善使得websocket能够集成springsecurity和jwt技能到师徒办理体系中来。咱们能够先去瞧一瞧这个项目,对接下来的教程能够理解得更加深透。

废话不多说,开端咱们的实战

我是怎么实现开源系统的实时聊天功能的?

第一步,咱们导入相关的jar包

	    <!--websocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>
        <!--websocket-->

第二步,创立websocket的service类

package com.wangfugui.apprentice.service;
import com.alibaba.fastjson.JSON;
import com.wangfugui.apprentice.dao.domain.Message;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@ServerEndpoint("/webSocket/{username}")
@Component
public class WebSocketServer {
	 //静态变量,用来记录当时在线衔接数。应该把它设计成线程安全的。
    private static AtomicInteger onlineNum = new AtomicInteger();
    //concurrent包的线程安全Set,用来寄存每个客户端对应的WebSocketServer目标。
    private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();
    //发送音讯
    public void sendMessage(Session session, String message) throws IOException {
        if(session != null){
            synchronized (session) {
                System.out.println("发送数据:" + message);
                session.getBasicRemote().sendText(message);
            }
        }
    }
    //给指定用户发送信息
    public void sendInfo(String userName, String message){
        Session session = sessionPools.get(userName);
        try {
            sendMessage(session, message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    // 群发音讯
    public void broadcast(String message){
    	for (Session session: sessionPools.values()) {
            try {
                sendMessage(session, message);
            } catch(Exception e){
                e.printStackTrace();
                continue;
            }
        }
    }
    //树立衔接成功调用
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "username") String userName){
        sessionPools.put(userName, session);
        addOnlineCount();
        System.out.println(userName + "参加webSocket!当时人数为" + onlineNum);
        // 广播上线音讯
        Message msg = new Message();
        msg.setDate(new Date());
        msg.setTo("0");
        msg.setText(userName);
        broadcast(JSON.toJSONString(msg,true));
    }
    //封闭衔接时调用
    @OnClose
    public void onClose(@PathParam(value = "username") String userName){
        sessionPools.remove(userName);
        subOnlineCount();
        System.out.println(userName + "断开webSocket衔接!当时人数为" + onlineNum);
        // 广播下线音讯
        Message msg = new Message();
        msg.setDate(new Date());
        msg.setTo("-2");
        msg.setText(userName);
        broadcast(JSON.toJSONString(msg,true));
    }
    //收到客户端信息后,根据接纳人的username把音讯推下去或许群发
    // to=-1群发音讯
    @OnMessage
    public void onMessage(String message) throws IOException{
        System.out.println("server get" + message);
        Message msg=JSON.parseObject(message, Message.class);
		msg.setDate(new Date());
		if (msg.getTo().equals("-1")) {
			broadcast(JSON.toJSONString(msg,true));
		} else {
			sendInfo(msg.getTo(), JSON.toJSONString(msg,true));
		}
    }
    //过错时调用
    @OnError
    public void onError(Session session, Throwable throwable){
        System.out.println("发生过错");
        throwable.printStackTrace();
    }
    public static void addOnlineCount(){
        onlineNum.incrementAndGet();
    }
    public static void subOnlineCount() {
        onlineNum.decrementAndGet();
    }
    public static AtomicInteger getOnlineNumber() {
        return onlineNum;
    }
    public static ConcurrentHashMap<String, Session> getSessionPools() {
        return sessionPools;
    }
}

咱们仔细看这个类里面的四个注解: @OnOpen @OnClose @OnMessage @OnError 分别是websocket衔接,封闭,收到音讯,过错时调用 @ServerEndpoint也要加上,接下来咱们装备一个config类使得上面的类收效:

package com.wangfugui.apprentice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
 * WebScoket装备处理器
 */
@Configuration
public class WebSocketConfig {
	 /**
     * ServerEndpointExporter 效果
     *
     * 这个Bean会主动注册运用@ServerEndpoint注解声明的websocket endpoint
     *
     * @return
     */
	@Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

这儿是最关键的两个装备类了。

第三步,本来到这儿就能够跑起来进行谈天了

前端的代码在库房里面,这儿就不贴出来来了。可是咱们发现由于师徒体系是集成的jwt的,所以咱们没有办法进行基本的谈天,由于都被阻拦了!所以咱们要做的第一步便是把相关接口放开:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        // post恳求要封闭csrf验证,否则拜访报错;实践开发中敞开,需求前端配合传递其他参数
        http.csrf().disable()
                .authorizeRequests()
                //swagger
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                //设置哪些路径不需求认证,这儿也能放行静态资源
                .antMatchers("/webSocket/**").anonymous()
                .antMatchers("/static/**").anonymous()
                .antMatchers("/css/**", "/js/**").anonymous()
                .antMatchers("/favicon.ico").anonymous()
                //放开注册,登录用户接口
                .antMatchers("/user/register").anonymous()
                .antMatchers("/login").anonymous()
                .antMatchers("/logoutSystem").anonymous()
                .antMatchers("/chatroom").anonymous()
                .antMatchers("/onlineusers").anonymous()
                .antMatchers("/currentuser").anonymous()
                .anyRequest().authenticated() // 一切恳求都需求验证
                .and()     //这儿选用链式编程
                .logout()
                //刊出成功后,调转的页面
                .logoutSuccessUrl("/login")
                // 装备自己的刊出URL,默以为 /logout
                .logoutUrl("/logoutSystem")
                // 是否销毁session,默许ture
                .invalidateHttpSession(true)
                //  删去指定的cookies
                .deleteCookies(JwtTokenUtils.TOKEN_HEADER)
                .and()
                //增加用户账号的认证
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                //增加用户权限的认证
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                //咱们能够准确地操控什么机遇创立session,有以下选项进行操控:
                //always – 假如session不存在总是需求创立;
                //ifRequired – 仅当需求时,创立session(默许装备);
                //never – 框架从不创立session,但假如已经存在,会运用该session ;
                //stateless – Spring Security不会创立session,或运用session;
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling()
                //增加没有带着token或许token无效操作
                .authenticationEntryPoint(new JWTAuthenticationEntryPoint())
                //增加无权限时的处理
                .accessDeniedHandler(new JWTAccessDeniedHandler());
    }

接着咱们更改咱们的默许登录接口:

 public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        //这儿特别注意是登录的接口,自定义登录接口,不写的话默许"/login"
        super.setFilterProcessesUrl("/loginvalidate");
    }

这样咱们的默许登录就能够登录了,可是由于要集成到咱们的体系中来,咱们需求jwt生成的字符串,所以在登录成功咱们要将jwt秘钥存储在cookie中来: 在JWTAuthenticationFilter类中

 // 成功验证后调用的办法
    // 假如验证成功,就生成token并回来
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) {
        JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
        System.out.println("jwtUser:" + jwtUser.toString());
        boolean isRemember = rememberMe.get() == 1;
        String role = "";
        Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
        for (GrantedAuthority authority : authorities){
            role = authority.getAuthority();
        }
        String token = JwtTokenUtils.createToken(jwtUser.getUsername(), role, isRemember);
        // 回来创立成功的token
        // 可是这儿创立的token只是单纯的token
        // 依照jwt的规定,最后恳求的时分应该是 `Bearer token`
        response.setHeader("Authorization", JwtTokenUtils.TOKEN_PREFIX + token);
        response.addCookie(new Cookie("Authorization",token));
    }

这样之后咱们在调用谈天室接口中就能够获取cookie,然后将在线列表显示出来

   @RequestMapping("/chatroom")
    public String chatroom(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        //假如没有cookie则回来登录页面
        Cookie authCookie = Arrays.stream(cookies).filter(cookie -> cookie.getName()
                .contains(JwtTokenUtils.TOKEN_HEADER)).collect(Collectors.toList()).get(0);
        if (authCookie == null) {
            return "login";
        }
        String tokenHeader = authCookie.getValue();
        String username = JwtTokenUtils.getUsername(tokenHeader);
        HttpSession session = request.getSession();
        User idByUserName = userService.getIdByUserName(username);
        session.setAttribute("uid", idByUserName.getId());
        return "chatroom";
    }

还有一个登录接口:

   @RequestMapping("/login")
    public String login(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return "login";
        }
        //假如没有cookie则回来登录页面
        List<Cookie> collect = Arrays.stream(cookies).filter(cookie -> cookie.getName()
                .contains(JwtTokenUtils.TOKEN_HEADER)).collect(Collectors.toList());
        if (collect.isEmpty()) {
            return "login";
        }
        return "home";
    }

咱们创立一个首页页面

<!DOCTYPE>
<html>
<head>
    <title>login</title>
</head>
<body>
<div class="container vertical-center">
    <a href="/chatroom">谈天室</a>
    <form action="/logoutSystem" method="post">
        <button type="submit" >刊出</button>
    </form>
</div>
</body>
</html>

这样咱们就能够进行刊出和进入谈天室了。剩余代码就不在这儿贴出,请咱们移步到库房观看:SpringBoot+WebSocket

说在之后

师徒体系我会一直更新,由于是开源的项目,所以我也期望又更多的小伙伴参加进来!! 这是程序员师徒办理体系的地址: 程序员师徒办理体系

我是怎么实现开源系统的实时聊天功能的?