Entwickeln Sie das Go-TCP-Framework von 0 auf 1 [1-Server erstellen, Verbindung und Geschäftsbindung kapseln, Basis-Router implementieren, globale Konfigurationsdatei extrahieren]

Entwickeln Sie das Go-TCP-Framework von 0 auf 1 [1-Build-Server, Paketverbindung und Geschäftsbindung, Implementierung des Basis-Routers]

Dieses Problem schließt hauptsächlich den Aufbau des Servers, die Kapselung der Verbindung und der Geschäftsbindung, die Implementierung des Basis-Routers (des Teils, der das Geschäft abwickelt) und die Extraktion der globalen Konfigurationsdatei des Frameworks ab

  • Lesen Sie Daten aus der Konfigurationsdatei (Server-Abhörport, Abhör-IP usw.) und führen Sie bestimmte Geschäftsvorgänge über einen benutzerdefinierten Router aus

Endgültige Projektstruktur der ersten Version:
Fügen Sie hier eine Bildbeschreibung ein

1 Erstellen Sie den Basisserver [V1.0]

1.1 Serverseitig schreiben

  • Schreiben Sie iserver.go, um die Serverschnittstelle zu definieren
  • Schreiben Sie server.go, definieren Sie die Serverstruktur und implementieren Sie die Schnittstelle

①/zinx/ziface/iserver.go:

package ziface

type IServer interface {
    
    
	Start()
	Stop()
	Serve()
}

②/zinx/znet/server.go

package znet

import (
	"fmt"
	"net"
)

type Server struct {
    
    
	Name      string
	IPVersion string
	IP        string
	Port      int
}

func NewServer(name string) *Server {
    
    
	s := &Server{
    
    
		Name:      name,
		IPVersion: "tcp4",
		IP:        "0.0.0.0",
		Port:      8090,
	}
	return s
}

func (s *Server) Start() {
    
    
	//启动服务监听端口
	fmt.Printf("[start] Server listener at IP:%s, Port %d is starting\n", s.IP, s.Port)

	go func() {
    
    
		addr, err := net.ResolveTCPAddr(s.IPVersion, fmt.Sprintf("%s:%d", s.IP, s.Port))
		if err != nil {
    
    
			fmt.Printf("resolve tcp addr error %v\n", err)
			return
		}
		listener, err := net.ListenTCP(s.IPVersion, addr)
		if err != nil {
    
    
			fmt.Println("listen ", s.IPVersion, " err ", err)
			return
		}
		fmt.Println("[start] Zinx server success ", s.Name, "Listening...")
		//阻塞连接,处理业务
		for {
    
    
			conn, err := listener.AcceptTCP()
			if err != nil {
    
    
				fmt.Println("Accept err ", err)
				continue
			}
			//处理业务:回显消息
			go func() {
    
    
				for {
    
    
					buf := make([]byte, 512)
					cnt, err := conn.Read(buf)
					if err != nil {
    
    
						fmt.Println("read buf err ", err)
						continue
					}
					fmt.Printf("receive client buf %s, cnt %d \n", buf, cnt)
					//回显读取到的字节数
					if _, err := conn.Write(buf[:cnt]); err != nil {
    
    
						fmt.Println("write buf err ", err)
						continue
					}
				}

			}()
		}
	}()
}

func (s *Server) Stop() {
    
    

}

func (s *Server) Serve() {
    
    
	s.Start()
	//阻塞,一直读取客户端所发送过来的消息
	select {
    
    }
}

1.2 Serverseitige Funktionen testen

① Erstellen Sie Server.go und Client.go

  1. Schreiben Sie myDemo/zinxV1.0/Server.go
package main

import "myTest/zinx/znet"

func main() {
    
    
	s := znet.NewServer("[Zinx v1.0]")
	s.Serve()
}
  1. Schreiben Sie myDemo/zinxV1.0/Client.go
package main

import (
	"fmt"
	"net"
	"time"
)

/*
模拟客户端
*/
func main() {
    
    
	fmt.Println("client start...")
	time.Sleep(time.Second * 1)
	//1 创建服务器连接
	conn, err := net.Dial("tcp", "127.0.0.1:8090")
	if err != nil {
    
    
		fmt.Println("client start err ", err)
		return
	}
	for {
    
    
		//2 调用连接向服务器发数据
		_, err := conn.Write([]byte("Hello Zinx v0.1"))
		if err != nil {
    
    
			fmt.Println("write conn err ", err)
			return
		}
		// 3 读取服务器返回的数据
		buf := make([]byte, 512)
		cnt, err := conn.Read(buf)
		if err != nil {
    
    
			fmt.Println("client read buf err ", err)
			return
		}
		fmt.Printf("server call back:%s, cnt=%d\n", buf, cnt)
		//cpu阻塞,让出cpu时间片,避免无限for循环导致其他程序无法获取cpu时间片
		time.Sleep(time.Second * 1)
	}
}

②Testergebnisse

Fügen Sie hier eine Bildbeschreibung ein

Es ist ersichtlich, dass der Server alle 1 Sekunde Daten vom Client empfängt und diese zurückgibt

2 Paketverbindungsverbindung, Geschäftsbindung [V2.0]

In Version V0.1 haben wir ein grundlegendes Server-Framework implementiert. Jetzt müssen wir eine weitere Ebene der Schnittstellenkapselung für Client-Links und verschiedene Geschäfte erstellen, die von verschiedenen Client-Links abgewickelt werden. Natürlich erstellen wir zuerst die Struktur.
Erstellen Sie nun eine Schnittstellendatei iconnection.go unter ziface. Natürlich legen wir die Implementierungsdatei in Connection.go unter znet ab.

Erforderliche Methode:

  1. Verbindung starten
  2. Verbindung beenden
  3. Rufen Sie das verbundene Conn-Objekt ab
  4. Holen Sie sich die ID der Verbindung
  5. Rufen Sie die Adresse und den Port der Clientverbindung ab
  6. Methode zum Senden von Daten
  7. Die Funktion der Geschäftsabwicklung ist durch die Verbindung gebunden

2.1 Paketanbindung

  • Definieren Sie die Iconconnection-Schnittstelle
  • Erstellen Sie eine Verbindungsstruktur und implementieren Sie iconconnection
  1. Erstellen Sie /zinx/ziface/iconnection.go:
package ziface

import "net"

type IConnection interface {
    
    
	//启动连接
	Start()
	//停止连接
	Stop()
	//获取当前连接的Conn对象
	GetTCPConnection() *net.TCPConn
	//获取当前连接模块的id
	GetConnectionID() uint32
	//获取远程客户端的TCP状态 IP:Port
	RemoteAddr() net.Addr
	//发送数据
	Send()
}

//定义一个处理连接业务的方法
type HandleFunc func(*net.TCPConn, []byte, int) error
  1. Erstellen Sie /zinx/znet/connection.go
package znet

import (
	"fmt"
	"myTest/zinx/ziface"
	"net"
)

type Connection struct {
    
    
	Conn      *net.TCPConn
	ConnID    uint32
	isClosed  bool
	handleAPI ziface.HandleFunc
	//告知当前的连接已经退出
	ExitChan chan bool
}

func NewConnection(conn *net.TCPConn, connID uint32, callback_api ziface.HandleFunc) *Connection {
    
    
	c := &Connection{
    
    
		Conn:      conn,
		ConnID:    connID,
		handleAPI: callback_api,
		isClosed:  false,
		ExitChan:  make(chan bool, 1),
	}
	return c
}

func (c *Connection) StartReader() {
    
    
	fmt.Println("reader goroutine is running...")
	defer fmt.Println("connID=", c.ConnID, "Reader is exit, remote addr is ", c.RemoteAddr().String())
	defer c.Stop()
	//读取数据
	for {
    
    
		buf := make([]byte, 512)
		cnt, err := c.Conn.Read(buf)
		if err != nil {
    
    
			fmt.Printf("connID %d receive buf err %s\n", c.ConnID, err)
			continue
		}
		//调用当前所绑定的处理业务的方法HandleAPI
		if err := c.handleAPI(c.Conn, buf, cnt); err != nil {
    
    
			fmt.Println("ConnID", c.ConnID, " handle is err ", err)
			break
		}
	}
}

//启动连接
func (c *Connection) Start() {
    
    
	fmt.Printf("ConnID %d is Start...", c.ConnID)
	go c.StartReader()
}

//停止连接
func (c *Connection) Stop() {
    
    
	fmt.Println("Connection Stop()...ConnectionID = ", c.ConnID)
	if c.isClosed {
    
    
		return
	}
	c.isClosed = true
	c.Conn.Close()
	close(c.ExitChan)
}

//获取当前连接的Conn对象
func (c *Connection) GetTCPConnection() *net.TCPConn {
    
    
	return c.Conn
}

//获取当前连接模块的id
func (c *Connection) GetConnectionID() uint32 {
    
    
	return c.ConnID
}

//获取远程客户端的TCP状态 IP:Port
func (c *Connection) RemoteAddr() net.Addr {
    
    
	return c.Conn.RemoteAddr()
}

//发送数据
func (c *Connection) Send() {
    
    

}

2.2 Ändern Sie server.go (realisieren Sie die Geschäftsverarbeitung über die gekapselte Verbindung).

Ändert server.go und fügt die CallBackToClient-Methode hinzu, um bestimmte Geschäfte zu realisieren
Fügen Sie hier eine Bildbeschreibung ein

Ersetzen Sie den verarbeitenden Geschäftslogikteil von server.go in der ZinxV1.0-Version durch den gekapselten Conn, um
Fügen Sie hier eine Bildbeschreibung ein
alle Codes aufzurufen:
/zinx/znet/server.go:

package znet

import (
	"fmt"
	"github.com/kataras/iris/v12/x/errors"
	"net"
)

type Server struct {
    
    
	Name      string
	IPVersion string
	IP        string
	Port      int
}

func NewServer(name string) *Server {
    
    
	s := &Server{
    
    
		Name:      name,
		IPVersion: "tcp4",
		IP:        "0.0.0.0",
		Port:      8090,
	}
	return s
}

//定义当前客户端连接所绑定的handleAPI(暂时写死处理业务逻辑:数据回显)
func CallBackToClient(conn *net.TCPConn, data []byte, cnt int) error {
    
    
	fmt.Println("[Conn handle] CallBackToClient....")
	if _, err := conn.Write(data[:cnt]); err != nil {
    
    
		fmt.Println("write buf err ", err)
		return errors.New("CallBackToClient error")
	}
	return nil
}

func (s *Server) Start() {
    
    
	//启动服务监听端口
	fmt.Printf("[start] Server listener at IP:%s, Port %d is starting\n", s.IP, s.Port)

	go func() {
    
    
		addr, err := net.ResolveTCPAddr(s.IPVersion, fmt.Sprintf("%s:%d", s.IP, s.Port))
		if err != nil {
    
    
			fmt.Printf("resolve tcp addr error %v\n", err)
			return
		}
		listener, err := net.ListenTCP(s.IPVersion, addr)
		if err != nil {
    
    
			fmt.Println("listen ", s.IPVersion, " err ", err)
			return
		}
		fmt.Println("[start] Zinx server success ", s.Name, "Listening...")
		//阻塞连接,处理业务
		for {
    
    
			conn, err := listener.AcceptTCP()
			if err != nil {
    
    
				fmt.Println("Accept err ", err)
				continue
			}
			var cid uint32 = 0
			dealConn := NewConnection(conn, cid, CallBackToClient)
			cid++
			//开启goroutine处理启动当前conn
			go dealConn.Start()
			处理业务:回显消息
			//go func() {
    
    
			//	for {
    
    
			//		buf := make([]byte, 512)
			//		cnt, err := conn.Read(buf)
			//		if err != nil {
    
    
			//			fmt.Println("read buf err ", err)
			//			continue
			//		}
			//		fmt.Printf("receive client buf %s, cnt %d \n", buf, cnt)
			//		//回显读取到的字节数
			//		if _, err := conn.Write(buf[:cnt]); err != nil {
    
    
			//			fmt.Println("write buf err ", err)
			//			continue
			//		}
			//	}
			//
			//}()
		}
	}()
}

func (s *Server) Stop() {
    
    

}

func (s *Server) Serve() {
    
    
	s.Start()
	//阻塞,一直读取客户端所发送过来的消息
	select {
    
    }
}

2.3 Testen Sie die Funktion von ZinkxV2.0

① Ändern Sie den Protokolldruck von Server.go und Client.go

Erstellen Sie /myDemo/ZinxV2.0/Client.go und /myDemo/ZinxV2.0/Server.go. Dieser Teil des Testcodes ist derselbe wie V1.0, ersetzen Sie einfach das Druckprotokoll durch Zinx2.0

  • Client.go
package main

import (
	"fmt"
	"net"
	"time"
)

/*
模拟客户端
*/
func main() {
    
    
	fmt.Println("client start...")
	time.Sleep(time.Second * 1)
	//1 创建服务器连接
	conn, err := net.Dial("tcp", "127.0.0.1:8090")
	if err != nil {
    
    
		fmt.Println("client start err ", err)
		return
	}
	for {
    
    
		//2 调用连接向服务器发数据
		_, err := conn.Write([]byte("Hello Zinx v0.2"))
		if err != nil {
    
    
			fmt.Println("write conn err ", err)
			return
		}
		// 3 读取服务器返回的数据
		buf := make([]byte, 512)
		cnt, err := conn.Read(buf)
		if err != nil {
    
    
			fmt.Println("client read buf err ", err)
			return
		}
		fmt.Printf("server call back:%s, cnt=%d\n", buf, cnt)
		//cpu阻塞,让出cpu时间片,避免无限for循环导致其他程序无法获取cpu时间片
		time.Sleep(time.Second * 1)
	}
}
  • Server.go
package main

import "myTest/zinx/znet"

func main() {
    
    
	s := znet.NewServer("[Zinx v2.0]")
	s.Serve()
}

②Testergebnisse

Fügen Sie hier eine Bildbeschreibung ein

3 Realisieren Sie den Basis-Router [V3.0]

3.1 Kapselung anfordern

Binden Sie Verbindungen und Daten zusammen

zinx/ziface/irequest.go:

package ziface

import "net"

type IRequest interface {
    
    
	GetConnection() *net.TCPConn
	GetData() []byte
}

zinx/znet/request.go:

package znet

import "net"

type Request struct {
    
    
	conn *net.TCPConn
	data []byte
}

func (r *Request) GetConnection() *net.TCPConn {
    
    
	return r.conn
}

func (r *Request) GetData() []byte {
    
    
	return r.data
}

3.2 Router-Modul

zinx/ziface/irouter.go

package ziface

type IRouter interface {
    
    
	//处理请求之前的方法
	PreHandle(request IRequest)
	Handler(request IRequest)
	//处理请求之后的方法
	PostHandler(request IRequest)
}

zinx/znet/router.go

package znet

import "myTest/zinx/ziface"

type BaseRouter struct {
    
    
}

//这里做了空实现,直接让后续Router继承BaseRouter,然后根据需要重写对应方法即可
func (br *BaseRouter) PreHandle(request ziface.IRequest) {
    
    }
func (br *BaseRouter) Handler(request ziface.IRequest)   {
    
    }

func (br *BaseRouter) PostHandler(request ziface.IRequest) {
    
    }

3.3 Framework integriertes Routermodul

  • Brechen Sie das HandlerFunc-Modul in znet/server.go ab und ändern Sie es in Router. Fügen Sie die Router-Eigenschaft in server.go hinzu
    Fügen Sie hier eine Bildbeschreibung ein
    Fügen Sie hier eine Bildbeschreibung ein
  • Ändern Sie den Parameter callback_api ziface.HandleFunc in znet/connection.go in Router
    Fügen Sie hier eine Bildbeschreibung ein

zinx/znet/connection.go

package znet

import (
	"fmt"
	"myTest/zinx/ziface"
	"net"
)

type Connection struct {
    
    
	Conn     *net.TCPConn
	ConnID   uint32
	isClosed bool
	//告知当前的连接已经退出
	ExitChan chan bool
	Router   ziface.IRouter
}

func NewConnection(conn *net.TCPConn, connID uint32, router ziface.IRouter) *Connection {
    
    
	c := &Connection{
    
    
		Conn:   conn,
		ConnID: connID,
		Router: router,
		isClosed: false,
		ExitChan: make(chan bool, 1),
	}
	return c
}

func (c *Connection) StartReader() {
    
    
	fmt.Println("reader goroutine is running...")
	defer fmt.Println("connID=", c.ConnID, "Reader is exit, remote addr is ", c.RemoteAddr().String())
	defer c.Stop()
	//读取数据
	for {
    
    
		buf := make([]byte, 512)
		_, err := c.Conn.Read(buf)
		if err != nil {
    
    
			fmt.Printf("connID %d receive buf err %s\n", c.ConnID, err)
			continue
		}
		//封装请求,改为router处理
		r := Request{
    
    
			conn: c.Conn,
			data: buf,
		}
		go func(request ziface.IRequest) {
    
    
			c.Router.PreHandle(request)
			c.Router.Handler(request)
			c.Router.PostHandler(request)
		}(&r)
	}
}

