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

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


  • 首页

  • 归档

  • 搜索

Java数据结构告诉你如何选用数据集合(1)

发表于 2021-11-26

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

开始学习编程的时候,目的在于如何实现功能。在我们熟悉编程之后,发现实现的方法是多种多样的。我们操作一个班级,可以选择数组、List、Set甚至于Map。但是具体实行起来,会发现情况复杂多变。而这个时候,实现方法的多样性也让我们束手无策。这个时候就需要数据结构登场了,学习数据结构我们就可以根据不同的情况选取最优的实现方法。当然了,还有一部分工作要结合软件工程和设计模式来实现。

下面我们来了解一下几个问题:

1、什么是数据结构?

2、数据结构有什么用?

3、数据结构在Java中的具体表现是什么?

下面一个个来解决这几个问题。

1、数据结构是以某种形式将数据组织在一起的合集。数据结构不仅是存储数据,还支持访问和处理数据的操作。 也就是说数据结构是:一组有组织的数据数据+数据的处理操作。

2、提高软件性能,这个在后面具体的数据结构中会了解到。

3、在Java中,一种数据结构就是一个容器,或者容器对象。(List、Set)

下面才是正题,线性表。

一、线性表的概念:

今天就来具体讲一下线性表。关于线性表的定义我这里就不赘述了,有一点理解线性表非常关键。线性表元素之间是一对一的关系,就是说线性表中的一个元素最多只有一个前驱元素和一个后继元素。 通俗讲就是 “前面一个,后面一个”。

二、线性表的存储结构

1、顺序存储结构:在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素****

2、链式存储结构:在计算机中用一组任意的存储单元存储线性表的数据元素****

可能上面两句话没有解释太多内容,不过不要紧,后面会具体讲。

三、顺序表

1、什么是顺序表:顺序存储结构的线性表(哈哈,就是这样)

2、顺序表的特点:

①元素存储在一段连续内存当中

②查找速度快

③插入数据速度慢

3、Java中的线性表ArrayList

在Java中,ArrayList就是一个线性表。List结构就是一个线性表的数据结构,它规定了线性表的共同属性和操作。在学习集合的 时候,发现List有一大堆实现类。其中ArrayList和LinkedList就是两个非常常用而且重要的类,它们也代表了线性表的两种存储 结构。ArrayList就是顺序存储结构,而LinkedList就是链式存储结构。

4、用Java编写一个顺序表

因为篇幅的问题,后面我会另外写两篇或者一篇关于手动写顺序表和链表的文章。这里主要讲一些内容。

①线性表是通过数组实现的

②当数组长度不够时要重新创建一个数组(Java中数组是定长的)

四、链表

1、什么是链表:链式存储结构的线性表

2、链表的特点:

①元素在内存中随机存储

②查找速度效率低

③插入速度效率高

3、Java中的链表LinkedList:上面已经讲了,这里就不赘述了。

4、用Java编写一个顺序表

①链表是通过节点实现的

②节点里面有节点数据以及下一个节点的地址

除了ArrayList和LinkedList,Java中还有许多其它的线性表。以后有机会和大家细细研究。关于线性表的实现,后面也会补齐文章。

​

本文转载自: 掘金

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

nosql不仅仅是sql Docker 安装 MongoDB

发表于 2021-11-26

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

web开发常用到的是缓存机制,提到缓存常见的redis、MongoDB并不可少。今天我们简单了解下mongoDB

Docker 安装 MongoDB

  • docker技术已经很成熟了。对我们安装服务来说也很方便了。这里贴出安装步骤
  • 拉取mongo最新版本镜像

docker pull mongo:latest

  • 启动容器

docker run -itd --name mongo -p 27017:27017 mongo --auth

  • 创建用户用于访问(admin)

docker exec -it mymongodb mongo admin

001.jpg

  • 至此,mongodb的docker安装已经完成了。下面我们就可以连接了。如果上面启动含有--auth表示mongo是区分权限的。在通过可视化工具链接的时候需要验证用户信息才可以操作的。不同用户拥有不同权限。
  • 首先我们先为连接进来的admin用户设置密码及权限

002.jpg

db.createUser({ user: 'admin', pwd: '密码', roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] });

  • 上面的admin用户拥有查看admin数据库的权限。
  • mongo内部内置了7中角色供我们使用
角色名 角色ID
数据库用户角色 read、readWrite
数据库管理角色 dbAdmin、dbOwner、userAdmin
集群管理角色 clusterAdmin、clusterManager、clusterMonitor、hostManager
备份恢复角色 backup、restore
所有数据库角色 readAnyDatabase、readWriteAnyDatabase
超级用户角色 root
内部角色 __system
  • 这里还有几个角色间接或直接提供了系统超级用户的访问(dbOwner 、userAdmin、userAdminAnyDatabase),下面看看具体角色功能
角色ID 功能
Read 允许用户读取指定数据库
readWrite 允许用户读写指定数据库
dbAdmin 允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问system.profile
userAdmin 允许用户向system.users集合写入,可以找指定数据库里创建、删除和管理用户
clusterAdmin 只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限
readAnyDatabase 只在admin数据库中可用,赋予用户所有数据库的读权限
readWriteAnyDatabase 只在admin数据库中可用,赋予用户所有数据库的读写权限
userAdminAnyDatabase 只在admin数据库中可用,赋予用户所有数据库的userAdmin权限
dbAdminAnyDatabase 只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限
root 只在admin数据库中可用。超级账号,超级权限

命令联系

创建集合

  • 在上面最后部分我们已经掌握了创建数据库和创建人及分配权限的问题。数据库之后我们就需要创建集合了。这里的集合可以理解成SQL中的Table。
  • 上面我们创建了两个人 admin 、root .admin权限只有管理人员的权限。root是超管权限。我们先看看admin创建集合情况

003.jpg

  • mongo给我们报了无权限的错误。同样的操作我们去可视化界面操作也是同样的结果

004.jpg

005.jpg

006.jpg

  • 上面我们在可视化界面是通过admin用户去连接的。这个时候我们连接的上而且能看到admin这个。但是我们没法查看任何集合。点开只会报如下错误

007.jpg

  • 和我们在命令行中执行的效果其实是一样的。admin用户的权限只是用来管理用户的。
  • 下面我们切换root用户超管权限就可以CURD了。

008.jpg

009.jpg

本文转载自: 掘金

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

JVM类加载机制

发表于 2021-11-26

JVM类加载机制

前言

一个对象在创建过程中,首先要判断这个类是否加载了,只有加载了我们才可以给他分配相应的内存。加载器并不是很神秘,加载器就是一个类。下面就让我们来一起了解一下什么是JVM的类加载机制。

01JVM类加载机制.png

类加载过程

类加载主要包含加载、验证、准备、解析、初始化、使用、卸载过程。

加载:使用到该类时,读入该类的class文件到内存中,并且在内存中会生成一个该类的java.lang.Class对象,作为这个类各种数据的访问入口。

验证:检验字节码文件的正确性。

准备:给类的静态变量分配内存,赋予默认值。

解析:将符号引用替换为直接引用(指向内存的地址)。

初始化:将类的静态变量赋予真正的值。

类加载器

类加载器并没有这么神秘,他其实就是一个类,主要分为以下几种类加载器。

  • 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
  • 自定义加载器:负责加载用户自定义路径下的类包

在我们执行代码的时候,首先由c++创建引导类加载器,然后再由JVM启动实例Launcher创建扩展类加载器、应用程序类加载器,由应用程序类加载器加载我们自定义的加载器。下面我们看一下Launcher构造类加载器的代码。

