开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Net 升级到 6 尝试

发表于 2021-11-29

.Net 的新版本,LTS 的 6 发布了。手头的基于 LTS 3.2 或者 5 的项目可以考虑进行升级了。以下是将自己的项目升级的尝试的记录。

为什么要升级

随着 .Net 6 一起发布的还有 VS 2022、PowerShell 7.2 和 C# 10,它们带来了一些列新的功能和更好的性能,要想享受这些,当然是需要一起升级的。以下是它们一些吸引我的点。

  1. 64 位 IDE。64 位意味着 VS 不在受限于仅仅 4G 的可用内存,大大提高打开、编辑、运行和调试项目的时间成本。
  2. 性能提升。.Net 6 在性能上得到了很大的提升,特别是在文件 IO 和 Json 解析上,对于我正在学习的规则引擎项目,需要大量的处理这两种情况。
  3. 热重载。现在开发中不再需要修改代码、停止应用、启动运行了,在开启热重载后保存代码之后应用会自动重载。

当然,它们的升级远不止这些,有兴趣的可以查看微软官方的升级文档。

升级项目版本

这一步很简单,在项目文件中将 TargetFramework 改为 net6.0 就可以了

1
xml复制代码<TargetFramework>net6.0</TargetFramework>

这么写的话会访问所有的跨平台的 API,但如果你要正对特定系统就行开发的话可以特化你的平台,比如可以写成

  • net6.0-android
  • net6.0-ios
  • net6.0-maccatalyst
  • net6.0-tvos
  • net6.0-windows

等等平台

全局引用

在 TargetFramework 同一级引入一个参数 ImplicitUsings 并将它设为 enable,开启全局引用。

1
xml复制代码<ImplicitUsings>enable</ImplicitUsings>

回到我们的代码中

image.png

我们发现不少 using 直接灰了,不再需要我们手动引入了。这是因为 .Net 在编译时会自动生成一个 [你的项目名字].GlobalUsings.g.cs 的文件,里面使用最新的 global using 特性为我们全局引入了常用的系统包,我们不再需要自己手动来引入它们了,它长这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C#复制代码// <auto-generated/>
global using global::Microsoft.AspNetCore.Builder;
global using global::Microsoft.AspNetCore.Hosting;
global using global::Microsoft.AspNetCore.Http;
global using global::Microsoft.AspNetCore.Routing;
global using global::Microsoft.Extensions.Configuration;
global using global::Microsoft.Extensions.DependencyInjection;
global using global::Microsoft.Extensions.Hosting;
global using global::Microsoft.Extensions.Logging;
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Net.Http.Json;
global using global::System.Threading;
global using global::System.Threading.Tasks;

当然,我们也可以使用自定义的文件替换它,也可以引入自己的全局引入文件引入我们项目中常用的库。

可空类型

在 TargetFramework 同一级再引入一个参数 Nullable 并将它设为 enable,开启可空类型。

1
xml复制代码<Nullable>enable</Nullable>

虽然 C# 早就引入了可空类型,但是在引入该类型之前已经存在的数据类型的可空会逃过编译器的检查,在运行时才会触发异常,打开这个功能,强制为它们添加可控添加。虽然这个参数不是 .Net 6 引入的,但是之前对于可空的误报过多,在 C# 10 升级了可空分析后,官方建议将其设为开启。

程序入口

6 之前的 .Net 网络应用程序的入口是由 Program.cs 和 Startup.cs 两个文件配合完成的。其中 Program.cs 是一个有一个 public static void Main(string[] args) 方法的类,在其中由引入 Startup.cs 启动应用,而 Startup.cs 是由 IConfiguration 类型构造,提供 ConfigureServices 和 Configure 两个返回 void 的方法构成的,典型的约定大约配置。
.Net 6 升级后统一使用 Program.cs 一个文件作为入口参数,而它也不再是一个类,而是一个脚本文件,是的,脚本文件,没有类也没有函数。看起来类似于 express.js 项目的入口文件的感觉。

改造前

Program.cs

1
2
3
4
5
6
7
8
9
10
11
C#复制代码public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
C#复制代码public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{

services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo {Title = "RuleService", Version = "v1"});
});
services.AddSingleton<Rules.RuleService>();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "RuleService v1"));
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}

改造后
Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
C#复制代码var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSingleton<Rules.RuleService>();

builder.Services.AddHttpClient();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

可以看到 Swagger 不再需要配置地址,而服务的注入管理需要的 Services 改为从 builder 中取出。整体风格也变得更加的简洁易读。

命名空间

文件的命名空间不再需要将代码全部包起来,而是直接放在文件头部了。
下面是一个文件的改造前后的对比

1
2
3
4
5
6
7
C#复制代码namespace RuleService.Rules
{
public class RuleResponse
{
public string Message { get; init; }
}
}
1
2
3
4
5
6
C#复制代码namespace RuleService.Rules;

public class RuleResponse
{
public string Message { get; init; }
}

后记

对于我的规则引擎项目至此就升级结束了。可以享受起最新的平台给我带来的便利了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

mica-mqtt 120 发布完善集群功能

发表于 2021-11-29

一、简介

mica-mqtt 基于 t-io 实现的简单、低延迟、高性能 的 mqtt 物联网开源组件。使用详见 mica-mqtt gitee 源码 mica-mqtt-example 模块。

mica-mqtt 更加易于集成到已有服务和二次开发,降低自研物联网平台开发成本。

二、功能

  • 支持 MQTT v3.1、v3.1.1 以及 v5.0 协议。
  • 支持 websocket mqtt 子协议(支持 mqtt.js)。
  • 支持 http rest api,http api 文档详见。
  • 支持 MQTT client 客户端。
  • 支持 MQTT server 服务端。
  • 支持 MQTT 遗嘱消息。
  • 支持 MQTT 保留消息。
  • 支持自定义消息(mq)处理转发实现集群。
  • MQTT 客户端 阿里云 mqtt 连接 demo。
  • 支持 GraalVM 编译成本机可执行程序。
  • 支持 Spring boot 项目快速接入(mica-mqtt-spring-boot-starter)。
  • mica-mqtt-spring-boot-starter 支持对接 Prometheus + Grafana。
  • 基于 redis pub/sub 实现集群,详见 mica-mqtt-broker 模块。

三、使用场景

  • 物联网(云端)
  • 物联网(边缘端)
  • 群组类 IM
  • 消息推送

四、待办

  • 优化处理 mqtt session,以及支持部分 mqtt v5.0 新特性。
  • 基于 easy-rule + druid sql 解析,实现规则引擎。

五、更新记录

  • :sparkles: mqtt-mqtt-core client IMqttClientConnectListener 添加 onDisconnect 方法。gitee #I4JT1D 感谢 @willianfu 同学反馈。
  • :sparkles: mica-mqtt-core server IMqttMessageListener 接口调整,不兼容老版本。
  • :sparkles: mica-mqtt-broker 调整上下行消息通道。
  • :sparkles: mica-mqtt-broker 添加节点管理,抽象业务层,方便接入,后续版本会对接规则引擎。
  • :sparkles: mica-mqtt-broker 调整默认的 Message 序列化方式,不兼容老版本。
  • :sparkles: mica-mqtt-broker 优化设备上下线,处理节点停机的情况。
  • :sparkles: 抽取 mica-mqtt-model 模块,方便后期支持消息桥接,Message 添加默认的消息序列化。 gitee #I4ECEO
  • :sparkles: mica-mqtt-model 完善 Message 消息模型,方便集群。
  • :bug: mica-mqtt-core MqttClient 修复 ssl 没有设置,感谢 @hjkJOJO 同学反馈。
  • :bug: 修复 websocket mqtt.js 需要拆包 gitee #I4JYJX 感谢 @Symous 同学反馈。
  • :memo: 完善 mica-mqtt-broker README.md,添加二开说明。
  • :memo: 统一 mica-mqtt server ip 文档。
  • :memo: 更新 README.md
  • :arrow_up: 升级 tio 到 3.7.5.v20211028-RELEASE AioDecodeException 改为 TioDecodeException,

六、重要更新说明

6.1 客户端 IMqttClientConnectListener 添加 onDisconnect 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public interface IMqttClientConnectListener {

/**
* 监听到消息
*
* @param context ChannelContext
* @param isReconnect 是否重连
*/
void onConnected(ChannelContext context, boolean isReconnect);

/**
* 连接关闭前触发本方法
*
* @param channelContext the channelContext
* @param throwable the throwable 有可能为空
* @param remark the remark 有可能为空
* @param isRemove is removed
*/
void onDisconnect(ChannelContext channelContext, Throwable throwable, String remark, boolean isRemove);

}

6.2 服务端 IMqttMessageListener onMessage 参数调整

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@FunctionalInterface
public interface IMqttMessageListener {

/**
* 监听到消息
*
* @param context ChannelContext
* @param clientId clientId
* @param message Message
*/
void onMessage(ChannelContext context, String clientId, Message message);

}

6.3 Message 对象添加更多属性

为了以后版本更方便的对接规则引擎对 Message 中的属性进行了扩充,并且内置 DefaultMessageSerializer 序列化,基于 ByteBuffer 进行的类协议解析的方式,速度更快,报文更小。

1
2
java复制代码byte[] data = DefaultMessageSerializer.INSTANCE.serialize(message);
Message message1 = DefaultMessageSerializer.INSTANCE.deserialize(data);

七、Spring boot 快速接入

7.1 添加依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>net.dreamlu</groupId>
<artifactId>mica-mqtt-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>