//启动连接
func (c *Connection) Start() {
    
    
	fmt.Printf("ConnID %d is Start...", c.ConnID)
	go c.StartReader()
}

//停止连接
func (c *Connection) Stop() {
    
    
	fmt.Println("Connection Stop()...ConnectionID = ", c.ConnID)
	if c.isClosed {
    
    
		return
	}
	c.isClosed = true
	c.Conn.Close()
	close(c.ExitChan)
}

//获取当前连接的Conn对象
func (c *Connection) GetTCPConnection() *net.TCPConn {
    
    
	return c.Conn
}

//获取当前连接模块的id
func (c *Connection) GetConnectionID() uint32 {
    
    
	return c.ConnID
}

//获取远程客户端的TCP状态 IP:Port
func (c *Connection) RemoteAddr() net.Addr {
    
    
	return c.Conn.RemoteAddr()
}

//发送数据
func (c *Connection) Send() {
    
    

}

zinx/znet/server.go

package znet

import (
	"fmt"
	"myTest/zinx/ziface"
	"net"
)

type Server struct {
    
    
	Name      string
	IPVersion string
	IP        string
	Port      int
	Router    ziface.IRouter
}

func NewServer(name string) *Server {
    
    
	s := &Server{
    
    
		Name:      name,
		IPVersion: "tcp4",
		IP:        "0.0.0.0",
		Port:      8090,
		Router:    nil,
	}
	return s
}

func (s *Server) Start() {
    
    
	//启动服务监听端口
	fmt.Printf("[start] Server listener at IP:%s, Port %d is starting\n", s.IP, s.Port)

	go func() {
    
    
		addr, err := net.ResolveTCPAddr(s.IPVersion, fmt.Sprintf("%s:%d", s.IP, s.Port))
		if err != nil {
    
    
			fmt.Printf("resolve tcp addr error %v\n", err)
			return
		}
		listener, err := net.ListenTCP(s.IPVersion, addr)
		if err != nil {
    
    
			fmt.Println("listen ", s.IPVersion, " err ", err)
			return
		}
		fmt.Println("[start] Zinx server success ", s.Name, "Listening...")
		//阻塞连接,处理业务
		for {
    
    
			conn, err := listener.AcceptTCP()
			if err != nil {
    
    
				fmt.Println("Accept err ", err)
				continue
			}
			var cid uint32 = 0
			dealConn := NewConnection(conn, cid, s.Router)
			cid++
			//开启goroutine处理启动当前conn
			go dealConn.Start()
		}
	}()
}

func (s *Server) Stop() {
    
    

}

func (s *Server) Serve() {
    
    
	s.Start()
	//阻塞,一直读取客户端所发送过来的消息
	select {
    
    }
}

func (s *Server) AddRouter(router ziface.IRouter) {
    
    
	s.Router = router
}

Testen Sie den integrierten Router-Effekt des Frameworks

myDemo/ZinxV3.0/client.go
package main

import (
	"fmt"
	"net"
	"time"
)

/*
模拟客户端
*/
func main() {
    
    
	fmt.Println("client start...")
	time.Sleep(time.Second * 1)
	//1 创建服务器连接
	conn, err := net.Dial("tcp", "127.0.0.1:8090")
	if err != nil {
    
    
		fmt.Println("client start err ", err)
		return
	}
	for {
    
    
		//2 调用连接向服务器发数据
		_, err := conn.Write([]byte("Hello Zinx v0.3"))
		if err != nil {
    
    
			fmt.Println("write conn err ", err)
			return
		}
		// 3 读取服务器返回的数据
		buf := make([]byte, 512)
		cnt, err := conn.Read(buf)
		if err != nil {
    
    
			fmt.Println("client read buf err ", err)
			return
		}
		fmt.Printf("server call back:%s, cnt=%d\n", buf, cnt)
		//cpu阻塞,让出cpu时间片,避免无限for循环导致其他程序无法获取cpu时间片
		time.Sleep(time.Second * 1)
	}
}
myDemo/ZinxV3.0/server.go
package main