1637812524(1).jpg
可以看到扩展类加载器父类没有设置,应用程序加载器父类为扩展类加载器。

双亲委派机制

在类加载过程中,有一个双亲委派机制,大致思想就是加载的类的时候,先让父类加载,父类加载不了让子类加载。

未命名文件 (6).png

为什么这样设计,主要出于以下两个原因:

  • 防止java核心库被自定义的加载器加载,试想一下,如果任何一个类加载器都可以加载java核心库的类,例如String,那么必定会带来安全问题,也会导致java核心库被修改。
  • 防止一个类被多个加载器加载。

构造自己的类加载器

下面就让我们来自己构造一个类加载器。

1637823926(1).jpg

主要要自己构建test/com/test/Test目录,并把Test.class文件放到该目录下,自定义类加载器的核心就是重写findClass方法,指定加载的目录。

Tomcat打破双亲委派机制

为什么tomcat会打破双亲委派机制?因为一个tomcat可能会有多个服务,虽然我们建议一个tomcat部署一个服务,每个服务用到的框架版本可能不一样,如果采用双亲委派机制,就会无法统一版本,因此tomcat打破了双亲委派机制,主要就是重写ClassLoader的loadClass方法。

本文转载自: 掘金

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

selenium的使用(一)

发表于 2021-11-26

selenium驱动对象/元素获取/发送信息

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
python复制代码import selenium
import urllib
import selenium.webdriver
import selenium.webdriver.common.keys
import time

"""
步骤:
1.开启webdriver对象
2.get获取网页信息
3.通过元素定位想要的位置信息
4.send_keys输入想要输入的信息
5.按键进行操作
"""

driver = selenium.webdriver.Chrome(r"C:\Users\xxx\Desktop\chromedriver.exe")
driver.get("http://www.baidu.com")
elem = driver.find_element_by_id("kw")

elem.send_keys("python")
time.sleep(3)
elem.send_keys(selenium.webdriver.common.keys.Keys.RETURN)

time.sleep(20)
driver.close()

模拟登录xxxx网站

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
python复制代码import selenium
import selenium.webdriver
import selenium.webdriver.common.keys
import time

driver = selenium.webdriver.Chrome(r"C:\Users\xxx\Desktop\chromedriver.exe")
driver.get("https://account.xxxxx.com/signin?returnUrl=https%3A%2F%2Fwww.xxx.com%2F")
elem = driver.find_element_by_id("LoginName")
elem.send_keys("*******")
elem2 = driver.find_element_by_id("Password")
elem2.send_keys("******")
time.sleep(3)
elem2.send_keys(selenium.webdriver.common.keys.Keys.RETURN)
time.sleep(10)
pagesource = driver.page_source
print(pagesource)

time.sleep(20)
driver.close()

page_source和find_element_by_xapth

相较于by_class_name的不确定性,使用by_xpath更能很好的定位元素的位置,by_xpath是最后的杀手锏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码import selenium
import selenium.webdriver
import selenium.webdriver.common.keys
import time
driver = selenium.webdriver.Chrome(r"C:\Users\xxxx\Desktop\chromedriver.exe")
driver.get("https://talent.baidu.com/external/baidu/index.html#/social/2/python")
time.sleep(5)
for i in range(10):
# elem = driver.find_element_by_class_name("next")
"""
相较于by_class_name的不确定性,使用by_xpath更能很好的定位元素的位置,by_xpath是最后的杀手锏
"""
elem = driver.find_element_by_xpath("//*[@class="pagination"]/ul//li[last()]/a")
print(elem)
time.sleep(3)

elem.click()
pagesource = driver.page_source
print(pagesource)
print("--"*40)

time.sleep(40)
driver.close()

本文转载自: 掘金

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

Go语言学习查缺补漏ing Day8

发表于 2021-11-26

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

Go语言学习查缺补漏ing Day8

本文收录于我的专栏:《让我们一起Golang》

一、为什么map的value值是不可寻址的?解决办法?

先来看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main
​
import "fmt"
​
type Hello struct {
x, y int
}
​
var m = map[string]Hello{
"hello": Hello{2, 3},
}
​
func main() {
m["hello"].x = 4
fmt.Println(m["hello"].x)
}

运行上面这个程序会报错:

1
2
go复制代码# command-line-arguments
.\demo.go:14:15: cannot assign to struct field m["hello"].x in map

为什么呢?下面来进行详细说明:

因为map是无法进行寻址的,也就是说可以获取m[“hello”].x的值,但是不能对其值进行修改。

究其原因,因为Go的map是通过散列表来实现的,说得更具体一点,就是通过数组和链表组合实现的。

并且Go的map也可以做到动态扩容,当进行扩容之后,map的value那块空间地址就会产生变化,所以无法对map的value进行寻址。

但是注意,map与slice切片的扩容有些不同,map是引用类型,扩容后,value引用地址不会变化,所以map value元素不可寻址。而slice扩容后是生成一个新的底层数组。

有什么解决办法呢?

解决办法一:使用临时变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main
​
import "fmt"
​
type Hello struct {
x, y int
}
​
var m = map[string]Hello{
"hello": Hello{2, 3},
}
​
func main() {
tmp := m["hello"]
tmp.x = 4
m["hello"] = tmp
fmt.Println(m["hello"].x)
}
​

解决办法二:使用指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码package main
​
import "fmt"
​
type Hello struct {
x, y int
}
​
var m = map[string]*Hello{
"hello": &Hello{2, 3},
}
​
func main() {
m["hello"].x = 4
fmt.Println(m["hello"].x)
}

二、遍历切片的循环次数会不会改变

看下面这段代码会不会出现死循环:

1
2
3
4
5
6
7
8
go复制代码package main
​
func main() {
values := []int{1, 2, 3}
for value := range values {
values = append(values, value)
}
}

答案是不会出现死循环,程序能够正常退出。

这是因为循环次数在for…range 之前就已经确定了,循环之内改变切片的长度,并不会影响循环次数。

三、 for…range复用临时变量

看一看下面这段代码,你认为会输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main
​
import (
"fmt"
"time"
)
​
func main() {
var s = [...]int{1, 2, 3}
for index, value := range s {
go func() {
fmt.Println(index, value)
}()
}
time.Sleep(time.Second * 3)
}
​

image-20211126112113115

哈哈,有的小伙伴会不会很奇怪为什么是输出一样的值?

因为这里使用:=的形式迭代变量,index和value都会在每次循环被重用,并不会进行重新声明。

所以各个协程都是输出循环结束后的index以及value值,而不是每个协程开始时的index以及value值。

那有什么解决办法呢?

解决办法之一:使用函数参数进行传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main
​
import (
"fmt"
"time"
)
​
func main() {
var s = [...]int{1, 2, 3}
for index, value := range s {
go func(index,value int) {
fmt.Println(index, value)
}(index,value)
}
time.Sleep(time.Second * 3)
}
​

image-20211126113014044

这样就能解决了。

解决办法二:使用临时变量进行传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main
​
import (
"fmt"
"time"
)
​
func main() {
var s = [...]int{1, 2, 3}
for index, value := range s {
i := index
v := value
go func() {
fmt.Println(i, v)
}()
}
time.Sleep(time.Second * 3)
}
​

image-20211126113203098

这样也能解决这个问题。

本文转载自: 掘金

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

windows安装Mysql

发表于 2021-11-26

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

1 下载

进入官网找到自己所需的安装包
dev.mysql.com/downloads/i…

mysql

2 安装

2.1 双击安装包

