亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務,提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

對于高性能的 RPC 框架,Netty 作為異步通信框架,幾乎成為必備品。例如,Dubbo 框架中通信組件,還有 RocketMQ 中生產(chǎn)者和消費者的通信,都使用了 Netty。今天,我們來看看 Netty 的基本架構(gòu)和原理。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

 

Netty 的特點與 NIO

Netty 是一個異步的、基于事件驅(qū)動的網(wǎng)絡應用框架,它可以用來開發(fā)高性能服務端和客戶端。

以前編寫網(wǎng)絡調(diào)用程序的時候,我們都會在客戶端創(chuàng)建一個 Socket,通過這個 Socket 連接到服務端。

服務端根據(jù)這個 Socket 創(chuàng)建一個 Thread,用來發(fā)出請求。客戶端在發(fā)起調(diào)用以后,需要等待服務端處理完成,才能繼續(xù)后面的操作。這樣線程會出現(xiàn)等待的狀態(tài)。

如果客戶端請求數(shù)越多,服務端創(chuàng)建的處理線程也會越多,JVM 如此多的線程并不是一件容易的事。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

使用阻塞 I/O 處理多個連接

為了解決上述的問題,推出了 NIO 的概念,也就是(Non-blocking I/O)。其中,Selector 機制就是 NIO 的核心。

當每次客戶端請求時,會創(chuàng)建一個 Socket Channel,并將其注冊到 Selector 上(多路復用器)。

然后,Selector 關注服務端 IO 讀寫事件,此時客戶端并不用等待 IO 事件完成,可以繼續(xù)做接下來的工作。

一旦,服務端完成了 IO 讀寫操作,Selector 會接到通知,同時告訴客戶端 IO 操作已經(jīng)完成。

接到通知的客戶端,就可以通過 SocketChannel 獲取需要的數(shù)據(jù)了。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

NIO 機制與 Selector

上面描述的過程有點異步的意思,不過,Selector 實現(xiàn)的并不是真正意義上的異步操作。

因為 Selector 需要通過線程阻塞的方式監(jiān)聽 IO 事件變更,只是這種方式?jīng)]有讓客戶端等待,是 Selector 在等待 IO 返回,并且通知客戶端去獲取數(shù)據(jù)。真正“異步 IO”(AIO)這里不展開介紹,有興趣可以自行查找。

說好了 NIO 再來談談 Netty,Netty 作為 NIO 的實現(xiàn),它適用于服務器/客戶端通訊的場景,以及針對于 TCP 協(xié)議下的高并發(fā)應用。

對于開發(fā)者來說,它具有以下特點:

  • 對 NIO 進行封裝,開發(fā)者不需要關注 NIO 的底層原理,只需要調(diào)用 Netty 組件就能夠完成工作。
  • 對網(wǎng)絡調(diào)用透明,從 Socket 建立 TCP 連接到網(wǎng)絡異常的處理都做了包裝。
  • 對數(shù)據(jù)處理靈活, Netty 支持多種序列化框架,通過“ChannelHandler”機制,可以自定義“編/解碼器”。
  • 對性能調(diào)優(yōu)友好,Netty 提供了線程池模式以及 Buffer 的重用機制(對象池化),不需要構(gòu)建復雜的多線程模型和操作隊列。

從一個簡單的例子開始

開篇講到了,為了滿足高并發(fā)下網(wǎng)絡請求,引入了 NIO 的概念。Netty 是針對 NIO 的實現(xiàn),在 NIO 封裝,網(wǎng)絡調(diào)用,數(shù)據(jù)處理以及性能優(yōu)化等方面都有不俗的表現(xiàn)。

學習架構(gòu)最容易的方式就是從實例入手,從客戶端訪問服務端的代碼來看看 Netty 是如何運作的。再一次介紹代碼中調(diào)用的組件以及組件的工作原理。

假設有一個客戶端去調(diào)用一個服務端,假設服務端叫做 EchoServer,客戶端叫做 EchoClient,用 Netty 架構(gòu)實現(xiàn)代碼如下。

服務端代碼

構(gòu)建服務器端,假設服務器接受客戶端傳來的信息,然后在控制臺打印。首先,生成 EchoServer,在構(gòu)造函數(shù)中傳入需要監(jiān)聽的端口號。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

構(gòu)造函數(shù)中傳入需要監(jiān)聽的端口號

接下來就是服務的啟動方法:

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

啟動 NettyServer 的 Start 方法

Server 的啟動方法涉及到了一些組件的調(diào)用,例如 EventLoopGroup,Channel。這些會在后面詳細講解。

這里有個大致的印象就好:

  • 創(chuàng)建 EventLoopGroup。
  • 創(chuàng)建 ServerBootstrap。
  • 指定所使用的 NIO 傳輸 Channel。
  • 使用指定的端口設置套接字地址。
  • 添加一個 ServerHandler 到 Channel 的 ChannelPipeline。
  • 異步地綁定服務器;調(diào)用 sync() 方法阻塞等待直到綁定完成。
  • 獲取 Channel 的 CloseFuture,并且阻塞當前線程直到它完成。
  • 關閉 EventLoopGroup,釋放所有的資源。

NettyServer 啟動以后會監(jiān)聽某個端口的請求,當接受到了請求就需要處理了。在 Netty 中客戶端請求服務端,被稱為“入站”操作。

可以通過 ChannelInboundHandlerAdapter 實現(xiàn),具體內(nèi)容如下:

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

處理來自客戶端的請求

從上面的代碼可以看出,服務端處理的代碼包含了三個方法。這三個方法都是根據(jù)事件觸發(fā)的。

他們分別是:

  • 當接收到消息時的操作,channelRead。
  • 消息讀取完成時的方法,channelReadComplete。
  • 出現(xiàn)異常時的方法,exceptionCaught。

客戶端代碼

客戶端和服務端的代碼基本相似,在初始化時需要輸入服務端的 IP 和 Port。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

 

同樣在客戶端啟動函數(shù)中包括以下內(nèi)容:

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

 

客戶端啟動程序的順序:

  • 創(chuàng)建 Bootstrap。
  • 指定 EventLoopGroup 用來監(jiān)聽事件。
  • 定義 Channel 的傳輸模式為 NIO(Non-BlockingInputOutput)。
  • 設置服務器的 InetSocketAddress。
  • 在創(chuàng)建 Channel 時,向 ChannelPipeline 中添加一個 EchoClientHandler 實例。
  • 連接到遠程節(jié)點,阻塞等待直到連接完成。
  • 阻塞,直到 Channel 關閉。
  • 關閉線程池并且釋放所有的資源。

客戶端在完成以上操作以后,會與服務端建立連接從而傳輸數(shù)據(jù)。同樣在接受到 Channel 中觸發(fā)的事件時,客戶端會觸發(fā)對應事件的操作。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

 

例如 Channel 激活,客戶端接受到服務端的消息,或者發(fā)生異常的捕獲。

從代碼結(jié)構(gòu)上看還是比較簡單的。服務端和客戶端分別初始化創(chuàng)建監(jiān)聽和連接。然后分別定義各自的 Handler 處理對方的請求。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

服務端/客戶端初始化和事件處理

Netty 核心組件

通過上面的簡單例子,發(fā)現(xiàn)有些 Netty 組件在服務初始化以及通訊時被用到,下面就來介紹一下這些組件的用途和關系。

①Channel

通過上面例子可以看出,當客戶端和服務端連接的時候會建立一個 Channel。

這個 Channel 我們可以理解為 Socket 連接,它負責基本的 IO 操作,例如:bind(),connect(),read(),write() 等等。

簡單的說,Channel 就是代表連接,實體之間的連接,程序之間的連接,文件之間的連接,設備之間的連接。同時它也是數(shù)據(jù)入站和出站的載體。

②EventLoop 和 EventLoopGroup

既然有了 Channel 連接服務,讓信息之間可以流動。如果服務發(fā)出的消息稱作“出站”消息,服務接受的消息稱作“入站”消息。那么消息的“出站”/“入站”就會產(chǎn)生事件(Event)。

例如:連接已激活;數(shù)據(jù)讀取;用戶事件;異常事件;打開鏈接;關閉鏈接等等。

順著這個思路往下想,有了數(shù)據(jù),數(shù)據(jù)的流動產(chǎn)生事件,那么就有一個機制去監(jiān)控和協(xié)調(diào)事件。

這個機制(組件)就是 EventLoop。在 Netty 中每個 Channel 都會被分配到一個 EventLoop。一個 EventLoop 可以服務于多個 Channel。

每個 EventLoop 會占用一個 Thread,同時這個 Thread 會處理 EventLoop 上面發(fā)生的所有 IO 操作和事件(Netty 4.0)。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

EventLoop 與 Channel 關系

