首页
关于
壁纸
直播
留言
友链
统计
Search
1
《吞食天地1》金手指代码
6,579 阅读
2
《三国志英杰传》攻略
6,442 阅读
3
白嫖Emby
6,369 阅读
4
Emby客户端IOS破解
6,338 阅读
5
破解emby-server
4,471 阅读
moonjerx
game
age-of-empires
zx3
san-guo-zhi
尼尔:机械纪元
net
emby
learn-video
docker
torrent
photoshop
route
minio
git
ffmpeg
im
vue
gitlab
typecho
svn
alipay
nasm
srs
mail-server
tailscale
kkfileview
aria2
webdav
synology
redis
oray
chemical
mxsite
math
π
x-ui
digital-currency
server
nginx
baota
k8s
http
cloud
linux
shell
database
vpn
esxi
rancher
domain
k3s
ewomail
os
android
windows
ios
app-store
macos
develop
java
javascript
uniapp
nodejs
hbuildx
maven
android-studio
jetbrain
jenkins
css
mybatis
php
python
hardware
hard-disk
pc
RAM
software
pt
calibre
notion
office
language
literature
philosophy
travel
登录
Search
标签搜索
ubuntu
mysql
openwrt
zerotier
springboot
centos
openvpn
jdk
吞食天地2
synology
spring
idea
windows11
吞食天地1
transmission
google-play
Japanese
xcode
群晖
kiftd
MoonjerX
累计撰写
380
篇文章
累计收到
466
条评论
首页
栏目
moonjerx
game
age-of-empires
zx3
san-guo-zhi
尼尔:机械纪元
net
emby
learn-video
docker
torrent
photoshop
route
minio
git
ffmpeg
im
vue
gitlab
typecho
svn
alipay
nasm
srs
mail-server
tailscale
kkfileview
aria2
webdav
synology
redis
oray
chemical
mxsite
math
π
x-ui
digital-currency
server
nginx
baota
k8s
http
cloud
linux
shell
database
vpn
esxi
rancher
domain
k3s
ewomail
os
android
windows
ios
app-store
macos
develop
java
javascript
uniapp
nodejs
hbuildx
maven
android-studio
jetbrain
jenkins
css
mybatis
php
python
hardware
hard-disk
pc
RAM
software
pt
calibre
notion
office
language
literature
philosophy
travel
页面
关于
壁纸
直播
留言
友链
统计
搜索到
380
篇与
moonjerx
的结果
2022-12-13
maven学习<dependencyManagement>正确使用方法
一、介绍 Maven中的dependencyManagement元素提供了一种管理依赖版本号的方式。在dependencyManagement元素中声明所依赖的jar包的版本号等信息,那么所有子项目再次引入此依赖jar包时则无需显式的列出版本号。Maven会沿着父子层级向上寻找拥有dependencyManagement 元素的项目,然后使用它指定的版本号。二、使用步骤创建以父依赖:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.yan</groupId> <artifactId>springcloud1</artifactId> <version>1.0-SNAPSHOT</version> <modules> <module>springcloud-api</module> <module>springcloud-provider-dept--8001</module> <module>springcloud-consumer-dept-80</module> <module>springcloud-eureka-7001</module> <module>springcloud-eureka-7002</module> <module>springcolud-eureka-7003</module> <module>springcloud-provider-dept--8002</module> <module>springcloud-provider-dept--8003</module> <module>springcloud-consumer-dept-feign</module> <module>springcloud-provider-dept-hystrix-8002</module> <module>springcloud-consumer-hystrix-dashboard</module> <module>springcloud-zuul-9527</module> <module>springcloud-config-server-3344</module> </modules> <!--打包方式pom--> <packaging>pom</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <junit.version>4.12</junit.version> <lombok.version>1.16.10</lombok.version> <log4j.version>1.2.17</log4j.version> </properties> <!-- 这里进行版本管理,模块需要用到什么需要自己去导--> <dependencyManagement> <dependencies> <!-- 导入springCloud依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.SR1</version> <type>pom</type> <scope>import</scope> </dependency> <!-- springBoot--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.4.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 连接数据库--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <!-- springBoot mybatis启动器--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!-- 日志测试--> <!-- junit--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> </dependency> <!-- lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <!-- log4j--> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.2.3</version> </dependency> </dependencies> </dependencyManagement> <build> </build> </project> 这里module就是使用到的子模块,他们都收到父模块的版本控制,但是依赖需要自己去导入,properties里面就是版本的一个控制,然后dependencyManagement里面就可以放我们的父依赖了总结 如果有多个子项目都引用同一样依赖,则可以避免在每个使用的子项目里都声明一个版本号。当想升级或切换到另一个版本时,只需要在顶层父容器里更新,而不需要逐个修改子项目;另外如果某个子项目需要另外的一个版本,只需要声明version即可。dependencyManagement中定义的只是依赖的声明,并不实现引入,因此子项目需要显式的声明需要用的依赖。
2022年12月13日
109 阅读
0 评论
0 点赞
2022-12-13
docker搭建maven私有仓库
(一)引言在实际开发工作中,通常需要搭建maven私有仓库。(二)Nexus介绍 Nexus 是Maven仓库管理器,如果你使用Maven,你可以从Maven中央仓库 下载所需要的构件(artifact),但这通常不是一个好的做法,你应该在本地架设一个Maven仓库服务器,在代理远程仓库的同时维护本地仓库,以节省带宽和时间,Nexus就可以满足这样的需要。此外,他还提供了强大的仓库管理功能,构件搜索功能,它基于REST,友好的UI是一个extjs的REST客户端,它占用较少的内存,基于简单文件系统而非数据库。这些优点使其日趋成为最流行的Maven仓库管理器。(三)安装docker请参考 Docker安装_Icoolkj的博客(四)docker中安装nexus31、拉取镜像## 通过docker search nexus 命令搜索一下docker公有库在的 nexus相关的镜像 [root@icoolkj ~]# docker search nexus## 拉取nexus3镜像 [root@icoolkj /]# docker pull sonatype/nexus3 Using default tag: latest latest: Pulling from sonatype/nexus3 f70d60810c69: Pull complete 545277d80005: Pull complete 10b49635409a: Pull complete Digest: sha256:3fd7e90bcf49fb55d87d852cab854e5669ed115b09bdb25f47c45ee0797231aa 147.6MB/295.3MB Status: Downloaded newer image for sonatype/nexus3:latest docker.io/sonatype/nexus3:latest [root@icoolkj /]# 2、建立数据储存文件夹## 建立数据存放文件夹,用于docker中nexus的数据与本地物理机映射 [root@icoolkj /]# mkdir -p /usr/local/nexus3/nexus-data [root@icoolkj /]# ll /usr/local/nexus3/ 总用量 4 drwxr-xr-x 2 root root 4096 6月 3 18:06 nexus-data ## 更改权限 [root@icoolkj /]# chmod 777 /usr/local/nexus3/nexus-data/ [root@icoolkj /]# ll /usr/local/nexus3/ 总用量 4 drwxr-xr-x 2 777 root 4096 6月 3 18:06 nexus-data [root@icoolkj /]# 3、安装并运行容器## 编写启动脚本docker-nexus3-start.sh docker rm -f docker-nexus3 || true docker run --name docker-nexus3 \ -p 8081:8081 \ -v /usr/local/nexus3/nexus-data:/nexus-data \ --restart=always \ -d sonatype/nexus3 ## 参数说明 --name nexus #启动该容器的名字,可以自己更改为自己想要的名字 -p 8081:8081 #端口映射,将本地8081端口映射为容器的8081端口,第一个8081可以改成自己想要放开的端口 -v /docker/nexus/nexus-data:/nexus-data # 将容器的/nexus-data地址 代理到 本地/docker/nexus/nexus-data文件夹下 --restart=always #在重启docker时,自动重启改容器。 -d sonatype/nexus3 #即为后台运行一直sonatype/nexus34、获取容器的日志[root@icoolkj nexus3]# docker logs -f --tail 20 docker-nexus3(五)使用nexus31、浏览器访问2、配置Nexus提示输入密码,并告知你的密码储存位置Your admin user password is located in /nexus-data/admin.password on the server.因为docker中nexus3的数据储存位置与本地物理机建立了映射关系,所有在物理机上的地址应该是/usr/local/nexus3/nexus-data/admin.password登录成功后需要更改密码,更改密码需要记住(浏览器都有记住密码的功能,顺⼿点保存⾯,下次你直接登录就好了);更改密码完成之后,admin.password⽂件⾃动删除!!!## 默认仓库说明 maven-central:maven中央库,默认从https://repo1.maven.org/maven2/拉取jar maven-releases:私库发行版jar,初次安装请将Deployment policy设置为Allow redeploy maven-snapshots:私库快照(调试版本)jar maven-public:仓库分组,把上面三个仓库组合在一起对外提供服务,在本地maven基础配置settings.xml或项目pom.xml中使用## Nexus仓库类型介绍 hosted:本地仓库,通常我们会部署自己的构件到这一类型的仓库。比如公司的第二方库。 proxy:代理仓库,它们被用来代理远程的公共仓库,如maven中央仓库。 group:仓库组,用来合并多个hosted/proxy仓库,当你的项目希望在多个repository使用资源时就不需要多次引用了,只需要引用一个group即可。如图所示,代理仓库负责代理远程中央仓库,托管仓库负责本地资源,组资源库 = 代理资源库 + 托管资源库3、配置阿里云代理仓库1)、新建仓库(Create repository)Repository-->Repositories-->Create repository-->maven2(proxy)填写仓库名称——maven-aliyun,并填入仓库urlhttps://maven.aliyun.com/repository/public2)、配置仓库组(默认已有一个maven-public) 注:注意仓库顺序。maven查找依赖时会依次遍历仓库组中的仓库。## 官方文档中建议: It is recommended practice to place hosted repositories higher in the list than proxy repositories. For proxy repositories, the repository manager needs to check the remote repository which will incur more overhead than a hosted repository lookup. 希望将hosted repositories【托管资源库】的顺序放在proxy repositories【代理资源库】之前,因为一个group【组资源库】中可以涵括这些托管资源库和代理资源库。而一整个的group是作为一个public,一个接口给别人使用的。所以当查找架包的时候,如果代理资源库在前面,那就是先从远程去查找jar,而不是先从托管资源库(本地仓库)去查找是否有jar。这样访问外网的消耗比起来在本地查找,当然是将托管资源库放在代理资源库之前的优先位置了。4、创建角色创建角色(develop),并分配nx-all权限Security-->Roles-->Create注:创建角色的同时可以为当前创建的角色分配权限。5、创建用户创建用户(developer),并授予develop角色Security-->Users-->Create注:创建用户并为创建的用户挂上相应的角色。(六)maven配置文件Maven下的setting.xml文件和项目中的pom.xml文件的关系是:settting.xml文件是全局设置,而pom.xml文件是局部设置。pom.xml文件对于项目来说,是优先使用的。而pom.xml文件中如果没有配置镜像地址的话,就按照settting.xml中定义的地址去查找。修改本地maven配置文件(conf/setting.xml)servers节点下添加以下内容(username和password为刚刚在nexus3中添加的用户和其密码) <!--nexus服务器,id为组仓库name--> <servers> <server> <id>maven-public</id> <username>developer</username> <password>icoolkj</password> </server> <server> <id>maven-releases</id> <!--对应pom.xml的id=releases的仓库--> <username>developer</username> <password>icoolkj</password> </server> <server> <id>maven-snapshots</id> <!--对应pom.xml中id=snapshots的仓库--> <username>developer</username> <password>icoolkj</password> </server> </servers>mirrors节点下添加以下内容 <!--仓库组的url地址,id和name可以写组仓库name,mirrorOf的值设置为central--> <mirrors> <mirror> <id>maven-public</id> <name>maven-public</name> <!--镜像采用配置好的组的地址--> <url>http://182.92.199.85:8081/repository/maven-public/</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors>(七)项目中发布pom.xml配置实际使用中distributionManagement可以配置在parent项目中,子项目无需重复配置。上述配置全部完成后就可以在项目中使用mven clean deploy将项目的jar包上传到自己的私服上了。 <repositories> <repository> <id>maven-public</id> <name>Nexus Repository</name> <url>http://192.168.1.188:8081/repository/maven-public/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>maven-public</id> <name>Nexus Plugin Repository</name> <url>http://192.168.1.188:8081/repository/maven-public/</url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> <!--项目分发信息,在执行mvn deploy后表示要发布的位置。有了这些信息就可以把网站部署到远程服务器或者把构件jar等部署到远程仓库。 --> <distributionManagement> <repository><!--部署项目产生的构件到远程仓库需要的信息 --> <id>maven-releases</id><!-- 此处id和settings.xml的id保持一致 --> <name>Nexus Release Repository</name> <url>http://192.168.1.188:8081/repository/maven-releases/</url> </repository> <snapshotRepository><!--构件的快照部署到哪里?如果没有配置该元素,默认部署到repository元素配置的仓库,参见distributionManagement/repository元素 --> <id>maven-snapshots</id><!-- 此处id和settings.xml的id保持一致 --> <name>Nexus Snapshot Repository</name> <url>http://192.168.1.188:8081/repository/maven-snapshots/</url> </snapshotRepository> </distributionManagement>至此,nexus搭建完毕,支持本地部署依赖jar包。
2022年12月13日
195 阅读
0 评论
0 点赞
2022-12-12
java实现WebSocket服务端
WebSocket服务端配置类import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Service; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * websocket配置类 * @author daiyg * @date 2021/8/24 17:19 */ @Service @Configuration @EnableWebSocket public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }服务端代码import com.caso.common.core.utils.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; /** * @author daiyg * @date 2021/8/24 18:15 */ @Component @ServerEndpoint(value = "/websocket/{id}/{key}", subprotocols = {"protocol"}) public class WebSocketServer { private static int onlineCount = 0; private static ConcurrentHashMap<String, WebSocketServer> webSocketSet = new ConcurrentHashMap<>(); private static ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; private static Logger log = LogManager.getLogger(WebSocketServer.class); private String id = ""; private String key= ""; /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(@PathParam(value = "id") String id,@PathParam("key") String key, Session session) { this.session = session; this.id = id;//接收到发送消息的人员编号 this.key = key; if ("1".equals(id)) { log.info("非门岗用户连接!"); } else { List<String> ids = new ArrayList<>(); ids = map.get(id); if(StringUtils.isEmpty(ids)){ ids = new ArrayList<>(); } ids.add(key); map.put(id,ids); webSocketSet.put(key, this); //加入set中 } addOnlineCount(); //在线数加1 log.info("用户" + id + "连接成功!当前在线人数为" + getOnlineCount()); try { sendMessage("连接成功"); } catch (IOException e) { log.error("websocket IO异常"); } } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { //删除map中值 List<String> keys = map.get(this.id); for (int i = 0; i < keys.size(); i++) { if(this.key.equals(keys.get(i))){ keys.remove(i); i--; } } webSocketSet.remove(this.key); //从set中删除 subOnlineCount(); //在线数减1 log.info("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message) { log.info("来自客户端的消息:" + message); } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误"); try { session.close(); } catch (IOException e) { e.printStackTrace(); } error.printStackTrace(); } /** * 服务端主动推送消息 * * @param message * @throws IOException */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 发送信息给指定ID用户,如果用户不在线则返回不在线信息给自己 * * @param message * @param personKeys * @throws IOException */ public static void sendtoUser(String message, String[] personKeys) throws IOException { //获取人员集合 for (String personKey : personKeys) { //获取map中对应的链接 if (map.get(personKey) != null && StringUtils.isNotEmpty(map.get(personKey))) { //遍历给连接人发送信息 for(String s : map.get(personKey)){ webSocketSet.get(s).sendMessage(message); } } else { //如果用户不在线则返回不在线信息给自己 log.error("当前用户不在线:" + personKey); } } } /** * 发送信息给所有人 * * @param * @throws IOException */ public void sendtoAll(String message) throws IOException { for (String key : webSocketSet.keySet()) { try { webSocketSet.get(key).sendMessage(message); } catch (IOException e) { e.printStackTrace(); } } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } }
2022年12月12日
101 阅读
0 评论
0 点赞
2022-12-12
Java实现WebSocket服务
一、使用Tomcat提供的WebSocket库 Java可以使用Tomcat提供的WebSocket库接口实现WebSocket服务,代码编写也非常的简单。现在的H5联网游戏基本上都是使用WebSocket协议,基于长连接,服务器可以主动推送消息,而不是传统的网页采用客户端轮询的方式获取服务器的消息。下面给出简单使用Tomcat的WebSocket服务的基本代码结构。@ServerEndpoint("/webSocket") public class WebSocket { @OnOpen public void onOpen(Session session) throws IOException{ logger.debug("新连接"); } @OnClose public void onClose(){ logger.debug("连接关闭"); } @OnMessage public void onMessage(String message, Session session) throws IOException { logger.debug("收到消息"); } @OnError public void onError(Session session, Throwable error){ error.printStackTrace(); } }二、WebSocket协议的整个流程1. 基于TCP协议WebSocket本质是基于TCP协议的,采用Java编写WebSocket服务时可以使用NIO或者AIO实现高并发的服务。2. 握手过程客户端采用TCP协议连接服务器指定端口后,首先需要发送一条HTTP的握手协议GET /web HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 127.0.0.1:8001 Origin: http://127.0.0.1:8001 Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw== Sec-WebSocket-Version: 13请求的头里面必须包含以下内容:Connection 其值为Upgrade,表示升级协议Upgrade 其值为websocket,表示升级为WebSocket协议Sec-WebSocket-Key 客户端发送给服务器的密钥,用于标识每个客户端,其值是16位的随机base64编码。Sec-WebSocket-Version WebSocket的协议版本服务器收到这条协议验证成功后进行协议升级,并且不会关闭Socket连接,并发送给客户端响应升级握手成功的HTTP协议包。HTTP/1.1 101 Switching Protocols Content-Length: 0 Upgrade: websocket Sec-Websocket-Accept: ZEs+c+VBk8Aj01+wJGN7Y15796g= Connection: Upgrade Date: Wed, 21 Jun 2017 03:29:14 GMT 响应的协议包里面,首先是101的状态码,更换协议;其中最重要的就是Sec-WebSocket-Accept字段。其值是通过客户端的Key加上固定的"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"密钥,通过采用16位的base64编码后发送给客户端验证,如果客户端也验证成功就表示握手完成。String acc = secKey + WEBSOCK_MAGIC_TAG; MessageDigest sh1 = MessageDigest.getInstance("SHA1"); String key = Base64.getEncoder().encodeToString(sh1.digest(acc.getBytes()));3. 数据的读写握手成功后就可以进行数据发送和读取,WebSocket的数据可以是二进制或者纯文本。每次读取和发送数据需要打包成帧数据,需要按照其标准的格式进行发送或读取才能够正常的进行数据通信。上图就是帧数据的结构图,解析帧数据的代码如下,由于是摘录的部分代码,所以只能作为理解和参考,不可直接使用。protected WebSocketFrameData ParseFrame(NetPacketBuffer bytes){ bytes.mark(); WebSocketFrameData frame = new WebSocketFrameData(); int opData = bytes.readByte(); frame.UnPackOpCodeHeader(opData); // 第一步 int length = frame.UnPackMaskHeader(bytes.readByte()); // 第二步 // 读取长度 if (length == 126) { length = bytes.readShort(); } else if (length == 127){ length = (int) bytes.readInt64(); } // 数据不足,进来的是半包 if(length + 4 > bytes.remaining()){ bytes.reset(); // return null; } // 读取mask if frame.mMasked byte[] masks = new byte[4]; // 第三步 for (int i = 0; i < 4; i++) { masks[i] = (byte) bytes.readByte(); } frame.mLength = length; frame.mData = bytes.readMulitBytes(length); frame.MaskData(masks); // 第四步 return frame; }上面代码中第一步是解析出当前帧是否是最后帧mFin标记、操作码mOpCode,采用位处理,具体的实现如下。public void UnPackOpCodeHeader(int opData){ mRsv1 = (opData & 64) == 64; mRsv2 = (opData & 32) == 32; mRsv3 = (opData & 16) == 16; mFin = (opData & 128) == 128; mOpCode = (opData & 15); }第二步在读取长度前,先解析当前帧是否有采用Mask掩码加密处理,并且里面有可能包含整个帧的长度信息,具体看上面的判断代码。public int UnPackMaskHeader(int mkData){ mMasked = (mkData & 128) == 128; return (mkData & 127); // 这里返回的是长度信息 }接下来就是读取Mask内容,注意只有客户端发送给服务端时需要采用Mask对数据做处理,服务端发送给客户端时不需要做处理。最后通过Mask掩码解析出真实数据。public void MaskData(byte[] masks){ if (!mMasked or masks.length == 0) return ; for (int i = 0; i < mLength; i++) { mData[i] = (byte) (mData[i] ^ masks[i % 4]); } }以上就解析出单帧的数据,帧数据可以分为消息数据(细分为文本数据和二进制数据)、PING包、PONG包、CLOSE包、CONTINUATION包(数据未发送完成包)。而且帧数据又有mFin标记数据是否完整,否则需要将多个帧数据合成一个完整的消息数据。// 读取帧数据,可能存在多帧数据,因此需要手动拆分 WebSocketFrameData frame = ParseFrame(mCachePacket); if(frame == null){ break; // 说明数据不完整,暂不处理。 } // 不完整的帧的时候,只有第一帧会标记帧的类型 opCode = opCode == -1? frame.mOpCode: opCode; mCacheFrame.append(frame.mData, 0, frame.mLength); if(!frame.mFin) // 非完整的数据不处理。 { continue; } // 处理完整的数据 switch(opCode) { case WebSocketFrameData.OP_TEXT: case WebSocketFrameData.OP_BINARY: mCacheFrame.flip(); this.OnMessage(mCacheFrame, opCode); break; case WebSocketFrameData.OP_PING: this.OnPing(mCacheFrame); break; case WebSocketFrameData.OP_PONG: this.OnPong(mCacheFrame); break; case WebSocketFrameData.OP_CLOSE: this.OnClosed(); break; case WebSocketFrameData.OP_CONTINUATION: this.Close(); break; } opCode = -1; mCacheFrame.clear();读取整个客户端的协议数据流程就已经完成了,服务端发送回去的数据就只需要注意两点:大的数据包需要分帧数据发送。不需要采用Mask掩码加密,因此Mask位置设置为0,并且不写入掩码数据。原文摘自
2022年12月12日
144 阅读
0 评论
0 点赞
2022-12-12
别人的字节面经
如果你知道 MySQL 一行记录的存储结构,那么这个问题对你没什么难度。如果你不知道也没关系,这次我跟大家聊聊 MySQL 一行记录是怎么存储的?知道了这个之后,除了能应解锁前面这道面试题,你还会解锁这些面试题:MySQL 的 NULL 值会占用空间吗?MySQL 怎么知道 varchar(n) 实际占用数据的大小?varchar(n) 中 n 最大取值为多少?行溢出后,MySQL 是怎么处理的?这些问题看似毫不相干,其实都是在围绕「 MySQL 一行记录的存储结构」这一个知识点,所以攻破了这个知识点后,这些问题就引刃而解了。MySQL 的数据存放在哪个文件?大家都知道 MySQL 的数据都是保存在磁盘的,那具体是保存在哪个文件呢?MySQL 存储的行为是由存储引擎实现的,MySQL 支持多种存储引擎,不同的存储引擎保存的文件自然也不同。InnoDB 是我们常用的存储引擎,也是 MySQL 默认的存储引擎。所以,本文主要以 InnoDB 存储引擎展开讨论。先来看看 MySQL 数据库的文件存放在哪个目录?mysql> SHOW VARIABLES LIKE 'datadir'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | datadir | /var/lib/mysql/ | +---------------+-----------------+ 1 row in set (0.00 sec)我们每创建一个 database(数据库) 都会在 /var/lib/mysql/ 目录里面创建一个以 database 为名的目录,然后保存表结构和表数据的文件都会存放在这个目录里。比如,我这里有一个名为 my_test 的 database,该 database 里有一张名为 t_order 数据库表。然后,我们进入 /var/lib/mysql/my_test 目录,看看里面有什么文件?[root@xiaolin ~]#ls /var/lib/mysql/my_test db.opt t_order.frm t_order.ibd可以看到,共有三个文件,这三个文件分别代表着:db.opt,用来存储当前数据库的默认字符集和字符校验规则。t_order.frm ,t_order 的表结构会保存在这个文件。在 MySQL 中建立一张表都会生成一个.frm 文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。t_order.ibd,t_order 的表数据会保存在这个文件。表数据既可以存在共享表空间文件(文件名:ibdata1)里,也可以存放在独占表空间文件(文件名:表名字.idb)。这个行为是由参数 innodb_file_per_table 控制的,若设置了参数 innodb_file_per_table 为 1,则会将存储的数据、索引等信息单独存储在一个独占表空间,从 MySQL 5.6.6 版本开始,它的默认值就是 1 了,因此从这个版本之后, MySQL 中每一张表的数据都存放在一个独立的 .idb 文件。好了,现在我们知道了一张数据库表的数据是保存在「 表名字.idb 」的文件里的,这个文件也称为独占表空间文件。那这个表空间文件的结构是怎么样的?表空间由段(segment)、区(extent)、页(page)、行(row)组成 ,InnoDB存储引擎的逻辑存储结构大致如下图:下面我们从下往上一个个看看。1、行(row)数据库表中的记录都是按行(row)进行存放的,每行记录根据不同的行格式,有不同的存储结构。后面我们详细介绍 InnoDB 存储引擎的行格式,也是本文重点介绍的内容。2、页(page)记录是按照行来存储的,但是数据库的读取并不以「行」为单位,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。因此,InnoDB 的数据是按「页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存。默认每个页的大小为 16KB,也就是最多能保证 16KB 的连续存储空间。页是 InnoDB 存储引擎磁盘管理的最小单元,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。页的类型有很多,常见的有数据页、undo 日志页、溢出页等等。数据表中的行记录是用「数据页」来管理的,数据页的结构这里我就不讲细说了,之前文章有说过,感兴趣的可以去看这篇文章:换一个角度看 B+ 树总之知道表中的记录存储在「数据页」里面就行。3、区(extent)我们知道 InnoDB 存储引擎是用 B+ 树来组织数据的。B+ 树中每一层都是通过双向链表连接起来的,如果是以页为单位来分配存储空间,那么链表中相邻的两个页之间的物理位置并不是连续的,可能离得非常远,那么磁盘查询时就会有大量的随机I/O,随机 I/O 是非常慢的。解决这个问题也很简单,就是让链表中相邻的页的物理位置也相邻,这样就可以使用顺序 I/O 了,那么在范围查询(扫描叶子节点)的时候性能就会很高。那具体怎么解决呢?在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了。4、段(segment)表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。段一般分为数据段、索引段和回滚段等。索引段:存放 B + 树的非叶子节点的区的集合;数据段:存放 B + 树的叶子节点的区的集合;回滚段:存放的是回滚数据的区的集合,之前讲事务隔离的时候就介绍到了 MVCC 利用了回滚段实现了多版本查询数据。好了,终于说完表空间的结构了。接下来,就具体讲一下 InnoDB 的行格式了。之所以要绕一大圈才讲行记录的格式,主要是想让大家知道行记录是存储在哪个文件,以及行记录在这个表空间文件中的哪个区域,有一个从上往下切入的视角,这样理解起来不会觉得很抽象。InnoDB 行格式有哪些?行格式(row_format),就是一条记录的存储结构。InnoDB 提供了 4 种行格式,分别是 Redundant、Compact、Dynamic和 Compressed 行格式。Redundant 是很古老的行格式了, MySQL 5.0 版本之前用的行格式,现在基本没人用了。由于 Redundant 不是一种紧凑的行格式,所以 MySQL 5.0 之后引入了 Compact 行记录存储方式,Compact 是一种紧凑的行格式,设计的初衷就是为了让一个数据页中可以存放更多的行记录,从 MySQL 5.1 版本之后,行格式默认设置成 Compact。Dynamic 和 Compressed 两个都是紧凑的行格式,它们的行格式都和 Compact 差不多,因为都是基于 Compact 改进一点东西。从 MySQL5.7 版本之后,默认使用 Dynamic 行格式。Redundant 行格式我这里就不讲了,因为现在基本没人用了,这次重点介绍 Compact 行格式,因为 Dynamic 和 Compressed 这两个行格式跟 Compact 非常像。所以,弄懂了 Compact 行格式,之后你们在去了解其他行格式,很快也能看懂。COMPACT 行格式长什么样?先跟 Compact 行格式混个脸熟,它长这样:可以看到,一条完整的记录分为「记录的额外信息」和「记录的真实数据」两个部分。接下里,分别详细说下。记录的额外信息记录的额外信息包含 3 个部分:变长字段长度列表、NULL 值列表、记录头信息。1. 变长字段长度列表varchar(n) 和 char(n) 的区别是什么,相信大家都非常清楚,char 是定长的,varchar 是变长的,变长字段实际存储的数据的长度(大小)不固定的。所以,在存储数据的时候要把这些数据占用的字节数也存起来,存到「变长字段长度列表」里面,读取数据的时候才能根据这个「变长字段长度列表」去读取对应长度的数据。其他 TEXT、BLOB 等变长字段也是这么实现的。为了展示「变长字段长度列表」具体是怎么保存变长字段占用的字节数,我们先创建这样一张表,字符集是 ascii(所以每一个字符占用的 1 字节),行格式是 Compact,t_user 表中 name 和 phone 字段都是变长字段:CREATE TABLE `t_user` ( `id` int(11) NOT NULL, `name` VARCHAR(20) NOT NULL, `phone` VARCHAR(20) DEFAULT NULL, `age` int(11) DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;现在 t_user 表里有这三条记录:接下来,我们看看看看这三条记录的行格式中的 「变长字段长度列表」是怎样存储的。先来看第一条记录:name 列的值为 a,长度是 1 字节,十六进制 0x01phone 列的值为 123,长度是 3 字节,十六进制 0x03age 列和 id 列不是变长字段,所以这里不用管。这些变长字段的长度值会按照列的顺序逆序存放(等下会说为什么要这么设计),所以「变长字段长度列表」里的内容是「 03 01」,而不是 「01 03」。同样的道理,我们也可以得出第二条记录的行格式中,「变长字段长度列表」里的内容是「 04 02」,如下图:第三条记录中 phone 列的值是 NULL,NULL 是不会存放在行格式中记录的真实数据部分里的,所以「变长字段长度列表」里不需要保存值为 NULL 的变长字段的长度。为什么「变长字段长度列表」的信息要按照逆序存放?这个设计是有想法的,主要是因为「记录头信息」中指向下一个记录的指针,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。「变长字段长度列表」中的信息之所以要逆序存放,是因为这样可以使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。同样的道理, NULL 值列表的信息也需要逆序存放。如果你不知道什么是 CPU Cache,可以看这篇文章:面试官:如何写出让 CPU 跑得更快的代码?,这属于计算机组成的知识。每个数据库表的行格式都有「变长字段字节数列表」吗?其实变长字段字节数列表不是必须的。当数据表没有变长字段的时候,比如全部都是 int 类型的字段,这时候表里的行格式就不会有「变长字段长度列表」了,因为没必要,不如去掉以节省空间。所以「变长字段长度列表」只出现在数据表有变长字段的时候。2. NULL 值列表表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。二进制位的值为1时,代表该列的值为NULL。二进制位的值为0时,代表该列的值不为NULL。另外,NULL 值列表必须用整数个字节的位表示(1字节8位),如果使用的二进制位个数不足整数个字节,则在字节的高位补 0。还是以 t_user 表的这三条记录作为例子:接下来,我们看看看看这三条记录的行格式中的 NULL 值列表是怎样存储的。先来看第一条记录,第一条记录所有列都有值,不存在 NULL 值,所以用二进制来表示是酱紫的:但是 InnoDB 是用整数字节的二进制位来表示NULL值列表的,现在不足 8 位,所以要在高位补 0,最终用二进制来表示是酱紫的:所以,对于第一条数据,NULL 值列表用十六进制表示是 0x00。接下来看第二条记录,第二条记录 age 列是 NULL 值,所以,对于第二条数据,NULL值列表用十六进制表示是 0x04。最后第三条记录,第三条记录 phone 列 和 age 列是 NULL 值,所以,对于第三条数据,NULL 值列表用十六进制表示是 0x06。我们把三条记录的 NULL 值列表都填充完毕后,它们的行格式是这样的:每个数据库表的行格式都有「NULL 值列表」吗?NULL 值列表也不是必须的。当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了。所以在设计数据库表的时候,通常都是建议将字段设置为 NOT NULL,这样可以节省 1 字节的空间(NULL 值列表占用 1 字节空间)。3. 记录头信息记录头信息中包含的内容很多,我就不一一列举了,这里说几个比较重要的:delete_mask :标识此条数据是否被删除。从这里可以知道,我们执行 detele 删除记录的时候,并不会真正的删除记录,只是将这个记录的 delete_mask 标记为 1。next_record:下一条记录的位置。从这里可以知道,记录与记录之间是通过链表组织的。在前面我也提到了,指向的是下一条记录的「记录头信息」和「真实数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。record_type:表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录 记录的真实数据 记录真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为:row_id、trx_id、roll_pointer,我们来看下这三个字段是什么。row_id如果我们建表的时候指定了主键或者唯一约束列,那么就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一约束,那么 InnoDB 就会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。trx_id事务id,表示这个数据是由哪个事务生成的。trx_id是必需的,占用 6 个字节。roll_pointer这条记录上一个版本的指针。roll_pointer 是必需的,占用 7 个字节。如果你熟悉 MVCC 机制,你应该就清楚 trx_id 和 roll_pointer 的作用了。varchar(n) 中 n 最大取值为多少?varchar(n) 字段类型的 n 代表的是最多存储的字符数量,那 n 最大能设置多少?这个问题要考虑两个因素:行格式中「变长字段长度列表」最大能表示多少字节?知道了这个才能知道,一行数据最大能存储多少字节的数据。数据库表的字符集,确定了这个,才能知道 1 个字符占用多少字节。行格式中「变长字段长度列表」有时候是占用 1 字节,有时候是占用 2 字节:如果变长字段允许存储的最大字节数小于等于 255 字节,「变长字段长度列表」就占用 1 个字节;如果变长字段允许存储的最大字节数大于 255 字节,「变长字段长度列表」就占用 2 个字节;可以看到,「 变长字段长度列表」占用的字节数最大不会不超过 2 字节。 2 个字节的最大值是 65535(十进制),从这里可以推测一行记录最大能存储 65535 字节的数据,实际上真的是这样吗?我这里以 ascii 字符集作为例子,这意味着 1 个字符占用 1 字节。那么 varchar(65535) 就意味着最多可存储 65535 个 ascii 字符,刚好满足一行记录最大能存储 65535 字节的数据。我们定义一个 varchar(65535) 类型的字段,字符集为 ascii 的数据库表。CREATE TABLE test ( `name` VARCHAR(65535) NULL ) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;看能不能成功创建一张表:可以看到,创建失败了。从报错信息就可以知道一行数据的最大字节数是 65535(不包含 TEXT、BLOBs 这种大对象类型),其中包含了 storage overhead。问题来了,这个 storage overhead 是什么呢?其实就是变长字段长度列表和 NULL 值列表,也就是说一行数据的最大字节数 65535,其实是包含「变长字段长度列表」和 「NULL 值列表」所占用的字节数的。我们存储字段类型为 varchar(n) 的数据时,其实分成了三个部分来存储:真实数据真实数据占用的字节数NULL 标识,如果不允许为NULL,这部分不需要前面我创建表的时候,字段是允许为 NULL 的,所以会占用 1 字节来存储 NULL 标识,字段是变长字段且变长字段允许存储的最大字节数大于 255 字节 ,所以会占用 2 字节存储真实数据的占用的字节数,所以最多可以存储 65535- 2 - 1 = 65532 个字节。我们先来测试看看 varchar(65533) 是否可行?可以看到,还是不行,接下来看看 varchar(65532) 是否可行?可以看到,创建成功了。当然,我上面这个例子是针对字符集为 ascii 情况,如果采用的是 UTF-8,varchar(n) 最多能存储的数据计算方式就不一样了:在 UTF-8 字符集下,一个字符串最多需要三个字节,varchar(n) 的 n 最大取值就是 65532/3 = 21844。上面所说的只是针对于一个字段的计算方式。如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。行溢出后,MySQL 是怎么处理的?MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是 16KB,也就是 16384字节,而一个 varchar(n) 类型的列最多可以存储 65532字节,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的「溢出页」中。如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。在一般情况下,InnoDB 的数据都是存放在 「数据页」中。但是当发生行溢出时,溢出的数据会存放到「溢出页」中。当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。大致如下图所示。上面这个是 Compact 行格式在发生行溢出后的处理。Compressed 和 Dynamic 这两个行格式和 Compact 非常类似,主要的区别在于处理行溢出数据时有些区别。这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中,看起来就像下面这样:总结MySQL 的 NULL 值是怎么存放的?MySQL 的 Compact 行格式中会用「NULL值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。NULL值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL值列表,这样可节省 1 字节的空间。MySQL 怎么知道 varchar(n) 实际占用数据的大小?MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。varchar(n) 中 n 最大取值为多少?一行记录最大能存储 65535 字节的数据,但是这个是包含「变长字段字节数列表所占用的字节数」和「NULL值列表所占用的字节数」。如果一张表只有一个 varchar(n) 字段,且允许为 NULL,字符集为 ascii。varchar(n) 中 n 最大取值为 65532。计算公式:65535 - 变长字段字节数列表所占用的字节数 - NULL值列表所占用的字节数 = 65535 - 2 - 1 = 65532行溢出后,MySQL 是怎么处理的?如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。Compact 行格式针对行溢出的处理是这样的:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。
2022年12月12日
141 阅读
0 评论
0 点赞
1
...
26
27
28
...
76
您的IP: