skynet socket.lua 讀寫緩衝區剖析

這兩天剖析了一下socket.lua,整體不是很難,主要是數據緩衝區的實現需要好好分析一下。

這裏讀寫數據也是用到了緩衝池的思想,爲了更加直觀的說明代碼,還有方便測試,我去掉lua代碼,把核心接口直接用C++實現了一遍:

#include <stdio.h>
#include <string.h>
#include <vector>
using std::vector;

#define LARGE_PAGE_NODE 12

struct buffer_node {
    char* msg;
    int sz;
    struct buffer_node* next;
};

struct socket_buffer {
    int size;
    int offset;
    struct buffer_node *head;
    struct buffer_node *tail;
};


struct buffer_pool {
    buffer_node** pool;
    buffer_node* head;
    int len;
    buffer_pool()  {
        len = 0;
        pool = NULL;
        head = NULL;
    }
    buffer_node* new_pool(int size) {        //構建size個緩衝池
        len = size;
        pool = new buffer_node*[size];
        for (int i = 0; i < size; i++)  {
            pool[i] = new buffer_node;
        }
        for (int i = 0; i < size; i++)  {
            pool[i]->msg = NULL;
            pool[i]->sz = 0;
            pool[i]->next = pool[i + 1];
        }
        pool[len - 1]->next = 0;
        head = pool[0];
        return head;
    }
};


int push_buffer(socket_buffer* sb, buffer_pool* pool, char* msg, int sz)     //寫數據到緩衝區
{
    buffer_node* free_node = NULL;
    if (pool->head == NULL)  {
        int len = pool->len + 1;
        int size = 8;
        if (len <= LARGE_PAGE_NODE - 3 )  {
            size <= len;
        }  else  {
            size <= LARGE_PAGE_NODE - 3;
        }
        free_node = pool->new_pool(size);
    }
    else  {
        free_node = pool->head;
    }
    pool->head = free_node->next;
    free_node->msg = msg;
    free_node->sz = sz;
    free_node->next = NULL;
    
    if (sb->head == NULL)  {
        sb->head = sb->tail = free_node;
    }  else  {
        sb->tail->next = free_node;
        sb->tail = free_node;
    }
    sb->size += sz;
    return sb->size;
}



void return_free_node(buffer_pool* pool, socket_buffer* sb)    //返回給緩衝池
{
    buffer_node* free_node = sb->head;
    sb->offset = 0;
    sb->head = free_node->next;
    if (sb->head == NULL)  {
        sb->tail = NULL;
    }
    free_node->next = pool->head;
    //delete free_node->msg;       //如果是堆上數據這裏必須要釋放
    free_node->msg = NULL;
    free_node->sz = 0;
    pool->head = free_node;
}


char* read_buffer(buffer_pool* pool, socket_buffer* sb, int sz)  //讀取緩衝區sz個字節
{
    if (sb->size < sz || sz == 0)  {
        return NULL;
    }  else  {
        sb->size -= sz;
        
        buffer_node* cur = sb->head;
        char* msg = new char[sz + 1];
        if (sz < cur->sz - sb->offset)  {
            memcpy(msg, cur->msg + sb->offset, sz);
            sb->offset += sz;
            msg[sz] = 0;
            return msg;
        }
        if (sz == cur->sz - sb->offset)  {
            memcpy(msg, cur->msg + sb->offset, sz);
            return_free_node(pool, sb);
            return msg;
        }
    }
}


char* readall(buffer_pool* pool, socket_buffer* sb)      //讀取緩衝區所有的字節
{
    vector<char> vt;
    while(sb->head)  {
        sb->head;
        buffer_node* cur = sb->head;
        int len = cur->sz - sb->offset;
        int raw_len = vt.size();
        vt.resize(len + raw_len);
        memcpy(&vt[raw_len], cur->msg + sb->offset, len);
        return_free_node(pool, sb);
    }
    sb->size = 0;
    int len = vt.size();
    char* msg = new char[len + 1];
    memcpy(msg, &vt[0], len);
    msg[len] = 0;
    return msg;
}

int main()
{
    socket_buffer* sb = new socket_buffer;
    buffer_pool* pool = new buffer_pool;
    char msg1[10] = "123456";
    
    push_buffer(sb, pool, msg1, 10);                 //第一次往緩衝區寫10個字節
    push_buffer(sb, pool, "abcdef", 6);              //第二次往緩衝區寫6個字節
    
    char* msg = read_buffer(pool, sb, 3);            //從緩衝區讀3個字節  
    printf("msg is %s\n", &msg[0]);
    
    delete sb;
    delete pool;
}

爲了更加清楚的講明白代碼的原理,我喜歡用一張張圖來描述其中的要點,這篇也不例外。

當socket數據到來時要用緩衝區來保存數據。剛開始的時候,緩衝池是空的,所以遇到緩衝池爲空的情況我們就要創建幾個緩衝區數據構成緩衝池。每個緩衝區有數據指針以及數據大小字段,當然緩衝區是以鏈表的形式連在一塊的,如下圖所示:

 

構建緩衝池.png

                                                                               

構建好緩衝池之後就可以往裏面寫數據了,我們取下頭結點作爲容器來盛裝數據,剩下的作爲空閒的緩衝區。爲了描述這個緩衝區數據,用一個指針指向這個緩衝區,且具有大小字段。由於讀緩衝區數據也需要記錄一些數據,例如讀取了多少,所以我們定義一個數據結構來描述當前緩衝區的狀況:

 

寫入數據1.png

                                                                                

如果我們一次性讀取的數據超過了一個緩衝區數據,觸及到了下一個緩衝區呢?所以這個數據結構應該是這樣的:

 

寫入數據2.png

                                                                                

可以看到他有一頭一尾兩個指針,來指向所需要用到緩衝區,構成一個閉環。發現雲風很喜歡用這個數據結構,在全局消息隊列中也用到了這個。

當某個緩衝區的數據讀取完畢之後,該緩衝區就要被回收,重複使用,這也是池的理念之一,如下圖:

 

回收緩衝區.png

                                                                              

空閒緩衝區的頭結點將指向這個新回收的緩衝區,以備下次使用。

下面中重點介紹寫入和讀取緩衝區數據的幾個函數。

socket.read(id,sz)將會讀取指定字節的數據,他會調用底層的lpopbuffer函數,就是上面的read_buffer函數。如果緩衝池中沒有足夠的數據,那麼將會返回nil,此時read函數調用suspend阻塞,直到有新的數據到來,緩衝池中的數據滿足要求。這個是利用協程的暫停和恢復實現的。

有新的數據到來時socket回調函數會被觸發,數據被寫入緩衝池中,這個底層是調用lpushbuffer實現的,對應上面的push_buffer函數。注意這個函數會返回緩衝池中所有未讀數據的長度,所有一旦緩衝池中的數據達到socket.read的長度要求,那麼將會調用wakeup喚醒之前被掛起的協程,從而再次調用lpopbuffer函數獲取數據。

如果sz參數是空,則將讀取整個緩衝區的數據。他在調用底層的lreadall函數,讀取整個緩衝池中的數據。

 

歡迎加入QQ羣 858791125 討論skynet,遊戲後臺開發,lua腳本語言等問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章