理解了 EventLoop,再來說 EventLoopGroup 就容易了,EventLoopGroup 是用來生成 EventLoop 的,還記得例子代碼中第一行就 new 了 EventLoopGroup 對象。

一個 EventLoopGroup 中包含了多個 EventLoop 對象。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

創(chuàng)建 EventLoopGroup

EventLoopGroup 要做的就是創(chuàng)建一個新的 Channel,并且給它分配一個 EventLoop。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

EventLoopGroup,EventLoop 和 Channel 的關系

在異步傳輸?shù)那闆r下,一個 EventLoop 是可以處理多個 Channel 中產(chǎn)生的事件的,它主要的工作就是事件的發(fā)現(xiàn)以及通知。

相對于以前一個 Channel 就占用一個 Thread 的情況。Netty 的方式就要合理多了。

客戶端發(fā)送消息到服務端,EventLoop 發(fā)現(xiàn)以后會告訴服務端:“你去獲取消息”,同時客戶端進行其他的工作。

當 EventLoop 檢測到服務端返回的消息,也會通知客戶端:“消息返回了,你去取吧“。客戶端再去獲取消息。整個過程 EventLoop 就是監(jiān)視器+傳聲筒。

③ChannelHandler,ChannelPipeline 和 ChannelHandlerContext

如果說 EventLoop 是事件的通知者,那么 ChannelHandler 就是事件的處理者。

在 ChannelHandler 中可以添加一些業(yè)務代碼,例如數(shù)據(jù)轉(zhuǎn)換,邏輯運算等等。

正如上面例子中展示的,Server 和 Client 分別都有一個 ChannelHandler 來處理,讀取信息,網(wǎng)絡可用,網(wǎng)絡異常之類的信息。

并且,針對出站和入站的事件,有不同的 ChannelHandler,分別是:

  • ChannelInBoundHandler(入站事件處理器)
  • ChannelOutBoundHandler(出站事件處理器)
從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

 

假設每次請求都會觸發(fā)事件,而由 ChannelHandler 來處理這些事件,這個事件的處理順序是由 ChannelPipeline 來決定的。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

ChannelHanlder 處理,出站/入站的事件

ChannelPipeline 為 ChannelHandler 鏈提供了容器。到 Channel 被創(chuàng)建的時候,會被 Netty 框架自動分配到 ChannelPipeline 上。

ChannelPipeline 保證 ChannelHandler 按照一定順序處理事件,當事件觸發(fā)以后,會將數(shù)據(jù)通過 ChannelPipeline 按照一定的順序通過 ChannelHandler。

說白了,ChannelPipeline 是負責“排隊”的。這里的“排隊”是處理事件的順序。

同時,ChannelPipeline 也可以添加或者刪除 ChannelHandler,管理整個隊列。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

 

如上圖,ChannelPipeline 使 ChannelHandler 按照先后順序排列,信息按照箭頭所示方向流動并且被 ChannelHandler 處理。

說完了 ChannelPipeline 和 ChannelHandler,前者管理后者的排列順序。那么它們之間的關聯(lián)就由 ChannelHandlerContext 來表示了。

每當有 ChannelHandler 添加到 ChannelPipeline 時,同時會創(chuàng)建 ChannelHandlerContext 。

ChannelHandlerContext 的主要功能是管理 ChannelHandler 和 ChannelPipeline 的交互。

不知道大家注意到?jīng)]有,開始的例子中 ChannelHandler 中處理事件函數(shù),傳入的參數(shù)就是 ChannelHandlerContext。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

 

ChannelHandlerContext 參數(shù)貫穿 ChannelPipeline,將信息傳遞給每個 ChannelHandler,是個合格的“通訊員”。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

ChannelHandlerContext 負責傳遞消息

把上面提到的幾個核心組件歸納一下,用下圖表示方便記憶他們之間的關系。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

Netty 核心組件關系圖

Netty 的數(shù)據(jù)容器

前面介紹了 Netty 的幾個核心組件,服務器在數(shù)據(jù)傳輸?shù)臅r候,產(chǎn)生事件,并且對事件進行監(jiān)控和處理。

接下來看看數(shù)據(jù)是如何存放以及是如何讀寫的。Netty 將 ByteBuf 作為數(shù)據(jù)容器,來存放數(shù)據(jù)。

ByteBuf 工作原理

從結(jié)構(gòu)上來說,ByteBuf 由一串字節(jié)數(shù)組構(gòu)成。數(shù)組中每個字節(jié)用來存放信息。

