第十四章 : Spring Boot 整合spring-session,使用redis共享

第十四章 : Spring Boot 整合spring-session,使用redis共享

前沿

本文重点讲述:spring boot工程中使用spring-session机制进行安全认证,并且通过redis存储session,满足集群部署、分布式系统的session共享。

基于SPringBoot 2.3.2.RELEASE

背景

在传统单机web应用中,一般使用tomcat/jetty等web容器时,用户的session都是由容器管理。浏览器使用cookie中记录sessionId,容器根据sessionId判断用户是否存在会话session。这里的限制是,session存储在web容器中,被单台服务器容器管理。

但是网站主键演变,分布式应用和集群是趋势(提高性能)。此时用户的请求可能被负载分发至不同的服务器,此时传统的web容器管理用户会话session的方式即行不通。除非集群或者分布式web应用能够共享session,尽管tomcat等支持这样做。但是这样存在以下两点问题:

1、需要侵入web容器,提高问题的复杂
2、web容器之间共享session,集群机器之间势必要交互耦合

基于这些,必须提供新的可靠的集群分布式/集群session的解决方案,突破traditional-session单机限制(即web容器session方式,下面简称traditional-session),spring-session应用而生。

traditional-session和spring-session的区别

在这里插入图片描述

springboot-session 集成redis示例
  1. 添加依赖:在pom.xml文件中添加Spring Session Redis的依赖。
<dependency>  <groupId>org.springframework.session</groupId>  <artifactId>spring-session-data-redis</artifactId>  
</dependency>
<dependency><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId>
</dependency><!-- 对象池,使用redis时必须引入 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
  1. 配置Redis:在application.yaml文件中添加Redis的配置信息,包括Redis的地址、端口号、密码等。
server:port: 8080servlet:context-path: /# session超时时间 默认30分钟session:timeout: 30m
spring:session:store-type: redisredis:# 会话刷新模式flush-mode: immediate# 用于存储会话的键的命名空间namespace: "spring:session"redis:host: 192.168.92.105port: 6379password: foobared# 连接超时时间(记得添加单位,Duration)timeout: 10000ms# Redis默认情况下有16个分片,这里配置具体使用的分片# database: 0lettuce:pool:# 连接池最大连接数(使用负值表示没有限制) 默认 8max-active: 8# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1max-wait: -1ms# 连接池中的最大空闲连接 默认 8max-idle: 8# 连接池中的最小空闲连接 默认 0min-idle: 0
  1. 注解开启session功能:并使用@EnableRedisHttpSession注解开启session功能。同时,可以设置session的超时时间。

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;@SpringBootApplication
    @EnableRedisHttpSession
    public class SpringbootDay09Application {public static void main(String[] args) {SpringApplication.run(SpringbootDay09Application.class, args);}}
    
  2. 创建Controller:在需要使用session的Controller中,注入HttpSession对象,并使用它来存储session数据。

    @RestController  
    public class SessionController {  @Autowired  private HttpSession httpSession;  @GetMapping("/set")  public String set(String name, String value) {  httpSession.setAttribute(name, value);  return "set " + name + "=" + value;  }  @GetMapping("/get")  public String get(String name) {  return (String) httpSession.getAttribute(name);  }  
    }
    
  3. 测试:分别调用各应用接口,查看sessionId是否一致。同时,可以查看Redis缓存信息,缓存中的sessionId与接口返回信息一致。

    在这里插入图片描述

    在这里插入图片描述

spring-session特点与工作原理
特点

spring-session在无需绑定web容器的情况下提供对集群session的支持。并提供对以下情况的透明集成:

  1. HttpSession:容许替换web容器的HttpSession
  2. WebSocket:使用WebSocket通信时,提供Session的活跃
  3. WebSession:容许以应用中立的方式替换webflux的webSession
工作原理
spring-session分为以下核心模块:
  • SessionRepositoryFilter:Servlet规范中Filter的实现,用来切换HttpSession至Spring Session,包装

    HttpServletRequest和HttpServletResponse

  • HttpServerletRequestWrapperHttpServletResponseWrapperHttpSessionWrapper包装器:包装原有的HttpServletRequest、HttpServletResponse和Spring Session,实现切换Session和透明继承HttpSession的关键之所在

  • Session:Spring Session模块

  • SessionRepository:管理Spring Session的模块

  • HttpSessionStrategy:映射HttpRequest和HttpResponse到Session的策略

    在这里插入图片描述

  1. SessionRepositoryFilter

    SessionRepositoryFilter继承OncePerRequestFilter实现Filter

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 设置SessionRepository至Request的属性中request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);// 包装原始HttpServletRequest至SessionRepositoryRequestWrapperSessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response);// 包装原始HttpServletResponse响应至SessionRepositoryResponseWrapperSessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);try {filterChain.doFilter(wrappedRequest, wrappedResponse);} finally {// 提交sessionwrappedRequest.commitSession();}}

2、 SessionRepository

public interface SessionRepository<S extends Session> {S createSession();void save(S var1);S findById(String var1);void deleteById(String var1);
}

创建、保存、获取、删除Session的接口行为。根据Session的不同,分为很多种Session操作仓库。

在这里插入图片描述

当创建一个RedisSession,然后存储在Redis中时,RedisSession的存储细节如下:

spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:expirations:1439245080000

Redis会为每个RedisSession存储三个k-v。

第一个:k-v用来存储Session的详细信息,包括Session的过期时间间隔、最近的访问时间、attributes等等。这个k的过期时间为Session的最大过期时间 + 5分钟。如果默认的最大过期时间为30分钟,则这个k的过期时间为35分钟
第二个:k-v用来表示Session在Redis中的过期,这个k-v不存储任何有用数据,只是表示Session过期而设置。这个k在Redis中的过期时间即为Session的过期时间间隔
第三个:k-v存储这个Session的id,是一个Set类型的Redis数据结构。这个k中的最后的1439245080000值是一个时间戳,根据这个Session过期时刻滚动至下一分钟而计算得出。

3、 Session

spring-session和tomcat中的Session的实现模式上有很大不同,tomcat中直接对HttpSession接口进行实现,而spring-session中则抽象出单独的Session层接口,让后再使用适配器模式将Session适配层Servlet规范中的HttpSession。spring-sesion中关于session的实现和适配整个UML类图如下:

在这里插入图片描述

MapSession的代码源码片段

public final class MapSession implements Session, Serializable {public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800;private String id;private final String originalId;private Map<String, Object> sessionAttrs;private Instant creationTime;private Instant lastAccessedTime;private Duration maxInactiveInterval;private static final long serialVersionUID = 7160779239673823561L;public MapSession() {this(generateId());}public MapSession(String id) {this.sessionAttrs = new HashMap();this.creationTime = Instant.now();this.lastAccessedTime = this.creationTime;this.maxInactiveInterval = Duration.ofSeconds(1800L);this.id = id;this.originalId = id;}public MapSession(Session session) {this.sessionAttrs = new HashMap();this.creationTime = Instant.now();this.lastAccessedTime = this.creationTime;this.maxInactiveInterval = Duration.ofSeconds(1800L);if (session == null) {throw new IllegalArgumentException("session cannot be null");} else {this.id = session.getId();this.originalId = this.id;this.sessionAttrs = new HashMap(session.getAttributeNames().size());Iterator var2 = session.getAttributeNames().iterator();while(var2.hasNext()) {String attrName = (String)var2.next();Object attrValue = session.getAttribute(attrName);if (attrValue != null) {this.sessionAttrs.put(attrName, attrValue);}}this.lastAccessedTime = session.getLastAccessedTime();this.creationTime = session.getCreationTime();this.maxInactiveInterval = session.getMaxInactiveInterval();}}public void setLastAccessedTime(Instant lastAccessedTime) {this.lastAccessedTime = lastAccessedTime;}public Instant getCreationTime() {return this.creationTime;}public String getId() {return this.id;}public String getOriginalId() {return this.originalId;}public String changeSessionId() {String changedId = generateId();this.setId(changedId);return changedId;}public Instant getLastAccessedTime() {return this.lastAccessedTime;}public void setMaxInactiveInterval(Duration interval) {this.maxInactiveInterval = interval;}public Duration getMaxInactiveInterval() {return this.maxInactiveInterval;}public boolean isExpired() {return this.isExpired(Instant.now());}boolean isExpired(Instant now) {if (this.maxInactiveInterval.isNegative()) {return false;} else {return now.minus(this.maxInactiveInterval).compareTo(this.lastAccessedTime) >= 0;}}public <T> T getAttribute(String attributeName) {return this.sessionAttrs.get(attributeName);}public Set<String> getAttributeNames() {return new HashSet(this.sessionAttrs.keySet());}public void setAttribute(String attributeName, Object attributeValue) {if (attributeValue == null) {this.removeAttribute(attributeName);} else {this.sessionAttrs.put(attributeName, attributeValue);}}public void removeAttribute(String attributeName) {this.sessionAttrs.remove(attributeName);}public void setCreationTime(Instant creationTime) {this.creationTime = creationTime;}public void setId(String id) {this.id = id;}public boolean equals(Object obj) {return obj instanceof Session && this.id.equals(((Session)obj).getId());}public int hashCode() {return this.id.hashCode();}private static String generateId() {return UUID.randomUUID().toString();}
}

