Http 持久连接与 HttpClient 连接池

转载自  Http 持久连接与 HttpClient 连接池

一、背景

HTTP协议是无状态的协议,即每一次请求都是互相独立的。因此它的最初实现是,每一个http请求都会打开一个tcp socket连接,当交互完毕后会关闭这个连接。

HTTP协议是全双工的协议,所以建立连接与断开连接是要经过三次握手与四次挥手的。显然在这种设计中,每次发送Http请求都会消耗很多的额外资源,即连接的建立与销毁。

于是,HTTP协议的也进行了发展,通过持久连接的方法来进行socket连接复用。

从图中可以看到:

  1. 在串行连接中,每次交互都要打开关闭连接

  2. 在持久连接中,第一次交互会打开连接,交互结束后连接并不关闭,下次交互就省去了建立连接的过程。

持久连接的实现有两种:HTTP/1.0+的keep-alive与HTTP/1.1的持久连接。

 

二、HTTP/1.0+的Keep-Alive

从1996年开始,很多HTTP/1.0浏览器与服务器都对协议进行了扩展,那就是“keep-alive”扩展协议。

注意,这个扩展协议是作为1.0的补充的“实验型持久连接”出现的。keep-alive已经不再使用了,最新的HTTP/1.1规范中也没有对它进行说明,只是很多应用延续了下来。

使用HTTP/1.0的客户端在首部中加上”Connection:Keep-Alive”,请求服务端将一条连接保持在打开状态。服务端如果愿意将这条连接保持在打开状态,就会在响应中包含同样的首部。如果响应中没有包含”Connection:Keep-Alive”首部,则客户端会认为服务端不支持keep-alive,会在发送完响应报文之后关闭掉当前连接。

通过keep-alive补充协议,客户端与服务器之间完成了持久连接,然而仍然存在着一些问题:

在HTTP/1.0中keep-alive不是标准协议,客户端必须发送Connection:Keep-Alive来激活keep-alive连接。

代理服务器可能无法支持keep-alive,因为一些代理是”盲中继”,无法理解首部的含义,只是将首部逐跳转发。所以可能造成客户端与服务端都保持了连接,但是代理不接受该连接上的数据。

 

三、HTTP/1.1的持久连接

HTTP/1.1采取持久连接的方式替代了Keep-Alive。

HTTP/1.1的连接默认情况下都是持久连接。如果要显式关闭,需要在报文中加上Connection:Close首部。即在HTTP/1.1中,所有的连接都进行了复用。

然而如同Keep-Alive一样,空闲的持久连接也可以随时被客户端与服务端关闭。不发送Connection:Close不意味着服务器承诺连接永远保持打开。

 

四、HttpClient如何生成持久连接

HttpClien中使用了连接池来管理持有连接,同一条TCP链路上,连接是可以复用的。HttpClient通过连接池的方式进行连接持久化。

其实“池”技术是一种通用的设计,其设计思想并不复杂:

  1. 当有连接第一次使用的时候建立连接

  2. 结束时对应连接不关闭,归还到池中

  3. 下次同个目的的连接可从池中获取一个可用连接

  4. 定期清理过期连接

所有的连接池都是这个思路,不过我们看HttpClient源码主要关注两点:

  • 连接池的具体设计方案,以供以后自定义连接池参考

  • 如何与HTTP协议对应上,即理论抽象转为代码的实现

 

4.1 HttpClient连接池的实现

HttpClient关于持久连接的处理在下面的代码中可以集中体现,下面从MainClientExec摘取了和连接池相关的部分,去掉了其他部分:

public class MainClientExec implements ClientExecChain {@Overridepublic CloseableHttpResponse execute(final HttpRoute route,final HttpRequestWrapper request,final HttpClientContext context,final HttpExecutionAware execAware) throws IOException, HttpException {//从连接管理器HttpClientConnectionManager中获取一个连接请求ConnectionRequestfinal ConnectionRequest connRequest = connManager.requestConnection(route, userToken);final HttpClientConnection managedConn;final int timeout = config.getConnectionRequestTimeout();//从连接请求ConnectionRequest中获取一个被管理的连接HttpClientConnectionmanagedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);//将连接管理器HttpClientConnectionManager与被管理的连接HttpClientConnection交给一个ConnectionHolder持有final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);try {HttpResponse response;if (!managedConn.isOpen()) {//如果当前被管理的连接不是出于打开状态,需要重新建立连接establishRoute(proxyAuthState, managedConn, route, request, context);}//通过连接HttpClientConnection发送请求response = requestExecutor.execute(request, managedConn, context);//通过连接重用策略判断是否连接可重用         if (reuseStrategy.keepAlive(response, context)) {//获得连接有效期final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);//设置连接有效期connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);//将当前连接标记为可重用状态connHolder.markReusable();} else {connHolder.markNonReusable();}}final HttpEntity entity = response.getEntity();if (entity == null || !entity.isStreaming()) {//将当前连接释放到池中,供下次调用connHolder.releaseConnection();return new HttpResponseProxy(response, null);} else {return new HttpResponseProxy(response, connHolder);}}

这里看到了在Http请求过程中对连接的处理是和协议规范是一致的,这里要展开讲一下具体实现。

PoolingHttpClientConnectionManager是HttpClient默认的连接管理器,首先通过requestConnection()获得一个连接的请求,注意这里不是连接。

public ConnectionRequest requestConnection(final HttpRoute route,final Object state) {final Future<CPoolEntry> future = this.pool.lease(route, state, null);return new ConnectionRequest() {@Overridepublic boolean cancel() {return future.cancel(true);}@Overridepublic HttpClientConnection get(final long timeout,final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {final HttpClientConnection conn = leaseConnection(future, timeout, tunit);if (conn.isOpen()) {final HttpHost host;if (route.getProxyHost() != null) {host = route.getProxyHost();} else {host = route.getTargetHost();}final SocketConfig socketConfig = resolveSocketConfig(host);conn.setSocketTimeout(socketConfig.getSoTimeout());}return conn;}};}

可以看到返回的ConnectionRequest对象实际上是一个持有了Future<CPoolEntry>,CPoolEntry是被连接池管理的真正连接实例。

从上面的代码我们应该关注的是:

  • Future<CPoolEntry> future = this.pool.lease(route, state, null)

    如何从连接池CPool中获得一个异步的连接,Future<CPoolEntry>

  • HttpClientConnection conn = leaseConnection(future, timeout, tunit)

    如何通过异步连接Future<CPoolEntry>获得一个真正的连接HttpClientConnection

 

4.2 Future<CPoolEntry>

看一下CPool是如何释放一个Future<CPoolEntry>的,AbstractConnPool核心代码如下:

private E getPoolEntryBlocking(final T route, final Object state,final long timeout, final TimeUnit tunit,final Future<E> future) throws IOException, InterruptedException, TimeoutException {//首先对当前连接池加锁,当前锁是可重入锁ReentrantLockthis.lock.lock();try {//获得一个当前HttpRoute对应的连接池,对于HttpClient的连接池而言,总池有个大小,每个route对应的连接也是个池,所以是“池中池”final RouteSpecificPool<T, C, E> pool = getPool(route);E entry;for (;;) {Asserts.check(!this.isShutDown, "Connection pool shut down");//死循环获得连接for (;;) {//从route对应的池中拿连接,可能是null,也可能是有效连接entry = pool.getFree(state);//如果拿到null,就退出循环if (entry == null) {break;}//如果拿到过期连接或者已关闭连接,就释放资源,继续循环获取if (entry.isExpired(System.currentTimeMillis())) {entry.close();}if (entry.isClosed()) {this.available.remove(entry);pool.free(entry, false);} else {//如果拿到有效连接就退出循环break;}}//拿到有效连接就退出if (entry != null) {this.available.remove(entry);this.leased.add(entry);onReuse(entry);return entry;}//到这里证明没有拿到有效连接,需要自己生成一个                final int maxPerRoute = getMax(route);//每个route对应的连接最大数量是可配置的,如果超过了,就需要通过LRU清理掉一些连接final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);if (excess > 0) {for (int i = 0; i < excess; i++) {final E lastUsed = pool.getLastUsed();if (lastUsed == null) {break;}lastUsed.close();this.available.remove(lastUsed);pool.remove(lastUsed);}}//当前route池中的连接数,没有达到上线if (pool.getAllocatedCount() < maxPerRoute) {final int totalUsed = this.leased.size();final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);//判断连接池是否超过上线,如果超过了,需要通过LRU清理掉一些连接if (freeCapacity > 0) {final int totalAvailable = this.available.size();//如果空闲连接数已经大于剩余可用空间,则需要清理下空闲连接if (totalAvailable > freeCapacity - 1) {if (!this.available.isEmpty()) {final E lastUsed = this.available.removeLast();lastUsed.close();final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());otherpool.remove(lastUsed);}}//根据route建立一个连接final C conn = this.connFactory.create(route);//将这个连接放入route对应的“小池”中entry = pool.add(conn);//将这个连接放入“大池”中this.leased.add(entry);return entry;}}//到这里证明没有从获得route池中获得有效连接,并且想要自己建立连接时当前route连接池已经到达最大值,即已经有连接在使用,但是对当前线程不可用boolean success = false;try {if (future.isCancelled()) {throw new InterruptedException("Operation interrupted");}//将future放入route池中等待pool.queue(future);//将future放入大连接池中等待this.pending.add(future);//如果等待到了信号量的通知,success为trueif (deadline != null) {success = this.condition.awaitUntil(deadline);} else {this.condition.await();success = true;}if (future.isCancelled()) {throw new InterruptedException("Operation interrupted");}} finally {//从等待队列中移除pool.unqueue(future);this.pending.remove(future);}//如果没有等到信号量通知并且当前时间已经超时,则退出循环if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) {break;}}//最终也没有等到信号量通知,没有拿到可用连接,则抛异常throw new TimeoutException("Timeout waiting for connection");} finally {//释放对大连接池的锁this.lock.unlock();}}

上面的代码逻辑有几个重要点:

  • 连接池有个最大连接数,每个route对应一个小连接池,也有个最大连接数

  • 不论是大连接池还是小连接池,当超过数量的时候,都要通过LRU释放一些连接

  • 如果拿到了可用连接,则返回给上层使用

  • 如果没有拿到可用连接,HttpClient会判断当前route连接池是否已经超过了最大数量,没有到上限就会新建一个连接,并放入池中

  • 如果到达了上限,就排队等待,等到了信号量,就重新获得一次,等待不到就抛超时异常

  • 通过线程池获取连接要通过ReetrantLock加锁,保证线程安全

到这里为止,程序已经拿到了一个可用的CPoolEntry实例,或者抛异常终止了程序。

 

4.3 HttpClientConnection

protected HttpClientConnection leaseConnection(final Future<CPoolEntry> future,final long timeout,final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {final CPoolEntry entry;try {//从异步操作Future<CPoolEntry>中获得CPoolEntryentry = future.get(timeout, tunit);if (entry == null || future.isCancelled()) {throw new InterruptedException();}Asserts.check(entry.getConnection() != null, "Pool entry with no connection");if (this.log.isDebugEnabled()) {this.log.debug("Connection leased: " + format(entry) + formatStats(entry.getRoute()));}//获得一个CPoolEntry的代理对象,对其操作都是使用同一个底层的HttpClientConnectionreturn CPoolProxy.newProxy(entry);} catch (final TimeoutException ex) {throw new ConnectionPoolTimeoutException("Timeout waiting for connection from pool");}}

 

五、HttpClient如何复用持久连接?

在上一章中,我们看到了HttpClient通过连接池来获得连接,当需要使用连接的时候从池中获得。

对应着第三章的问题:

  1. 当有连接第一次使用的时候建立连接

  2. 结束时对应连接不关闭,归还到池中

  3. 下次同个目的的连接可从池中获取一个可用连接

  4. 定期清理过期连接

我们在第四章中看到了HttpClient是如何处理1、3的问题的,那么第2个问题是怎么处理的呢?

即HttpClient如何判断一个连接在使用完毕后是要关闭,还是要放入池中供他人复用?再看一下MainClientExec的代码

//发送Http连接response = requestExecutor.execute(request, managedConn, context);//根据重用策略判断当前连接是否要复用if (reuseStrategy.keepAlive(response, context)) {//需要复用的连接,获取连接超时时间,以response中的timeout为准final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);if (this.log.isDebugEnabled()) {final String s;//timeout的是毫秒数,如果没有设置则为-1,即没有超时时间if (duration > 0) {s = "for " + duration + " " + TimeUnit.MILLISECONDS;} else {s = "indefinitely";}this.log.debug("Connection can be kept alive " + s);}//设置超时时间,当请求结束时连接管理器会根据超时时间决定是关闭还是放回到池中connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);//将连接标记为可重用connHolder.markReusable();} else {//将连接标记为不可重用connHolder.markNonReusable();}

可以看到,当使用连接发生过请求之后,有连接重试策略来决定该连接是否要重用,如果要重用就会在结束后交给HttpClientConnectionManager放入池中。

那么连接复用策略的逻辑是怎么样的呢?

public class DefaultClientConnectionReuseStrategy extends DefaultConnectionReuseStrategy {public static final DefaultClientConnectionReuseStrategy INSTANCE = new DefaultClientConnectionReuseStrategy();@Overridepublic boolean keepAlive(final HttpResponse response, final HttpContext context) {//从上下文中拿到requestfinal HttpRequest request = (HttpRequest) context.getAttribute(HttpCoreContext.HTTP_REQUEST);if (request != null) {//获得Connection的Headerfinal Header[] connHeaders = request.getHeaders(HttpHeaders.CONNECTION);if (connHeaders.length != 0) {final TokenIterator ti = new BasicTokenIterator(new BasicHeaderIterator(connHeaders, null));while (ti.hasNext()) {final String token = ti.nextToken();//如果包含Connection:Close首部,则代表请求不打算保持连接,会忽略response的意愿,该头部这是HTTP/1.1的规范if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {return false;}}}}//使用父类的的复用策略return super.keepAlive(response, context);}}

看一下父类的复用策略

if (canResponseHaveBody(request, response)) {final Header[] clhs = response.getHeaders(HTTP.CONTENT_LEN);//如果reponse的Content-Length没有正确设置,则不复用连接//因为对于持久化连接,两次传输之间不需要重新建立连接,则需要根据Content-Length确认内容属于哪次请求,以正确处理“粘包”现象//所以,没有正确设置Content-Length的response连接不能复用if (clhs.length == 1) {final Header clh = clhs[0];try {final int contentLen = Integer.parseInt(clh.getValue());if (contentLen < 0) {return false;}} catch (final NumberFormatException ex) {return false;}} else {return false;}}if (headerIterator.hasNext()) {try {final TokenIterator ti = new BasicTokenIterator(headerIterator);boolean keepalive = false;while (ti.hasNext()) {final String token = ti.nextToken();//如果response有Connection:Close首部,则明确表示要关闭,则不复用if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {return false;//如果response有Connection:Keep-Alive首部,则明确表示要持久化,则复用} else if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(token)) {keepalive = true;}}if (keepalive) {return true;}} catch (final ParseException px) {return false;}}//如果response中没有相关的Connection首部说明,则高于HTTP/1.0版本的都复用连接  return !ver.lessEquals(HttpVersion.HTTP_1_0);

总结一下:

  • 如果request首部中包含Connection:Close,不复用

  • 如果response中Content-Length长度设置不正确,不复用

  • 如果response首部包含Connection:Close,不复用

  • 如果reponse首部包含Connection:Keep-Alive,复用

  • 都没命中的情况下,如果HTTP版本高于1.0则复用

从代码中可以看到,其实现策略与我们第二、三章协议层的约束是一致的。

 

六、HttpClient如何清理过期连接

在HttpClient4.4版本之前,在从连接池中获取重用连接的时候会检查下是否过期,过期则清理。

之后的版本则不同,会有一个单独的线程来扫描连接池中的连接,发现有离最近一次使用超过设置的时间后,就会清理。默认的超时时间是2秒钟。

public CloseableHttpClient build() {//如果指定了要清理过期连接与空闲连接,才会启动清理线程,默认是不启动的if (evictExpiredConnections || evictIdleConnections) {//创造一个连接池的清理线程final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,maxIdleTime, maxIdleTimeUnit);closeablesCopy.add(new Closeable() {@Overridepublic void close() throws IOException {connectionEvictor.shutdown();try {connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS);} catch (final InterruptedException interrupted) {Thread.currentThread().interrupt();}}});//执行该清理线程connectionEvictor.start();}

可以看到在HttpClientBuilder进行build的时候,如果指定了开启清理功能,会创建一个连接池清理线程并运行它。

public IdleConnectionEvictor(final HttpClientConnectionManager connectionManager,final ThreadFactory threadFactory,final long sleepTime, final TimeUnit sleepTimeUnit,final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {this.connectionManager = Args.notNull(connectionManager, "Connection manager");this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;this.thread = this.threadFactory.newThread(new Runnable() {@Overridepublic void run() {try {//死循环,线程一直执行while (!Thread.currentThread().isInterrupted()) {//休息若干秒后执行,默认10秒Thread.sleep(sleepTimeMs);//清理过期连接connectionManager.closeExpiredConnections();//如果指定了最大空闲时间,则清理空闲连接if (maxIdleTimeMs > 0) {connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);}}} catch (final Exception ex) {exception = ex;}}});}

总结一下:

  • 只有在HttpClientBuilder手动设置后,才会开启清理过期与空闲连接

  • 手动设置后,会启动一个线程死循环执行,每次执行sleep一定时间,调用HttpClientConnectionManager的清理方法清理过期与空闲连接。

 

七、本文总结

  • HTTP协议通过持久连接的方式,减轻了早期设计中的过多连接问题

  • 持久连接有两种方式:HTTP/1.0+的Keep-Avlive与HTTP/1.1的默认持久连接

  • HttpClient通过连接池来管理持久连接,连接池分为两个,一个是总连接池,一个是每个route对应的连接池

  • HttpClient通过异步的Future<CPoolEntry>来获取一个池化的连接

  • 默认连接重用策略与HTTP协议约束一致,根据response先判断Connection:Close则关闭,在判断Connection:Keep-Alive则开启,最后版本大于1.0则开启

  • 只有在HttpClientBuilder中手动开启了清理过期与空闲连接的开关后,才会清理连接池中的连接

  • HttpClient4.4之后的版本通过一个死循环线程清理过期与空闲连接,该线程每次执行都sleep一会,以达到定期执行的效果

 

上面的研究是基于HttpClient源码的个人理解,如果有误,希望大家积极留言讨论。

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

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

相关文章

jdbc解析excel文件,批量插入数据至库中

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂”前言现在是&#xff1a;2022年5月20日09:32:38今天遇到了个这样的需求&#xff0c;解析excel表中的数据&#xff0c;以JDBC的方式&#xff0c;将数据批量更新至不同的数据表中。注意&…

线程2

public class test{/*** 测试延迟继承* param args*/public static void main(String[] args) {Cat catnew Cat();cat.start();//启动线程&#xff0c;会导致run函数的运行Dog dognew Dog();//创建一个线程对象Thread tnew Thread(dog);t.start();}} //继承Thread创建线程 clas…

.NET及.NET Core系统架构

.NET 官方架构指南 Microservices and Docker Containers Web Applications with ASP.NET 官网地址&#xff1a;https://www.microsoft.com/net/learn/architecture 三层及多层架构 Multitier Architecture ASP.NET N-Tier Architecture Schema Visual Studio N-Tier Examp…

Spring Boot 自动配置的 “魔法” 是如何实现的?

转载自 Spring Boot 自动配置的 “魔法” 是如何实现的&#xff1f; Spring Boot是Spring旗下众多的子项目之一&#xff0c;其理念是约定优于配置&#xff0c;它通过实现了自动配置&#xff08;大多数用户平时习惯设置的配置作为默认配置&#xff09;的功能来为用户快速构建出…

解决vue登录信息不及时更新问题

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂 前言 现在是&#xff1a;2022年5月20日09:59:34 前面写过一篇文章&#xff0c;基于bladex框架实现的模拟登录&#xff0c;后来在测试的过程中发现了个问题&#xff0c;即A系统在跳转到本系…

学习ASP.NET Core,怎能不了解请求处理管道[2]: 服务器在管道中的“龙头”地位

ASP.NET Core管道由注册的服务器和一系列中间件构成。我们在上一篇中深入剖析了中间件&#xff0c;现在我们来了解一下服务器。服务器是ASP .NET Core管道的第一个节点&#xff0c;它负责完整请求的监听和接收&#xff0c;最终对请求的响应同样也由它完成。[本文已经同步到《AS…

for循环(二)

利用for循环按规律打出星星 #include<stdio.h>main(){int i,j;for(i0;i<10;i){for(j0;j<i;j){printf(" *"); }printf("\n");} }

接口方法上的注解无法被@Aspect声明的切面拦截的原因分析

转载自 接口方法上的注解无法被Aspect声明的切面拦截的原因分析 前言 在Spring中使用MyBatis的Mapper接口自动生成时&#xff0c;用一个自定义的注解标记在Mapper接口的方法中&#xff0c;再利用Aspect定义一个切面&#xff0c;拦截这个注解以记录日志或者执行时长。但是惊奇…

springboot实现用户统一认证、管理(单点登录)

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂 前言 现在是&#xff1a;2022年5月25日13:44:16 最近和模拟登录杠上了&#xff0c;这不&#xff0c;又来了个需求&#xff0c;还是以这个技术点入手的。 需求大概是这样的&#xff1a;为了…

学习ASP.NET Core,怎能不了解请求处理管道[1]: 中间件究竟是个什么东西?

ASP.NET Core管道虽然在结构组成上显得非常简单&#xff0c;但是在具体实现上却涉及到太多的对象&#xff0c;所以我们在 “通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程”&#xff08;上篇、中篇、下篇&#xff09; 中围绕着一个经过极度简化的模拟管道讲述…

springboot实现用户统一认证、管理

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂”前言现在是&#xff1a;2022年5月25日13:44:16最近和模拟登录杠上了&#xff0c;这不&#xff0c;又来了个需求&#xff0c;还是以这个技术点入手的。需求大概是这样的&#xff1a;为了统…

Mybatis 使用的 9 种设计模式,真是太有用了

转载自 Mybatis 使用的 9 种设计模式&#xff0c;真是太有用了 虽然我们都知道有26个设计模式&#xff0c;但是大多停留在概念层面&#xff0c;真实开发中很少遇到&#xff0c;Mybatis源码中使用了大量的设计模式&#xff0c;阅读源码并观察设计模式在其中的应用&#xff0c;…

springboot实现用户统一认证、管理-前端实现

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂 前言 现在是&#xff1a;2022年6月2日15:43:51 上篇文章讲述了springboot中实现用户统一认证的具体内容&#xff0c;主要从后端角度出发的&#xff0c;其实大部分功能还是前端与后端交互的…

Unity3damp;amp;C#分布式游戏服务器ET框架介绍-组件式设计

前几天写了《开源分享 Unity3d客户端与C#分布式服务端游戏框架》&#xff0c;受到很多人关注&#xff0c;QQ群几天就加了80多个人。开源这个框架的主要目的也是分享自己设计ET的一些想法&#xff0c;所以我准备写一系列的文章&#xff0c;介绍下自己的思路跟设计&#xff0c;每…

springboot+vue实现用户统一认证、管理-前端实现

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂”前言现在是&#xff1a;2022年6月2日15:43:51上篇文章讲述了springboot中实现用户统一认证的具体内容&#xff0c;主要从后端角度出发的&#xff0c;其实大部分功能还是前端与后端交互的…

JS中 [] == ![]结果为true,而 {} == !{}却为false, 追根刨底

转载自 JS中 [] ![]结果为true&#xff0c;而 {} !{}却为false&#xff0c; 追根刨底 console.log( [] ![] ) // true console.log( {} !{} ) // false 在比较字符串、数值和布尔值的相等性时&#xff0c;问题还比较简单。但在涉及到对象的比较时&#xff0c;问题就变…

Centos7 amp;amp; Docker amp;amp; Jenkins amp;amp; ASP.NET Core

写在前面 Docker一直很火热&#xff0c;一直想把原本的Jenkins自动部署工具搬到Docker上面&#xff0c;无奈今年一直忙于各种事情&#xff0c;迟迟未实施这个事情&#xff0c;正好迎来了dotnet core 2.0 的正式发布&#xff0c;升级项目的同时&#xff0c;顺便直接将Jenkins搬到…

国民体质测定标准手册及标准解析成JSON文件计算分数,java解析excel文件

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂 前言 现在是&#xff1a;2022年6月14日10:07:27 最近在做体质测评的功能&#xff0c;需要依据《国民体质测定标准手册及标准》&#xff0c;根据用户的个人信息&#xff0c;从而计算出各个…

getchar与putchar用法

#include<stdio.h>main(){int i;igetchar();//相当于char i;scanf("%c",&i); putchar(i);//相当于printf("%c",i); 需要i是字符才能输出不能是变量printf("\n");printf("%d",i);}输出结果一致 #include<stdio.h>main…

TCP为什么是三次握手和四次挥手

转载自 TCP为什么是三次握手和四次挥手 为什么建立连接是三次握手断开连接是四次挥手&#xff1f; 三次握手的流程和四次挥手的流程是什么&#xff1f; 三次握手与四次回收分别对应TCP连接与断开过程 tcp报文格式 标志位含义 ACK&#xff1a;确认序号有效。 SYN&#x…