linux設備驅動之API的實現

Linux 下API的實現

作者: 韓大衛@ 吉林師範大學

驅動工程師工作內容之一就是向上層應用端提供API,這個API完成並封裝了全部的與硬件芯片的I/O操作。

本問簡單的說明了一個實現API函數的全部過程。

總體上看分爲:

1,用戶API
2,用戶中間層(與底層通信)
3,底層中間層(尋找對應的驅動函數)
4,驅動函數
5,  CPU讀寫I/O端口。

我們主要的工作就是這個驅動部分

這個驅動函數功能是:將數據包裝成kernel 中可操作的結構體, 按既有的通信方式發送給CPU,

這種通信方式就是通過總線了。可以是I2C等等。

最後,CPU返回執行結構,驅動函數再返回給用戶層。

再將這些API 包裝成庫, 這樣,用戶層的程序就可以直接引用了。

下面的API功能是是: 統計交換芯片的接口速率。

Test/main.c 是一個測試程序, API 和API 的底層驅動的實現文件和頭文件都在下面的代碼中,

另外,用戶層與底層之間的通信,使用的是socket ,這是相比與共享內存等最合適最高效的方式。

路徑如下: 用戶層API--- 中間層(socket)----| -----驅動中間層(socket)---(驅動函數)

這不是完整的程序,只是程序中的骨幹部分。

如果需要全部的程序,請聯繫作者[email protected]

***** ********************
轉載請務必表明出處。
********* ***************************
在test/main.c 中:


#include "inc/jas.h"
#include <jas_sw_cmd_api.h>
#include <jas_cmd_cli_api.h>
#include <jas_frm_cmd_api.h>




在  inc/cmd_api/jas_sw_cmd_api.h  中:


 int jas_sw_intf_cntr_rate_cal(int dev_id, int intf_id, jas_intf_eth_rate_t *cntr);   

//這就是用戶層使用的API

./libjascmd/jas_cmd_cli_api.h  中:

 int jas_cmd_cli_api_send_msg(struct jas_cmd_api_cmd_msg tmsg);
  int jas_cmd_cli_api_recv_msg(void *data, uint32_t size);
 


在Main.c 中的  jas_sw_intf_cntr_rate_cal()定義在:

jas_api/switch/sw_cmd_api.c 中:



int jas_sw_intf_cntr_rate_cal(int dev_id, int intf_id, jas_intf_eth_rate_t *data){                                                   
    int ret = JAS_SUCCESS;
 
    if(data == NULL)
        return JAS_INV_ARG;
 
    ret = jas_cmd_sw_api_intf_cntr_rate_cal(dev_id, intf_id, data);
    if(ret < 0)
        return ret;
 
    return ret;
}
 

jas_cmd_sw_api_intf_cntr_rate_cal

這個函數是一箇中間層,介於用戶層和驅動層之間, 它的聲明是在:

libjascmd/jas_cmd_sw_api.h  中:

int jas_cmd_sw_api_intf_cntr_rate_cal(uint8_t dev_id, uint8_t intf_id, jas_intf_eth_rate_t *jas_cntr_data);     



定義是在  ibjascmd/jas_cmd_sw_api.c  中:


int jas_cmd_sw_api_intf_cntr_rate_cal(uint8_t dev_id, uint8_t intf_id, jas_intf_eth_rate_t *jas_cntr_data){                          
    int ret = JAS_SUCCESS;
    struct jas_cmd_api_cmd_msg msg;
                   
    msg.dev_id = dev_id;
    msg.intf_id = intf_id;
    msg.cmd = SW_INTF_CNTR_RATE_CAL;
    msg.dev_type = JAS_CMD_SWITCH;
    
    ret = jas_cmd_cli_api_send_msg(msg);
    if (ret < 0) {
        return JAS_SET_ERROR;
    }              
    
    ret = jas_cmd_cli_api_recv_msg(jas_cntr_data, (uint32_t)sizeof(jas_intf_eth_rate_t));
    if(ret < 0) {
        return ret;
    }
                   
    return ret;    
}



通過使用socket  ,發送和接受msg . 實現與驅動層的通信。

那麼,socket的接受端是在哪裏?

可以通過  grep -rn  “SW_INTF_CNTR_RATE_CAL”  的方式,查找出相應的位置。

結果如下:


在libjascmd/jas_cmd_cli_api.c 中:

發現了  case SW_INTF_CNTR_RATE_CAL

進入這個文件中,  就可以找到這個socket 的接受端, 函數如下:


int jas_cmd_cli_serv_check_client(cmdStreamPTR streamP, char *buff, uint32_t length){
    socketStreamPTR stream = (socketStreamPTR)streamP;
        
    if(length == 0)
        return JAS_SUCCESS;
        
    if(strncmp(buff, "@@JAS_CMD_API", 12) == 0){
        write(stream->socket, "Connected!", 10);
        return JAS_SUCCESS;
    }   
        
    if((length != sizeof(struct jas_cmd_api_cmd_msg)) || (strncmp(buff, "########", 8) != 0)){
        sys_err("%s, %d, Get Error Buffer: %s, length: %d(%d)\n", __func__, __LINE__, buff, length, sizeof(struct                    jas_cmd_api_cmd_msg));
        return JAS_FAILURE;
    }                                                                                                                                
        
    jas_cmd_cli_serv_run_cmd(streamP, buff);
        
    return 0;
}       



進入jas_cmd_cli_serv_run_cmd

int jas_cmd_cli_serv_run_cmd(cmdStreamPTR streamP, char *buff){
    struct jas_cmd_api_cmd_msg cmsg;
    struct jas_cmd_cli_msg msg;
    socketStreamPTR stream = (socketStreamPTR)streamP;
 
    memcpy(&cmsg, buff, sizeof(cmsg));
 
    memset(&msg, 0, sizeof(struct jas_cmd_cli_msg));
    memcpy(msg.flag, "########", 8);
 
    msg.ret = JAS_SUCCESS;
    msg.len = sizeof(struct jas_cmd_cli_msg) - 8;
 
    switch(cmsg.dev_type){
    case JAS_CMD_FRAMER:
        __jas_cmd_api_run_frm_cmd(cmsg, &msg);                                                                                       
        break;
    case JAS_CMD_SWITCH:
        __jas_cmd_api_run_sw_cmd(cmsg, &msg);
        break;
    default:
        sys_err("%s, %d, Unknow Dev Type, buff: %s, cmsg.cmd: %d, cmsg.dev_id: %d.\n",
                    __func__, __LINE__, buff, cmsg.cmd, cmsg.dev_id);
        break;
    }      
           
    write(stream->socket, &msg, sizeof(struct jas_cmd_cli_msg));
    return 0;
}          



這樣就清楚了,通過msg中的變量類型,進入
__jas_cmd_api_run_frm_cmd(cmsg, &msg);

或者
   __jas_cmd_api_run_sw_cmd(cmsg, &msg);

處理msg, 最後將處理後的msg通過 write(stream->socket, &msg, sizeof(struct jas_cmd_cli_msg)) 發送出去,  

這樣就實現了用中間層的通信。


進入  static int __jas_cmd_api_run_sw_cmd()看一下,

