【Nebula系列】通過UNIX域套接字傳遞描述符的應用

来源:https://www.cnblogs.com/bwar/archive/2018/07/18/9332873.html
-Advertisement-
Play Games

  傳送文件描述符是高併發網路服務編程的一種常見實現方式。 "Nebula" 高性能通用網路框架即採用了UNIX域套接字傳遞文件描述符設計和實現。本文詳細說明一下傳送文件描述符的應用。 1. TCP伺服器程式設計範式   開發一個伺服器程式,有較多的的程式設計 ...


  傳送文件描述符是高併發網路服務編程的一種常見實現方式。Nebula 高性能通用網路框架即採用了UNIX域套接字傳遞文件描述符設計和實現。本文詳細說明一下傳送文件描述符的應用。

1. TCP伺服器程式設計範式

  開發一個伺服器程式,有較多的的程式設計範式可供選擇,不同範式有其自身的特點和實用範圍,明瞭不同範式的特性有助於我們伺服器程式的開發。常見的TCP伺服器程式設計範式有以下幾種:

  • 迭代伺服器
  • 併發伺服器,每個客戶請求fork一個子進程
  • 預先派生子進程,每個子進程無保護地調用accept
  • 預先派生子進程,使用文件上鎖保護accept
  • 預先派生子進程,使用線程互斥鎖上鎖保護accept
  • 預先派生子進程,父進程向子進程傳遞套接字描述符
  • 併發伺服器,每個客戶端請求創建一個線程
  • 預先創建線程伺服器,使用互斥鎖上鎖保護accept
  • 預先創建線程伺服器,由主線程調用accept

  當系統負載較輕時,傳統的併發伺服器程式模型就夠了。相對於傳統的每個客戶一次fork設計,預先創建一個進程池或線程池可以減少進程式控制制CPU時間,大約可減少10倍以上。

  某些實現允許多個子進程或線程阻塞在accept上,然而在另一些實現中,我們必須使用文件鎖、線程互斥鎖或其他類型的鎖來確保每次只有一個子進程或線程在accept。

  一般來講,所有子進程或線程都調用accept要比父進程或主線程調用accept後將描述字傳遞個子進程或線程來得快且簡單。

2. Nebula 為什麼採用傳遞文件描述符方式?

  Nebula框架是預先創建多進程,由Manager主進程accept後傳遞文件描述符到Worker子進程的服務模型(Nebula進程模型)。為什麼不採用像nginx那樣多線程由子線程使用互斥鎖上鎖保護accept的服務模型?而且這種服務模型的實現比傳遞文件描述符來得還簡單一些。

  Nebula框架採用無鎖設計,進程之前完全不共用數據,不存在需要互斥訪問的地方。沒錯,會存在數據多副本問題,但這些多副本往往只是些配置數據,占用不了太大記憶體,與加鎖解鎖帶來的代碼複雜度及鎖開銷相比這點記憶體代價更划算也更簡單。

  同一個Nebula服務的工作進程間不相互通信,採用進程和線程並無太大差異,之所以採用進程而不是線程的最重要考慮是Nebula是出於穩定性和容錯性考慮。Nebula是通用框架,完全業務無關,業務都是通過動態載入的方式或通過將Nebula鏈接進業務Server的方式來實現。Nebula框架無法預知業務代碼的質量,但可以保證在服務因業務代碼導致coredump或其他情況時,框架可以實時監控到並立刻拉起服務進程,最大程度保障服務可用性。

  決定Nebula採用傳遞文件描述符方式的最重要一點是:Nebula定位是高性能分散式服務集群解決方案的基礎通信框架,其設計更多要為構建分散式服務集群而考慮。集群不同服務節點之間通過TCP通信,而所有邏輯都是Worker進程負責,這意味著節點之間通信需要指定到Worker進程,而如果採用子進程競爭accept的方式無法保證指定的子進程獲得資源,那麼第一個通信數據包將會路由錯誤。採用傳遞文件描述符方式可以很完美地解決這個問題,而且傳遞文件描述符也非常高效。

