单机运行
用户a通过服务器进入房间room,用户b也通过房间进入room,用户之间是通过session来通话的,所以session直接存储在集合中就可以了。
因为session存储在一台服务器的集合中,所以每次发送消息的时候,直接发给房间内的所有人的session就可以了。
多机运行
假如有两台服务器,用户a通过服务器A进入了房间room,用户b通过服务器B也进入了房间room,由于session是用集合存储在各自的服务器中的,所以这种情况下,用户a发的消息只能通过服务器A,但是服务器A中是没有用户b的session的,所以当用户a发送消息的时候,用户b就收不到用户a发送的消息了。
实现共享session
那如何在多台服务器之间实现websocket session共享,其实就是session存在一个公共的地方,但是存在哪里呢,想过用redis存储session,但是session不能转为string,也不能序列化,所以就存不进去。
之后发现redis有个订阅发布的功能,然后我用这个算是变相实现了,思路如下:
当用户a通过服务器A进入房间room,然后将用户存入redis的房间room中,用户b通过服务器B进入房间room,也将用户b存入redis的房间room中,并且都订阅房间room.将各自的session还是存储在各自的服务器中,
然后发消息的时候,发布消息到redis的room中,然后监听redis中的room房间,当有消息发送的时候,判断redis的房间中是否存在本地用户,如果存在则发送给当前服务器对应的用户。
在本机用两个tomcat配置不同的端口号模拟启动,访问测试成功,效果如下:
RedisConfig 这里没有配置JedisConnectionFactory,因为在application.yml中配置集群(windows集群配置参考这篇博客https://www.cnblogs.com/tommy-huang/p/6240083.html)时会自动注入连接工厂。
配置监听容器RedisMessageListenerContainer,用于监听redis发布的消息。
package com.test;/*
*/
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.*;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import java.util.HashSet;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<?,?> getRedisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory){
RedisTemplate redisTemplate= new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
@Bean
public RedisMessageListenerContainer initRedisContainer(@Autowired RedisConnectionFactory redisConnectionFactory){
RedisMessageListenerContainer container=new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
return container;
}
}
SocketRest
实现MessageListener接口,重写onMessage方法,接收redis消息并发送。
方法container.addMessageListener(socketRest,topic); socketRest为监听者,topic为监听的房间号。每当第一个用户进入房间后,就为该房间增加一个监听者,用来监听该房间的消息。
package com.test;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;
import org.springframework.stereotype.Component;
@ServerEndpoint("/websocket/{roomId}/{username}")
@Component
public class SocketRest implements MessageListener{
private static final Logger log=LoggerFactory.getLogger(SocketRest.class);
@Autowired
private RedisUtil redisUtil;
//用户对应session
private static Map<Object,Session> UserSessions=new ConcurrentHashMap<>();
//房间号对应SocketRest
private static Map<String,SocketRest> sockets=new ConcurrentHashMap<>();
@Autowired
private RedisMessageListenerContainer container;
private SocketRest socketRest;
private String roomName;
public SocketRest(){
ApplicationContext context = ApplicationContextRegister.getApplicationContext();
redisUtil=context.getBean(RedisUtil.class);
container=context.getBean(RedisMessageListenerContainer.class);
}
@OnOpen
public void connect(@PathParam("roomId") String roomId, @PathParam("username")String username, Session session) throws Exception{
System.out.println("a user is connected");
//加上前缀room
roomName="room"+roomId;
//判断房间是否存在
if(!redisUtil.sGet("rooms").contains(roomName)){
log.info("当前房间不存在");
sendmessageToOne("当前房间不存在",session);
session.close();
return;
}
//判断用户是否存在
//判断其他房间是否存在该用户,存在则踢出
Set<Object> set=redisUtil.sGet("rooms");
Set<Object> tes=new HashSet(set);
tes.remove(roomName);
if(tes.size()>0){
for (Object room:tes){
Set<Object> roomuser= redisUtil.sGet(room.toString());
for(Object o:roomuser){
System.out.println("roomuser: "+o.toString());
if(o.toString().equals(username)){
Session ses = UserSessions.get(username);
sendMsg(username,"你已再其他房间登陆",ses);
ses.close();
UserSessions.remove(username);
return;
}
}
}
}
System.out.println(" --------------");
//判断当前用户是否存在该房间
//如果不存在则添加该用户到房间
redisUtil.sSet(roomName,username);
//增加用户和session对应集合
if(!UserSessions.containsKey(username)){
UserSessions.put(username,session);
sendMsg(username,"进入直播间",session);
}
Topic topic=new ChannelTopic(roomName);
if(!sockets.containsKey(roomName)){
sockets.put(roomName,this);
}
//加入监听消息,为每个房间创建一个监听者
container.addMessageListener(sockets.get(roomName),topic);
log.info("当前用户:"+username+" 加入房间"+roomName+" 人数:"+redisUtil.sGetSetSize(roomName));
}
/**
* 断开连接
* @param username 用户id
* @param session 用户sesison
*/
@OnClose
public void disConnect(@PathParam("username") String username, Session session) {
try {
//移除对应的session
UserSessions.remove(username);
for(Object s:UserSessions.keySet()){
System.out.println("我是房间中剩余的人:"+s);
}
//移除redis房间中人
redisUtil.setRemove(roomName,username);
session.close();
}catch (Exception e){
log.info("disConnect Exception");
}
log.info("a client has disconnected!");
}
/**
* 关闭房间
* @param roomId 房间号
* @param username 用户id
*/
public void closeLiveRoom(@PathParam("roomId") String roomId,@PathParam("username") String username){
try {
//移除redis中的房间
Topic topic=new ChannelTopic("room"+roomId);
Set<Object> set=redisUtil.sGet(roomId);
//获取交集,移除当前服务器存在的session
set.retainAll(UserSessions.keySet());
for(Object userkey:set){
UserSessions.remove(userkey);
}
//取消订阅房间
container.removeMessageListener(socketRest,topic);
}catch (Exception e){
log.info("closeLiveRoom exception");
}
}
/**
发送消息
*/
@OnMessage
public void sendMsg(@PathParam("username") String username,String msg, Session session){
try {
HashMap message=new HashMap();
message.put("msg",msg);
message.put("username",username);
if(redisUtil.sGet(roomName).size()==1){
HashMap map=new HashMap();
map.put("msg",msg);
map.put("username",username);
sendmessageToOne(JSON.toJSONString(map),session);
return;
}
redisUtil.convertAndSend(roomName,JSON.toJSONString(message));
// broadcast(roomName,message,username);
}catch (Exception e){
e.printStackTrace();
log.info("sendMsg exception:"+e);
return;
}
}
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
/**
* 发给一个人
*/
private static void sendmessageToOne(String msg, Session session) {
try {
session.getAsyncRemote().sendText(msg);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 发给所有人
* @param roomName
* @param msg
* @param username
* @throws Exception
*/
public void broadcast(String roomName, HashMap msg,String username) {
try {
if(!redisUtil.sHasKey(roomName,username)){
System.out.println("当前房间不存在该用户");
return;
}
//获取本服务器房间所有人和redis中存储的房间的人取交集,然后发送消息
Set<Object> users = redisUtil.sGet(roomName);
Set<Object> userkey= UserSessions.keySet();
System.out.println("userkey:"+JSON.toJSONString(userkey));
HashSet set=new HashSet(userkey);
set.retainAll(users);
for(Object user:set){
System.out.println("user: "+user.toString());
Session sess=UserSessions.get(user.toString());
System.out.println("session: "+sess);
sendmessageToOne(JSON.toJSONString(msg),sess);
}
}catch (Exception e){
e.printStackTrace();
log.info("bd exception");
}
}
//接收redis中的消息
@Override
public void onMessage(Message msgs, byte[] pattern) {
try {
//消息内容
byte[] body=msgs.getBody();
System.out.println("body: "+body);
//订阅房间
String topic=new String(pattern);
//获取存在的房间中的用户
String result= new String(body,"utf-8");
System.out.println("msg: "+result);
JSONObject js= JSON.parseObject(result);
String username=js.getString("username");
String msg=js.getString("msg");
Set<Object> SessionKeys= UserSessions.keySet();
HashMap message=new HashMap();
message.put("msg",msg);
message.put("username",username);
broadcast(topic,message,username);
}catch (Exception e){
log.info("onMessage exception");
}
}
}
application.yml
server:
port: 8088
logging:
config: classpath:log4j2.yml
spring:
profiles:
active: test
---
spring:
profiles: test
datasource:
url: jdbc:mysql://localhost:3306/test
driver-class-name: com.mysql.jdbc.Driver
data-password: 1234
data-username: root
redis:
cluster:
nodes: 127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
database: 0
host: 127.0.0.1:7001
port: 6379
session:
store-type: redis
WebsocketConfig 打包成war用外置tomcat部署时需要注释,不注释会和tomcat自带的websocket注入bean时冲突。
@Configuration
public class WebsocketConfig {
@Bean
public ServerEndpointExporter config(){
return new ServerEndpointExporter();
}
}
启动时写入房间
@Component
public class StartupRunner implements CommandLineRunner {
@Autowired
private RedisUtil redisUtil;
@Override
public void run(String... strings) throws Exception{
if(!redisUtil.sGet("room111").isEmpty()){
System.out.println("清空数据");
redisUtil.del("room111");
}
redisUtil.sSet("rooms","room111","room222");
Set<Object> set=redisUtil.sGet("rooms");
for (Object o:set){
System.out.println("------>"+o.toString());
}
}
}
ApplicationContextRegister 解决websocket中不能注入bean.
package com.test;/*
*/
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextRegister implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
}
RedisUtil 在网上找的个比较全的工具类。
增加订阅发布方法, channel订阅房间号,message发送给该房间的消息。
public void convertAndSend(String channel ,Object message){ redisTemplate.convertAndSend(channel,message); }
package com.test;/*
*/
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import redis.clients.jedis.JedisPoolConfig;
@Component
public class RedisUtil{
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
//========================redis订阅发布===============================
public void convertAndSend(String channel ,Object message){
redisTemplate.convertAndSend(channel,message);
}
//=============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key,long time){
try {
if(time>0){
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key){
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key){
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String ... key){
if(key!=null&&key.length>0){
if(key.length==1){
redisTemplate.delete(key[0]);
}else{
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key){
return key==null?null:redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key,Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key,Object value,long time){
try {
if(time>0){
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}else{
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @return
*/
public long incr(String key, long delta){
if(delta<0){
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @return
*/
public long decr(String key, long delta){
if(delta<0){
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key,String item){
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object,Object> hmget(String key){
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String,Object> map){
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String,Object> map, long time){
try {
redisTemplate.opsForHash().putAll(key, map);
if(time>0){
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key,String item,Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key,String item,Object value,long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if(time>0){
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item){
redisTemplate.opsForHash().delete(key,item);
}
/**
* 判断hash表中是否有该项的值
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item){
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item,double by){
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item,double by){
return redisTemplate.opsForHash().increment(key, item,-by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
* @return
*/
public Set<Object> sGet(String key){
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key,Object value){
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object...values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key,long time,Object...values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if(time>0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
* @param key 键
* @return
*/
public long sGetSetSize(String key){
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object ...values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key,long start, long end){
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
* @param key 键
* @return
*/
public long lGetListSize(String key){
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key,long index){
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index,Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key,long count,Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
启动类Application
package com.test;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
@EnableAutoConfiguration
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(Application.class);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>test</artifactId>
<version>1.0</version>
<packaging>war</packaging>
<name>test</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 支持@Slf4j -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<!-- provide 表示打包时可以不添加
<scope>provided</scope>-->
</dependency>
<!-- javaee -->
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0</version>
<scope>provided</scope>
</dependency>
<!-- 导入websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 导入redis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency> <!-- exclude掉spring-boot的默认log配置 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<!-- 去掉内置logging-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!-- springboot 启动配置-->
<!-- 引入log4j2依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency> <!-- 加上这个才能辨认到log4j2.yml文件 -->
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
用tomcat部署时,如果不是默认的8080,则需要添加端口号,在server.xml中配置,增加端口以及设置编码。
<Connector port="8088" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" URIEncoding="UTF-8" />
设置项目启动路径,docBase即项目名称
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Context path="" docBase="whatsup" debug="0" reloadable="true" />
<!-- SingleSignOn valve, share authentication between web applications
Documentation at: /docs/config/valve.html -->
<!--
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
--><!-- Access log processes all example.
Documentation at: /docs/config/valve.html
Note: The pattern used is equivalent to using pattern="common" -->
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" /></Host>
log4j.yml
Appenders:
Console: #输出到控制台
name: CONSOLE #Appender命名
target: SYSTEM_OUT
PatternLayout:
pattern: "%d{yyyy-MM-dd HH:mm:ss,SSS}:%4p %t (%F:%L) - %m%n"
RollingFile: # 输出到文件,超过256MB归档
- name: ROLLING_FILE
ignoreExceptions: false
fileName: springboot.log
filePattern: "/springboot/logs/$${date:yyyy-MM}/springboot -%d{yyyy-MM-dd}-%i.log.gz"
PatternLayout:
pattern: "%d{yyyy-MM-dd HH:mm:ss,SSS}:%4p %t (%F:%L) - %m%n"
Policies:
SizeBasedTriggeringPolicy:
size: "256 MB"
DefaultRolloverStrategy:
max: 1000
Loggers:
Root:
level: info
AppenderRef:
- ref: CONSOLE
Logger: #单独设置某些包的输出级别
- name: app.com.kenho.mapper #复数加上-
additivity: false #去除重复的log
level: trace
AppenderRef:
- ref: CONSOLE #复数加上-
- ref: ROLLING_FILE #复数加上-
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
var ws;
function connect() {
var address = document.getElementById("address").value;
console.log(address);
ws = new WebSocket("ws://" + address);
/*
*监听三种状态的变化 。js会回调
*/
ws.onopen = function(message) {
console.log(message);
};
ws.onclose = function (message) {
};
ws.onmessage = function (message) {
showMessage(message.data);
};
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
ws.close();
};
}
//关闭连接
function closeWebSocket(){
ws.close();
}
//发送消息
function send(){
var input = document.getElementById("msg").value;
ws.send(input);
input.value = "";
}
function showMessage(message) {
console.log("message"+message);
var obj= eval('(' + message + ')');
var br = document.createElement("br")
var div = document.getElementById("showChatMessage");
var ms=obj.msg;
var name=obj.username;
console.log("ms: "+ms);
console.log("name "+name);
if(ms=="进入直播间"){
var ms= document.createTextNode(name+ms);
div.appendChild(ms);
}else{
var sendmsg=name+":"+ms;
var text = document.createTextNode(sendmsg);
div.appendChild(text);
}
div.appendChild(br);
}
</script>
</head>
<style>
.main{
text-align: center; /*让div内部文字居中*/
background-color: #fff;
width: 450px;
margin: auto;
position: absolute;
top: 0;
border: 1px solid deepskyblue;
}
</style>
<body>
<div class="main">
<div style="width: 450px">
<input type="text" id="address" name="address" placeholder="请输入地址" style="height:25px;width:350px">
<input type="button" value="连接" id="connect" onclick="connect()" name="connect">
</div>
<div id="showChatMessage" style="width: 450px;height: 300px;border: 1px solid "></div>
<div>
<input style="width: 200px" type="text" size="80" id="msg" name="msg" placeholder="输入聊天内容" />
<input type="button" onclick="send()" value="发送">
</div>
</div>
</body>
</html>