import (
	"fmt"
	"myTest/zinx/ziface"
	"myTest/zinx/znet"
)

//自定义一个Router,测试路由功能
type PingRouter struct {
    
    
	znet.BaseRouter
}

func (pr *PingRouter) PreHandle(request ziface.IRequest) {
    
    
	_, err := request.GetConnection().Write([]byte("pre handle success..."))
	if err != nil {
    
    
		fmt.Println("server call pre handle err ", err)
		return
	}
	fmt.Println("server call pre handle...")
}

func (pr *PingRouter) Handler(request ziface.IRequest) {
    
    
	_, err := request.GetConnection().Write([]byte("handle success..."))
	if err != nil {
    
    
		fmt.Println("server call handle err ", err)
		return
	}
	fmt.Println("server call handler....")
}

func (pr *PingRouter) PostHandler(request ziface.IRequest) {
    
    
	_, err := request.GetConnection().Write([]byte("post handle success..."))
	if err != nil {
    
    
		fmt.Println("server call post handle err ", err)
		return
	}
	fmt.Println("server call post handler...")
}

func main() {
    
    
	s := znet.NewServer("[Zinx v3.0]")
	//添加自定义路由
	router := &PingRouter{
    
    }
	s.AddRouter(router)
	s.Serve()
}

Endeffekt:
Fügen Sie hier eine Bildbeschreibung ein

Gemäß dem Entwurfsmuster der Vorlagenmethode wird der Aufruf abgeschlossen

4 Extrahieren Sie die globale Konfigurationsdatei [V4.0]

4.1 Schreiben Sie /zinx/util/globalobj.go

Wird hauptsächlich zum Lesen der Informationen der ZINX-Konfigurationsdatei verwendet

package util

import (
	"encoding/json"
	"io/ioutil"
	"myTest/zinx/ziface"
)

type GlobalObj struct {
    
    
	TCPServer ziface.IServer //当前全局Zinx的server对象
	Host      string         //当前服务器主机监听的ip
	TcpPort   int            //当前服务器主机监听的端口号
	Name      string         //当前服务器的名称

	Version        string //当前Zinx的版本号
	MaxConn        int    //当前服务器所允许的最大连接数
	MaxPackageSize uint32 //当前Zinx框架数据包的最大值
}

var GlobalObject *GlobalObj

//从配置文件中重新加载GlobalObject的信息
func (g *GlobalObj) Reload() {
    
    
	data, err := ioutil.ReadFile("conf/zinx.json")
	if err != nil {
    
    
		panic(err)
	}
	//将json文件数据解析到struct中
	err = json.Unmarshal(data, &GlobalObject)
	if err != nil {
    
    
		panic(err)
	}
}

//在其他文件导入该util包的时候会加载init
func init() {
    
    
	GlobalObject = &GlobalObj{
    
    
		Name:           "ZinxServerApp",
		Version:        "V0.4",
		TcpPort:        8090,
		Host:           "0.0.0.0",
		MaxConn:        120,
		MaxPackageSize: 4096,
	}
	//尝试从conf/zinx.json中去加载用户自定义的参数
	GlobalObject.Reload()
}

4.2 Ersetzen Sie vorher den Hardcode in server.go

Fügen Sie die Abschnitte /zinx/znet/server.go und /zinx/znet/connection.go hinzu

  • Server:
    Fügen Sie hier eine Bildbeschreibung ein
  • Verbindung:Fügen Sie hier eine Bildbeschreibung ein

4.3 Testen

Schreiben Sie myDemo/ZinxV4.0

  • Und schreiben Sie die entsprechende .json-Konfigurationsdatei (Client.go und Server.go sind identisch mit V3.0).

Fügen Sie hier eine Bildbeschreibung ein
zinx.json

{
    
    
  "Name": "Zinx Server Application",
  "Version": "V0.4",
  "Host": "0.0.0.0",
  "TcpPort": 8091,
  "MaxConn": 30,
  "MaxPackageSize": 1024
}

Endeffekt:
Fügen Sie hier eine Bildbeschreibung ein

Referenz: https://www.yuque.com/aceld/npyr8s/bgftov

Acho que você gosta

Origin blog.csdn.net/weixin_45565886/article/details/131973229
Recomendado
Clasificación