双击运行下载好的mysql-installer-community-xxxx.msi,程序运行需要一些时间,请等待一下。
init-mysql

2.2 安装欢迎界面

运行成功之后,进入mysql安装的欢迎界面.勾选我同意协议,选择Next进行安装
welcome-mysql

2.3 选择安装类型

进入类型选择页面,如果只想安装mysql server的就选择server only模式,选择好了点击Next
::: tip 服务类型说明

  • developer default(开发者默认):安装mysql开发所需的所有产品
  • server only(服务器):只安装mysql服务器产品
  • client only(客户端):只安装没有服务器的mysql客户端产品
  • full(完全):安装所有包含的mysql产品和功能
  • custom(手动):手动选择系统上应安装的产品
    :::
    mysqltype

2.4 选择mysql的安装路径

一般选择默认路径,点击Next即可,如图

mysql-path

2.5 安装mysql程序

在安装所选界面能看到我们接下来所需要安装的程序,点击execute

mysql-exeute
执行完成后点击Next

mysql-install

2.6 产品配置

点击Next即可

product-config

2.7 mysql server类型选择

这里选择Next,类型说明如下
::: tip mysql server类型说明

  • standalone mysql server/classic mysql replication:独立的mysql服务器/经典的mysql复制。选择这个选项,如果你想运行mysql服务器是独立的,有机会以后配置经典的mysql复制
  • innodb cluster sandbox thst setup(for testing only):innodb集群沙箱test设置(仅用于测试)
    :::

mysql-server-type

2.8 设置服务器配置类型

设置服务器配置类型以及连接端口,这里默认配置即可,点击next

  • Config Type:选择Development Machine,用于小型以及学习所用足够了。
  • Port number:输入3306,也可以输入其他最好是3306-3309之间。

mysql-network

2.8 配置root的密码

mysql-password

2.9 配置mysql显示名字

配置mysql在windows系统中的名字,是否选择开机启动mysql服务(默认开机自启动),其它的没进行修改,点击”Next”。

mysql-name

2.10 应用配置

Mysql server:apply configuration(应用配置页面),选择execute进行安装配置

apply-config
mysql-finish

2.11 安装完成的产品配置

安装程序又回到了product configuration页面,此时我们看到mysql server安装成功的显示,点击Next

product-finish

2.12 安装完成

mysql-install-finish

3 配置mysql环境变量

上面安装的是时候我们看到mysql默认安装路径是:C:\Program Files\MySQL\MySQL Server 5.7
我的电脑右键—>属性高级系统设置环境变量新建MYSQL_HOME,将安装目录输入

mysql-home

找到path编辑:输入%MYSQL_HOME%\bin

mysql-home-path

4 验证

  • 1、打开cmd输入mysql –u root –p
  • 2、输入root的密码
  • 3、显示如下页面及说明安装成功

mysql

本文转载自: 掘金

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

【BUG日记】【MySQL】多个排序字段,是有优先级的,先来

发表于 2021-11-26

【日期】:2021/11/26

【问题】:在用JPA查询语句并多个字段排序时候,查询的排序结果未达到想要的效果

【原因】:拼接的MySQL语句排序字段顺序不对

【如何发现】 :以为是jpa的操作问题,但是通过写MySQL语句在数据库里进行查询,发现是MySQL语句问题,然后把排序字段的位置换了一下,尝试运行了一下。发现达到我想要的效果了。

【如何修复】 :多个排序字段想要达到想要的查询排序结果,是根据排序字段的先后顺序的。优先级是先越排前越高。

举个例子:

  1. 查询当前文章中,阅读量最多、评论最多、是新文章。(ps:注意顺序)
1
mysql复制代码select * from t_ey_essay where is_hide=0 and is_del = 0 order by read_sum desc, comment_sum desc, id desc

查询结果:
image.png
2. 查询当前文章中,阅读量最多、是新文章、评论最多。(ps:注意顺序)

1
mysql复制代码select * from t_ey_essay where is_hide=0 and is_del = 0 order by read_sum desc, id desc, comment_sum desc

查询结果:
image.png
3. 查询当前文章中,是新文章、阅读量最多、评论最多。(ps:注意顺序)

1
mysql复制代码select * from t_ey_essay where is_hide=0 and is_del = 0 order by id desc, read_sum desc, comment_sum desc

查询结果:
image.png

【总结】 :排序也有优先级,先来先优先排序!

【最后】

感谢你看到最后,如果你持有不同的看法,欢迎你在文章下方进行留言、评论。

我是南方者,一个热爱计算机更热爱祖国的南方人。

  文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。

本文转载自: 掘金

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

使用volatile关键字时的伪共享问题

发表于 2021-11-26

在使用volatile关键字的时候,需要额外关注一下伪共享的问题。

先说一下cpu缓存的模型:

企业微信截图_16375725381706.png

cpu和主内存中间存在缓存,访问缓存的速度比内存快很多。cpu读取数据时,会先尝试在缓存里获取,缓存不能命中时才去读内存;操作一个数据时,会先在缓存里修改,再将结果同步到主内存中。

缓存读取的单位是缓存行,也就是说cpu每次从缓存里读取数据都是读取一个缓存行。

我们知道volatile关键字可以用来保证变量的可见性,它实现这个功能靠的是缓存一致性协议:一旦cpu修改了某个缓存行的数据,对于其他cpu而言,这个缓存行就失效了,只能从内存中重新加载。

缓存行的大小一般是64字节,可以存8个long类型变量。

现在假设这样一种情况,我们有一组volatile修饰的long变量,被多个线程同时操作。这些变量在内存上是连续的,存储在同一个缓存行上。由于缓存一致性协议,只要其中有任意一个变量被修改,都会使整个缓存行失效,其他cpu要读取时只能重新访问内存,从而造成比较大的开销,这个问题就是“伪共享(fase sharing)”。

解决伪共享的一种有效的方式就是填充缓存行。如果我们能保证每一个volatile修饰的变量都能独占缓存行,就不会因为其他变量被修改而使缓存失效,这是一种以空间换时间的策略。

下面这段代码摘自Martin Thompson的博客,可以证明填充缓存行的效果:

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
java复制代码public final class FalseSharing
implements Runnable
{
public final static int NUM_THREADS = 4; // change
public final static long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;

private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
static
{
for (int i = 0; i < longs.length; i++)
{
longs[i] = new VolatileLong();
}
}

public FalseSharing(final int arrayIndex)
{
this.arrayIndex = arrayIndex;
}

public static void main(final String[] args) throws Exception
{
final long start = System.nanoTime();
runTest();
System.out.println("duration = " + (System.nanoTime() - start));
}

private static void runTest() throws InterruptedException
{
Thread[] threads = new Thread[NUM_THREADS];

for (int i = 0; i < threads.length; i++)
{
threads[i] = new Thread(new FalseSharing(i));
}

for (Thread t : threads)
{
t.start();
}

for (Thread t : threads)
{
t.join();
}
}

public void run()
{
long i = ITERATIONS + 1;
while (0 != --i)
{
longs[arrayIndex].value = i;
}
}

public final static class VolatileLong
{
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // comment out
}
}

这段代码中同时开启4个线程,对5千万个volatile修饰的变量循环执行读写操作。

注意61行的代码,声明了6个没有作用的长整型变量,目的就是增加每两个volatile变量之间的间隙,尽可能保证两个volatile变量不会出现在同一个缓存行上。

我们可以将61行注释掉,再看看输出的运行时间。

不注释的时间是16544754400,注释后的时间是43853611600,填充缓存行后执行快了3倍。