ByteBuf 提供了兩個索引,一個用于讀取數(shù)據(jù),一個用于寫入數(shù)據(jù)。這兩個索引通過在字節(jié)數(shù)組中移動,來定位需要讀或者寫信息的位置。

當從 ByteBuf 讀取,它的 readerIndex(讀索引)將會根據(jù)讀取的字節(jié)數(shù)遞增。

同樣,當寫 ByteBuf 時,它的 writerIndex 也會根據(jù)寫入的字節(jié)數(shù)進行遞增。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

ByteBuf 讀寫索引圖例

需要注意的是極限的情況是 readerIndex 剛好讀到了 writerIndex 寫入的地方。

如果 readerIndex 超過了 writerIndex 的時候,Netty 會拋出 IndexOutOf-BoundsException 異常。

ByteBuf 使用模式

談了 ByteBuf 的工作原理以后,再來看看它的使用模式。

根據(jù)存放緩沖區(qū)的不同分為三類:

  • 堆緩沖區(qū),ByteBuf 將數(shù)據(jù)存儲在 JVM 的堆中,通過數(shù)組實現(xiàn),可以做到快速分配。由于在堆上被 JVM 管理,在不被使用時可以快速釋放。可以通過 ByteBuf.array() 來獲取 byte[] 數(shù)據(jù)。
  • 直接緩沖區(qū),在 JVM 的堆之外直接分配內(nèi)存,用來存儲數(shù)據(jù)。其不占用堆空間,使用時需要考慮內(nèi)存容量。它在使用 Socket 傳遞時性能較好,因為間接從緩沖區(qū)發(fā)送數(shù)據(jù),在發(fā)送之前 JVM 會先將數(shù)據(jù)復制到直接緩沖區(qū)再進行發(fā)送。由于,直接緩沖區(qū)的數(shù)據(jù)分配在堆之外,通過 JVM 進行垃圾回收,并且分配時也需要做復制的操作,因此使用成本較高。
  • 復合緩沖區(qū),顧名思義就是將上述兩類緩沖區(qū)聚合在一起。Netty 提供了一個 CompsiteByteBuf,可以將堆緩沖區(qū)和直接緩沖區(qū)的數(shù)據(jù)放在一起,讓使用更加方便。

ByteBuf 的分配

聊完了結(jié)構(gòu)和使用模式,再來看看 ByteBuf 是如何分配緩沖區(qū)的數(shù)據(jù)的。

Netty 提供了兩種 ByteBufAllocator 的實現(xiàn),他們分別是:

  • PooledByteBufAllocator,實現(xiàn)了 ByteBuf 的對象的池化,提高性能減少內(nèi)存碎片。
  • Unpooled-ByteBufAllocator,沒有實現(xiàn)對象的池化,每次會生成新的對象實例。

對象池化的技術和線程池,比較相似,主要目的是提高內(nèi)存的使用率。池化的簡單實現(xiàn)思路,是在 JVM 堆內(nèi)存上構(gòu)建一層內(nèi)存池,通過 allocate 方法獲取內(nèi)存池中的空間,通過 release 方法將空間歸還給內(nèi)存池。

對象的生成和銷毀,會大量地調(diào)用 allocate 和 release 方法,因此內(nèi)存池面臨碎片空間回收的問題,在頻繁申請和釋放空間后,內(nèi)存池需要保證連續(xù)的內(nèi)存空間,用于對象的分配。

基于這個需求,有兩種算法用于優(yōu)化這一塊的內(nèi)存分配:伙伴系統(tǒng)和 slab 系統(tǒng)。

伙伴系統(tǒng),用完全二叉樹管理內(nèi)存區(qū)域,左右節(jié)點互為伙伴,每個節(jié)點代表一個內(nèi)存塊。內(nèi)存分配將大塊內(nèi)存不斷二分,直到找到滿足所需的最小內(nèi)存分片。

內(nèi)存釋放會判斷釋放內(nèi)存分片的伙伴(左右節(jié)點)是否空閑,如果空閑則將左右節(jié)點合成更大塊內(nèi)存。

slab 系統(tǒng),主要解決內(nèi)存碎片問題,將大塊內(nèi)存按照一定內(nèi)存大小進行等分,形成相等大小的內(nèi)存片構(gòu)成的內(nèi)存集。

按照內(nèi)存申請空間的大小,申請盡量小塊內(nèi)存或者其整數(shù)倍的內(nèi)存,釋放內(nèi)存時,也是將內(nèi)存分片歸還給內(nèi)存集。

