用Java写个聊天室——WebSocket的小试牛刀

介绍一下主人翁吧

WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。

为什么需要WebSocket

正所谓技术服务于业务,如果技术不能解决业务上的痛点,那它存在的意义在哪?咱先分析几个业务场景:

1. 订单支付
2. 系统通知
3. 即时通讯

在很多电商项目中,当我们利用支付宝或者微信扫码支付后,我们要等待网页响应,然后跳转支付结果页面。其实在整个支付过程中,浏览器先向服务器发送支付请求,然后服务端给浏览器返回一个二维码,然后客户扫码支付,但是支付没支付,浏览器是不知道的啊,只有服务端知道,所以浏览器要不停去问服务端:“xxx订单支付成功了吗?”,就像下图这样
用Java写个聊天室——WebSocket的小试牛刀_第1张图片
这种实现方法很简单,就是ajax轮询。

系统通知:比如有些论坛,当你发了一篇帖子,过一会有人回复你了,这个时候一般系统都会推送给你,告诉你有个新回复。

即时通讯:网页上的在线客服,你与客服的对话。

对于实时性要求不高的业务场景,我们可以用ajax轮询基本就可以实现需求。但是对于实时性要求特别高的呢,比如即时通讯,这你不能一直ajax去轮询吧。
所以我们希望服务端能自己主动通知浏览器,而不是浏览器每次都是自己去问服务端。

所以!WebSocket协议,它来了!
当浏览器和服务端建立WebSocket协议后,他两的对话就变成下图这样了
用Java写个聊天室——WebSocket的小试牛刀_第2张图片

项目

花了亿点点时间算是大概介绍了一下websocket,和它的作用。
现在到正题了,用Java写一个聊天室!
实现功能如下:好友列表好友在线状态提醒一对一聊天收到新消息提醒

技术栈:

后端(主要):Spring Boot + Socket.io + Mybatis Plus + Mysql
前端(主要):Vue + Element UI + Socket.io

实现效果如下:

用Java写个聊天室——WebSocket的小试牛刀_第3张图片

前端代码

1.引入Socket.io

npm install vue-socket.io --save

// webSocket
import VueSocketIO from 'vue-socket.io'
Vue.use(new VueSocketIO({
  debug: true,
  connection: `http://127.0.0.1:9999/?token=${token}`, //这里的token是用户登录成功之后服务端给的一个token,用来验证用户
  vuex: {
    store,
    actionPrefix: 'SOCKET_',
    mutationPrefix: 'SOCKET_'
  }
}))

2.具体在组件中使用

<template>
//这里是组件内容
</template>
<script>
	export default {
		name: 'demo',
		data() {
			return {
			}
		},
		sockets: {
			connect() {
      			console.log('connect连接服务端')
    		},
    		disconnect() {
      			console.log('从服务器断开连接....')
     		},
     		// reconnect重连
    		reconnect() {
      			console.log('正在尝试重新连接....')
    		},
    		//监听服务端的server_event,要与服务端一致
    		server_event(data) {
    			console.log('收到服务端的通知',data)
    		}
		}
	}
</script>

后端代码

