作为一名程序员,工作也7、8年了,这是我第一次写文章,实在是惭愧。之前因工作需要一直都是百度、google,而没有想过静下心来自己研究原理、源码,导致现在还是一只菜鸟。。。
好了。言归正传,本次实践的话是基于nacos作为配置、注册中心的,各位大佬可以去了解一下nacos的官网,虽然最近这个组件爆出了漏洞,但不可否认其功能还真的是很强大的,附上官网地址:https://nacos.io/zh-cn/
项目使用jdk1.8进行开发。使用maven做jar包管理,分了很多模块子项目,包括基础服务的common模块(Redis、MongoDB、RocketMQ组件集成,UUID、DateUtil、加解密工具类等),服务的提供者demo-provider,服务的消费者demo-consumer,websocket服务等,demo项目的结构如下:
所有子模块的Parent项目的POM信息如下,虽然我引用了zipkin和sleuth,但还未实现相关功能,后续有集成了,我再发文章出来,各位不需要的,可以自行删除:
ParentPOM:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>XX.XX</groupId> <artifactId>platform</artifactId> <version>0.0.1-SNAPSHOT</version> <name>platform</name> <description>XX中台服务</description> <packaging>pom</packaging> <properties> <spring-boot.version>2.0.4.RELEASE</spring-boot.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> </properties> <modules> <module>common</module> <module>gateway</module> <module>servers/demo-provider</module> <module>servers/demo-consumer</module> <module>servers/taskinput</module> <module>servers/imchat</module> </modules> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.boot</groupId>--> <!-- <artifactId>spring-boot-starter-security</artifactId>--> <!-- </dependency>--> <!-- 引入Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project>
如下是GateWay内容:
POM
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>XX.XX</groupId> <artifactId>platform</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>XX.XX.platform</groupId> <artifactId>gateway</artifactId> <version>0.0.1-SNAPSHOT</version> <name>gateway</name> <description>XX中台服务网关</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Finchley.SR1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>0.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>0.2.1.RELEASE</version> </dependency> <dependency> <groupId>XX.XX.platform</groupId> <artifactId>common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <finalName>../../docker/XX-gateway</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.smartgis.GatewayStarterApplication</mainClass> <layout>ZIP</layout> </configuration> <executions> <execution> <goals> <goal>repackage</goal><!--可以把依赖的包都打包到生成的Jar包中--> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> </project>
GateWay配置文件信息(application.yml):
server: port: 8001 spring: application: name: demo-gateway cloud: nacos: discovery: namespace: 0ee7047e-90db-45d9-a85c-b3aa206aba5b server-addr: 127.0.0.1:8848 gateway: # 此处不注掉,会导致gateway转发websocket服务的时候出现自动断开的情况,报504错误 # default-filters: # - name: Hystrix # args: # name: myfallback # fallbackUri: forward:/defaultfallback globalcors: corsConfigurations: '[/**]': allowedHeaders: "*" allowedOrigins: "*" allowedMethods: "*" # 此处不注掉,会导致gateway转发websocket服务的时候出现自动断开的情况,报504错误 # discovery: # locator: # enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称进行路由 routes: - id: taskinput uri: lb://taskinput order: 0 predicates: - Path=/taskinput/** filters: - StripPrefix=1 #去掉前缀,具体实现参考StripPrefixGatewayFilterFactory - AddResponseHeader=X-Response-Default-Foo, Default-Bar - name: Hystrix args: name: myfallback fallbackUri: forward:/defaultfallback - id: demo-provider uri: lb://demo-provider order: 0 predicates: - Path=/provider/** filters: - StripPrefix=1 #去掉前缀,具体实现参考StripPrefixGatewayFilterFactory - AddResponseHeader=X-Response-Default-Foo, Default-Bar - id: demo-consumer uri: lb://demo-consumer order: 0 predicates: - Path=/consumer/** filters: - StripPrefix=1 #去掉前缀,具体实现参考StripPrefixGatewayFilterFactory - AddResponseHeader=X-Response-Default-Foo, Default-Bar #实际的超时时间是(ReadTimeout+ConnectTimeout)*(MaxAutoRetries+1)*(MaxAutoRetriesNextServer+1) #如果MaxAutoRetries和MaxAutoRetriesNextServer都设为0,那么实际超时就是(ReadTimeout+ConnectTimeout)了 - id: imchat uri: lb:ws://imchat predicates: - Path=/ws/** filters: - StripPrefix=1 # - id: imchat # uri: lb://imchat # predicates: # - Path=/ws/info/** # - id: imchat # uri: lb:ws://imchat # predicates: # - Path=/ws/** - id: bulletscreen uri: lb://bullet predicates: - Path=/bullet/info/** - id: bulletscreen uri: lb:ws://bullet predicates: - Path=/bullet/** ribbon: eureka: enabled: false eager-load: enabled: true #饥饿加载,系统启动时创建好ribbon客户端而不是在使用时去创建 ConnectTimeout: 2000 #单位ms,请求连接超时时间 ReadTimeout: 4000 #单位ms,请求处理的超时时间 OkToRetryOnAllOperations: false #对所有操作请求都进行重试 MaxAutoRetriesNextServer: 0 #切换实例的重试次数 MaxAutoRetries: 0 #对当前实例的重试次数 ServerListRefreshInterval: 2000 #Interval to refresh the server list from the source NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule management: endpoints: web: exposure: include: refresh
Hystrix断路器:
import XX.XX.platform.common.base.Result; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; @RestController public class DefaultHystrixController { @RequestMapping("/defaultfallback") @ResponseBody public Result defaultFallBack() { return Result.createFail("服务不可用,请稍后重试。"); } }
启动类:
@SpringBootApplication @EnableDiscoveryClient @RefreshScope public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
如下是IMChat服务:
为啥叫IMChat不叫websocket呢,因为最初我想到websocket服务,是想要搭建IM服务的,然后结合MQ服务,我想着可以通过websocket服务进行前后端的侨联,然后消息通过MQ进行转发,达到分布式的目的,从demo服务来看,这种想法似乎可行,但后续有啥问题,因为我能力有限,还不清楚,请各位大佬指教
POM:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>XX.XX</groupId> <artifactId>platform</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>XX.XX.platform</groupId> <artifactId>imchat</artifactId> <version>0.0.1-SNAPSHOT</version> <name>imchat</name> <description>聊天/推送服务</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>0.2.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>0.2.1.RELEASE</version> </dependency> <!--web 组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>XX.XX.platform</groupId> <artifactId>common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <finalName>../../docker/imchat-server</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
配置文件application.yml
server: port: 1999 spring: application: name: imchat cloud: nacos: discovery: namespace: 0ee7047e-90db-45d9-a85c-b3aa206aba5b server-addr: 127.0.0.1:8848 data: mongodb: uri: mongodb://root:root@XX.XX.XX.XX:27017/bhplatform?maxpoolsize=10&minpoolsize=1&maxidletimems=600000&maxlifetimems=1800000 cache: redis: time-to-live: 60s type: redis #redis配置 redis: cluster: nodes: 127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384 timeout: 6000ms lettuce: pool: max-active: 8 max-wait: -1ms max-idle: 8 min-idle: 0 database: 0 jackson: time-zone: GMT+8 apache: rocketmq: consumer: pushConsumer: XXPushConsumer producer: producerGroup: XX namesrvAddr: 127.0.0.1:9876 logging: path: /java-log level: root: info management: endpoints: web: exposure: include: refresh
MyWebSocketHandler
import com.alibaba.fastjson.JSONObject; import XX.XX.common.DateUtil; import XX.XX.common.base.BaseCode; import XX.XX.common.cache.redis.RedisService; import XX.XX.entity.IM.IMChatMessage; import XX.XX.entity.IM.IMChatRequest; import XX.XX.entity.IM.IMChatResponse; import XX.XX.common.mq.producer.Producer; import lombok.extern.slf4j.Slf4j; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageExt; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.socket.*; import com.XX.XX.taskinput.entity.UserVO; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; @Slf4j @Service public class MyWebSocketHandler implements WebSocketHandler { @Autowired private RedisService redisService; @Autowired private Producer rocketProducer; /** * NameServer 地址 */ @Value("${apache.rocketmq.namesrvAddr}") private String namesrvAddr; private static final ConcurrentHashMap map = new ConcurrentHashMap(); @Override public void afterConnectionEstablished(WebSocketSession session) { log.info("connect websocket successful!"); IMChatMessage chat = new IMChatMessage(); chat.setGmtCreat(DateUtil.getCurrentDateTime()); chat.setData(session.getId()); chat.setType(BaseCode.SINGLELOGIN); String userid = session.getAttributes().get(BaseCode.USERID).toString(); log.info(userid); chat.setFrom(userid); sendMessageToUser(userid, chat); WebSocketSession sessiono = (WebSocketSession) map.get(userid + BaseCode.SESSION); if (sessiono != null) { log.info("close original session-start"); try { sessiono.close(); } catch (IOException e) { log.info("close original session failed"); } } DefaultMQPushConsumer consumero = (DefaultMQPushConsumer) map.get(userid + BaseCode.CONSUMER); if (consumero != null) { log.info("close original consumer-start"); consumero.shutdown(); } //todo 开启线程,读取群和好友消息 //需要执行的代码 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(userid); consumer.setNamesrvAddr(namesrvAddr); try { // 订阅PushTopic下Tag为push的消息,都订阅消息 consumer.subscribe(BaseCode.IMCHAT, userid); // 程序第一次启动从消息队列头获取数据 consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); //可以修改每次消费消息的数量,默认设置是每次消费一条 consumer.setConsumeMessageBatchMaxSize(1); //在此监听中消费信息,并返回消费的状态信息 consumer.registerMessageListener((MessageListenerOrderly) (list, consumeOrderlyContext) -> { if (!session.isOpen()) { consumer.shutdown(); return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; } for (MessageExt messageExt : list) { try { session.sendMessage(new TextMessage(new String(messageExt.getBody(), "UTF-8"))); log.info("消费响应:MsgId:" + messageExt.getMsgId() + ",msgBody:" + new String(messageExt.getBody(), "UTF-8") + ",tag:" + messageExt.getTags() + ",topic:" + messageExt.getTopic()); } catch (IOException e) { return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; } } return ConsumeOrderlyStatus.SUCCESS; }); log.info("start consumer"); consumer.start(); map.put(userid + BaseCode.CONSUMER, consumer); map.put(userid + BaseCode.SESSION, session); } catch (Exception e) { e.printStackTrace(); log.info("start consumer exception"); } } @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message){ log.info("handle message start"); try { IMChatRequest chat = JSONObject.parseObject((String) message.getPayload(), IMChatRequest.class); UserVO user = (UserVO) redisService.get(session.getAttributes().get(BaseCode.TOKEN).toString()); if (user == null) { session.sendMessage(new TextMessage(new IMChatResponse(BaseCode.FAILCODE, BaseCode.FAILSSMSG, "token is invalid").toString())); session.close(); } else { user.setToken(""); IMChatMessage chatMessage = new IMChatMessage(); chatMessage.setGmtCreat(DateUtil.getCurrentDateTime()); chatMessage.setData(chat.getData()); chatMessage.setType(chat.getType()); chatMessage.setFrom(user); if (chat.getTouserid().length > 0) { for (String toid : chat.getTouserid()) { sendMessageToUser(toid, chatMessage); } } } } catch (Exception e) { log.error("e", e); } } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { log.info("handle message start"); if (session.isOpen()) { session.close(); } log.error("connect error", exception); String userid = session.getAttributes().get(BaseCode.USERID).toString(); DefaultMQPushConsumer consumer = (DefaultMQPushConsumer) map.get(userid + BaseCode.CONSUMER); consumer.shutdown(); map.remove(userid + BaseCode.CONSUMER); map.remove(userid + BaseCode.SESSION); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus){ log.error("connection closed: " + closeStatus); String userid = session.getAttributes().get(BaseCode.USERID).toString(); DefaultMQPushConsumer consumer = (DefaultMQPushConsumer) map.get(userid + BaseCode.CONSUMER); consumer.shutdown(); map.remove(userid + BaseCode.CONSUMER); map.remove(userid + BaseCode.SESSION); } @Override public boolean supportsPartialMessages() { return false; } /** * 发送信息给指定用户 * * @param clientId * @param message * @return */ public boolean sendMessageToUser(String clientId, IMChatMessage message) { log.info("to userid:" + clientId + ",tomessage:" + message.toString()); rocketProducer.sendMsg(BaseCode.IMCHAT, clientId, message.toString()); return true; } }
WebSocketInterceptor
import XX.XX.common.base.BaseCode; import XX.XX.platform.common.cache.redis.RedisService; import XX.XX.platform.taskinput.entity.UserVO; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.stereotype.Service; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import java.util.Map; @Slf4j @Service public class WebSocketInterceptor implements HandshakeInterceptor { @Autowired private RedisService redisService; //在握手之前执行该方法, 继续握手返回true, 中断握手返回false. 通过attributes参数设置WebSocketSession的属性 @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes){ if (request instanceof ServletServerHttpRequest) { String uri = request.getURI().toString(); String token = uri.substring(uri.lastIndexOf("/")+1); UserVO user = (UserVO) redisService.get(token); log.info("current token is:"+token); if(user==null) { return false; } attributes.put(BaseCode.TOKEN,token); attributes.put(BaseCode.USERID,user.getUserid()); log.info(user.getUserid()); } return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { log.info("coming webSocketInterceptor afterHandshake method..."); } }
WebSocketConfig
import XX.XX.platform.imchat.interceptor.WebSocketInterceptor; import XX.XX.platform.imchat.listener.MyWebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; /** * 首先注入一个ServerEndpointExporterBean,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint */ @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private MyWebSocketHandler myWebSocketHandler; @Autowired private WebSocketInterceptor webSocketInterceptor; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler, "/imchat/{TOKEN}") .setAllowedOrigins("*") .addInterceptors(webSocketInterceptor); } }
ImchatApplication
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.ComponentScan; @SpringBootApplication @EnableDiscoveryClient @ComponentScan({"XX.XX.platform"}) public class ImchatApplication { public static void main(String[] args) { SpringApplication.run(ImchatApplication.class, args); } }
效果:
以上是本次demo的全部代码,涉及到其它模块的,各位有需要可以留言,另外很感谢hasor的作者@哈库纳玛塔塔 大佬给的帮助和支持,同时也给大家推荐一下大佬的开源框架dataway,我们最近在用,很好用,强烈推荐,附上官网:https://www.hasor.net/doc/