值得一提的是,上面这种填充方式在jdk1.7以后有可能并不管用。有文章指出jdk1.7会在编译期间优化那些没有作用的变量,导致上述代码失去效果。(不过似乎这也和虚拟机有关,我在jdk1.8,hotspot的环境下并没有复现这个问题)

网上我看到有两种绕开这个问题的方式。

第一种是通过添加一个操作填充变量的方法,骗过编译器的优化机制:

1
2
3
4
5
6
7
8
java复制代码public final static class VolatileLong
{
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6;
public long sum() {
return p1 + p2 + p3 + p4 + p5 + p6;
}
}

第二种则是利用继承,将填充量放在子类里,也可以绕开优化(Disruptor框架中使用了这样的方式):

1
2
3
4
5
6
7
java复制代码public static class VolatileLong
{
public volatile long value = 0L;
}
public final static class PaddingLong extends VolatileLong {
public long p1, p2, p3, p4, p5, p6;
}

另外,java8中提供了一个@Contended注解,可以用于对齐缓存行,解决伪共享。使用时需要在jvm指令上添加:-XX:-RestrictContended

本文转载自: 掘金

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

C++ 中的 std next_permutation 和

发表于 2021-11-26

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

std::next_permutation

它用于将范围 [first, last) 中的元素重新排列为下一个字典序更大的排列。一个排列是 N! 元素可以采用的可能排列(其中 N 是范围内的元素数)。不同的排列可以根据它们在字典上相互比较的方式进行排序。代码的复杂度为 O(n*n!),其中还包括打印所有排列。

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c++复制代码模板
bool next_permutation(首先是
双向
迭代器, 最后是 双向迭代器 ); 参数:
first, last : 初始的双向迭代器
和序列的最终位置。范围
used 是 [first, last),其中包含所有元素
在 first 和 last 之间,包括指向的元素
by first 但不是 last 指向的元素。

返回值:
true : 如果函数可以重新排列
对象作为字典序更大的排列。
否则,该函数返回 false 以指示
安排不大于以前,
但可能是最低的(按升序排序)。

应用: next_permutation 是为给定的值数组找到下一个字典序更大的值。

例子:

1
2
3
4
5
c++复制代码输入:1 2 3 的下一个排列是 
输出:1 3 2

输入:4 6 8 的下一个排列是
输出:4 8 6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c++复制代码#include <algorithm>
#include <iostream>
using namespace std;

int main()
{
int arr[] = { 1, 2, 3 };

sort(arr, arr + 3);

cout << "3!3个元素的可能排列:\n";
do {
cout << arr[0] << " " << arr[1] << " " << arr[2] << "\n";
} while (next_permutation(arr, arr + 3));

cout << "循环后: " << arr[0] << ' '
<< arr[1] << ' ' << arr[2] << '\n';

return 0;
}

输出:

1
2
3
4
5
6
7
8
c++复制代码3!3个元素的可能排列:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
循环后:1 2 3

std::prev_permutation

它用于将范围 [first, last) 中的元素重新排列为前一个按字典顺序排列的排列。一个排列是 N! 元素可以采用的可能排列(其中 N 是范围内的元素数)。不同的排列可以根据它们在字典上相互比较的方式进行排序。

语法 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c++复制代码模板
bool prev_permutation(首先是
双向
迭代器, 最后是 双向迭代器 ); 参数:
first, last : 初始的双向迭代器
和序列的最终位置。范围
使用的是 [first, last),其中包含所有
first 和 last 之间的元素,包括
first 指向的元素但不是元素
最后指出。

返回值:
true : 如果函数可以重新排列
对象作为字典序较小的排列。
否则,该函数返回 false 以指示
安排不低于以前,
但最大的可能(按降序排序)。

应用: prev_permutation 是为给定的值数组找到以前的字典序较小的值。

例子:

1
2
3
4
5
c++复制代码输入:3 2 1的prev排列是 
输出:3 1 2

输入:8 6 4 的上一个排列是
输出:8 4 6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c++复制代码#include <algorithm>

#include <iostream>
using namespace std;
int main()
{
int arr[] = { 1, 2, 3 };

sort(arr, arr + 3);
reverse(arr, arr + 3);

cout << "3!3个元素的可能排列:\n";
do {
cout << arr[0] << " " << arr[1] << " " << arr[2] << "\n";
} while (prev_permutation(arr, arr + 3));

cout << "循环后: " << arr[0] << ' ' << arr[1]
<< ' ' << arr[2] << '\n';

return 0;
}

输出:

1
2
3
4
5
6
7
8
c++复制代码3!3个元素的可能排列:
3 2 1
3 1 2
2 3 1
2 1 3
1 3 2
1 2 3
循环后:3 2 1

本文转载自: 掘金

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

代理网关设计与实现(基于NETTY) 一 问题背景 二 技术

发表于 2021-11-26

简介:本文重点在代理网关本身的设计与实现,而非代理资源的管理与维护。

作者 | 新然

来源 | 阿里技术公众号

一 问题背景

  1. 平台端购置一批裸代理,来做广告异地展现审核。从外部购置的代理,使用方式为:
  2. 通过给定的HTTP 的 API 提取代理 IP:PORT,返回的结果会给出代理的有效时长 3~5 分钟,以及代理所属地域;

从提取的代理中,选取指定地域,添加认证信息,请求获取结果;

本文设计实现一个通过的代理网关:

  1. 管理维护代理资源,并做代理的认证鉴权;
  2. 对外暴露统一的代理入口,而非动态变化的代理IP:PORT;
  3. 流量过滤及限流,比如:静态资源不走代理;

本文重点在代理网关本身的设计与实现,而非代理资源的管理与维护。

注:本文包含大量可执行的JAVA代码以解释代理相关的原理

二 技术路线

本文的技术路线。在实现代理网关之前,首先介绍下代理相关的原理及如何实现

  1. 透明代理;
  2. 非透明代理;
  3. 透明的上游代理;
  4. 非透明的上游代理;

最后,本文要构建代理网关,本质上就是一个非透明的上游代理,并给出详细的设计与实现。

1 透明代理

透明代理是代理网关的基础,本文采用JAVA原生的NIO进行详细介绍。在实现代理网关时,实际使用的为NETTY框架。原生NIO的实现对理解NETTY的实现有帮助。

透明代理设计三个交互方,客户端、代理服务、服务端,其原理是:

  1. 代理服务在收到连接请求时,判定:如果是CONNECT请求,需要回应代理连接成功消息到客户端;
  2. CONNECT请求回应结束后,代理服务需要连接到CONNECT指定的远程服务器,然后直接转发客户端和远程服务通信;
  3. 代理服务在收到非CONNECT请求时,需要解析出请求的远程服务器,然后直接转发客户端和远程服务通信;

需要注意的点是:

  1. 通常HTTPS请求,在通过代理前,会发送CONNECT请求;连接成功后,会在信道上进行加密通信的握手协议;因此连接远程的时机是在CONNECT请求收到时,因为此后是加密数据;
  2. 透明代理在收到CONNECT请求时,不需要传递到远程服务(远程服务不识别此请求);
  3. 透明代理在收到非CONNECT请求时,要无条件转发;