RedisSession的代码源码片段

final class RedisSession implements Session {private final MapSession cached;private final Map<String, Object> delta = new HashMap();private boolean isNew;private String originalSessionId;RedisSession(MapSession cached, boolean isNew) {this.cached = cached;this.isNew = isNew;this.originalSessionId = cached.getId();if (this.isNew) {this.delta.put("creationTime", cached.getCreationTime().toEpochMilli());this.delta.put("maxInactiveInterval", (int)cached.getMaxInactiveInterval().getSeconds());this.delta.put("lastAccessedTime", cached.getLastAccessedTime().toEpochMilli());}if (this.isNew || RedisSessionRepository.this.saveMode == SaveMode.ALWAYS) {this.getAttributeNames().forEach((attributeName) -> {this.delta.put(RedisSessionRepository.getAttributeKey(attributeName), cached.getAttribute(attributeName));});}}public String getId() {return this.cached.getId();}public String changeSessionId() {return this.cached.changeSessionId();}public <T> T getAttribute(String attributeName) {T attributeValue = this.cached.getAttribute(attributeName);if (attributeValue != null && RedisSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {this.delta.put(RedisSessionRepository.getAttributeKey(attributeName), attributeValue);}return attributeValue;}public Set<String> getAttributeNames() {return this.cached.getAttributeNames();}public void setAttribute(String attributeName, Object attributeValue) {this.cached.setAttribute(attributeName, attributeValue);this.delta.put(RedisSessionRepository.getAttributeKey(attributeName), attributeValue);this.flushIfRequired();}public void removeAttribute(String attributeName) {this.setAttribute(attributeName, (Object)null);}public Instant getCreationTime() {return this.cached.getCreationTime();}public void setLastAccessedTime(Instant lastAccessedTime) {this.cached.setLastAccessedTime(lastAccessedTime);this.delta.put("lastAccessedTime", this.getLastAccessedTime().toEpochMilli());this.flushIfRequired();}public Instant getLastAccessedTime() {return this.cached.getLastAccessedTime();}public void setMaxInactiveInterval(Duration interval) {this.cached.setMaxInactiveInterval(interval);this.delta.put("maxInactiveInterval", (int)this.getMaxInactiveInterval().getSeconds());this.flushIfRequired();}public Duration getMaxInactiveInterval() {return this.cached.getMaxInactiveInterval();}public boolean isExpired() {return this.cached.isExpired();}private void flushIfRequired() {if (RedisSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {this.save();}}private boolean hasChangedSessionId() {return !this.getId().equals(this.originalSessionId);}private void save() {this.saveChangeSessionId();this.saveDelta();if (this.isNew) {this.isNew = false;}}private void saveChangeSessionId() {if (this.hasChangedSessionId()) {if (!this.isNew) {String originalSessionIdKey = RedisSessionRepository.this.getSessionKey(this.originalSessionId);String sessionIdKey = RedisSessionRepository.this.getSessionKey(this.getId());RedisSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey, sessionIdKey);}this.originalSessionId = this.getId();}}private void saveDelta() {if (!this.delta.isEmpty()) {String key = RedisSessionRepository.this.getSessionKey(this.getId());RedisSessionRepository.this.sessionRedisOperations.opsForHash().putAll(key, new HashMap(this.delta));RedisSessionRepository.this.sessionRedisOperations.expireAt(key, Date.from(Instant.ofEpochMilli(this.getLastAccessedTime().toEpochMilli()).plusSeconds(this.getMaxInactiveInterval().getSeconds())));this.delta.clear();}}}
}