3. 文件描述符傳遞函數和數據結構

  文件描述符傳遞通過調用sendmsg()函數發送,調用recvmsg()函數接收:

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

  這兩個函數與sendto和recvfrom函數相似,只不過可以傳輸更複雜的數據結構,不僅可以傳輸一般數據,還可以傳輸額外的數據,即文件描述符。下麵來看結構體msghdr及其相關結構體 :

struct msghdr {
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */
    struct iovec *msg_iov;        /* scatter/gather array */
    size_t        msg_iovlen;     /* # elements in msg_iov */
    void         *msg_control;    /* ancillary data, see below */
    size_t        msg_controllen; /* ancillary data buffer len */
    int           msg_flags;      /* flags on received message */
};


/* iovec結構體 */
struct iovec {
    void  *iov_base;    /* Starting address */
    size_t iov_len;     /* Number of bytes to transfer */
};


/* cmsghdr結構體 */
struct cmsghdr {
    socklen_t cmsg_len;    /* data byte count, including header */
    int       cmsg_level;  /* originating protocol */
    int       cmsg_type;   /* protocol-specific type */
    /* followed by unsigned char cmsg_data[]; */
};

  msghdr結構成員說明:

  • msg_name :即對等方的地址指針,不關心時設為NULL即可.
  • msg_namelen:地址長度,不關心時設置為0即可.
  • msg_iov:結構體iovec指針; iovec的成員iov_base 可以認為是傳輸正常數據時的buf,iov_len 是buf 的大小。
  • msg_iovlen:iovec類型的元素的個數,每一個緩衝區的起始地址和大小由iovec類型自包含。當有n個iovec結構體時,此值為n。
  • msg_control:是一個指向cmsghdr 結構體的指針,用來發送或接收控制信息。
  • msg_controllen :控制信息所占用的位元組數。註意,msg_controllen與前面的msg_iovlen不同,msg_iovlen是指的由成員msg_iov所指向的iovec型的數組的元素個數,而msg_controllen,則是所有控制信息所占用的總的位元組數。
  • msg_flags : 用來描述接受到的消息的性質,由調用recvmsg時傳入的flags參數設置。

  為了對齊,可能存在一些填充位元組,跟不同系統的實現有關控制信息的數據部分,是直接存儲在cmsghdr結構體的cmsg_type之後的。但中間可能有一些由於對齊產生的填充位元組,由於這些填充數據的存在,對於這些控制數據的訪問,必須使用Linux提供的一些專用巨集來完成:

#include <sys/socket.h>

/* 返回msgh所指向的msghdr類型的緩衝區中的第一個cmsghdr結構體的指針。*/
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);

/* 返回傳入的cmsghdr類型的指針的下一個cmsghdr結構體的指針。 */
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);

/* 根據傳入的length大小,返回一個包含了添加對齊作用的填充數據後的大小。 */
size_t CMSG_ALIGN(size_t length);

/* 傳入的參數length指的是一個控制信息元素(即一個cmsghdr結構體)後面數據部分的位元組數,返回的是這個控制信息的總的位元組數,即包含了頭部(即cmsghdr各成員)、數據部分和填充數據的總和。*/
size_t CMSG_SPACE(size_t length);

/* 根據傳入的cmsghdr指針參數,返回其後面數據部分的指針。*/
size_t CMSG_LEN(size_t length);

/* 傳入的參數是一個控制信息中的數據部分的大小,返回的是這個根據這個數據部分大小,需要配置的cmsghdr結構體中cmsg_len成員的值。這個大小將為對齊添加的填充數據也包含在內。*/
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);