7.2 服务端配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码mqtt:
server:
enabled: true # 是否开启,默认:true
ip: 0.0.0.0 # 服务端 ip 默认:0.0.0.0
port: 5883 # 端口,默认:1883
name: Mica-Mqtt-Server # 名称,默认:Mica-Mqtt-Server
buffer-allocator: HEAP # 堆内存和堆外内存,默认:堆内存
heartbeat-timeout: 120000 # 心跳超时,单位毫秒,默认: 1000 * 120
read-buffer-size: 8092 # 接收数据的 buffer size,默认:8092
max-bytes-in-message: 8092 # 消息解析最大 bytes 长度,默认:8092
debug: true # 如果开启 prometheus 指标收集建议关闭
websocket-enable: true # 开启 websocket 子协议,默认开启
websocket-port: 8083 # websocket 端口,默认:8083

7.3 服务端可实现接口(注册成 Spring Bean 即可)

接口 是否必须 说明
IMqttServerUniqueIdService 否 用于 clientId 不唯一时,自定义实现唯一标识,后续接口使用它替代 clientId
IMqttServerAuthHandler 是 用于服务端认证
IMqttServerSubscribeValidator 是 1.1.3 新增,用于服务端订阅校验
IMqttMessageListener 是 消息监听
IMqttConnectStatusListener 是 连接状态监听
IMqttSessionManager 否 session 管理
IMqttMessageStore 集群是,单机否 遗嘱和保留消息存储
AbstractMqttMessageDispatcher 集群是,单机否 消息转发,(遗嘱、保留消息转发)
IpStatListener 否 t-io ip 状态监听

7.4 Prometheus + Grafana 监控对接

得益于 t-io 良好的设计,监控指标直接对接的 t-iostat,目前支持下列指标,后期会不断完善。

支持得指标 说明
mqtt_connections_accepted 共接受过连接数
mqtt_connections_closed 关闭过的连接数
mqtt_connections_size 当前连接数
mqtt_messages_handled_packets 已处理消息数
mqtt_messages_handled_bytes 已处理消息字节数
mqtt_messages_received_packets 已接收消息数
mqtt_messages_received_bytes 已处理消息字节数
mqtt_messages_send_packets 已发送消息数
mqtt_messages_send_bytes 已发送消息字节数

mqtt监控1.jpg

关于 mica-mqtt-spring-boot-starter 更多请查看文档:gitee.com/596392912/m…

八、普通 java 项目接入

8.1 maven 依赖

1
2
3
4
5
xml复制代码 <dependency>
<groupId>net.dreamlu</groupId>
<artifactId>mica-mqtt-core</artifactId>
<version>1.2.0</version>
</dependency>

8.2 mica-mqtt 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码 // 初始化 mqtt 客户端
MqttClient client = MqttClient.create()
.ip("127.0.0.1")
.port(1883) // 默认:1883
.username("admin")
.password("123456")
.version(MqttVersion.MQTT_5) // 默认:3_1_1
.clientId("xxxxxx") // 默认:MICA-MQTT- 前缀和 36进制的纳秒数
.connect(); // 连接

// 消息订阅,同类方法 subxxx
client.subQos0("/test/#", (topic, payload) -> {
logger.info(topic + '\t' + ByteBufferUtil.toString(payload));
});
// 取消订阅
client.unSubscribe("/test/#");

// 发送消息
client.publish("/test/client", ByteBuffer.wrap("mica最牛皮".getBytes(StandardCharsets.UTF_8)));

// 断开连接
client.disconnect();
// 重连
client.reconnect();
// 停止
client.stop();

8.3 mica-mqtt 服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// 注意:为了能接受更多链接(降低内存),请添加 jvm 参数 -Xss129k
MqttServer mqttServer = MqttServer.create()
// 默认:0.0.0.0
.ip("0.0.0.0")
// 默认:1883
.port(1883)
// 默认为: 8092(mqtt 默认最大消息大小),为了降低内存可以减小小此参数,如果消息过大 t-io 会尝试解析多次(建议根据实际业务情况而定)
.readBufferSize(512)
// 消息监听
.messageListener((context, clientId, message) -> {
logger.info("clientId:{} message:{} payload:{}", clientId, message, ByteBufferUtil.toString(message.getPayload()));
})
.debug() // 开启 t-io debug 信息日志
.start();

// 发送给某个客户端
mqttServer.publish("clientId","/test/123", ByteBuffer.wrap("mica最牛皮".getBytes()));

// 发送给所有在线监听这个 topic 的客户端
mqttServer.publishAll("/test/123", ByteBuffer.wrap("mica最牛皮".getBytes()));

// 停止服务
mqttServer.stop();

九、集群演示视频

mica-mqtt集群.gif

关注《mica微服务专栏》,更多精彩内容每天等你来发现!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

关于Nginx你了解多少了呢 写在前面 Nginx负载均衡

发表于 2021-11-29

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

写在前面

关于Nginx你了解多少了呢?

今天我想分享一下负载均衡和镜像服务器。

Nginx负载均衡

nginx负载均衡介绍

负载均衡的意思是在服务器集群中,需要有一台服务器作为调度者,客户端所有的请求都由调度者接收,调度者再根据每台服务器的负载情况,将请求分配给对应的服务器去处理;

在这个过程中,调度者如何合理分配任务,保证所有服务器将性能充分发挥,从而保持服务器集群的整体性能最优,这就是负载均衡的问题了。

nginx负载均衡的方式

1、轮询

轮询方式是Nginx负载默认的方式,顾名思义,所有请求都按照时间顺序分配到不同的服务上,如果服务Down掉,可以自动剔除,如下配置后轮训10001服务和10002服务。

1
2
3
4
vbscript复制代码upstream dalaoyang-server{
server localhost:10001;
server localhost:10002;
}

2、权重

指定每个服务的权重比例,weight和访问比率成正比,通常用于后端服务机器性能不统一,将性能好的分配权重高来发挥服务器最大性能,如下配置后10002服务的访问比率会是10001服务的二倍。

1
2
3
4
ini复制代码upstream dalaoyang-server{
server localhost:10001 weight=1;
server localhost:10002 weight=2;
}

3、iphash

每个请求都根据访问ip的hash结果分配,经过这样的处理,每个访客固定访问一个后端服务,如下配置(ip_hash可以和weight配合使用)。

1
2
3
4
5
ini复制代码upstream dalaoyang-server{
ip_hash;
server localhost:10001 weight=1;
server localhost:10002 weight=2;
}

4、最少连接

将请求分配到连接数最少的服务上。

1
2
3
4
5
ini复制代码upstream dalaoyang-server{
least_conn;
server localhost:10001 weight=1;
server localhost:10002 weight=2;
}

5、fair

按后端服务器的响应时间来分配请求,响应时间短的优先分配。需要插件来帮我们实现。

1
2
3
4
5
ini复制代码upstream dalaoyang-server{
server localhost:10001 weight=1;
server localhost:10002 weight=2;
fair;
}

Nginx配置

以轮询为例,如下是nginx.conf完整代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码worker_processes 1;
events{
worker_connections 1024;
}

http{
upstream dalaoyang-server{
server localhost:10001;
server localhost:10002;
}
server{
listen 10000;
server_name localhost;
location / {
proxy_pass http://dalaoyang-server;
proxy_redirect default;
}
}
}

Nginx镜像服务器

Nginx的proxy_store作用是直接把静态文件在本地硬盘创建并读取,类似于七牛或者又拍这样的镜像CDN功能,首次访问会自动获取源站的静态图片等文件,之后的访问就是直接从CDN服务器读取,加快了速度。

需要配置一下参数:

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码#启用缓存到本地的功能
proxy_storeon;
#表示用户读写权限,如果在error中报路径不允许访问的话就用"chomod-Ra+rw"将下面配置的路径改为相应的权限
proxy_store_access user:rw group:rw all:rw;
#此处为文件的缓存路径,这个路径是和url中的文件路径一致的
proxy_temp_path 缓存目录;
#在上面的配置之后,虽然文件被缓存到了本地磁盘上,但每次请求仍会向远端拉取文件,为了避免去远端拉取文件,还必须增加:
if(!-e $request_filename){
proxy_pass http://192.168.10.10;
}
注:"!-e$request_filename"正则表达式,匹配缓存目录中的文件与源文件是否存在。
"http://192.168.10.10"源服务器的地址,默认端口80,如监听其他端口,此处要指出,例如4000端口,http://192.168.10.10:4000

整体配置如下(修改nginx的配置文件nginx.conf):

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码location / {
//这里的location是要换成自己经过精确匹配的location,
//比如要缓存图片要写成"location~*\.(gif|jpg|jepg|png|bmp)${"
expires 3d; //所有链接,浏览器缓存过期时间为3天
proxy_set_header Accept-Encoding '';
root /home/mpeg/nginx; //此目录为服务器的根目录,下面的if语句就是判断此目录下是否有响应的文件
proxy_store on; //表示开启缓存
proxy_store_access user:rw group:rw all:rw; //表示用户读写权限
proxy_temp_path /home/mpeg/nginx; //此处为文件的缓存路径,这个路径是和url中的文件路径一致的
if(!-e $request_filename){
proxy_pass http://192.168.0.1; //此处为要被代理的服务器的地址
}
}

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

网络协议-TCP的四次挥手 结束语

发表于 2021-11-29

本文正在参与 “网络协议必知必会

山有峰顶,海有彼岸,漫漫长路,终有回转,余味苦涩,终会有回甘。别被眼前的磨难打败了,或许光明就在你放弃前的那一刻。带着愉快的心情做一个愉快的梦,醒来后,又是新的一天。

世界上任何的书籍都不能带给你好运,但是它们能让你悄悄的成为你自己的

前言

TCP位于传输层,是一个可靠的连接服务,为了准确的传输数据,TCP采用了三次握手,四次挥手策略. 这里讲的是四次挥手(也叫四次握手)

TCP首部格式

TCP首部数据格式,通常是20个字节再加可变字段,其中有6个特殊的标识bit,分别是URG,ACK,PSH,RST,SYN,FIN等,位置见下图

image.png

标识位 含义
URG 紧急指针有效
ACK 确认序号有效
PSH 接收方应该尽快将这个报文段交给应用层
RST 复位,关闭异常连接
SYN 同步序号用来发起一个连接
FIN 发端完成发送任务
…

本文中用到的标识位是 ACK FIN, 使用的时候bit位设置为1,否则默认为0. 也用到了32位序号(Sequence number)和32位确认序号(Acknowledgment number),这里是用来存放双发的初始序列号(ISN)的.

大写的ACK FIN代表标志位,
小写的seq代表Sequence number,
小写的ack代表Acknowledgment number,

四次挥手

通俗的说

以客户端先断开的话

客户端: 我数据发送完了,可以关闭连接了.

服务端: 收到了,等我发送完数据.

服务端: 我数据发送完了,我也可以关闭连接了.

客户端: 收到了.

随后服务端关闭连接, 客户端等指定时间后也关闭了连接.

正常的说

以客户端先断开为例:

在四次挥的过程中,会使用ACK FIN,seq和ack.

第一次握手(客户端): 发送请求,TCP中设置FIN=1 ,seq设置为本机的ISN.

第二次握手(服务端): 收到客户端的数据之后,发送请求,TCP中设置ACK=1 ,seq设置为本机的ISN,并将ack设置为客户端的ISN+1

第三次握手(服务端): 等服务端数据传送完成之后, 发送请求, TCP中设置ACK=1 FIN=1, seq设置为本机的另一个ISN(叫ISN2) ,ack设置为客户端的ISN+1

第四次握手(客户端): 客户端收到返回信息后, 发送请求, TCP中设置ACK=1 , seq设置为本机的ISN+1 ,ack设置为服务器的ISN2+1

服务端关闭连接,客户端等待2MSL时间之后再次关闭.

图如下

image.png

示例如下
20.1.0.1是我的电脑,20.1.0.128电脑上的一个虚拟机

在128上使用tcpdump监听ens33(虚拟机网卡)的80(nginx)端口

1
css复制代码tcpdump -i ens33 port 80 and host 20.1.0.1 -S -n

在1电脑上使用telnet请求20.1.0.128的80端口

1
复制代码telnet 20.1.0.128 80

之后Ctrl+C之后

tcpdump监听日志如下

Ctrl+C的数据

1
2
3
yaml复制代码10:33:19.377458 IP 20.1.0.1.58633 > 20.1.0.128.http: Flags [P.], seq 2066280967:2066280968, ack 3607501457, win 2053, length 1: HTTP
10:33:19.377573 IP 20.1.0.128.http > 20.1.0.1.58633: Flags [.], ack 2066280968, win 229, length 0
10:33:19.377855 IP 20.1.0.128.http > 20.1.0.1.58633: Flags [P.], seq 3607501457:3607501766, ack 2066280968, win 229, length 309: HTTP: HTTP/1.1 400 Bad Request

四次挥手的数据

1
2
3
4
yaml复制代码10:33:19.378017 IP 20.1.0.128.http > 20.1.0.1.58633: Flags [F.], seq 3607501766, ack 2066280968, win 229, length 0
10:33:19.378079 IP 20.1.0.1.58633 > 20.1.0.128.http: Flags [.], ack 3607501767, win 2051, length 0
10:33:19.382138 IP 20.1.0.1.58633 > 20.1.0.128.http: Flags [F.], seq 2066280968, ack 3607501767, win 2051, length 0
10:33:19.382234 IP 20.1.0.128.http > 20.1.0.1.58633: Flags [.], ack 2066280969, win 229, length 0

针对四次挥手的攻击

FIN Flood

RST Flood

…

结束语

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

如果您喜欢我的文章,可以[关注]+[点赞]+[评论],您的三连是我前进的动力,期待与您共同成长~

1
2
3
4
arduino复制代码    作者:ZOUZDC
链接:https://juejin.cn/post/7028963866063306760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【kafka】 丢消息处理总结 丢消息

发表于 2021-11-29

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

丢消息

参考:

  • juejin.cn/post/684490…
  • blog.csdn.net/qq_34495753…

生产者端、消费者端、kafka端,都有可能发生丢消息的情况。

生产者端

原因:

  • 网络不稳定
  • 或。。。

解决办法:

  • 监听回调事件,发送失败则重发,持续失败则需要检查发生发送失败的情况。

消费者端

消费者端丢失消息的情况可能如下:

  • pull到了某消息后,自动提交的limit过了但还没处理完,随后提交了offset的更新,再做处理;
    • 然而,在提交之后消费者崩溃了,此时本应该被消费的那个offset位置对应的消息,无法被处理。

解决办法:

  • 禁止自动提交offset,转由消费完之后手动提交offset,但这个也有一个问题就是:如果消费完了,在提交offset的时候消费者崩溃了,那么消息会被消费两次。
  • 也就是说:由于消费者端更新offset和消费并不是原子性的,在崩溃的状况下无法保证消息有且仅有消费一次,因此消费端必须保证幂等性,加上手动更新offset,才能保证消息不丢失。

kafka端

kafka丢消息的最大可能如下:

  • 前提知识:kafka的分区、多副本机制。

分区中的leader副本,在往follower副本同步时,并没有完全同步最新的消息,就挂掉了。

那么这些没有同步的消息,自然就取不到了。

解决办法:

  • 确保在所有副本中都不会有未同步最新消息的情况。

这种解决方式是在生产者端把消息发送给所有副本,确保副本都接收到才算发送成功。

这种解决方式需要设置:acks = all(默认acks= 1,代表leader副本接收之后就算成功)

当然,这种方式会导致发送请求的效率下降。

+ 在ACK = all的时候,需要记得还有一个东西需要配置:**min.insync.replicas = m**
    - 这个变量表示,消息至少要被写入到m个副本,才可以给生产者返回发送成功。
+ 同时,还有一个变量和这个有关:**replication.factor= 3**
    - 这个变量表示每个分区的副本数。为了保证kafka的健壮性,分区副本数必须大于上面配置的最小写入分区数,否则只要有副本挂了,就会导致中间件无法写入消息。
  • 确保没有最新消息的副本,在leader宕机后的选举中不会被选为新leader。

这个解决方案需要配置unclean.leader.election.enable = false。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

DNS网络协议初探 DNS协议初探

发表于 2021-11-29

DNS协议初探

本文正在参与 “网络协议必知必会”征文活动

DNS 使用TCP还是UDP协议?

DNS 迭代查询 和 递归查询 是什么?

如何构建DNS报文?

  1. 什么是DNS

​ DNS: (Domain Name System) 域名系统, 用户在使用常用网络软件,例如: 浏览器,邮件 等,但是这些服务需要指定服务器的ip地址和端口号,这个难以记忆,大家更喜欢易读、有一定含义的名字,DNS就能满足这个要求,实现将域名映射为IP地址,这个就称为域名解析。目前互联网 DNS为分布式数据库, 域名服务器分布在世界各地,每个服务器也只存储了部分域名信息。

1.1 域名层次化

​ 域名结构由标号序列组成,标点符号用点隔开, 例如: juejin.cn. www.jd.com. 可总结为: ....三级域名.二级域名.顶级域名.根域名(可省略)

根

截止目前为止,全球上共有13个根服务器 ,从 a - m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码# yum -y install bind-utils
# dig | grep root | awk '{print $NF}' | sort
a.root-servers.net.
b.root-servers.net.
c.root-servers.net.
d.root-servers.net.
e.root-servers.net.
f.root-servers.net.
g.root-servers.net.
h.root-servers.net.
i.root-servers.net.
j.root-servers.net.
k.root-servers.net.
l.root-servers.net.
m.root-servers.net.
#

顶级域名分类:

  • 国家顶级域名nTLD
    • cn: 表示中国
    • us: 表示美国
    • jp: 表示日本
    • …
  • 通用顶级域名gTLD
    • com: 表示工商企业
    • net: 表示网络提供商
    • gov: 政府专用
    • …
  • 基础结构域名
    • arpa

1.2 域名服务器分类

​ 域名服务器以区为单位,可以分为: 1. 根域名服务器 2. 顶级域名服务器 3. 权威域名服务器 4. 中间域名服务器

根域名服务器

全球共有13个根域名服务器,可通过 dig 获取。

顶级域名服务器

TLD服务器,负责管理该顶级域名下的二级域名。

权威域名服务器

负载一个区的域名服务器,保存该区的域名 和 IP地址映射关系。

中间服务器

不属于如上三种服务器,就是中间域名服务器。

1.3 通过实例来判断服务器分类

