Repository: crossoverJie/cim
Branch: master
Commit: f1f3bae6a3d5
Files: 226
Total size: 423.0 KB
Directory structure:
gitextract_k26pvt4s/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ └── bug_report.md
│ └── workflows/
│ ├── docker.yml
│ ├── maven.yml
│ └── reusable_run_tests.yml
├── .gitignore
├── CLAUDE.md
├── LICENSE
├── Makefile
├── README-zh.md
├── README.md
├── checkstyle/
│ ├── checkstyle.xml
│ └── suppressions.xml
├── cim-client/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── crossoverjie/
│ │ │ └── cim/
│ │ │ └── client/
│ │ │ ├── CIMClientApplication.java
│ │ │ ├── config/
│ │ │ │ ├── AppConfiguration.java
│ │ │ │ ├── BeanConfig.java
│ │ │ │ └── SwaggerConfig.java
│ │ │ ├── scanner/
│ │ │ │ └── Scan.java
│ │ │ ├── service/
│ │ │ │ ├── InnerCommand.java
│ │ │ │ ├── InnerCommandContext.java
│ │ │ │ ├── MsgHandle.java
│ │ │ │ ├── MsgLogger.java
│ │ │ │ ├── ShutDownSign.java
│ │ │ │ └── impl/
│ │ │ │ ├── AsyncMsgLogger.java
│ │ │ │ ├── EchoServiceImpl.java
│ │ │ │ ├── MsgCallBackListener.java
│ │ │ │ ├── MsgHandler.java
│ │ │ │ └── command/
│ │ │ │ ├── CloseAIModelCommand.java
│ │ │ │ ├── DelayMsgCommand.java
│ │ │ │ ├── EchoInfoCommand.java
│ │ │ │ ├── EmojiCommand.java
│ │ │ │ ├── OpenAIModelCommand.java
│ │ │ │ ├── PrefixSearchCommand.java
│ │ │ │ ├── PrintAllCommand.java
│ │ │ │ ├── PrintOnlineUsersCommand.java
│ │ │ │ ├── QueryHistoryCommand.java
│ │ │ │ └── ShutDownCommand.java
│ │ │ └── util/
│ │ │ └── SpringBeanFactory.java
│ │ └── resources/
│ │ ├── application.yaml
│ │ └── banner.txt
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── crossoverjie/
│ │ └── cim/
│ │ ├── client/
│ │ │ └── service/
│ │ │ ├── InnerCommandContextTest.java
│ │ │ └── impl/
│ │ │ └── AsyncMsgLoggerTest.java
│ │ └── server/
│ │ └── test/
│ │ ├── CommonTest.java
│ │ └── EchoTest.java
│ └── resources/
│ └── application.yaml
├── cim-client-sdk/
│ ├── README.md
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── crossoverjie/
│ │ └── cim/
│ │ └── client/
│ │ └── sdk/
│ │ ├── Client.java
│ │ ├── ClientBuilder.java
│ │ ├── ClientState.java
│ │ ├── Event.java
│ │ ├── FetchOfflineMsgJob.java
│ │ ├── ReConnectManager.java
│ │ ├── RouteManager.java
│ │ ├── impl/
│ │ │ ├── ClientBuilderImpl.java
│ │ │ ├── ClientConfigurationData.java
│ │ │ └── ClientImpl.java
│ │ └── io/
│ │ ├── CIMClientHandle.java
│ │ ├── CIMClientHandleInitializer.java
│ │ ├── MessageListener.java
│ │ ├── ReconnectCheck.java
│ │ └── backoff/
│ │ ├── BackoffStrategy.java
│ │ └── RandomBackoff.java
│ └── test/
│ ├── java/
│ │ └── com/
│ │ └── crossoverjie/
│ │ └── cim/
│ │ └── client/
│ │ └── sdk/
│ │ ├── ClientTest.java
│ │ └── OfflineMsgTest.java
│ └── resources/
│ ├── application-route.yaml
│ └── init.sql
├── cim-common/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── crossoverjie/
│ │ │ └── cim/
│ │ │ └── common/
│ │ │ ├── constant/
│ │ │ │ └── Constants.java
│ │ │ ├── core/
│ │ │ │ └── proxy/
│ │ │ │ ├── DynamicUrl.java
│ │ │ │ ├── Request.java
│ │ │ │ └── RpcProxyManager.java
│ │ │ ├── data/
│ │ │ │ └── construct/
│ │ │ │ ├── RingBufferWheel.java
│ │ │ │ ├── SortArrayMap.java
│ │ │ │ └── TrieTree.java
│ │ │ ├── enums/
│ │ │ │ ├── StatusEnum.java
│ │ │ │ └── SystemCommandEnum.java
│ │ │ ├── exception/
│ │ │ │ ├── CIMException.java
│ │ │ │ └── GenericException.java
│ │ │ ├── kit/
│ │ │ │ └── HeartBeatHandler.java
│ │ │ ├── metastore/
│ │ │ │ ├── AbstractConfiguration.java
│ │ │ │ ├── MetaStore.java
│ │ │ │ ├── ZkConfiguration.java
│ │ │ │ └── ZkMetaStoreImpl.java
│ │ │ ├── pojo/
│ │ │ │ ├── CIMUserInfo.java
│ │ │ │ └── RouteInfo.java
│ │ │ ├── req/
│ │ │ │ └── BaseRequest.java
│ │ │ ├── res/
│ │ │ │ ├── BaseResponse.java
│ │ │ │ └── NULLBody.java
│ │ │ ├── route/
│ │ │ │ └── algorithm/
│ │ │ │ ├── RouteHandle.java
│ │ │ │ ├── consistenthash/
│ │ │ │ │ ├── AbstractConsistentHash.java
│ │ │ │ │ ├── ConsistentHashHandle.java
│ │ │ │ │ ├── SortArrayMapConsistentHash.java
│ │ │ │ │ └── TreeMapConsistentHash.java
│ │ │ │ ├── loop/
│ │ │ │ │ └── LoopHandle.java
│ │ │ │ └── random/
│ │ │ │ └── RandomHandle.java
│ │ │ └── util/
│ │ │ ├── HttpClient.java
│ │ │ ├── NettyAttrUtil.java
│ │ │ ├── RouteInfoParseUtil.java
│ │ │ ├── SnowflakeIdWorker.java
│ │ │ └── StringUtil.java
│ │ ├── proto/
│ │ │ └── cim.proto
│ │ └── resources/
│ │ └── log4j.properties
│ └── test/
│ └── java/
│ └── com/
│ └── crossoverjie/
│ └── cim/
│ └── common/
│ ├── CommonTest.java
│ ├── core/
│ │ └── proxy/
│ │ └── RpcProxyManagerTest.java
│ ├── data/
│ │ └── construct/
│ │ ├── RingBufferWheelTest.java
│ │ ├── ScheduledTest.java
│ │ ├── SortArrayMapTest.java
│ │ ├── TimerTest.java
│ │ └── TrieTreeTest.java
│ ├── enums/
│ │ └── SystemCommandEnumTypeTest.java
│ ├── metastore/
│ │ └── MetaStoreTest.java
│ ├── route/
│ │ └── algorithm/
│ │ ├── consistenthash/
│ │ │ ├── ConsistentHashHandleTest.java
│ │ │ ├── RangeCheckTestUtil.java
│ │ │ ├── SortArrayMapConsistentHashTest.java
│ │ │ └── TreeMapConsistentHashTest.java
│ │ ├── loop/
│ │ │ └── LoopHandleTest.java
│ │ └── random/
│ │ └── RandomHandleTest.java
│ └── util/
│ ├── HttpClientTest.java
│ └── ProtocolTest.java
├── cim-forward-route/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── crossoverjie/
│ │ │ └── cim/
│ │ │ └── route/
│ │ │ ├── RouteApplication.java
│ │ │ ├── config/
│ │ │ │ ├── AppConfiguration.java
│ │ │ │ ├── BeanConfig.java
│ │ │ │ ├── MySqlPersistenceConfig.java
│ │ │ │ ├── OfflineMsgStoreConfig.java
│ │ │ │ └── SwaggerConfig.java
│ │ │ ├── constant/
│ │ │ │ └── Constant.java
│ │ │ ├── controller/
│ │ │ │ └── RouteController.java
│ │ │ ├── exception/
│ │ │ │ └── ExceptionHandlingController.java
│ │ │ ├── factory/
│ │ │ │ └── OfflineMsgFactory.java
│ │ │ ├── kit/
│ │ │ │ └── NetAddressIsReachable.java
│ │ │ ├── service/
│ │ │ │ ├── AccountService.java
│ │ │ │ ├── CommonBizService.java
│ │ │ │ ├── OfflineMsgService.java
│ │ │ │ ├── UserInfoCacheService.java
│ │ │ │ └── impl/
│ │ │ │ ├── AccountServiceRedisImpl.java
│ │ │ │ ├── OfflineMsgServiceImpl.java
│ │ │ │ └── UserInfoCacheServiceImpl.java
│ │ │ └── util/
│ │ │ └── SpringBeanFactory.java
│ │ └── resources/
│ │ ├── application.yaml
│ │ ├── banner.txt
│ │ └── lua/
│ │ └── offLine.lua
│ └── test/
│ ├── java/
│ │ ├── CommonTest.java
│ │ └── com/
│ │ └── crossoverjie/
│ │ └── cim/
│ │ └── route/
│ │ └── service/
│ │ └── impl/
│ │ ├── AbstractBaseTest.java
│ │ ├── AccountServiceRedisImplTest.java
│ │ ├── RedisTest.java
│ │ └── UserInfoCacheServiceImplTest.java
│ └── resources/
│ ├── application.yaml
│ └── init.sql
├── cim-integration-test/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── crossoverjie/
│ │ └── cim/
│ │ └── client/
│ │ └── sdk/
│ │ ├── route/
│ │ │ ├── AbstractRouteBaseTest.java
│ │ │ └── OfflineMsgStoreRouteBaseTest.java
│ │ └── server/
│ │ └── AbstractServerBaseTest.java
│ └── test/
│ └── resources/
│ ├── application-client.yaml
│ └── application-route.yaml
├── cim-persistence/
│ ├── cim-persistence-api/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ └── java/
│ │ └── com/
│ │ └── crossoverjie/
│ │ └── cim/
│ │ └── persistence/
│ │ └── api/
│ │ ├── config/
│ │ │ └── BeanConfig.java
│ │ ├── pojo/
│ │ │ ├── OfflineMsg.java
│ │ │ └── OfflineMsgLastSendRecord.java
│ │ ├── service/
│ │ │ └── OfflineMsgStore.java
│ │ └── vo/
│ │ └── req/
│ │ └── SaveOfflineMsgReqVO.java
│ ├── cim-persistence-mysql/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── crossoverjie/
│ │ │ └── cim/
│ │ │ └── persistence/
│ │ │ └── mysql/
│ │ │ ├── config/
│ │ │ │ └── MyBatisConfig.java
│ │ │ ├── offlinemsg/
│ │ │ │ ├── OfflineMsgDb.java
│ │ │ │ └── mapper/
│ │ │ │ ├── OfflineMsgLastSendRecordMapper.java
│ │ │ │ └── OfflineMsgMapper.java
│ │ │ └── util/
│ │ │ └── MapToJsonTypeHandler.java
│ │ └── resources/
│ │ └── mapper/
│ │ ├── OfflineMsgLastSendRecordMapper.xml
│ │ └── OfflineMsgMapper.xml
│ ├── cim-persistence-redis/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── crossoverjie/
│ │ │ └── cim/
│ │ │ └── persistence/
│ │ │ └── redis/
│ │ │ ├── OfflineMsgBuffer.java
│ │ │ ├── constant/
│ │ │ │ └── Constant.java
│ │ │ └── kit/
│ │ │ └── OfflineMsgScriptExecutor.java
│ │ └── resources/
│ │ └── lua/
│ │ ├── deleteOfflineMsg.lua
│ │ ├── fetchOfflineMsg.lua
│ │ └── saveOfflineMsg.lua
│ └── pom.xml
├── cim-rout-api/
│ ├── pom.xml
│ └── src/
│ └── main/
│ └── java/
│ └── com/
│ └── crossoverjie/
│ └── cim/
│ └── route/
│ └── api/
│ ├── RouteApi.java
│ └── vo/
│ ├── req/
│ │ ├── ChatReqVO.java
│ │ ├── LoginReqVO.java
│ │ ├── OfflineMsgReqVO.java
│ │ ├── P2PReqVO.java
│ │ ├── RegisterInfoReqVO.java
│ │ └── SendMsgReqVO.java
│ └── res/
│ ├── CIMServerResVO.java
│ ├── RegisterInfoResVO.java
│ └── SendMsgResVO.java
├── cim-server/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── crossoverjie/
│ │ │ └── cim/
│ │ │ └── server/
│ │ │ ├── CIMServerApplication.java
│ │ │ ├── config/
│ │ │ │ ├── AppConfiguration.java
│ │ │ │ ├── BeanConfig.java
│ │ │ │ └── SwaggerConfig.java
│ │ │ ├── controller/
│ │ │ │ └── IndexController.java
│ │ │ ├── handle/
│ │ │ │ └── CIMServerHandle.java
│ │ │ ├── init/
│ │ │ │ └── CIMServerInitializer.java
│ │ │ ├── kit/
│ │ │ │ ├── RegistryMetaStore.java
│ │ │ │ ├── RouteHandler.java
│ │ │ │ └── ServerHeartBeatHandlerImpl.java
│ │ │ ├── server/
│ │ │ │ └── CIMServer.java
│ │ │ └── util/
│ │ │ ├── SessionSocketHolder.java
│ │ │ └── SpringBeanFactory.java
│ │ └── resources/
│ │ ├── application.yaml
│ │ └── banner.txt
│ └── test/
│ └── com/
│ └── crossoverjie/
│ └── cim/
│ └── server/
│ └── util/
│ └── NettyAttrUtilTest.java
├── cim-server-api/
│ ├── pom.xml
│ └── src/
│ └── main/
│ └── java/
│ └── com/
│ └── crossoverjie/
│ └── cim/
│ └── server/
│ └── api/
│ ├── ServerApi.java
│ └── vo/
│ ├── req/
│ │ └── SendMsgReqVO.java
│ └── res/
│ ├── OfflineMsgResVO.java
│ ├── SaveOfflineMsgResVO.java
│ └── SendMsgResVO.java
├── doc/
│ └── QA.md
├── docker/
│ ├── README.md
│ ├── allin1-ubuntu.Dockerfile
│ ├── client-ubuntu.Dockerfile
│ ├── supervisord.conf
│ └── wait-for-it.sh
├── pom.xml
├── script/
│ ├── build.sh
│ ├── deploy.sh
│ ├── route-startup.sh
│ └── server-startup.sh
└── sql/
├── 01schema.sql
├── offline_msg.sql
└── offline_msg_last_send_record.sql
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: 'Please check if there are similar issues before submitting'
labels: ''
assignees: ''
---
**Bug Description**
A clear and concise description of what the bug is.
**Reproduce**
step:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected results**
A clear and concise description of what you expected to happen.
**Screenshot**
If applicable, add screenshots to help explain your problem.
**Additional Information**
Add any other context about the problem here.
================================================
FILE: .github/workflows/docker.yml
================================================
name: Multi-Platform Docker Build
on:
push:
tags: ['image-*']
workflow_dispatch: # keep this to allow manual triggering of the workflow
jobs:
build-multi-arch:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # multipass needs this permission to authenticate with the registry
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
platforms: linux/amd64,linux/arm64,linux/arm/v7 # set the platforms you want to build for
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Multi-Arch Image
uses: docker/build-push-action@v5
with:
context: .
file: docker/allin1-ubuntu.Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
ghcr.io/crossoverjie/allin1-ubuntu:latest
ghcr.io/crossoverjie/allin1-ubuntu:${{ github.run_id }}
cache-from: type=gha
cache-to: type=gha,mode=max
================================================
FILE: .github/workflows/maven.yml
================================================
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Java CI with Maven
on:
push:
branches: [ "master" ]
pull_request:
# Run on all pull requests regardless of target branch to support stacked PRs
branches: [ "**" ]
jobs:
build:
uses: ./.github/workflows/reusable_run_tests.yml
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
================================================
FILE: .github/workflows/reusable_run_tests.yml
================================================
name: A reusable workflow to build and run the unit test suite
on:
workflow_call:
secrets:
codecov_token:
required: true
workflow_dispatch:
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Run Checkstyle
run: mvn checkstyle:check --file pom.xml
continue-on-error: true
- name: Build with Maven
run: mvn -B package --file pom.xml
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
================================================
FILE: .gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Maven template
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
!/.mvn/wrapper/maven-wrapper.jar
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/tasks.xml
.idea/**/dictionaries
.idea/**/shelf
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
# CMake
cmake-build-debug/
cmake-build-release/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
.idea/
*.iml
# Eclipse Project
bin
*.project
*.settings/
*.classpath
*.factorypath
.vscode/
/.metadata/
================================================
FILE: CLAUDE.md
================================================
# CIM Project Guide
CIM (Cross-platform Instant Messaging) is a Java-based instant messaging framework.
## Requirements
- **Minimum JDK Version**: JDK 17
- **Build Tool**: Maven
- **Spring Boot Version**: 3.3.0
## Environment Setup
Before running compile or test commands, set the correct JDK version:
```bash
export JAVA_HOME=$JAVA_17_HOME
```
`JAVA_17_HOME` is defined in `~/.zshrc`.
## Common Commands
### Compile the project
```bash
mvn clean compile
```
### Run tests
```bash
mvn test
```
### Package the project
```bash
mvn clean package -DskipTests
```
### Full build (with tests)
```bash
mvn clean install
```
## Notes
- Checkstyle code style checks run automatically during Maven's `validate` phase
- Ensure code passes Checkstyle checks before committing
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 crossoverJie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
IMAGE_NAME = allin1-ubuntu
REGISTRY = ghcr.io
OWNER = $(shell git config --get remote.origin.url | sed -e 's/.*github.com[:/]\([^/]*\)\/.*/\1/')
TAG_PREFIX = image
# make tag VERSION=v1.0
tag:
$(if $(VERSION),,$(error VERSION variable not set. Usage: make tag VERSION=x.y.z))
git tag -f $(TAG_PREFIX)-$(VERSION)
git push origin $(TAG_PREFIX)-$(VERSION)
# list all tags with the prefix
list-tags:
git tag -l "$(TAG_PREFIX)-*" | sort -V
# get the latest tag
get-latest:
@git describe --abbrev=0 --tags --match="$(TAG_PREFIX)-*" 2>/dev/null | sed 's/$(TAG_PREFIX)-//' || echo "No tags found"
# make version TYPE=major|minor|patch
version:
$(if $(TYPE),,$(error TYPE variable not set. Options: major, minor, patch))
$(eval CURRENT := $(shell make get-latest))
$(if $(CURRENT),,$(error No existing tags found. Create first tag with: make tag VERSION=1.0.0))
$(eval MAJOR := $(shell echo $(CURRENT) | cut -d. -f1))
$(eval MINOR := $(shell echo $(CURRENT) | cut -d. -f2))
$(eval PATCH := $(shell echo $(CURRENT) | cut -d. -f3))
$(if $(filter $(TYPE),major),$(eval NEW_VERSION := $(($(MAJOR)+1)).0.0))
$(if $(filter $(TYPE),minor),$(eval NEW_VERSION := $(MAJOR).$(($(MINOR)+1)).0))
$(if $(filter $(TYPE),patch),$(eval NEW_VERSION := $(MAJOR).$(MINOR).$(($(PATCH)+1))))
make tag VERSION=$(NEW_VERSION)
.PHONY: tag list-tags get-latest version
================================================
FILE: README-zh.md
================================================
[](https://codecov.io/gh/crossoverJie/cim)
[](https://github.com/crossoverJie/cim)
[](https://juejin.im/post/5c2bffdc51882509181395d7)
📘[介绍](#介绍) |📽[视频演示](#视频演示) | 🏖[TODO LIST](#todo-list) | 🌈[系统架构](#系统架构) |💡[流程图](#流程图)|🌁[快速启动](#快速启动)|👨🏻✈️[内置命令](#客户端内置命令)|🎤[通信](#群聊私聊)|❓[QA](https://github.com/crossoverJie/cim/blob/master/doc/QA.md)|💌[联系作者](#联系作者)
[English](README.md)
# V2.0
- [x] 升级至 JDK17 & springboot3.0
- [x] Client SDK
- [ ] 客户端使用 [picocli](https://picocli.info/) 替代 springboot
- [x] 支持集成测试
- [ ] 集成 OpenTelemetry
- [ ] 支持单节点启动(不依赖外部组件)
- [ ] 第三方组件支持替换(Redis/Zookeeper 等)
- [ ] 支持 Web 客户端(websocket)
- [x] 支持 Docker 容器
- [ ] 支持 Kubernetes 部署
- [ ] 支持二进制客户端(使用 golang 构建)
## 介绍
`CIM(CROSS-IM)` 是面向开发者的 `IM(即时通讯)` 系统;同时提供了一些组件帮助开发者构建自己可扩展的 `IM`。
借助 `CIM` 你可以实现以下需求:
- `IM` 即时通讯系统。
- `APP` 消息推送中间件。
- `IOT` 海量连接场景中的消息中间件。
> 如果在使用或开发过程中有任何问题,可以[联系作者](#联系作者)。
## 视频演示
> 点击下方链接可以查看视频版 Demo。
| YouTube | Bilibili|
| :------:| :------: |
| [群聊](https://youtu.be/_9a4lIkQ5_o) [私聊](https://youtu.be/kfEfQFPLBTQ) | [群聊](https://www.bilibili.com/video/av39405501) [私聊](https://www.bilibili.com/video/av39405821) |
|
|

## TODO LIST
* [x] [群聊](#群聊)
* [x] [私聊](#私聊)
* [x] [内置命令](#客户端内置命令)
* [x] [聊天记录查询](#聊天记录查询)
* [x] [一键开启 AI 模式](#ai-模式)
* [x] 使用 `Google Protocol Buffer` 高效编解码
* [x] 根据实际情况灵活的水平扩容、缩容
* [x] 服务端自动剔除离线客户端
* [x] 客户端自动重连
* [x] [延时消息](#延时消息)
* [x] SDK 开发包
* [ ] 分组群聊
* [ ] 离线消息
* [ ] 消息加密
## 系统架构

- `CIM` 中的各个组件均采用 `SpringBoot` 构建
- 客户端基于 [cim-client-sdk](https://github.com/crossoverJie/cim/tree/master/cim-client-sdk) 构建
- 采用 `Netty` 构建底层通信
- `MetaStore` 用于 `IM-server` 服务的注册与发现
### cim-server
IM 服务端,用于接收客户端连接、消息转发、消息推送等功能。
支持集群部署。
### cim-route
路由服务器;用于处理消息路由、消息转发、用户登录、用户下线以及一些运维工具(获取在线用户数等)。
### cim-client
IM 客户端终端,一个命令即可启动并与其他人进行通信(群聊、私聊)。
## 流程图

- Server 注册到 `MetaStore`
- Route 订阅 `MetaStore`
- Client 登录到 Route
- Route 从 `MetaStore` 获取 Server 信息
- Client 与 Server 建立连接
- Client1 发送消息到 Route
- Route 选择 Server 并将消息转发给 Server
- Server 将消息推送给 Client2
## 快速启动
### Docker
`allin1` 镜像内置了 Zookeeper、Redis、cim-server、cim-forward-route 四个服务,使用 [Supervisor](http://supervisord.org/) 统一管理,开箱即用。
**支持平台:** linux/amd64, linux/arm64, linux/arm/v7
**端口说明:**
| 端口 | 服务 | 说明 |
|------|---------|-------------|
| 2181 | Zookeeper | 服务注册与发现 |
| 6379 | Redis | 数据缓存 |
| 8083 | Route Server | HTTP API 路由服务 |
拉取镜像并启动:
```shell
docker pull ghcr.io/crossoverjie/allin1-ubuntu:latest
docker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 ghcr.io/crossoverjie/allin1-ubuntu:latest
```
容器启动后,可参考下方 [注册账号](#注册账号) 和 [启动客户端](#启动客户端) 章节快速体验完整的 IM 流程。
### 本地构建 Docker 镜像
如果需要从源码构建镜像:
```shell
# 在项目根目录执行
docker build -t cim-allin1:latest -f docker/allin1-ubuntu.Dockerfile .
docker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 cim-allin1:latest
```
### 本地编译
首先需要安装 `Zookeeper`、`Redis` 并保证网络通畅。
```shell
docker run --rm --name zookeeper -d -p 2181:2181 zookeeper:3.9.2
docker run --rm --name redis -d -p 6379:6379 redis:7.4.0
```
```shell
git clone https://github.com/crossoverJie/cim.git
cd cim
mvn clean install -DskipTests=true
cd cim-server && cim-client && cim-forward-route
mvn clean package spring-boot:repackage -DskipTests=true
```
### 部署 IM-server(cim-server)
```shell
cp /cim/cim-server/target/cim-server-1.0.0-SNAPSHOT.jar /xx/work/server0/
cd /xx/work/server0/
nohup java -jar /root/work/server0/cim-server-1.0.0-SNAPSHOT.jar --cim.server.port=9000 --app.zk.addr=zk地址 > /root/work/server0/log.file 2>&1 &
```
> cim-server 集群部署同理,只要保证 Zookeeper 地址相同即可。
### 部署路由服务器(cim-forward-route)
```shell
cp /cim/cim-server/cim-forward-route/target/cim-forward-route-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
nohup java -jar /root/work/route0/cim-forward-route-1.0.0-SNAPSHOT.jar --app.zk.addr=zk地址 --spring.redis.host=redis地址 --spring.redis.port=6379 > /root/work/route/log.file 2>&1 &
```
> cim-forward-route 本身就是无状态,可以部署多台;使用 Nginx 代理即可。
### 启动客户端
```shell
cp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
java -jar cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id=唯一客户端ID --cim.user.userName=用户名 --cim.route.url=http://路由服务器:8083/
```


如上图,启动两个客户端可以互相通信即可。
### 本地启动客户端
#### 注册账号
```shell
curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
"reqNo": "1234567890",
"timeStamp": 0,
"userName": "zhangsan"
}' 'http://路由服务器:8083/registerAccount'
```
从返回结果中获取 `userId`
```json
{
"code":"9000",
"message":"成功",
"reqNo":null,
"dataBody":{
"userId":1547028929407,
"userName":"test"
}
}
```
#### 启动本地客户端
```shell
# 启动本地客户端
cp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
java -jar cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id=上方返回的userId --cim.user.userName=用户名 --cim.route.url=http://路由服务器:8083/
```
## 客户端内置命令
| 命令 | 描述|
| ------ | ------ |
| `:q!` | 退出客户端|
| `:olu` | 获取所有在线用户信息 |
| `:all` | 获取所有命令 |
| `:q [option]` | 【:q 关键字】查询聊天记录 |
| `:ai` | 开启 AI 模式 |
| `:qai` | 关闭 AI 模式 |
| `:pu` | 模糊匹配用户 |
| `:info` | 获取客户端信息 |
| `:emoji [option]` | 查询表情包 [option:页码] |
| `:delay [msg] [delayTime]` | 发送延时消息 |
| `:` | 更多命令正在开发中。。 |

### 聊天记录查询

使用命令 `:q 关键字` 即可查询与个人相关的聊天记录。
> 客户端聊天记录默认存放在 `/opt/logs/cim/`,所以需要这个目录的写入权限。也可在启动命令中加入 `--cim.msg.logger.path = /自定义` 参数自定义目录。
### AI 模式

使用命令 `:ai` 开启 AI 模式,之后所有的消息都会由 `AI` 响应。
`:qai` 退出 AI 模式。
### 前缀匹配用户名

使用命令 `:qu prefix` 可以按照前缀的方式搜索用户信息。
> 该功能主要用于在移动端中的输入框中搜索用户。
### 群聊/私聊
#### 群聊



群聊只需要在控制台里输入消息回车后即可发送,同时所有在线客户端都可收到消息。
#### 私聊
私聊首先需要知道对方的 `userID` 才能进行。
输入命令 `:olu` 可列出所有在线用户。

接着使用 `userId;;消息内容` 的格式即可发送私聊消息。




同时另一个账号收不到消息。

### emoji 表情支持
使用命令 `:emoji 1` 查询出所有表情列表,使用表情别名即可发送表情。


### 延时消息
发送 10s 的延时消息:
```shell
:delay delayMsg 10
```

## 联系作者
## 贡献指南
欢迎贡献代码!提交 PR 前,请确保代码通过 Checkstyle 检查。
### 代码风格
本项目使用 [Checkstyle](https://checkstyle.org/) 来规范代码风格,规则定义在 `checkstyle/checkstyle.xml` 中。
**提交前在本地运行 Checkstyle:**
```shell
mvn checkstyle:check
```
**主要规则:**
- `{`、`}` 和运算符前后使用空格
- 行尾不能有空格
- 文件必须以换行符结尾
- 删除未使用的 import
- 常量(`static final`)必须使用 `UPPER_SNAKE_CASE` 命名
- 使用 Java 风格的数组声明:`String[] args`(而非 `String args[]`)
**快速构建时跳过 Checkstyle:**
```shell
mvn package -Dcheckstyle.skip=true
```
最近开通了知识星球,感谢大家对 CIM 的支持,为大家提供 100 份 10 元优惠券,也就是 69-10=59 元,具体福利大家可以扫码参考再决定是否加入。
> PS: 后续会在星球开始 V2.0 版本的重构,感兴趣的可以加入星球当面催更(当然代码依然会开源)。
- [crossoverJie@gmail.com](mailto:crossoverJie@gmail.com)
- 微信公众号

================================================
FILE: README.md
================================================
[](https://codecov.io/gh/crossoverJie/cim)
[](https://github.com/crossoverJie/cim)
[](https://juejin.im/post/5c2bffdc51882509181395d7)
📘[Introduction](#introduction) |📽[Video Demo](#video-demo) | 🏖[TODO LIST](#todo-list) | 🌈[Architecture](#architecture) |💡[Flow Chart](#flow-chart)|🌁[Quick Start](#quick-start)|👨🏻✈️[Built-in Commands](#built-in-commands)|🎤[Chat](#group-chatprivate-chat)|❓[QA](https://github.com/crossoverJie/cim/blob/master/doc/QA.md)|💌[Contact](#contact)
[中文文档](README-zh.md)
# V2.0
- [x] Upgrade to JDK17 & springboot3.0
- [x] Client SDK
- [ ] Client use [picocli](https://picocli.info/) instead of springboot.
- [x] Support integration testing.
- [ ] Integrate OpenTelemetry .
- [ ] Support single node startup(Contains no components).
- [ ] Third-party components support replacement(Redis/Zookeeper, etc.).
- [ ] Support web client(websocket).
- [x] Support docker container.
- [ ] Support kubernetes operation.
- [ ] Supports binary client(build with golang).
## Introduction
`CIM(CROSS-IM)` is an `IM (instant messaging)` system for developers; it also provides some components to help developers build their own scalable `IM`.
Using `CIM`, you can achieve the following requirements:
- `IM` instant messaging system.
- Message push middleware for `APP`.
- Message middleware for `IOT` massive connection scenarios.
> If you have any questions during use or development, you can [contact the author](#contact).
## Video Demo
> Click the links below to watch the video demo.
| YouTube | Bilibili|
| :------:| :------: |
| [Group Chat](https://youtu.be/_9a4lIkQ5_o) [Private Chat](https://youtu.be/kfEfQFPLBTQ) | [Group Chat](https://www.bilibili.com/video/av39405501) [Private Chat](https://www.bilibili.com/video/av39405821) |
|
|

## TODO LIST
* [x] [Group Chat](#group-chat)
* [x] [Private Chat](#private-chat)
* [x] [Built-in Commands](#built-in-commands)
* [x] [Chat History Query](#chat-history-query)
* [x] [AI Mode](#ai-mode)
* [x] Efficient encoding/decoding with `Google Protocol Buffer`
* [x] Flexible horizontal scaling based on actual needs
* [x] Server-side automatic removal of offline clients
* [x] Client automatic reconnection
* [x] [Delayed Messages](#delayed-messages)
* [x] SDK development package
* [ ] Group categorization
* [ ] Offline messages
* [ ] Message encryption
## Architecture

- Each component in `CIM` is built using `SpringBoot`
- Client build with [cim-client-sdk](https://github.com/crossoverJie/cim/tree/master/cim-client-sdk)
- Use `Netty` to build the underlying communication.
- `MetaStore` is used for registration and discovery of `IM-server` services.
### cim-server
IM server is used to receive client connections, message forwarding, message push, etc.
Support cluster deployment.
### cim-route
Route server; used to process message routing, message forwarding, user login, user offline, and some operation tools (get the number of online users, etc.).
### cim-client
IM client terminal, a command can be started and initiated to communicate with others (group chat, private chat).
## Flow Chart

- Server register to `MetaStore`
- Route subscribe `MetaStore`
- Client login to Route
- Route get Server info from `MetaStore`
- Client open connection to Server
- Client1 send message to Route
- Route select Server and forward message to Server
- Server push message to Client2
## Quick Start
### Docker
The `allin1` image comes with Zookeeper, Redis, cim-server, and cim-forward-route pre-installed, all managed by [Supervisor](http://supervisord.org/) for an out-of-the-box experience.
**Supported platforms:** linux/amd64, linux/arm64, linux/arm/v7
**Port mapping:**
| Port | Service | Description |
|------|---------|-------------|
| 2181 | Zookeeper | Service registration & discovery |
| 6379 | Redis | Data caching |
| 8083 | Route Server | HTTP API routing service |
Pull the image and start the container:
```shell
docker pull ghcr.io/crossoverjie/allin1-ubuntu:latest
docker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 ghcr.io/crossoverjie/allin1-ubuntu:latest
```
After the container starts, refer to the [Register Account](#register-account) and [Start Client](#start-client) sections below to experience the full IM workflow.
### Build Docker Image Locally
To build the Docker image from source:
```shell
# Run from the project root directory
docker build -t cim-allin1:latest -f docker/allin1-ubuntu.Dockerfile .
docker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 cim-allin1:latest
```
### Build from Source
First, install `Zookeeper` and `Redis` and ensure the network is accessible.
```shell
docker run --rm --name zookeeper -d -p 2181:2181 zookeeper:3.9.2
docker run --rm --name redis -d -p 6379:6379 redis:7.4.0
```
```shell
git clone https://github.com/crossoverJie/cim.git
cd cim
mvn clean install -DskipTests=true
cd cim-server && cim-client && cim-forward-route
mvn clean package spring-boot:repackage -DskipTests=true
```
### Deploy IM-server (cim-server)
```shell
cp /cim/cim-server/target/cim-server-1.0.0-SNAPSHOT.jar /xx/work/server0/
cd /xx/work/server0/
nohup java -jar /root/work/server0/cim-server-1.0.0-SNAPSHOT.jar --cim.server.port=9000 --app.zk.addr= > /root/work/server0/log.file 2>&1 &
```
> For cim-server cluster deployment, just ensure all instances point to the same Zookeeper address.
### Deploy Route Server (cim-forward-route)
```shell
cp /cim/cim-server/cim-forward-route/target/cim-forward-route-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
nohup java -jar /root/work/route0/cim-forward-route-1.0.0-SNAPSHOT.jar --app.zk.addr= --spring.redis.host= --spring.redis.port=6379 > /root/work/route/log.file 2>&1 &
```
> cim-forward-route is stateless and can be deployed on multiple nodes; use Nginx as a reverse proxy.
### Start Client
```shell
cp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
java -jar cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id= --cim.user.userName= --cim.route.url=http://:8083/
```


As shown above, two clients can communicate with each other.
### Local Client Startup
#### Register Account
```shell
curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
"reqNo": "1234567890",
"timeStamp": 0,
"userName": "zhangsan"
}' 'http://:8083/registerAccount'
```
Get the `userId` from the response:
```json
{
"code":"9000",
"message":"success",
"reqNo":null,
"dataBody":{
"userId":1547028929407,
"userName":"test"
}
}
```
#### Start Local Client
```shell
# Start local client
cp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
java -jar cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id= --cim.user.userName= --cim.route.url=http://:8083/
```
## Built-in Commands
| Command | Description |
| ------ | ------ |
| `:q!` | Quit the client |
| `:olu` | List all online users |
| `:all` | Show all available commands |
| `:q [keyword]` | Search chat history by keyword |
| `:ai` | Enable AI mode |
| `:qai` | Disable AI mode |
| `:pu` | Fuzzy search users |
| `:info` | Show client information |
| `:emoji [option]` | Browse emoji list [option: page number] |
| `:delay [msg] [delayTime]` | Send a delayed message |
| `:` | More commands are under development... |

### Chat History Query

Use the command `:q keyword` to search chat history related to you.
> Client chat history is stored in `/opt/logs/cim/` by default, so write permission is required for this directory. You can also customize the directory by adding `--cim.msg.logger.path=/custom/path` to the startup command.
### AI Mode

Use the command `:ai` to enable AI mode. After that, all messages will be responded to by `AI`.
Use `:qai` to exit AI mode.
### Prefix Match Username

Use the command `:qu prefix` to search user information by prefix.
> This feature is primarily designed for searching users in input fields on mobile clients.
### Group Chat/Private Chat
#### Group Chat



For group chat, simply type a message in the console and press Enter to send. All online clients will receive the message.
#### Private Chat
To send a private message, you need to know the recipient's `userID`.
Use the command `:olu` to list all online users.

Then use the format `userId;;message content` to send a private message.




Meanwhile, the other account will not receive the message.

### Emoji Support
Use the command `:emoji 1` to list all available emojis. Use the emoji alias to send an emoji.


### Delayed Messages
Send a message with a 10-second delay:
```shell
:delay delayMsg 10
```

## Contact
## Contributing
We welcome contributions! Before submitting a PR, please ensure your code passes the Checkstyle check.
### Code Style
This project uses [Checkstyle](https://checkstyle.org/) to enforce code style. The rules are defined in `checkstyle/checkstyle.xml`.
**Run Checkstyle locally before committing:**
```shell
mvn checkstyle:check
```
**Key rules:**
- Use spaces around `{`, `}`, and operators
- No trailing whitespace
- Files must end with a newline
- Remove unused imports
- Constants (`static final`) must be `UPPER_SNAKE_CASE`
- Use Java-style array declarations: `String[] args` (not `String args[]`)
**Skip Checkstyle for quick builds:**
```shell
mvn package -Dcheckstyle.skip=true
```
- [crossoverJie@gmail.com](mailto:crossoverJie@gmail.com)
================================================
FILE: checkstyle/checkstyle.xml
================================================
================================================
FILE: checkstyle/suppressions.xml
================================================
================================================
FILE: cim-client/pom.xml
================================================
4.0.0
com.crossoverjie.netty
cim
1.0.0-SNAPSHOT
cim-client
jar
UTF-8
UTF-8
17
2.5.0
com.google.protobuf
protobuf-java
com.crossoverjie.netty
cim-common
com.crossoverjie.netty
cim-client-sdk
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
test
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-starter-actuator
junit
junit
com.alibaba
fastjson
com.vdurmont
emoji-java
5.0.0
com.crossoverjie.netty
cim-rout-api
org.springframework.boot
spring-boot-maven-plugin
repackage
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/CIMClientApplication.java
================================================
package com.crossoverjie.cim.client;
import com.crossoverjie.cim.client.scanner.Scan;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author crossoverJie
*/
@Slf4j
@SpringBootApplication
public class CIMClientApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(CIMClientApplication.class, args);
log.info("Client start success");
}
@Override
public void run(String... args) {
Scan scan = new Scan();
Thread thread = new Thread(scan);
thread.setName("scan-thread");
thread.start();
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/config/AppConfiguration.java
================================================
package com.crossoverjie.cim.client.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* Function:
*
* @author crossoverJie
* Date: 2018/8/24 01:43
* @since JDK 1.8
*/
@Component
@Data
public class AppConfiguration {
@Value("${cim.user.id}")
private Long userId;
@Value("${cim.user.userName}")
private String userName;
@Value("${cim.msg.logger.path}")
private String msgLoggerPath;
@Value("${cim.heartbeat.time}")
private long heartBeatTime;
@Value("${cim.reconnect.count}")
private int reconnectCount;
@Value("${cim.route.url}")
private String routeUrl;
@Value("${cim.callback.thread.queue.size}")
private int queueSize;
@Value("${cim.callback.thread.pool.size}")
private int poolSize;
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/config/BeanConfig.java
================================================
package com.crossoverjie.cim.client.config;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.client.sdk.io.backoff.RandomBackoff;
import com.crossoverjie.cim.client.service.MsgLogger;
import com.crossoverjie.cim.client.service.ShutDownSign;
import com.crossoverjie.cim.client.service.impl.MsgCallBackListener;
import com.crossoverjie.cim.common.data.construct.RingBufferWheel;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import jakarta.annotation.Resource;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Function:bean 配置
*
* @author crossoverJie
* Date: 24/05/2018 15:55
* @since JDK 1.8
*/
@Configuration
public class BeanConfig {
@Resource
private AppConfiguration appConfiguration;
@Resource
private ShutDownSign shutDownSign;
@Resource
private MsgLogger msgLogger;
@Bean
public Client buildClient(@Qualifier("callBackThreadPool") ThreadPoolExecutor callbackThreadPool,
Event event) {
OkHttpClient okHttpClient = new OkHttpClient.Builder().connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(3, TimeUnit.SECONDS)
.writeTimeout(3, TimeUnit.SECONDS)
.retryOnConnectionFailure(true).build();
return Client.builder()
.auth(ClientConfigurationData.Auth.builder()
.userName(appConfiguration.getUserName())
.userId(appConfiguration.getUserId())
.build())
.routeUrl(appConfiguration.getRouteUrl())
.loginRetryCount(appConfiguration.getReconnectCount())
.event(event)
.reconnectCheck(client -> !shutDownSign.checkStatus())
.okHttpClient(okHttpClient)
.messageListener(new MsgCallBackListener(msgLogger, event))
.callbackThreadPool(callbackThreadPool)
.backoffStrategy(new RandomBackoff())
.build();
}
/**
* http client
*
* @return okHttp
*/
@Bean
public OkHttpClient okHttpClient() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(3, TimeUnit.SECONDS)
.writeTimeout(3, TimeUnit.SECONDS)
.retryOnConnectionFailure(true);
return builder.build();
}
/**
* Create callback thread pool
*
* @return
*/
@Bean("callBackThreadPool")
public ThreadPoolExecutor buildCallerThread() {
BlockingQueue queue = new LinkedBlockingQueue<>(appConfiguration.getQueueSize());
ThreadFactory executor = new ThreadFactoryBuilder()
.setNameFormat("msg-callback-%d")
.setDaemon(true)
.build();
return new ThreadPoolExecutor(appConfiguration.getPoolSize(), appConfiguration.getPoolSize(), 1,
TimeUnit.MILLISECONDS, queue, executor);
}
@Bean
public RingBufferWheel bufferWheel() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
return new RingBufferWheel(executorService);
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/config/SwaggerConfig.java
================================================
package com.crossoverjie.cim.client.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI createRestApi() {
return new OpenAPI()
.info(apiInfo());
}
private Info apiInfo() {
return new Info()
.title("cim client")
.description("cim client api")
.termsOfService("http://crossoverJie.top")
.contact(contact())
.version("1.0.0");
}
private Contact contact() {
Contact contact = new Contact();
contact.setName("crossoverJie");
return contact;
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/scanner/Scan.java
================================================
package com.crossoverjie.cim.client.scanner;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.MsgHandle;
import com.crossoverjie.cim.client.service.MsgLogger;
import com.crossoverjie.cim.client.util.SpringBeanFactory;
import java.util.Scanner;
import lombok.SneakyThrows;
/**
* Function:
*
* @author crossoverJie
* Date: 2018/12/21 16:44
* @since JDK 1.8
*/
public class Scan implements Runnable {
private final MsgHandle msgHandle;
private final MsgLogger msgLogger;
private final Event event;
public Scan() {
this.msgHandle = SpringBeanFactory.getBean(MsgHandle.class);
this.msgLogger = SpringBeanFactory.getBean(MsgLogger.class);
this.event = SpringBeanFactory.getBean(Event.class);
}
@SneakyThrows
@Override
public void run() {
Scanner sc = new Scanner(System.in);
while (true) {
String msg = sc.nextLine();
if (msgHandle.checkMsg(msg)) {
continue;
}
// internal cmd
if (msgHandle.innerCommand(msg)) {
continue;
}
msgHandle.sendMsg(msg);
// write to log
msgLogger.log(msg);
event.info(msg);
}
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/InnerCommand.java
================================================
package com.crossoverjie.cim.client.service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:26
* @since JDK 1.8
*/
public interface InnerCommand {
/**
* 执行
* @param msg
*/
void process(String msg) throws Exception;
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/InnerCommandContext.java
================================================
package com.crossoverjie.cim.client.service;
import com.crossoverjie.cim.client.service.impl.command.PrintAllCommand;
import com.crossoverjie.cim.client.util.SpringBeanFactory;
import com.crossoverjie.cim.common.enums.SystemCommandEnum;
import com.crossoverjie.cim.common.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:39
* @since JDK 1.8
*/
@Slf4j
@Component
public class InnerCommandContext {
/**
* 获取执行器实例
* @param command 执行器实例
* @return
*/
public InnerCommand getInstance(String command) {
Map allClazz = SystemCommandEnum.getAllClazz();
//兼容需要命令后接参数的数据 :q cross
String[] trim = command.trim().split(" ");
String clazz = allClazz.get(trim[0]);
InnerCommand innerCommand = null;
try {
if (StringUtil.isEmpty(clazz)) {
clazz = PrintAllCommand.class.getName();
}
innerCommand = (InnerCommand) SpringBeanFactory.getBean(Class.forName(clazz));
} catch (Exception e) {
log.error("Exception", e);
}
return innerCommand;
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/MsgHandle.java
================================================
package com.crossoverjie.cim.client.service;
/**
* Function:消息处理器
*
* @author crossoverJie
* Date: 2018/12/26 11:11
* @since JDK 1.8
*/
public interface MsgHandle {
/**
* 统一的发送接口,包含了 groupChat p2pChat
*
* @param msg
*/
void sendMsg(String msg) throws Exception;
/**
* 校验消息
*
* @param msg
* @return 不能为空,后续可以加上一些敏感词
* @throws Exception
*/
boolean checkMsg(String msg);
/**
* 执行内部命令
*
* @param msg
* @return 是否应当跳过当前消息(包含了":" 就需要跳过)
*/
boolean innerCommand(String msg) throws Exception;
/**
* 开启 AI 模式
*/
void openAIModel();
/**
* 关闭 AI 模式
*/
void closeAIModel();
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/MsgLogger.java
================================================
package com.crossoverjie.cim.client.service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019/1/6 15:23
* @since JDK 1.8
*/
public interface MsgLogger {
/**
* write log
* @param msg
*/
void log(String msg);
/**
* 停止写入
*/
void stop();
/**
* 查询聊天记录
* @param key 关键字
* @return
*/
String query(String key);
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/ShutDownSign.java
================================================
package com.crossoverjie.cim.client.service;
import org.springframework.stereotype.Component;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-02-27 16:17
* @since JDK 1.8
*/
@Component
public class ShutDownSign {
private boolean isCommand;
/**
* Set user exit sign.
*/
public void shutdown() {
isCommand = true;
}
public boolean checkStatus() {
return isCommand;
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/AsyncMsgLogger.java
================================================
package com.crossoverjie.cim.client.service.impl;
import com.crossoverjie.cim.client.config.AppConfiguration;
import com.crossoverjie.cim.client.service.MsgLogger;
import jakarta.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Stream;
/**
* Function:
*
* @author crossoverJie
* Date: 2019/1/6 15:26
* @since JDK 1.8
*/
@Slf4j
@Service
public class AsyncMsgLogger implements MsgLogger {
/**
* The default buffer size.
*/
private static final int DEFAULT_QUEUE_SIZE = 16;
private final BlockingQueue blockingQueue = new ArrayBlockingQueue(DEFAULT_QUEUE_SIZE);
private volatile boolean started = false;
private final Worker worker = new Worker();
@Resource
private AppConfiguration appConfiguration;
@Override
public void log(String msg) {
// start worker
startMsgLogger();
try {
// TODO: 2019/1/6 消息堆满是否阻塞线程?
blockingQueue.put(msg);
} catch (InterruptedException e) {
log.error("InterruptedException", e);
}
}
private class Worker extends Thread {
@Override
public void run() {
while (started) {
try {
String msg = blockingQueue.take();
writeLog(msg);
} catch (InterruptedException e) {
break;
}
}
}
}
private void writeLog(String msg) {
LocalDate today = LocalDate.now();
int year = today.getYear();
int month = today.getMonthValue();
int day = today.getDayOfMonth();
String dir = appConfiguration.getMsgLoggerPath() + appConfiguration.getUserName() + "/";
String fileName = dir + year + month + day + ".log";
Path file = Paths.get(fileName);
boolean exists = Files.exists(Paths.get(dir), LinkOption.NOFOLLOW_LINKS);
try {
if (!exists) {
Files.createDirectories(Paths.get(dir));
}
List lines = Collections.singletonList(msg);
Files.write(file, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
log.info("IOException", e);
}
}
/**
* Begin worker
*/
private void startMsgLogger() {
if (started) {
return;
}
worker.setDaemon(true);
worker.setName("AsyncMsgLogger-Worker");
started = true;
worker.start();
}
@Override
public void stop() {
started = false;
worker.interrupt();
}
@Override
public String query(String key) {
StringBuilder sb = new StringBuilder();
Path path = Paths.get(appConfiguration.getMsgLoggerPath() + appConfiguration.getUserName() + "/");
try {
@Cleanup
Stream list = Files.list(path);
List collect = list.toList();
for (Path file : collect) {
List strings = Files.readAllLines(file);
for (String msg : strings) {
if (msg.trim().contains(key)) {
sb.append(msg).append("\n");
}
}
}
} catch (IOException e) {
log.info("IOException", e);
}
return sb.toString().replace(key, "\033[31;4m" + key + "\033[0m");
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/EchoServiceImpl.java
================================================
package com.crossoverjie.cim.client.service.impl;
import com.crossoverjie.cim.client.config.AppConfiguration;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.MsgLogger;
import com.vdurmont.emoji.EmojiParser;
import jakarta.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalTime;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-08-27 22:37
* @since JDK 1.8
*/
@Service
public class EchoServiceImpl implements Event {
private static final String PREFIX = "$";
@Resource
private AppConfiguration appConfiguration;
@Resource
private MsgLogger msgLogger;
@Override
public void debug(String msg, Object... replace) {
msgLogger.log(String.format("Debug[%s]", msg));
}
@Override
public void info(String msg, Object... replace) {
// Make terminal can display the emoji
msg = EmojiParser.parseToUnicode(msg);
String date = LocalDate.now() + " " + LocalTime.now().withNano(0).toString();
msg = "[" + date + "] \033[31;4m" + appConfiguration.getUserName() + PREFIX + "\033[0m" + " " + msg;
String log = print(msg, replace);
System.out.println(log);
}
@Override
public void warn(String msg, Object... replace) {
info(String.format("Warn##%s##", msg), replace);
}
@Override
public void error(String msg, Object... replace) {
info(String.format("Error!!%s!!", msg), replace);
}
@Override
public void fatal(Client client) {
info("{} fatal error, shutdown client", client.getAuth());
}
/**
* print msg
*
* @param msg
* @param place
* @return
*/
private String print(String msg, Object... place) {
StringBuilder sb = new StringBuilder();
int k = 0;
for (int i = 0; i < place.length; i++) {
int index = msg.indexOf("{}", k);
if (index == -1) {
return msg;
}
if (index != 0) {
sb.append(msg, k, index);
sb.append(place[i]);
if (place.length == 1) {
sb.append(msg, index + 2, msg.length());
}
} else {
sb.append(place[i]);
if (place.length == 1) {
sb.append(msg, index + 2, msg.length());
}
}
k = index + 2;
}
if (sb.toString().equals("")) {
return msg;
} else {
return sb.toString();
}
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/MsgCallBackListener.java
================================================
package com.crossoverjie.cim.client.service.impl;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.sdk.io.MessageListener;
import com.crossoverjie.cim.client.service.MsgLogger;
import com.crossoverjie.cim.common.constant.Constants;
import java.util.Map;
/**
* Function:自定义收到消息回调
*
* @author crossoverJie
* Date: 2019/1/6 17:49
* @since JDK 1.8
*/
public class MsgCallBackListener implements MessageListener {
private final MsgLogger msgLogger;
private final Event event;
public MsgCallBackListener(MsgLogger msgLogger, Event event) {
this.msgLogger = msgLogger;
this.event = event;
}
@Override
public void received(Client client, Map properties, String msg) {
String sendUserName = properties.getOrDefault(Constants.MetaKey.SEND_USER_NAME, "nobody");
this.msgLogger.log(sendUserName + ":" + msg);
this.event.info(sendUserName + ":" + msg);
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/MsgHandler.java
================================================
package com.crossoverjie.cim.client.service.impl;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.InnerCommandContext;
import com.crossoverjie.cim.client.service.MsgHandle;
import com.crossoverjie.cim.common.util.StringUtil;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2018/12/26 11:15
* @since JDK 1.8
*/
@Slf4j
@Service
public class MsgHandler implements MsgHandle {
@Resource
private InnerCommandContext innerCommandContext;
@Resource
private Client client;
private boolean aiModel = false;
@Override
public void sendMsg(String msg) throws Exception {
if (aiModel) {
aiChat(msg);
} else {
normalChat(msg);
}
}
private void normalChat(String msg) throws Exception {
String[] totalMsg = msg.split(";;");
if (totalMsg.length > 1) {
P2PReqVO p2PReqVO = new P2PReqVO();
p2PReqVO.setReceiveUserId(Long.parseLong(totalMsg[0]));
p2PReqVO.setMsg(totalMsg[1]);
client.sendP2P(p2PReqVO);
} else {
client.sendGroup(msg);
}
}
/**
* AI model
*
* @param msg
*/
private void aiChat(String msg) {
msg = msg.replace("吗", "");
msg = msg.replace("嘛", "");
msg = msg.replace("?", "!");
msg = msg.replace("?", "!");
msg = msg.replace("你", "我");
System.out.println("AI:\033[31;4m" + msg + "\033[0m");
}
@Override
public boolean checkMsg(String msg) {
if (StringUtil.isEmpty(msg)) {
log.warn("不能发送空消息!");
return true;
}
return false;
}
@Override
public boolean innerCommand(String msg) throws Exception {
if (msg.startsWith(":")) {
InnerCommand instance = innerCommandContext.getInstance(msg);
instance.process(msg);
return true;
} else {
return false;
}
}
@Override
public void openAIModel() {
aiModel = true;
}
@Override
public void closeAIModel() {
aiModel = false;
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/CloseAIModelCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.MsgHandle;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:37
* @since JDK 1.8
*/
@Service
public class CloseAIModelCommand implements InnerCommand {
@Resource
private MsgHandle msgHandle;
@Resource
private Event event;
@Override
public void process(String msg) {
msgHandle.closeAIModel();
event.info("\033[31;4m" + "。゚(゚´ω`゚)゚。 AI 下线了!" + "\033[0m");
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/DelayMsgCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.MsgHandle;
import com.crossoverjie.cim.common.data.construct.RingBufferWheel;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-09-25 00:37
* @since JDK 1.8
*/
@Service
@Slf4j
public class DelayMsgCommand implements InnerCommand {
@Resource
private Event event;
@Resource
private MsgHandle msgHandle;
@Resource
private RingBufferWheel ringBufferWheel;
@Override
public void process(String msg) {
if (msg.split(" ").length <= 2) {
event.info("incorrect commond, :delay [msg] [delayTime]");
return;
}
String message = msg.split(" ")[1];
int delayTime = Integer.parseInt(msg.split(" ")[2]);
RingBufferWheel.Task task = new DelayMsgJob(message);
task.setKey(delayTime);
ringBufferWheel.addTask(task);
event.info(msg);
}
private class DelayMsgJob extends RingBufferWheel.Task {
private String msg;
public DelayMsgJob(String msg) {
this.msg = msg;
}
@Override
public void run() {
try {
msgHandle.sendMsg(msg);
} catch (Exception e) {
log.error("Delay message send error", e);
}
}
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/EchoInfoCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:37
* @since JDK 1.8
*/
@Service
public class EchoInfoCommand implements InnerCommand {
@Resource
private Client client;
@Resource
private Event event;
@Override
public void process(String msg) {
event.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
event.info("client info={}", client.getAuth());
event.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/EmojiCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.vdurmont.emoji.Emoji;
import com.vdurmont.emoji.EmojiManager;
import com.vdurmont.emoji.EmojiParser;
import jakarta.annotation.Resource;
import java.util.List;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:37
* @since JDK 1.8
*/
@Service
public class EmojiCommand implements InnerCommand {
@Resource
private Event event;
@Override
public void process(String msg) {
if (msg.split(" ").length <= 1) {
event.info("incorrect commond, :emoji [option]");
return;
}
String value = msg.split(" ")[1];
if (value != null) {
int index = Integer.parseInt(value);
List all = (List) EmojiManager.getAll();
all = all.subList(5 * index, 5 * index + 5);
for (Emoji emoji : all) {
event.info(EmojiParser.parseToAliases(emoji.getUnicode()) + "--->" + emoji.getUnicode());
}
}
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/OpenAIModelCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.MsgHandle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:37
* @since JDK 1.8
*/
@Service
public class OpenAIModelCommand implements InnerCommand {
@Autowired
private MsgHandle msgHandle;
@Override
public void process(String msg) {
msgHandle.openAIModel();
System.out.println("\033[31;4m" + "Hello,我是估值两亿的 AI 机器人!" + "\033[0m");
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrefixSearchCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.common.data.construct.TrieTree;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import jakarta.annotation.Resource;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:37
* @since JDK 1.8
*/
@Slf4j
@Service
public class PrefixSearchCommand implements InnerCommand {
@Resource
private Client client;
@Resource
private Event event;
@Override
public void process(String msg) {
try {
Set onlineUsers = client.getOnlineUser();
TrieTree trieTree = new TrieTree();
for (CIMUserInfo onlineUser : onlineUsers) {
trieTree.insert(onlineUser.getUserName());
}
String[] split = msg.split(" ");
String key = split[1];
List list = trieTree.prefixSearch(key);
for (String res : list) {
res = res.replace(key, "\033[31;4m" + key + "\033[0m");
event.info(res);
}
} catch (Exception e) {
log.error("Exception", e);
}
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrintAllCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.common.enums.SystemCommandEnum;
import jakarta.annotation.Resource;
import java.util.Map;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:37
* @since JDK 1.8
*/
@Service
public class PrintAllCommand implements InnerCommand {
@Resource
private Event event;
@Override
public void process(String msg) {
Map allStatusCode = SystemCommandEnum.getAllStatusCode();
event.info("====================================");
for (Map.Entry stringStringEntry : allStatusCode.entrySet()) {
String key = stringStringEntry.getKey();
String value = stringStringEntry.getValue();
event.info(key + "----->" + value);
}
event.info("====================================");
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrintOnlineUsersCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import jakarta.annotation.Resource;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:37
* @since JDK 1.8
*/
@Slf4j
@Service
public class PrintOnlineUsersCommand implements InnerCommand {
@Resource
private Client client;
@Resource
private Event event;
@Override
public void process(String msg) {
try {
Set onlineUsers = client.getOnlineUser();
event.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
for (CIMUserInfo onlineUser : onlineUsers) {
event.info("userId={}=====userName={}", onlineUser.getUserId(), onlineUser.getUserName());
}
event.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
} catch (Exception e) {
log.error("Exception", e);
}
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/QueryHistoryCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.MsgLogger;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:37
* @since JDK 1.8
*/
@Slf4j
@Service
public class QueryHistoryCommand implements InnerCommand {
@Resource
private MsgLogger msgLogger;
@Resource
private Event event;
@Override
public void process(String msg) {
String[] split = msg.split(" ");
if (split.length < 2) {
return;
}
String res = msgLogger.query(split[1]);
event.info(res);
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/ShutDownCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.MsgLogger;
import com.crossoverjie.cim.client.service.ShutDownSign;
import com.crossoverjie.cim.common.data.construct.RingBufferWheel;
import jakarta.annotation.Resource;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.stereotype.Service;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-27 19:28
* @since JDK 1.8
*/
@Slf4j
@Service
@ConditionalOnWebApplication
public class ShutDownCommand implements InnerCommand {
@Resource
private Client cimClient;
@Resource
private MsgLogger msgLogger;
@Resource(name = "callBackThreadPool")
private ThreadPoolExecutor callBackExecutor;
@Resource
private Event event;
@Resource
private ShutDownSign shutDownSign;
@Resource
private RingBufferWheel ringBufferWheel;
@Override
public void process(String msg) throws Exception {
event.info("cim client closing...");
cimClient.close();
shutDownSign.shutdown();
msgLogger.stop();
callBackExecutor.shutdown();
ringBufferWheel.stop(false);
try {
while (!callBackExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
event.info("thread pool closing");
}
} catch (Exception e) {
log.error("exception", e);
}
event.info("cim close success!");
System.exit(0);
}
}
================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/util/SpringBeanFactory.java
================================================
package com.crossoverjie.cim.client.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public final class SpringBeanFactory implements ApplicationContextAware {
private static ApplicationContext context;
public static T getBean(Class c) {
return context.getBean(c);
}
public static T getBean(String name, Class clazz) {
return context.getBean(name, clazz);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
}
================================================
FILE: cim-client/src/main/resources/application.yaml
================================================
spring:
application:
name: cim-client
# web port
server:
port: 8082
logging:
level:
root: error
# enable swagger
springdoc:
swagger-ui:
enabled: true
# log path
cim:
msg:
logger:
path: /opt/logs/cim/
route:
url: http://localhost:8083 # route url suggested that this is Nginx address
user: # cim userId and userName
id: 1747309836375
userName: yuge
callback:
thread:
queue:
size: 10
pool:
size: 1
heartbeat:
time: 60 # cim heartbeat time (seconds)
reconnect:
count: 3
================================================
FILE: cim-client/src/main/resources/banner.txt
================================================
_ ___ __
____(_)_ _ ____/ (_)__ ___ / /_
/ __/ / ' \ / __/ / / -_) _ \/ __/
\__/_/_/_/_/ \__/_/_/\__/_//_/\__/
Power by @crossoverJie
================================================
FILE: cim-client/src/test/java/com/crossoverjie/cim/client/service/InnerCommandContextTest.java
================================================
package com.crossoverjie.cim.client.service;
import com.crossoverjie.cim.client.CIMClientApplication;
import com.crossoverjie.cim.common.enums.SystemCommandEnum;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest(classes = CIMClientApplication.class)
@RunWith(SpringRunner.class)
public class InnerCommandContextTest {
@Autowired
private InnerCommandContext context;
@Test
public void execute() throws Exception {
String msg = ":all";
InnerCommand execute = context.getInstance(msg);
execute.process(msg);
}
// @Test
public void execute3() throws Exception {
// TODO: 2024/8/31 Integration test
String msg = SystemCommandEnum.ONLINE_USER.getCommandType();
InnerCommand execute = context.getInstance(msg);
execute.process(msg);
}
@Test
public void execute4() throws Exception {
String msg = ":q 天气";
InnerCommand execute = context.getInstance(msg);
execute.process(msg);
}
@Test
public void execute5() throws Exception {
String msg = ":q crossoverJie";
InnerCommand execute = context.getInstance(msg);
execute.process(msg);
}
@Test
public void execute6() throws Exception {
String msg = SystemCommandEnum.AI.getCommandType();
InnerCommand execute = context.getInstance(msg);
execute.process(msg);
}
@Test
public void execute7() throws Exception {
String msg = SystemCommandEnum.QAI.getCommandType();
InnerCommand execute = context.getInstance(msg);
execute.process(msg);
}
// @Test
public void execute8() throws Exception {
// TODO: 2024/8/31 Integration test
String msg = ":pu cross";
InnerCommand execute = context.getInstance(msg);
execute.process(msg);
}
@Test
public void execute9() throws Exception {
String msg = SystemCommandEnum.INFO.getCommandType();
InnerCommand execute = context.getInstance(msg);
execute.process(msg);
}
@Test
public void execute10() throws Exception {
String msg = "dsds";
InnerCommand execute = context.getInstance(msg);
execute.process(msg);
}
// @Test
public void quit() throws Exception {
String msg = ":q!";
InnerCommand execute = context.getInstance(msg);
execute.process(msg);
}
}
================================================
FILE: cim-client/src/test/java/com/crossoverjie/cim/client/service/impl/AsyncMsgLoggerTest.java
================================================
package com.crossoverjie.cim.client.service.impl;
import com.crossoverjie.cim.client.CIMClientApplication;
import com.crossoverjie.cim.client.service.MsgLogger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.TimeUnit;
@SpringBootTest(classes = CIMClientApplication.class)
@RunWith(SpringRunner.class)
public class AsyncMsgLoggerTest {
@Autowired
private MsgLogger msgLogger;
@Test
public void writeLog() throws Exception {
for (int i = 0; i < 10; i++) {
msgLogger.log("zhangsan:【asdsd】" + i);
}
TimeUnit.SECONDS.sleep(2);
}
@Test
public void query() {
String crossoverJie = msgLogger.query("crossoverJie");
System.out.println(crossoverJie);
}
}
================================================
FILE: cim-client/src/test/java/com/crossoverjie/cim/server/test/CommonTest.java
================================================
package com.crossoverjie.cim.server.test;
import com.crossoverjie.cim.common.core.proxy.RpcProxyManager;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.common.res.BaseResponse;
import com.crossoverjie.cim.route.api.RouteApi;
import com.crossoverjie.cim.route.api.vo.req.LoginReqVO;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vdurmont.emoji.EmojiParser;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import org.junit.Test;
/**
* Function:
*
* @author crossoverJie
* Date: 22/05/2018 18:44
* @since JDK 1.8
*/
@Slf4j
public class CommonTest {
@Test
public void searchMsg2() {
StringBuilder sb = new StringBuilder();
String allMsg = "于是在之前的基础上我完善了一些内容,先来看看这个项目的介绍吧:\n" +
"\n" +
"CIM(CROSS-IM) 一款面向开发者的 IM(即时通讯)系统;同时提供了一些组件帮助开发者构建一款属于自己可水平扩展的 IM 。\n" +
"\n" +
"借助 CIM 你可以实现以下需求:";
String key = "CIM";
String[] split = allMsg.split("\n");
for (String msg : split) {
if (msg.trim().contains(key)) {
sb.append(msg).append("\n");
}
}
int pos = 0;
String result = sb.toString();
int count = 1;
int multiple = 2;
while ((pos = result.indexOf(key, pos)) >= 0) {
log.info("{},{}",pos, pos + key.length());
pos += key.length();
count++;
}
System.out.println(sb.toString());
System.out.println(sb.toString().replace(key, "\033[31;4m" + key + "\033[0m"));
}
@Test
public void log() {
String msg = "hahahdsadsd";
LocalDate today = LocalDate.now();
int year = today.getYear();
int month = today.getMonthValue();
int day = today.getDayOfMonth();
String dir = "/opt/logs/cim/zhangsan" + "/";
String fileName = dir + year + month + day + ".log";
log.info("fileName={}", fileName);
Path file = Paths.get(fileName);
boolean exists = Files.exists(Paths.get(dir), LinkOption.NOFOLLOW_LINKS);
try {
if (!exists) {
Files.createDirectories(Paths.get(dir));
}
List lines = Arrays.asList(msg);
Files.write(file, lines, Charset.forName("UTF-8"), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
log.info("IOException", e);
}
}
@Test
public void emoji() throws Exception {
String str = "An :grinning:awesome :smiley:string 😄with a few :wink:emojis!";
String result = EmojiParser.parseToUnicode(str);
System.out.println(result);
result = EmojiParser.parseToAliases(str);
System.out.println(result);
//
// Collection all = EmojiManager.getAll();
// for (Emoji emoji : all) {
// System.out.println(EmojiParser.parseToAliases(emoji.getUnicode()) + "--->" + emoji.getUnicode() );
// }
}
@Test
public void emoji2() {
String emostring = "😂";
String faceWithTearsOfJoy = emostring.replaceAll("\uD83D\uDE02", "face with tears of joy");
System.out.println(faceWithTearsOfJoy);
System.out.println("======" + faceWithTearsOfJoy.replaceAll("face with tears of joy","\uD83D\uDE02"));
}
// @Test
public void deSerialize() throws Exception {
RouteApi routeApi = RpcProxyManager.create(RouteApi.class, "http://localhost:8083", new OkHttpClient());
BaseResponse login =
routeApi.login(new LoginReqVO(1725722966520L, "cj"));
System.out.println(login.getDataBody());
BaseResponse> setBaseResponse = routeApi.onlineUser();
log.info("setBaseResponse={}",setBaseResponse.getDataBody());
}
@Test
public void json() throws JsonProcessingException, ClassNotFoundException {
String json = "{\"code\":\"9000\",\"message\":\"成功\",\"reqNo\":null,\"dataBody\":{\"ip\":\"127.0.0.1\",\"cimServerPort\":11211,\"httpPort\":8081}}";
ObjectMapper objectMapper = new ObjectMapper();
Class> generic = null;
for (Method declaredMethod : RouteApi.class.getDeclaredMethods()) {
if (declaredMethod.getName().equals("login")) {
Type returnType = declaredMethod.getGenericReturnType();
// check if the return type is a parameterized type
if (returnType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) returnType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type typeArgument : actualTypeArguments) {
System.out.println("generic: " + typeArgument.getTypeName());
generic = Class.forName(typeArgument.getTypeName());
break;
}
} else {
System.out.println("not a generic type");
}
}
}
BaseResponse response = objectMapper.readValue(json,
objectMapper.getTypeFactory().constructParametricType(BaseResponse.class, generic));
System.out.println(response.getDataBody().getIp());
}
private static class Gen {
private T t;
private R r;
}
interface TestInterface {
Gen login();
}
@Test
public void test1() throws JsonProcessingException {
String json = "{\"code\":\"200\",\"message\":\"Success\",\"reqNo\":null,\"dataBody\":[{\"userId\":\"123\",\"userName\":\"Alice\"}, {\"userId\":\"456\",\"userName\":\"Bob\"}]}";
ObjectMapper objectMapper = new ObjectMapper();
// 获取 BaseResponse> 的泛型参数
Type setType = getGenericTypeOfBaseResponse();
// 将泛型类型传递给 ObjectMapper 进行反序列化
BaseResponse> response = objectMapper.readValue(json,
objectMapper.getTypeFactory().constructParametricType(BaseResponse.class, objectMapper.getTypeFactory().constructType(setType)));
System.out.println("Response Code: " + response.getCode());
System.out.println("Online Users: ");
for (CIMUserInfo user : response.getDataBody()) {
System.out.println("User ID: " + user.getUserId() + ", User Name: " + user.getUserName());
}
}
// 通过反射获取 BaseResponse> 中的泛型类型
public static Type getGenericTypeOfBaseResponse() {
// 这里模拟你需要处理的 BaseResponse>
ParameterizedType baseResponseType = (ParameterizedType) new TypeReference>>() {}.getType();
// 获取 BaseResponse 的泛型参数,即 Set
Type[] actualTypeArguments = baseResponseType.getActualTypeArguments();
// 返回第一个泛型参数 (Set)
return actualTypeArguments[0];
}
}
================================================
FILE: cim-client/src/test/java/com/crossoverjie/cim/server/test/EchoTest.java
================================================
package com.crossoverjie.cim.server.test;
import org.junit.Assert;
import org.junit.Test;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-08-28 01:47
* @since JDK 1.8
*/
public class EchoTest {
@Test
public void echo() {
String msg = "{} say,you {}";
String[] place = {"zhangsan", "haha"};
String log = log(msg, place);
System.out.println(log);
Assert.assertEquals(log,"zhangsan say,you haha");
}
@Test
public void echo2() {
String msg = "{} say,you {},zhangsan say {}";
String[] place = {"zhangsan", "haha", "nihao"};
String log = log(msg, place);
System.out.println(log);
Assert.assertEquals(log,"zhangsan say,you haha,zhangsan say nihao");
}
@Test
public void echo3() {
String msg = "see you {},zhangsan say";
String[] place = {"zhangsan"};
String log = log(msg, place);
System.out.println(log);
Assert.assertEquals(log,"see you zhangsan,zhangsan say");
}
@Test
public void echo4() {
String msg = "{}see you,zhangsan say";
String[] place = {"!!!"};
String log = log(msg, place);
System.out.println(log);
Assert.assertEquals(log,"!!!see you,zhangsan say");
}
@Test
public void echo5() {
String msg = "see you,zhangsan say{}";
String[] place = {"!!!"};
String log = log(msg, place);
System.out.println(log);
Assert.assertEquals(log,"see you,zhangsan say!!!");
}
@Test
public void echo6() {
String msg = "see you,zhangsan say";
String[] place = {""};
String log = log(msg, place);
System.out.println(log);
Assert.assertEquals(log,"see you,zhangsan say");
}
private String log(String msg, String... place) {
StringBuilder sb = new StringBuilder();
int k = 0;
for (int i = 0; i < place.length; i++) {
int index = msg.indexOf("{}", k);
if (index == -1) {
return msg;
}
if (index != 0) {
sb.append(msg, k, index);
sb.append(place[i]);
if (place.length == 1) {
sb.append(msg, index + 2, msg.length());
}
} else {
sb.append(place[i]);
if (place.length == 1) {
sb.append(msg, index + 2, msg.length());
}
}
k = index + 2;
}
return sb.toString();
}
}
================================================
FILE: cim-client/src/test/resources/application.yaml
================================================
spring:
application:
name: cim-client
main:
# this will not be used to create real spring context, because don't need this context in test case.
web-application-type: none
# web port
server:
port: 8082
logging:
level:
root: error
# enable swagger
springdoc:
swagger-ui:
enabled: true
# log path
cim:
msg:
logger:
path: /opt/logs/cim/
route:
url: http://localhost:8083 # route url suggested that this is Nginx address
user: # cim userId and userName
id: 1722343979085
userName: zhangsan
callback:
thread:
queue:
size: 1000
pool:
size: 2
heartbeat:
time: 60 # cim heartbeat time (seconds)
reconnect:
count: 3
================================================
FILE: cim-client-sdk/README.md
================================================
```java
var auth1 = ClientConfigurationData.Auth.builder()
.userId(id)
.userName(cj)
.build();
Client client1 = Client.builder()
.auth(auth1)
.routeUrl(routeUrl)
.build();
ClientState.State state = client1.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
Optional serverInfo = client1.getServerInfo();
Assertions.assertTrue(serverInfo.isPresent());
// send msg
String msg = "hello";
client1.sendGroup(msg);
// get oline user
Set onlineUser = client1.getOnlineUser();
```
================================================
FILE: cim-client-sdk/pom.xml
================================================
4.0.0
com.crossoverjie.netty
cim
1.0.0-SNAPSHOT
cim-client-sdk
17
17
UTF-8
com.crossoverjie.netty
cim-common
com.crossoverjie.netty
cim-rout-api
org.junit.jupiter
junit-jupiter
test
org.junit.vintage
junit-vintage-engine
test
com.crossoverjie.netty
cim-integration-test
${project.version}
test
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/Client.java
================================================
package com.crossoverjie.cim.client.sdk;
import com.crossoverjie.cim.client.sdk.impl.ClientBuilderImpl;
import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;
import java.io.Closeable;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
public interface Client extends Closeable {
static ClientBuilder builder() {
return new ClientBuilderImpl();
}
default void sendP2P(P2PReqVO p2PReqVO) throws Exception {
sendP2PAsync(p2PReqVO).get();
}
CompletableFuture sendP2PAsync(P2PReqVO p2PReqVO);
default void sendGroup(String msg) throws Exception {
sendGroupAsync(msg).get();
}
CompletableFuture sendGroupAsync(String msg);
ClientState.State getState();
ClientConfigurationData.Auth getAuth();
Set getOnlineUser() throws Exception;
Optional getServerInfo();
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ClientBuilder.java
================================================
package com.crossoverjie.cim.client.sdk;
import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.client.sdk.io.MessageListener;
import com.crossoverjie.cim.client.sdk.io.ReconnectCheck;
import java.util.concurrent.ThreadPoolExecutor;
import com.crossoverjie.cim.client.sdk.io.backoff.BackoffStrategy;
import okhttp3.OkHttpClient;
/**
* @author crossoverJie
*/
public interface ClientBuilder {
Client build();
ClientBuilder auth(ClientConfigurationData.Auth auth);
ClientBuilder routeUrl(String routeUrl);
ClientBuilder loginRetryCount(int loginRetryCount);
ClientBuilder event(Event event);
ClientBuilder reconnectCheck(ReconnectCheck reconnectCheck);
ClientBuilder okHttpClient(OkHttpClient okHttpClient);
ClientBuilder messageListener(MessageListener messageListener);
ClientBuilder callbackThreadPool(ThreadPoolExecutor callbackThreadPool);
ClientBuilder backoffStrategy(BackoffStrategy backoffStrategy);
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ClientState.java
================================================
package com.crossoverjie.cim.client.sdk;
import java.util.concurrent.atomic.AtomicReference;
public abstract class ClientState {
private static final AtomicReference STATE = new AtomicReference<>(State.Initialized);
public enum State {
/**
* Client state
*/
Initialized, Reconnecting, Ready, Closed, Failed
}
public void setState(State s) {
STATE.set(s);
}
public State getState() {
return STATE.get();
}
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/Event.java
================================================
package com.crossoverjie.cim.client.sdk;
public interface Event {
void debug(String msg, Object... replace);
void info(String msg, Object... replace);
void warn(String msg, Object... replace);
void error(String msg, Object... replace);
void fatal(Client client);
class DefaultEvent implements Event {
@Override
public void debug(String msg, Object... replace) {
System.out.println(msg);
}
@Override
public void info(String msg, Object... replace) {
System.out.println(msg);
}
@Override
public void warn(String msg, Object... replace) {
System.out.println(msg);
}
@Override
public void error(String msg, Object... replace) {
System.err.println(msg);
}
@Override
public void fatal(Client client) {
}
}
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/FetchOfflineMsgJob.java
================================================
package com.crossoverjie.cim.client.sdk;
import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.common.data.construct.RingBufferWheel;
public class FetchOfflineMsgJob extends RingBufferWheel.Task {
private static final int INITIAL_DELAY_SECONDS = 5;
private RouteManager routeManager;
private ClientConfigurationData conf;
public FetchOfflineMsgJob(RouteManager routeManager, ClientConfigurationData conf) {
this.routeManager = routeManager;
this.conf = conf;
setKey(INITIAL_DELAY_SECONDS); //It will be sent with a 5-second delay
}
@Override
public void run() {
routeManager.fetchOfflineMsgs(conf.getAuth().getUserId());
}
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ReConnectManager.java
================================================
package com.crossoverjie.cim.client.sdk;
import com.crossoverjie.cim.client.sdk.impl.ClientImpl;
import com.crossoverjie.cim.common.kit.HeartBeatHandler;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.netty.channel.ChannelHandlerContext;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
public final class ReConnectManager {
private ScheduledExecutorService scheduledExecutorService;
/**
* Trigger reconnect job
*
* @param ctx
*/
public void reConnect(ChannelHandlerContext ctx) {
buildExecutor();
scheduledExecutorService.scheduleAtFixedRate(() -> {
try {
ClientImpl.getClient().getHeartBeatHandler().process(ctx);
} catch (Exception e) {
ClientImpl.getClient().getConf().getEvent().error("ReConnectManager reConnect error", e);
}
},
0, 10, TimeUnit.SECONDS);
}
/**
* Close reconnect job if reconnect success.
*/
public void reConnectSuccess() {
scheduledExecutorService.shutdown();
}
/***
* build a thread executor
*/
private void buildExecutor() {
if (scheduledExecutorService == null || scheduledExecutorService.isShutdown()) {
ThreadFactory factory = new ThreadFactoryBuilder()
.setNameFormat("reConnect-job-%d")
.setDaemon(true)
.build();
scheduledExecutorService = new ScheduledThreadPoolExecutor(1, factory);
}
}
private static class ClientHeartBeatHandle implements HeartBeatHandler {
@Override
public void process(ChannelHandlerContext ctx) throws Exception {
ClientImpl.getClient().reconnect();
}
}
public static ReConnectManager createReConnectManager() {
return new ReConnectManager();
}
public static HeartBeatHandler createHeartBeatHandler() {
return new ClientHeartBeatHandle();
}
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/RouteManager.java
================================================
package com.crossoverjie.cim.client.sdk;
import com.crossoverjie.cim.client.sdk.impl.ClientImpl;
import com.crossoverjie.cim.common.core.proxy.RpcProxyManager;
import com.crossoverjie.cim.common.enums.StatusEnum;
import com.crossoverjie.cim.common.exception.CIMException;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.common.res.BaseResponse;
import com.crossoverjie.cim.common.res.NULLBody;
import com.crossoverjie.cim.route.api.RouteApi;
import com.crossoverjie.cim.route.api.vo.req.ChatReqVO;
import com.crossoverjie.cim.route.api.vo.req.LoginReqVO;
import com.crossoverjie.cim.route.api.vo.req.OfflineMsgReqVO;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import okhttp3.OkHttpClient;
public class RouteManager {
private final RouteApi routeApi;
private final Event event;
public RouteManager(String routeUrl, OkHttpClient okHttpClient, Event event) {
routeApi = RpcProxyManager.create(RouteApi.class, routeUrl, okHttpClient);
this.event = event;
}
public CIMServerResVO getServer(LoginReqVO loginReqVO) throws Exception {
BaseResponse cimServerResVO = routeApi.login(loginReqVO);
// repeat fail
if (!cimServerResVO.getCode().equals(StatusEnum.SUCCESS.getCode())) {
event.info(cimServerResVO.getMessage());
// when client in Reconnecting state, could exit.
if (ClientImpl.getClient().getState() == ClientState.State.Reconnecting) {
event.warn("###{}###", StatusEnum.RECONNECT_FAIL.getMessage());
throw new CIMException(StatusEnum.RECONNECT_FAIL);
}
}
return cimServerResVO.getDataBody();
}
public CompletableFuture sendP2P(CompletableFuture future, P2PReqVO p2PReqVO) {
return CompletableFuture.runAsync(() -> {
try {
BaseResponse response = routeApi.p2pRoute(p2PReqVO);
if (response.getCode().equals(StatusEnum.OFF_LINE.getCode())) {
future.completeExceptionally(new CIMException(StatusEnum.OFF_LINE));
}
future.complete(null);
} catch (Exception e) {
future.completeExceptionally(e);
event.error("send p2p msg error", e);
}
});
}
public CompletableFuture sendGroupMsg(ChatReqVO chatReqVO) {
return CompletableFuture.runAsync(() -> {
try {
routeApi.groupRoute(chatReqVO);
} catch (Exception e) {
event.error("send group msg error", e);
}
});
}
public void offLine(Long userId) {
ChatReqVO vo = new ChatReqVO(userId, "offLine", null);
routeApi.offLine(vo);
}
public Set onlineUser() throws Exception {
BaseResponse> onlineUsersResVO = routeApi.onlineUser();
return onlineUsersResVO.getDataBody();
}
public void fetchOfflineMsgs(Long userId) {
OfflineMsgReqVO offlineMsgReqVO = OfflineMsgReqVO.builder().receiveUserId(userId).build();
routeApi.fetchOfflineMsgs(offlineMsgReqVO);
}
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientBuilderImpl.java
================================================
package com.crossoverjie.cim.client.sdk.impl;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.ClientBuilder;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.sdk.io.MessageListener;
import com.crossoverjie.cim.client.sdk.io.ReconnectCheck;
import com.crossoverjie.cim.client.sdk.io.backoff.BackoffStrategy;
import com.crossoverjie.cim.common.util.StringUtil;
import java.util.concurrent.ThreadPoolExecutor;
import okhttp3.OkHttpClient;
public class ClientBuilderImpl implements ClientBuilder {
private final ClientConfigurationData conf;
public ClientBuilderImpl() {
this(new ClientConfigurationData());
}
public ClientBuilderImpl(ClientConfigurationData conf) {
this.conf = conf;
}
@Override
public Client build() {
return new ClientImpl(conf);
}
@Override
public ClientBuilder auth(ClientConfigurationData.Auth auth) {
if (auth.getUserId() <= 0 || StringUtil.isEmpty(auth.getUserName())) {
throw new IllegalArgumentException("userId and userName must be set");
}
this.conf.setAuth(auth);
return this;
}
@Override
public ClientBuilder routeUrl(String routeUrl) {
if (StringUtil.isEmpty(routeUrl)) {
throw new IllegalArgumentException("routeUrl must be set");
}
this.conf.setRouteUrl(routeUrl);
return this;
}
@Override
public ClientBuilder loginRetryCount(int loginRetryCount) {
this.conf.setLoginRetryCount(loginRetryCount);
return this;
}
@Override
public ClientBuilder event(Event event) {
this.conf.setEvent(event);
return this;
}
@Override
public ClientBuilder reconnectCheck(ReconnectCheck reconnectCheck) {
this.conf.setReconnectCheck(reconnectCheck);
return this;
}
@Override
public ClientBuilder okHttpClient(OkHttpClient okHttpClient) {
this.conf.setOkHttpClient(okHttpClient);
return this;
}
@Override
public ClientBuilder messageListener(MessageListener messageListener) {
this.conf.setMessageListener(messageListener);
return this;
}
@Override
public ClientBuilder callbackThreadPool(ThreadPoolExecutor callbackThreadPool) {
this.conf.setCallbackThreadPool(callbackThreadPool);
return this;
}
@Override
public ClientBuilder backoffStrategy(BackoffStrategy backoffStrategy) {
this.conf.setBackoffStrategy(backoffStrategy);
return this;
}
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientConfigurationData.java
================================================
package com.crossoverjie.cim.client.sdk.impl;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.sdk.io.backoff.BackoffStrategy;
import com.crossoverjie.cim.client.sdk.io.MessageListener;
import com.crossoverjie.cim.client.sdk.io.backoff.RandomBackoff;
import com.crossoverjie.cim.client.sdk.io.ReconnectCheck;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.concurrent.ThreadPoolExecutor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import okhttp3.OkHttpClient;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ClientConfigurationData {
private Auth auth;
@Data
@AllArgsConstructor
@Builder
public static class Auth {
private long userId;
private String userName;
}
private String routeUrl;
private int loginRetryCount = 5;
@JsonIgnore
private Event event = new Event.DefaultEvent();
@JsonIgnore
private MessageListener messageListener =
(client, properties, msg) -> System.out.printf("id:[%s] msg:[%s]%n \n", client.getAuth(), msg);
@JsonIgnore
private OkHttpClient okHttpClient = new OkHttpClient();
@JsonIgnore
private ThreadPoolExecutor callbackThreadPool;
@JsonIgnore
private ReconnectCheck reconnectCheck = (__) -> true;
@JsonIgnore
private BackoffStrategy backoffStrategy = new RandomBackoff();
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientImpl.java
================================================
package com.crossoverjie.cim.client.sdk.impl;
import static com.crossoverjie.cim.common.enums.StatusEnum.RECONNECT_FAIL;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.ClientState;
import com.crossoverjie.cim.client.sdk.FetchOfflineMsgJob;
import com.crossoverjie.cim.client.sdk.ReConnectManager;
import com.crossoverjie.cim.client.sdk.RouteManager;
import com.crossoverjie.cim.client.sdk.io.CIMClientHandleInitializer;
import com.crossoverjie.cim.common.data.construct.RingBufferWheel;
import com.crossoverjie.cim.common.exception.CIMException;
import com.crossoverjie.cim.common.kit.HeartBeatHandler;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.common.protocol.BaseCommand;
import com.crossoverjie.cim.common.protocol.Request;
import com.crossoverjie.cim.route.api.vo.req.ChatReqVO;
import com.crossoverjie.cim.route.api.vo.req.LoginReqVO;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ClientImpl extends ClientState implements Client {
@Getter
private final ClientConfigurationData conf;
private static final int CALLBACK_QUEUE_SIZE = 1024;
private static final int CALLBACK_POOL_SIZE = 10;
// ======= private ========
private int errorCount;
private SocketChannel channel;
private final RouteManager routeManager;
@Getter
private final HeartBeatHandler heartBeatHandler = ReConnectManager.createHeartBeatHandler();
@Getter
private final ReConnectManager reConnectManager = ReConnectManager.createReConnectManager();
@Getter
private static ClientImpl client;
@Getter
private static Map clientMap = new ConcurrentHashMap<>();
@Getter
private final Request heartBeatPacket;
private RingBufferWheel ringBufferWheel;
// Client connected server info
private CIMServerResVO serverInfo;
public ClientImpl(ClientConfigurationData conf) {
this.conf = conf;
if (this.conf.getCallbackThreadPool() == null) {
BlockingQueue queue = new LinkedBlockingQueue<>(CALLBACK_QUEUE_SIZE);
ThreadFactory factory = new ThreadFactoryBuilder()
.setNameFormat("msg-callback-%d")
.setDaemon(true)
.build();
this.conf.setCallbackThreadPool(
new ThreadPoolExecutor(CALLBACK_POOL_SIZE, CALLBACK_POOL_SIZE, 1, TimeUnit.SECONDS, queue,
factory));
}
routeManager = new RouteManager(conf.getRouteUrl(), conf.getOkHttpClient(), conf.getEvent());
heartBeatPacket = Request.newBuilder()
.setRequestId(this.conf.getAuth().getUserId())
.setReqMsg("ping")
.setCmd(BaseCommand.PING)
.build();
client = this;
clientMap.put(conf.getAuth().getUserId(), this);
connectServer(v -> this.conf.getEvent().info("Login success!"));
postConnectionSetup();
}
/**
* 1. Pull offline messages from the server
*/
private void postConnectionSetup() {
ringBufferWheel = new RingBufferWheel(Executors.newFixedThreadPool(1));
ringBufferWheel.addTask(new FetchOfflineMsgJob(routeManager, conf));
}
private void connectServer(Consumer success) {
this.doConnectServer().whenComplete((r, e) -> {
if (r) {
success.accept(null);
}
if (e != null) {
if (e instanceof CIMException cimException && cimException.getErrorCode()
.equals(RECONNECT_FAIL.getCode())) {
this.conf.getEvent().fatal(this);
} else {
if (errorCount++ >= this.conf.getLoginRetryCount()) {
this.conf.getEvent()
.error("The maximum number of reconnections has been reached[{}]times, exit cim client!",
errorCount);
this.conf.getEvent().fatal(this);
}
}
}
});
}
/**
* 1. User login and get target server
* 2. Connect target server
* 3. send login cmd to server
*/
private CompletableFuture doConnectServer() {
CompletableFuture future = new CompletableFuture<>();
this.userLogin(future).ifPresentOrElse((cimServer) -> {
this.doConnectServer(cimServer, future);
this.loginServer();
this.serverInfo = cimServer;
future.complete(true);
}, () -> {
this.conf.getEvent().error("Login fail!, cannot get server info!");
this.conf.getEvent().fatal(this);
future.complete(false);
});
return future;
}
/**
* Login and get server info
*
* @return Server info
*/
private Optional userLogin(CompletableFuture future) {
LoginReqVO loginReqVO = new LoginReqVO(conf.getAuth().getUserId(),
conf.getAuth().getUserName());
CIMServerResVO cimServer = null;
try {
cimServer = routeManager.getServer(loginReqVO);
log.info("cimServer=[{}]", cimServer);
} catch (Exception e) {
log.error("login fail", e);
future.completeExceptionally(e);
}
return Optional.ofNullable(cimServer);
}
private final EventLoopGroup group = new NioEventLoopGroup(0, new DefaultThreadFactory("cim-work"));
private void doConnectServer(CIMServerResVO cimServer, CompletableFuture future) {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new CIMClientHandleInitializer());
ChannelFuture sync;
try {
sync = bootstrap.connect(cimServer.getIp(), cimServer.getCimServerPort()).sync();
if (sync.isSuccess()) {
this.conf.getEvent().info("Start cim client success!");
channel = (SocketChannel) sync.channel();
}
} catch (InterruptedException e) {
future.completeExceptionally(e);
}
}
/**
* Send login cmd to server
*/
private void loginServer() {
Request login = Request.newBuilder()
.setRequestId(this.conf.getAuth().getUserId())
.setReqMsg(this.conf.getAuth().getUserName())
.setCmd(BaseCommand.LOGIN_REQUEST)
.build();
channel.writeAndFlush(login)
.addListener((ChannelFutureListener) channelFuture ->
this.conf.getEvent().info("Registry cim server success!")
);
}
/**
* 1. clear route information.
* 2. reconnect.
* 3. shutdown reconnect job.
* 4. reset reconnect state.
* @throws Exception
*/
public void reconnect() throws Exception {
if (channel != null && channel.isActive()) {
return;
}
this.serverInfo = null;
// clear route information.
this.routeManager.offLine(this.getConf().getAuth().getUserId());
this.conf.getEvent().info("cim trigger reconnecting....");
this.conf.getBackoffStrategy().runBackoff();
// don't set State ready, because when connect success, the State will be set to ready automate.
connectServer(v -> {
this.reConnectManager.reConnectSuccess();
this.conf.getEvent().info("Great! reConnect success!!!");
});
}
@Override
public void close() {
if (channel != null) {
channel.close();
channel = null;
}
super.setState(ClientState.State.Closed);
this.routeManager.offLine(this.getAuth().getUserId());
this.clientMap.remove(this.getAuth().getUserId());
ringBufferWheel.stop(true);
}
@Override
public void sendP2P(P2PReqVO p2PReqVO) throws Exception {
recordSendLog(sendP2PAsync(p2PReqVO), "P2P");
}
@Override
public void sendGroup(String msg) throws Exception {
recordSendLog(sendGroupAsync(msg), "GROUP");
}
private void recordSendLog(CompletableFuture future, String msgWay) {
future.orTimeout(10, TimeUnit.SECONDS)
.whenComplete((result, throwable) -> {
if (throwable == null) {
log.info("{} message task completed successfully", msgWay);
} else if (throwable instanceof TimeoutException) {
log.error("{} message processing timeout", msgWay, throwable);
} else {
log.warn("{} message task completed with exception", msgWay, throwable);
}
});
}
@Override
public CompletableFuture sendP2PAsync(P2PReqVO p2PReqVO) {
CompletableFuture future = new CompletableFuture<>();
p2PReqVO.setUserId(this.conf.getAuth().getUserId());
return routeManager.sendP2P(future, p2PReqVO);
}
@Override
public CompletableFuture sendGroupAsync(String msg) {
// TODO: 2024/9/12 return messageId
return this.routeManager.sendGroupMsg(new ChatReqVO(this.conf.getAuth().getUserId(), msg, null));
}
@Override
public ClientConfigurationData.Auth getAuth() {
return this.conf.getAuth();
}
@Override
public ClientState.State getState() {
return super.getState();
}
@Override
public Set getOnlineUser() throws Exception {
return routeManager.onlineUser();
}
@Override
public Optional getServerInfo() {
return Optional.ofNullable(this.serverInfo);
}
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/CIMClientHandle.java
================================================
package com.crossoverjie.cim.client.sdk.io;
import com.crossoverjie.cim.client.sdk.ClientState;
import com.crossoverjie.cim.client.sdk.impl.ClientImpl;
import com.crossoverjie.cim.common.constant.Constants;
import com.crossoverjie.cim.common.protocol.BaseCommand;
import com.crossoverjie.cim.common.protocol.Response;
import com.crossoverjie.cim.common.util.NettyAttrUtil;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
@ChannelHandler.Sharable
@Slf4j
public class CIMClientHandle extends SimpleChannelInboundHandler {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent idleStateEvent) {
if (idleStateEvent.state() == IdleState.WRITER_IDLE) {
ctx.writeAndFlush(ClientImpl.getClient().getHeartBeatPacket()).addListeners((ChannelFutureListener) future -> {
if (!future.isSuccess()) {
log.error("heart beat error,close Channel");
ClientImpl.getClient().getConf().getEvent().warn("heart beat error,close Channel");
future.channel().close();
}
});
}
}
super.userEventTriggered(ctx, evt);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
ClientImpl.getClient().getConf().getEvent().debug("ChannelActive");
ClientImpl.getClient().setState(ClientState.State.Ready);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
if (!ClientImpl.getClient().getConf().getReconnectCheck().isNeedReconnect(ClientImpl.getClient())) {
return;
}
ClientImpl.getClient().setState(ClientState.State.Closed);
ClientImpl.getClient().getConf().getEvent().warn("Client inactive, let's reconnect");
ClientImpl.getClient().getReConnectManager().reConnect(ctx);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Response msg) {
if (msg.getCmd() == BaseCommand.PING) {
ClientImpl.getClient().getConf().getEvent().debug("received ping from server");
NettyAttrUtil.updateReaderTime(ctx.channel(), System.currentTimeMillis());
}
if (msg.getCmd() != BaseCommand.PING) {
String receiveUserId = msg.getPropertiesMap().get(Constants.MetaKey.RECEIVE_USER_ID);
ClientImpl client = ClientImpl.getClientMap().get(Long.valueOf(receiveUserId));
if (client == null) {
log.error("client not found for userId: {}", receiveUserId);
return;
}
// callback
client.getConf().getCallbackThreadPool().execute(() -> {
log.info("client address: {} :{}", ctx.channel().remoteAddress(), client);
MessageListener messageListener = client.getConf().getMessageListener();
if (msg.getBatchResMsgCount() > 0) {
for (int i = 0; i < msg.getBatchResMsgCount(); i++) {
messageListener.received(client, msg.getPropertiesMap(), msg.getBatchResMsg(i));
}
} else {
messageListener.received(client, msg.getPropertiesMap(), msg.getResMsg());
}
});
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ClientImpl.getClient().getConf().getEvent().error(cause.getCause().toString());
ctx.close();
}
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/CIMClientHandleInitializer.java
================================================
package com.crossoverjie.cim.client.sdk.io;
import com.crossoverjie.cim.common.protocol.Response;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
import io.netty.handler.timeout.IdleStateHandler;
public class CIMClientHandleInitializer extends ChannelInitializer {
private final CIMClientHandle cimClientHandle = new CIMClientHandle();
@Override
protected void initChannel(Channel ch) {
ch.pipeline()
.addLast(new IdleStateHandler(0, 10, 0))
// google Protobuf
.addLast(new ProtobufVarint32FrameDecoder())
.addLast(new ProtobufDecoder(Response.getDefaultInstance()))
.addLast(new ProtobufVarint32LengthFieldPrepender())
.addLast(new ProtobufEncoder())
.addLast(cimClientHandle);
}
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/MessageListener.java
================================================
package com.crossoverjie.cim.client.sdk.io;
import com.crossoverjie.cim.client.sdk.Client;
import java.util.Map;
public interface MessageListener {
/**
* @param client client
* @param properties meta data
* @param msg msgs
*/
void received(Client client, Map properties, String msg);
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/ReconnectCheck.java
================================================
package com.crossoverjie.cim.client.sdk.io;
import com.crossoverjie.cim.client.sdk.Client;
public interface ReconnectCheck {
/**
* By the default, the client will reconnect to the server when the connection is close(inactive).
* @return false if the client should not reconnect to the server.
*/
boolean isNeedReconnect(Client client);
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/backoff/BackoffStrategy.java
================================================
package com.crossoverjie.cim.client.sdk.io.backoff;
import java.util.concurrent.TimeUnit;
/**
* @author:qjj
* @create: 2024-09-21 12:16
* @Description: backoff strategy interface
*/
public interface BackoffStrategy {
/**
* @return the backoff time in milliseconds
*/
long nextBackoff();
/**
* Run the backoff strategy
* @throws InterruptedException
*/
default void runBackoff() throws InterruptedException {
TimeUnit.SECONDS.sleep(nextBackoff());
}
}
================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/backoff/RandomBackoff.java
================================================
package com.crossoverjie.cim.client.sdk.io.backoff;
/**
* @author:qjj
* @create: 2024-09-21 12:22
* @Description: random backoff strategy
*/
public class RandomBackoff implements BackoffStrategy {
@Override
public long nextBackoff() {
return (int) (Math.random() * 7 + 3);
}
}
================================================
FILE: cim-client-sdk/src/test/java/com/crossoverjie/cim/client/sdk/ClientTest.java
================================================
package com.crossoverjie.cim.client.sdk;
import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.client.sdk.impl.ClientImpl;
import com.crossoverjie.cim.client.sdk.io.backoff.RandomBackoff;
import com.crossoverjie.cim.client.sdk.route.AbstractRouteBaseTest;
import com.crossoverjie.cim.common.constant.Constants;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import com.crossoverjie.cim.route.constant.Constant;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@Slf4j
public class ClientTest extends AbstractRouteBaseTest {
@AfterEach
public void tearDown() {
super.close();
}
@Test
public void groupChat() throws Exception {
super.starSingleServer();
super.startRoute(Constant.OfflineStoreMode.REDIS);
String routeUrl = "http://localhost:8083";
String cj = "crossoverJie";
String zs = "zs";
Long id = super.registerAccount(cj);
Long zsId = super.registerAccount(zs);
var auth1 = ClientConfigurationData.Auth.builder()
.userId(id)
.userName(cj)
.build();
var auth2 = ClientConfigurationData.Auth.builder()
.userId(zsId)
.userName(zs)
.build();
@Cleanup
Client client1 = Client.builder()
.auth(auth1)
.routeUrl(routeUrl)
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state = client1.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
Optional serverInfo = client1.getServerInfo();
Assertions.assertTrue(serverInfo.isPresent());
System.out.println("client1 serverInfo = " + serverInfo.get());
AtomicReference client2Receive = new AtomicReference<>();
@Cleanup
Client client2 = Client.builder()
.auth(auth2)
.routeUrl(routeUrl)
.messageListener((client, properties, message) -> {
client2Receive.set(message);
Assertions.assertEquals(properties.get(Constants.MetaKey.SEND_USER_ID), String.valueOf(auth1.getUserId()));
Assertions.assertEquals(properties.get(Constants.MetaKey.SEND_USER_NAME), auth1.getUserName());
})
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state2 = client2.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));
Optional serverInfo2 = client2.getServerInfo();
Assertions.assertTrue(serverInfo2.isPresent());
System.out.println("client2 serverInfo = " + serverInfo2.get());
// send msg
String msg = "hello";
client1.sendGroup(msg);
Set onlineUser = client1.getOnlineUser();
Assertions.assertEquals(onlineUser.size(), 2);
onlineUser.forEach(userInfo -> {
log.info("online user = {}", userInfo);
Long userId = userInfo.getUserId();
if (userId.equals(id)) {
Assertions.assertEquals(cj, userInfo.getUserName());
} else if (userId.equals(zsId)) {
Assertions.assertEquals(zs, userInfo.getUserName());
}
});
Awaitility.await().untilAsserted(
() -> Assertions.assertEquals(msg, client2Receive.get()));
super.stopSingle();
client1.close();
client2.close();
}
@Test
public void testP2PChat() throws Exception {
super.starSingleServer();
super.startRoute(Constant.OfflineStoreMode.REDIS);
String routeUrl = "http://localhost:8083";
String cj = "cj";
String zs = "zs";
String ls = "ls";
Long cjId = super.registerAccount(cj);
Long zsId = super.registerAccount(zs);
Long lsId = super.registerAccount(ls);
var auth1 = ClientConfigurationData.Auth.builder()
.userName(cj)
.userId(cjId)
.build();
var auth2 = ClientConfigurationData.Auth.builder()
.userName(zs)
.userId(zsId)
.build();
var auth3 = ClientConfigurationData.Auth.builder()
.userName(ls)
.userId(lsId)
.build();
var client1Receive = new ArrayList<>();
@Cleanup
Client client1 = Client.builder()
.auth(auth1)
.routeUrl(routeUrl)
.messageListener((__, properties, message) -> {
log.info("client1 receive message = {}", message);
client1Receive.add(message);
})
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state = client1.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
Optional serverInfo = client1.getServerInfo();
Assertions.assertTrue(serverInfo.isPresent());
System.out.println("client1 serverInfo = " + serverInfo.get());
// client2
AtomicReference client2Receive = new AtomicReference<>();
@Cleanup
Client client2 = Client.builder()
.auth(auth2)
.routeUrl(routeUrl)
.messageListener((client, properties, message) -> client2Receive.set(message))
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state2 = client2.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));
Optional serverInfo2 = client2.getServerInfo();
Assertions.assertTrue(serverInfo2.isPresent());
System.out.println("client2 serverInfo = " + serverInfo2.get());
// client3
AtomicReference client3Receive = new AtomicReference<>();
@Cleanup
Client client3 = Client.builder()
.auth(auth3)
.routeUrl(routeUrl)
.messageListener((client, properties, message) -> {
log.info("client3 receive message = {}", message);
client3Receive.set(message);
})
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state3 = client3.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));
Optional serverInfo3 = client3.getServerInfo();
Assertions.assertTrue(serverInfo3.isPresent());
System.out.println("client3 serverInfo = " + serverInfo3.get());
// client1 send msg to client3
String msg = "hello";
client1.sendP2P(P2PReqVO.builder()
.receiveUserId(lsId)
.msg(msg)
.build());
Set onlineUser = client1.getOnlineUser();
Assertions.assertEquals(onlineUser.size(), 3);
onlineUser.forEach(userInfo -> {
log.info("online user = {}", userInfo);
Long userId = userInfo.getUserId();
if (userId.equals(cjId)) {
Assertions.assertEquals(cj, userInfo.getUserName());
} else if (userId.equals(zsId)) {
Assertions.assertEquals(zs, userInfo.getUserName());
} else if (userId.equals(lsId)) {
Assertions.assertEquals(ls, userInfo.getUserName());
}
});
// client2 send batch msg to client1
var batchMsg = List.of("a","b","c");
client2.sendP2P(P2PReqVO.builder()
.receiveUserId(cjId)
.batchMsg(batchMsg)
.build());
Assertions.assertEquals(ClientImpl.getClientMap().size(), 3);
Awaitility.await().untilAsserted(
() -> Assertions.assertEquals(msg, client3Receive.get()));
Awaitility.await().untilAsserted(
() -> Assertions.assertNull(client2Receive.get()));
Awaitility.await().untilAsserted(
() -> Assertions.assertEquals(batchMsg, client1Receive));
super.stopSingle();
client1.close();
client2.close();
client3.close();
Assertions.assertEquals(ClientImpl.getClientMap().size(), 0);
}
/**
* 1. Start two servers.
* 2. Start two client, and send message.
* 3. Stop one server which is connected by client1.
* 4. Wait for client1 reconnect.
* 5. Send message again.
*
* @throws Exception
*/
@Test
public void testReconnect() throws Exception {
super.startTwoServer();
super.startRoute(Constant.OfflineStoreMode.REDIS);
String routeUrl = "http://localhost:8083";
String cj = "cj";
String zs = "zs";
Long cjId = super.registerAccount(cj);
Long zsId = super.registerAccount(zs);
var auth1 = ClientConfigurationData.Auth.builder()
.userName(cj)
.userId(cjId)
.build();
var auth2 = ClientConfigurationData.Auth.builder()
.userName(zs)
.userId(zsId)
.build();
var backoffStrategy = new RandomBackoff();
@Cleanup
Client client1 = Client.builder()
.auth(auth1)
.routeUrl(routeUrl)
.backoffStrategy(backoffStrategy)
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state = client1.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
AtomicReference client2Receive = new AtomicReference<>();
@Cleanup
Client client2 = Client.builder()
.auth(auth2)
.routeUrl(routeUrl)
.messageListener((client, properties, message) -> client2Receive.set(message))
.backoffStrategy(backoffStrategy)
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state2 = client2.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));
Optional serverInfo2 = client2.getServerInfo();
Assertions.assertTrue(serverInfo2.isPresent());
System.out.println("client2 serverInfo = " + serverInfo2.get());
// send msg
String msg = "hello";
client1.sendGroup(msg);
Awaitility.await()
.untilAsserted(() -> Assertions.assertEquals(msg, client2Receive.get()));
client2Receive.set("");
System.out.println("ready to restart server");
TimeUnit.SECONDS.sleep(3);
Optional serverInfo = client1.getServerInfo();
Assertions.assertTrue(serverInfo.isPresent());
System.out.println("server info = " + serverInfo.get());
super.stopServer(serverInfo.get().getCimServerPort());
System.out.println("stop server success! " + serverInfo.get());
// Waiting server stopped, and client reconnect.
TimeUnit.SECONDS.sleep(30);
System.out.println("reconnect state: " + client1.getState());
Awaitility.await().atMost(15, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
serverInfo = client1.getServerInfo();
Assertions.assertTrue(serverInfo.isPresent());
System.out.println("client1 reconnect server info = " + serverInfo.get());
// Send message again.
log.info("send message again, client2Receive = {}", client2Receive.get());
client1.sendGroup(msg);
Awaitility.await()
.untilAsserted(() -> Assertions.assertEquals(msg, client2Receive.get()));
super.stopTwoServer();
client1.close();
client2.close();
}
@Test
public void offLineAndOnline() throws Exception {
super.starSingleServer();
super.startRoute(Constant.OfflineStoreMode.REDIS);
String routeUrl = "http://localhost:8083";
String cj = "crossoverJie";
String zs = "zs";
Long id = super.registerAccount(cj);
Long zsId = super.registerAccount(zs);
var auth1 = ClientConfigurationData.Auth.builder()
.userId(id)
.userName(cj)
.build();
var auth2 = ClientConfigurationData.Auth.builder()
.userId(zsId)
.userName(zs)
.build();
@Cleanup
Client client1 = Client.builder()
.auth(auth1)
.routeUrl(routeUrl)
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state = client1.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
Optional serverInfo = client1.getServerInfo();
Assertions.assertTrue(serverInfo.isPresent());
System.out.println("client1 serverInfo = " + serverInfo.get());
AtomicReference client2Receive = new AtomicReference<>();
Client client2 = Client.builder()
.auth(auth2)
.routeUrl(routeUrl)
.messageListener((client, properties, message) -> client2Receive.set(message))
// Avoid auto reconnect, this test will manually close client.
.reconnectCheck((client) -> false)
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state2 = client2.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));
Optional serverInfo2 = client2.getServerInfo();
Assertions.assertTrue(serverInfo2.isPresent());
System.out.println("client2 serverInfo = " + serverInfo2.get());
// send msg
String msg = "hello";
client1.sendGroup(msg);
Awaitility.await().untilAsserted(
() -> Assertions.assertEquals(msg, client2Receive.get()));
client2Receive.set("");
// Manually offline
client2.close();
TimeUnit.SECONDS.sleep(10);
client2 = Client.builder()
.auth(auth2)
.routeUrl(routeUrl)
.messageListener((client, properties, message) -> client2Receive.set(message))
// Avoid to auto reconnect, this test will manually close client.
.reconnectCheck((client) -> false)
.build();
ClientState.State state3 = client2.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));
// send msg again
client1.sendGroup(msg);
Awaitility.await().untilAsserted(
() -> Assertions.assertEquals(msg, client2Receive.get()));
super.stopSingle();
client1.close();
client2.close();
}
@Test
public void testClose() throws Exception {
super.starSingleServer();
super.startRoute(Constant.OfflineStoreMode.REDIS);
String routeUrl = "http://localhost:8083";
String cj = "crossoverJie";
Long id = super.registerAccount(cj);
var auth1 = ClientConfigurationData.Auth.builder()
.userId(id)
.userName(cj)
.build();
Client client1 = Client.builder()
.auth(auth1)
.routeUrl(routeUrl)
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state = client1.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
Optional serverInfo = client1.getServerInfo();
Assertions.assertTrue(serverInfo.isPresent());
System.out.println("client1 serverInfo = " + serverInfo.get());
client1.close();
ClientState.State state1 = client1.getState();
Assertions.assertEquals(ClientState.State.Closed, state1);
super.stopSingle();
}
@Test
public void testIncorrectUser() throws Exception {
super.starSingleServer();
super.startRoute(Constant.OfflineStoreMode.REDIS);
String routeUrl = "http://localhost:8083";
String cj = "xx";
long id = 100L;
var auth1 = ClientConfigurationData.Auth.builder()
.userId(id)
.userName(cj)
.build();
Client client1 = Client.builder()
.auth(auth1)
.routeUrl(routeUrl)
.build();
TimeUnit.SECONDS.sleep(3);
Assertions.assertDoesNotThrow(client1::close);
super.stopSingle();
}
}
================================================
FILE: cim-client-sdk/src/test/java/com/crossoverjie/cim/client/sdk/OfflineMsgTest.java
================================================
package com.crossoverjie.cim.client.sdk;
import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.client.sdk.impl.ClientImpl;
import com.crossoverjie.cim.client.sdk.route.OfflineMsgStoreRouteBaseTest;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;
import com.crossoverjie.cim.route.constant.Constant;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.*;
import java.util.ArrayList;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class OfflineMsgTest extends OfflineMsgStoreRouteBaseTest {
@Test
public void testP2POfflineChatRedis() throws Exception {
super.starSingleServer();
super.startRoute(Constant.OfflineStoreMode.REDIS);
String routeUrl = "http://localhost:8083";
String cj = "cj";
String ls = "ls";
Long cjId = super.registerAccount(cj);
Long lsId = super.registerAccount(ls);
var auth1 = ClientConfigurationData.Auth.builder()
.userName(cj)
.userId(cjId)
.build();
var auth3 = ClientConfigurationData.Auth.builder()
.userName(ls)
.userId(lsId)
.build();
var client1Receive = new ArrayList<>();
Client client1 = Client.builder()
.auth(auth1)
.routeUrl(routeUrl)
.messageListener((__, properties, message) -> {
log.info("client1 receive message = {}", message);
client1Receive.add(message);
})
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state = client1.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
Optional serverInfo = client1.getServerInfo();
Assertions.assertTrue(serverInfo.isPresent());
System.out.println("client1 serverInfo = " + serverInfo.get());
// client3
AtomicReference client3Receive = new AtomicReference<>();
Client client3 = Client.builder()
.auth(auth3)
.routeUrl(routeUrl)
.messageListener((client, properties, message) -> {
log.info("client3 receive message = {}", message);
client3Receive.set(message);
})
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state3 = client3.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));
Optional serverInfo3 = client3.getServerInfo();
Assertions.assertTrue(serverInfo3.isPresent());
System.out.println("client3 serverInfo = " + serverInfo3.get());
// client1 send msg to client3
String msg = "hello";
client1.sendP2P(P2PReqVO.builder()
.receiveUserId(lsId)
.msg(msg)
.build());
Set onlineUser = client1.getOnlineUser();
Assertions.assertEquals(onlineUser.size(), 2);
onlineUser.forEach(userInfo -> {
log.info("online user = {}", userInfo);
Long userId = userInfo.getUserId();
if (userId.equals(cjId)) {
Assertions.assertEquals(cj, userInfo.getUserName());
} else if (userId.equals(lsId)) {
Assertions.assertEquals(ls, userInfo.getUserName());
}
});
Awaitility.await().untilAsserted(
() -> Assertions.assertEquals(msg, client3Receive.get()));
// Manually offline client3
client3.close();
client3Receive.set(null);
ClientState.State closeState = client3.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Closed, closeState));
// client1 send client3 an offline message
String offlineMsg = "offline message";
client1.sendP2P(P2PReqVO.builder()
.receiveUserId(lsId)
.msg(offlineMsg)
.build());
// online again
client3 = Client.builder()
.auth(auth3)
.routeUrl(routeUrl)
.messageListener((client, properties, message) -> {
log.info("client3 online again receive message = {}", message);
client3Receive.set(message);
})
.build();
ClientState.State client3AgainState = client3.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, client3AgainState));
// check offline message
Awaitility.await().untilAsserted(
() -> Assertions.assertEquals(offlineMsg, client3Receive.get()));
// close
client1.close();
client3.close();
super.close();
super.stopSingle();
Assertions.assertEquals(ClientImpl.getClientMap().size(), 0);
}
@Test
public void testP2POfflineChatMysql() throws Exception {
super.starSingleServer();
super.startRoute(Constant.OfflineStoreMode.MYSQL);
String routeUrl = "http://localhost:8083";
String cj = "cj";
String ls = "ls";
Long cjId = super.registerAccount(cj);
Long lsId = super.registerAccount(ls);
var auth1 = ClientConfigurationData.Auth.builder()
.userName(cj)
.userId(cjId)
.build();
var auth3 = ClientConfigurationData.Auth.builder()
.userName(ls)
.userId(lsId)
.build();
var client1Receive = new ArrayList<>();
Client client1 = Client.builder()
.auth(auth1)
.routeUrl(routeUrl)
.messageListener((__, properties, message) -> {
log.info("client1 receive message = {}", message);
client1Receive.add(message);
})
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state = client1.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
Optional serverInfo = client1.getServerInfo();
Assertions.assertTrue(serverInfo.isPresent());
System.out.println("client1 serverInfo = " + serverInfo.get());
// client3
AtomicReference client3Receive = new AtomicReference<>();
Client client3 = Client.builder()
.auth(auth3)
.routeUrl(routeUrl)
.messageListener((client, properties, message) -> {
log.info("client3 receive message = {}", message);
client3Receive.set(message);
})
.build();
TimeUnit.SECONDS.sleep(3);
ClientState.State state3 = client3.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));
Optional serverInfo3 = client3.getServerInfo();
Assertions.assertTrue(serverInfo3.isPresent());
System.out.println("client3 serverInfo = " + serverInfo3.get());
// client1 send msg to client3
String msg = "hello";
client1.sendP2P(P2PReqVO.builder()
.receiveUserId(lsId)
.msg(msg)
.build());
Set onlineUser = client1.getOnlineUser();
Assertions.assertEquals(onlineUser.size(), 2);
onlineUser.forEach(userInfo -> {
log.info("online user = {}", userInfo);
Long userId = userInfo.getUserId();
if (userId.equals(cjId)) {
Assertions.assertEquals(cj, userInfo.getUserName());
} else if (userId.equals(lsId)) {
Assertions.assertEquals(ls, userInfo.getUserName());
}
});
Awaitility.await().untilAsserted(
() -> Assertions.assertEquals(msg, client3Receive.get()));
// Manually offline client3
client3.close();
client3Receive.set(null);
ClientState.State closeState = client3.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Closed, closeState));
// client1 send client3 an offline message
String offlineMsg = "offline message";
client1.sendP2P(P2PReqVO.builder()
.receiveUserId(lsId)
.msg(offlineMsg)
.build());
// online again
client3 = Client.builder()
.auth(auth3)
.routeUrl(routeUrl)
.messageListener((client, properties, message) -> {
log.info("client3 online again receive message = {}", message);
client3Receive.set(message);
})
.build();
ClientState.State client3AgainState = client3.getState();
Awaitility.await().atMost(10, TimeUnit.SECONDS)
.untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, client3AgainState));
// check offline message
Awaitility.await().untilAsserted(
() -> Assertions.assertEquals(offlineMsg, client3Receive.get()));
// close
client1.close();
client3.close();
super.close();
super.stopSingle();
Assertions.assertEquals(ClientImpl.getClientMap().size(), 0);
}
}
================================================
FILE: cim-client-sdk/src/test/resources/application-route.yaml
================================================
spring:
application:
name:
cim-forward-route
data:
redis:
host: 127.0.0.1
port: 6379
jedis:
pool:
max-active: 100
max-idle: 100
max-wait: 1000
min-idle: 10
# web port
server:
port: 8083
logging:
level:
root: info
# enable swagger
springdoc:
swagger-ui:
enabled: true
app:
zk:
connect:
timeout: 30000
root: /route
# route strategy
#app.route.way=com.crossoverjie.cim.common.route.algorithm.loop.LoopHandle
# route strategy
#app.route.way=com.crossoverjie.cim.common.route.algorithm.random.RandomHandle
# route strategy
route:
way:
handler: com.crossoverjie.cim.common.route.algorithm.loop.LoopHandle
#app.route.way.consitenthash=com.crossoverjie.cim.common.route.algorithm.consistenthash.SortArrayMapConsistentHash
consitenthash: com.crossoverjie.cim.common.route.algorithm.consistenthash.TreeMapConsistentHash
offline:
store:
mode: redis
redis:
expire:
message-ttl-days: 3
================================================
FILE: cim-client-sdk/src/test/resources/init.sql
================================================
-- 创建表
CREATE TABLE IF NOT EXISTS `offline_msg`
(
`id`
BIGINT
PRIMARY
KEY
AUTO_INCREMENT,
`message_id`
BIGINT
NOT
NULL,
`receive_user_id`
BIGINT
NOT
NULL,
`content`
VARCHAR(2000),
`message_type` INT,
`status` TINYINT COMMENT '0: Pending, 1: Acked',
`created_at` DATETIME,
`properties` VARCHAR(2000),
INDEX `idx_receive_user_id`
(
`receive_user_id`
)
);
CREATE TABLE offline_msg_last_send_record
(
receive_user_id BIGINT NOT NULL PRIMARY KEY,
last_message_id BIGINT,
updated_at DATETIME
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
================================================
FILE: cim-common/pom.xml
================================================
cim
com.crossoverjie.netty
1.0.0-SNAPSHOT
4.0.0
1.0.0-SNAPSHOT
cim-common
org.projectlombok
lombok
org.slf4j
slf4j-api
com.google.protobuf
protobuf-java
com.github.xiaoymin
knife4j-openapi3-jakarta-spring-boot-starter
com.squareup.okhttp3
okhttp
com.github.ben-manes.caffeine
caffeine
com.101tec
zkclient
org.apache.curator
curator-recipes
org.apache.zookeeper
zookeeper
junit
junit
org.junit.vintage
junit-vintage-engine
test
org.junit.jupiter
junit-jupiter
test
io.netty
netty-all
${netty.version}
com.alibaba
fastjson
kr.motd.maven
os-maven-plugin
1.5.0.Final
org.xolstice.maven.plugins
protobuf-maven-plugin
0.5.1
com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}
grpc-java
io.grpc:protoc-gen-grpc-java:1.19.0:exe:${os.detected.classifier}
compile
compile-custom
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/constant/Constants.java
================================================
package com.crossoverjie.cim.common.constant;
/**
* Function:常量
*
* @author crossoverJie
* Date: 28/03/2018 17:41
* @since JDK 1.8
*/
public class Constants {
/**
* 服务端手动 push 次数
*/
public static final String COUNTER_SERVER_PUSH_COUNT = "counter.server.push.count";
/**
* 客户端手动 push 次数
*/
public static final String COUNTER_CLIENT_PUSH_COUNT = "counter.client.push.count";
public static class MetaKey {
public static final String SEND_USER_ID = "sendUserId";
public static final String SEND_USER_NAME = "sendUserName";
public static final String RECEIVE_USER_ID = "receiveUserId";
public static final String RECEIVE_USER_NAME = "receiveUserName";
}
//从数据库读取离线消息的每次获取量
public static final Integer FETCH_OFFLINE_MSG_LIMIT = 100;
public static final Integer OFFLINE_MSG_PENDING = 0;
public static final Integer OFFLINE_MSG_DELIVERED = 1;
public static final Integer MSG_TYPE_TEXT = 0;
public static final Integer MSG_TYPE_IMAGE = 1;
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/DynamicUrl.java
================================================
package com.crossoverjie.cim.common.core.proxy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author crossoverJie
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicUrl {
boolean useMethodEndpoint() default true;
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/Request.java
================================================
package com.crossoverjie.cim.common.core.proxy;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* @author crossoverJie
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Request {
String method() default POST;
String url() default "";
String GET = "GET";
String POST = "POST";
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/RpcProxyManager.java
================================================
package com.crossoverjie.cim.common.core.proxy;
import static com.crossoverjie.cim.common.enums.StatusEnum.VALIDATION_FAIL;
import com.alibaba.fastjson.JSONObject;
import com.crossoverjie.cim.common.exception.CIMException;
import com.crossoverjie.cim.common.util.HttpClient;
import com.crossoverjie.cim.common.util.StringUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.net.URI;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Response;
/**
* RpcProxyManager is a proxy manager for creating dynamic proxy instances of interfaces.
* It handles HTTP requests and responses using OkHttpClient.
*
* @param the type of the proxied interface
*/
@Slf4j
public final class RpcProxyManager {
private Class clazz;
private String url;
private OkHttpClient okHttpClient;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* Private constructor to initialize RpcProxyManager.
*
* @param clazz Proxied interface
* @param url Server provider URL
* @param okHttpClient HTTP client
*/
private RpcProxyManager(Class clazz, String url, OkHttpClient okHttpClient) {
this.clazz = clazz;
this.url = url;
this.okHttpClient = okHttpClient;
}
private RpcProxyManager(Class clazz, OkHttpClient okHttpClient) {
this(clazz, "", okHttpClient);
}
/**
* Default private constructor.
*/
private RpcProxyManager() {
}
/**
* Creates a proxy instance of the specified interface.
*
* @param clazz Proxied interface
* @param url Server provider URL
* @param okHttpClient HTTP client
* @param Type of the proxied interface
* @return Proxy instance of the specified interface
*/
public static T create(Class clazz, String url, OkHttpClient okHttpClient) {
return new RpcProxyManager<>(clazz, url, okHttpClient).getInstance();
}
public static T create(Class clazz, OkHttpClient okHttpClient) {
return new RpcProxyManager<>(clazz, okHttpClient).getInstance();
}
/**
* Gets the proxy instance of the API.
*
* @return Proxy instance of the API
*/
@SuppressWarnings("unchecked")
public T getInstance() {
return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{clazz},
new ProxyInvocation());
}
/**
* ProxyInvocation is an invocation handler for handling method calls on proxy instances.
*/
private class ProxyInvocation implements InvocationHandler {
/**
* Handles method calls on proxy instances.
*
* @param proxy The proxy instance
* @param method The method being called
* @param args The arguments of the method call
* @return The result of the method call
* @throws Throwable if an error occurs during method invocation
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Response result = null;
String serverUrl = url + "/" + method.getName();
Request annotation = method.getAnnotation(Request.class);
if (annotation != null && StringUtil.isNotEmpty(annotation.url())) {
serverUrl = url + "/" + annotation.url();
}
URI serverUri = new URI(serverUrl);
serverUrl = serverUri.normalize().toString();
Object para = null;
Class> parameterType = null;
for (int i = 0; i < method.getParameterAnnotations().length; i++) {
Annotation[] annotations = method.getParameterAnnotations()[i];
if (annotations.length == 0) {
para = args[i];
parameterType = method.getParameterTypes()[i];
}
for (Annotation ann : annotations) {
if (ann instanceof DynamicUrl) {
if (args[i] instanceof String) {
serverUrl = (String) args[i];
if (((DynamicUrl) ann).useMethodEndpoint()) {
serverUrl = serverUrl + "/" + method.getName();
}
break;
} else {
throw new CIMException("DynamicUrl must be String type");
}
}
}
}
try {
if (annotation != null && annotation.method().equals(Request.GET)) {
result = HttpClient.get(okHttpClient, serverUrl);
} else {
if (args == null || args.length > 2 || para == null || parameterType == null) {
throw new IllegalArgumentException(VALIDATION_FAIL.message());
}
JSONObject jsonObject = new JSONObject();
for (Field field : parameterType.getDeclaredFields()) {
field.setAccessible(true);
jsonObject.put(field.getName(), field.get(para));
}
result = HttpClient.post(okHttpClient, jsonObject.toString(), serverUrl);
}
if (method.getReturnType() == void.class) {
return null;
}
String json = result.body().string();
Type genericTypeOfBaseResponse = getGenericTypeOfBaseResponse(method);
if (genericTypeOfBaseResponse == null) {
return objectMapper.readValue(json, method.getReturnType());
} else {
return objectMapper.readValue(json, objectMapper.getTypeFactory()
.constructParametricType(method.getReturnType(),
objectMapper.getTypeFactory().constructType(genericTypeOfBaseResponse)));
}
} finally {
if (result != null) {
result.body().close();
}
}
}
}
private Type getGenericTypeOfBaseResponse(Method declaredMethod) {
Type returnType = declaredMethod.getGenericReturnType();
// check if the return type is a parameterized type
if (returnType instanceof ParameterizedType parameterizedType) {
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type typeArgument : actualTypeArguments) {
return typeArgument;
}
}
return null;
}
/**
* Gets the generic type of the BaseResponse.
*
* @param declaredMethod The method whose return type is being checked
* @return The generic type of the BaseResponse, or null if not found
* @throws ClassNotFoundException if the class of the generic type is not found
private Class> getBaseResponseGeneric(Method declaredMethod) throws ClassNotFoundException {
Type returnType = declaredMethod.getGenericReturnType();
// check if the return type is a parameterized type
if (returnType instanceof ParameterizedType parameterizedType) {
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type typeArgument : actualTypeArguments) {
// BaseResponse only has one generic type
return getClass(typeArgument);
}
}
return null;
}
public static Class> getClass(Type type) throws ClassNotFoundException {
if (type instanceof Class>) {
// 普通类型,直接返回
return (Class>) type;
} else if (type instanceof ParameterizedType) {
// 参数化类型,返回原始类型
return getClass(((ParameterizedType) type).getRawType());
} else if (type instanceof TypeVariable>) {
// 类型变量,无法在运行时获取具体类型
return Object.class;
} else {
throw new ClassNotFoundException("无法处理的类型: " + type.toString());
}
}*/
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/RingBufferWheel.java
================================================
package com.crossoverjie.cim.common.data.construct;
import lombok.extern.slf4j.Slf4j;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Function:Ring Queue, it can be used to delay task.
*
* @author crossoverJie
* Date: 2019-09-20 14:46
* @since JDK 1.8
*/
@Slf4j
public final class RingBufferWheel {
/**
* default ring buffer size
*/
private static final int STATIC_RING_SIZE = 64;
private Object[] ringBuffer;
private int bufferSize;
/**
* business thread pool
*/
private ExecutorService executorService;
private volatile int size = 0;
/***
* task stop sign
*/
private volatile boolean stop = false;
/**
* task start sign
*/
private volatile AtomicBoolean start = new AtomicBoolean(false);
/**
* total tick times
*/
private AtomicInteger tick = new AtomicInteger();
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private AtomicInteger taskId = new AtomicInteger();
private Map taskMap = new ConcurrentHashMap<>(16);
/**
* Create a new delay task ring buffer by default size
*
* @param executorService the business thread pool
*/
public RingBufferWheel(ExecutorService executorService) {
this.executorService = executorService;
this.bufferSize = STATIC_RING_SIZE;
this.ringBuffer = new Object[bufferSize];
}
/**
* Create a new delay task ring buffer by custom buffer size
*
* @param executorService the business thread pool
* @param bufferSize custom buffer size
*/
public RingBufferWheel(ExecutorService executorService, int bufferSize) {
this(executorService);
if (!powerOf2(bufferSize)) {
throw new RuntimeException("bufferSize=[" + bufferSize + "] must be a power of 2");
}
this.bufferSize = bufferSize;
this.ringBuffer = new Object[bufferSize];
}
/**
* Add a task into the ring buffer(thread safe)
*
* @param task business task extends {@link Task}
*/
public int addTask(Task task) {
int key = task.getKey();
int id;
try {
lock.lock();
int index = mod(key, bufferSize);
task.setIndex(index);
Set tasks = get(index);
int cycleNum = cycleNum(key, bufferSize);
if (tasks != null) {
task.setCycleNum(cycleNum);
tasks.add(task);
} else {
task.setIndex(index);
task.setCycleNum(cycleNum);
Set sets = new HashSet<>();
sets.add(task);
put(key, sets);
}
id = taskId.incrementAndGet();
task.setTaskId(id);
taskMap.put(id, task);
size++;
} finally {
lock.unlock();
}
start();
return id;
}
/**
* Cancel task by taskId
* @param id unique id through {@link #addTask(Task)}
* @return
*/
public boolean cancel(int id) {
boolean flag = false;
Set tempTask = new HashSet<>();
try {
lock.lock();
Task task = taskMap.get(id);
if (task == null) {
return false;
}
Set tasks = get(task.getIndex());
for (Task tk : tasks) {
if (tk.getKey() == task.getKey() && tk.getCycleNum() == task.getCycleNum()) {
size--;
flag = true;
taskMap.remove(id);
} else {
tempTask.add(tk);
}
}
//update origin data
ringBuffer[task.getIndex()] = tempTask;
} finally {
lock.unlock();
}
return flag;
}
/**
* Thread safe
*
* @return the size of ring buffer
*/
public int taskSize() {
return size;
}
/**
* Same with method {@link #taskSize}
* @return
*/
public int taskMapSize() {
return taskMap.size();
}
/**
* Start background thread to consumer wheel timer, it will always run until you call method {@link #stop}
*/
public void start() {
if (!start.get()) {
if (start.compareAndSet(start.get(), true)) {
log.info("Delay task is starting");
Thread job = new Thread(new TriggerJob());
job.setName("consumer RingBuffer thread");
job.start();
start.set(true);
}
}
}
/**
* Stop consumer ring buffer thread
*
* @param force True will force close consumer thread and discard all pending tasks
* otherwise the consumer thread waits for all tasks to completes before closing.
*/
public void stop(boolean force) {
if (force) {
log.info("Delay task is forced stop");
stop = true;
executorService.shutdownNow();
} else {
log.info("Delay task is stopping");
if (taskSize() > 0) {
try {
lock.lock();
condition.await();
stop = true;
} catch (InterruptedException e) {
log.error("InterruptedException", e);
} finally {
lock.unlock();
}
}
executorService.shutdown();
}
}
private Set get(int index) {
return (Set) ringBuffer[index];
}
private void put(int key, Set tasks) {
int index = mod(key, bufferSize);
ringBuffer[index] = tasks;
}
/**
* Remove and get task list.
* @param key
* @return task list
*/
private Set remove(int key) {
Set tempTask = new HashSet<>();
Set result = new HashSet<>();
Set tasks = (Set) ringBuffer[key];
if (tasks == null) {
return result;
}
for (Task task : tasks) {
if (task.getCycleNum() == 0) {
result.add(task);
size2Notify();
} else {
// decrement 1 cycle number and update origin data
task.setCycleNum(task.getCycleNum() - 1);
tempTask.add(task);
}
// remove task, and free the memory.
taskMap.remove(task.getTaskId());
}
//update origin data
ringBuffer[key] = tempTask;
return result;
}
private void size2Notify() {
try {
lock.lock();
size--;
if (size == 0) {
condition.signal();
}
} finally {
lock.unlock();
}
}
private boolean powerOf2(int target) {
if (target < 0) {
return false;
}
int value = target & (target - 1);
if (value != 0) {
return false;
}
return true;
}
private int mod(int target, int mod) {
// equals target % mod
target = target + tick.get();
return target & (mod - 1);
}
private int cycleNum(int target, int mod) {
//equals target/mod
return target >> Integer.bitCount(mod - 1);
}
/**
* An abstract class used to implement business.
*/
public abstract static class Task extends Thread {
private int index;
private int cycleNum;
private int key;
/**
* The unique ID of the task
*/
private int taskId;
@Override
public void run() {
}
public int getKey() {
return key;
}
/**
*
* @param key Delay time(seconds)
*/
public void setKey(int key) {
this.key = key;
}
public int getCycleNum() {
return cycleNum;
}
private void setCycleNum(int cycleNum) {
this.cycleNum = cycleNum;
}
public int getIndex() {
return index;
}
private void setIndex(int index) {
this.index = index;
}
public int getTaskId() {
return taskId;
}
public void setTaskId(int taskId) {
this.taskId = taskId;
}
}
private class TriggerJob implements Runnable {
@Override
public void run() {
int index = 0;
while (!stop) {
try {
Set tasks = remove(index);
for (Task task : tasks) {
executorService.submit(task);
}
if (++index > bufferSize - 1) {
index = 0;
}
//Total tick number of records
tick.incrementAndGet();
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
log.error("Exception", e);
}
}
log.info("Delay task has stopped");
}
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/SortArrayMap.java
================================================
package com.crossoverjie.cim.common.data.construct;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.apache.curator.shaded.com.google.common.collect.Sets;
/**
* Function:根据 key 排序的 Map
*
* @author crossoverJie
* Date: 2019-02-25 18:17
* @since JDK 1.8
*/
public class SortArrayMap extends AbstractMap {
/**
* 核心数组
*/
private Node[] buckets;
private static final int DEFAULT_SIZE = 10;
/**
* 数组大小
*/
private int size = 0;
public SortArrayMap() {
buckets = new Node[DEFAULT_SIZE];
}
/**
* 写入数据
* @param key
* @param value
*/
public void add(Long key, String value) {
checkSize(size + 1);
Node node = new Node(key, value);
buckets[size++] = node;
}
public SortArrayMap remove(String value) {
List list = new ArrayList<>(Arrays.asList(buckets));
list.removeIf(next -> next != null && next.value.equals(value));
buckets = list.toArray(new Node[0]);
return this;
}
/**
* 校验是否需要扩容
* @param size
*/
private void checkSize(int size) {
if (size >= buckets.length) {
//扩容自身的 3/2
int oldLen = buckets.length;
int newLen = oldLen + (oldLen >> 1);
buckets = Arrays.copyOf(buckets, newLen);
}
}
/**
* 顺时针取出数据
* @param key
* @return
*/
public String firstNodeValue(long key) {
if (size == 0) {
return null;
}
for (Node bucket : buckets) {
if (bucket == null) {
break;
}
if (bucket.key >= key) {
return bucket.value;
}
}
return buckets[0].value;
}
/**
* 排序
*/
public void sort() {
Arrays.sort(buckets, 0, size, (o1, o2) -> {
if (o1.key > o2.key) {
return 1;
} else {
return 0;
}
});
}
public void print() {
for (Node bucket : buckets) {
if (bucket == null) {
continue;
}
System.out.println(bucket.toString());
}
}
@Override
public int size() {
return size;
}
@Override
public void clear() {
buckets = new Node[DEFAULT_SIZE];
size = 0;
}
@Override
public Set> entrySet() {
Set> set = Sets.newHashSet();
for (Node bucket : buckets) {
set.add(new SimpleEntry<>(String.valueOf(bucket.key), bucket.value));
}
return set;
}
@Override
public Set keySet() {
Set set = Sets.newHashSet();
for (Node bucket : buckets) {
if (bucket == null) {
continue;
}
set.add(bucket.value);
}
return set;
}
/**
* 数据节点
*/
private class Node {
public Long key;
public String value;
public Node(Long key, String value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"key=" + key +
", value='" + value + '\'' +
'}';
}
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/TrieTree.java
================================================
package com.crossoverjie.cim.common.data.construct;
import com.crossoverjie.cim.common.util.StringUtil;
import java.util.ArrayList;
import java.util.List;
/**
* Function:字典树字符前缀模糊匹配
*
* @author crossoverJie
* Date: 2019/1/7 18:58
* @since JDK 1.8
*/
public class TrieTree {
/**
* 大小写都可保存
*/
private static final int CHILDREN_LENGTH = 26 * 2;
/**
* 存放的最大字符串长度
*/
private static final int MAX_CHAR_LENGTH = 16;
private static final char UPPERCASE_STAR = 'A';
/**
* 小写就要 -71
*/
private static final char LOWERCASE_STAR = 'G';
private Node root;
public TrieTree() {
root = new Node();
}
/**
* 写入
*
* @param data
*/
public void insert(String data) {
this.insert(this.root, data);
}
private void insert(Node root, String data) {
char[] chars = data.toCharArray();
for (int i = 0; i < chars.length; i++) {
char aChar = chars[i];
int index;
if (Character.isUpperCase(aChar)) {
index = aChar - UPPERCASE_STAR;
} else {
//小写就要 -71
index = aChar - LOWERCASE_STAR;
}
if (index >= 0 && index < CHILDREN_LENGTH) {
if (root.children[index] == null) {
Node node = new Node();
root.children[index] = node;
root.children[index].data = chars[i];
}
//最后一个字符设置标志
if (i + 1 == chars.length) {
root.children[index].isEnd = true;
}
//指向下一节点
root = root.children[index];
}
}
}
/**
* 递归深度遍历
*
* @param key
* @return
*/
public List prefixSearch(String key) {
List value = new ArrayList();
if (StringUtil.isEmpty(key)) {
return value;
}
char k = key.charAt(0);
int index;
if (Character.isUpperCase(k)) {
index = k - UPPERCASE_STAR;
} else {
index = k - LOWERCASE_STAR;
}
if (root.children != null && root.children[index] != null) {
return query(root.children[index], value,
key.substring(1), String.valueOf(k));
}
return value;
}
private List query(Node child, List value, String key, String result) {
if (child.isEnd && key == null) {
value.add(result);
}
if (StringUtil.isNotEmpty(key)) {
char ca = key.charAt(0);
int index;
if (Character.isUpperCase(ca)) {
index = ca - UPPERCASE_STAR;
} else {
index = ca - LOWERCASE_STAR;
}
if (child.children[index] != null) {
query(child.children[index], value, key.substring(1).equals("") ? null : key.substring(1), result + ca);
}
} else {
for (int i = 0; i < CHILDREN_LENGTH; i++) {
if (child.children[i] == null) {
continue;
}
int j;
if (Character.isUpperCase(child.children[i].data)) {
j = UPPERCASE_STAR + i;
} else {
j = LOWERCASE_STAR + i;
}
char temp = (char) j;
query(child.children[i], value, null, result + temp);
}
}
return value;
}
/**
* 查询所有
*
* @return
*/
public List all() {
char[] chars = new char[MAX_CHAR_LENGTH];
List value = depth(this.root, new ArrayList(), chars, 0);
return value;
}
public List depth(Node node, List list, char[] chars, int index) {
if (node.children == null || node.children.length == 0) {
return list;
}
Node[] children = node.children;
for (int i = 0; i < children.length; i++) {
Node child = children[i];
if (child == null) {
continue;
}
if (child.isEnd) {
chars[index] = child.data;
char[] temp = new char[index + 1];
for (int j = 0; j < chars.length; j++) {
if (chars[j] == 0) {
continue;
}
temp[j] = chars[j];
}
list.add(String.valueOf(temp));
return list;
} else {
chars[index] = child.data;
index++;
depth(child, list, chars, index);
index = 0;
}
}
return list;
}
/**
* 字典树节点
*/
private class Node {
/**
* 是否为最后一个字符
*/
public boolean isEnd = false;
/**
* 如果只是查询,则不需要存储数据
*/
public char data;
public Node[] children = new Node[CHILDREN_LENGTH];
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/enums/StatusEnum.java
================================================
package com.crossoverjie.cim.common.enums;
import java.util.ArrayList;
import java.util.List;
/**
* @author crossoverJie
*/
public enum StatusEnum {
/** 成功 */
SUCCESS("9000", "Success"),
/** 成功 */
FALLBACK("8000", "FALL_BACK"),
/** 参数校验失败**/
VALIDATION_FAIL("3000", "invalid argument"),
/** 失败 */
FAIL("4000", "Failure"),
/** 重复登录 */
REPEAT_LOGIN("5000", "Repeat login, log out an account please!"),
/** 请求限流 */
REQUEST_LIMIT("6000", "请求限流"),
/** 账号不在线 */
OFF_LINE("7000", "You selected user is offline!, please try again later!"),
SERVER_NOT_AVAILABLE("7100", "cim server is not available, please try again later!"),
RECONNECT_FAIL("7200", "Reconnect fail, continue to retry!"),
/** 登录信息不匹配 */
ACCOUNT_NOT_MATCH("9100", "The User information you have used is incorrect!"),
OFFLINE_MESSAGE_STORAGE_ERROR("9200", "Offline message storage error!"),
OFFLINE_MESSAGE_FETCH_ERROR("9201", "Offline message fetch error!"),
OFFLINE_MESSAGE_DELETE_ERROR("9202", "Offline message delete error!");
/** 枚举值码 */
private final String code;
/** 枚举描述 */
private final String message;
/**
* 构建一个 StatusEnum 。
* @param code 枚举值码。
* @param message 枚举描述。
*/
private StatusEnum(String code, String message) {
this.code = code;
this.message = message;
}
/**
* 得到枚举值码。
* @return 枚举值码。
*/
public String getCode() {
return code;
}
/**
* 得到枚举描述。
* @return 枚举描述。
*/
public String getMessage() {
return message;
}
/**
* 得到枚举值码。
* @return 枚举值码。
*/
public String code() {
return code;
}
/**
* 得到枚举描述。
* @return 枚举描述。
*/
public String message() {
return message;
}
/**
* 通过枚举值码查找枚举值。
* @param code 查找枚举值的枚举值码。
* @return 枚举值码对应的枚举值。
* @throws IllegalArgumentException 如果 code 没有对应的 StatusEnum 。
*/
public static StatusEnum findStatus(String code) {
for (StatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
throw new IllegalArgumentException("ResultInfo StatusEnum not legal:" + code);
}
/**
* 获取全部枚举值。
*
* @return 全部枚举值。
*/
public static List getAllStatus() {
List list = new ArrayList();
for (StatusEnum status : values()) {
list.add(status);
}
return list;
}
/**
* 获取全部枚举值码。
*
* @return 全部枚举值码。
*/
public static List getAllStatusCode() {
List list = new ArrayList();
for (StatusEnum status : values()) {
list.add(status.code());
}
return list;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/enums/SystemCommandEnum.java
================================================
package com.crossoverjie.cim.common.enums;
import java.util.HashMap;
import java.util.Map;
/**
* Function:
*
* @author crossoverJie
* Date: 2018/12/26 18:38
* @since JDK 1.8
*/
public enum SystemCommandEnum {
ALL(":all ", "获取所有命令", "PrintAllCommand"),
ONLINE_USER(":olu ", "获取所有在线用户", "PrintOnlineUsersCommand"),
QUIT(":q! ", "退出程序", "ShutDownCommand"),
QUERY(":q ", "【:q 关键字】查询聊天记录", "QueryHistoryCommand"),
AI(":ai ", "开启 AI 模式", "OpenAIModelCommand"),
QAI(":qai ", "关闭 AI 模式", "CloseAIModelCommand"),
PREFIX(":pu ", "模糊匹配用户", "PrefixSearchCommand"),
EMOJI(":emoji ", "emoji 表情列表", "EmojiCommand"),
INFO(":info ", "获取客户端信息", "EchoInfoCommand"),
DELAY_MSG(":delay ", "delay message, :delay [msg] [delayTime]", "DelayMsgCommand");
/** 枚举值码 */
private final String commandType;
/** 枚举描述 */
private final String desc;
/**
* 实现类
*/
private final String clazz;
/**
* 构建一个 。
* @param commandType 枚举值码。
* @param desc 枚举描述。
*/
private SystemCommandEnum(String commandType, String desc, String clazz) {
this.commandType = commandType;
this.desc = desc;
this.clazz = clazz;
}
/**
* 得到枚举值码。
* @return 枚举值码。
*/
public String getCommandType() {
return commandType;
}
/**
* 获取 class。
* @return class。
*/
public String getClazz() {
return clazz;
}
/**
* 得到枚举描述。
* @return 枚举描述。
*/
public String getDesc() {
return desc;
}
/**
* 得到枚举值码。
* @return 枚举值码。
*/
public String code() {
return commandType;
}
/**
* 得到枚举描述。
* @return 枚举描述。
*/
public String message() {
return desc;
}
/**
* 获取全部枚举值码。
*
* @return 全部枚举值码。
*/
public static Map getAllStatusCode() {
Map map = new HashMap(16);
for (SystemCommandEnum status : values()) {
map.put(status.getCommandType(), status.getDesc());
}
return map;
}
public static Map getAllClazz() {
Map map = new HashMap(16);
for (SystemCommandEnum status : values()) {
map.put(status.getCommandType().trim(), "com.crossoverjie.cim.client.service.impl.command." + status.getClazz());
}
return map;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/exception/CIMException.java
================================================
package com.crossoverjie.cim.common.exception;
import com.crossoverjie.cim.common.enums.StatusEnum;
/**
* Function:
*
* @author crossoverJie
* Date: 2018/8/25 15:26
* @since JDK 1.8
*/
public class CIMException extends GenericException {
public CIMException(String errorCode, String errorMessage) {
super(errorMessage);
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
public CIMException(Exception e, String errorCode, String errorMessage) {
super(e, errorMessage);
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
public CIMException(String message) {
super(message);
this.errorMessage = message;
}
public CIMException(StatusEnum statusEnum) {
super(statusEnum.getMessage());
this.errorMessage = statusEnum.message();
this.errorCode = statusEnum.getCode();
}
public CIMException(StatusEnum statusEnum, String message) {
super(message);
this.errorMessage = message;
this.errorCode = statusEnum.getCode();
}
public CIMException(Exception oriEx) {
super(oriEx);
}
public CIMException(Throwable oriEx) {
super(oriEx);
}
public CIMException(String message, Exception oriEx) {
super(message, oriEx);
this.errorMessage = message;
}
public CIMException(String message, Throwable oriEx) {
super(message, oriEx);
this.errorMessage = message;
}
public static boolean isResetByPeer(String msg) {
if ("Connection reset by peer".equals(msg)) {
return true;
}
return false;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/exception/GenericException.java
================================================
package com.crossoverjie.cim.common.exception;
import java.io.Serializable;
/**
* Function:
*
* @author crossoverJie
* Date: 2018/8/25 15:27
* @since JDK 1.8
*/
public class GenericException extends RuntimeException implements Serializable {
private static final long serialVersionUID = 1L;
String errorCode;
String errorMessage;
public GenericException() {
}
public GenericException(String message) {
super(message);
}
public GenericException(Exception oriEx) {
super(oriEx);
}
public GenericException(Exception oriEx, String message) {
super(message, oriEx);
}
public GenericException(Throwable oriEx) {
super(oriEx);
}
public GenericException(String message, Exception oriEx) {
super(message, oriEx);
}
public GenericException(String message, Throwable oriEx) {
super(message, oriEx);
}
public String getErrorCode() {
return this.errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return this.errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/kit/HeartBeatHandler.java
================================================
package com.crossoverjie.cim.common.kit;
import io.netty.channel.ChannelHandlerContext;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-01-20 17:15
* @since JDK 1.8
*/
public interface HeartBeatHandler {
/**
* 处理心跳
* @param ctx
* @throws Exception
*/
void process(ChannelHandlerContext ctx) throws Exception;
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/metastore/AbstractConfiguration.java
================================================
package com.crossoverjie.cim.common.metastore;
import lombok.Builder;
import lombok.Data;
/**
* @author crossverJie
*/
@Data
@Builder
public class AbstractConfiguration {
private String metaServiceUri;
private int timeoutMs;
private RETRY retryPolicy;
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/metastore/MetaStore.java
================================================
package com.crossoverjie.cim.common.metastore;
import java.util.List;
import java.util.Set;
/**
* @author crossoverJie
*/
public interface MetaStore {
void initialize(AbstractConfiguration> configuration) throws Exception;
/**
* Get available server list
* @return available server list
* @throws Exception exception
*/
Set getAvailableServerList() throws Exception;
/**
* Add server to meta store
* @throws Exception exception
*/
void addServer(String ip, int cimServerPort, int httpPort) throws Exception;
/**
* Subscribe server list
* @param childListener child listener
* @throws Exception exception
*/
void listenServerList(ChildListener childListener) throws Exception;
/**
* @throws Exception
*/
void rebuildCache() throws Exception;
interface ChildListener {
/**
* Child changed
* @param parentPath parent path(eg. for zookeeper: [/cim])
* @param currentChildren current children
* @throws Exception exception
*/
void childChanged(String parentPath, List currentChildren) throws Exception;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/metastore/ZkConfiguration.java
================================================
package com.crossoverjie.cim.common.metastore;
import org.apache.curator.RetryPolicy;
/**
* @author crossoverJie
*/
public class ZkConfiguration extends AbstractConfiguration {
ZkConfiguration(String metaServiceUri, int timeout, RetryPolicy retryPolicy) {
super(metaServiceUri, timeout, retryPolicy);
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/metastore/ZkMetaStoreImpl.java
================================================
package com.crossoverjie.cim.common.metastore;
import com.crossoverjie.cim.common.pojo.RouteInfo;
import com.crossoverjie.cim.common.util.RouteInfoParseUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.I0Itec.zkclient.ZkClient;
import org.apache.curator.framework.CuratorFramework;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.Watcher;
/**
* @author crossovreJie
*/
@Slf4j
public class ZkMetaStoreImpl implements MetaStore {
public static final String ROOT = "/cim";
private ZkClient client;
Cache cache;
@Override
public void initialize(AbstractConfiguration> configuration) throws Exception {
cache = Caffeine.newBuilder().build();
client = new ZkClient(configuration.getMetaServiceUri(), configuration.getTimeoutMs());
}
@Override
public Set getAvailableServerList() throws Exception {
if (cache.asMap().size() > 0) {
return cache.asMap().keySet();
}
List coll = client.getChildren(ROOT);
Map voidMap = coll.stream().collect(Collectors.toMap(
Function.identity(),
Function.identity()
));
cache.putAll(voidMap);
return voidMap.keySet();
}
@Override
public void addServer(String ip, int cimServerPort, int httpPort) throws Exception {
boolean exists = client.exists(ROOT);
if (!exists) {
client.createPersistent(ROOT);
}
String zkParse = RouteInfoParseUtil.parse(RouteInfo.builder()
.ip(ip)
.cimServerPort(cimServerPort)
.httpPort(httpPort)
.build());
String serverPath = String.format("%s/%s", ROOT, zkParse);
client.createEphemeral(serverPath);
log.info("Add server to zk [{}]", serverPath);
}
@Override
public void listenServerList(ChildListener childListener) throws Exception {
client.subscribeChildChanges(ROOT, (parentPath, currentChildren) -> {
log.info("Clear and update local cache parentPath=[{}],current server list=[{}]", parentPath, currentChildren.toString());
childListener.childChanged(parentPath, currentChildren);
// TODO: 2024/8/19 maybe can reuse currentChildren.
// Because rebuildCache() will re-fetch the server list from zk.
rebuildCache();
});
}
@Override
public synchronized void rebuildCache() throws Exception {
cache.invalidateAll();
this.getAvailableServerList();
}
private List watchedGetChildren(CuratorFramework client, String path) throws Exception {
/**
* Get children and set a watcher on the node. The watcher notification will come through the
* CuratorListener (see setDataAsync() above).
*/
return client.getChildren().watched().forPath(path);
}
private void createEphemeral(CuratorFramework client, String path, byte[] payload) throws Exception {
// this will create the given EPHEMERAL ZNode with the given data
client.create().withMode(CreateMode.EPHEMERAL).forPath(path, payload);
}
private void create(CuratorFramework client, String path, byte[] payload) throws Exception {
// this will create the given ZNode with the given data
client.create().forPath(path, payload);
}
private void watchedGetChildren(CuratorFramework client, String path, Watcher watcher)
throws Exception {
// Get children and set the given watcher on the node.
client.getChildren().usingWatcher(watcher).forPath(path);
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/pojo/CIMUserInfo.java
================================================
package com.crossoverjie.cim.common.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Function: 用户信息
*
* @author crossoverJie
* Date: 2018/12/24 02:33
* @since JDK 1.8
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CIMUserInfo {
private Long userId;
private String userName;
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/pojo/RouteInfo.java
================================================
package com.crossoverjie.cim.common.pojo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
/**
* Function:
*
* @author crossoverJie
* Date: 2020-04-12 20:48
* @since JDK 1.8
*/
@Data
@AllArgsConstructor
@Builder
public final class RouteInfo {
private String ip;
private Integer cimServerPort;
private Integer httpPort;
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/req/BaseRequest.java
================================================
package com.crossoverjie.cim.common.req;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Function:
* @author crossoverJie
* Date: 2017/6/7 下午11:28
* @since JDK 1.8
*/
public class BaseRequest {
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "reqNo", example = "1234567890")
private String reqNo;
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "timestamp", example = "0")
private int timeStamp;
public BaseRequest() {
this.setTimeStamp((int)(System.currentTimeMillis() / 1000));
}
public String getReqNo() {
return reqNo;
}
public void setReqNo(String reqNo) {
this.reqNo = reqNo;
}
public int getTimeStamp() {
return timeStamp;
}
public void setTimeStamp(int timeStamp) {
this.timeStamp = timeStamp;
}
@Override
public String toString() {
return "BaseRequest{" +
"reqNo='" + reqNo + '\'' +
", timeStamp=" + timeStamp +
'}';
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/res/BaseResponse.java
================================================
package com.crossoverjie.cim.common.res;
import com.crossoverjie.cim.common.enums.StatusEnum;
import com.crossoverjie.cim.common.util.StringUtil;
import java.io.Serializable;
public class BaseResponse implements Serializable {
private String code;
private String message;
/**
* 请求号
*/
private String reqNo;
private T dataBody;
public BaseResponse() {}
public BaseResponse(T dataBody) {
this.dataBody = dataBody;
}
public BaseResponse(String code, String message) {
this.code = code;
this.message = message;
}
public BaseResponse(String code, String message, T dataBody) {
this.code = code;
this.message = message;
this.dataBody = dataBody;
}
public BaseResponse(String code, String message, String reqNo, T dataBody) {
this.code = code;
this.message = message;
this.reqNo = reqNo;
this.dataBody = dataBody;
}
public static BaseResponse create(T t) {
return new BaseResponse(t);
}
public static BaseResponse create(T t, StatusEnum statusEnum) {
return new BaseResponse(statusEnum.getCode(), statusEnum.getMessage(), t);
}
public static BaseResponse createSuccess(T t, String message) {
String msg = StringUtil.isNullOrEmpty(message) ? StatusEnum.SUCCESS.getMessage() : message;
return new BaseResponse(StatusEnum.SUCCESS.getCode(), msg, t);
}
public static BaseResponse createFail(T t, String message) {
return new BaseResponse(StatusEnum.FAIL.getCode(), StringUtil.isNullOrEmpty(message) ? StatusEnum.FAIL.getMessage() : message, t);
}
public static BaseResponse create(T t, StatusEnum statusEnum, String message) {
return new BaseResponse(statusEnum.getCode(), message, t);
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getDataBody() {
return dataBody;
}
public void setDataBody(T dataBody) {
this.dataBody = dataBody;
}
public String getReqNo() {
return reqNo;
}
public void setReqNo(String reqNo) {
this.reqNo = reqNo;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/res/NULLBody.java
================================================
package com.crossoverjie.cim.common.res;
/**
* Function:空对象,用在泛型中,表示没有额外的请求参数或者返回参数
*
* @author crossoverJie
* Date: 2017/6/7 下午11:57
* @since JDK 1.8
*/
public class NULLBody {
public NULLBody() {}
public static NULLBody create() {
return new NULLBody();
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/RouteHandle.java
================================================
package com.crossoverjie.cim.common.route.algorithm;
import com.crossoverjie.cim.common.pojo.RouteInfo;
import java.util.List;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-02-27 00:31
* @since JDK 1.8
*/
public interface RouteHandle {
/**
* 再一批服务器里进行路由
* @param values
* @param key
* @return
*/
// TODO: 2024/9/13 Use List instead of List to make the code more type-safe
String routeServer(List values, String key);
List removeExpireServer(RouteInfo routeInfo);
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/AbstractConsistentHash.java
================================================
package com.crossoverjie.cim.common.route.algorithm.consistenthash;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
/**
* Function:一致性 hash 算法抽象类
*
* @author crossoverJie
* Date: 2019-02-27 00:35
* @since JDK 1.8
*/
public abstract class AbstractConsistentHash {
/**
* 新增节点
* @param key
* @param value
*/
protected abstract void add(long key, String value);
/**
* remove node
* @param value node
* @return current data
*/
protected abstract Map remove(String value);
/**
* Clear old data in the structure
*/
protected abstract void clear();
/**
* 排序节点,数据结构自身支持排序可以不用重写
*/
protected void sort() { }
/**
* 根据当前的 key 通过一致性 hash 算法的规则取出一个节点
* @param value
* @return
*/
protected abstract String getFirstNodeValue(String value);
/**
* 传入节点列表以及客户端信息获取一个服务节点
* @param values
* @param key
* @return
*/
public String process(List values, String key) {
// fix https://github.com/crossoverJie/cim/issues/79
clear();
for (String value : values) {
add(hash(value), value);
}
sort();
return getFirstNodeValue(key);
}
/**
* hash 运算
* @param value
* @return
*/
public Long hash(String value) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 not supported", e);
}
md5.reset();
byte[] keyBytes = null;
try {
keyBytes = value.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unknown string :" + value, e);
}
md5.update(keyBytes);
byte[] digest = md5.digest();
// hash code, Truncate to 32-bits
long hashCode = ((long) (digest[3] & 0xFF) << 24)
| ((long) (digest[2] & 0xFF) << 16)
| ((long) (digest[1] & 0xFF) << 8)
| (digest[0] & 0xFF);
long truncateHashCode = hashCode & 0xffffffffL;
return truncateHashCode;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/ConsistentHashHandle.java
================================================
package com.crossoverjie.cim.common.route.algorithm.consistenthash;
import com.crossoverjie.cim.common.pojo.RouteInfo;
import com.crossoverjie.cim.common.route.algorithm.RouteHandle;
import com.crossoverjie.cim.common.util.RouteInfoParseUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-02-27 00:33
* @since JDK 1.8
*/
public class ConsistentHashHandle implements RouteHandle {
private AbstractConsistentHash hash;
public void setHash(AbstractConsistentHash hash) {
this.hash = hash;
}
@Override
public String routeServer(List values, String key) {
return hash.process(values, key);
}
@Override
public List removeExpireServer(RouteInfo routeInfo) {
Map remove = hash.remove(RouteInfoParseUtil.parse(routeInfo));
return new ArrayList<>(remove.keySet());
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/SortArrayMapConsistentHash.java
================================================
package com.crossoverjie.cim.common.route.algorithm.consistenthash;
import com.crossoverjie.cim.common.data.construct.SortArrayMap;
import com.google.common.annotations.VisibleForTesting;
import java.util.Map;
/**
* Function:自定义排序 Map 实现
*
* @author crossoverJie
* Date: 2019-02-27 00:38
* @since JDK 1.8
*/
public class SortArrayMapConsistentHash extends AbstractConsistentHash {
private SortArrayMap sortArrayMap = new SortArrayMap();
/**
* 虚拟节点数量
*/
private static final int VIRTUAL_NODE_SIZE = 2;
@Override
public void add(long key, String value) {
for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) {
Long hash = super.hash("vir" + key + i);
sortArrayMap.add(hash, value);
}
sortArrayMap.add(key, value);
}
@Override
protected Map remove(String value) {
sortArrayMap = sortArrayMap.remove(value);
return sortArrayMap;
}
@Override
public void sort() {
sortArrayMap.sort();
}
/**
* Used only in test.
* @return Return the data structure of the current algorithm.
*/
@VisibleForTesting
public SortArrayMap getSortArrayMap() {
return sortArrayMap;
}
@Override
protected void clear() {
sortArrayMap.clear();
}
@Override
public String getFirstNodeValue(String value) {
long hash = super.hash(value);
return sortArrayMap.firstNodeValue(hash);
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/TreeMapConsistentHash.java
================================================
package com.crossoverjie.cim.common.route.algorithm.consistenthash;
import com.crossoverjie.cim.common.enums.StatusEnum;
import com.crossoverjie.cim.common.exception.CIMException;
import com.google.common.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Function:TreeMap 实现
*
* @author crossoverJie
* Date: 2019-02-27 01:16
* @since JDK 1.8
*/
public class TreeMapConsistentHash extends AbstractConsistentHash {
private final TreeMap treeMap = new TreeMap();
/**
* 虚拟节点数量
*/
private static final int VIRTUAL_NODE_SIZE = 2;
@Override
public void add(long key, String value) {
for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) {
Long hash = super.hash("vir" + key + i);
treeMap.put(hash, value);
}
treeMap.put(key, value);
}
@Override
protected Map remove(String value) {
treeMap.entrySet().removeIf(next -> next.getValue().equals(value));
Map result = new HashMap<>(treeMap.entrySet().size());
for (Map.Entry longStringEntry : treeMap.entrySet()) {
result.put(longStringEntry.getValue(), "");
}
return result;
}
@Override
protected void clear() {
treeMap.clear();
}
/**
* Used only in test.
* @return Return the data structure of the current algorithm.
*/
@VisibleForTesting
public TreeMap getTreeMap() {
return treeMap;
}
@Override
public String getFirstNodeValue(String value) {
long hash = super.hash(value);
SortedMap last = treeMap.tailMap(hash);
if (!last.isEmpty()) {
return last.get(last.firstKey());
}
if (treeMap.size() == 0) {
throw new CIMException(StatusEnum.SERVER_NOT_AVAILABLE);
}
return treeMap.firstEntry().getValue();
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/loop/LoopHandle.java
================================================
package com.crossoverjie.cim.common.route.algorithm.loop;
import com.crossoverjie.cim.common.enums.StatusEnum;
import com.crossoverjie.cim.common.exception.CIMException;
import com.crossoverjie.cim.common.pojo.RouteInfo;
import com.crossoverjie.cim.common.route.algorithm.RouteHandle;
import com.crossoverjie.cim.common.util.RouteInfoParseUtil;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-02-27 15:13
* @since JDK 1.8
*/
public class LoopHandle implements RouteHandle {
private final AtomicLong index = new AtomicLong();
private List values;
@Override
public String routeServer(List values, String key) {
if (values.size() == 0) {
throw new CIMException(StatusEnum.SERVER_NOT_AVAILABLE);
}
this.values = values;
Long position = index.incrementAndGet() % values.size();
if (position < 0) {
position = 0L;
}
return values.get(position.intValue());
}
@Override
public List removeExpireServer(RouteInfo routeInfo) {
String parse = RouteInfoParseUtil.parse(routeInfo);
values.removeIf(next -> next.equals(parse));
return values;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/random/RandomHandle.java
================================================
package com.crossoverjie.cim.common.route.algorithm.random;
import com.crossoverjie.cim.common.enums.StatusEnum;
import com.crossoverjie.cim.common.exception.CIMException;
import com.crossoverjie.cim.common.pojo.RouteInfo;
import com.crossoverjie.cim.common.route.algorithm.RouteHandle;
import com.crossoverjie.cim.common.util.RouteInfoParseUtil;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* Function: 路由策略, 随机
*
* @Author: jiangyunxiong
* @Date: 2019/3/7 11:56
* @since JDK 1.8
*/
public class RandomHandle implements RouteHandle {
private List values;
@Override
public String routeServer(List values, String key) {
int size = values.size();
if (size == 0) {
throw new CIMException(StatusEnum.SERVER_NOT_AVAILABLE);
}
this.values = values;
int offset = ThreadLocalRandom.current().nextInt(size);
return values.get(offset);
}
@Override
public List removeExpireServer(RouteInfo routeInfo) {
String parse = RouteInfoParseUtil.parse(routeInfo);
values.removeIf(next -> next.equals(parse));
return values;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/util/HttpClient.java
================================================
package com.crossoverjie.cim.common.util;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
/**
* Function:
*
* @author crossoverJie
* Date: 2020-04-25 00:39
* @since JDK 1.8
*/
public final class HttpClient {
private static final MediaType MEDIA_TYPE = MediaType.parse("application/json");
public static Response post(OkHttpClient okHttpClient, String params, String url) throws IOException {
RequestBody requestBody = RequestBody.create(MEDIA_TYPE, params);
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Response response = okHttpClient.newCall(request).execute();
if (!response.isSuccessful()) {
throw new IOException("request url [" + url + "], params [" + params + "] failed, response code: " + response.code()
+ ", message: " + response.message());
}
return response;
}
public static Response get(OkHttpClient okHttpClient, String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.get()
.build();
Response response = okHttpClient.newCall(request).execute();
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
return response;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/util/NettyAttrUtil.java
================================================
package com.crossoverjie.cim.common.util;
import io.netty.channel.Channel;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
/**
* Function:
*
* @author crossoverJie
* Date: 2019/1/9 00:57
* @since JDK 1.8
*/
public class NettyAttrUtil {
private static final AttributeKey ATTR_KEY_READER_TIME = AttributeKey.valueOf("readerTime");
public static void updateReaderTime(Channel channel, Long time) {
channel.attr(ATTR_KEY_READER_TIME).set(time.toString());
}
public static Long getReaderTime(Channel channel) {
String value = getAttribute(channel, ATTR_KEY_READER_TIME);
if (value != null) {
return Long.valueOf(value);
}
return null;
}
private static String getAttribute(Channel channel, AttributeKey key) {
Attribute attr = channel.attr(key);
return attr.get();
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/util/RouteInfoParseUtil.java
================================================
package com.crossoverjie.cim.common.util;
import com.crossoverjie.cim.common.exception.CIMException;
import com.crossoverjie.cim.common.pojo.RouteInfo;
import static com.crossoverjie.cim.common.enums.StatusEnum.VALIDATION_FAIL;
/**
* Function:
*
* @author crossoverJie
* Date: 2020-04-12 20:42
* @since JDK 1.8
*/
public class RouteInfoParseUtil {
public static RouteInfo parse(String info) {
try {
String[] serverInfo = info.split(":");
return new RouteInfo(serverInfo[0], Integer.parseInt(serverInfo[1]), Integer.parseInt(serverInfo[2]));
} catch (Exception e) {
throw new CIMException(VALIDATION_FAIL);
}
}
public static String parse(RouteInfo routeInfo) {
return routeInfo.getIp() + ":" + routeInfo.getCimServerPort() + ":" + routeInfo.getHttpPort();
}
private RouteInfoParseUtil() {
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/util/SnowflakeIdWorker.java
================================================
package com.crossoverjie.cim.common.util;
/**
* @author zhongcanyu
* @date 2025/5/18
* @description
*/
public class SnowflakeIdWorker {
private final long workerId = 1L;
private static final long EPOCH = 1622505600000L;
private long sequence = 0L;
private long lastTimestamp = -1L;
private static final long WORKER_ID_BITS = 10L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private long tilNextMillis(long lastTs) {
long ts = System.currentTimeMillis();
while (ts <= lastTs) {
ts = System.currentTimeMillis();
}
return ts;
}
public synchronized long nextId() {
long ts = System.currentTimeMillis();
if (ts < lastTimestamp) {
throw new IllegalStateException("Clock moved backwards.");
}
if (ts == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
ts = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = ts;
return ((ts - EPOCH) << TIMESTAMP_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
}
================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/util/StringUtil.java
================================================
package com.crossoverjie.cim.common.util;
/**
* Function:
*
* @author crossoverJie
* Date: 22/05/2018 15:16
* @since JDK 1.8
*/
public class StringUtil {
public StringUtil() {
}
public static boolean isNullOrEmpty(String str) {
return null == str || 0 == str.trim().length();
}
public static boolean isEmpty(String str) {
return str == null || "".equals(str.trim());
}
public static boolean isNotEmpty(String str) {
return str != null && !"".equals(str.trim());
}
public static String formatLike(String str) {
return isNotEmpty(str) ? "%" + str + "%" : null;
}
}
================================================
FILE: cim-common/src/main/proto/cim.proto
================================================
syntax = "proto3";
package com.crossoverjie.cim.common.protocol;
option java_package = "com.crossoverjie.cim.common.protocol";
option java_multiple_files = true;
message Request{
int64 requestId = 2;
string reqMsg = 1;
BaseCommand cmd = 3;
map properties = 4;
repeated string batchReqMsg = 5;
}
message Response{
int64 responseId = 2;
string resMsg = 1;
BaseCommand cmd = 3;
map properties = 4;
repeated string batchResMsg = 5;
}
enum BaseCommand{
LOGIN_REQUEST = 0;
MESSAGE = 1;
PING = 2;
OFFLINE = 3;
}
================================================
FILE: cim-common/src/main/resources/log4j.properties
================================================
# Global logging configuration
log4j.rootLogger=DEBUG,CONSOLE,LOGFILE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Target = System.out
log4j.appender.CONSOLE.Threshold = INFO
log4j.appender.CONSOLE.Encoding = UTF-8
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %5p (%c:%L) - %m%n
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File =./logs/error.log
log4j.appender.LOGFILE.Threshold = ERROR
log4j.appender.LOGFILE.Encoding = UTF-8
log4j.appender.LOGFILE.layout = org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/CommonTest.java
================================================
package com.crossoverjie.cim.common;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-09-23 14:21
* @since JDK 1.8
*/
public class CommonTest {
@Test
public void test2() {
System.out.println(LocalDate.now().toString());
System.out.println(LocalTime.now().withNano(0).toString());
}
@Test
public void test() throws InterruptedException {
System.out.println(is2(9));
System.out.println(Integer.bitCount(64 - 1));
int target = 1569312600;
int mod = 64;
System.out.println(target % mod);
System.out.println(mod(target, mod));
System.out.println("============");
System.out.println(cycleNum(256, 64));
}
private int mod(int target, int mod) {
// equals target % mod
return target & (mod - 1);
}
private int cycleNum(int target, int mod) {
//equals target/mod
return target >> Integer.bitCount(mod - 1);
}
private boolean is2(int target) {
if (target < 0) {
return false;
}
int value = target & (target - 1);
if (value != 0) {
return false;
}
return true;
}
private void cycle() throws InterruptedException {
int index = 0;
while (true) {
System.out.println("=======" + index);
if (++index > 63) {
index = 0;
}
TimeUnit.MILLISECONDS.sleep(200);
}
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/core/proxy/RpcProxyManagerTest.java
================================================
package com.crossoverjie.cim.common.core.proxy;
import com.crossoverjie.cim.common.exception.CIMException;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import okhttp3.OkHttpClient;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class RpcProxyManagerTest {
@Test
public void testGet() {
OkHttpClient client = new OkHttpClient();
String url = "https://api.github.com/users";
Github github = RpcProxyManager.create(Github.class, url, client);
GithubResponse githubResponse = github.crossoverjie();
Assertions.assertEquals(githubResponse.getName(), "crossoverJie");
github.torvalds();
}
@Test
public void testPost() {
OkHttpClient client = new OkHttpClient();
String url = "http://echo.free.beeceptor.com";
Echo echo = RpcProxyManager.create(Echo.class, url, client);
EchoRequest request = new EchoRequest();
request.setName("crossoverJie");
request.setAge(18);
request.setCity("shenzhen");
EchoResponse response = echo.echo(request);
Assertions.assertEquals(response.getParsedBody().getName(), "crossoverJie");
Assertions.assertEquals(response.getParsedBody().getAge(), 18);
Assertions.assertEquals(response.getParsedBody().getCity(), "shenzhen");
}
@Test
public void testUrl() {
OkHttpClient client = new OkHttpClient();
String url = "http://echo.free.beeceptor.com/sample-request?author=beeceptor";
/**
* {
* "method": "POST",
* "protocol": "http",
* "host": "echo.free.beeceptor.com",
* "path": "/sample-request?author=beeceptor",
* "ip": "111.249.70.48:41382",
* "headers": {
* "Host": "echo.free.beeceptor.com",
* "User-Agent": "okhttp/3.3.1",
* "Content-Length": "50",
* "Accept-Encoding": "gzip",
* "Content-Type": "application/json; charset=utf-8",
* "Via": "1.1 Caddy"
* },
* "parsedQueryParams": {
* "author": "beeceptor"
* },
* "parsedBody": {
* "city": "shenzhen",
* "name": "crossoverJie",
* "age": 18
* }
* }
*/
Echo echo = RpcProxyManager.create(Echo.class, client);
EchoRequest request = new EchoRequest();
request.setName("crossoverJie");
request.setAge(18);
request.setCity("shenzhen");
EchoResponse response = echo.echoTarget(url, request);
Assertions.assertEquals(response.getParsedBody().getName(), "crossoverJie");
Assertions.assertEquals(response.getParsedBody().getAge(), 18);
Assertions.assertEquals(response.getParsedBody().getCity(), "shenzhen");
response = echo.echoTarget(request, url);
Assertions.assertEquals(response.getParsedBody().getName(), "crossoverJie");
String req = "/request";
response = echo.request("http://echo.free.beeceptor.com", request);
Assertions.assertEquals(response.getPath(), req);
Assertions.assertEquals(response.getParsedBody().getAge(), 18);
Assertions.assertThrows(CIMException.class, () -> echo.echoTarget(request));
}
@Test
public void testFail() {
OkHttpClient client = new OkHttpClient();
String url = "http://echo.free.beeceptor.com";
Echo echo = RpcProxyManager.create(Echo.class, url, client);
EchoRequest request = new EchoRequest();
request.setName("crossoverJie");
request.setAge(18);
request.setCity("shenzhen");
Assertions.assertThrows(IllegalArgumentException.class, () -> echo.fail(request, "test", ""));
}
@Test
public void testGeneric() {
OkHttpClient client = new OkHttpClient();
String url = "http://echo.free.beeceptor.com";
Echo echo = RpcProxyManager.create(Echo.class, url, client);
EchoRequest request = new EchoRequest();
request.setName("crossoverJie");
request.setAge(18);
request.setCity("shenzhen");
EchoGeneric response = echo.echoGeneric(request);
Assertions.assertEquals(response.getHeaders().getHost(), "echo.free.beeceptor.com");
}
interface Echo {
@Request(url = "sample-request?author=beeceptor")
EchoResponse echo(EchoRequest message);
@Request(url = "sample-request?author=beeceptor")
EchoResponse echoTarget(@DynamicUrl(useMethodEndpoint = false) String url, EchoRequest message);
EchoResponse echoTarget(EchoRequest message, @DynamicUrl(useMethodEndpoint = false) String url);
@Request(url = "sample-request?author=beeceptor")
EchoResponse echoTarget(@DynamicUrl EchoRequest message);
EchoResponse request(@DynamicUrl() String url, EchoRequest message);
@Request(url = "sample-request?author=beeceptor")
EchoResponse fail(EchoRequest message, String s, String s1);
@Request(url = "sample-request?author=beeceptor")
EchoGeneric echoGeneric(EchoRequest message);
}
@Data
public static class EchoRequest {
private String name;
private int age;
private String city;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class CIMServerResVO implements Serializable {
private String ip;
private Integer cimServerPort;
private Integer httpPort;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@AllArgsConstructor
@NoArgsConstructor
public static class EchoGeneric {
private String method;
private String protocol;
private String host;
private T headers;
}
@NoArgsConstructor
@Data
public static class EchoResponse {
@JsonProperty("method")
private String method;
@JsonProperty("protocol")
private String protocol;
@JsonProperty("host")
private String host;
@JsonProperty("path")
private String path;
@JsonProperty("ip")
private String ip;
@JsonProperty("headers")
private HeadersDTO headers;
@JsonProperty("parsedQueryParams")
private ParsedQueryParamsDTO parsedQueryParams;
@JsonProperty("parsedBody")
private ParsedBodyDTO parsedBody;
@NoArgsConstructor
@Data
public static class HeadersDTO {
@JsonProperty("Host")
private String host;
@JsonProperty("User-Agent")
private String userAgent;
@JsonProperty("Content-Length")
private String contentLength;
@JsonProperty("Accept")
private String accept;
@JsonProperty("Content-Type")
private String contentType;
@JsonProperty("Accept-Encoding")
private String acceptEncoding;
@JsonProperty("Via")
private String via;
}
@NoArgsConstructor
@Data
public static class ParsedQueryParamsDTO {
@JsonProperty("author")
private String author;
}
@NoArgsConstructor
@Data
public static class ParsedBodyDTO {
@JsonProperty("name")
private String name;
@JsonProperty("age")
private Integer age;
@JsonProperty("city")
private String city;
}
}
interface Github {
@Request(method = Request.GET)
GithubResponse crossoverjie();
@Request(method = Request.GET)
void torvalds();
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public static class GithubResponse {
@JsonProperty("name")
private String name;
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/RingBufferWheelTest.java
================================================
package com.crossoverjie.cim.common.data.construct;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RingBufferWheelTest {
public static void main(String[] args) throws Exception {
test8();
}
private static void test8() throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(2);
RingBufferWheel wheel = new RingBufferWheel(executorService);
while (true) {
log.info("task size={}, task map size={}", wheel.taskSize(), wheel.taskMapSize());
TimeUnit.SECONDS.sleep(1);
for (int i = 0; i < 1000; i++) {
RingBufferWheel.Task task = new ByteTask(1024 * 1024);
task.setKey(1);
wheel.addTask(task);
}
}
}
private static class ByteTask extends RingBufferWheel.Task {
private byte[] b;
public ByteTask(int size) {
this.b = new byte[size];
}
@Override
public void run() {
// empty task
}
}
private static void test1() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
RingBufferWheel.Task task = new Task();
task.setKey(10);
RingBufferWheel wheel = new RingBufferWheel(executorService);
wheel.addTask(task);
task = new Task();
task.setKey(74);
wheel.addTask(task);
while (true) {
log.info("task size={}", wheel.taskSize());
TimeUnit.SECONDS.sleep(1);
}
}
private static void test2() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
RingBufferWheel.Task task = new Task();
task.setKey(10);
RingBufferWheel wheel = new RingBufferWheel(executorService);
wheel.addTask(task);
task = new Task();
task.setKey(74);
wheel.addTask(task);
wheel.start();
// new Thread(() -> {
// while (true){
// logger.info("task size={}" , wheel.taskSize());
// try {
// TimeUnit.SECONDS.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// }).start();
TimeUnit.SECONDS.sleep(12);
wheel.stop(true);
}
private static void test3() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
RingBufferWheel.Task task = new Task();
task.setKey(10);
RingBufferWheel wheel = new RingBufferWheel(executorService);
wheel.addTask(task);
task = new Task();
task.setKey(60);
wheel.addTask(task);
TimeUnit.SECONDS.sleep(2);
wheel.stop(false);
}
private static void test4() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
RingBufferWheel wheel = new RingBufferWheel(executorService);
for (int i = 0; i < 65; i++) {
RingBufferWheel.Task task = new Job(i);
task.setKey(i);
wheel.addTask(task);
}
wheel.start();
log.info("task size={}", wheel.taskSize());
wheel.stop(false);
}
private static void test5() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
RingBufferWheel wheel = new RingBufferWheel(executorService, 512);
for (int i = 0; i < 65; i++) {
RingBufferWheel.Task task = new Job(i);
task.setKey(i);
wheel.addTask(task);
}
log.info("task size={}", wheel.taskSize());
wheel.stop(false);
}
private static void test6() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
RingBufferWheel wheel = new RingBufferWheel(executorService, 512);
for (int i = 0; i < 10; i++) {
RingBufferWheel.Task task = new Job(i);
task.setKey(i);
wheel.addTask(task);
}
TimeUnit.SECONDS.sleep(5);
RingBufferWheel.Task task = new Job(15);
task.setKey(15);
wheel.addTask(task);
log.info("task size={}", wheel.taskSize());
wheel.stop(false);
}
private static void test7() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
RingBufferWheel wheel = new RingBufferWheel(executorService, 512);
for (int i = 0; i < 10; i++) {
RingBufferWheel.Task task = new Job(i);
task.setKey(i);
wheel.addTask(task);
}
RingBufferWheel.Task task = new Job(15);
task.setKey(15);
int cancel = wheel.addTask(task);
new Thread(() -> {
boolean flag = wheel.cancel(cancel);
log.info("cancel id={},key={} result={}", cancel, task.getKey(), flag);
}).start();
RingBufferWheel.Task task1 = new Job(20);
task1.setKey(20);
wheel.addTask(task1);
log.info("task size={}", wheel.taskSize());
wheel.stop(false);
}
private static void concurrentTest() throws Exception {
BlockingQueue queue = new LinkedBlockingQueue(10);
ThreadFactory product = new ThreadFactoryBuilder()
.setNameFormat("msg-callback-%d")
.setDaemon(true)
.build();
ThreadPoolExecutor business = new ThreadPoolExecutor(4, 4, 1, TimeUnit.MILLISECONDS, queue, product);
ExecutorService executorService = Executors.newFixedThreadPool(10);
RingBufferWheel wheel = new RingBufferWheel(executorService);
for (int i = 0; i < 10; i++) {
business.execute(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 30; i1++) {
RingBufferWheel.Task task = new Job(i1);
task.setKey(i1);
wheel.addTask(task);
}
}
});
}
log.info("task size={}", wheel.taskSize());
wheel.stop(false);
}
private static class Job extends RingBufferWheel.Task {
private int num;
public Job(int num) {
this.num = num;
}
@Override
public void run() {
log.info("number={}", num);
}
}
private static class Task extends RingBufferWheel.Task {
@Override
public void run() {
log.info("================");
}
}
public static void hashTimerTest() {
BlockingQueue queue = new LinkedBlockingQueue(10);
ThreadFactory product = new ThreadFactoryBuilder()
.setNameFormat("msg-callback-%d")
.setDaemon(true)
.build();
ThreadPoolExecutor business = new ThreadPoolExecutor(4, 4, 1, TimeUnit.MILLISECONDS, queue, product);
HashedWheelTimer hashedWheelTimer = new HashedWheelTimer();
for (int i = 0; i < 10; i++) {
int finalI = i;
business.execute(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 10; i1++) {
hashedWheelTimer.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
log.info("====" + finalI);
}
}, finalI, TimeUnit.SECONDS);
}
}
});
}
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/ScheduledTest.java
================================================
package com.crossoverjie.cim.common.data.construct;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-10-11 10:41
* @since JDK 1.8
*/
@Slf4j
public class ScheduledTest {
public static void main(String[] args) {
log.info("start.....");
ThreadFactory scheduled = new ThreadFactoryBuilder()
.setNameFormat("scheduled-%d")
.build();
ScheduledThreadPoolExecutor scheduledExecutorService = new ScheduledThreadPoolExecutor(2, scheduled);
scheduledExecutorService.schedule(() -> log.info("scheduled........."), 3, TimeUnit.SECONDS);
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/SortArrayMapTest.java
================================================
package com.crossoverjie.cim.common.data.construct;
import java.util.SortedMap;
import java.util.TreeMap;
import org.junit.Test;
public class SortArrayMapTest {
int count = 1000000;
@Test
public void ad() {
SortArrayMap map = new SortArrayMap();
for (int i = 0; i < 9; i++) {
map.add(Long.valueOf(i), "127.0.0." + i);
}
map.print();
System.out.println(map.size());
}
@Test
public void add() {
SortArrayMap map = new SortArrayMap();
for (int i = 0; i < 10; i++) {
map.add(Long.valueOf(i), "127.0.0." + i);
}
map.print();
System.out.println(map.size());
}
@Test
public void add2() {
SortArrayMap map = new SortArrayMap();
for (int i = 0; i < 20; i++) {
map.add(Long.valueOf(i), "127.0.0." + i);
}
map.sort();
map.print();
System.out.println(map.size());
}
@Test
public void add3() {
SortArrayMap map = new SortArrayMap();
map.add(100L, "127.0.0.100");
map.add(10L, "127.0.0.10");
map.add(8L, "127.0.0.8");
map.add(1000L, "127.0.0.1000");
map.print();
System.out.println(map.size());
}
@Test
public void firstNode() {
SortArrayMap map = new SortArrayMap();
map.add(100L, "127.0.0.100");
map.add(10L, "127.0.0.10");
map.add(8L, "127.0.0.8");
map.add(1000L, "127.0.0.1000");
map.sort();
map.print();
String value = map.firstNodeValue(101);
System.out.println(value);
}
@Test
public void firstNode2() {
SortArrayMap map = new SortArrayMap();
map.add(100L, "127.0.0.100");
map.add(10L, "127.0.0.10");
map.add(8L, "127.0.0.8");
map.add(1000L, "127.0.0.1000");
map.sort();
map.print();
String value = map.firstNodeValue(1);
System.out.println(value);
}
@Test
public void firstNode3() {
SortArrayMap map = new SortArrayMap();
map.add(100L, "127.0.0.100");
map.add(10L, "127.0.0.10");
map.add(8L, "127.0.0.8");
map.add(1000L, "127.0.0.1000");
map.sort();
map.print();
String value = map.firstNodeValue(1001);
System.out.println(value);
}
@Test
public void firstNode4() {
SortArrayMap map = new SortArrayMap();
map.add(100L, "127.0.0.100");
map.add(10L, "127.0.0.10");
map.add(8L, "127.0.0.8");
map.add(1000L, "127.0.0.1000");
map.sort();
map.print();
String value = map.firstNodeValue(9);
System.out.println(value);
}
@Test
public void add4() {
SortArrayMap map = new SortArrayMap();
map.add(100L, "127.0.0.100");
map.add(10L, "127.0.0.10");
map.add(8L, "127.0.0.8");
map.add(1000L, "127.0.0.1000");
map.sort();
map.print();
System.out.println(map.size());
}
@Test
public void add5() {
SortArrayMap map = new SortArrayMap();
long star = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
double d = Math.random();
int ran = (int) (d * 100);
map.add(Long.valueOf(i + ran), "127.0.0." + i);
}
map.sort();
long end = System.currentTimeMillis();
System.out.println("排序耗时 " + (end - star));
System.out.println(map.size());
}
@Test
public void add6() {
SortArrayMap map = new SortArrayMap();
long star = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
double d = Math.random();
int ran = (int) (d * 100);
map.add(Long.valueOf(i + ran), "127.0.0." + i);
}
long end = System.currentTimeMillis();
System.out.println("不排耗时 " + (end - star));
System.out.println(map.size());
}
@Test
public void add7() {
TreeMap treeMap = new TreeMap();
long star = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
double d = Math.random();
int ran = (int) (d * 100);
treeMap.put(Long.valueOf(i + ran), "127.0.0." + i);
}
long end = System.currentTimeMillis();
System.out.println("耗时 " + (end - star));
System.out.println(treeMap.size());
}
@Test
public void add8() {
TreeMap map = new TreeMap();
map.put(100L, "127.0.0.100");
map.put(10L, "127.0.0.10");
map.put(8L, "127.0.0.8");
map.put(1000L, "127.0.0.1000");
SortedMap last = map.tailMap(101L);
if (!last.isEmpty()) {
System.out.println(last.get(last.firstKey()));
} else {
System.out.println(map.firstEntry().getValue());
}
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/TimerTest.java
================================================
package com.crossoverjie.cim.common.data.construct;
import java.util.Timer;
import java.util.TimerTask;
import lombok.extern.slf4j.Slf4j;
/**
* Function:
*
* @author crossoverJie
* Date: 2019-10-09 22:48
* @since JDK 1.8
*/
@Slf4j
public class TimerTest {
public static void main(String[] args) {
log.info("start");
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
log.info("test");
}
}, 50000);
timer.schedule(new TimerTask() {
@Override
public void run() {
log.info("test");
}
}, 30000);
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/TrieTreeTest.java
================================================
package com.crossoverjie.cim.common.data.construct;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
public class TrieTreeTest {
@Test
public void insert() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("abc");
trieTree.insert("abcd");
}
@Test
public void all() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("ABC");
trieTree.insert("abC");
List all = trieTree.all();
String result = "";
for (String s : all) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue("ABC,abC,".equals(result));
}
@Test
public void all2() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("abc");
trieTree.insert("abC");
List all = trieTree.all();
String result = "";
for (String s : all) {
result += s + ",";
System.out.println(s);
}
//Assert.assertTrue("ABC,abC,".equals(result));
}
@Test
public void prefixSea() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("java");
trieTree.insert("jsf");
trieTree.insert("jsp");
trieTree.insert("javascript");
trieTree.insert("php");
String result = "";
List ab = trieTree.prefixSearch("jav");
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals("java,javascript,"));
}
@Test
public void prefixSea2() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("java");
trieTree.insert("jsf");
trieTree.insert("jsp");
trieTree.insert("javascript");
trieTree.insert("php");
String result = "";
List ab = trieTree.prefixSearch("j");
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals("java,javascript,jsf,jsp,"));
}
@Test
public void prefixSea3() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("java");
trieTree.insert("jsf");
trieTree.insert("jsp");
trieTree.insert("javascript");
trieTree.insert("php");
String result = "";
List ab = trieTree.prefixSearch("js");
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals("jsf,jsp,"));
}
@Test
public void prefixSea4() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("java");
trieTree.insert("jsf");
trieTree.insert("jsp");
trieTree.insert("javascript");
trieTree.insert("php");
String result = "";
List ab = trieTree.prefixSearch("jav");
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals("java,javascript,"));
}
@Test
public void prefixSea5() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("java");
trieTree.insert("jsf");
trieTree.insert("jsp");
trieTree.insert("javascript");
trieTree.insert("php");
String result = "";
List ab = trieTree.prefixSearch("js");
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals("jsf,jsp,"));
}
@Test
public void prefixSearch() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("abc");
trieTree.insert("abd");
trieTree.insert("ABe");
List ab = trieTree.prefixSearch("AB");
for (String s : ab) {
System.out.println(s);
}
System.out.println("========");
//char[] chars = new char[3] ;
//for (int i = 0; i < 3; i++) {
// int a = 97 + i ;
// chars[i] = (char) a ;
//}
//
//String s = String.valueOf(chars);
//System.out.println(s);
}
@Test
public void prefixSearch2() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("Cde");
trieTree.insert("CDa");
trieTree.insert("ABe");
List ab = trieTree.prefixSearch("AC");
for (String s : ab) {
System.out.println(s);
}
Assert.assertTrue(ab.size() == 0);
}
@Test
public void prefixSearch3() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("Cde");
trieTree.insert("CDa");
trieTree.insert("ABe");
List ab = trieTree.prefixSearch("CD");
for (String s : ab) {
System.out.println(s);
}
Assert.assertTrue(ab.size() == 1);
}
@Test
public void prefixSearch4() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("Cde");
trieTree.insert("CDa");
trieTree.insert("ABe");
List ab = trieTree.prefixSearch("Cd");
String result = "";
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals("Cde,"));
}
@Test
public void prefixSearch44() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("a");
trieTree.insert("b");
trieTree.insert("c");
trieTree.insert("d");
trieTree.insert("e");
trieTree.insert("f");
trieTree.insert("g");
trieTree.insert("h");
}
@Test
public void prefixSearch5() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("Cde");
trieTree.insert("CDa");
trieTree.insert("ABe");
trieTree.insert("CDfff");
trieTree.insert("Cdfff");
List ab = trieTree.prefixSearch("Cd");
String result = "";
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals("Cde,Cdfff,"));
}
@Test
public void prefixSearch6() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("Cde");
trieTree.insert("CDa");
trieTree.insert("ABe");
trieTree.insert("CDfff");
trieTree.insert("Cdfff");
List ab = trieTree.prefixSearch("CD");
String result = "";
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals("CDa,CDfff,"));
}
@Test
public void prefixSearch7() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("Cde");
trieTree.insert("CDa");
trieTree.insert("ABe");
trieTree.insert("CDfff");
trieTree.insert("Cdfff");
List ab = trieTree.prefixSearch("");
String result = "";
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals(""));
}
@Test
public void prefixSearch8() throws Exception {
TrieTree trieTree = new TrieTree();
List ab = trieTree.prefixSearch("");
String result = "";
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals(""));
}
@Test
public void prefixSearch9() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("Cde");
trieTree.insert("CDa");
trieTree.insert("ABe");
trieTree.insert("CDfff");
trieTree.insert("Cdfff");
List ab = trieTree.prefixSearch("CDFD");
String result = "";
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals(""));
}
@Test
public void prefixSearch10() throws Exception {
TrieTree trieTree = new TrieTree();
trieTree.insert("crossoverJie");
trieTree.insert("zhangsan");
List ab = trieTree.prefixSearch("c");
String result = "";
for (String s : ab) {
result += s + ",";
System.out.println(s);
}
Assert.assertTrue(result.equals("crossoverJie,"));
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/enums/SystemCommandEnumTypeTest.java
================================================
package com.crossoverjie.cim.common.enums;
import java.util.Map;
import org.junit.Test;
public class SystemCommandEnumTypeTest {
@Test
public void getAllStatusCode() throws Exception {
Map allStatusCode = SystemCommandEnum.getAllStatusCode();
for (Map.Entry stringStringEntry : allStatusCode.entrySet()) {
String key = stringStringEntry.getKey();
String value = stringStringEntry.getValue();
System.out.println(key + "----->" + value);
}
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/metastore/MetaStoreTest.java
================================================
package com.crossoverjie.cim.common.metastore;
import java.util.List;
import java.util.concurrent.TimeUnit;
import lombok.SneakyThrows;
import org.I0Itec.zkclient.ZkClient;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.CuratorWatcher;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.Stat;
public class MetaStoreTest {
private static final String CONNECTION_STRING = "127.0.0.1:2181";
// TODO: 2024/8/30 integration test
@SneakyThrows
// @Test
public void testZk() {
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(CONNECTION_STRING)
.retryPolicy(retryPolicy)
.connectionTimeoutMs(5000)
.sessionTimeoutMs(5000)
.build();
client.start();
Stat stat = client.checkExists().forPath("/cim");
if (stat == null) {
create(client, "/cim", null);
}
List list = null;
list = curatorWatcherGetChildren(client, "/cim", watchedEvent -> {
String name = Thread.currentThread().getName();
System.out.println("watchedEvent = " + watchedEvent + " name = " + name);
// try {
// List children = watchedGetChildren(client, "/cim");
// System.out.println("children = " + children);
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
});
System.out.println(list);
// createEphemeral(client, "/cim/route1", null);
// createEphemeral(client, "/cim/route2", null);
TimeUnit.SECONDS.sleep(1000);
}
public static void createEphemeral(CuratorFramework client, String path, byte[] payload) throws Exception {
// this will create the given EPHEMERAL ZNode with the given data
client.create().withMode(CreateMode.EPHEMERAL).forPath(path, payload);
}
public static void create(CuratorFramework client, String path, byte[] payload) throws Exception {
// this will create the given ZNode with the given data
client.create().forPath(path, payload);
}
public static List watchedGetChildren(CuratorFramework client, String path) throws Exception {
/**
* Get children and set a watcher on the node. The watcher notification will come through the
* CuratorListener (see setDataAsync() above).
*/
return client.getChildren().watched().forPath(path);
}
public static List watchedGetChildren(CuratorFramework client, String path, Watcher watcher)
throws Exception {
/**
* Get children and set the given watcher on the node.
*/
return client.getChildren().usingWatcher(watcher).forPath(path);
}
public static List curatorWatcherGetChildren(CuratorFramework client, String path, CuratorWatcher watcher)
throws Exception {
/**
* Get children and set the given watcher on the node.
*/
return client.getChildren().usingWatcher(watcher).forPath(path);
}
@SneakyThrows
// @Test
public void zkClientTest() {
ZkClient zkClient = new ZkClient(CONNECTION_STRING, 5000);
zkClient.subscribeChildChanges("/cim", (parentPath, currentChildren) -> {
System.out.println("parentPath = " + parentPath);
System.out.println("currentChildren = " + currentChildren);
});
TimeUnit.SECONDS.sleep(1000);
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/ConsistentHashHandleTest.java
================================================
package com.crossoverjie.cim.common.route.algorithm.consistenthash;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import com.crossoverjie.cim.common.pojo.RouteInfo;
import com.crossoverjie.cim.common.util.RouteInfoParseUtil;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
class ConsistentHashHandleTest {
@Test
void removeSortMapExpireServer() {
ConsistentHashHandle routeHandle = new ConsistentHashHandle();
routeHandle.setHash(new SortArrayMapConsistentHash());
List strings = new ArrayList<>();
for (int i = 0; i < 10; i++) {
var routeInfo = new RouteInfo("127.0.0." + i, 1000, 2000);
strings.add(RouteInfoParseUtil.parse(routeInfo));
}
RouteInfo routeInfo = new RouteInfo("127.0.0.9", 1000, 2000);
String parse = RouteInfoParseUtil.parse(routeInfo);
String r1 = routeHandle.routeServer(strings, parse);
String r2 = routeHandle.routeServer(strings, parse);
assertEquals(r1, r2);
List list = routeHandle.removeExpireServer(routeInfo);
boolean contains = list.contains(parse);
assertFalse(contains);
}
@Test
void removeTreeMapExpireServer() {
ConsistentHashHandle routeHandle = new ConsistentHashHandle();
routeHandle.setHash(new TreeMapConsistentHash());
List strings = new ArrayList<>();
for (int i = 0; i < 10; i++) {
var routeInfo = new RouteInfo("127.0.0." + i, 1000, 2000);
strings.add(RouteInfoParseUtil.parse(routeInfo));
}
RouteInfo routeInfo = new RouteInfo("127.0.0.9", 1000, 2000);
String parse = RouteInfoParseUtil.parse(routeInfo);
String r1 = routeHandle.routeServer(strings, parse);
String r2 = routeHandle.routeServer(strings, parse);
assertEquals(r1, r2);
List list = routeHandle.removeExpireServer(routeInfo);
boolean contains = list.contains(parse);
assertFalse(contains);
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/RangeCheckTestUtil.java
================================================
package com.crossoverjie.cim.common.route.algorithm.consistenthash;
import org.junit.Assert;
/**
* @description: TODO
* @author: zhangguoa
* @date: 2024/9/12 9:58
* @project: cim
*/
public class RangeCheckTestUtil {
public static void assertInRange(int value, int l, int r) {
Assert.assertTrue(value >= l && value <= r);
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/SortArrayMapConsistentHashTest.java
================================================
package com.crossoverjie.cim.common.route.algorithm.consistenthash;
import com.crossoverjie.cim.common.data.construct.SortArrayMap;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.Test;
public class SortArrayMapConsistentHashTest {
@Test
public void getFirstNodeValue() {
AbstractConsistentHash map = new SortArrayMapConsistentHash();
List strings = new ArrayList<>();
for (int i = 0; i < 10; i++) {
strings.add("127.0.0." + i);
}
String process1 = map.process(strings, "zhangsan");
for (int i = 0; i < 100; i++) {
String process = map.process(strings, "zhangsan");
Assert.assertEquals(process1, process);
}
}
@Test
public void getFirstNodeValue2() {
AbstractConsistentHash map = new SortArrayMapConsistentHash();
List strings = new ArrayList<>();
for (int i = 0; i < 10; i++) {
strings.add("127.0.0." + i);
}
String process1 = map.process(strings, "zhangsan2");
for (int i = 0; i < 100; i++) {
String process = map.process(strings, "zhangsan2");
Assert.assertEquals(process1, process);
}
}
@Test
public void getFirstNodeValue3() {
AbstractConsistentHash map = new SortArrayMapConsistentHash();
List strings = new ArrayList<>();
for (int i = 0; i < 10; i++) {
strings.add("127.0.0." + i);
}
String process1 = map.process(strings, "1551253899106");
for (int i = 0; i < 100; i++) {
String process = map.process(strings, "1551253899106");
Assert.assertEquals(process1, process);
}
}
@Test
public void getFirstNodeValue4() {
AbstractConsistentHash map = new SortArrayMapConsistentHash();
List strings = new ArrayList<>();
strings.add("45.78.28.220:9000:8081");
strings.add("45.78.28.220:9100:9081");
String process1 = map.process(strings, "1551253899106");
for (int i = 0; i < 100; i++) {
String process = map.process(strings, "1551253899106");
Assert.assertEquals(process1, process);
}
}
@Test
public void getFirstNodeValue5() {
AbstractConsistentHash map = new SortArrayMapConsistentHash();
List strings = new ArrayList<>();
strings.add("45.78.28.220:9000:8081");
strings.add("45.78.28.220:9100:9081");
strings.add("45.78.28.220:9100:10081");
String process1 = map.process(strings, "1551253899106");
for (int i = 0; i < 100; i++) {
String process = map.process(strings, "1551253899106");
Assert.assertEquals(process1, process);
}
}
@Test
public void getFirstNodeValue6() {
AbstractConsistentHash map = new SortArrayMapConsistentHash();
List strings = new ArrayList<>();
strings.add("45.78.28.220:9000:8081");
strings.add("45.78.28.220:9100:9081");
strings.add("45.78.28.220:9100:10081");
String process1 = map.process(strings, "1551253899106");
for (int i = 0; i < 100; i++) {
String process = map.process(strings, "1551253899106");
Assert.assertEquals(process1, process);
}
}
@Test
public void getFirstNodeValue7() {
AbstractConsistentHash map = new SortArrayMapConsistentHash();
List strings = new ArrayList<>();
strings.add("45.78.28.220:9000:8081");
strings.add("45.78.28.220:9100:9081");
strings.add("45.78.28.220:9100:10081");
strings.add("45.78.28.220:9100:00081");
String process1 = map.process(strings, "1551253899106");
for (int i = 0; i < 100; i++) {
String process = map.process(strings, "1551253899106");
Assert.assertEquals(process1, process);
}
}
@Test
public void getFirstNodeValue8() {
AbstractConsistentHash map = new SortArrayMapConsistentHash();
List strings = new ArrayList<>();
for (int i = 0; i < 10; i++) {
strings.add("127.0.0." + i);
}
Set processes = new HashSet<>();
for (int i = 0; i < 10; i++) {
String process = map.process(strings, "zhangsan" + i);
processes.add(process);
}
RangeCheckTestUtil.assertInRange(processes.size(), 2, 10);
}
@Test
public void testVirtualNode() throws NoSuchFieldException, IllegalAccessException {
SortArrayMapConsistentHash map = new SortArrayMapConsistentHash();
List strings = new ArrayList<>();
for (int i = 0; i < 10; i++) {
strings.add("127.0.0." + i);
}
String process = map.process(strings, "zhangsan");
SortArrayMap sortArrayMap = map.getSortArrayMap();
int virtualNodeSize = 2;
System.out.println("sortArrayMapSize = " + sortArrayMap.size() + "\n" + "virtualNodeSize = " + virtualNodeSize);
Assert.assertEquals(sortArrayMap.size(), (virtualNodeSize + 1) * 10);
}
}
================================================
FILE: cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/TreeMapConsistentHashTest.java
================================================
package com.crossoverjie.cim.common.route.algorithm.consistenthash;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;
import org.junit.Assert;
import org.junit.Test;
public class TreeMapConsistentHashTest {
@Test
public void getFirstNodeValue() {
AbstractConsistentHash map = new TreeMapConsistentHash();
List strings = new ArrayList<>();
for (int i = 0; i < 10; i++) {
strings.add("127.0.0." + i);
}
String process1 = map.process(strings, "zhangsan");
for (int i = 0; i < 100; i++) {
String process = map.process(strings, "zhangsan");
Assert.assertEquals(process1, process);
}
}
@Test
public void getFirstNodeValue2() {
AbstractConsistentHash map = new TreeMapConsistentHash();
List