在RedisSession中有两个非常重要的成员属性:

cached:实际上是一个MapSession实例,用于做本地缓存,每次在getAttribute时无需从Redis中获取,主要为了improve性能
delta:用于跟踪变化数据,做持久化

4、SessionRepositoryRequestWrapper

对于开发人员获取HttpSession的api

HttpServletRequest request = ...;
HttpSession session = request.getSession(true);

在spring session中request的实际类型SessionRepositoryRequestWrapper。调用SessionRepositoryRequestWrapper的getSession方法会触发创建spring session,而非web容器的HttpSession。

SessionRepositoryRequestWrapper用来包装原始的HttpServletRequest实现HttpSession切换至Spring Session。是透明Spring Session透明集成HttpSession的关键。

SessionRepositoryRequestWrapper继承HttpServletRequestWrapper,在构造方法中将原有的HttpServletRequest通过调用super完成对HttpServletRequestWrapper中持有的HttpServletRequest初始化赋值,然后重写和session相关的方法。这样就保证SessionRepositoryRequestWrapper的其他方法调用都是使用原有的HttpServletRequest的数据,只有session相关的是重写的逻辑。

private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {private final HttpServletResponse response;private S requestedSession;private boolean requestedSessionCached;private String requestedSessionId;private Boolean requestedSessionIdValid;private boolean requestedSessionInvalidated;private SessionRepositoryRequestWrapper(HttpServletRequest request, HttpServletResponse response) {super(request);this.response = response;}private void commitSession() {SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper wrappedSession = this.getCurrentSession();if (wrappedSession == null) {if (this.isInvalidateClientSession()) {SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);}} else {S session = wrappedSession.getSession();this.clearRequestedSessionCache();SessionRepositoryFilter.this.sessionRepository.save(session);String sessionId = session.getId();if (!this.isRequestedSessionIdValid() || !sessionId.equals(this.getRequestedSessionId())) {SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);}}}private SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getCurrentSession() {return (SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper)this.getAttribute(SessionRepositoryFilter.CURRENT_SESSION_ATTR);}private void setCurrentSession(SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession) {if (currentSession == null) {this.removeAttribute(SessionRepositoryFilter.CURRENT_SESSION_ATTR);} else {this.setAttribute(SessionRepositoryFilter.CURRENT_SESSION_ATTR, currentSession);}}public String changeSessionId() {HttpSession session = this.getSession(false);if (session == null) {throw new IllegalStateException("Cannot change session ID. There is no session associated with this request.");} else {return this.getCurrentSession().getSession().changeSessionId();}}public boolean isRequestedSessionIdValid() {if (this.requestedSessionIdValid == null) {S requestedSession = this.getRequestedSession();if (requestedSession != null) {requestedSession.setLastAccessedTime(Instant.now());}return this.isRequestedSessionIdValid(requestedSession);} else {return this.requestedSessionIdValid;}}private boolean isRequestedSessionIdValid(S session) {if (this.requestedSessionIdValid == null) {this.requestedSessionIdValid = session != null;}return this.requestedSessionIdValid;}private boolean isInvalidateClientSession() {return this.getCurrentSession() == null && this.requestedSessionInvalidated;}public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = this.getCurrentSession();if (currentSession != null) {return currentSession;} else {S requestedSession = this.getRequestedSession();if (requestedSession != null) {if (this.getAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR) == null) {requestedSession.setLastAccessedTime(Instant.now());this.requestedSessionIdValid = true;currentSession = new SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(requestedSession, this.getServletContext());currentSession.markNotNew();this.setCurrentSession(currentSession);return currentSession;}} else {if (SessionRepositoryFilter.SESSION_LOGGER.isDebugEnabled()) {SessionRepositoryFilter.SESSION_LOGGER.debug("No session found by id: Caching result for getSession(false) for this HttpServletRequest.");}this.setAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR, "true");}if (!create) {return null;} else {if (SessionRepositoryFilter.SESSION_LOGGER.isDebugEnabled()) {SessionRepositoryFilter.SESSION_LOGGER.debug("A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for " + SessionRepositoryFilter.SESSION_LOGGER_NAME, new RuntimeException("For debugging purposes only (not an error)"));}S session = SessionRepositoryFilter.this.sessionRepository.createSession();session.setLastAccessedTime(Instant.now());currentSession = new SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(session, this.getServletContext());this.setCurrentSession(currentSession);return currentSession;}}}public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession() {return this.getSession(true);}public String getRequestedSessionId() {if (this.requestedSessionId == null) {this.getRequestedSession();}return this.requestedSessionId;}public RequestDispatcher getRequestDispatcher(String path) {RequestDispatcher requestDispatcher = super.getRequestDispatcher(path);return new SessionRepositoryFilter.SessionRepositoryRequestWrapper.SessionCommittingRequestDispatcher(requestDispatcher);}private S getRequestedSession() {if (!this.requestedSessionCached) {List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);Iterator var2 = sessionIds.iterator();while(var2.hasNext()) {String sessionId = (String)var2.next();if (this.requestedSessionId == null) {this.requestedSessionId = sessionId;}S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);if (session != null) {this.requestedSession = session;this.requestedSessionId = sessionId;break;}}this.requestedSessionCached = true;}return this.requestedSession;}private void clearRequestedSessionCache() {this.requestedSessionCached = false;this.requestedSession = null;this.requestedSessionId = null;}

5、 SessionRepositoryResponseWrapper

private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {private final SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request;SessionRepositoryResponseWrapper(SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request, HttpServletResponse response) {super(response);if (request == null) {throw new IllegalArgumentException("request cannot be null");} else {this.request = request;}}protected void onResponseCommitted() {this.request.commitSession();}}
5、 SessionRepositoryResponseWrapper ```java
private final class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {private final SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request;SessionRepositoryResponseWrapper(SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper request, HttpServletResponse response) {super(response);if (request == null) {throw new IllegalArgumentException("request cannot be null");} else {this.request = request;}}protected void onResponseCommitted() {this.request.commitSession();}}

从注释上可以看出包装响应时为了:确保如果响应被提交session能够被保存。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/256807.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uni-app 微信小程序之好看的ui登录页面(二)

文章目录 1. 页面效果2. 页面样式代码 更多登录ui页面 uni-app 微信小程序之好看的ui登录页面&#xff08;一&#xff09; uni-app 微信小程序之好看的ui登录页面&#xff08;二&#xff09; uni-app 微信小程序之好看的ui登录页面&#xff08;三&#xff09; uni-app 微信小程…

最新版本——Hadoop3.3.6单机版完全部署指南

大家好&#xff0c;我是独孤风&#xff0c;大数据流动的作者。 本文基于最新的 Hadoop 3.3.6 的版本编写&#xff0c;带大家通过单机版充分了解 Apache Hadoop 的使用。本文更强调实践&#xff0c;实践是大数据学习的重要环节&#xff0c;也能在实践中对该技术有更深的理解&…

如何使用ArcGIS Pro制作类似CAD的尺寸注记

经常使用CAD制图的朋友应该比较熟悉CAD内的尺寸标注&#xff0c;这样的标注看起来直观且简洁&#xff0c;那么在ArcGIS Pro内能不能制作这样尺寸注记呢&#xff0c;答案是肯定的&#xff0c;这里为大家介绍一下制作的方法&#xff0c;希望能对你有所帮助。 数据来源 本教程所…

node.js和浏览器之间的区别

node.js是什么 Node.js是一种基于Chrome V8引擎的JavaScript运行环境&#xff0c;可以在服务器端运行JavaScript代码 Node.js 在浏览器之外运行 V8 JavaScript 引擎。 这使得 Node.js 非常高效。 浏览器如何运行js代码 nodejs运行环境 在浏览器中&#xff0c;大部分时间你所…

SLAM算法与工程实践——SLAM基本库的安装与使用(3):Pangolin库

SLAM算法与工程实践系列文章 下面是SLAM算法与工程实践系列文章的总链接&#xff0c;本人发表这个系列的文章链接均收录于此 SLAM算法与工程实践系列文章链接 下面是专栏地址&#xff1a; SLAM算法与工程实践系列专栏 文章目录 SLAM算法与工程实践系列文章SLAM算法与工程实践…

2021年第十届数学建模国际赛小美赛A题气道阻力的评估解题全过程文档及程序

2021年第十届数学建模国际赛小美赛 A题 气道阻力的评估 原题再现&#xff1a; 气道阻力的定义是通过肺气道产生单位气流所需的经肺压力的变化。更简单地说&#xff0c;它是嘴和肺泡之间的压力差&#xff0c;除以气流。影响气道阻力的因素是多方面的&#xff0c;我们需要探讨这…

Java简易版:UDP协议实现群聊

要先 运行服务端&#xff0c;在运行客户端&#xff0c;否则会报错。 服务端&#xff1a; package 二十一章;import java.io.*; import java.net.*; import java.util.ArrayList; public class T{public static ServerSocket server_socket;public static ArrayList<Socket…

领域驱动架构(DDD)建模

一、背景 常见的软件开发方式是拿到产品需求后&#xff0c;直接考虑数据库中表应该如何设计&#xff0c;这种方式已经将设计与业务需求脱节&#xff0c;而更多的是直接考虑应该如何实现了&#xff0c;这有点本末倒置。而DDD是从领域(问题域)为出发点进行的设计方法。 领域驱动…

记账中心二开

系统预设了 这几种 FSubSystem 为子系统 T_VC_SubSystem 卡片显示的表 字段 FNeedRalteAccount 设置为1的话 &#xff0c;需要与总账连用系统将去查找 系统状态控制表。 如果系统状态 没有配置这个子系统 将无法显示数据 select sysStaCtr.fid from T_BD_SystemStatusCt…

统信UOS_麒麟KYLINOS上跨架构下载离线软件包

原文链接&#xff1a;统信UOS/麒麟KYLINOS上跨架构下载离线软件包 hello&#xff0c;大家好啊&#xff0c;今天给大家带来一篇在统信UOS/麒麟KYLINOS上跨架构下载离线软件包的实用教程。在我们的日常工作中&#xff0c;可能会遇到这样的情况&#xff1a;需要为不同架构的设备下…

STM32 cubeMX 呼吸灯实验

文章代码使用 HAL 库。 文章目录 一、1.PWM原理二、LED 原理图三、使用cubemx 配置 led四、PWM 相关函数五、PWM占空比占空比计算六、PWM 呼吸灯重要代码总结 呼吸灯 一、1.PWM原理 PWM全称为脉冲宽度调制&#xff08;Pulse Width Modulation&#xff09;&#xff0c;是一种常…

【软考】信息系统项目管理师论文方向猜想

报喜不报忧&#xff0c;每天都在为鸡零狗碎推诿扯皮&#xff0c;属实是有辱师门。 通过软考&#xff0c;目前算是真正有意义的事情。 虽然都说高项的论文是个玄学&#xff0c;但是道听途说了一些通关感想还是蛮有启发的。 文件要求 参考了一份广西省高级工程师评审的文件&am…