4. 文件描述符傳遞要點

  sendmsg提供了可以傳遞控制信息的功能,要實現的傳遞描述符這一功能必須要用到這個控制信息。在msghdr變數的cmsghdr成員中,由控制頭cmsg_level和cmsg_type來設置傳遞文件描述符這一屬性,並將要傳遞的文件描述符作為數據部分,保存在cmsghdr變數的後面。這樣就可以實現傳遞文件描述符這一功能,這種情況是不需要使用msg_iov來傳遞數據的。

  具體地說,為msghdr的成員msg_control分配一個cmsghdr的空間,將該cmsghdr結構的cmsg_level設置為SOL_SOCKET,cmsg_type設置為SCM_RIGHTS,並將要傳遞的文件描述符作為數據部分,調用sendmsg即可。其中SCM表示socket-level control message,SCM_RIGHTS表示我們要傳遞訪問許可權。

  跟發送部分一樣,為控制信息配置好屬性,併在其後分配一個文件描述符的數據部分後,在成功調用recvmsg後,控制信息的數據部分就是在接收進程中的新的文件描述符了,接收進程可直接對該文件描述符進行操作。

  文件描述符傳遞並不是將文件描述符數字傳遞,而是文件描述符對應數據結構。在主進程accept的到的文件描述符7傳遞到子進程後文件描述符有可能是7,更有可能是7以外的其他數值,但無論是什麼數值並不重要,重要的是傳遞之後的連接跟傳遞之前的連接是同一個連接。

  通常在完成文件描述符傳遞後,接收進程接管文件描述符,發送進程則應調用close關閉已傳遞的文件描述符。發送進程關閉描述符並不造成關閉該文件或設備,因為該描述符對應的文件仍被視為由接收者進程打開(即使接收進程尚未接收到該描述符)。

  文件描述符傳遞可經由基於STREAMS的管道,也可經由UNIX域套接字。兩種方式在《UNIX網路編程》中均有描述,Nebula採用的UNIX域套接字傳遞文件描述符。

  創建用於傳遞文件描述符的UNIX域套接字用到socketpair函數:

#include <sys/types.h>  
#include <sys/socket.h>  
  
int socketpair(int d, int type, int protocol, int sv[2]);  

  傳入的參數sv為一個整型數組,有兩個元素。當調用成功後,這個數組的兩個元素即為2個文件描述符。一對連接起來的Unix匿名域套接字就建立起來了,它們就像一個全雙工的管道,每一端都既可讀也可寫。

5. Nebula框架中傳遞文件描述符的實現

  Nebula框架的文件描述符屬於SocketChannel的基本屬性,文件描述符傳遞方法是SocketChannel的靜態方法。

文件描述符傳遞方法聲明:

static int SendChannelFd(int iSocketFd, int iSendFd, int iCodecType, std::shared_ptr<NetLogger> pLogger);
static int RecvChannelFd(int iSocketFd, int& iRecvFd, int& iCodecType, std::shared_ptr<NetLogger> pLogger);

文件描述符發送方法實現:

/**
* @brief 發送文件描述符
* @param iSocketFd 由socketpair()創建的UNIX域套接字,用於傳遞文件描述符
* @param iSendFd 待發送的文件描述符
* @param iCodecType 通信通道編解碼類型
* @param pLogger 日誌類指針
* @return errno 錯誤碼
*/
int SocketChannel::SendChannelFd(int iSocketFd, int iSendFd, int iCodecType, std::shared_ptr<NetLogger> pLogger)
{
    ssize_t             n;
    struct iovec        iov[1];
    struct msghdr       msg;
    tagChannelCtx stCh;
    int iError = 0;

    stCh.iFd = iSendFd;
    stCh.iCodecType = iCodecType;

    union
    {
        struct cmsghdr  cm;
        char            space[CMSG_SPACE(sizeof(int))];
    } cmsg;

    if (stCh.iFd == -1)
    {
        msg.msg_control = NULL;
        msg.msg_controllen = 0;

    }
    else
    {
        msg.msg_control = (caddr_t) &cmsg;
        msg.msg_controllen = sizeof(cmsg);

        memset(&cmsg, 0, sizeof(cmsg));

        cmsg.cm.cmsg_len = CMSG_LEN(sizeof(int));
        cmsg.cm.cmsg_level = SOL_SOCKET;
        cmsg.cm.cmsg_type = SCM_RIGHTS;

        *(int *) CMSG_DATA(&cmsg.cm) = stCh.iFd;
    }

    msg.msg_flags = 0;

    iov[0].iov_base = (char*)&stCh;
    iov[0].iov_len = sizeof(tagChannelCtx);

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    n = sendmsg(iSocketFd, &msg, 0);

    if (n == -1)
    {
        pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "sendmsg() failed, errno %d", errno);
        iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
        return(iError);
    }

    return(ERR_OK);
}

