這次整合借鑒了以下博主的智慧
websocket和socketio的區(qū)別
socket.io.js最簡(jiǎn)版單頁HTML測(cè)試工具
Netty-SocketIO多路復(fù)用
springboot學(xué)習(xí)(四十三) springboot使用netty-socketio實(shí)現(xiàn)消息推送
SpringBoot集成SocketIO
一、準(zhǔn)備工作
1、maven依賴
socketio的核心依賴就只有這個(gè)
<!-- netty-socketio: 仿`node.js`實(shí)現(xiàn)的socket.io服務(wù)端 -->
<dependency>
<groupId>com.corundumstudio.socketio</groupId>
<artifactId>netty-socketio</artifactId>
<version>1.7.19</version>
</dependency>
2、socketIO的yml配置
#自定義socketio配置,你可以直接硬編碼,看個(gè)人喜好
socketio:
# socketio請(qǐng)求地址
host: 127.0.0.1
# socketio端口
port: 9999
# 設(shè)置最大每幀處理數(shù)據(jù)的長度,防止他人利用大數(shù)據(jù)來攻擊服務(wù)器
maxFramePayloadLength: 1048576
# 設(shè)置http交互最大內(nèi)容長度
maxHttpContentLength: 1048576
# socket連接數(shù)大小(如只監(jiān)聽一個(gè)端口boss線程組為1即可)
bossCount: 1
# 連接數(shù)大小
workCount: 100
# 允許客戶請(qǐng)求
allowCustomRequests: true
# 協(xié)議升級(jí)超時(shí)時(shí)間(毫秒),默認(rèn)10秒。HTTP握手升級(jí)為ws協(xié)議超時(shí)時(shí)間
upgradeTimeout: 1000000
# Ping消息超時(shí)時(shí)間(毫秒),默認(rèn)60秒,這個(gè)時(shí)間間隔內(nèi)沒有接收到心跳消息就會(huì)發(fā)送超時(shí)事件
pingTimeout: 6000000
# Ping消息間隔(毫秒),默認(rèn)25秒??蛻舳讼蚍?wù)器發(fā)送一條心跳消息間隔
pingInterval: 25000
# 命名空間,多個(gè)以逗號(hào)分隔,
namespaces: /test,/socketIO
#namespaces: /socketIO
3、socketIO的config代碼
package com.gzgs.socketio.common.config;
import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Optional;
@Configuration
public class SocketIOConfig {
@Value("${socketio.host}")
private String host;
@Value("${socketio.port}")
private Integer port;
@Value("${socketio.bossCount}")
private int bossCount;
@Value("${socketio.workCount}")
private int workCount;
@Value("${socketio.allowCustomRequests}")
private boolean allowCustomRequests;
@Value("${socketio.upgradeTimeout}")
private int upgradeTimeout;
@Value("${socketio.pingTimeout}")
private int pingTimeout;
@Value("${socketio.pingInterval}")
private int pingInterval;
@Value("${socketio.namespaces}")
private String[] namespaces;
@Bean
public SocketIOServer socketIOServer() {
SocketConfig socketConfig = new SocketConfig();
socketConfig.setTcpNoDelay(true);
socketConfig.setSoLinger(0);
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
config.setSocketConfig(socketConfig);
config.setHostname(host);
config.setPort(port);
config.setBossThreads(bossCount);
config.setWorkerThreads(workCount);
config.setAllowCustomRequests(allowCustomRequests);
config.setUpgradeTimeout(upgradeTimeout);
config.setPingTimeout(pingTimeout);
config.setPingInterval(pingInterval);
//服務(wù)端
final SocketIOServer server = new SocketIOServer(config);
//添加命名空間(如果你不需要命名空間,下面的代碼可以去掉)
Optional.ofNullable(namespaces).ifPresent(nss ->
Arrays.stream(nss).forEach(server::addNamespace));
return server;
}
//這個(gè)對(duì)象是用來掃描socketio的注解,比如 @OnConnect、@OnEvent
@Bean
public SpringAnnotationScanner springAnnotationScanner() {
return new SpringAnnotationScanner(socketIOServer());
}
}
4、SocketIOServer啟動(dòng)或關(guān)閉
我在啟動(dòng)類里面定義了啟動(dòng)或者關(guān)閉SocketIOServer
package com.gzgs.socketio;
import com.corundumstudio.socketio.SocketIOServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;
@SpringBootApplication
public class SocketioServerApplication {
public static void main(String[] args) {
SpringApplication.run(SocketioServerApplication.class, args);
}
}
@Component
@Slf4j
class SocketIOServerRunner implements CommandLineRunner, DisposableBean {
@Autowired
private SocketIOServer socketIOServer;
@Override
public void run(String... args) throws Exception {
socketIOServer.start();
log.info("SocketIOServer==============================啟動(dòng)成功");
}
@Override
public void destroy() throws Exception {
//如果用kill -9 這個(gè)監(jiān)聽是沒用的,有可能會(huì)導(dǎo)致你服務(wù)kill掉了,但是socket服務(wù)沒有kill掉
socketIOServer.stop();
log.info("SocketIOServer==============================關(guān)閉成功");
}
}
springboot整合socketIO的工作已經(jīng)完成了
5、項(xiàng)目目錄結(jié)構(gòu)
參考下即可,核心是如何配置以及如何啟動(dòng)/關(guān)閉SocketIO
二、客戶端和服務(wù)端建立連接
1、服務(wù)端
1.1 用戶緩存信息ClientCache
package com.gzgs.socketio.common.cache;
import com.corundumstudio.socketio.SocketIOClient;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 這是存儲(chǔ)用戶的緩存信息
*/
@Component
public class ClientCache {
//用于存儲(chǔ)用戶的socket緩存信息
private static ConcurrentHashMap<String, HashMap<UUID, SocketIOClient>> concurrentHashMap = new ConcurrentHashMap<>();
//保存用戶信息
public void saveClient(String userId,UUID sessionId,SocketIOClient socketIOClient){
HashMap<UUID, SocketIOClient> sessionIdClientCache = concurrentHashMap.get(userId);
if(sessionIdClientCache == null){
sessionIdClientCache = new HashMap<>();
}
sessionIdClientCache.put(sessionId,socketIOClient);
concurrentHashMap.put(userId,sessionIdClientCache);
}
//獲取用戶信息
public HashMap<UUID,SocketIOClient> getUserClient(String userId){
return concurrentHashMap.get(userId);
}
//根據(jù)用戶id和session刪除用戶某個(gè)session信息
public void deleteSessionClientByUserId(String userId,UUID sessionId){
concurrentHashMap.get(userId).remove(sessionId);
}
//刪除用戶緩存信息
public void deleteUserCacheByUserId(String userId){
concurrentHashMap.remove(userId);
}
}
1.2 SocketIOServerHandler
用于監(jiān)聽客戶端的建立連接請(qǐng)求和關(guān)閉連接請(qǐng)求
package com.gzgs.socketio.common.handler;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.OnConnect;
import com.corundumstudio.socketio.annotation.OnDisconnect;
import com.gzgs.socketio.common.cache.ClientCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Slf4j
@Component
public class SocketIOServerHandler {
@Autowired
private ClientCache clientCache;
/**
* 建立連接
* @param client 客戶端的SocketIO
*/
@OnConnect
public void onConnect(SocketIOClient client) {
//因?yàn)槲叶x用戶的參數(shù)為userId,你也可以定義其他名稱 客戶端請(qǐng)求 http://localhost:9999?userId=12345
//下面兩種是加了命名空間的,他會(huì)請(qǐng)求對(duì)應(yīng)命名空間的方法(就類似你進(jìn)了不同的房間玩游戲)
//因?yàn)槲叶x用戶的參數(shù)為userId,你也可以定義其他名稱 客戶端請(qǐng)求 http://localhost:9999/test?userId=12345
//因?yàn)槲叶x用戶的參數(shù)為userId,你也可以定義其他名稱 客戶端請(qǐng)求 http://localhost:9999/SocketIO?userId=12345
String userId = client.getHandshakeData().getSingleUrlParam("userId");
//同一個(gè)頁面sessionid一樣的
UUID sessionId = client.getSessionId();
//保存用戶的信息在緩存里面
clientCache.saveClient(userId,sessionId,client);
log.info("SocketIOServerHandler-用戶id:{},sessionId:{},建立連接成功",userId,sessionId);
}
/**
* 關(guān)閉連接
* @param client 客戶端的SocketIO
*/
@OnDisconnect
public void onDisconnect(SocketIOClient client){
//因?yàn)槲叶x用戶的參數(shù)為userId,你也可以定義其他名稱
String userId = client.getHandshakeData().getSingleUrlParam("userId");
//sessionId,頁面唯一標(biāo)識(shí)
UUID sessionId = client.getSessionId();
//clientCache.deleteUserCacheByUserId(userId);
//只會(huì)刪除用戶某個(gè)頁面會(huì)話的緩存,不會(huì)刪除該用戶不同會(huì)話的緩存,比如:用戶同時(shí)打開了谷歌和QQ瀏覽器,當(dāng)你關(guān)閉谷歌時(shí)候,只會(huì)刪除該用戶谷歌的緩存會(huì)話
clientCache.deleteSessionClientByUserId(userId,sessionId);
log.info("SocketIOServerHandler-用戶id:{},sessionId:{},關(guān)閉連接成功",userId,sessionId);
}
}
2、客戶端
直接復(fù)制建立html文件,在瀏覽器打開就可以使用了
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>SocketIO客戶端測(cè)試環(huán)境</title>
<base>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/socket.io/2.1.1/socket.io.js"></script>
<style>
body {
padding: 20px;
}
#console {
height: 450px;
overflow: auto;
}
.connect-msg {
color: green;
}
.disconnect-msg {
color: red;
}
</style>
</head>
<body>
<h1>客戶端測(cè)試環(huán)境</h1>
<hr style="height:1px;border:none;border-top:1px solid black;" />
<div style="width: 700px; float: left">
<h3>SocketClient建立連接</h3>
<div style="border: 1px;">
<label>socketio服務(wù)端地址:</label>
<!--
http://localhost 服務(wù)端ip
9999 服務(wù)端socket端口(服務(wù)端提供)
test或socketIO 命名空間(可自定義)如果不定義命名空間,默認(rèn)是/ 比如:http://localhost:9999?userId=12345
userId 用戶id參數(shù)(可自定義)
ps:因?yàn)槲叶x了命名空間/test和/socketIO,所以我這里也可以用
http://localhost:9999/test?userId=12345
http://localhost:9999/socketIO?userId=12345
這里我用http://localhost:9999?userId=12345建立連接,因?yàn)檫@里還不涉及到請(qǐng)求不同命名空間的方法
-->
<input type="text" id="url" value="http://localhost:9999?userId=12345" style="width: 500px;">
<br>
<br>
<button id="connect" style="width: 100px;">建立連接</button>
<button id="disconnect" style="width: 100px;">斷開連接</button>
</div>
<hr style="height:1px;border:none;border-top:1px solid black;" />
<h3>SocketClient發(fā)送消息</h3>
<div style="border: 1px;">
<label>socketEvent名稱:</label><input type="text" id="socketEvent" value="getUserRooms">
<br><br>
<textarea id="content" maxlength="1000" cols="40" rows="5" placeholder="請(qǐng)輸入內(nèi)容"></textarea>
<button id="send" style="width: 100px;">發(fā)送消息</button>
</div>
<hr style="height:1px;border:none;border-top:1px solid black;" />
</div>
<div style="float: left;margin-left: 50px;">
<h3>SocketIO互動(dòng)消息</h3>
<button id="clean" style="width: 100px;">清理輸出</button>
<div id="console" class="well"></div>
</div>
</body>
<script type="text/javascript">
var socket ;
var errorCount = 0;
var isConnected = false;
var maxError = 5;
//連接
function connect(url) {
//var opts = {
// query: 'userId='+userId
//};
//socket = io.connect(url, opts);
socket = io.connect(url);
//socket.nsp = "/socketIO";//定義命名空間
console.log(socket)
//監(jiān)聽本次連接回調(diào)函數(shù)
socket.on('connect', function () {
isConnected =true;
console.log("連接成功");
serverOutput('<span class="connect-msg"><font color="blue">'+getNowTime()+' </font>連接成功</span>');
errorCount=0;
});
//監(jiān)聽消息
socket.on('message', function (data) {
output('<span class="connect-msg"><font color="blue">'+getNowTime()+' </font>' + data + ' </span>');
console.log(data);
});
//監(jiān)聽斷開
socket.on('disconnect', function () {
isConnected =false;
console.log("連接斷開");
serverOutput('<span class="disconnect-msg"><font color="blue">'+getNowTime()+' </font>' + '已下線! </span>');
});
//監(jiān)聽斷開錯(cuò)誤
socket.on('connect_error', function(data){
serverOutput('<span class="disconnect-msg"><font color="blue">'+getNowTime()+' </font>;' + '連接錯(cuò)誤-'+data+' </span>');
errorCount++;
if(errorCount>=maxError){
socket.disconnect();
}
});
//監(jiān)聽連接超時(shí)
socket.on('connect_timeout', function(data){
serverOutput('<span class="disconnect-msg"><font color="blue">'+getNowTime()+' </font>' + '連接超時(shí)-'+data+' </span>');
errorCount++;
if(errorCount>=maxError){
socket.disconnect();
}
});
//監(jiān)聽錯(cuò)誤
socket.on('error', function(data){
serverOutput('<span class="disconnect-msg"><font color="blue">'+getNowTime()+' </font>' + '系統(tǒng)錯(cuò)誤-'+data+' </span>');
errorCount++;
if(errorCount>=maxError){
socket.disconnect();
}
});
/*socket.on('ack', function(data){
console.log("ack:"+data)
var str = '消息發(fā)送失敗';
if(data==1){
str = '消息發(fā)送成功';
}
serverOutput('<span class="connect-msg"><font color="blue">'+getNowTime()+' </font>' + str+' </span>');
});*/
}
function output(message) {
var element = $("<div>" + " " + message + "</div>");
$('#console').prepend(element);
}
function serverOutput(message) {
var element = $("<div>" + message + "</div>");
$('#console').prepend(element);
}
//連接
$("#connect").click(function(){
if(!isConnected){
var url = $("#url").val();
connect(url);
}else {
serverOutput('<span class="disconnect-msg"><font color="blue">'+getNowTime()+' </font>' + '已經(jīng)成功建立連接,不要重復(fù)建立?。?! </span>');
}
})
//斷開連接
$("#disconnect").click(function(){
if(isConnected){
socket.disconnect();
}
})
//發(fā)送消息
$("#send").click(function(){
var socketEvent = $("#socketEvent").val();//自定義的事件名稱
var content = $("#content").val();//發(fā)送的內(nèi)容
socket.emit(socketEvent,content,function(data1,data2){
console.log("ack1:"+data1);
console.log("ack2:"+data2);
});
})
//清理消息
$("#clean").click(function(){
$('#console').html("");
})
function getNowTime(){
var date=new Date();
var year=date.getFullYear(); //獲取當(dāng)前年份
var mon=date.getMonth()+1; //獲取當(dāng)前月份
var da=date.getDate(); //獲取當(dāng)前日
var h=date.getHours(); //獲取小時(shí)
var m=date.getMinutes(); //獲取分鐘
var s=date.getSeconds(); //獲取秒
var ms=date.getMilliseconds();
var d=document.getElementById('Date');
var date =year+'/'+mon+'/'+da+' '+h+':'+m+':'+s+':'+ms;
return date;
}
</script>
</html>
html效果如下:
3、簡(jiǎn)單的演示
自己點(diǎn)擊建立連接和斷開連接按鈕測(cè)試玩下
ps:http://localhost:9999?userId=12345是沒有命名空間的請(qǐng)求
三、廣播
1、SocketIO基礎(chǔ)概念圖
SocketIO、namespace(命名空間)、room(房間)的關(guān)系如下:
SocketIO廣播是以namespace或者room為維度的,具體如下:
如果不定義namespace,默認(rèn)是/
如果定義了namespace,沒有定義room,房間默認(rèn)的名字和namespace一樣。
2、定義namespace
你也可以這樣定義
server.addNamespace(“/test”);
server.addNamespace(“/socketIO”);
3、創(chuàng)建namespace所屬的Handler
3.1 自定義Handler
package com.gzgs.socketio.common.handler;
import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.annotation.OnEvent;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class TestHandler {
//測(cè)試使用
@OnEvent("testHandler")
public void testHandler(SocketIOClient client, String data, AckRequest ackRequest) throws JsonProcessingException {
log.info("MyTestHandler:{}",data);
if(ackRequest.isAckRequested()){
//返回給客戶端,說我接收到了
ackRequest.sendAckData("MyTestHandler",data);
}
}
}
package com.gzgs.socketio.common.handler;
import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.annotation.OnEvent;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class SocketIOHandler {
//測(cè)試使用
@OnEvent("socketIOHandler")
public void testHandler(SocketIOClient client, String data, AckRequest ackRequest) throws JsonProcessingException {
log.info("SocketIOHandler:{}",data);
if(ackRequest.isAckRequested()){
//返回給客戶端,說我接收到了
ackRequest.sendAckData("SocketIOHandler",data);
}
}
}
3.2 監(jiān)聽自定義Handler
在啟動(dòng)類的SocketIO監(jiān)聽里面加入監(jiān)聽
package com.gzgs.socketio;
import com.corundumstudio.socketio.SocketIOServer;
import com.gzgs.socketio.common.handler.SocketIOHandler;
import com.gzgs.socketio.common.handler.TestHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;
@SpringBootApplication
public class SocketioServerApplication {
public static void main(String[] args) {
SpringApplication.run(SocketioServerApplication.class, args);
}
}
@Component
@Slf4j
class SocketIOServerRunner implements CommandLineRunner, DisposableBean {
@Autowired
private SocketIOServer socketIOServer;
@Autowired
private TestHandler testHandler;
@Autowired
private SocketIOHandler socketIOHandler;
@Override
public void run(String... args) throws Exception {
//namespace分別交給各自的Handler監(jiān)聽,這樣就可以隔離,只有客戶端指定namespace,才能訪問對(duì)應(yīng)Handler。
//比如:http://localhost:9999/test?userId=12345
socketIOServer.getNamespace("/test").addListeners(testHandler);
socketIOServer.getNamespace("/socketIO").addListeners(socketIOHandler);
socketIOServer.start();
log.info("SocketIOServer==============================啟動(dòng)成功");
}
@Override
public void destroy() throws Exception {
socketIOServer.stop();
log.info("SocketIOServer==============================關(guān)閉成功");
}
}
3.3演示
3.3.1 正確演示
3.3.1 錯(cuò)誤演示
文章來源:http://www.zghlxwxcb.cn/news/detail-690801.html
四、常用方法
其他的一些測(cè)試我寫在下面的代碼上,自己去測(cè)試才能更好的理解文章來源地址http://www.zghlxwxcb.cn/news/detail-690801.html
1、加入房間
//加入房間
@OnEvent("joinRoom")
public void joinRooms(SocketIOClient client, String data, AckRequest ackRequest) throws JsonProcessingException {
client.joinRoom(data);
if(ackRequest.isAckRequested()){
//返回給客戶端,說我接收到了
ackRequest.sendAckData("加入房間","成功");
}
}
2、離開房間
//離開房間
@OnEvent("leaveRoom")
public void leaveRoom(SocketIOClient client, String data, AckRequest ackRequest) throws JsonProcessingException {
client.leaveRoom(data);
if(ackRequest.isAckRequested()){
//返回給客戶端,說我接收到了
ackRequest.sendAckData("離開房間","成功");
}
}
3、獲取用戶所有房間
//獲取該用戶所有房間
@OnEvent("getUserRooms")
public void getUserRooms(SocketIOClient client, String data, AckRequest ackRequest) throws JsonProcessingException {
String userId = client.getHandshakeData().getSingleUrlParam("userId");
Set<String> allRooms = client.getAllRooms();
for (String room:allRooms){
System.out.println("房間名稱:"+room);
}
log.info("服務(wù)器收到消息,客戶端用戶id:{} | 客戶發(fā)送的消息:{} | 是否需要返回給客戶端內(nèi)容:{} ",userId,data,ackRequest.isAckRequested());
if(ackRequest.isAckRequested()){
//返回給客戶端,說我接收到了
ackRequest.sendAckData("你好","哈哈哈");
}
}
4、發(fā)送消息給指定的房間
@OnEvent("sendRoomMessage")
public void sendRoomMessage(SocketIOClient client, String data, AckRequest ackRequest) throws JsonProcessingException {
String userId = client.getHandshakeData().getSingleUrlParam("userId");
Set<String> allRooms = client.getAllRooms();
for (String room:allRooms){
log.info("房間:{}",room);
//發(fā)送給指定空間名稱以及房間的人,并且排除不發(fā)給自己
socketIoServer.getNamespace("/socketIO").getRoomOperations(room).sendEvent("message",client, data);
//發(fā)送給指定空間名稱以及房間的人,包括自己
//socketIoServer.getNamespace("/socketIO").getRoomOperations(room).sendEvent("message", data);;
}
if(ackRequest.isAckRequested()){
//返回給客戶端,說我接收到了
ackRequest.sendAckData("發(fā)送消息到指定的房間","成功");
}
}
5、廣播消息給指定的Namespace下所有客戶端
//廣播消息給指定的Namespace下所有客戶端
@OnEvent("sendNamespaceMessage")
public void sendNamespaceMessage(SocketIOClient client, String data, AckRequest ackRequest) throws JsonProcessingException {
socketIoServer.getNamespace("/socketIO").getBroadcastOperations().sendEvent("message",client, data);;
if(ackRequest.isAckRequested()){
//返回給客戶端,說我接收到了
ackRequest.sendAckData("發(fā)送消息到指定的房間","成功");
}
}
6、點(diǎn)對(duì)點(diǎn)發(fā)送
//點(diǎn)對(duì)點(diǎn)
public void sendMessageOne(String userId) throws JsonProcessingException {
HashMap<UUID, SocketIOClient> userClient = clientCache.getUserClient(userId);
for (UUID sessionId : userClient.keySet()) {
socketIoServer.getNamespace("/socketIO").getClient(sessionId).sendEvent("message", "這是點(diǎn)對(duì)點(diǎn)發(fā)送");
}
}
到了這里,關(guān)于日常記錄-SpringBoot整合netty-socketio的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!