👉使用 JDK 的api 发起http 请求示例。http 请求连接超时其实是 Socket 连接超时。
URL url = new URL("https://raw.github.com/square/okhttp/master/README.md");
// 真正的网络请求通过 sun.net.www.protocol.https.HttpsClient 实现
// 底层通过Socket 来建立网络连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 作为方法参数,透传调用 java.net.Socket#connect(endpoint, timeout)
connection.setConnectTimeout(100);
openConnection 并不会发起网络连接。只有主动调用 connect(),或者获取response 相关的操作,才会发起网络通信。socket 连接建立通过DualStackPlainSocketImpl 实现。
参考:sun.net.www.protocol.http.HttpURLConnection#connect
DualStackPlainSocketImpl
是 Java 中用于实现双栈 (IPv4/IPv6) 套接字的一个内部类。
/**
* 超时网络连接实现
* 如果设置超时时间,首先设置操作系统socket 非阻塞模式,然后等待timeout获取socket 状态。决定是否抛中断异常
* @param timeout 来自于 setConnectTimeout🎈
* @see DualStackPlainSocketImpl#socketConnect(InetAddress, int, int)
*/
void socketConnect(InetAddress address, int port, int timeout) throws IOException {
if (timeout <= 0) {
connectResult = connect0(nativefd, address, port);
} else {
// 设置I/O为非阻塞模式,对应JNI Java_java_net_DualStackPlainSocketImpl_configureBlocking
configureBlocking(nativefd, false);
try {
// 非阻塞模式,直接返回
connectResult = connect0(nativefd, address, port);
if (connectResult == WOULDBLOCK) {
// 借助操作系统select api,实现超时等待。如果没有连接成功,则抛出异常
// 对应JNI Java_java_net_DualStackPlainSocketImpl_waitForConnect
waitForConnect(nativefd, timeout);
}
}
}
}
通过使用计时器任务和中断机制,实现了对客户端执行HTTP请求超时的管理。
如果请求执行时间超过了设定的超时时间,则自动中断请求。
有效地避免因为网络问题或服务器响应慢导致的客户端线程长时间挂起的问题。
常用的资源管理方案,启动线程异步监控和中断工作任务。🎈
/**
* 使用计时器方案,当http 请求超时,中断操作。
* @see com.amazonaws.http.timers.client.ClientExecutionTimer 中断计时器
* @see 源码
*/
private Response<Output> executeWithTimer() throws InterruptedException {
// 启动一个计时器(异步线程),这个计时器在客户端执行超时时会被触发。
ClientExecutionAbortTrackerTask clientExecutionTrackerTask =
clientExecutionTimer.startTimer(getClientExecutionTimeout(requestConfig));
try {
executionContext.setClientExecutionTrackerTask(clientExecutionTrackerTask);
// 执行正常的 http 请求
return doExecute();
} finally {
// 取消计时器任务,避免不必要的中断。
executionContext.getClientExecutionTrackerTask().cancelTask();
}
}
/**
* 启动一个计时器任务(定时调度),当客户端执行超过指定的超时时间时,这个任务会被执行。
* @see com.amazonaws.http.timers.client.ClientExecutionTimer#scheduleTimerTask 源码
*/
private ClientExecutionAbortTrackerTask scheduleTimerTask(int clientExecutionTimeoutMillis) {
// 执行中断当前线程并中止HTTP请求。
ClientExecutionAbortTask timerTask = new ClientExecutionAbortTaskImpl(Thread.currentThread());
// 调度延迟任务
ScheduledFuture<?> timerTaskFuture = executor.schedule(timerTask, clientExecutionTimeoutMillis,
TimeUnit.MILLISECONDS);
return new ClientExecutionAbortTrackerTaskImpl(timerTask, timerTaskFuture);
}
/**
* @see com.amazonaws.http.timers.client.ClientExecutionAbortTaskImpl 源码
*/
public class ClientExecutionAbortTaskImpl implements ClientExecutionAbortTask {
// 存储当前正在执行的HTTP请求 和任务线程,以便在需要时可以被中止。
private HttpRequestBase currentHttpRequest;
private final Thread thread;
/**
* 触发时中断调用线程并中止HTTP请求。
*/
public void run() {
if (!thread.isInterrupted()) {
thread.interrupt();
}
if (!currentHttpRequest.isAborted()) {
// 调用httpclient abortConnection()。
// Closes this socket.
currentHttpRequest.abort();
}
}
}
调用后端接口,发现接口返回的数据有两组 json, 非数组。非常奇怪🙉
{
"name": "John",
"age": 30
}
{
"path": "/examples/servlets/servlet/JsonExample",
"code": 500
}
/**
* Servlet 包版本问题,导致 SpringMVC Servlet 打印日志报错
* @see org.springframework.web.servlet.FrameworkServlet#processRequest
*/
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) {
try {
doService(request, response);
}
catch (ServletException | IOException ex) {
}
finally {
// HttpStatus httpStatus = HttpStatus.resolve(status); since 5.0 才有这个方法 ❌
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
第一个 json 是业务逻辑返回内容。controller 逻辑没有发生异常,正常写入 response。
第二个 json 是 SpringMVC Servlet 发生异常后,由 Tomcat ErrorPage 转发到默认的错误处理 Servlet 生成的内容。
两个 json 内容通过 RequestDispatcher.include 方法,都写入到 response。
借助 Tomcat examples 应用,可以快速搭建案例场景。✔
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
// Servlet 中返回 JSON 字符串
public class JsonServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
String json = "{\"name\": \"John\", \"age\": 30}";
PrintWriter out = response.getWriter();
out.print(json);
// 业务内容写入 response, response 变为 submit 状态。
out.flush();
// 模拟上述 SpringMVC 异常
if(true) {
throw new RuntimeException("Bad area ref ");
}
}
}
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
// 对应 SpringBoot 提供的 BasicErrorController
public class ErrorServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String path = request.getRequestURI();
String json = "{\"path\": \"" + path + "\", \"code\": 500}";
PrintWriter out = response.getWriter();
out.print(json);
}
}
<servlet>
<servlet-name>JsonExample</servlet-name>
<servlet-class>JsonServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>JsonExample</servlet-name>
<url-pattern>/servlets/servlet/JsonExample</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>ErrorExample</servlet-name>
<servlet-class>ErrorServlet</servlet-class>
</servlet>
<!-- 对应的注册过程: org.springframework.boot.autoconfigure.web.BasicErrorController -->
<servlet-mapping>
<servlet-name>ErrorExample</servlet-name>
<url-pattern>/error</url-pattern>
</servlet-mapping>
<!-- 对应: ErrorPageRegistry.addErrorPages(errorPage); -->
<error-page>
<exception-type>java.lang.RuntimeException</exception-type>
<location>/error</location>
</error-page>
cd /d/services/apache-tomcat-8.0.48/webapps/examples/WEB-INF/classes
# /D/repository/javax/servlet/servlet-api/2.4/servlet-api-2.4.jar
javac -cp servlet-api-2.4.jar JsonServlet.java
javac -cp servlet-api-2.4.jar ErrorServlet.java
运行 Tomcat。 访问链接 http://localhost:8080/examples/servlets/servlet/JsonExample
基于《Insight h2database 更新、读写锁以及事务原理》对于更新流程有了深入了解。在独占锁的简单模型上,分析 h2database 基于乐观锁(并发控制机制)的行锁锁定机制。
验证 h2database MVCC 机制,需要有并发的环境,不能再使用 org.h2.tools.Shell
。可以使用 debug 模式,运行 org.h2.tools.Console
。使用不同的浏览器模拟多 session
-- session 1 更新数据并打标
SET AUTOCOMMIT OFF;
update city set code = 'bjx' where id = 9;
-- session 2 读取数据正常
select * from city where id = 9;
-- session 2 更新数据异常,提示 Timeout,其实是并发更新冲突异常
update city set code = 'sjx' where id = 9;
Timeout trying to lock table "CITY";
上述的并发异常,在内部的错误代码为:org.h2.api.ErrorCode#CONCURRENT_UPDATE_1 trying to update the same row from within two connections at the same time
在使用 vue 本地开发前后端功能时,发现配置的代理不能正确的请求到后端服务。
通过观察 nodeJs 日志,发现代理 path rewrite 的路径有问题。
借此阅读 webpack-dev-server 源码,了解其工作原理。
前端 vue 工程的 vue.config.js 代理配置如下:
module.exports = {
devServer: {
proxy: {
// 转发到稳定的线上服务
'/someApi': {
target: "online.foo.com",
changeOrigin: true,
pathRewrite: {
'^/someApi': ''
}
},
// 转发到开发中的服务。
// 由于 devServer 路由规则,导致前端发起的 /someApiExtend/something 请求,并没有转发到 development.foo.com。
'/someApiExtend': {
target: "development.foo.com",
changeOrigin: true,
pathRewrite: {
'^/someApiExtend': ''
}
}
}
}
}
/someApiExtend/something 请求被 ‘/someApi’ 优先处理,Rewrite to 的请求解析为:Extend/something。 直接提示 404。❌
Serves a webpack app. 提供开发环境服务
Updates the browser on changes. 热编译和部署
webpack-dev-server 可用于快速开发应用程序。请查阅 开发指南 开始使用。
其中提到了使用到 http-proxy-middleware 软件包。
通过源码分析可以看到集成使用的方式
/**
* 根据 options 解析集成的组件,注册 express web应用中。
* @returns {void}
* @see 核心实现源码文件 lib\Server.js
*/
setupMiddlewares() {
/**
* @type {Array<Middleware>} 需要的组件集合
*/
let middlewares = [];
// 简单case, 如果配置了压缩,则初始化并加入到组件集合。最终注册到 express。 express.use(compression())
// compress is placed last and uses unshift so that it will be the first middleware used
if (this.options.compress) {
const compression = require("compression");
middlewares.push({ name: "compression", middleware: compression() });
}
// 如上的 compress, 引入并初始化代理配置 createProxyMiddleware
if (this.options.proxy) {
const { createProxyMiddleware } = require("http-proxy-middleware");
const getProxyMiddleware = (proxyConfig) => {
// It is possible to use the `bypass` method without a `target` or `router`.
// However, the proxy middleware has no use in this case, and will fail to instantiate.
if (proxyConfig.target) {
const context = proxyConfig.context || proxyConfig.path;
return createProxyMiddleware(
/** @type {string} */ (context),
proxyConfig
);
}
};
/**
* 遍历 devServer.proxy 配置,并初始化为 proxyMiddleware对象,添加到组件合集里。
*/
/** @type {ProxyConfigArray} */
(this.options.proxy).forEach((proxyConfigOrCallback) => {
// 代理配置集合,
let proxyConfig = typeof proxyConfigOrCallback === "function" ? proxyConfigOrCallback() : proxyConfigOrCallback;
let proxyMiddleware = (getProxyMiddleware(proxyConfig));
middlewares.push({
name: "http-proxy-middleware",
// 通过 handler 初始化配置,简单来说,就是调用 getProxyMiddleware(proxyConfig)
middleware: handler,
});
});
}
// 把组件集合注册到 express web 应用中
middlewares.forEach((middleware) => {
/** @type {import("express").Application} */
(this.app).use(middleware.middleware);
});
}
首先,判断是否启用 proxy, 并引入 http-proxy-middleware
组件
然后,遍历 devServer.proxy 集合配置,生成对应的代理配置对象
最后,把 proxy 配置对象(proxyMiddleware)注册到 express
web 应用中
The one-liner node.js http-proxy middleware for connect, express, next.js
方便理解 webpack-dev-server 集成 http-proxy-middleware 软件包的过程
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use(
'/api',
createProxyMiddleware({
target: 'http://www.example.org/secret',
changeOrigin: true,
}),
);
’/’ matches any path, all requests will be proxied.
‘/api’ matches paths starting with /api
✔ 遇到的疑问,此刻得以解答。
webpack-dev-server 基于 express web 框架实现的一个便于开发环境的 Sever
devServer.proxy 功能是其中一个特性,通过 http-proxy-middleware 实现
proxy 配置path 的规则,参考 Context matching 即可。
webpack-dev-server 的代码结构和实现,对于架构设计具有很大的借鉴意义。👍