完整的透明代理的实现不到约300行代码,完整摘录如下:

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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
java复制代码@Slf4j
public class SimpleTransProxy {

public static void main(String[] args) throws IOException {
int port = 8006;
ServerSocketChannel localServer = ServerSocketChannel.open();
localServer.bind(new InetSocketAddress(port));
Reactor reactor = new Reactor();
// REACTOR线程
GlobalThreadPool.REACTOR_EXECUTOR.submit(reactor::run);

// WORKER单线程调试
while (localServer.isOpen()) {
// 此处阻塞等待连接
SocketChannel remoteClient = localServer.accept();

// 工作线程
GlobalThreadPool.WORK_EXECUTOR.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
// 代理到远程
SocketChannel remoteServer = new ProxyHandler().proxy(remoteClient);

// 透明传输
reactor.pipe(remoteClient, remoteServer)
.pipe(remoteServer, remoteClient);
}
});
}
}
}

@Data
class ProxyHandler {
private String method;
private String host;
private int port;
private SocketChannel remoteServer;
private SocketChannel remoteClient;

/**
* 原始信息
*/
private List<ByteBuffer> buffers = new ArrayList<>();
private StringBuilder stringBuilder = new StringBuilder();

/**
* 连接到远程
* @param remoteClient
* @return
* @throws IOException
*/
public SocketChannel proxy(SocketChannel remoteClient) throws IOException {
this.remoteClient = remoteClient;
connect();
return this.remoteServer;
}

public void connect() throws IOException {
// 解析METHOD, HOST和PORT
beforeConnected();

// 链接REMOTE SERVER
createRemoteServer();

// CONNECT请求回应,其他请求WRITE THROUGH
afterConnected();
}

protected void beforeConnected() throws IOException {
// 读取HEADER
readAllHeader();

// 解析HOST和PORT
parseRemoteHostAndPort();
}

/**
* 创建远程连接
* @throws IOException
*/
protected void createRemoteServer() throws IOException {
remoteServer = SocketChannel.open(new InetSocketAddress(host, port));
}

/**
* 连接建立后预处理
* @throws IOException
*/
protected void afterConnected() throws IOException {
// 当CONNECT请求时,默认写入200到CLIENT
if ("CONNECT".equalsIgnoreCase(method)) {
// CONNECT默认为443端口,根据HOST再解析
remoteClient.write(ByteBuffer.wrap("HTTP/1.0 200 Connection Established\r\nProxy-agent: nginx\r\n\r\n".getBytes()));
} else {
writeThrouth();
}
}

protected void writeThrouth() {
buffers.forEach(byteBuffer -> {
try {
remoteServer.write(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}
});
}

/**
* 读取请求内容
* @throws IOException
*/
protected void readAllHeader() throws IOException {
while (true) {
ByteBuffer clientBuffer = newByteBuffer();
int read = remoteClient.read(clientBuffer);
clientBuffer.flip();
appendClientBuffer(clientBuffer);
if (read < clientBuffer.capacity()) {
break;
}
}
}

/**
* 解析出HOST和PROT
* @throws IOException
*/
protected void parseRemoteHostAndPort() throws IOException {
// 读取第一批,获取到METHOD
method = parseRequestMethod(stringBuilder.toString());

// 默认为80端口,根据HOST再解析
port = 80;
if ("CONNECT".equalsIgnoreCase(method)) {
port = 443;
}

this.host = parseHost(stringBuilder.toString());

URI remoteServerURI = URI.create(host);
host = remoteServerURI.getHost();

if (remoteServerURI.getPort() > 0) {
port = remoteServerURI.getPort();
}
}

protected void appendClientBuffer(ByteBuffer clientBuffer) {
buffers.add(clientBuffer);
stringBuilder.append(new String(clientBuffer.array(), clientBuffer.position(), clientBuffer.limit()));
}

protected static ByteBuffer newByteBuffer() {
// buffer必须大于7,保证能读到method
return ByteBuffer.allocate(128);
}

private static String parseRequestMethod(String rawContent) {
// create uri
return rawContent.split("\r\n")[0].split(" ")[0];
}

private static String parseHost(String rawContent) {
String[] headers = rawContent.split("\r\n");
String host = "host:";
for (String header : headers) {
if (header.length() > host.length()) {
String key = header.substring(0, host.length());
String value = header.substring(host.length()).trim();
if (host.equalsIgnoreCase(key)) {
if (!value.startsWith("http://") && !value.startsWith("https://")) {
value = "http://" + value;
}
return value;
}
}
}
return "";
}

}

@Slf4j
@Data
class Reactor {

private Selector selector;

private volatile boolean finish = false;

@SneakyThrows
public Reactor() {
selector = Selector.open();
}

@SneakyThrows
public Reactor pipe(SocketChannel from, SocketChannel to) {
from.configureBlocking(false);
from.register(selector, SelectionKey.OP_READ, new SocketPipe(this, from, to));
return this;
}

@SneakyThrows
public void run() {
try {
while (!finish) {
if (selector.selectNow() > 0) {
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey selectionKey = it.next();
if (selectionKey.isValid() && selectionKey.isReadable()) {
((SocketPipe) selectionKey.attachment()).pipe();
}
it.remove();
}
}
}
} finally {
close();
}
}

@SneakyThrows
public synchronized void close() {
if (finish) {
return;
}
finish = true;
if (!selector.isOpen()) {
return;
}
for (SelectionKey key : selector.keys()) {
closeChannel(key.channel());
key.cancel();
}
if (selector != null) {
selector.close();
}
}

public void cancel(SelectableChannel channel) {
SelectionKey key = channel.keyFor(selector);
if (Objects.isNull(key)) {
return;
}
key.cancel();
}

@SneakyThrows
public void closeChannel(Channel channel) {
SocketChannel socketChannel = (SocketChannel)channel;
if (socketChannel.isConnected() && socketChannel.isOpen()) {
socketChannel.shutdownOutput();
socketChannel.shutdownInput();
}
socketChannel.close();
}
}

@Data
@AllArgsConstructor
class SocketPipe {

private Reactor reactor;

private SocketChannel from;

private SocketChannel to;

@SneakyThrows
public void pipe() {
// 取消监听
clearInterestOps();

GlobalThreadPool.PIPE_EXECUTOR.submit(new Runnable() {
@SneakyThrows
@Override
public void run() {
int totalBytesRead = 0;
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (valid(from) && valid(to)) {
byteBuffer.clear();
int bytesRead = from.read(byteBuffer);
totalBytesRead = totalBytesRead + bytesRead;
byteBuffer.flip();
to.write(byteBuffer);
if (bytesRead < byteBuffer.capacity()) {
break;
}
}
if (totalBytesRead < 0) {
reactor.closeChannel(from);
reactor.cancel(from);
} else {
// 重置监听
resetInterestOps();
}
}
});
}

protected void clearInterestOps() {
from.keyFor(reactor.getSelector()).interestOps(0);
to.keyFor(reactor.getSelector()).interestOps(0);
}

protected void resetInterestOps() {
from.keyFor(reactor.getSelector()).interestOps(SelectionKey.OP_READ);
to.keyFor(reactor.getSelector()).interestOps(SelectionKey.OP_READ);
}

private boolean valid(SocketChannel channel) {
return channel.isConnected() && channel.isRegistered() && channel.isOpen();
}
}

以上,借鉴NETTY:

  1. 首先初始化REACTOR线程,然后开启代理监听,当收到代理请求时处理。
  2. 代理服务在收到代理请求时,首先做代理的预处理,然后又SocketPipe做客户端和远程服务端双向转发。
  3. 代理预处理,首先读取第一个HTTP请求,解析出METHOD, HOST, PORT。
  4. 如果是CONNECT请求,发送回应Connection Established,然后连接远程服务端,并返回SocketChannel
  5. 如果是非CONNECT请求,连接远程服务端,写入原始请求,并返回SocketChannel
  6. SocketPipe在客户端和远程服务端,做双向的转发;其本身是将客户端和服务端的SocketChannel注册到REACTOR
  7. REACTOR在监测到READABLE的CHANNEL,派发给SocketPipe做双向转发。

测试

代理的测试比较简单,指向代码后,代理服务监听8006端口,此时:

curl -x ‘localhost:8006’ httpbin.org/get测试HTTP请求

curl -x ‘localhost:8006’ httpbin.org/get测试HTTPS请…

注意,此时代理服务代理了HTTPS请求,但是并不需要-k选项,指示非安全的代理。因为代理服务本身并没有作为一个中间人,并没有解析出客户端和远程服务端通信的内容。在非透明代理时,需要解决这个问题。

2 非透明代理

非透明代理,需要解析出客户端和远程服务端传输的内容,并做相应的处理。

当传输为HTTP协议时,SocketPipe传输的数据即为明文的数据,可以拦截后直接做处理。

当传输为HTTPS协议时,SocketPipe传输的有效数据为加密数据,并不能透明处理。

另外,无论是传输的HTTP协议还是HTTPS协议,SocketPipe读到的都为非完整的数据,需要做聚批的处理。

  1. SocketPipe聚批问题,可以采用类似BufferedInputStream对InputStream做Decorate的模式来实现,相对比较简单;详细可以参考NETTY的HttpObjectAggregator;
  2. HTTPS原始请求和结果数据的加密和解密的处理,需要实现的NIO的SOCKET CHANNEL;

SslSocketChannel封装原理

考虑到目前JDK自带的NIO的SocketChannel并不支持SSL;已有的SSLSocket是阻塞的OIO。如图:

可以看出

  1. 每次入站数据和出站数据都需要 SSL SESSION 做握手;
  2. 入站数据做解密,出站数据做加密;
  3. 握手,数据加密和数据解密是统一的一套状态机;

以下,代码实现 SslSocketChannel

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
ini复制代码public class SslSocketChannel {

/**
* 握手加解密需要的四个存储
*/
protected ByteBuffer myAppData; // 明文
protected ByteBuffer myNetData; // 密文
protected ByteBuffer peerAppData; // 明文
protected ByteBuffer peerNetData; // 密文

/**
* 握手加解密过程中用到的异步执行器
*/
protected ExecutorService executor = Executors.newSingleThreadExecutor();

/**
* 原NIO 的 CHANNEL
*/
protected SocketChannel socketChannel;

/**
* SSL 引擎
*/
protected SSLEngine engine;

public SslSocketChannel(SSLContext context, SocketChannel socketChannel, boolean clientMode) throws Exception {
// 原始的NIO SOCKET
this.socketChannel = socketChannel;

// 初始化BUFFER
SSLSession dummySession = context.createSSLEngine().getSession();
myAppData = ByteBuffer.allocate(dummySession.getApplicationBufferSize());
myNetData = ByteBuffer.allocate(dummySession.getPacketBufferSize());
peerAppData = ByteBuffer.allocate(dummySession.getApplicationBufferSize());
peerNetData = ByteBuffer.allocate(dummySession.getPacketBufferSize());
dummySession.invalidate();

engine = context.createSSLEngine();
engine.setUseClientMode(clientMode);
engine.beginHandshake();
}

/**
* 参考 https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html
* 实现的 SSL 的握手协议
* @return
* @throws IOException
*/
protected boolean doHandshake() throws IOException {
SSLEngineResult result;
HandshakeStatus handshakeStatus;

int appBufferSize = engine.getSession().getApplicationBufferSize();
ByteBuffer myAppData = ByteBuffer.allocate(appBufferSize);
ByteBuffer peerAppData = ByteBuffer.allocate(appBufferSize);
myNetData.clear();
peerNetData.clear();

handshakeStatus = engine.getHandshakeStatus();
while (handshakeStatus != HandshakeStatus.FINISHED && handshakeStatus != HandshakeStatus.NOT_HANDSHAKING) {
switch (handshakeStatus) {
case NEED_UNWRAP:
if (socketChannel.read(peerNetData) < 0) {
if (engine.isInboundDone() && engine.isOutboundDone()) {
return false;
}
try {
engine.closeInbound();
} catch (SSLException e) {
log.debug("收到END OF STREAM,关闭连接.", e);
}
engine.closeOutbound();
handshakeStatus = engine.getHandshakeStatus();
break;
}
peerNetData.flip();
try {
result = engine.unwrap(peerNetData, peerAppData);
peerNetData.compact();
handshakeStatus = result.getHandshakeStatus();
} catch (SSLException sslException) {
engine.closeOutbound();
handshakeStatus = engine.getHandshakeStatus();
break;
}
switch (result.getStatus()) {
case OK:
break;
case BUFFER_OVERFLOW:
peerAppData = enlargeApplicationBuffer(engine, peerAppData);
break;
case BUFFER_UNDERFLOW:
peerNetData = handleBufferUnderflow(engine, peerNetData);
break;
case CLOSED:
if (engine.isOutboundDone()) {
return false;
} else {
engine.closeOutbound();
handshakeStatus = engine.getHandshakeStatus();
break;
}
default:
throw new IllegalStateException("无效的握手状态: " + result.getStatus());
}
break;
case NEED_WRAP:
myNetData.clear();
try {
result = engine.wrap(myAppData, myNetData);
handshakeStatus = result.getHandshakeStatus();
} catch (SSLException sslException) {
engine.closeOutbound();
handshakeStatus = engine.getHandshakeStatus();
break;
}
switch (result.getStatus()) {
case OK :
myNetData.flip();
while (myNetData.hasRemaining()) {
socketChannel.write(myNetData);
}
break;
case BUFFER_OVERFLOW:
myNetData = enlargePacketBuffer(engine, myNetData);
break;
case BUFFER_UNDERFLOW:
throw new SSLException("加密后消息内容为空,报错");
case CLOSED:
try {
myNetData.flip();
while (myNetData.hasRemaining()) {
socketChannel.write(myNetData);
}
peerNetData.clear();
} catch (Exception e) {
handshakeStatus = engine.getHandshakeStatus();
}
break;
default:
throw new IllegalStateException("无效的握手状态: " + result.getStatus());
}
break;
case NEED_TASK:
Runnable task;
while ((task = engine.getDelegatedTask()) != null) {
executor.execute(task);
}
handshakeStatus = engine.getHandshakeStatus();
break;
case FINISHED:
break;
case NOT_HANDSHAKING:
break;
default:
throw new IllegalStateException("无效的握手状态: " + handshakeStatus);
}
}

return true;
}

/**
* 参考 https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html
* 实现的 SSL 的传输读取协议
* @param consumer
* @throws IOException
*/
public void read(Consumer<ByteBuffer> consumer) throws IOException {
// BUFFER初始化
peerNetData.clear();
int bytesRead = socketChannel.read(peerNetData);
if (bytesRead > 0) {
peerNetData.flip();
while (peerNetData.hasRemaining()) {
peerAppData.clear();
SSLEngineResult result = engine.unwrap(peerNetData, peerAppData);
switch (result.getStatus()) {
case OK:
log.debug("收到远程的返回结果消息为:" + new String(peerAppData.array(), 0, peerAppData.position()));
consumer.accept(peerAppData);
peerAppData.flip();
break;
case BUFFER_OVERFLOW:
peerAppData = enlargeApplicationBuffer(engine, peerAppData);
break;
case BUFFER_UNDERFLOW:
peerNetData = handleBufferUnderflow(engine, peerNetData);
break;
case CLOSED:
log.debug("收到远程连接关闭消息.");
closeConnection();
return;
default:
throw new IllegalStateException("无效的握手状态: " + result.getStatus());
}
}
} else if (bytesRead < 0) {
log.debug("收到END OF STREAM,关闭连接.");
handleEndOfStream();
}
}

public void write(String message) throws IOException {
write(ByteBuffer.wrap(message.getBytes()));
}

/**
* 参考 https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html
* 实现的 SSL 的传输写入协议
* @param message
* @throws IOException
*/
public void write(ByteBuffer message) throws IOException {
myAppData.clear();
myAppData.put(message);
myAppData.flip();
while (myAppData.hasRemaining()) {
myNetData.clear();
SSLEngineResult result = engine.wrap(myAppData, myNetData);
switch (result.getStatus()) {
case OK:
myNetData.flip();
while (myNetData.hasRemaining()) {
socketChannel.write(myNetData);
}
log.debug("写入远程的消息为: {}", message);
break;
case BUFFER_OVERFLOW:
myNetData = enlargePacketBuffer(engine, myNetData);
break;
case BUFFER_UNDERFLOW:
throw new SSLException("加密后消息内容为空.");
case CLOSED:
closeConnection();
return;
default:
throw new IllegalStateException("无效的握手状态: " + result.getStatus());
}
}
}

/**
* 关闭连接
* @throws IOException
*/
public void closeConnection() throws IOException {
engine.closeOutbound();
doHandshake();
socketChannel.close();
executor.shutdown();
}

/**
* END OF STREAM(-1)默认是关闭连接
* @throws IOException
*/
protected void handleEndOfStream() throws IOException {
try {
engine.closeInbound();
} catch (Exception e) {
log.error("END OF STREAM 关闭失败.", e);
}
closeConnection();
}

}

以上:

  1. 基于 SSL 协议,实现统一的握手动作;
  2. 分别实现读取的解密,和写入的加密方法;
  3. 将 SslSocketChannel 实现为 SocketChannel的Decorator;

SslSocketChannel测试服务端

基于以上封装,简单测试服务端如下

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
scss复制代码@Slf4j
public class NioSslServer {

public static void main(String[] args) throws Exception {
NioSslServer sslServer = new NioSslServer("127.0.0.1", 8006);
sslServer.start();
// 使用 curl -vv -k 'https://localhost:8006' 连接
}

private SSLContext context;

private Selector selector;

public NioSslServer(String hostAddress, int port) throws Exception {
// 初始化SSL Context
context = serverSSLContext();

// 注册监听器
selector = SelectorProvider.provider().openSelector();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(hostAddress, port));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}

public void start() throws Exception {

log.debug("等待连接中.");

while (true) {
selector.select();
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
SelectionKey key = selectedKeys.next();
selectedKeys.remove();
if (!key.isValid()) {
continue;
}
if (key.isAcceptable()) {
accept(key);
} else if (key.isReadable()) {
((SslSocketChannel)key.attachment()).read(buf->{});
// 直接回应一个OK
((SslSocketChannel)key.attachment()).write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nOK\r\n\r\n");
((SslSocketChannel)key.attachment()).closeConnection();
}
}
}
}

private void accept(SelectionKey key) throws Exception {
log.debug("接收新的请求.");

SocketChannel socketChannel = ((ServerSocketChannel)key.channel()).accept();
socketChannel.configureBlocking(false);

SslSocketChannel sslSocketChannel = new SslSocketChannel(context, socketChannel, false);
if (sslSocketChannel.doHandshake()) {
socketChannel.register(selector, SelectionKey.OP_READ, sslSocketChannel);
} else {
socketChannel.close();
log.debug("握手失败,关闭连接.");
}
}
}

以上:

  1. 由于是NIO,简单的测试需要用到NIO的基础组件Selector进行测试;
  2. 首先初始化ServerSocketChannel,监听8006端口;
  3. 接收到请求后,将SocketChannel封装为SslSocketChannel,注册到Selector
  4. 接收到数据后,通过SslSocketChannel做read和write;

SslSocketChannel测试客户端

基于以上服务端封装,简单测试客户端如下

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
java复制代码@Slf4j
public class NioSslClient {

public static void main(String[] args) throws Exception {
NioSslClient sslClient = new NioSslClient("httpbin.org", 443);
sslClient.connect();
// 请求 'https://httpbin.org/get'
}

private String remoteAddress;

private int port;

private SSLEngine engine;

private SocketChannel socketChannel;

private SSLContext context;

/**
* 需要远程的HOST和PORT
* @param remoteAddress
* @param port
* @throws Exception
*/
public NioSslClient(String remoteAddress, int port) throws Exception {
this.remoteAddress = remoteAddress;
this.port = port;

context = clientSSLContext();
engine = context.createSSLEngine(remoteAddress, port);
engine.setUseClientMode(true);
}

public boolean connect() throws Exception {
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(remoteAddress, port));
while (!socketChannel.finishConnect()) {
// 通过REACTOR,不会出现等待情况
//log.debug("连接中..");
}

SslSocketChannel sslSocketChannel = new SslSocketChannel(context, socketChannel, true);
sslSocketChannel.doHandshake();

// 握手完成后,开启SELECTOR
Selector selector = SelectorProvider.provider().openSelector();
socketChannel.register(selector, SelectionKey.OP_READ, sslSocketChannel);

// 写入请求
sslSocketChannel.write("GET /get HTTP/1.1\r\n"
+ "Host: httpbin.org:443\r\n"
+ "User-Agent: curl/7.62.0\r\n"
+ "Accept: */*\r\n"
+ "\r\n");

// 读取结果
while (true) {
selector.select();
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
SelectionKey key = selectedKeys.next();
selectedKeys.remove();
if (key.isValid() && key.isReadable()) {
((SslSocketChannel)key.attachment()).read(buf->{
log.info("{}", new String(buf.array(), 0, buf.position()));
});
((SslSocketChannel)key.attachment()).closeConnection();
return true;
}
}
}
}
}

以上:

  1. 客户端的封装测试,是为了验证封装 SSL 协议双向都是OK的,
  2. 在后文的非透明上游代理中,会同时使用 SslSocketChannel做服务端和客户端
  3. 以上封装与服务端封装类似,不同的是初始化 SocketChannel,做connect而非bind

总结

以上:

  1. 非透明代理需要拿到完整的请求数据,可以通过 Decorator模式,聚批实现;
  2. 非透明代理需要拿到解密后的HTTPS请求数据,可以通过SslSocketChannel对原始的SocketChannel做封装实现;
  3. 最后,拿到请求后,做相应的处理,最终实现非透明的代理。

3 透明上游代理

透明上游代理相比透明代理要简单,区别是

  1. 透明代理需要响应 CONNECT请求,透明上游代理不需要,直接转发即可;
  2. 透明代理需要解析CONNECT请求中的HOST和PORT,并连接服务端;透明上游代理只需要连接下游代理的IP:PORT,直接转发请求即可;
  3. 透明的上游代理,只是一个简单的SocketChannel管道;确定下游的代理服务端,连接转发请求;

只需要对透明代理做以上简单的修改,即可实现透明的上游代理。

4 非透明上游代理

非透明的上游代理,相比非透明的代理要复杂一些

以上,分为四个组件:客户端,代理服务(ServerHandler),代理服务(ClientHandler),服务端

  1. 如果是HTTP的请求,数据直接通过 客户端<->ServerHandler<->ClientHandler<->服务端,代理网关只需要做简单的请求聚批,就可以应用相应的管理策略;
  2. 如果是HTTPS请求,代理作为客户端和服务端的中间人,只能拿到加密的数据;因此,代理网关需要作为HTTPS的服务方与客户端通信;然后作为HTTPS的客户端与服务端通信;
  3. 代理作为HTTPS服务方时,需要考虑到其本身是个非透明的代理,需要实现非透明代理相关的协议;
  4. 代理作为HTTPS客户端时,需要考虑到其下游是个透明的代理,真正的服务方是客户端请求的服务方;