Netty 內(nèi)存池管理以 Allocate 對象的形式出現(xiàn)。一個 Allocate 對象由多個 Arena 組成,每個 Arena 能執(zhí)行內(nèi)存塊的分配和回收。

Arena 內(nèi)有三類內(nèi)存塊管理單元:

  • TinySubPage
  • SmallSubPage
  • ChunkList

Tiny 和 Small 符合 Slab 系統(tǒng)的管理策略,ChunkList 符合伙伴系統(tǒng)的管理策略。

當用戶申請內(nèi)存介于 tinySize 和 smallSize 之間時,從 tinySubPage 中獲取內(nèi)存塊。

申請內(nèi)存介于 smallSize 和 pageSize 之間時,從 smallSubPage 中獲取內(nèi)存塊;介于 pageSize 和 chunkSize 之間時,從 ChunkList 中獲取內(nèi)存;大于 ChunkSize(不知道分配內(nèi)存的大小)的內(nèi)存塊不通過池化分配。

Netty 的 Bootstrap

說完了 Netty 的核心組件以及數(shù)據(jù)存儲。再回到最開始的例子程序,在程序最開始的時候會 new 一個 Bootstrap 對象,后面所有的配置都是基于這個對象展開的。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

生成 Bootstrap 對象

Bootstrap 的作用就是將 Netty 核心組件配置到程序中,并且讓他們運行起來。

從 Bootstrap 的繼承結(jié)構(gòu)來看,分為兩類分別是 Bootstrap 和 ServerBootstrap,一個對應客戶端的引導,另一個對應服務端的引導。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

支持客戶端和服務端的程序引導

客戶端引導 Bootstrap,主要有兩個方法 bind() 和 connect()。Bootstrap 通過 bind() 方法創(chuàng)建一個 Channel。

在 bind() 之后,通過調(diào)用 connect() 方法來創(chuàng)建 Channel 連接。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

Bootstrap 通過 bind 和 connect 方法創(chuàng)建連接

服務端引導 ServerBootstrap,與客戶端不同的是在 Bind() 方法之后會創(chuàng)建一個 ServerChannel,它不僅會創(chuàng)建新的 Channel 還會管理已經(jīng)存在的 Channel。

從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

ServerBootstrap 通過 bind 方法創(chuàng)建/管理連接

通過上面的描述,服務端和客戶端的引導存在兩個區(qū)別:

  • ServerBootstrap(服務端引導)綁定一個端口,用來監(jiān)聽客戶端的連接請求。而 Bootstrap(客戶端引導)只要知道服務端 IP 和 Port 建立連接就可以了。
  • Bootstrap(客戶端引導)需要一個 EventLoopGroup,但是 ServerBootstrap(服務端引導)則需要兩個 EventLoopGroup。因為服務器需要兩組不同的 Channel。第一組 ServerChannel 自身監(jiān)聽本地端口的套接字。第二組用來監(jiān)聽客戶端請求的套接字。
從 Spring Boot 程序啟動深入理解 Netty 異步架構(gòu)原理

ServerBootstrap 有兩組 EventLoopGroup

總結(jié)

我們從 NIO 入手,談到了 Selector 的核心機制。然后通過介紹 Netty 客戶端和服務端源代碼運行流程,讓大家對 Netty 編寫代碼有基本的認識。

在 Netty 的核心組件中,Channel 提供 Socket 的連接通道,EventLoop 會對應 Channel 監(jiān)聽其產(chǎn)生的事件,并且通知執(zhí)行者。EventloopGroup 的容器,負責生成和管理 EventLoop。

ChannelPipeline 作為 ChannelHandler 的容器會綁定到 Channel 上,然后由 ChannelHandler 提供具體事件處理。另外,ChannelHandlerContext 為 ChannelHandler 和 ChannelPipeline 提供信息共享。

ByteBuf 作為 Netty 的數(shù)據(jù)容器,通過字節(jié)數(shù)組的方式存儲數(shù)據(jù),并且通過讀索引和寫索引來引導讀寫操作。

上述的核心組件都是通過 Bootstrap 來配置并且引導啟動的,Bootstrap 啟動方式雖然一致,但是針對客戶端和服務端有些許的區(qū)別。

 

來源:https://mp.weixin.qq.com/s/Sosyv2pRrB8ry471mk5w6g

原文《高性能底層怎么運作?一文幫你吃透Netty架構(gòu)原理》

侵刪

分享到:
標簽:架構(gòu)
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數(shù)有氧達人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定