在咱們初期學習Unity3D培訓目標:讓U3D初學者能夠更快速的掌握U3D技術,自行製做修改素材,能夠獨立完成2D、3D小規模遊戲及網頁遊戲開發。後面就應該朝着主程的方面前進php
今天給你們講一下如何作一個好的主程前端
假如,我如今接手一個新項目,個人身份仍是主程序。在下屬人員一一到位以前,在和製做人以及主策劃充分溝通後,我須要先獨自思考如下問題:java
一、服務器跑在什麼樣的操做系統環境下?
二、採用哪幾種語言開發?主要是什麼?
三、服務器和客戶端以什麼樣的接口通信?
四、採用哪些第三方的類庫?mysql
除了技術背景以外,考慮這些問題的時候必定要充分考慮項目需求和所能擁有的資源。程序員
我以爲,先不要想一組須要幾臺機器各有什麼功能這樣的問題,也不要想須要多少個daemon進程。假設就一臺服務器,就一個進程,把所須要的資源往最小了考慮,把架構往最簡單的方向想,直到發現,「哦,這麼作沒法知足策劃要求的併發量」,再去修改設計方案。web
操做系統:越單一越好。雖然FreeBSD的網絡性能更好、雖然Solaris很是穩定,但選什麼就是什麼,最好別混着來。前端是FreeBSD,後端是Solaris,運營的人會苦死。也不要瞧不起用Windows的人,用Windows照樣也能支持一組一萬人在線,總之,能知足策劃需求,好招程序員,運營成本低是要點。不一樣的操做系統有不一樣的特性,若是你真的對它們都很熟悉,那麼一定能找到一個理由,一個足夠充分的理由讓你選擇A而不是B而不是C。但作決策的時候要注意不要因小失大。面試
Programming Language:傳統來講,基本都是C/C++。可是你也知道,這東西門檻很高,好的C/C++程序員很難招。用Perl/Python/Lua行不行?固然能夠。可是純腳本也很差,一般來講是混合着來。你要明白哪些是關鍵部分,我是說執行次數最多的地方而不是說元寶,這些必須用性能高的語言實現(好比C/C++好比Java),其它像節日活動這樣好久才執行一次的,隨便吧。腳本的好處是,能夠快速搭原型。因此,儘早的,在你作完基本的地圖和戰鬥模塊以後,立馬跑機器人測試吞吐量。這時候項目開發進度還不到10%,不行就趕忙改。
此處特別舉個例子就是Java GC的問題。既然你要用java,而jvm須要經過執行garbage collection來回收內存,而garbage collection會使整個應用停頓,那你不妨試一試,內存在達到峯值的時候會停多久?策劃能夠接受嗎?若是不能夠,你能夠採用其它的GC策略再試一試。這個問題應該不是Java獨有的。網遊和網站應用相比它很注重流暢性。這是你務必須要考慮的。sql
至於選擇什麼樣的腳本語言,以及腳本在你的遊戲中到底是佔80%仍是20%?須要根據需求來看。有沒有遊戲徹底不用腳本?有。有沒有遊戲濫用腳本?也有。若是你引入腳本的目的是由於策劃不會C/C++而你但願策劃能本身獨立實現更多的遊戲功能。你但願策劃去寫腳本?腳本也是程序,策劃寫的腳本難道就比程序員寫腳本好?仍是由於策劃工資便宜?策劃由於腳本寫錯了致使大故障還少嗎(此處特別以網易的產品舉例)?綜合權衡下,仍是算了吧。問問你一塊兒工做的程序員哥們兒,他們最喜歡什麼語言,什麼用起來最順手,就用什麼當腳本。注意不光要考慮開發速度快,還要考慮調試方便。數據庫
整體來講,操做系統和編程語言的選擇,隨大流便可。標新立異沒什麼好處。小地方的實現你能夠玩玩,總體仍是要越保守越好。編程
而後說通信的問題。服務器和客戶端怎麼鏈接上的?
往最下面看,物理和鏈路層。有多是以太網,有多是ADSL,在北京還有不少像歌華寬帶這樣的採用75歐同軸電纜或者電力線上網的。你不要企圖在這一層作什麼優化,你要充分考慮的是不一樣的網絡傳輸媒質網絡延遲不同。更噁心的是你正常的數據包可能會被某些網吧的SB路由器當作P2P數據包給封掉,或是甚至被解析成Wake-On-Lan這樣的含義。楊建還會給你講,什麼是MTU,把數據包限制在多大才能儘可能讓請求在一個包內發完。是的,這些很精細的東西,等咱遊戲作的差很少了再慢慢研究。先略過。
往上看,IP層。再往上,你要考慮用TCP仍是UDP或是兩者混合。UDP的優點是overhead小、延遲低,典型的用例就是《天下貳》,聽說是純UDP。再好比《龍之谷》,聽說是有小部分是UDP。負面的一點呢,就是它太過於簡單因此用起來太過於複雜。你要是對本身沒信心,TCP吧,隨大流就好。
往上,採用什麼樣的應用協議。大多數rpc協議都是既支持TCP又支持UDP的。我所用過的有sun rpc、corba、webservice、json、java RMI以及一些專有協議。若是你有精力,仍是本身搞一套吧,網遊所用的東西,仍是越專有越好,給抓包作外掛的人加一點門檻。這裏很是強調的一點,你採用什麼樣的序列化方式與你採用什麼樣的網絡協議是無關的,你的應用協議和你傳輸協議應該也是無關的(既支持TCP又支持UDP的)。若是作框架的人把本身限制的太死或者耦合太緊,那麼用框架的人會很是痛苦。因此,不必在此爲了性能作過多優化。結構簡單清晰是王道。
不少人對網絡開發的認識還停留在定義一個struct、memcpy到socket buffer、send,而後一個勁的給別人強調遇到指針怎麼辦、數組的長度不能超過多少、整個包的長度不能超過多少等等。序列化實際上是面向對象程序設計的一個很核心的要素。連glib/gtk/Berkeley DB這些純C的框架都是基於OOP設計的,因此我以爲您就算是C程序員也不必排斥它。我講這個是說,你應當作應用的人儘量的避免用memcpy/memset這樣的方式初始化數據、傳送數據。若是你是C程序員,你多提供一些g_object_new這樣的函數;若是你是C++程序員,寫好你的構造和析構函數;若是你是JAVA程序員還死活不懂OOP,那算了吧,改行吧。
網絡這一層有些很精妙的東西,尤爲是當你規模擴大須要分佈式擴展的時候。你想一想看爲何sun rpc須要先去rpcbind詢問一次而後才連真正的進程呢?RMI返回的時候爲何須要同時返回IP和端口號呢?web service那麼通用,大部分瀏覽器都支持直接從瀏覽器調用web service那麼爲何主流的方式倒是json呢?
sun rpc是全部RPC機制中歷史最久的吧?它在設計初版的時候,每一個rpc調用都是由一問一答來組成,稱爲two-way messaging。客戶端在發出請求以後,一直等服務器的答覆,若是一直到指定時間後依然沒收到答覆,那麼執行timeout邏輯。在第一個請求收到答覆(或者timeout)以前,沒法發起第二個答覆。直到某一天,Sun的程序發現他們須要異步處理一些事情,因而設計了one-way messaging,客戶端在發起請求的時候,只要把這個東西塞到本地的IO隊列裏,就返回。可是若是socket buffer滿了怎麼辦?仍是會等在那裏。因而以爲這個還不完全,因而又作了Non-Blocking Messaging,在kernel的socket buffer前面加了一個用戶態的rpc buffer,大多數時候它都是空的,當socket buffer堆滿了的時候,再往這裏面塞。若是這個buffer也滿了怎麼辦?我以爲無非就三種處理手段:
1、阻塞。若是這麼作,就是說原本是套非阻塞的設計可是某些狀況下仍是會阻塞?那麼給用的人解釋起來太麻煩用起來也太麻煩。算了。
2、悄然丟棄。 不是全部的數據均可以丟。聊天的無所謂,可是交易的就不行。因此須要在消息類型上加判斷。
3、關閉鏈接。 最簡單粗暴,卻也最有效。
在使用two-way messaging的時候,必定要記住設置超時,免得像某些傻瓜同樣由於一個請求把整個server堵死。可是我以爲timeout設多久徹底是個經驗值,太大了沒做用,過小了失敗的太多。
至少在有一點咱們能夠大鬆一口氣,就是不用擔憂數據量大到須要多網卡同時分擔中斷。一般來講網絡遊戲的流量都是很小的,對玩家來講一個56K的貓或者128K的DSL就夠了。若是你的策劃給你提了一個很BT的需求致使要耗費大量帶寬,那麼你最好把這個應用分到單獨的tcp 鏈接上,免得由於它阻塞而致使關鍵的業務(好比地圖消息)停滯。
我一直想把rpc的部分實現塞到kernel裏。對客戶端的好處是增長了逆向工程的成本,對服務器的好處是網關能夠很高效。就像LVS那樣,前端收完包以後在kernel裏處理完而後馬上轉出去,不用切換到用戶態。而GameServer處理完以後,甚至不用通過網關,直接回復。目的不在於分擔網關的壓力,而是說下降響應延遲。就算讓GameServer承擔部分加密和壓縮的計算量,它的CPU也足夠用。
不過對於網遊,考慮動態擴容爲時太早。通常都是新開幾組服務器。
我在作服務器安裝包的時候,分的很清楚:程序、配置文件、數據庫。
程序,就是編譯好的二進制文件。最好是全靜態編譯,由於它簡單。動態連接的優勢以及其它一些高級話題我後面講,可是一般來講,動態的複雜的結構得不償失。
配置文件整體來講能夠分爲文本文件和二進制文件(廢話)。文本文件的好處是開發過程當中易於調試和修改,最終發佈後也易於追蹤問題。二進制文件的好處是小、精巧、不易把信息泄露給外人知道。java的打jar包的技術算是一個折衷的優點吧?我最看重的是易於調試和修改,因此基本都用文本文件。而這其中,表現力最強的就是xml,因此基本都是xml。
可是xml多了怎麼管理就是個問題。我得整理份文檔,每一個xml都是什麼格式,作什麼用途的,最好每一個xml再寫一個xsd。事實是配置文件是隨着需求變化最頻繁的部分,而換個角度說我以前強調的序列化。因此,正確的思路是這樣:
一、程序員分析需求文檔,肯定須要什麼樣的對象來表示配置
二、某套序列化框架,它利用某種xml解析庫把xml變成內存中的對象
三、策劃提供xml
只要這個框架作的好,根本不須要文檔或xsd來描述xml。我這裏說策劃提供xml,那麼策劃怎麼提供xml呢?按照我所看見的策劃的習慣,他們最喜歡的是兩種方式:
一、對於結構簡單的數據,編輯excel表
二、對於結構複雜的(如涉及樹、環的),提供專門的編輯工具
對於1,咱們能夠給excel作plugin,或者作一個工具從excel表導出成xml。對於2,讓編輯工具能夠導出成xml。可是最終很重要很重要很重要的一點就是要讓全部的工具集成在一塊兒,作好版本管理以及跨版本diff和merge。如何管理數據要好比何定義數據如何描述數據更難更重要。
不少同事和個人共識都是:要作一款好遊戲,工具很重要。多個項目作完後,外人能看見的最大的積累就是工具和流程。
數據庫在遊戲中的重要性,是一個很使人玩味的東西。你能夠聽見不少人告訴你說,咱們作遊戲根本不須要數據庫。是的,像單機遊戲那樣,在某個目錄下建立一個文件,save/load就好了。這就是我所看到的當今的大型網遊的主流作法。
哦,你要反對了。你說你知道某某遊戲用的是mysql,某某遊戲用的是oracle,等等。是的,你手上的信息可能比我多不少不少倍,可是關鍵點在於,數據庫在整個系統中的角色究竟是什麼?
典型的場景是這樣:啓動一個單獨的進程稱之爲DB Gate。當用戶登陸的時候,邏輯服務器找DB Gate要數據,DB Gate沒有因而就去找後面的Mysql要,而後讀過來以後就放在這裏,DB Gate就是一個相似於memcached的東西。因此後面不管是用mysql仍是oracle仍是plain text均可以,但實際上會在其它方面有些細微的差異。
它和網站應用相比,數據更容易作cache,把握好上線和下線這兩個點便可,cache的命中率很容易達到4個9或者更高。可是從另外一個方面,網絡遊戲的數據關聯邏輯遠遠比網站複雜,並且對原子性、一致性、隔離性要求更高。如今是你本身來管理cache,因而併發控制就沒辦法交給數據庫來作。
問題一:我不本身作cache,我就直接讀寫數據庫。就像php+mysql那樣,中間也不套memcache,行不行? 我不知道。你能夠試一試。
問題二:SQL or NoSQL ? 我仍是回答不了。你作個demo跑機器人試一試。
總之,東西是活的。沒有必要非要怎麼着非不能怎麼着。檢驗的標準很簡單:一、是否完成了策劃提出的功能需求 二、效率是否達到了預期目標
對於第一個,QA和策劃都會去檢查。對於2,跑機器人以及封測期間調優是王道。
對於數據庫開發,我仍是很強調面向對象那套觀點。把數據庫裏的表映射到對象,把對象抽象成接口,每一個模塊以接口對外提供服務,不一樣模塊不要直接經過表共享數據。或者,你能夠讀個人表,但不要寫!由於數據的約束條件未必是能夠由DBMS徹底保證的,某些約束是難以用數據庫自己的語言表述的。
數據是網遊的核心,網遊基本都是數據驅動的,因此數值策劃纔會這麼吃香。
或者換個角度想,DBMS它是什麼?
一、它管理數據。幫助咱們高效的讀取和修改數據。由於數據的動態性,因此咱們須要Btree這樣的結構,而不是隨便找個TXT追加寫。可是換個角度想,網絡遊戲有什麼特色?插入多,可是刪除操做極少極少。那麼是否能夠採用其它的結構呢?順序重要嗎?爲何不用Hash呢?
二、它負責備份和恢復數據。這基本是任何現代的數據庫系統必須提供的基本功能。可是網絡遊戲又特殊一點,它要求能按指定時間「回檔」。時間能夠有半小時的偏差,可是這個功能必須有。因而數據庫能支持增量備份,或者它的備份能支持版本很重要。
三、它使用logging system保證在忽然宕機的時候數據依然是完整和一致的。但是若是咱們要本身作cache,那麼就要求咱們在應用層面所作的原子性保證必須在cache中也能體現出來。這些cache要麼全刷,要麼全不刷。
四、它提供併發功能。拿傳統的php+mysql架構來講,爲何同一個應用能夠被分佈式的部署在多臺機器上?魔力就在數據庫上。
既然有人輕視數據庫,那麼也可反其道重視數據庫。把90%的邏輯都放在數據庫裏完成。多招一些熟悉SQL熟悉存儲過程的,主要的邏輯都由他們完成。
接着說我在併發上的考慮。
一臺機器仍是多臺機器?單進程仍是多進程?單線程仍是多線程?等等。
我以爲併發問題是最沒章法可循的問題。你能夠這麼作也能夠那麼作。網絡遊戲的重點是在邏輯開發上,而作邏輯開發的人不要關心究竟是epoll仍是select。總之制定框架的時候須要定好一個規矩:單線程仍是多線程、訪問哪些數據的時候須要加鎖(可能還須要跨進程的加鎖)、誰來作load balancer、若是有一臺機器宕了怎麼辦、哪些任務必需要以特定的順序執行,等等。規矩定下來,一切都順了。可這個規矩要足夠的簡單。
若是是多線程,我想過兩種模式:Thread per Connection和Task based thread pool。如今機器的內存愈來愈大了,因此前者的開銷是能夠忍受的,1000人在線,就算每一個線程要被系統佔去2M,那麼也才2G。而通常的3D遊戲作個 3-4千人在線就好了,配個大內存的機器,還剩下足夠多的內存給應用使用。多簡單啊!網絡遊戲中,不少請求都是隻須要訪問單個角色的數據就夠了,反過來講不少數據均可以作成Thread Local的,免去了同步代價。
而Task based thread pool的伸縮性相對來講就好的多,可是併發問題也麻煩一些,何況從rpc請求被unmarshal完到扔到task pool裏面又多了一次線程切換,若是換成Leader-Follower那樣的模式,少了切換可是模型又更復雜了一些。
若是是單線程的,那麼一切都是事件驅動的而且事件的處理都是非阻塞的。那麼就得避開數據庫讀寫或者在處理的過程當中再產生新的rpc請求,不然很是麻煩。
併發問題的瓶頸每每是在於怎麼下降鎖衝突上。Task Pool裏面的全部線程都在執行Task,可是都在等同一把鎖,多悲劇啊。難點在於下降模塊耦合、採用適當的排隊機制等等。我以爲這裏沒有什麼萬金油,下降模塊耦合原本就沒什麼套路可循,而排隊機制有不少種,沒有最好的,各有利弊。
對於死鎖,個人容忍度比之前大了不少。我以爲每臺機器天天的死鎖數量在10個之內都是能夠忍受的,要有死鎖檢測、打斷機制而且重作的時候不會產生反作用。對玩家的感覺而言就是忽然卡了一下,但是網絡不也常常會忽然卡一下嗎?不頻繁就好。
我最鍾愛的模式就是「生產者-消費者」模式,萬能的利器。例如Task Pool就是基於這樣的模式。它的核心東西無非就是一個隊列,若是要支持定時,那麼就是一個優先隊列(deadline time做爲優先級)。講個細節,我面試的時候問了不少面試者,優先隊列應該用什麼樣的數據結構實現,結果都挺讓我失望的。
順便發個牢騷,Sun JDK的executor的實現,BUG太多了。還那麼巧,都被我趕上了?
說些雜七雜八的東西吧。
我剛入行的時候就一直在問,爲何網遊服務器常常要停機維護?爲何常常都是好幾個小時?爲何非要分紅不一樣組的服務器而且數據基本不互通?爲何不構造一個大世界把全部玩家放在一塊兒?
我如今不問了,這些問題基本都找到了答案。不是技術作不到,並且有不少它之外的東西在左右這些。至少我在盡力不回檔這件事情上已經作的比較好了。
我想說的就是,入這行就得遵照這行的規矩。若是你是個老手了,根本不必來看我這一系列的P話。若是你是新手,那麼我是在向你介紹現狀。策劃是甲方,咱們是乙方,在盡力知足策劃的需求且不會顯著增長成本的前提下作有限的創新,這是我給本身定的設計原則。
(支付寶剛通知我,我又收到了5塊錢的捐贈。謝謝,謝謝你們)
若是你是一個受過良好訓練的程序員,那麼如下基本規則是懂的:
一、不要把須要翻譯的常量字符串寫在代碼裏
二、不要直接在代碼中間寫498595這樣的magic number
三、向版本控制系統提交代碼的時候應該寫註釋
四、需求是常常變的,而且常常是災難性的
可每每知道是一回事兒,作又是另一回事。尤爲是不要相信策劃那張嘴,寫成word文檔纔算數。
和你們分享一些我在版本控制上的經驗和教訓。
最先接觸這個問題,是在sina的時候,由QA部門的同事以及周琦單獨專門給我講jira、svn。當時受益很大。
周琦一再給我強調,在產品生命週期中,源代碼版本管理和發佈部署是獨立的兩套東西。源代碼版本管理是用subversion這樣的東西來作(更早一點咱們還在用cvs)。發佈部署,一是編譯的過程,二是對外推送部署的過程,是一套相對獨立的東西。周琦的特點在於他把這兩者經過svn hook腳本的方式給自動串起來了。
我一直想要作一套OBS這樣的東西找一臺服務器專門做build server,惋惜一直沒時間去寫。就本身寫了一個腳本(原本是sh的,後來成perl,後來成groovy),它的做用是根據分支名和版本號從subversion下載代碼,而後編譯,而後放到指定位置。而後通知發佈服務器從那裏拿東西推到外邊。缺點它缺少併發控制,而且沒有UI界面。致使作完以後就成我的專屬的了。
爲何每次要選擇一個空目錄checkout而後編譯,而不是在上次的基礎上svn up而後編譯?這個和Java/Ant有點關係。在寫Makefile的時候,儘管能夠指定把當前目錄下的.cpp文件所有都編譯,可是這是不推薦的作法。由於相比於寫代碼的時間,把代碼文件添加到Makefile中的時間能夠忽略不計。而我當時給ant寫build.xml時,是用**/*.java的方式去匹配,因而把src下的全部能編譯的全編譯了。可我在編譯以前會執行一些腳本用於生成一些代碼,某些是單獨存放的,可是某些和其它手寫的代碼放在了一塊兒。因此爲了保持最終的jar包乾淨,寧肯犧牲編譯的時間。
在提供給QA的測試環境中能夠很方便的經過GM指令獲得版本號,這個是編譯的時候打包工具寫進去的。而編譯系統務必保證相同版本號的東西每次編譯出來都是相同的東西。雖然二進制比對結果可能不一致,可是邏輯功能上是一致的。
對於svn的分支管理,有兩種廣泛策略:
一、每一個人一個單獨的分支。作完本身的功能後往主幹merge
二、都在主幹上工做。須要發版本的時候建立新分支。
前一種須要你們都比較熟悉svn的用法,熟悉版本管理的基本概念。後一種則把全部活堆給一個專門發版本的人。他來建立分支,他來merge(或是誰的功能誰merge)。而且這樣的話,絕大多數代碼是不須要merge的,因此我根據實際狀況選擇了後一種。
因而在正在運行的系統中發現bug的時候,立馬獲取版本號,從那個版本上建立分支而且把分支名喊一聲告訴你們,而後找問題,把補丁merge到過去,編譯,發佈,測試,推到外面。
發版本很累,這件事情在去年秋天上線後,一直到春節,佔去了我90%的精力。其中最重要的就是比對功能和bug列表。常常,你分不清楚這到底算是一個bug呢,仍是提需求的時候就沒說清楚因此這是一個新功能,反正都列一塊兒的。挨個和svn提交記錄比對。
部署也是一個頗有講究的過程。個人原則是,先刪除老的程序和配置文件,而後複製新的過去,數據庫的數據和日誌文件保留,審計日誌保留。這件事情原本還爭論過老的要不要刪,可不能夠直接覆蓋,最終他們答應了個人需求。過程挺曲折的,中間有不少噁心的細節問題,好比NFS的本地cache的問題。
對於數據庫,咱們能智能的感知數據庫結構更改並自動生成升級腳本(天哪,我這算不算泄密)。這竟然也是一把雙刃劍。優勢是減輕了開發人員的工做量,缺點是更改數據庫變得太隨意,隨意的添表添字段致使數據膨脹的厲害。
個人遺憾是沒有把上面這些東西和數據編輯器串起來。那麼作有點是數值策劃調整數據更容易看到真實效果,缺點是也很容易亂來。若是這中間要通過svn,那麼太慢太曲折。若是這中間不通過svn,那麼鬼知道他們如今測的是什麼版本的東西,他常常會發現最終出去的東西跟他當時測的仍是不同,畢竟,是不少人在同一個服務器上測試。很難給他們解釋這個事情。
因此我當時還漏了一個東西一直想作可是沒作,就是一個很簡單的web gui能讓全部策劃本身啓動、中止服務器,本身編譯、同步數據。各弄各的,互不干擾。可是吧,策劃畢竟是策劃,它們缺少基本的QA知識。他們不明白爲何一個底層功能好好的怎麼忽然就很差使了(由於上層某處要加新功能,因此底下的代碼要重構),他們不明白爲了一個bug被改掉以後反覆又出現了,甚至對於分支和版本號這個東西,絕大多數策劃都理解起來困難。可是整個產品的開發、發佈模型就是這樣,因此這些概念必須從一開始就溝通好、貫徹好。相比而下,這些倒和美術沒什麼事兒。
都是些小活兒。
另外我一直在想要不要在配置文件和game server之間套一個gconf這樣的東西,外部更改配置,gconf通知listener也就是game server,呃,一個很不成熟的想法。
另外不少人一直想,在不重啓進程的狀況下,替換掉映像中的某個函數,修BUG。若是這個daemon程序是用C/C++寫的,這個時候用dlopen加載一個so,設置一個參數就能夠了。若是是JAVA而且用JDWP開了DEBUG,那麼too easy。若是沒有,那麼unload jar/load jar吧。
我一直在構思一個可動態拆卸/替換/裝載的架構,一個簡單的不像OSGi那麼複雜的東西,但是想法一直不大成熟,由於沒有找到太簡單的方法。個人基本想法是有一個object container,把service抽象成object,service和serivce之間的交互都要去這個object container中經過name lookup的方式獲得一個句柄,而後通信。配置文件不能視成一成不變的,它們也是動態數據的一部分,不能再經過靜態的getInstance得到,也必須經過這個object container查找。可是未必是一個global object container,每一個module能夠有本身的object container。或是module instance持有reference,請求派發給module,module派發給object的時候把須要的reference傳給過去,意思就是module就是一個object container,不過不是被lookup,而是主動構造好塞進去。
更多精彩點擊http://www.gopedu.com