三 设计与实现

本文需要构建的是非透明上游代理,以下采用NETTY框架给出详细的设计实现。上文将统一代理网关分为两大部分,ServerHandler和ClientHandler,以下

  1. 介绍代理网关服务端相关实现;
  2. 介绍代理网关客户端相关实现;

1 代理网关服务端

主要包括

  1. 初始化代理网关服务端
  2. 初始化服务端处理器
  3. 服务端协议升级与处理

初始化代理网关服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss复制代码    public void start() {
HookedExecutors.newSingleThreadExecutor().submit(() ->{
log.info("开始启动代理服务器,监听端口:{}", auditProxyConfig.getProxyServerPort());
EventLoopGroup bossGroup = new NioEventLoopGroup(auditProxyConfig.getBossThreadCount());
EventLoopGroup workerGroup = new NioEventLoopGroup(auditProxyConfig.getWorkThreadCount());
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ServerChannelInitializer(auditProxyConfig))
.bind(auditProxyConfig.getProxyServerPort()).sync().channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("代理服务器被中断.", e);
Thread.currentThread().interrupt();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
});
}

代理网关初始化相对简单,

  1. bossGroup线程组,负责接收请求
  2. workerGroup线程组,负责处理接收的请求数据,具体处理逻辑封装在ServerChannelInitializer中。

代理网关服务的请求处理器在 ServerChannelInitializer中定义为

1
2
3
4
5
6
7
java复制代码    @Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new HttpRequestDecoder())
.addLast(new HttpObjectAggregator(auditProxyConfig.getMaxRequestSize()))
.addLast(new ServerChannelHandler(auditProxyConfig));
}

首先解析HTTP请求,然后做聚批的处理,最后ServerChannelHandler实现代理网关协议;

代理网关协议:

  1. 判定是否是CONNECT请求,如果是,会存储CONNECT请求;暂停读取,发送代理成功的响应,并在回应成功后,升级协议;
  2. 升级引擎,本质上是采用SslSocketChannel对原SocketChannel做透明的封装;
  3. 最后根据CONNECT请求连接远程服务端;

详细实现为:

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
scss复制代码    @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
FullHttpRequest request = (FullHttpRequest)msg;

try {
if (isConnectRequest(request)) {
// CONNECT 请求,存储待处理
saveConnectRequest(ctx, request);

// 禁止读取
ctx.channel().config().setAutoRead(false);

// 发送回应
connectionEstablished(ctx, ctx.newPromise().addListener(future -> {
if (future.isSuccess()) {
// 升级
if (isSslRequest(request) && !isUpgraded(ctx)) {
upgrade(ctx);
}

// 开放消息读取
ctx.channel().config().setAutoRead(true);
ctx.read();
}
}));

} else {
// 其他请求,判定是否已升级
if (!isUpgraded(ctx)) {

// 升级引擎
upgrade(ctx);
}

// 连接远程
connectRemote(ctx, request);
}
} finally {
ctx.fireChannelRead(msg);
}
}

2 代理网关客户端

代理网关服务端需要连接远程服务,进入代理网关客户端部分。

代理网关客户端初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码    /**
* 初始化远程连接
* @param ctx
* @param httpRequest
*/
protected void connectRemote(ChannelHandlerContext ctx, FullHttpRequest httpRequest) {
Bootstrap b = new Bootstrap();
b.group(ctx.channel().eventLoop()) // use the same EventLoop
.channel(ctx.channel().getClass())
.handler(new ClientChannelInitializer(auditProxyConfig, ctx, safeCopy(httpRequest)));

// 动态连接代理
FullHttpRequest originRequest = ctx.channel().attr(CONNECT_REQUEST).get();
if (originRequest == null) {
originRequest = httpRequest;
}
ChannelFuture cf = b.connect(new InetSocketAddress(calculateHost(originRequest), calculatePort(originRequest)));
Channel cch = cf.channel();
ctx.channel().attr(CLIENT_CHANNEL).set(cch);
}

以上:

  1. 复用代理网关服务端的workerGroup线程组;
  2. 请求和结果的处理封装在ClientChannelInitializer;
  3. 连接的远程服务端的HOST和PORT在服务端收到的请求中可以解析到。

代理网关客户端的处理器的初始化逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码    @Override
protected void initChannel(SocketChannel ch) throws Exception {
SocketAddress socketAddress = calculateProxy();
if (!Objects.isNull(socketAddress)) {
ch.pipeline().addLast(new HttpProxyHandler(calculateProxy(), auditProxyConfig.getUserName(), auditProxyConfig
.getPassword()));
}
if (isSslRequest()) {
String host = host();
int port = port();
if (StringUtils.isNoneBlank(host) && port > 0) {
ch.pipeline().addLast(new SslHandler(sslEngine(host, port)));
}
}
ch.pipeline().addLast(new ClientChannelHandler(clientContext, httpRequest));
}

以上:

  1. 如果下游是代理,那么会采用HttpProxyHandler,经由下游代理与远程服务端通信;
  2. 如果当前需要升级为SSL协议,会对SocketChannel做透明的封装,实现SSL通信。
  3. 最后,ClientChannelHandler只是简单消息的转发;唯一的不同是,由于代理网关拦截了第一个请求,此时需要将拦截的请求,转发到服务端。

四 其他问题

代理网关实现可能面临的问题:

1 内存问题

代理通常面临的问题是OOM。本文在实现代理网关时保证内存中缓存时当前正在处理的HTTP/HTTPS请求体。内存使用的上限理论上为实时处理的请求数量*请求体的平均大小,HTTP/HTTPS的请求结果,直接使用堆外内存,零拷贝转发。

2 性能问题

性能问题不应提早考虑。本文使用NETTY框架实现的代理网关,内部大量使用堆外内存,零拷贝转发,避免了性能问题。

代理网关一期上线后曾面临一个长连接导致的性能问题,

  1. CLIENT和SERVER建立TCP长连接后(比如,TCP心跳检测),通常要么是CLIENT关闭TCP连接,或者是SERVER关闭;
  2. 如果双方长时间占用TCP连接资源而不关闭,就会导致SOCKET资源泄漏;现象是:CPU资源爆满,处理空闲连接;新连接无法建立;

使用IdleStateHandler定时监控空闲的TCP连接,强制关闭;解决了该问题。

五 总结

本文聚焦于统一代理网关的核心,详细介绍了代理相关的技术原理。

代理网关的管理部分,可以在ServerHandler部分维护,也可以在ClientHandler部分维护;

  1. ServerHandler可以拦截转换请求
  2. ClientHanlder可控制请求的出口

注:本文使用Netty的零拷贝;存储请求以解析处理;但并未实现对RESPONSE的处理;也就是RESPONSE是直接通过网关,此方面避免了常见的代理实现,内存泄漏OOM相关问题;

最后,本文实现代理网关后,针对代理的资源和流经代理网关的请求做了相应的控制,主要包括:

  1. 当遇到静态资源的请求时,代理网关会直接请求远程服务端,不会通过下游代理
  2. 当请求HEADER中包含地域标识时,代理网关会尽力保证请求打入指定的地域代理,经由地域代理访问远程服务端

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

1…175176177…956

开发者博客

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