Maven主要引入

 		<dependency>
            <groupId>com.corundumstudio.socketiogroupId>
            <artifactId>netty-socketioartifactId>
            <version>1.7.18version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-websocketartifactId>
        dependency>`

配置文件(.yml)

# SocketIO配置
socketio:
  host: 127.0.0.1
  # SocketIO端口
  port: 9999
  bossCount: 1
  # 连接数大小
  workCount: 100
  # 允许客户请求
  allowCustomRequests: true
  # 协议升级超时时间(毫秒),默认10秒,HTTP握手升级为ws协议超时时间
  upgradeTimeout: 10000
  # Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件
  pingTimeout: 60000
  # Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔
  pingInterval: 25000
  # 设置HTTP交互最大内容长度
  maxHttpContentLength: 1048576
  # 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器
  maxFramePayloadLength: 1048576

Socket.io配置代码

@Data
@ConfigurationProperties("socketio")
public class AppProperties {

    /**
     * host
     */
    private String host;

    /**
     * port
     */
    private Integer port;

    /**
     * bossCount
     */
    private int bossCount;

    /**
     * workCount
     */
    private int workCount;

    /**
     * allowCustomRequests
     */
    private boolean allowCustomRequests;

    /**
     * upgradeTimeout
     */
    private int upgradeTimeout;

    /**
     * pingTimeout
     */
    private int pingTimeout;

    /**
     * pingInterval
     */
    private int pingInterval;

    private int maxHttpContentLength;

    private int maxFramePayloadLength;
}
@Slf4j
@Configuration
@EnableConfigurationProperties({
        AppProperties.class,
})
public class AppConfiguration {

    @Bean
    public SocketIOServer socketIoServer(AppProperties appProperties) {
        SocketConfig socketConfig = new SocketConfig();
        socketConfig.setTcpNoDelay(true);
        socketConfig.setSoLinger(0);
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
        config.setSocketConfig(socketConfig);
        config.setHostname(appProperties.getHost());
        config.setPort(appProperties.getPort());
        config.setBossThreads(appProperties.getBossCount());
        config.setWorkerThreads(appProperties.getWorkCount());
        config.setAllowCustomRequests(appProperties.isAllowCustomRequests());
        config.setUpgradeTimeout(appProperties.getUpgradeTimeout());
        config.setPingTimeout(appProperties.getPingTimeout());
        config.setPingInterval(appProperties.getPingInterval());
        config.setMaxHttpContentLength(appProperties.getMaxHttpContentLength());
        config.setMaxFramePayloadLength(appProperties.getMaxFramePayloadLength());
        return new SocketIOServer(config);
    }
}

这里很关键!!!主要是发送和接受消息!

@Slf4j
@Service
@RequiredArgsConstructor
public class MsgSendService {
	
	//这里存放和浏览器的链接,键:SessionId
    private static final Map<String, SocketIOClient> CLIENT_MAP = new ConcurrentHashMap<>();

	//这里保存用户对应的SessionId的链接,键:UserId 值:SessionId
	 private static final Map<String, String> USER_CLIENT_MAP = new ConcurrentHashMap<>();

    private final SocketIOServer socketIOServer;
/**
     * 启动的时候会被调用一次
     */
    @PostConstruct
    private void autoStart() {
        log.info("start ws");
        socketIOServer.addConnectListener(client -> {
            String token = getClientToken(client, "token");
            //这里做了安全校验
            if (checkToken(client, token)) {
                CLIENT_MAP.put(client.getSessionId().toString(), client);
            } else {
                client.disconnect();
            }
            log.info("目前在线用户有:{}", CLIENT_MAP.size());
        });
        socketIOServer.addDisconnectListener(client -> {
            String token = getClientToken(client, "token");
            CLIENT_MAP.remove(client.getSessionId().toString());
            client.disconnect();
            log.info("移除client:{}", client.getSessionId());
        });
		//监听'sendMsg'的事件
        socketIOServer.addEventListener("sendMsg",Integer.class,((socketIOClient, integer, ackRequest) -> {
            System.out.println(integer);
        }));
        socketIOServer.start();
        log.info("start finish");
    }
    
	private String getClientToken(SocketIOClient client, String key) {
        Map<String, List<String>> params = client.getHandshakeData().getUrlParams();
        List<String> list = params.get(key);
        if (CollUtil.isNotEmpty(list)) {
        //这里获取的是用户登录成功后由系统签发的token,所以我们只要校验这个token就行了
            return list.get(0);
        }
        return null;
    }
//这里就是主要的验证token方法,根据具体的业务来
    private boolean checkToken(SocketIOClient client, String token) {
     
        log.info("检查token是否有效:{}", token);
        if (StrUtil.isNotEmpty(token) && !"undefined".equals(token)){
           		//这里写验证代码
           		Boolean tokenFlag = true;
           		if(tokenFlag){
           		//验证通过,绑定用户和sessionId
           			USER_CLIENT_MAP.put(userId,client.getSessionId().toString());
           			return true;
           		}
            }
        }

        return false;
    }
	 @PreDestroy
    private void onDestroy() {
        if (socketIOServer != null) {
            socketIOServer.stop();
        }
    }

	//具体给浏览器发送通知的方法
	public void sendMsg(Object demo) {
        CLIENT_MAP.forEach((key, value) -> {
            UUID sessionId = value.getSessionId();
            System.out.println(sessionId);
            //这里很关键server_event一定前后端的事件名称一定要一致
            value.sendEvent("server_event", demo);
            log.info("发送数据成功:{}", key);
        });
    }
}

结束

首先,我要道歉,我没有放出具体实现的代码,但是我把最基础的代码放出来了,就是服务端通知浏览器的代码。
主要是因为目前我代码写的太乱了,完全就是一个练手项目。其实我说的功能基本都是基于那两个集合实现的,有兴趣的可以自己捣鼓。

你可能感兴趣的:(java,websocket)