文件描述符接收方法實現:

/**
* @brief 接收文件描述符
* @param iSocketFd 由socketpair()創建的UNIX域套接字,用於傳遞文件描述符
* @param iRecvFd 接收到的文件描述符
* @param iCodecType 接收到的通信通道編解碼類型
* @param pLogger 日誌類指針
* @return errno 錯誤碼
*/
int SocketChannel::RecvChannelFd(int iSocketFd, int& iRecvFd, int& iCodecType, std::shared_ptr<NetLogger> pLogger)
{
    ssize_t             n;
    struct iovec        iov[1];
    struct msghdr       msg;
    tagChannelCtx stCh;
    int iError = 0;

    union {
        struct cmsghdr  cm;
        char            space[CMSG_SPACE(sizeof(int))];
    } cmsg;

    iov[0].iov_base = (char*)&stCh;
    iov[0].iov_len = sizeof(tagChannelCtx);

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    msg.msg_control = (caddr_t) &cmsg;
    msg.msg_controllen = sizeof(cmsg);

    n = recvmsg(iSocketFd, &msg, 0);

    if (n == -1) {
        pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "recvmsg() failed, errno %d", errno);
        iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
        return(iError);
    }

    if (n == 0) {
        pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "recvmsg() return zero, errno %d", errno);
        iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
        return(ERR_CHANNEL_EOF);
    }

    if ((size_t) n < sizeof(tagChannelCtx))
    {
        pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "rrecvmsg() returned not enough data: %z, errno %d", n, errno);
        iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
        return(iError);
    }

    if (cmsg.cm.cmsg_len < (socklen_t) CMSG_LEN(sizeof(int)))
    {
        pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "recvmsg() returned too small ancillary data");
        iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
        return(iError);
    }

    if (cmsg.cm.cmsg_level != SOL_SOCKET || cmsg.cm.cmsg_type != SCM_RIGHTS)
    {
        pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__,
                        "recvmsg() returned invalid ancillary data level %d or type %d", cmsg.cm.cmsg_level, cmsg.cm.cmsg_type);
        iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
        return(iError);
    }

    stCh.iFd = *(int *) CMSG_DATA(&cmsg.cm);

    if (msg.msg_flags & (MSG_TRUNC|MSG_CTRUNC))
    {
        pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "recvmsg() truncated data");
        iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
        return(iError);
    }

    iRecvFd = stCh.iFd;
    iCodecType = stCh.iCodecType;

    return(ERR_OK);
}

Manager進程的void Manager::CreateWorker()方法創建用於傳遞文件描述符的UNIX域套接字:

int iControlFds[2];
int iDataFds[2];
if (socketpair(PF_UNIX, SOCK_STREAM, 0, iControlFds) < 0)
{
    LOG4_ERROR("error %d: %s", errno, strerror_r(errno, m_szErrBuff, 1024));
}
if (socketpair(PF_UNIX, SOCK_STREAM, 0, iDataFds) < 0)
{
    LOG4_ERROR("error %d: %s", errno, strerror_r(errno, m_szErrBuff, 1024));
}

Manager進程發送文件描述符:

int iCodec = m_stManagerInfo.eCodec;    // 將編解碼方式和文件描述符一同發送給Worker進程
int iErrno = SocketChannel::SendChannelFd(worker_pid_fd.second, iAcceptFd, iCodec, m_pLogger);
if (iErrno == 0)
{
    AddWorkerLoad(worker_pid_fd.first);
}
else
{
    LOG4_ERROR("error %d: %s", iErrno, strerror_r(iErrno, m_szErrBuff, 1024));
}
close(iAcceptFd);   // 發送完畢,關閉文件描述符

Worker進程接收文件描述符:

int iAcceptFd = -1;
int iCodec = 0;     // 這裡的編解碼方式在RecvChannelFd方法中獲得
int iErrno = SocketChannel::RecvChannelFd(m_stWorkerInfo.iManagerDataFd, iAcceptFd, iCodec, m_pLogger);

  至此,Nebula框架的文件描述符傳遞分享完畢,下麵再看看nginx中的文件描述符傳遞實現。