本次查询,DNS服务器信息如下

  • 本地域名服务器: 61.139.2.69
  • 根域名服务器: a.root-servers.net (198.41.0.4#53)
  • 顶级域名服务器: g.dns.cn (66.198.183.65#53)
  • 权威域名服务器: vip4.alidns.com (47.113.183.36#53)

利用dig追踪日志如下

dig +trace 可以追踪请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
bash复制代码# dig +trace juejin.cn. @61.139.2.69 

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7_9.7 <<>> +trace juejin.cn. @61.139.2.69
;; global options: +cmd
. 28351 IN NS a.root-servers.net.
. 28351 IN NS b.root-servers.net.
. 28351 IN NS c.root-servers.net.
. 28351 IN NS d.root-servers.net.
. 28351 IN NS e.root-servers.net.
. 28351 IN NS f.root-servers.net.
. 28351 IN NS g.root-servers.net.
. 28351 IN NS h.root-servers.net.
. 28351 IN NS i.root-servers.net.
. 28351 IN NS j.root-servers.net.
. 28351 IN NS k.root-servers.net.
. 28351 IN NS l.root-servers.net.
. 28351 IN NS m.root-servers.net.
;; Received 228 bytes from 61.139.2.69#53(61.139.2.69) in 3 ms

cn. 172800 IN NS c.dns.cn.
cn. 172800 IN NS g.dns.cn.
cn. 172800 IN NS b.dns.cn.
cn. 172800 IN NS ns.cernet.net.
cn. 172800 IN NS e.dns.cn.
cn. 172800 IN NS f.dns.cn.
cn. 172800 IN NS a.dns.cn.
cn. 172800 IN NS d.dns.cn.
cn. 86400 IN DS 57724 8 2 5D0423633EB24A499BE78AA22D1C0C9BA36218FF49FD95A4CDF1A4AD 97C67044
cn. 86400 IN RRSIG DS 8 1 86400 20211211170000 20211128160000 14748 . gM8pprqRpkmDpcu6kNU5kffmW1jo9dmT/CjK7g9dUH4F2purVO1Txiyr RszAgzMWe7HmxeLLEN1s0p2vxbQvQ0uQZn7DMA5eJWbNf/rINyT6vmMK BndvUuTJ74wnEkiXY8Cviim597TEFWl5w7Z9Bn0FwM2nzt5OHBDben24 Fca3vbIXK3Q2n1cDbpO01We/VbiUrgcxlNAxm68wC8gWwLypFNFDXw5V tHVwwX4NsKG1si6n5lyuKramPj+GM9YV/htDNSZKzjEyHTrp/lfwQoxX Eju+cOhmFvnnSFRLS+9EyVmil+i822M2QyVbLhmwjkW8pdER+RxXd+pv vrm6gA==
;; Received 700 bytes from 198.41.0.4#53(a.root-servers.net) in 223 ms

juejin.cn. 86400 IN NS vip3.alidns.com.
juejin.cn. 86400 IN NS vip4.alidns.com.
3QDAQA092EE5BELP64A74EBNB8J53D7E.cn. 21600 IN NSEC3 1 1 10 AEF123AB 3QM14FQ32F1CJFTP8D3J5BCTNP5BIELO NS SOA RRSIG DNSKEY NSEC3PARAM
3QDAQA092EE5BELP64A74EBNB8J53D7E.cn. 21600 IN RRSIG NSEC3 8 2 21600 20211225190654 20211125181736 38388 cn. B735xzQYqVTspNxPes9yYW+EnnN1GuaKhRhI4UuowLiDdRqwrk1I/+3F aqzWerS2SUO+nhzcSzl+NeiIwBBuOjyh/NgF15e1WBHvc8PR2cG3HXSo rD3usJlX1rlaOZH6EP5k/VkMSktveAeJo3foOF104UXuljG0yRQqp+4v sh0=
DEFPMSCATA3DEUN2HJMIGDHN15FBH1AM.cn. 21600 IN NSEC3 1 1 10 AEF123AB DGCV05BN5EJCBB4F86S87BCR5CJOA3IC NS DS RRSIG
DEFPMSCATA3DEUN2HJMIGDHN15FBH1AM.cn. 21600 IN RRSIG NSEC3 8 2 21600 20211225184833 20211125181738 38388 cn. ghEDLlzom97T/4SL7znLbN5PKD0qyInh6VgcDw6dZPmoDy7eDd0Gk1Vw VoSSnn7dBp7AK8zEkAygKg28QiNFgPlWDlUtM37R3H3OGqfnJRiJ7R85 n/HSMchAunBtMgkSrtmXUVryzUm5inyi/FxH2iVc4fgGQZlLNmZr5BG+ lqQ=
;; Received 577 bytes from 66.198.183.65#53(g.dns.cn) in 274 ms

juejin.cn. 600 IN CNAME juejin.cn.w.cdngslb.com.
;; Received 75 bytes from 47.113.183.36#53(vip4.alidns.com) in 46 ms

#

解析请求图大致如下

image.png

1.4 DNS递归解析和迭代解析

递归查询: 在进行查询时,若没有被查询的域名信息,则进行代理查询,直至查询到IP地址/或者是查询失败 , 然后返回给本地域名服务器。

迭代查询: 不会进行代理查询请求,而是将结果或者是下一跳域名服务器返回给本地域名服务器。

图示

迭代查询

image.png

递归查询

image.png

  1. DNS报文解析

参考 rfc1035 datatracker.ietf.org/doc/html/rf…

请求报文

image.png

响应报文

image.png

2.1 DNS 整个报文格式

1
2
3
4
5
6
7
8
9
10
11
bash复制代码    +---------------------+
| Header |
+---------------------+
| Question | the question for the name server
+---------------------+
| Answer | RRs answering the question
+---------------------+
| Authority | RRs pointing toward an authority
+---------------------+
| Additional | RRs holding additional information
+---------------------+

2.2 Head: 请求头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码                                    1  1  1  1  1  1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

ID: 16bit 查询/响应编号

作为 查询 和 应答标识ID

QR: 1bit 报文类型

0: 查询

1: 响应

Opcode: 4bit 查询的类型

0: 标准查询

1: 反查询

2: 服务器状态请求

3-15: 暂时闲置

AA: 1bit 权威应答

1: 是

0: 否

TC: 1bit 超出最大允许的长度(UDP包为512字节)

UDP 包为 512 字节,若返回报文超过512个字节,则需要将状态标志位 TC 置为1 然后将报文返回给客户端,客户端收到后会再次使用 TCP 进行查询请求

1: 超出最大长度,截断

0: 正常

RD: 1bit 期望递归查询

1: 需要递归查询

0: 不需要递归查询

RA: 1bit 可用递归 , 用于响应报文中

1: 支持递归

0: 不支持递归

Z: 3bit

保留

RCODE: 4bit 响应代码

0: 没有错误

1: 格式错误

2: 服务器故障

3: 名称错误

4: 不支持请求类型的查询

5: 服务器拒绝查询

6-15: 备用

2.3 Question 查询请求

1
2
3
4
5
6
7
8
9
10
11
bash复制代码                                    1  1  1  1  1  1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ QNAME /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QTYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QCLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

QNAME: 表示查询的域名

每个标签由一个8字节的长度 和 响应的字符组成

例如 juejin.com

在报文中QNAME应该展示为

6 j u e j in 3 c o m 0

image.png

QTYPE: 16bit 请求的类型

1: A

2: NS

3: MD

4: MF

5: CNAME

6: SOA

7: MB

8: MG

9: MR

10: NULL

11: WKS

12: PTR

13: HINFO

14: MINFO

15: MX

16: TXT

QCLASS: 16bit 请求的方式

1: 一般网络请求

2: CSNET

3: CHAOS

4: Hesiod

2.4 Answer 响应报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码                                    1  1  1  1  1  1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ /
/ NAME /
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/ RDATA /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

**NAEM: 16bit **

资源记录域名 , 采用 2个byte 来表示 , CO 为固定标记位,代表后面的值为偏移量

1
2
3
bash复制代码    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1 1| OFFSET |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

TYPE 16bit

RDATA资源类型

CLASS: 16bit

请求类型

TTL: 32bit

缓存时间,为0则不能缓存

RDLENGTH: 16bit

RDATA字节数

RDATA: RDLENGTH byte

资源记录值

  1. DNS golang 代码案例

3.1 获取请求报文

获取/生成A记录报文Demo 代码放在了 gitee 上 gitee.com/pdudo/Sampl…

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
golang复制代码package main

import (
"fmt"
"gitee.com/pdudo/SampleDNSTool"
"log"
"net"
)

func main() {


var dnsInfo SampleDNSTool.DNSInfo

udpconn ,err := net.ListenUDP("udp",&net.UDPAddr{
IP: net.IPv4(0,0,0,0),
Port: 53,
})

if err != nil {
log.Fatal("listen udp error" , err)
}

for {
buf := make([]byte,1024)
n , err := udpconn.Read(buf[:])
if err != nil {
log.Println("udpconn error " , err)
}

dnsInfo.GetHeader(buf[:n])
dnsInfo.GetQuestion(buf[:n])

fmt.Println("DNS 查询ID: " ,dnsInfo.Header.ID ,
"HeaderStatus:" , dnsInfo.Header.HeaderStatus ,
"QCount:" , dnsInfo.Header.QCOUNT,
"Qname:" , dnsInfo.QueryInfo.QNAMEString ,
"QTYPE:",dnsInfo.QueryInfo.QTYPE ,
"QCLASS:",dnsInfo.QueryInfo.QCLASS)
}
}

启动服务器 并且 使用nslookup模拟请求

1
2
bash复制代码# go build
# ./testDNSQuery

客户端

1
2
3
4
bash复制代码# nslookup juejin.com 127.0.0.1
;; connection timed out; no servers could be reached

#

程序输出

1
2
3
4
bash复制代码# ./testDNSQuery
DNS 查询ID: 27284 HeaderStatus: {0 0 0 0 1 0 0 0} QCount: 1 Qname: juejin.com QTYPE: 1 QCLASS: 1
DNS 查询ID: 27284 HeaderStatus: {0 0 0 0 1 0 0 0} QCount: 1 Qname: juejin.com QTYPE: 1 QCLASS: 1
DNS 查询ID: 27284 HeaderStatus: {0 0 0 0 1 0 0 0} QCount: 1 Qname: juejin.com QTYPE: 1 QCLASS: 1

生成响应报文

修改代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
golang复制代码package main

import (
"fmt"
"gitee.com/pdudo/SampleDNSTool"
"log"
"net"
)

func main() {

var dnsInfo SampleDNSTool.DNSInfo

udpconn ,err := net.ListenUDP("udp",&net.UDPAddr{
IP: net.IPv4(0,0,0,0),
Port: 53,
})

if err != nil {
log.Fatal("listen udp error" , err)
}

for {
buf := make([]byte,1024)
n , fromudpaddr , err := udpconn.ReadFromUDP(buf[:])
if err != nil {
log.Println("udpconn error " , err)
}

dnsInfo.GetHeader(buf[:n])
dnsInfo.GetQuestion(buf[:n])

fmt.Println("DNS收到请求 , 查询ID: " ,dnsInfo.Header.ID ,
"HeaderStatus:" , dnsInfo.Header.HeaderStatus ,
"QCount:" , dnsInfo.Header.QCOUNT,
"Qname:" , dnsInfo.QueryInfo.QNAMEString ,
"QTYPE:",dnsInfo.QueryInfo.QTYPE ,
"QCLASS:",dnsInfo.QueryInfo.QCLASS)

// 构建回复报文
if (1 == dnsInfo.QueryInfo.QTYPE) {
ip := make([]byte,4)
ip[0] = 192
ip[1] = 168
ip[2] = 111
ip[3] = 129
res := dnsInfo.GenerateAnswers(buf[:n],ip,0,1)

//udpconn.Write(res)
udpconn.WriteToUDP(res,fromudpaddr)
}
}
}

利用nslookup查看效果

1
2
3
4
5
6
7
8
bash复制代码# nslookup juejin.com 127.0.0.1
Server: 127.0.0.1
Address: 127.0.0.1#53

Non-authoritative answer:
Name: juejin.com
Address: 192.168.111.129
;; connection timed out; no servers could be reached
1
bash复制代码var code = "df7cfb57-4300-4caa-a3bc-3a489833b53c"

完

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

关于knife4j的文档自动注册功能的解决 1 存在问题 2

发表于 2021-11-29

这是我参与11月更文挑战的第20天,活动详情查看:11月更文挑战

1 存在问题

上一篇,关于knife4j整合微服务聚合文档, 在日常项目中,使用简单,方便, 可是存在一个问题, 就是需要在文档服务中,手动的配置其他服务的路由地址,而且, 每次新增一个服务,都需要配置,使用起来不是很灵活便捷,那有没有解决方案, 文档服务,主动去nacos中获取服务,自动注册到文档服务的呢?, 答案是肯定的, 对于这一块,knife4j工具提供了相关的入口.

2 解决方案

对于上次的knife4j整合微服务聚合文档文章做增强功能, 业务服务可复用之前的, 本次只对文档服务改造即可.

文档服务案列

Nacos服务类

主要处理nacos中服务实例,包括鉴权,nacos配置等.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
java复制代码public class DocNacosService extends NacosService {

Logger logger = LoggerFactory.getLogger(NacosService.class);
/**
* Nacos获取实例列表OpenAPI接口,详情参考:https://nacos.io/zh-cn/docs/open-api.html
*/
private static final String NACOS_INSTANCE_LIST_API = "/v1/ns/instance/list";
/**
* 服务名称
*/
private final String serviceUrl;
/**
* Nacos注册中心鉴权,参考issue:https://gitee.com/xiaoym/knife4j/issues/I28IF9 since 2.0.9
*/
private final String accessToken;
/**
* Nacos配置
*/
private final NacosRoute nacosRoute;

public DocNacosService(String serviceUrl, String accessToken,
NacosRoute nacosRoute) {
super(serviceUrl, accessToken, nacosRoute);
this.serviceUrl = serviceUrl;
this.accessToken = accessToken;
this.nacosRoute = nacosRoute;
}


@Override
public Optional<NacosInstance> call() throws Exception {
List<String> params = new ArrayList<>();
params.add("serviceName=" + nacosRoute.getServiceName());
//默认聚合时只返回健康实例
params.add("healthyOnly=true");
if (StrUtil.isNotBlank(nacosRoute.getGroupName())) {
params.add("groupName=" + nacosRoute.getGroupName());
}
if (StrUtil.isNotBlank(nacosRoute.getNamespaceId())) {
params.add("namespaceId=" + nacosRoute.getNamespaceId());
}
if (StrUtil.isNotBlank(nacosRoute.getClusters())) {
params.add("clusters=" + nacosRoute.getClusters());
}
//Nacos鉴权 since2.0.9
if (StrUtil.isNotBlank(this.accessToken)) {
params.add("accessToken=" + this.accessToken);
}
String parameter = CollectionUtil.join(params, "&");
String api = serviceUrl + NACOS_INSTANCE_LIST_API + "?" + parameter;
if (logger.isDebugEnabled()) {
logger.debug("Nacos API:{}", api);
}
HttpGet get = new HttpGet(api);
CloseableHttpResponse response = getClient().execute(get);
if (response != null) {
int statusCode = response.getStatusLine().getStatusCode();
if (logger.isDebugEnabled()) {
logger.debug("Nacos Response Status:{}", statusCode);
}
if (statusCode == HttpStatus.SC_OK) {
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
if (StrUtil.isNotBlank(content)) {
if (logger.isDebugEnabled()) {
logger.debug("Response Content:{}", content);
}
JsonElement jsonElement = JsonParser.parseString(content);
if (jsonElement != null && jsonElement.isJsonObject()) {
JsonElement instances = jsonElement.getAsJsonObject().get("hosts");
if (instances != null && instances.isJsonArray()) {
Type type = new TypeToken<List<NacosInstance>>() {
}.getType();
List<NacosInstance> nacosInstances = new Gson()
.fromJson(instances, type);
if (CollectionUtil.isNotEmpty(nacosInstances)) {
NacosInstance nacosInstance = nacosInstances.stream().findAny()
.get();
nacosInstance.setServiceName(nacosRoute.getServiceName());
return Optional.of(nacosInstance);
}
}
}
}
} else {
get.abort();
}
}
return Optional.empty();
}

}

Nacos服务资源库类

主要是初始化nacos资源库, 从本地文件获取,从nacos注册中心获取服务加载到本地资源库.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
java复制代码public class DocNacosRepository extends NacosRepository {

@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private Environment environment;

private volatile boolean stop = false;
private Thread thread;
Logger logger = LoggerFactory.getLogger(NacosRepository.class);

private NacosSetting nacosSetting;

final ThreadPoolExecutor threadPoolExecutor = ThreadUtil.newExecutor(5, 5);

private Map<String, NacosInstance> nacosInstanceMap = new HashMap<>();


public DocNacosRepository(
NacosSetting nacosSetting) {
super(nacosSetting);
this.nacosSetting = nacosSetting;

if (nacosSetting != null && CollectionUtil.isNotEmpty(nacosSetting.getRoutes())) {
initNacos(nacosSetting);
applyRoutes(nacosSetting);
}

}

/**
* 初始化 nacos配置属性
*/
private void applyRoutes(NacosSetting nacosSetting) {
if (CollectionUtil.isNotEmpty(nacosInstanceMap)) {
nacosSetting.getRoutes().forEach(nacosRoute -> {
if (nacosRoute.getRouteAuth() == null || !nacosRoute.getRouteAuth().isEnable()) {
nacosRoute.setRouteAuth(nacosSetting.getRouteAuth());
}
this.routeMap.put(nacosRoute.pkId(), new SwaggerRoute(nacosRoute,
nacosInstanceMap.get(nacosRoute.getServiceName())));
});
nacosSetting.getRoutes().forEach(nacosRoute -> this.routeMap.put(nacosRoute.pkId(),
new SwaggerRoute(nacosRoute,
nacosInstanceMap.get(nacosRoute.getServiceName()))));
}
}

@Override
public void initNacos(NacosSetting nacosSetting) {
List<Future<Optional<NacosInstance>>> optionalList = new ArrayList<>();
nacosSetting.initAccessToken();
nacosSetting.getRoutes().forEach(nacosRoute -> optionalList.add(threadPoolExecutor
.submit(new NacosService(nacosSetting.getServiceUrl(), nacosSetting.getSecret(),
nacosRoute))));
optionalList.stream().forEach(optionalFuture -> {
try {
Optional<NacosInstance> nacosInstanceOptional = optionalFuture.get();
if (nacosInstanceOptional.isPresent()) {
nacosInstanceMap.put(nacosInstanceOptional.get().getServiceName(),
nacosInstanceOptional.get());
}
} catch (Exception e) {
logger.error("nacos get error:" + e.getMessage(), e);
}
});
}

@Override
public NacosSetting getNacosSetting() {
return nacosSetting;
}

@Override
public BasicAuth getAuth(String header) {
BasicAuth basicAuth = null;
if (nacosSetting != null && CollectionUtil.isNotEmpty(nacosSetting.getRoutes())) {
if (nacosSetting.getRouteAuth() != null && nacosSetting.getRouteAuth().isEnable()) {
basicAuth = nacosSetting.getRouteAuth();
//判断route服务中是否再单独配置
BasicAuth routeBasicAuth = getAuthByRoute(header, nacosSetting.getRoutes());
if (routeBasicAuth != null) {
basicAuth = routeBasicAuth;
}
} else {
basicAuth = getAuthByRoute(header, nacosSetting.getRoutes());
}
}
return basicAuth;
}

@Override
public void start() {
logger.info("start Nacos hearbeat Holder thread.");
thread = new Thread(() -> {
while (!stop) {
try {
ThreadUtil.sleep(HEART_BEAT_DURATION);
logger.debug("nacos hearbeat start working...");
this.nacosSetting.initAccessToken();

List<NacosRoute> routes = this.nacosSetting.getRoutes();
// yaml配置文件中没有路由,则自动从注册中心去获取在线服务,转为route
if (CollectionUtil.isEmpty(routes)) {
routes = getServiceToRouteList();
}

//校验该服务是否在线
routes.forEach(nacosRoute -> {
try {
NacosService nacosService = new DocNacosService(
this.nacosSetting.getServiceUrl(),
this.nacosSetting.getSecret(), nacosRoute);
//单线程check即可
Optional<NacosInstance> nacosInstanceOptional = nacosService.call();
if (nacosInstanceOptional.isPresent()) {
this.routeMap.put(nacosRoute.pkId(),
new SwaggerRoute(nacosRoute, nacosInstanceOptional.get()));
} else {
//当前服务下线,剔除
this.routeMap.remove(nacosRoute.pkId());
}
} catch (Exception e) {
//发生异常,剔除服务
this.routeMap.remove(nacosRoute.pkId());
logger.debug(e.getMessage(), e);
}
});
} catch (Exception e) {
logger.debug(e.getMessage(), e);
}

}
});
thread.setDaemon(true);
thread.start();
}

/**
* 从nacos中获取服务列表
* @return
*/
private List<NacosRoute> getServiceToRouteList() {
List<NacosRoute> nacosRouteList = Lists.newArrayList();
List<String> services = discoveryClient.getServices();
if (CollectionUtil.isEmpty(services)){
return nacosRouteList;
}
for (String service : services) {
NacosRoute nacosRoute = new NacosRoute();
nacosRoute.setGroupName(environment.getProperty("spring.cloud.nacos.discovery.group"));
nacosRoute.setNamespaceId(environment.getProperty("spring.cloud.nacos.discovery.namespace"));
nacosRoute.setClusters(environment.getProperty("spring.cloud.nacos.discovery.cluster-name"));
nacosRoute.setName(service);
nacosRoute.setServiceName(service);
nacosRoute.setServicePath(service);
nacosRoute.setLocation("/v2/api-docs");
nacosRouteList.add(nacosRoute);
}

return nacosRouteList;
}

@Override
public void close() {
logger.info("stop Nacos heartbeat Holder thread.");
this.stop = true;
if (thread != null) {
ThreadUtil.interrupt(thread, true);
}
}

}

路由分发类

主要处理请求的路由转发等, 对于返回的结果, 可以根据业务的不同,返回不同状态.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
java复制代码public class DocRouteDispatcher extends RouteDispatcher {

/**
* 请求头
*/
public static final String ROUTE_PROXY_HEADER_NAME = "knfie4j-gateway-request";
public static final String ROUTE_PROXY_HEADER_BASIC_NAME = "knife4j-gateway-basic-request";
public static final String OPENAPI_GROUP_ENDPOINT = "/swagger-resources";
public static final String OPENAPI_GROUP_INSTANCE_ENDPOINT = "/swagger-instance";
public static final String ROUTE_BASE_PATH = "/";

Logger logger = LoggerFactory.getLogger(RouteDispatcher.class);
/**
* 当前项目的contextPath
*/
private String rootPath;

private RouteRepository routeRepository;

private RouteExecutor routeExecutor;

private RouteCache<String, SwaggerRoute> routeCache;

private Set<String> ignoreHeaders = new HashSet<>();

public DocRouteDispatcher(RouteRepository routeRepository,
RouteCache<String, SwaggerRoute> routeRouteCache,
ExecutorEnum executorEnum,
String rootPath) {
super(routeRepository, routeRouteCache, executorEnum, rootPath);

this.routeRepository = routeRepository;
this.routeCache = routeRouteCache;
this.rootPath = rootPath;
initExecutor(executorEnum);
ignoreHeaders.addAll(Arrays.asList(new String[]{
"host", "content-length", ROUTE_PROXY_HEADER_NAME, ROUTE_PROXY_HEADER_BASIC_NAME, "Request-Origion"
}));
}

private void initExecutor(ExecutorEnum executorEnum) {
if (executorEnum == null) {
throw new IllegalArgumentException("ExecutorEnum can not be empty");
}
switch (executorEnum) {
case APACHE:
this.routeExecutor = new ApacheClientExecutor();
break;
case OKHTTP:
this.routeExecutor = new OkHttpClientExecutor();
break;
default:
throw new UnsupportedOperationException("UnSupported ExecutorType:" + executorEnum.name());
}
}


@Override
public boolean checkRoute(String header) {
if (StrUtil.isNotBlank(header)) {
SwaggerRoute swaggerRoute = routeRepository.getRoute(header);
if (swaggerRoute != null) {
return StrUtil.isNotBlank(swaggerRoute.getUri());
}
}
return false;
}

@Override
public void execute(HttpServletRequest request, HttpServletResponse response) {
try {
RouteRequestContext routeContext = new RouteRequestContext();
this.buildContext(routeContext, request);
RouteResponse routeResponse = routeExecutor.executor(routeContext);
writeResponseStatus(routeResponse, response);
// todo
// 请求/v2/api-docs 响应状态设为200 不抛出其他状态
if(response.getStatus()!=200 && request.getRequestURI().equals("/v2/api-docs")){
response.setStatus(200);
}
writeResponseHeader(routeResponse, response);
writeBody(routeResponse, response);
} catch (Exception e) {
logger.error("has Error:{}", e.getMessage());
logger.error(e.getMessage(), e);
//write Default
writeDefault(request, response, e.getMessage());
}
}

@Override
protected void writeDefault(HttpServletRequest request, HttpServletResponse response, String errMsg) {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
try {
PrintWriter printWriter = response.getWriter();
Map<String, String> map = new HashMap<>();
map.put("message", errMsg);
// todo
// 请求/v2/api-docs 响应状态设为200 不抛出其他状态
map.put("code", "200");
map.put("path", request.getRequestURI());
new JSONObject(map).write(printWriter);
printWriter.close();
} catch (IOException e) {
//ignore
}
}

/**
* Write 响应状态码
*
* @param routeResponse routeResponse
* @param response response
*/
@Override
protected void writeResponseStatus(RouteResponse routeResponse, HttpServletResponse response) {
if (routeResponse != null) {
response.setStatus(routeResponse.getStatusCode());
}
}

/**
* Write响应头
*
* @param routeResponse route响应对象
* @param response 响应response
*/
@Override
protected void writeResponseHeader(RouteResponse routeResponse, HttpServletResponse response) {
if (routeResponse != null) {
if (CollectionUtil.isNotEmpty(routeResponse.getHeaders())) {
for (HeaderWrapper header : routeResponse.getHeaders()) {
if (!StrUtil.equalsIgnoreCase(header.getName(), "Transfer-Encoding")) {
response.addHeader(header.getName(), header.getValue());
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("响应类型:{},响应编码:{}", routeResponse.getContentType(), routeResponse.getCharsetEncoding());
}
response.setContentType(routeResponse.getContentType());
if (routeResponse.getContentLength() > 0) {
response.setContentLengthLong(routeResponse.getContentLength());
}
response.setCharacterEncoding(routeResponse.getCharsetEncoding().displayName());
}
}

/**
* 响应内容
*
* @param routeResponse route响应对象
* @param response 响应对象
*/
@Override
protected void writeBody(RouteResponse routeResponse, HttpServletResponse response) throws IOException {
if (routeResponse != null) {
if (routeResponse.success()) {
InputStream inputStream = routeResponse.getBody();
if (inputStream != null) {
int read = -1;
byte[] bytes = new byte[1024 * 1024];
ServletOutputStream outputStream = response.getOutputStream();
while ((read = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, read);
}
IoUtil.close(inputStream);
IoUtil.close(outputStream);
}
} else {
String text = routeResponse.text();
if (StrUtil.isNotBlank(text)) {
PrintWriter printWriter = response.getWriter();
printWriter.write(text);
printWriter.close();
}
}

}
}

/**
* 构建路由的请求上下文
*
* @param routeRequestContext 请求上下文
* @param request 请求对象
*/
@Override
protected void buildContext(RouteRequestContext routeRequestContext, HttpServletRequest request) throws IOException {
// 当前请求是否basic请求
String basicHeader = request.getHeader(ROUTE_PROXY_HEADER_BASIC_NAME);
if (StrUtil.isNotBlank(basicHeader)) {
BasicAuth basicAuth = routeRepository.getAuth(basicHeader);
if (basicAuth != null) {
//增加Basic请求头
routeRequestContext.addHeader("Authorization", RouteUtils.authorize(basicAuth.getUsername(),
basicAuth.getPassword()));
}
}
SwaggerRoute swaggerRoute = getRoute(request.getHeader(ROUTE_PROXY_HEADER_NAME));
//String uri="http://knife4j.xiaominfo.com";
String uri = swaggerRoute.getUri();
if (StrUtil.isBlank(uri)) {
throw new RuntimeException("Uri is Empty");
}
String host = URI.create(uri).getHost();
String fromUri = request.getRequestURI();
StringBuilder requestUrlBuilder = new StringBuilder();
requestUrlBuilder.append(uri);
// 判断当前聚合项目的contextPath
if (StrUtil.isNotBlank(this.rootPath) && !StrUtil.equals(this.rootPath, ROUTE_BASE_PATH)) {
fromUri = fromUri.replaceFirst(this.rootPath, "");
}
// 判断servicePath
if (StrUtil.isNotBlank(swaggerRoute.getServicePath()) && !StrUtil.equals(swaggerRoute.getServicePath(),
ROUTE_BASE_PATH)) {
if (StrUtil.startWith(fromUri, swaggerRoute.getServicePath())) {
//实际在请求时,剔除servicePath,否则会造成404
fromUri = fromUri.replaceFirst(swaggerRoute.getServicePath(), "");
}
}
requestUrlBuilder.append(fromUri);
//String requestUrl=uri+fromUri;
String requestUrl = requestUrlBuilder.toString();
if (logger.isDebugEnabled()) {
logger.debug("目标请求Url:{},请求类型:{},Host:{}", requestUrl, request.getMethod(), host);
}
routeRequestContext.setOriginalUri(fromUri);
routeRequestContext.setUrl(requestUrl);
routeRequestContext.setMethod(request.getMethod());
Enumeration<String> enumeration = request.getHeaderNames();
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
if (!ignoreHeaders.contains(key.toLowerCase())) {
routeRequestContext.addHeader(key, value);
}
}
routeRequestContext.addHeader("Host", host);
Enumeration<String> params = request.getParameterNames();
while (params.hasMoreElements()) {
String name = params.nextElement();
String value = request.getParameter(name);
//logger.info("param-name:{},value:{}",name,value);
routeRequestContext.addParam(name, value);
}
// 增加文件,sinc 2.0.9
try {
Collection<Part> parts=request.getParts();
if (CollectionUtil.isNotEmpty(parts)){
parts.forEach(part -> routeRequestContext.addPart(part));
}
} catch (ServletException e) {
//ignore
logger.warn("get part error,message:"+e.getMessage());
}
routeRequestContext.setRequestContent(request.getInputStream());
}

@Override
public SwaggerRoute getRoute(String header) {
//去除缓存机制,由于Eureka以及Nacos设立了心跳检测机制,服务在多节点部署时,节点ip可能存在变化,导致调试最终转发给已经下线的服务
//since 2.0.9
SwaggerRoute swaggerRoute = routeRepository.getRoute(header);
return swaggerRoute;
}

@Override
public List<SwaggerRoute> getRoutes() {
return routeRepository.getRoutes();
}

}

Nacos配置类

自动配置, 将nacos资源库,路由分发等加载到容器中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
java复制代码@Configuration
@AutoConfigureAfter(Knife4jAggregationAutoConfiguration.class)
@ConditionalOnProperty(name = "knife4j.enableAggregation", havingValue = "true")
public class DocNacosConfiguration {

final Environment environment;

@Autowired
public DocNacosConfiguration(Environment environment) {
this.environment = environment;
}

@Primary
@Bean(initMethod = "start", destroyMethod = "close")
@ConditionalOnProperty(name = "knife4j.nacos.enable", havingValue = "true")
@RefreshScope
public NacosRepository customNacosRepository(
@Autowired Knife4jAggregationProperties customKnife4jAggregationProperties) {
return new DocNacosRepository(customKnife4jAggregationProperties.getNacos());
}

/**
* 配合nacos配置中心动态刷新
*/
@Primary
@Bean
@ConfigurationProperties(prefix = "knife4j")
@RefreshScope
@ConditionalOnProperty(name = "knife4j.nacos.enable", havingValue = "true")
public Knife4jAggregationProperties customKnife4jAggregationProperties() {
return new Knife4jAggregationProperties();
}

@Bean
@Primary
@ConditionalOnProperty(name = "knife4j.nacos.enable", havingValue = "true")
public RouteDispatcher customRouteDispatcher(@Autowired RouteRepository routeRepository,
@Autowired RouteCache<String, SwaggerRoute> routeCache) {
//获取当前项目的contextPath
String contextPath = environment.getProperty("server.servlet.context-path");
if (StrUtil.isBlank(contextPath)) {
contextPath = "/";
}
if (StrUtil.isNotBlank(contextPath) && !StrUtil
.equals(contextPath, RouteDispatcher.ROUTE_BASE_PATH)) {
//判断是否/开头
if (!StrUtil.startWith(contextPath, RouteDispatcher.ROUTE_BASE_PATH)) {
contextPath = RouteDispatcher.ROUTE_BASE_PATH + contextPath;
}
}
return new DocRouteDispatcher(routeRepository, routeCache, ExecutorEnum.APACHE,
contextPath);
}

}

服务启动类

1
2
3
4
5
6
7
8
9
java复制代码@EnableDiscoveryClient
@SpringBootApplication
@Slf4j
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
log.info("启动成功");
}
}

配置文件application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yml复制代码server:
port: 8000
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
application:
name: knife4j-doc # 服务名称

knife4j:
# 开启聚合
enableAggregation: true
nacos:
enable: true
serviceUrl: http://localhost:8848/nacos

配置文件spring.factories

1
2
factories复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.cf.config.DocNacosConfiguration

测试

测试步骤

1 启动nacos服务

2 启动两个Demo业务服务

3 启动文档服务

4 本地访问 http://localhost:8000/doc.html

测试结果

1 测试结果, 发现业务服务的在线文档和文档服务完美聚合.

2 通过测试, 下线其中一个服务, 文档服务中,也会剔除掉相应的在文档.

3 将在线文档转为离线文档下载到本地, 功能正常

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

看了就能用的HTML+Freemarker小知识

发表于 2021-11-29

大家好,我是“Java分布式架构实战”的作者Jamesfu。

问题起源

最近在按工信部要求对网站做适老化无障碍浏览改造。在改造过程中发现一个Bug如下图所示:
image.png
经过多轮排查,最终定位到网站变灰相关的一段freemarker代码,发现它的ELSE分支输出了一个空的<span></span>标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码[#assign nowTime = .now?string["hhmmSSsss"]/]
[#if checkGrayScale]
[#if grayScaleType?? && 1==grayScaleType]
<script>
var isHomeGrayByTemplate = true
</script>
<link rel="stylesheet" href="/static/assets/css/homeGrayscale.css?t=${nowTime?number}"/>
[#elseif grayScaleType?? && 2==grayScaleType]
<script>
var isHomeGrayByTemplate = false
</script>
<link rel="stylesheet" href="/static/assets/css/grayscale.css?t=${nowTime?number}"/>
[/#if]
<script type="text/javascript" src="/static/assets/third/jquery.gray.min.js?t=${nowTime?number}"></script>
<script type="text/javascript" defer="defer" src="/static/assets/js/grayScale.js?t=${nowTime?number}"></script>
[#else]
<span></span>
[/#if]

我推断就是这里的问题,因为span标签是需要渲染的,所以出现在了body标签中,不能出现在head标签中。经过多轮测试发现确实是这里的问题。

结论

  1. 在HTML <head></head>标签中不要出现具体的需要渲染的HTML标签,比如<span></span>等,否则会出现渲染标签以后的内容都会输出到<body></body>标签中。
  2. 在freemarker模板中,如果ELSE分析不输出任何内容,发现freemarker不会输出空文件,即不更新静态化文件,因此在ELSE分支中还必须输出点什么,所以我决定输出一个空script标签,最终问题得到解决。

最终提交的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码[#assign nowTime = .now?string["hhmmSSsss"]/]
[#if checkGrayScale]
[#if grayScaleType?? && 1==grayScaleType]
<script>
var isHomeGrayByTemplate = true
</script>
<link rel="stylesheet" href="/static/assets/css/homeGrayscale.css?t=${nowTime?number}"/>
[#elseif grayScaleType?? && 2==grayScaleType]
<script>
var isHomeGrayByTemplate = false
</script>
<link rel="stylesheet" href="/static/assets/css/grayscale.css?t=${nowTime?number}"/>
[/#if]
<script type="text/javascript" src="/static/assets/third/jquery.gray.min.js?t=${nowTime?number}"></script>
<script type="text/javascript" defer="defer" src="/static/assets/js/grayScale.js?t=${nowTime?number}"></script>
[#else]
<script></script>
[/#if]

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

网络编程学习2--套接字、地址

发表于 2021-11-29

socket英文意思是”插口”,在网络编程中,它的寓意是可以通过插口接入的方式,快速完成网络连接和数据收发.

客户端和服务器工作的核心逻辑

客户端和服务器工作的核心逻辑.png
服务器端:

  1. 初始化socket(在客户端发起连接请求前,服务器必须先初始化好).
  2. 执行bind函数(将自己的服务能力绑定到一个众所周知的地址和端口上)
  3. 执行listen操作(将原先的socket转换为服务端的socket)
  4. 服务器阻塞在accept上,等待客户端请求的到来

客户端:

  1. 初始化socket
  2. 执行connect向服务器端的地址和端口发起连接请求(地址和端口是客户端预先知道的).

建立连接后,就可以进行数据传输了.

客户端进程向操作系统内核发起 write 字节流写操作,内核协议栈将字节流通过网络设备传输到服务器端,服务器端从内核得到信息,将字节流从内核读入到进程中,并开始业务逻辑的处理,完成之后,服务器端再将得到的结果以同样的方式写给客户端。可以看到,一旦连接建立,数据的传输就不再是单向的,而是双向的,这也是 TCP 的一个显著特性。

当客户端和服务器端完成交互后,需要断开连接时,就会执行close函数.操作系统内核此时会通过原先的连接链路向服务器端发送一个 FIN 包,服务器收到之后执行被动关闭,这时候整个链路处于半关闭状态,此后,服务器端也会执行 close 函数,整个链路才会真正关闭。半关闭的状态下,发起 close 请求的一方在没有收到对方 FIN 包之前都认为连接是正常的;而在全关闭的状态下,双方都感知连接已经关闭。

以上所有操作都是通过socket来完成的,socket是用来建立连接,传输数据的唯一途径.

socket地址格式

在使用socket时,首先要解决双方的寻址问题,由于需要socket的地址来建立连接,所以我们首先需要找到这个地址. 就像打电话(使用socket进行通信)时首先需要查找电话簿(寻址),找到你想要联系的那个人,你才可以建立连接,开始交流

通用sokcet地址格式

1
2
3
4
5
6
7
C复制代码/* POSIX.1g 规范规定了地址族为2字节的值.  */
typedef unsigned short int sa_family_t;
/* 描述通用套接字地址 */
struct sockaddr{
sa_family_t sa_family; /* 地址族. 16-bit*/
char sa_data[14]; /* 具体的地址值 112-bit */
};

在这个结构体里,第一个字段是地址族,它表示使用什么样的方式对地址进行解释和保存,好比电话簿里的手机号格式,或者是固话格式,这两种格式的长度和含义都是不同的.地址族在 glibc 里的定义非常多,常用的有以下几种:

  • AF_LOCAL:表示的是本地地址,对应的是 Unix 套接字,这种情况一般用于本地 socket 通信,很多情况下也可以写成 AF_UNIX、AF_FILE;
  • AF_INET:因特网使用的 IPv4 地址;
  • AF_INET6:因特网使用的 IPv6 地址

这里的 AF_ 表示的含义是 Address Family,即地址族, 但是很多情况下,我们也会看到以 PF_ 表示的宏,比如 PF_INET、PF_INET6 等,实际上 PF_ 的意思是 Protocol Family,也就是协议族的意思。我们用 AF_xxx 这样的值来初始化 socket 地址,用 PF_xxx 这样的值来初始化 socket。我们在<sys/socket.h>头文件中可以清晰地看到,这两个值本身就是一一对应的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C复制代码/* 各种地址族的宏定义  */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX PF_UNIX
#define AF_FILE PF_FILE
#define AF_INET PF_INET
#define AF_AX25 PF_AX25
#define AF_IPX PF_IPX
#define AF_APPLETALK PF_APPLETALK
#define AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#define AF_ATMPVC PF_ATMPVC
#define AF_X25 PF_X25
#define AF_INET6 PF_INET6

通用地址结构中的通用的意思是适用于多种地址族.

IPv4 socket地址格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C复制代码/* IPV4套接字地址,32bit值.  */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};

/* 描述IPV4的套接字地址格式 */
struct sockaddr_in
{
sa_family_t sin_family; /* 16-bit */ /* 地址族 */
in_port_t sin_port; /* 端口号 16-bit*/
struct in_addr sin_addr; /* Internet address. 32-bit */ /* 具体的地址值(IPv4的地址是32bit的) */


/* 这里仅仅用作占位符,不做实际用处 */
unsigned char sin_zero[8];
};

和通用地址格式一样,都有一个16bit的sin_family字段,对于IPv4来说这个字段的值就是AF_INET.

对于端口号,我们可以看到端口号最多是 16-bit,也就是说最大支持 2 的 16 次方,这个数字是 65536,所以我们应该知道支持寻址的端口号最多就是 65535。有一些保留端口,即约定俗成的,已经被对应服务广为使用的端口,如ssh的22端口,http的80端口,一般而言,大于5000的端口可以作为我们自己应用程序的端口使用.

glibc定义的保留端口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
C复制代码
/* Standard well-known ports. */
enum
{
IPPORT_ECHO = 7, /* Echo service. */
IPPORT_DISCARD = 9, /* Discard transmissions service. */
IPPORT_SYSTAT = 11, /* System status service. */
IPPORT_DAYTIME = 13, /* Time of day service. */
IPPORT_NETSTAT = 15, /* Network status service. */
IPPORT_FTP = 21, /* File Transfer Protocol. */
IPPORT_TELNET = 23, /* Telnet protocol. */
IPPORT_SMTP = 25, /* Simple Mail Transfer Protocol. */
IPPORT_TIMESERVER = 37, /* Timeserver service. */
IPPORT_NAMESERVER = 42, /* Domain Name Service. */
IPPORT_WHOIS = 43, /* Internet Whois service. */
IPPORT_MTP = 57,


IPPORT_TFTP = 69, /* Trivial File Transfer Protocol. */
IPPORT_RJE = 77,
IPPORT_FINGER = 79, /* Finger service. */
IPPORT_TTYLINK = 87,
IPPORT_SUPDUP = 95, /* SUPDUP protocol. */


IPPORT_EXECSERVER = 512, /* execd service. */
IPPORT_LOGINSERVER = 513, /* rlogind service. */
IPPORT_CMDSERVER = 514,
IPPORT_EFSSERVER = 520,

/* UDP ports. */
IPPORT_BIFFUDP = 512,
IPPORT_WHOSERVER = 513,
IPPORT_ROUTESERVER = 520,


/* Ports less than this value are reserved for privileged processes. */
IPPORT_RESERVED = 1024,


/* Ports greater this value are reserved for (non-privileged) servers. */
IPPORT_USERRESERVED = 5000

IPv6 socket地址格式

1
2
3
4
5
6
7
8
C复制代码struct sockaddr_in6
{
sa_family_t sin6_family; /* 16-bit */ /* 地址族 值为AF_INET6 */
in_port_t sin6_port; /* 传输端口号 # 16-bit */
uint32_t sin6_flowinfo; /* IPv6流控信息 32-bit*/
struct in6_addr sin6_addr; /* IPv6地址128-bit */
uint32_t sin6_scope_id; /* IPv6域ID 32-bit */
};

整个结构体长度是 28 个字节,其中流控信息和域 ID 先不用管,这两个字段,一个在 glibc 的官网上根本没出现,另一个是当前未使用的字段。这里的地址族显然应该是 AF_INET6,端口同 IPv4 地址一样,关键的地址从 32 位升级到 128 位,这个数字就大到恐怖了,完全解决了寻址数字不够的问题。

本地socket地址格式

前面的IPv4和IPv6地址格式都是因特网socket的格式,而本地socket格式是用来作为本地进程间的通信的,也就是前面提到的 AF_LOCAL

1
2
3
4
C复制代码struct sockaddr_un {
unsigned short sun_family; /* 固定为 AF_LOCAL */
char sun_path[108]; /* 路径名 */
};

几种socket地址格式的比较

IPv4和IPv6的socket地址格式的长度是固定的,而本地socket地址格式的长度是可变的.

几种socket地址格式的比较.png

问题

  1. 这些socket地址格式有什么共性?
    都具有地址族字段和地址字段(本地socket地址格式中,路径名即是地址).

通过通用socket地址格式来提供一个统一的接口,然后通过地址族字段来确定具体是什么类型的地址.
2. 为什么本地socket格式不需要端口号,而 IPv4 和 IPv6 socket格式却需要端口号?
在unix系统中,一切皆文件,socket也是文件.

本地socket本质上是在访问本地的文件系统,根据文件路径就可以区分,所以不需要端口号.而远程socket是直接将一段字节流发送到远程计算机的一个进程,而远程计算机可能同时有多个进程在进行监听,所以需要用端口号标志发给哪一个进程.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

某小厂Java面试题:深拷贝和浅拷贝区别了解吗?什么是引用拷

发表于 2021-11-29

大家好,我是Guide!

今天给大家分享一个比较基础也是非常常见的 Java 面试题:“深拷贝和浅拷贝区别了解吗?什么是引用拷贝?”。

另外,本文也已经放到了我的网站:javaguide.cn/ 。

关于深拷贝和浅拷贝区别,我这里先给结论:

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
  • 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

上面的结论没有完全理解的话也没关系,我们来看一个具体的案例!

浅拷贝

浅拷贝的示例代码如下,我们这里实现了 Cloneable 接口,并重写了 clone() 方法。

clone() 方法的实现很简单,直接调用的是父类 Object 的 clone() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码public class Address implements Cloneable{
private final String name;
// 省略构造函数、Getter&Setter方法
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}

public class Person implements Cloneable {
private Address address;
// 省略构造函数、Getter&Setter方法
@Override
public Person clone() {
try {
Person person = (Person) super.clone();
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}

测试 :

1
2
3
4
java复制代码Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());

从输出结构就可以看出, person1 的克隆对象和 person1 使用的仍然是同一个 Address 对象。

深拷贝

这里我们简单对 Person 类的 clone() 方法进行修改,连带着要把 Person 对象内部的 Address 对象一起复制。

1
2
3
4
5
6
7
8
9
10
java复制代码@Override
public Person clone() {
try {
Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}

测试 :

1
2
3
4
java复制代码Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());

从输出结构就可以看出,虽然 person1 的克隆对象和 person1 包含的 Address 对象已经是不同的了。

那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。

我专门画了一张图来描述浅拷贝、深拷贝、引用拷贝:

shallow&deep-copy.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…117118119…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%