整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          HTML框架分層設(shè)計(jì)

          算機(jī)網(wǎng)絡(luò)中的OSI七層模型

          計(jì)算機(jī)網(wǎng)絡(luò)中的OSI(Open Systems Interconnection)七層模型是一種理論框架,用于描述計(jì)算機(jī)網(wǎng)絡(luò)中數(shù)據(jù)通信的過程。OSI模型將計(jì)算機(jī)網(wǎng)絡(luò)通信過程劃分為七個(gè)層次,每個(gè)層次都有其特定的功能和協(xié)議。這種分層結(jié)構(gòu)有助于研究和理解計(jì)算機(jī)網(wǎng)絡(luò)中的通信原理。以下是OSI七層模型的各個(gè)層次及其主要功能:

          應(yīng)用層是OSI模型的第七層,也是網(wǎng)絡(luò)應(yīng)用程序和網(wǎng)絡(luò)協(xié)議之間的接口。應(yīng)用層主要負(fù)責(zé)為用戶提供各類應(yīng)用服務(wù),如文件傳輸、電子郵件、Web瀏覽等。

          表示層是OSI模型的第六層,主要負(fù)責(zé)處理在網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù)的表示方式,如數(shù)據(jù)加密、解密、壓縮、解壓縮等。表示層確保了不同系統(tǒng)之間的數(shù)據(jù)兼容性。

          會(huì)話層是OSI模型的第五層,主要負(fù)責(zé)建立、維護(hù)和終止應(yīng)用程序之間的通信會(huì)話。會(huì)話層提供了數(shù)據(jù)交換的同步和確認(rèn)機(jī)制。

          傳輸層是OSI模型的第四層,主要負(fù)責(zé)在源主機(jī)和目標(biāo)主機(jī)之間提供可靠的、端到端的數(shù)據(jù)傳輸服務(wù)。傳輸層通過分段、封裝和重組數(shù)據(jù)來實(shí)現(xiàn)可靠的數(shù)據(jù)傳輸。常見的傳輸層協(xié)議包括TCP(傳輸控制協(xié)議)和UDP(用戶數(shù)據(jù)報(bào)協(xié)議)。

          網(wǎng)絡(luò)層是OSI模型的第三層,主要負(fù)責(zé)將數(shù)據(jù)包從源主機(jī)路由到目標(biāo)主機(jī)。網(wǎng)絡(luò)層主要負(fù)責(zé)邏輯尋址、路由選擇和分組轉(zhuǎn)發(fā)。常見的網(wǎng)絡(luò)層協(xié)議包括IP(互聯(lián)網(wǎng)協(xié)議)和ICMP(互聯(lián)網(wǎng)控制報(bào)文協(xié)議)。

          數(shù)據(jù)鏈路層是OSI模型的第二層,主要負(fù)責(zé)將網(wǎng)絡(luò)層傳來的數(shù)據(jù)包封裝成幀(Frame),并在同一局域網(wǎng)內(nèi)進(jìn)行傳輸。數(shù)據(jù)鏈路層主要負(fù)責(zé)物理尋址、數(shù)據(jù)成幀、錯(cuò)誤檢測(cè)和流量控制。常見的數(shù)據(jù)鏈路層協(xié)議包括以太網(wǎng)(Ethernet)、令牌環(huán)(Token Ring)和無線局域網(wǎng)(Wi-Fi)等。

          物理層是OSI模型的第一層,主要負(fù)責(zé)在物理介質(zhì)上實(shí)現(xiàn)比特流的透明傳輸。物理層主要關(guān)注硬件接口、電氣特性、光纖、無線傳輸?shù)确矫娴膯栴}。

          OSI七層模型提供了一個(gè)通用的框架,幫助研究和理解計(jì)算機(jī)網(wǎng)絡(luò)中的通信原理。實(shí)際應(yīng)用中,我們通常使用TCP/IP四層模型,它包括了應(yīng)用層、傳輸層、網(wǎng)絡(luò)層和鏈路層,與OSI模型有一定的對(duì)應(yīng)關(guān)系。

          HTML框架的必要性

          HTML框架進(jìn)行分層設(shè)計(jì)的主要原因是為了提高代碼的可讀性、可維護(hù)性和可重用性。將HTML框架分層可以提高整體項(xiàng)目的結(jié)構(gòu)和邏輯,便于開發(fā)者更好地理解和修改代碼。分層設(shè)計(jì)具有以下優(yōu)點(diǎn):

          1. 提高可讀性:通過將HTML框架劃分為不同的層次,可以使代碼結(jié)構(gòu)更清晰,有助于開發(fā)者快速理解代碼的功能。
          2. 便于維護(hù):分層設(shè)計(jì)有助于將功能模塊化,這樣可以方便地修改或替換某個(gè)模塊,而不會(huì)影響其他部分的代碼。這有助于提高項(xiàng)目的可維護(hù)性。
          3. 可重用性:將HTML框架分層可以將公共部分提取為可重用的組件,這樣可以在不同項(xiàng)目中重復(fù)使用這些組件,提高開發(fā)效率。
          4. 適應(yīng)性:分層設(shè)計(jì)可以讓HTML框架更容易適應(yīng)不同的設(shè)備和屏幕尺寸,提高項(xiàng)目的兼容性。
          5. 便于協(xié)作:在大型項(xiàng)目中,通常會(huì)有多個(gè)開發(fā)者參與。通過分層設(shè)計(jì),開發(fā)者可以專注于自己的模塊,減少代碼沖突和溝通成本。

          HTML框架的組成

          HTML框架包括Application層``middleware層``route層``codec層``transport層 Application層 應(yīng)用層通常包括與業(yè)務(wù)邏輯相關(guān)的代碼,如Web應(yīng)用程序的控制器(Controller)、視圖(View)和模型(Model)。應(yīng)用層的主要作用是處理用戶請(qǐng)求并返回相應(yīng)的響應(yīng)。

          Middleware層 中間件層是介于應(yīng)用層和底層框架之間的一層,負(fù)責(zé)處理一些通用的功能,如身份驗(yàn)證、授權(quán)、緩存、日志記錄等。中間件層有助于將業(yè)務(wù)邏輯與通用功能分離,使得應(yīng)用層更加簡(jiǎn)潔和易于維護(hù)。

          Route層 路由層負(fù)責(zé)處理HTTP請(qǐng)求的URL和HTTP方法(如GET、POST等),將請(qǐng)求分發(fā)到相應(yīng)的控制器和方法。路由層的主要作用是根據(jù)URL映射來定位具體的功能代碼。

          Codec層 編解碼層負(fù)責(zé)處理數(shù)據(jù)的編碼和解碼。在Web開發(fā)中,編碼和解碼通常涉及到HTML、CSS、JavaScript等前端技術(shù)的處理,以及JSON、XML等數(shù)據(jù)交換格式的處理。編解碼層的主要作用是將數(shù)據(jù)轉(zhuǎn)換為特定的格式,以便在不同層之間進(jìn)行傳輸和處理。

          Transport層 傳輸層負(fù)責(zé)處理底層的網(wǎng)絡(luò)通信,如TCP、UDP等協(xié)議的使用。在Web開發(fā)中,傳輸層通常涉及到HTTP協(xié)議的處理,包括請(qǐng)求和響應(yīng)的創(chuàng)建、發(fā)送和接收。傳輸層的主要作用是確保數(shù)據(jù)的可靠傳輸和在網(wǎng)絡(luò)中的正確路由。

          這些層次在實(shí)際應(yīng)用中可能因框架和場(chǎng)景的不同而有所差異。但是,從您提供的描述來看,它們分別負(fù)責(zé)處理Web應(yīng)用程序中的不同功能,共同構(gòu)成了一個(gè)完整的Web開發(fā)框架。

          HTML框架和服務(wù)端客戶端之間的通信對(duì)比

          Application層應(yīng)用層設(shè)計(jì)

          應(yīng)用層設(shè)計(jì)主要是設(shè)置各種接口,用于路由使用。

          例如在字節(jié)后端進(jìn)階版中的大項(xiàng)目中的注冊(cè)接口。

          /douyin/user/register/ - 用戶注冊(cè)接口

          新用戶注冊(cè)時(shí)提供用戶名,密碼,昵稱即可,用戶名需要保證唯一。創(chuàng)建成功后返回用戶 id 和權(quán)限token.

          接口類型

          POST

          接口定義

          go復(fù)制代碼syntax = "proto2";
          package douyin.core;
          
          message douyin_user_register_request {
            required string username = 1; // 注冊(cè)用戶名,最長(zhǎng)32個(gè)字符
            required string password = 2; // 密碼,最長(zhǎng)32個(gè)字符
          }
          
          message douyin_user_register_response {
            required int32 status_code = 1; // 狀態(tài)碼,0-成功,其他值-失敗
            optional string status_msg = 2; // 返回狀態(tài)描述
            required int64 user_id = 3; // 用戶id
            required string token = 4; // 用戶鑒權(quán)token
          }
          
          go復(fù)制代碼func Register(username, password string) (id int64, token int64, err error) {
             if len(username) > 32 {
                return 0, 0, errors.New("用戶名過長(zhǎng),不可超過32位")
             }
             if len(password) > 32 {
                return 0, 0, errors.New("密碼過長(zhǎng),不可超過32位")
             }
             // 先查布隆過濾器,不存在直接返回錯(cuò)誤,降低數(shù)據(jù)庫(kù)的壓力
             if userNameFilter.TestString(username) {
                return 0, 0, errors.New("用戶名已經(jīng)存在!")
             }
             //雪花算法生成token
             node, err := snowflake.NewNode(1) //這里的userIdInt64就是 User.Id(主鍵)
             if err != nil {
                log.Println("雪花算法生成id錯(cuò)誤!")
                log.Println(err)
             }
             token1 := node.Generate().Int64()
             tokenStr := strconv.FormatInt(token1, 10)
             user := domain.User{}
             // 再查緩存
             data, err := dao.RedisClient.Get(context.Background(), tokenStr).Result()
             if err == redis.Nil {
                fmt.Println("token does not exist")
             } else if err != nil {
                fmt.Println("Error:", err)
             } else {
                num, err := strconv.ParseInt(data, 10, 64)
                if err != nil {
                   fmt.Println("Error:", err)
                   return num, 0, err
                }
          
                return num, token1, nil
             }
             //在查數(shù)據(jù)庫(kù)
             user = domain.User{}
             dao.DB.Model(&domain.User{}).Where("name = ?", username).Find(&user)
             if user.Id != 0 {
                return 0, 0, errors.New("用戶已存在")
             }
             user.Name = username
             // 加密存儲(chǔ)用戶密碼
             user.Salt = randSalt()
             buf := bytes.Buffer{}
             buf.WriteString(username)
             buf.WriteString(password)
             buf.WriteString(user.Salt)
             pwd, err1 := bcrypt.GenerateFromPassword(buf.Bytes(), bcrypt.MinCost)
             if err1 != nil {
                return 0, 0, err
             }
             user.Pwd = string(pwd)
          
             //存在mysql里邊
             dao.DB.Model(&domain.User{}).Create(&user)
             //再把用戶id作為鍵 用戶的所有信息作為值存在其中
             //用戶信息的緩存是 保存在redis中 一個(gè)以id為鍵 user json為值
             jsonuser, err1 := MarshalUser(user)
             if err1 != nil {
                fmt.Println("err1", err1)
                return 0, 0, err1
             }
             err = dao.RedisClient.Set(context.Background(), strconv.FormatInt(user.Id, 10), jsonuser, 0).Err()
             if err != nil {
                fmt.Println("err", err)
                return 0, 0, err
             }
             // 布隆過濾器中加入新用戶
             userIdFilter.AddString(strconv.FormatInt(user.Id, 10))
             userNameFilter.AddString(username)
             return user.Id, token1, nil
          }
          

          本接口注冊(cè)功能實(shí)現(xiàn):把所有信息存在mysql里邊當(dāng)然redis里邊也存在這些信息,當(dāng)然username也存在了布容過濾器中去,當(dāng)接收到用戶的username的時(shí)候我們現(xiàn)在布容過濾器中先查詢是否存在如果存在則直接返回err,不存在然后再在redis里邊查詢,因?yàn)閞edis相比于mysql是更為輕量級(jí)的所以我們要先在redis里邊進(jìn)行查,如果查不到再進(jìn)mysql里邊查去,查不到說明沒有注冊(cè)過,可以注冊(cè)。

          命名規(guī)范

          遵循命名規(guī)范原則。

          Middleware層中間件

          gin框架里的中間件分為全局中間件,局部中間件。那么什么是中間件?中間件是為應(yīng)用提供通用服務(wù)和功能的軟件。數(shù)據(jù)管理、應(yīng)用服務(wù)、消息傳遞、身份驗(yàn)證和 API 管理通常都要通過中間件。在gin框架里,就是我們的所有API接口都要經(jīng)過我們的中間件,我們可以在中間件做一些攔截處理。

          中間件常用模型

          全局中間件

          這個(gè)是在服務(wù)啟動(dòng)就開始注冊(cè),全局意味著所有API接口都會(huì)經(jīng)過這里。Gin的中間件是通過Use方法設(shè)置的,它接收一個(gè)可變參數(shù),所以我們同時(shí)可以設(shè)置多個(gè)中間件。

          首先定義如下

          go復(fù)制代碼// 1.創(chuàng)建路由
          r := gin.Default()  //默認(rèn)帶Logger(), Recovery()這兩個(gè)內(nèi)置中間件
          r:= gin.New()      //不帶任何中間件
          // 注冊(cè)中間件  
          r.Use(MiddleWare())
          r.Use(MiddleWare2())
          

          注意的是

          gin.Default()默認(rèn)使用了Logger和Recovery中間件,其中:Logger中間件將日志寫入gin.DefaultWriter,即使配置了GIN_MODE=release。Recovery中間件會(huì)recover任何panic。如果有panic的話,會(huì)寫入500響應(yīng)碼。如果不想使用上面兩個(gè)默認(rèn)的中間件,可以使用gin.New()新建一個(gè)沒有任何默認(rèn)中間件的路由。

          go復(fù)制代碼// 定義中間
          func MiddleWare() gin.HandlerFunc {
              return func(c *gin.Context) {
                  t := time.Now()
                  fmt.Println("中間件開始執(zhí)行了")
                  // 設(shè)置變量到Context的key中,可以通過Get()取
                  c.Set("request", "這是中間件設(shè)置的值")
                  status := c.Writer.Status()
                  fmt.Println("中間件執(zhí)行完畢", status)        
                  t2 := time.Since(t)
                  fmt.Println("time:", t2)
              }
          }
          

          然后啟動(dòng)我們的服務(wù),訪問任意一個(gè)接口可以看到輸出如下

          這是請(qǐng)求先到了中間件,然后在到我們的API接口。在中間件里可以設(shè)置變量到Context的key中,然后在我們的API接口取值。

          go復(fù)制代碼        r.GET("/", func(c *gin.Context) {
                      // 取值
                      req, _ := c.Get("request")
                      fmt.Println("request:", req)
                      // 頁面接收
                      c.JSON(200, gin.H{"request": req})
                  })
          

          這時(shí)候在訪問就可以看到中間件設(shè)置的值是

          next方法是在中間件里面使用,這個(gè)是執(zhí)行后續(xù)中間件請(qǐng)求處理的意思(含沒有執(zhí)行的中間件和我們定義的GET方法處理,如果連續(xù)注冊(cè)幾個(gè)中間件則會(huì)是按照順序先進(jìn)后出的執(zhí)行,遇到next就去執(zhí)行下一個(gè)中間件里的next前面方法。

          go復(fù)制代碼        // 執(zhí)行函數(shù)
                  c.Next()
                  // 中間件執(zhí)行完后續(xù)的一些事情
          

          局部中間件

          局部中間件意味著部分接口才會(huì)生效,只在局部使用,這時(shí)候訪問http:127.0.0.1:8000/ 才會(huì)看到中間件的日志打印,其他API接口則不會(huì)出現(xiàn)。

          go復(fù)制代碼   //局部中間件使用
              r.GET("/", MiddleWare(), func(c *gin.Context) {
                  // 取值
                  req, _ := c.Get("request")
                  fmt.Println("request:", req)
                  // 頁面接收
                  c.JSON(200, gin.H{"request": req})
              })
          

          gin內(nèi)置中間件

          go復(fù)制代碼
          func BasicAuth(accounts Accounts) HandlerFunc
          
          func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc
          
          func Bind(val interface{}) HandlerFunc
          
          func ErrorLogger() HandlerFunc
          
          func ErrorLoggerT(typ ErrorType) HandlerFunc
          
          func Logger() HandlerFunc
          
          func LoggerWithConfig(conf LoggerConfig) HandlerFunc
          
          func LoggerWithFormatter(f LogFormatter) HandlerFunc
          
          func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc
          
          func Recovery() HandlerFunc
          
          func RecoveryWithWriter(out io.Writer) HandlerFunc
          
          func WrapF(f http.HandlerFunc) HandlerFunc
          
          func WrapH(h http.Handler) HandlerFunc
          

          總結(jié)

          通過自定義中間件,我們可以很方便的攔截請(qǐng)求,來做一些我們需要做的事情,比如日志記錄、授權(quán)校驗(yàn)、各種過濾等等。

          route層路由層

          Gin 是一個(gè)標(biāo)準(zhǔn)的 Web 服務(wù)框架,遵循 Restful API 接口規(guī)范,其路由庫(kù)是基于 httproute 實(shí)現(xiàn)的。

          本節(jié)將從 Gin 路由開始,詳細(xì)講述各種路由場(chǎng)景下,如何通過 Gin 來實(shí)現(xiàn)。

          基本路由

          1. GET:用于處理從客戶端發(fā)起的HTTP GET請(qǐng)求。GET請(qǐng)求用于從服務(wù)器獲取數(shù)據(jù),不應(yīng)對(duì)服務(wù)器上的數(shù)據(jù)進(jìn)行更改。例如,獲取用戶信息、獲取文章列表等。
          2. POST:用于處理從客戶端發(fā)起的HTTP POST請(qǐng)求。POST請(qǐng)求通常用于向服務(wù)器發(fā)送數(shù)據(jù),用于創(chuàng)建新的資源或更新已有的資源。例如,用戶注冊(cè)、發(fā)布文章、更新用戶信息等。
          3. PUT:用于處理從客戶端發(fā)起的HTTP PUT請(qǐng)求。PUT請(qǐng)求通常用于更新服務(wù)器上的資源。例如,更新用戶信息、更新文章內(nèi)容等。
          4. DELETE:用于處理從客戶端發(fā)起的HTTP DELETE請(qǐng)求。DELETE請(qǐng)求通常用于從服務(wù)器刪除資源。例如,刪除用戶賬戶、刪除文章等。
          5. PATCH:用于處理從客戶端發(fā)起的HTTP PATCH請(qǐng)求。PATCH請(qǐng)求通常用于對(duì)服務(wù)器上的資源進(jìn)行部分更新。例如,更新用戶的部分信息、更新文章標(biāo)題等。
          6. OPTIONS:用于處理從客戶端發(fā)起的HTTP OPTIONS請(qǐng)求。OPTIONS請(qǐng)求用于獲取服務(wù)器支持的HTTP方法。例如,跨域資源共享(CORS)場(chǎng)景。
          7. HEAD:用于處理從客戶端發(fā)起的HTTP HEAD請(qǐng)求。HEAD請(qǐng)求類似于GET請(qǐng)求,但不返回響應(yīng)體。主要用于獲取響應(yīng)頭信息。例如,檢查資源是否存在,但不需要獲取資源內(nèi)容。
          8. ANY:用于處理任何HTTP方法(GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD)的請(qǐng)求。適用于處理多種HTTP方法的情況。
          9. CONNECT:用于處理從客戶端發(fā)起的HTTP CONNECT請(qǐng)求。CONNECT請(qǐng)求通常用于建立客戶端與服務(wù)器之間的隧道,用于代理或其他場(chǎng)景。
          10. TRACE:用于處理從客戶端發(fā)起的HTTP TRACE請(qǐng)求。TRACE請(qǐng)求用于測(cè)試或診斷網(wǎng)絡(luò)連接。服務(wù)器應(yīng)當(dāng)返回原始的請(qǐng)求信息,以便客戶端可以檢查中間代理或防火墻是否進(jìn)行了修改。

          示例

          字節(jié)大項(xiàng)目注冊(cè)接口

          go復(fù)制代碼syntax = "proto2";
          package douyin.core;
          
          message douyin_user_register_request {
            required string username = 1; // 注冊(cè)用戶名,最長(zhǎng)32個(gè)字符
            required string password = 2; // 密碼,最長(zhǎng)32個(gè)字符
          }
          
          message douyin_user_register_response {
            required int32 status_code = 1; // 狀態(tài)碼,0-成功,其他值-失敗
            optional string status_msg = 2; // 返回狀態(tài)描述
            required int64 user_id = 3; // 用戶id
            required string token = 4; // 用戶鑒權(quán)token
          }
          
          go復(fù)制代碼func Register(c *gin.Context) {
          
             username := c.Query("username")
             password := c.Query("password")
             id, token, err := service.Register(username, password)
             if err != nil {
                c.JSON(http.StatusOK, domain.Response{StatusCode: 1, StatusMsg: err.Error()})
             } else {
          
                c.JSON(http.StatusOK, domain.UserLoginResponse{
                   //可以直接去掉
                   Response: domain.Response{StatusCode: 0},
                   Id:       id,
                   Token:    token,
                })
             }
          }
          

          go復(fù)制代碼package main
          
          import (
             "github.com/gin-gonic/gin"
             "github.com/goTouch/TicTok_SimpleVersion/controller"
          )
          
          func initRouter(r *gin.Engine) {
             // public directory is used to serve static resources
             r.Static("/static", "./public")
          
             apiRouter := r.Group("/douyin")
          
             // basic apis
             //controller.VerifyToken,
             apiRouter.POST("/user/", controller.UserInfo)
             apiRouter.POST("/user/register/", controller.LoginLimit, controller.Register)
             apiRouter.POST("/user/login/", controller.LoginLimit, controller.Login)
             }
          

          codec層

          在Web開發(fā)中,編碼和解碼通常涉及到HTML、CSS、JavaScript等前端技術(shù)的處理,以及JSON、XML等數(shù)據(jù)交換格式的處理。編解碼層的主要作用是將數(shù)據(jù)轉(zhuǎn)換為特定的格式,以便在不同層之間進(jìn)行傳輸和處理。

          示例

          在postman中的示例 json

          xml

          html復(fù)制代碼{"status_code":1,"status_msg":"redis: nil"}
          {"status_code":2,"status_msg":"no multipart boundary param in Content-Type"}
          
          Text復(fù)制代碼{"status_code":1,"status_msg":"redis: nil"}
          {"status_code":2,"status_msg":"no multipart boundary param in Content-Type"}
          
          Auto復(fù)制代碼{
              "status_code": 1,
              "status_msg": "redis: nil"
          }{
              "status_code": 2,
              "status_msg": "no multipart boundary param in Content-Type"
          }
          
          

          transport層傳輸層

          傳輸層負(fù)責(zé)處理底層的網(wǎng)絡(luò)通信,如TCP、UDP等協(xié)議的使用。在Web開發(fā)中,傳輸層通常涉及到HTTP協(xié)議的處理,包括請(qǐng)求和響應(yīng)的創(chuàng)建、發(fā)送和接收。傳輸層的主要作用是確保數(shù)據(jù)的可靠傳輸和在網(wǎng)絡(luò)中的正確路由。

          golang語言中net/http這個(gè)庫(kù)中的conn 他是BIO自帶阻塞

          1. BIO (Blocking I/O)

          同步阻塞I/O模式,數(shù)據(jù)的讀取寫入必須阻塞在一個(gè)線程內(nèi)等待其完成。

          1.1 傳統(tǒng) BIO

          BIO通信(一請(qǐng)求一應(yīng)答)模型圖如下(圖源網(wǎng)絡(luò),原出處不明):

          采用 BIO 通信模型 的服務(wù)端,通常由一個(gè)獨(dú)立的 Acceptor 線程負(fù)責(zé)監(jiān)聽客戶端的連接。我們一般通過在 while(true) 循環(huán)中服務(wù)端會(huì)調(diào)用 accept() 方法等待接收客戶端的連接的方式監(jiān)聽請(qǐng)求,請(qǐng)求一旦接收到一個(gè)連接請(qǐng)求,就可以建立通信套接字在這個(gè)通信套接字上進(jìn)行讀寫操作,此時(shí)不能再接收其他客戶端連接請(qǐng)求,只能等待同當(dāng)前連接的客戶端的操作執(zhí)行完成, 不過可以通過多線程來支持多個(gè)客戶端的連接,如上圖所示。

          如果要讓 BIO 通信模型 能夠同時(shí)處理多個(gè)客戶端請(qǐng)求,就必須使用多線程(主要原因是 socket.accept()、 socket.read()、 socket.write() 涉及的三個(gè)主要函數(shù)都是同步阻塞的),也就是說它在接收到客戶端連接請(qǐng)求之后為每個(gè)客戶端創(chuàng)建一個(gè)新的線程進(jìn)行鏈路處理,處理完成之后,通過輸出流返回應(yīng)答給客戶端,線程銷毀。這就是典型的 一請(qǐng)求一應(yīng)答通信模型 。我們可以設(shè)想一下如果這個(gè)連接不做任何事情的話就會(huì)造成不必要的線程開銷,不過可以通過 線程池機(jī)制 改善,線程池還可以讓線程的創(chuàng)建和回收成本相對(duì)較低。使用FixedThreadPool 可以有效的控制了線程的最大數(shù)量,保證了系統(tǒng)有限的資源的控制,實(shí)現(xiàn)了N(客戶端請(qǐng)求數(shù)量):M(處理客戶端請(qǐng)求的線程數(shù)量)的偽異步I/O模型(N 可以遠(yuǎn)遠(yuǎn)大于 M),下面一節(jié)"偽異步 BIO"中會(huì)詳細(xì)介紹到。

          我們?cè)僭O(shè)想一下當(dāng)客戶端并發(fā)訪問量增加后這種模型會(huì)出現(xiàn)什么問題?

          程是寶貴的資源,線程的創(chuàng)建和銷毀成本很高,除此之外,線程的切換成本也是很高的。尤其在 Linux 這樣的操作系統(tǒng)中,線程本質(zhì)上就是一個(gè)進(jìn)程,創(chuàng)建和銷毀線程都是重量級(jí)的系統(tǒng)函數(shù)。如果并發(fā)訪問量增加會(huì)導(dǎo)致線程數(shù)急劇膨脹可能會(huì)導(dǎo)致線程堆棧溢出、創(chuàng)建新線程失敗等問題,最終導(dǎo)致進(jìn)程宕機(jī)或者僵死,不能對(duì)外提供服務(wù)。 golang實(shí)現(xiàn)BIO

          NIO

          NIO: NIO是一種同步非阻塞IO, 基于Reactor模型來實(shí)現(xiàn)的。其實(shí)相當(dāng)于就是一個(gè)線程處理大量的客戶端的請(qǐng)求,通過一個(gè)線程輪詢大量的channel,每次就獲取一批有事件的channel,然后對(duì)每個(gè)請(qǐng)求啟動(dòng)一個(gè)線程處理即可。這里的核心就是非阻塞,就那個(gè)selector一個(gè)線程就可以不停輪詢channel,所有客戶端請(qǐng)求都不會(huì)阻塞,直接就會(huì)進(jìn)來,大不了就是等待一下排著隊(duì)而已。這里面優(yōu)化BIO的核心就是,一個(gè)客戶端并不是時(shí)時(shí)刻刻都有數(shù)據(jù)進(jìn)行交互,沒有必要死耗著一個(gè)線程不放,所以客戶端選擇了讓線程歇一歇,只有客戶端有相應(yīng)的操作的時(shí)候才發(fā)起通知,創(chuàng)建一個(gè)線程來處理請(qǐng)求。
          ————————————————
          NIO:模型圖

          Reactor模型:

          NIO核心組件詳細(xì)講解

          學(xué)習(xí)NIO先來搞清楚一些相關(guān)的概念,NIO通訊有哪些相關(guān)組件,對(duì)應(yīng)的作用都是什么,之間有哪些聯(lián)系?

          多路復(fù)用機(jī)制實(shí)現(xiàn)Selector

          首先我們來了解下傳統(tǒng)的Socket網(wǎng)絡(luò)通訊模型。

          傳統(tǒng)Socket通訊原理圖

          為什么傳統(tǒng)的socket不支持海量連接

          每次一個(gè)客戶端接入,都是要在服務(wù)端創(chuàng)建一個(gè)線程來服務(wù)這個(gè)客戶端的,這會(huì)導(dǎo)致大量的客戶端的時(shí)候,服務(wù)端的線程數(shù)量可能達(dá)到幾千甚至幾萬,幾十萬,這會(huì)導(dǎo)致服務(wù)器端程序負(fù)載過高,不堪重負(fù),最終系統(tǒng)崩潰死掉。

          • 接著來看下NIO是如何基于Selector實(shí)現(xiàn)多路復(fù)用機(jī)制支持的海量連接。

          NIO原理圖

          多路復(fù)用機(jī)制是如何支持海量連接

          NIO的線程模型 對(duì)Socket發(fā)起的連接不需要每個(gè)都創(chuàng)建一個(gè)線程,完全可以使用一個(gè)Selector來多路復(fù)用監(jiān)聽N多個(gè)Channel是否有請(qǐng)求,該請(qǐng)求是對(duì)應(yīng)的連接請(qǐng)求,還是發(fā)送數(shù)據(jù)的請(qǐng)求,這里面是基于操作系統(tǒng)底層的Select通知機(jī)制的,一個(gè)Selector不斷的輪詢多個(gè)Channel,這樣避免了創(chuàng)建多個(gè)線程,只有當(dāng)莫個(gè)Channel有對(duì)應(yīng)的請(qǐng)求的時(shí)候才會(huì)創(chuàng)建線程,可能說1000個(gè)請(qǐng)求, 只有100個(gè)請(qǐng)求是有數(shù)據(jù)交互的, 這個(gè)時(shí)候可能server端就提供10個(gè)線程就能夠處理這些請(qǐng)求。這樣的話就可以避免了創(chuàng)建大量的線程。

          NIO如何通過Buffer來緩沖數(shù)據(jù)的

          NIO中的Buffer是個(gè)什么東西 ?

          學(xué)習(xí)NIO,首當(dāng)其沖就是要了解所謂的Buffer緩沖區(qū),這個(gè)東西是NIO里比較核心的一個(gè)部分,一般來說,如果你要通過NIO寫數(shù)據(jù)到文件或者網(wǎng)絡(luò),或者是從文件和網(wǎng)絡(luò)讀取數(shù)據(jù)出來此時(shí)就需要通過Buffer緩沖區(qū)來進(jìn)行。Buffer的使用一般有如下幾個(gè)步驟:

          寫入數(shù)據(jù)到Buffer,調(diào)用flip()方法,從Buffer中讀取數(shù)據(jù),調(diào)用clear()方法或者compact()方法。

          Buffer中對(duì)應(yīng)的Position, Mark, Capacity,Limit都啥?

          capacity: 緩沖區(qū)容量的大小,就是里面包含的數(shù)據(jù)大小。
          limit: 對(duì)buffer緩沖區(qū)使用的一個(gè)限制,從這個(gè)index開始就不能讀取數(shù)據(jù)了。
          position: 代表著數(shù)組中可以開始讀寫的index, 不能大于limit。
          mark: 是類似路標(biāo)的東西,在某個(gè)position的時(shí)候,設(shè)置一下mark,此時(shí)就可以設(shè)置一個(gè)標(biāo)記,后續(xù)調(diào)用reset()方法可以把position復(fù)位到當(dāng)時(shí)設(shè)置的那個(gè)mark上去,把position或limit調(diào)整為小于mark的值時(shí),就丟棄這個(gè)mark。如果使用的是Direct模式創(chuàng)建的Buffer的話,就會(huì)減少中間緩沖直接使用的是DirectorBuffer來進(jìn)行數(shù)據(jù)的存儲(chǔ)。
          ————————————————

          如何通過Channel和FileChannel讀取Buffer數(shù)據(jù)寫入磁盤的

          NIO中,Channel是什么?

          Channel是NIO中的數(shù)據(jù)通道,類似流,但是又有些不同,Channel即可從中讀取數(shù)據(jù),又可以從寫數(shù)據(jù)到通道中,但是流的讀寫通常是單向的。Channel可以異步的讀寫。Channel中的數(shù)據(jù)總是要先讀到一個(gè)Buffer中,或者從緩沖區(qū)中將數(shù)據(jù)寫到通道中。

          FileChannel的作用是什么 Buffer有不同的類型,同樣Channel也有好幾個(gè)類型。 FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel。這些通道涵蓋了UDP 和 TCP 網(wǎng)絡(luò)IO,以及文件IO。而FileChannel就是文件IO對(duì)應(yīng)的管道, 在讀取文件的時(shí)候會(huì)用到這個(gè)管道。 golang的NIO

          前面一篇文章:「高頻面試題」瀏覽器從輸入url到頁面展示中間發(fā)生了什么 中,我們有對(duì)瀏覽器的渲染流程做了一個(gè)概括性的介紹,今天這篇文章我們將深入學(xué)習(xí)這部分內(nèi)容。

          對(duì)于很多前端開發(fā)來說,平常做工主要專注于業(yè)務(wù)開發(fā),對(duì)瀏覽器的渲染階段可能不是很了解。實(shí)際上這個(gè)階段很重要,了解瀏覽器的渲染過程,能讓我們知道我們寫的HTML、CSS、JS代碼是如何被解析,并最終渲染成一個(gè)頁面的,在頁面性能優(yōu)化的時(shí)候有相應(yīng)的解決思路。

          我們先來看一個(gè)問題:

          HTML、CSS、JS文件在瀏覽器中是如何轉(zhuǎn)化成頁面的?

          如果你回答不上來,那就往下看吧。

          按照渲染的時(shí)間順序,渲染過程可以分為下面幾個(gè)子階段:構(gòu)建DOM樹、樣式計(jì)算、布局階段、分層、柵格化和合成顯示。

          下面詳細(xì)看下每個(gè)階段都做了哪些事情。

          1. 構(gòu)建DOM樹

          HTML文檔描述一個(gè)頁面的結(jié)構(gòu),但是瀏覽器無法直接理解和使用HTML,所以需要通過HTML解析器將HTML轉(zhuǎn)換成瀏覽器能夠理解的結(jié)構(gòu)——DOM樹。

          HTML文檔中所有內(nèi)容皆為節(jié)點(diǎn),各節(jié)點(diǎn)之間有層級(jí)關(guān)系,彼此相連,構(gòu)成DOM樹。

          構(gòu)建過程:讀取HTML文檔的字節(jié)(Bytes),將字節(jié)轉(zhuǎn)換成字符(Chars),依據(jù)字符確定標(biāo)簽(Tokens),將標(biāo)簽轉(zhuǎn)換成節(jié)點(diǎn)(Nodes),以節(jié)點(diǎn)為基準(zhǔn)構(gòu)建DOM樹。參考下圖:

          打開Chrome的開發(fā)者工具,在控制臺(tái)輸入 document 后回車,就能看到一個(gè)完整的DOM樹結(jié)構(gòu),如下圖所示:

          在控制臺(tái)打印出來的DOM結(jié)構(gòu)和HTML內(nèi)容幾乎一樣,但和HTML不同的是,DOM是保存在內(nèi)存中的樹狀結(jié)構(gòu),可以通過JavaScript來查詢或修改其內(nèi)容。

          2. 樣式計(jì)算

          樣式計(jì)算這個(gè)階段,是為了計(jì)算出DOM節(jié)點(diǎn)中每個(gè)元素的表現(xiàn)樣式。

          2.1 解析CSS

          CSS樣式可以通過下面三種方式引入:

          • 通過link引用外部的CSS文件
          • style 標(biāo)簽內(nèi)的CSS
          • 元素的style屬性內(nèi)嵌的CSS

          和HTML一樣,瀏覽器無法直接理解純文本的CSS樣式,需要通過CSS解析器將CSS解析成 styleSheets 結(jié)構(gòu),也就是我們常說的 CSSOM樹

          styleSheets結(jié)構(gòu)同樣具備查詢和修改功能:

          document.styleSheets

          2.2 屬性值標(biāo)準(zhǔn)化

          屬性值標(biāo)準(zhǔn)化看字面意思有點(diǎn)不好理解,我們通過下面一個(gè)例子來看看什么是屬性值標(biāo)準(zhǔn)化:

          在寫CSS樣式的時(shí)候,我們?cè)谠O(shè)置color屬性值的時(shí)候,經(jīng)常會(huì)用white、red等,但是這種值瀏覽器的渲染引擎不容易理解,所以需要將所有值轉(zhuǎn)換成渲染引擎容易理解的、標(biāo)準(zhǔn)化的計(jì)算值,這個(gè)過程就是屬性值標(biāo)準(zhǔn)化。

          white標(biāo)準(zhǔn)化后的值為 rgb(255, 255, 255)

          2.3 計(jì)算DOM樹中每個(gè)節(jié)點(diǎn)的樣式

          完成樣式的屬性值標(biāo)準(zhǔn)化后,就需要計(jì)算每個(gè)節(jié)點(diǎn)的樣式屬性,這個(gè)階段CSS有兩個(gè)規(guī)則我們需要清楚:

          • 繼承規(guī)則:每個(gè)DOM節(jié)點(diǎn)都包含有父節(jié)點(diǎn)的樣式
          • 層疊規(guī)則:層疊是CSS的一個(gè)基本特征,是一個(gè)定義了如何合并來自多個(gè)源的屬性值的算法。

          樣式計(jì)算階段是為了計(jì)算出DOM節(jié)點(diǎn)中每個(gè)元素的具體樣式,在計(jì)算過程中需要遵守CSS的繼承和層疊兩個(gè)規(guī)則。

          該階段最終輸出的內(nèi)容是每個(gè)DOM節(jié)點(diǎn)的樣式,并被保存在 ComputedStyle 的結(jié)構(gòu)中。

          3. 布局階段

          經(jīng)過上面的兩個(gè)步驟,我們已經(jīng)拿到了DOM樹和DOM樹中元素的樣式,接下來需要計(jì)算DOM樹中可見元素的幾何位置,這個(gè)計(jì)算過程就是布局。

          3.1 創(chuàng)建布局樹

          在DOM樹中包含了一些不可見的元素,例如 head 標(biāo)簽,設(shè)置了 display:none 屬性的元素,所以我們需要額外構(gòu)建一棵只包含可見元素的布局樹。

          構(gòu)建過程:從DOM樹的根節(jié)點(diǎn)開始遍歷,將所有可見的節(jié)點(diǎn)加到布局樹中,忽略不可見的節(jié)點(diǎn)。

          3.2 布局計(jì)算

          到這里我們就有了一棵構(gòu)建好的布局樹,就可以開始計(jì)算布局樹節(jié)點(diǎn)的坐標(biāo)位置了。從根節(jié)點(diǎn)開始遍歷,結(jié)合上面計(jì)算得到的樣式,確定每個(gè)節(jié)點(diǎn)對(duì)象在頁面上的具體大小和位置,將這些信息保存在布局樹中。

          布局階段的輸出是一個(gè)盒子模型,它會(huì)精確地捕獲每個(gè)元素在屏幕內(nèi)的確切位置與大小。

          4. 分層

          現(xiàn)在我們已經(jīng)有了布局樹,也知道了每個(gè)元素的具體位置信息,但是還不能開始繪制頁面,因?yàn)轫撁嬷袝?huì)有像3D變換、頁面滾動(dòng)、或者用 z-index 進(jìn)行z軸排序等復(fù)雜效果,為了更方便實(shí)現(xiàn)這些效果,渲染引擎還需要為特定的節(jié)點(diǎn)生成專用的圖層,并生成一棵對(duì)應(yīng)的圖層樹(LayerTree)。

          在Chrome瀏覽器中,我們可以打開開發(fā)者工具,選擇 Elements-Layers 標(biāo)簽,就可以看到頁面的分層情況,如下圖所示:

          瀏覽器的頁面實(shí)際上被分成了很多圖層,這些圖層疊加后合成了最終的頁面。

          到這里,我們構(gòu)建了兩棵樹:布局樹和圖層樹。下面我們來看下這兩棵樹之間的關(guān)系:

          正常情況下,并不是布局樹的每個(gè)節(jié)點(diǎn)都包含一個(gè)圖層,如果一個(gè)節(jié)點(diǎn)沒有對(duì)應(yīng)的圖層,那么這個(gè)節(jié)點(diǎn)就從屬于父節(jié)點(diǎn)的圖層。

          那節(jié)點(diǎn)要滿足什么條件才會(huì)被提升為一個(gè)單獨(dú)的圖層?只要滿足下面其中一個(gè)條件即可:

          • 擁有層疊上下文屬性的元素會(huì)被提升為單獨(dú)的一個(gè)圖層
          • 需要剪裁(clip)的地方也會(huì)被創(chuàng)建為圖層。

          5. 圖層繪制

          構(gòu)建好圖層樹之后,渲染引擎就會(huì)對(duì)圖層樹中的每個(gè)圖層進(jìn)行繪制。

          渲染引擎實(shí)現(xiàn)圖層繪制,會(huì)把一個(gè)圖層的繪制拆分成很多小的繪制指令,然后將這些指令按照順序組成一個(gè)繪制列表。

          6. 柵格化(raster)操作

          繪制一個(gè)圖層時(shí)會(huì)生成一個(gè)繪制列表,這只是用來記錄繪制順序和繪制指令的列表,實(shí)際上繪制操作是由渲染引擎中的合成線程來完成的。

          通過下圖來看下渲染主線程和合成線程之間的關(guān)系:

          當(dāng)圖層的繪制列表準(zhǔn)備好后,主線程會(huì)把該繪制列表提交給合成線程,合成線程開始工作。

          首先合成線程會(huì)將圖層劃分為圖塊(tile),圖塊大小通常是 256256 或者 512512。

          然后合成線程會(huì)按照視口附近的圖塊來優(yōu)先生成位圖,實(shí)際生成位圖的操作是由柵格化來執(zhí)行的。所謂柵格化,是指將圖塊轉(zhuǎn)換為位圖。而圖塊是柵格化執(zhí)行的最小單位。渲染進(jìn)程維護(hù)了一個(gè)柵格化的線程池,所有的圖塊柵格化都是在線程池內(nèi)執(zhí)行的,運(yùn)行方式如下圖所示:

          7. 合成和顯示

          一旦所有圖塊都被光柵化,合成線程就會(huì)生成一個(gè)繪制圖塊的命令——“DrawQuad”,然后將該命令提交給瀏覽器進(jìn)程。

          瀏覽器進(jìn)程里面有一個(gè)名字叫做 viz 的組件,用來接收合成線程發(fā)過來的 DrawQuad 命令,然后根據(jù)命令執(zhí)行。 DrawQuad 命令,將其頁面內(nèi)容繪制到內(nèi)存中,最后再將內(nèi)存顯示在屏幕上。

          多年開發(fā)老碼農(nóng)福利贈(zèng)送:網(wǎng)頁制作,網(wǎng)站開發(fā),web前端開發(fā),從最零基礎(chǔ)開始的的HTML+CSS+JavaScript。jQuery,Vue、React、Ajax,node,angular框架等到移動(dòng)端小程序項(xiàng)目實(shí)戰(zhàn)【視頻+工具+電子書+系統(tǒng)路線圖】都有整理,需要的伙伴可以私信我,發(fā)送“前端”等3秒后就可以獲取領(lǐng)取地址,送給每一位對(duì)編程感興趣的小伙伴

          8. 總結(jié)

          一個(gè)完整的渲染流程可以總結(jié)如下:

          • 1、渲染進(jìn)程將HTML內(nèi)容轉(zhuǎn)換為瀏覽器能夠讀懂的DOM樹結(jié)構(gòu)。
          • 2、渲染引擎將CSS樣式表轉(zhuǎn)化為瀏覽器可以理解的styleSheets,計(jì)算出DOM節(jié)點(diǎn)的樣式。
          • 3、創(chuàng)建布局樹,并計(jì)算所需元素的布局信息。
          • 4、對(duì)布局樹進(jìn)行分層,并生成分層樹。
          • 5、為每個(gè)圖層生成繪制列表,并將其提交到合成線程。
          • 6、合成線程將圖層分圖塊,并柵格化將圖塊轉(zhuǎn)換成位圖。
          • 7、合成線程發(fā)送繪制圖塊命令給瀏覽器進(jìn)程。瀏覽器進(jìn)程根據(jù)指令生成頁面,并顯示到顯示器上。

          渲染過程中還有兩個(gè)我們經(jīng)常聽到的概念:重排和重繪。在這篇文章中就不細(xì)說了,下一篇文章再詳細(xì)介紹。

          象一下,成千上萬的人聚集在一個(gè)空間里,我們被要求了解更多關(guān)于它們的信息,或者收集關(guān)于這個(gè)群體或一群人的信息。

          首先,我認(rèn)為最好的辦法就是將他們分組,并從他們中創(chuàng)建一種與某種事物相關(guān)的新型群體。關(guān)系取決于我們?nèi)绾未_定將它們分組。例如,我們可以根據(jù)家庭,年齡,文化背景,教育背景,性別等將他們分組

          這些用于分組的參數(shù)是我們所知道的機(jī)器學(xué)習(xí)中的特征。將人或物體分組的方法沒有被告知要考慮的參數(shù)/特征被稱為聚類,并且該方法屬于一類機(jī)器學(xué)習(xí),稱為Un-監(jiān)督學(xué)習(xí)。

          分層聚類通過不斷合并兩個(gè)最相似的組來建立分組的層次結(jié)構(gòu)。每個(gè)組從一個(gè)項(xiàng)目開始,并且通過計(jì)算每個(gè)組之間的距離來增加每個(gè)組的迭代次數(shù),并且最接近的一個(gè)被合并一起形成新的群體,這一直重復(fù),直到他們只有一個(gè)群體。

          現(xiàn)在我們?nèi)绾未_定親密度; 我們可以使用計(jì)算一組實(shí)體之間的相似度的算法來確定接近度。可以使用歐幾里得或皮爾遜相關(guān)。

          但是,大多數(shù)情況下,我們將處理的數(shù)據(jù)比其他數(shù)據(jù)多,在這種類型的數(shù)據(jù)中,歐幾里德距離將無法正常工作。因此我們使用Pearson相關(guān)性,因?yàn)樗鼤?huì)通過真正試圖確定兩組數(shù)據(jù)如何適合一條直線來糾正。

          function pearson(v1,v2){

          //calculate the pearson correlation

          var sum1 = sum(v1);

          var sum2 = sum(v2);

          var sum1sq,sum2sq,dat1=[],dat2=[];

          for(var v in v1){

          dat1.push(Math.pow(v1[v],2));//the square if each datapoint in v1

          }

          sum1sq = sum(dat1);//the sum of data1

          for(var v in v2){

          dat2.push(Math.pow(v2[v],2));

          }

          sum2sq = sum(dat2);

          //console.log(sum1sq,sum2sq);

          var psum,pdata=[];

          for(var i in v1){

          pdata.push(v1[i]*v2[i]);//calc the product of v1 and v2

          }

          psum = sum(pdata);

          var num = psum - (sum1*sum2/v1.length);

          var den = Math.sqrt((sum1sq-Math.pow(sum1,2)/v1.length)*(sum2sq-Math.pow(sum2,2)/v1.length));

          if(den==0) return 0;

          return 1.0-num/den;//normalize the result between 0 and 1

          }

          當(dāng)兩個(gè)項(xiàng)目完美匹配時(shí),Pearson將始終返回1.0,而在完全沒有關(guān)系時(shí)接近0.0。

          在分層聚類中,每個(gè)聚類可以是具有兩個(gè)分支的樹中的一個(gè)點(diǎn),也可以是端點(diǎn)。并且該群集還包含有關(guān)其位置的數(shù)據(jù),該數(shù)據(jù)可以是端點(diǎn)的行數(shù)據(jù),也可以是其他節(jié)點(diǎn)類型的兩個(gè)分支的合并數(shù)據(jù)。然后我們創(chuàng)建一個(gè)包含構(gòu)建分層樹所需的所有這些屬性的類。

          function bicluster(opt){

          //to store the state of data

          this.left = opt.left || null;

          this.right = opt.right || null;

          this.vec = opt.vec;

          this.id = opt.id || 0;

          this.distance = opt.distance || 0.0;

          }

          我們通過創(chuàng)建一組/僅僅是原始項(xiàng)目的集群來啟動(dòng)算法。主循環(huán)(while循環(huán))通過嘗試每個(gè)可能的對(duì)并計(jì)算它們的相關(guān)性來搜索兩個(gè)最佳匹配。

          最好的一對(duì)群集合成一個(gè)群集。新群集的數(shù)據(jù)是兩個(gè)群集數(shù)據(jù)的平均值,并且重復(fù)該過程直到只剩下一個(gè)群集。由于時(shí)間消耗,存儲(chǔ)相關(guān)結(jié)果是很好的,因?yàn)樗鼈儗⑹冀K被重新計(jì)算。

          合并群集函數(shù)

          function mergevecs(a,b){//merge two array

          var mergdata = [];

          for(var i=0;i<a.length;i++){

          mergdata.push((a[i] + b[i])/2.0);

          }

          return mergdata;

          }

          主函數(shù)

          function hcluster(rows,distance){

          //row=>data,distance =>pearson

          var distances = {};

          var currentclustid = -1;

          var clust = [];

          for(var i=0;i<rows.length;i++){

          clust.push(new bicluster({vec:rows[i],id:i}));//propagate an array with an object

          }

          //console.log(distance(clust[1].vec,clust[1].vec));

          var store=[];

          while(clust.length > 1){//loop until the lengt of the cluster array is greater than 1

          let lowestpair= [0,1];//the lowest pair has index 0 and 1 if the array(i.e closest dist)

          var closest =distance(clust[0].vec,clust[1].vec);

          //console.lo(clust);

          for(var i=0;i<clust.length;i++){

          for(var j=i+1; j<clust.length; j++ ){

          var y = clust[i].id+","+clust[j].id; //store the id has string for object property

          if(!( y in distances)){//store the distance

          distances[clust[i].id+","+clust[j].id]= distance(clust[i].vec,clust[j].vec);

          }

          var d = distances[clust[i].id+","+clust[j].id]

          if(d < closest){//choose the lowest distance and store the index

          closest = d;

          lowestpair[0] =i;

          lowestpair[1] =j;

          }

          }

          }

          var mergevec = mergevecs(clust[lowestpair[0]].vec,clust[lowestpair[1]].vec);

          var newcluster = new bicluster({vec:mergevec,left:clust[lowestpair[0]],

          right:clust[lowestpair[1]],

          distance:closest,id:currentclustid});

          currentclustid -=1;//decrease the cluster id

          //store.push("("+lowestpair[1]+","+lowestpair[0]+")")

          clust.splice(lowestpair[1],1);

          clust.splice(lowestpair[0],1);

          clust.push(newcluster);

          //lend--;

          }

          return clust[0];

          }

          分層聚類的可視化圖被稱為展示層次結(jié)構(gòu)中dendogram which di排列的節(jié)點(diǎn)。檢查頁面開頭的圖片。我們不會(huì)走得太遠(yuǎn)。但是我們找到了以格式良好的方式打印出簇的方法。

          function v(n){

          //function to space the output properly

          var space =[];

          for(var i =0;i< n;i++){

          space.push(' ');

          }

          return space;

          }

          function printclust(clust,labels,n){

          var space = v(n).join('')

          if(clust.id < 0){//indicate a group(parent)

          console.log(space+'-');

          }

          else{

          console.log(space+labels[clust.id]);// the child

          }

          if(clust['left'] !=null){

          printclust(clust['left'],labels,n+1);

          }

          if(clust['right'] !=null){

          printclust(clust['right'],labels,n+1);

          }

          }

          然后讓我們?cè)囋囘@個(gè)算法:

          var data = [[1.0,8.0],[3.0,8.0],[2.0,7.0],[1.5,1.0],[4.0,2.0]];

          var labels=['A','B','C','D','E'];

          //euclidean distance is best for this

          //since the data have the same number of entry

          var euclid= function(v1,v2){

          var sum=0;

          for(var i=0;i<v1.length;i++){

          sum+= Math.pow(v1[i]-v2[i],2);

          }

          return sum;

          };

          var clust = hcluster(data,euclid);

          printclust(clust,labels);

          //output

          -

          -

          B

          -

          A

          C

          -

          D

          E

          輸出顯示B,A,C屬于同一個(gè)根節(jié)點(diǎn),而D,E屬于另一個(gè)根節(jié)點(diǎn)。

          讓我們?cè)囋囌嬲臄?shù)據(jù)集上的算法。它是一個(gè)包含數(shù)據(jù)矩陣的文件,關(guān)于不同的博客和與之關(guān)聯(lián)的單詞(特征)。博客名稱被表示為行并且文字具有列。該矩陣基于每個(gè)博客文章中每個(gè)詞的出現(xiàn)

          我們創(chuàng)建一個(gè)從該文件讀取數(shù)據(jù)的函數(shù),并返回一個(gè)包含博客名稱的對(duì)象,一個(gè)與每個(gè)博客關(guān)聯(lián)的浮動(dòng)詞的數(shù)組以及這些詞(特征)的列表。這個(gè)文件可以在這里下載(https://github.com/steveoni/Hierarchical_clustering_js/blob/master/bb.txt)。

          /**

          *@param filename

          *@return an object:name of the rows,coulumn and the data

          **/

          function readfile(filename){

          //read file synchronously and split by newline to an array

          var lines = fs.readFileSync(filename).toString().split("\n");

          var colnames = lines[0].trim().split('\t');//get the first index which is the column(word in each blog)

          var p,l=[];

          var rownames= [];//store each rownames

          var data =[];//store the matrix data

          lines.shift();// delete the column from the data.

          for(var line in lines){

          p= lines[line].trim().split('\t');//convert the string to array base on the tab

          rownames.push(p[0]);//index 0 is the name of the row(the title of the blog)

          p.shift();//remove the title

          data.push(p);//store the values

          }

          var data2=new Array(data.length);

          for(var i=0;i<data2.length;i++){

          data2[i] = zeros(data[0].length);//fill the array with zeros

          }

          for(var i =0;i<data.length;i++){

          var i_dat = data[i];

          for(var j=0;j<i_dat.length;j++){

          data2[i][j] += parseFloat(data[i][j]);//store the array with the float of the actual digit

          }

          }

          return {rownames:rownames,

          colnames:colnames,

          data:data2};

          }

          然后讓它運(yùn)行`hcluster`:

          var file = readfile('bb.txt')

          var clutster = hcluster(file.data,pearson);

          printclust(cluster,file.rownames,0);


          主站蜘蛛池模板: 国产乱码伦精品一区二区三区麻豆| 国产女人乱人伦精品一区二区 | 亚洲一区二区三区在线| 99久久精品国产免看国产一区 | 久久se精品一区二区| 日本在线视频一区二区| 色综合一区二区三区| 日韩精品区一区二区三VR | 成人在线一区二区| 蜜臀AV在线播放一区二区三区| 精品成人一区二区三区免费视频| 久久精品一区二区| 中文乱码人妻系列一区二区| 色狠狠一区二区三区香蕉蜜桃| 久久久综合亚洲色一区二区三区| 国产精品一区二区三区久久| 日本内射精品一区二区视频| 亚洲熟女综合色一区二区三区| 一区二区三区高清在线 | 亚洲午夜一区二区三区| 国产一区二区在线观看视频| 奇米精品一区二区三区在| 亚洲香蕉久久一区二区| 亚洲国产精品一区二区第一页免 | 国产精品无码一区二区三区毛片| 中文字幕VA一区二区三区 | 日本免费一区二区三区四区五六区| 无码精品一区二区三区| 国产美女一区二区三区| 香蕉在线精品一区二区| 国产伦精品一区二区免费| 亚洲一区二区三区免费观看| 北岛玲在线一区二区| 中文字幕日韩丝袜一区| 国产一区二区在线观看app| 国语对白一区二区三区| 性盈盈影院免费视频观看在线一区| 国产在线精品一区二区高清不卡 | 国精产品一区二区三区糖心| 国产乱人伦精品一区二区在线观看| 国产一区二区电影|