6. Nginx文件描述符傳遞代碼實現

  Nginx的文件描述符傳遞代碼在os/unix/ngx_channel.c文件中。

nginx中發送文件描述符代碼:

ngx_int_t
ngx_write_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size,
    ngx_log_t *log)
{
    ssize_t             n;
    ngx_err_t           err;
    struct iovec        iov[1];
    struct msghdr       msg;

#if (NGX_HAVE_MSGHDR_MSG_CONTROL)

    union {
        struct cmsghdr  cm;
        char            space[CMSG_SPACE(sizeof(int))];
    } cmsg;

    if (ch->fd == -1) {
        msg.msg_control = NULL;
        msg.msg_controllen = 0;

    } else {
        msg.msg_control = (caddr_t) &cmsg;
        msg.msg_controllen = sizeof(cmsg);

        ngx_memzero(&cmsg, sizeof(cmsg));

        cmsg.cm.cmsg_len = CMSG_LEN(sizeof(int));
        cmsg.cm.cmsg_level = SOL_SOCKET;
        cmsg.cm.cmsg_type = SCM_RIGHTS;

        /*
         * We have to use ngx_memcpy() instead of simple
         *   *(int *) CMSG_DATA(&cmsg.cm) = ch->fd;
         * because some gcc 4.4 with -O2/3/s optimization issues the warning:
         *   dereferencing type-punned pointer will break strict-aliasing rules
         *
         * Fortunately, gcc with -O1 compiles this ngx_memcpy()
         * in the same simple assignment as in the code above
         */

        ngx_memcpy(CMSG_DATA(&cmsg.cm), &ch->fd, sizeof(int));
    }

    msg.msg_flags = 0;

#else

    if (ch->fd == -1) {
        msg.msg_accrights = NULL;
        msg.msg_accrightslen = 0;

    } else {
        msg.msg_accrights = (caddr_t) &ch->fd;
        msg.msg_accrightslen = sizeof(int);
    }

#endif

    iov[0].iov_base = (char *) ch;
    iov[0].iov_len = size;

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    n = sendmsg(s, &msg, 0);

    if (n == -1) {
        err = ngx_errno;
        if (err == NGX_EAGAIN) {
            return NGX_AGAIN;
        }

        ngx_log_error(NGX_LOG_ALERT, log, err, "sendmsg() failed");
        return NGX_ERROR;
    }

    return NGX_OK;
}

nginx中接收文件描述符代碼:

ngx_int_t
ngx_read_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size, ngx_log_t *log)
{
    ssize_t             n;
    ngx_err_t           err;
    struct iovec        iov[1];
    struct msghdr       msg;

#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
    union {
        struct cmsghdr  cm;
        char            space[CMSG_SPACE(sizeof(int))];
    } cmsg;
#else
    int                 fd;
#endif

    iov[0].iov_base = (char *) ch;
    iov[0].iov_len = size;

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
    msg.msg_control = (caddr_t) &cmsg;
    msg.msg_controllen = sizeof(cmsg);
#else
    msg.msg_accrights = (caddr_t) &fd;
    msg.msg_accrightslen = sizeof(int);
#endif

    n = recvmsg(s, &msg, 0);

    if (n == -1) {
        err = ngx_errno;
        if (err == NGX_EAGAIN) {
            return NGX_AGAIN;
        }

        ngx_log_error(NGX_LOG_ALERT, log, err, "recvmsg() failed");
        return NGX_ERROR;
    }

    if (n == 0) {
        ngx_log_debug0(NGX_LOG_DEBUG_CORE, log, 0, "recvmsg() returned zero");
        return NGX_ERROR;
    }

    if ((size_t) n < sizeof(ngx_channel_t)) {
        ngx_log_error(NGX_LOG_ALERT, log, 0,
                      "recvmsg() returned not enough data: %z", n);
        return NGX_ERROR;
    }

#if (NGX_HAVE_MSGHDR_MSG_CONTROL)

    if (ch->command == NGX_CMD_OPEN_CHANNEL) {

        if (cmsg.cm.cmsg_len < (socklen_t) CMSG_LEN(sizeof(int))) {
            ngx_log_error(NGX_LOG_ALERT, log, 0,
                          "recvmsg() returned too small ancillary data");
            return NGX_ERROR;
        }

        if (cmsg.cm.cmsg_level != SOL_SOCKET || cmsg.cm.cmsg_type != SCM_RIGHTS)
        {
            ngx_log_error(NGX_LOG_ALERT, log, 0,
                          "recvmsg() returned invalid ancillary data "
                          "level %d or type %d",
                          cmsg.cm.cmsg_level, cmsg.cm.cmsg_type);
            return NGX_ERROR;
        }

        /* ch->fd = *(int *) CMSG_DATA(&cmsg.cm); */

        ngx_memcpy(&ch->fd, CMSG_DATA(&cmsg.cm), sizeof(int));
    }

    if (msg.msg_flags & (MSG_TRUNC|MSG_CTRUNC)) {
        ngx_log_error(NGX_LOG_ALERT, log, 0,
                      "recvmsg() truncated data");
    }

#else

    if (ch->command == NGX_CMD_OPEN_CHANNEL) {
        if (msg.msg_accrightslen != sizeof(int)) {
            ngx_log_error(NGX_LOG_ALERT, log, 0,
                          "recvmsg() returned no ancillary data");
            return NGX_ERROR;
        }

        ch->fd = fd;
    }

#endif

    return n;
}

  Nebula框架系列技術分享 之 《通過UNIX域套接字傳遞文件描述符》。 如果覺得這篇文章對你有用,如果覺得Nebula框架還可以,幫忙到Nebula的Github碼雲給個star,謝謝。Nebula不僅是一個框架,還提供了一系列基於這個框架的應用,目標是打造一個高性能分散式服務集群解決方案。

參考資料:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • goroutine只是由官方實現的超級"線程池"而已,每個實例4 5kb的棧記憶體占用和用於實現機制而大幅減少的創建和銷毀開銷。 併發不是並行(多CPU): 併發主要由切換時間片來實現"同時"運行,並行則是直接利用多核實現多線程的運行,但Go可以設置使用核數,以發揮多核電腦的能力。 通過go關鍵字實 ...
  • MIPS架構下的MCU,指令集包含R-Type、I-Type、J-Type三種,在數電課程設計時為了給MCU編寫指令集,需要將彙編語言轉化成機器代碼,這裡分享一下自己寫的Matlab 的 GUI。 主函數 C2M 函數rig_f 用來尋找名稱對應的寄存器地址 函數rig_n 用來將5位十進位數轉換成 ...
  • 原創 The Suspects Time Limit: 1000MS Memory Limit: 20000K Total Submissions: 48698 Accepted: 23286 Description Severe acute respiratory syndrome (SARS), ...
  • 1. 學習計劃 第二天:商品列表功能實現 1、服務中間件dubbo 2、工程改造為基於soa架構 3、商品列表查詢功能實現。 2. 將工程改造為SOA架構 2.1. 分析 由於宜立方商城是基於soa的架構,表現層和服務層是不同的工程。所以要實現商品列表查詢需要兩個系統之間進行通信。 如何實現遠程通信 ...
  • provider pom 連接註冊器register需要applicationContext需要web.xml載入配置文件監聽器 註冊 介面 實現類 註意這個@Service是dubbo的Service 右擊maven項目run as選maven build.. 輸入tomcat7:run 啟動這個 ...
  • w2 16、第二周-第02章節-Python3.5-模塊初識 sys模塊 sys.path sys.argv os模塊 os.system os.popen os.mkdir 17、第二周-第03章節-Python3.5-模塊初識2 18、第二周-第04章節-Python3.5-pyc是什麼 19、 ...
  • http://www.cnblogs.com/baixl/p/4170599.html ...
  • gets()函數 因為用gets函數輸入數組時,只知道數組開始處,不知道數組有多少個元素,輸入字元過長,會導致緩衝區溢出,多餘字元可能占用未使用的記憶體,也可能擦掉程式中的其他數據,後續用fgets函數代替。 fgets函數 一小段代碼舉例: (1) fgets函數一次讀入10 - 1個字元,如果少於 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...