static int __jas_cmd_api_run_sw_cmd(struct jas_cmd_api_cmd_msg cmsg, struct jas_cmd_cli_msg *msg){
 
    switch(cmsg.cmd){
    case SW_HW_REG_SET:
        {
            jas_reg_t *preg = (jas_reg_t *)cmsg.data;
            msg->ret = jas_sw_drv_hw_reg_set(cmsg.dev_id, preg->regaddr, preg->value);
        }
        break;

….....

    case SW_INTF_CNTR_RATE_CAL:                                                                                                      
        {              
            jas_intf_eth_cntr_t *cntr;
            cntr = (jas_intf_eth_cntr_t *)msg->data;

            msg->ret = jas_sw_drv_intf_cntr_rate_cal(cmsg.dev_id, cmsg.intf_id, cntr);

             // 這裏使用了真正的驅動函數

        }              
        break;         


….. …
    return 0;
}


這個jas_sw_drv_intf_cntr_rate_cal()就是驅動層的相應功能實現函數。

jas_sw_drv_intf_cntr_rate_cal()  的定於是在: libsw/sw_drv_api.c

int jas_sw_drv_intf_cntr_rate_cal(uint8_t dev_id, uint8_t intf_id, jas_intf_eth_rate_t *jas_cntr_data){
     
    jas_intf_eth_cntr_t sw_status_info1;
    jas_intf_eth_cntr_t sw_status_info2;
    int32_t result;
    uint64_t *data;
    time_t lt,lt1,lt2;
    struct tm* ptr1;
    struct tm* ptr2;
     
    memset(&sw_status_info1, 0, sizeof(sw_status_info1));
    memset(&sw_status_info2, 0, sizeof(sw_status_info2));
     
    jas_sw_drv_intf_cntr_clear_on_read_set(dev_id, intf_id, JAS_FALSE);
    jas_sw_drv_intf_cntr_get(dev_id, intf_id, &sw_status_info1);
    lt1 = time(NULL);
     
    sleep(1);
     
    jas_sw_drv_intf_cntr_clear_on_read_set(dev_id,intf_id, JAS_FALSE);
    jas_sw_drv_intf_cntr_get(dev_id, intf_id, &sw_status_info2);
    lt2 = time(NULL);   
     
    lt = lt2 - lt1;
    ptr1 = localtime(&lt1);           
    ptr2 = localtime(&lt2);           
     
  result = device_port_rate_cal(sw_status_info2.good_pkts.rx * 8,
                                sw_status_info1.good_pkts.rx * 8,
                                &(jas_cntr_data->good_pkts.rx),
                                lt);
     
    result = device_port_rate_cal(sw_status_info2.good_pkts.tx * 8,
                                sw_status_info1.good_pkts.tx * 8,
                                &(jas_cntr_data->good_pkts.tx),
                                lt);
     
    result = device_port_rate_cal(sw_status_info2.good_bytes.rx * 8,    
                                sw_status_info1.good_bytes.rx * 8,
                                &(jas_cntr_data->good_bytes.rx),
                                lt);
    result = device_port_rate_cal(sw_status_info2.good_bytes.tx * 8,    
                                sw_status_info1.good_bytes.tx * 8,
                                &(jas_cntr_data->good_bytes.tx),
                                lt);
     
    return  0;
}    



現在有一個問題:

jas_sw_drv_intf_cntr_rate_cal()只是在sw_drv_api.c 中有定義,沒有在任何.h文件中有聲明,


那麼在libjascmd/jas_cmd_cli_api.c  中的


case SW_INTF_CNTR_RATE_CAL:              
        {                                    
            jas_intf_eth_cntr_t *cntr;       
            cntr = (jas_intf_eth_cntr_t *)msg->data;
            msg->ret = jas_sw_drv_intf_cntr_rate_cal(cmsg.dev_id, cmsg.intf_id, cntr);                                               
        }                                    
        break;                               

jas_sw_drv_intf_cntr_rate_cal()的聲明是在 在inc/cmd_api/jas_sw_cmd_api.h 中



#include <jas_sw_cmd_api.h>

進而使用了這個函數。


這樣就完成了API的實現


路徑如下: 用戶層API--- 中間層(socket)----| -----驅動中間層(socket)---(驅動函數)


詳細說明如下:

用戶層API :             jas_sw_intf_cntr_rate_cal()

API的聲明(頭文件):      inc/cmd_api/jas_sw_cmd_api.h
    
int jas_sw_intf_cntr_rate_cal(int dev_id, int intf_id, jas_intf_eth_rate_t *cntr);       

API的定義(實現):        jas_api/switch/sw_cmd_api.c

int jas_sw_intf_cntr_rate_cal(int dev_id, int intf_id, jas_intf_eth_rate_t *data){                                                   
    int ret = JAS_SUCCESS;
          
    if(data == NULL)
        return JAS_INV_ARG;
          
    ret = jas_cmd_sw_api_intf_cntr_rate_cal(dev_id, intf_id, data);
    if(ret < 0)
        return ret;
          
    return ret;
}         
          



用戶中間層的函數:   jas_cmd_sw_api_intf_cntr_rate_cal()

聲明(頭文件):      libjascmd/jas_cmd_sw_api.h

定義:                 libjascmd/jas_cmd_sw_api.c

int jas_cmd_sw_api_intf_cntr_rate_cal(uint8_t dev_id, uint8_t intf_id, jas_intf_eth_rate_t *jas_cntr_data){                          
    int ret = JAS_SUCCESS;
    struct jas_cmd_api_cmd_msg msg;
 
    msg.dev_id = dev_id;
    msg.intf_id = intf_id;
    msg.cmd = SW_INTF_CNTR_RATE_CAL;
    msg.dev_type = JAS_CMD_SWITCH;
 
    ret = jas_cmd_cli_api_send_msg(msg);
    if (ret < 0) {
        return JAS_SET_ERROR;
    }    
 
    ret = jas_cmd_cli_api_recv_msg(jas_cntr_data, (uint32_t)sizeof(jas_intf_eth_rate_t));
    if(ret < 0) {
        return ret;
    }
 
    return ret;
}


驅動層中間函數 : __jas_cmd_api_run_sw_cmd

位置: libjascmd/jas_cmd_cli_api.c



 case SW_INTF_CNTR_RATE_CAL:                                                                                                      
        {              
            jas_intf_eth_cntr_t *cntr;
            cntr = (jas_intf_eth_cntr_t *)msg->data;
            msg->ret = jas_sw_drv_intf_cntr_rate_cal(cmsg.dev_id, cmsg.intf_id, cntr);
        }              
        break;         




驅動函數: jas_sw_drv_intf_cntr_rate_cal()

聲明:     inc/drv/sw_drv_api.h


定義:      libsw/sw_drv_api.c

int jas_sw_drv_intf_cntr_rate_cal(uint8_t dev_id, uint8_t intf_id, jas_intf_eth_rate_t *jas_cntr_data){
 
    jas_intf_eth_cntr_t sw_status_info1;
    jas_intf_eth_cntr_t sw_status_info2;
    int32_t result;
    uint64_t *data;
    time_t lt,lt1,lt2;
    struct tm* ptr1;
    struct tm* ptr2;
    
    memset(&sw_status_info1, 0, sizeof(sw_status_info1));
    memset(&sw_status_info2, 0, sizeof(sw_status_info2));
 
    jas_sw_drv_intf_cntr_clear_on_read_set(dev_id, intf_id, JAS_FALSE);
    jas_sw_drv_intf_cntr_get(dev_id, intf_id, &sw_status_info1);
    lt1 = time(NULL);
    
….

}                             


*************** ******************************************************


2012.7.30    14:00

遇到一個錯誤:

jascmd/jas_cmd_cli_api.c
libjascmd/jas_cmd_cli_api.c: In function '__jas_cmd_api_run_i2c_cmd':
libjascmd/jas_cmd_cli_api.c:335: error: 'I2C_INTF_I2C_TMP_GET' undeclared (first use in this function)
libjascmd/jas_cmd_cli_api.c:335: error: (Each undeclared identifier is reported only once
libjascmd/jas_cmd_cli_api.c:335: error: for each function it appears in.)

在jascmd/jas_cmd_cli_api.c  中明明有:
 #include "jas_cmd_i2c_api.h"    


在jascmd/jas_cmd_i2c_api.h 也有
        
typedef enum {
    I2C_INTF_I2C_TMP_GET = 0,
    I2C_INTF_I2C_RTC_GET,
    I2C_INTF_I2C_EEPROM_GET,
    I2C_INTF_I2C_SW_XFP_GET,
    I2C_INTF_I2C_SW_SFP_GET,
}jas_cmd_i2c_api_cmd_type_t;
            
但爲什麼說沒有定義呢?
結果是:#ifdef  __JAS_CMD_I2C_API_H    
應改爲:#ifndef  __JAS_CMD_I2C_API_H    

下午17:00

..//libjascmd.so: undefined reference to `jas_i2c_drv_intf_i2c_tmp_get'
..//libjascmd.so: undefined reference to `jas_i2c_drv_intf_i2c_sw_xfp_get'
..//libjascmd.so: undefined reference to `jas_i2c_drv_intf_i2c_sw_sfp_get'
..//libjascmd.so: undefined reference to `jas_i2c_drv_intf_i2c_rtc_get'
..//libjascmd.so: undefined reference to `jas_i2c_drv_intf_i2c_eeprom_get'


在Makefile 也添加了libi2c 的信息,
爲什麼還是找不到?

    
原來是在test 中的Makefile 沒有添加li2c


**** ************************************



int jas_cmd_frm_api_intf_status_get(int dev_id, int intf_id, jas_frm_intf_status_t *status){
    int ret = JAS_SUCCESS;
    struct jas_cmd_api_cmd_msg msg;  
                       
    JAS_NULL_PTR_CHECK(status);
                       
    msg.dev_id = dev_id;
    msg.intf_id = intf_id;
    msg.cmd = FRM_INTF_STATUS_GET;
    msg.dev_type = JAS_CMD_FRAMER;  
                       
    ret = jas_cmd_cli_api_send_msg(msg);                                                                                              
    if (ret < 0) {  
        return JAS_SET_ERROR;
    }                  
                       
    ret = jas_cmd_cli_api_recv_msg(status, (uint32_t)sizeof(jas_frm_intf_status_t));
    if(ret < 0) {    
        return JAS_SET_ERROR;
    }                  
                       
    return ret;        
}  

*********** ********
jas_cmd_cli_api_send_msg 定義如下:

int jas_cmd_cli_api_send_msg(struct jas_cmd_api_cmd_msg cmsg){
    jas_cmd_cli_api_handle_t *ctrl = (jas_cmd_cli_api_handle_t *)handle;                                                              
                 
    JAS_NULL_PTR_CHECK(handle);
                 
    memcpy(cmsg.flag, "########", 8);  
                 
    if(__sock_check(ghandle[ctrl->id]->sockfd)){
        sys_err("socket %d break off.\n", ghandle[ctrl->id]->sockfd);
        close(ghandle[ctrl->id]->sockfd);
        ghandle[ctrl->id]->sockfd = 0;
        if(__reconnect(ctrl->id) != 0){  
                sys_err("Failed to reconnect server.\n");
                return JAS_SOCK_ERROR;
        }        
    }            
                 
    return write(ctrl->sockfd, &cmsg, sizeof(struct jas_cmd_api_cmd_msg));
}                



strcut  jas_cmd_cli_api_handle_t  的定義是:

typedef struct jas_cmd_api_handle{
    int id;  
    int sockfd;
    char *ip_addr;
    uint32_t port;
}jas_cmd_cli_api_handle_t;                                                                                                            
 

__sock_check() 定義如下:

static int __sock_check(int sockfd){
    fd_set fds;
    int ret;
    struct timeval tv_sel;
      
    if(sockfd == 0){
        sys_err("%s, %d, error, %s.\n", __func__, __LINE__, "SOCK FD is 0");
        return JAS_FAILURE;
    }     
          
    tv_sel.tv_sec = 0;
    tv_sel.tv_usec = 0;
    ret = select(sockfd + 1, &fds, NULL, NULL, &tv_sel);  
    if(ret != 0){
        if(ret < 0)
            sys_err("%s, %d, error, %s.\n", __func__, __LINE__, strerror(errno));
    }  /*                  
        else {   
            if(FD_ISSET(sockfd, &fds)) {
                len = read(sockfd, buf, 100);
                if(len == 0) {
                        sys_err("sockfd %d break off.\n", sockfd);
                        return -1;
                }                                                                                                                     
                sys_info("__sock_check buff: %s\n", buf);
            }    
        }           
*/      
           
    return 0;
}          
ret = select(sockfd + 1, &fds, NULL, NULL, &tv_sel);  

sockfd +1 :是指集合中所有文件描述符的範圍,即所有文件描述符的最大值加1.

fds是一個文件描述符的集合,監視這些文件描述符的讀變化的

第一個NULL 表示 不關心任何文件的寫變化

第二個NULL 表示 不用監視文件錯誤異常。


時間值爲0秒0毫秒,即將select()函數設置成一個純粹的非阻塞函 數,不管文件描述符是否有變化,

都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;



***************** ****

 return write(ctrl->sockfd, &cmsg, sizeof(struct jas_cmd_api_cmd_msg));


向 ctr-> sockfd 發送 cmsg, 大小爲sizeof(struct jas_cmd_api_cmd_msg);


************* **********


jas_cmd_cli_api_recv_msg() 函數定義是:


int jas_cmd_cli_api_recv_msg(void *data, uint32_t size){                                                                              
    jas_cmd_cli_api_handle_t *ctrl = (jas_cmd_cli_api_handle_t *)handle;
    struct jas_cmd_cli_msg *msg = NULL;
    int len = CMD_MAX_MSG;
    int i, read_len, mlen=0;
    struct timeval timeout;
 
    JAS_NULL_PTR_CHECK(handle);
 
    timeout.tv_sec = 900;
    timeout.tv_usec = 0;
    mlen = len;
 
    read_len = __sock_read_data(ctrl->sockfd, buf_rd, &timeout, &mlen);
    if(read_len < len){
        sys_err("%s, %d, Read buffer error: \"%s\", len %d(%ld).\n", __func__, __LINE__, buf_rd, read_len, len);
        return JAS_FAILURE;
    }
 
    /*get the right msg length*/
    sys_debug("%s, %d, read buffer: \"%s\", len %d(%ld).\n", __func__, __LINE__, buf_rd, read_len, len);
    for(i = 0; i < read_len; i++){
        if(buf_rd[i] == '#'){
            if(strncmp((char *)&buf_rd[i], "########", 8) == 0){
                msg = (struct jas_cmd_cli_msg *)&buf_rd[i];
                sys_debug("print msg:\n");
                sys_debug("valid len: %d, msg ret: %d.\n",  
                        msg->len, msg->ret);
                if(msg->ret != JAS_SUCCESS){
                    sys_err("Operation failed!\n");
                    return msg->ret;
                }
                if(msg->len != (len - 8)){
                    sys_err("Get Wrong Length: %d(%d).\n",  
                            (len - 8), msg->len);
                    return JAS_FAILURE;
                }
                break;
            }
        }
   if(data != NULL)
        memcpy(data, msg->data, size);
 
    return 0;
}
 


__sock_read_data() 定義是:


/*return length of data*/
static int __sock_read_data(int sockfd, uint8_t *buf, struct timeval *timeout, int *len){
    fd_set fds;
    int ret, mlen=0;
    struct timeval tv1, tv2, tv_sel;  
 
    if(sockfd == 0){
        sys_err("%s, %d, error, %s.\n", __func__, __LINE__, "SOCK FD is 0");
        return JAS_INV_ARG;
    }
 
    gettimeofday(&tv1, NULL);                                                                                                         
    while(1){
        gettimeofday(&tv2, NULL);
        if(((tv2.tv_sec-tv1.tv_sec)*1000000 + (tv2.tv_usec-tv1.tv_usec)) >=  
                    (timeout->tv_sec*1000000 + timeout->tv_usec)){

            return 0;
        }
 
        FD_ZERO(&fds);
        FD_SET(sockfd, &fds);
        tv_sel.tv_sec = 0;
        tv_sel.tv_usec = 100000;
        ret = select(sockfd + 1, &fds, NULL, NULL, &tv_sel);   
        if(ret != 0){
            if(ret < 0){
                sys_err("%s, %d, error, %s.\n", __func__, __LINE__, strerror(errno));
                return JAS_FAILURE;
            }
            if(FD_ISSET(sockfd, &fds)){
                mlen += read(sockfd, &buf[mlen], MSG_RD_BUF_LEN);
                sys_debug("buff: %s, len: %d.\n", buf, mlen);
                if((len != NULL) && (mlen < *len)){
                    continue;
                }
                return mlen;
            }
 
            return -1;
        }
    }
 
    return 0;
}
 

   FD_ZERO(&fds);
        FD_SET(sockfd, &fds);

FD_ZERO(&fds) 清空集合fds
FD_SET(sockfd, &fds);    將一個給定的文件描述符socket加入集合fds之中

     ret = select(sockfd + 1, &fds, NULL, NULL, &tv_sel);   

集合最大文件描述符號範圍 : sockfd +1

&fds , 監視fds 文件描述符集合。

NULL: 不關心文件寫變化
NULL :不關心文件異常

&tv_sel :  0 : 秒  100000: 微秒/

即select在100000 微妙內內阻塞,超時時間之內有事件到來就返回了,否則在超時後不管怎樣一定返回。


FD_ISSET(sockfd, &fds)      fds中的文件描述符 sockfd 是否可以讀。

 mlen += read(sockfd, &buf[mlen], MSG_RD_BUF_LEN);

可以讀的話,  mlen 統計 read的字節數。

__sock_read_data(ctrl->sockfd, buf_rd, &timeout, &mlen);     

函數作用是:

讀取ctrl->sockfd 上的內容,放入buf_rd[] 中,&mlen 是要讀取的最小長度,返回讀取的字節數。


以上是用戶層的API 發送和接收接口函數。

********* ******************************************





















發佈了29 篇原創文章 · 獲贊 28 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章