??傳送文件描述符是高并發(fā)網(wǎng)絡(luò)服務(wù)編程的一種常見實現(xiàn)方式。Nebula 高性能通用網(wǎng)絡(luò)框架即采用了UNIX域套接字傳遞文件描述符設(shè)計和實現(xiàn)。本文詳細(xì)說明一下傳送文件描述符的應(yīng)用。
創(chuàng)新互聯(lián)自2013年創(chuàng)立以來,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項目成都網(wǎng)站設(shè)計、網(wǎng)站制作、外貿(mào)營銷網(wǎng)站建設(shè)網(wǎng)站策劃,項目實施與項目整合能力。我們以讓每一個夢想脫穎而出為使命,1280元掇刀做網(wǎng)站,已為上家服務(wù),為掇刀各地企業(yè)和個人服務(wù),聯(lián)系電話:028-86922220
??開發(fā)一個服務(wù)器程序,有較多的的程序設(shè)計范式可供選擇,不同范式有其自身的特點和實用范圍,明了不同范式的特性有助于我們服務(wù)器程序的開發(fā)。常見的TCP服務(wù)器程序設(shè)計范式有以下幾種:
??當(dāng)系統(tǒng)負(fù)載較輕時,傳統(tǒng)的并發(fā)服務(wù)器程序模型就夠了。相對于傳統(tǒng)的每個客戶一次fork設(shè)計,預(yù)先創(chuàng)建一個進(jìn)程池或線程池可以減少進(jìn)程控制CPU時間,大約可減少10倍以上。
??某些實現(xiàn)允許多個子進(jìn)程或線程阻塞在accept上,然而在另一些實現(xiàn)中,我們必須使用文件鎖、線程互斥鎖或其他類型的鎖來確保每次只有一個子進(jìn)程或線程在accept。
??一般來講,所有子進(jìn)程或線程都調(diào)用accept要比父進(jìn)程或主線程調(diào)用accept后將描述字傳遞個子進(jìn)程或線程來得快且簡單。
??Nebula框架是預(yù)先創(chuàng)建多進(jìn)程,由Manager主進(jìn)程accept后傳遞文件描述符到Worker子進(jìn)程的服務(wù)模型(Nebula進(jìn)程模型)。為什么不采用像nginx那樣多線程由子線程使用互斥鎖上鎖保護(hù)accept的服務(wù)模型?而且這種服務(wù)模型的實現(xiàn)比傳遞文件描述符來得還簡單一些。
??Nebula框架采用無鎖設(shè)計,進(jìn)程之前完全不共享數(shù)據(jù),不存在需要互斥訪問的地方。沒錯,會存在數(shù)據(jù)多副本問題,但這些多副本往往只是些配置數(shù)據(jù),占用不了太大內(nèi)存,與加鎖解鎖帶來的代碼復(fù)雜度及鎖開銷相比這點內(nèi)存代價更劃算也更簡單。
??同一個Nebula服務(wù)的工作進(jìn)程間不相互通信,采用進(jìn)程和線程并無太大差異,之所以采用進(jìn)程而不是線程的最重要考慮是Nebula是出于穩(wěn)定性和容錯性考慮。Nebula是通用框架,完全業(yè)務(wù)無關(guān),業(yè)務(wù)都是通過動態(tài)加載的方式或通過將Nebula鏈接進(jìn)業(yè)務(wù)Server的方式來實現(xiàn)。Nebula框架無法預(yù)知業(yè)務(wù)代碼的質(zhì)量,但可以保證在服務(wù)因業(yè)務(wù)代碼導(dǎo)致coredump或其他情況時,框架可以實時監(jiān)控到并立刻拉起服務(wù)進(jìn)程,最大程度保障服務(wù)可用性。
??決定Nebula采用傳遞文件描述符方式的最重要一點是:Nebula定位是高性能分布式服務(wù)集群解決方案的基礎(chǔ)通信框架,其設(shè)計更多要為構(gòu)建分布式服務(wù)集群而考慮。集群不同服務(wù)節(jié)點之間通過TCP通信,而所有邏輯都是Worker進(jìn)程負(fù)責(zé),這意味著節(jié)點之間通信需要指定到Worker進(jìn)程,而如果采用子進(jìn)程競爭accept的方式無法保證指定的子進(jìn)程獲得資源,那么第一個通信數(shù)據(jù)包將會路由錯誤。采用傳遞文件描述符方式可以很完美地解決這個問題,而且傳遞文件描述符也非常高效。
??文件描述符傳遞通過調(diào)用sendmsg()函數(shù)發(fā)送,調(diào)用recvmsg()函數(shù)接收:
#include
#include
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
??這兩個函數(shù)與sendto和recvfrom函數(shù)相似,只不過可以傳輸更復(fù)雜的數(shù)據(jù)結(jié)構(gòu),不僅可以傳輸一般數(shù)據(jù),還可以傳輸額外的數(shù)據(jù),即文件描述符。下面來看結(jié)構(gòu)體msghdr及其相關(guān)結(jié)構(gòu)體 :
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結(jié)構(gòu)體 */
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
/* cmsghdr結(jié)構(gòu)體 */
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結(jié)構(gòu)成員說明:
??為了對齊,可能存在一些填充字節(jié),跟不同系統(tǒng)的實現(xiàn)有關(guān)控制信息的數(shù)據(jù)部分,是直接存儲在cmsghdr結(jié)構(gòu)體的cmsg_type之后的。但中間可能有一些由于對齊產(chǎn)生的填充字節(jié),由于這些填充數(shù)據(jù)的存在,對于這些控制數(shù)據(jù)的訪問,必須使用Linux提供的一些專用宏來完成:
#include
/* 返回msgh所指向的msghdr類型的緩沖區(qū)中的第一個cmsghdr結(jié)構(gòu)體的指針。*/
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
/* 返回傳入的cmsghdr類型的指針的下一個cmsghdr結(jié)構(gòu)體的指針。 */
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);
/* 根據(jù)傳入的length大小,返回一個包含了添加對齊作用的填充數(shù)據(jù)后的大小。 */
size_t CMSG_ALIGN(size_t length);
/* 傳入的參數(shù)length指的是一個控制信息元素(即一個cmsghdr結(jié)構(gòu)體)后面數(shù)據(jù)部分的字節(jié)數(shù),返回的是這個控制信息的總的字節(jié)數(shù),即包含了頭部(即cmsghdr各成員)、數(shù)據(jù)部分和填充數(shù)據(jù)的總和。*/
size_t CMSG_SPACE(size_t length);
/* 根據(jù)傳入的cmsghdr指針參數(shù),返回其后面數(shù)據(jù)部分的指針。*/
size_t CMSG_LEN(size_t length);
/* 傳入的參數(shù)是一個控制信息中的數(shù)據(jù)部分的大小,返回的是這個根據(jù)這個數(shù)據(jù)部分大小,需要配置的cmsghdr結(jié)構(gòu)體中cmsg_len成員的值。這個大小將為對齊添加的填充數(shù)據(jù)也包含在內(nèi)。*/
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);
??sendmsg提供了可以傳遞控制信息的功能,要實現(xiàn)的傳遞描述符這一功能必須要用到這個控制信息。在msghdr變量的cmsghdr成員中,由控制頭cmsg_level和cmsg_type來設(shè)置傳遞文件描述符這一屬性,并將要傳遞的文件描述符作為數(shù)據(jù)部分,保存在cmsghdr變量的后面。這樣就可以實現(xiàn)傳遞文件描述符這一功能,這種情況是不需要使用msg_iov來傳遞數(shù)據(jù)的。
??具體地說,為msghdr的成員msg_control分配一個cmsghdr的空間,將該cmsghdr結(jié)構(gòu)的cmsg_level設(shè)置為SOL_SOCKET,cmsg_type設(shè)置為SCM_RIGHTS,并將要傳遞的文件描述符作為數(shù)據(jù)部分,調(diào)用sendmsg即可。其中SCM表示socket-level control message,SCM_RIGHTS表示我們要傳遞訪問權(quán)限。
??跟發(fā)送部分一樣,為控制信息配置好屬性,并在其后分配一個文件描述符的數(shù)據(jù)部分后,在成功調(diào)用recvmsg后,控制信息的數(shù)據(jù)部分就是在接收進(jìn)程中的新的文件描述符了,接收進(jìn)程可直接對該文件描述符進(jìn)行操作。
??文件描述符傳遞并不是將文件描述符數(shù)字傳遞,而是文件描述符對應(yīng)數(shù)據(jù)結(jié)構(gòu)。在主進(jìn)程accept的到的文件描述符7傳遞到子進(jìn)程后文件描述符有可能是7,更有可能是7以外的其他數(shù)值,但無論是什么數(shù)值并不重要,重要的是傳遞之后的連接跟傳遞之前的連接是同一個連接。
??通常在完成文件描述符傳遞后,接收進(jìn)程接管文件描述符,發(fā)送進(jìn)程則應(yīng)調(diào)用close關(guān)閉已傳遞的文件描述符。發(fā)送進(jìn)程關(guān)閉描述符并不造成關(guān)閉該文件或設(shè)備,因為該描述符對應(yīng)的文件仍被視為由接收者進(jìn)程打開(即使接收進(jìn)程尚未接收到該描述符)。
??文件描述符傳遞可經(jīng)由基于STREAMS的管道,也可經(jīng)由UNIX域套接字。兩種方式在《UNIX網(wǎng)絡(luò)編程》中均有描述,Nebula采用的UNIX域套接字傳遞文件描述符。
??創(chuàng)建用于傳遞文件描述符的UNIX域套接字用到socketpair函數(shù):
#include
#include
int socketpair(int d, int type, int protocol, int sv[2]);
??傳入的參數(shù)sv為一個整型數(shù)組,有兩個元素。當(dāng)調(diào)用成功后,這個數(shù)組的兩個元素即為2個文件描述符。一對連接起來的Unix匿名域套接字就建立起來了,它們就像一個全雙工的管道,每一端都既可讀也可寫。
??Nebula框架的文件描述符屬于SocketChannel的基本屬性,文件描述符傳遞方法是SocketChannel的靜態(tài)方法。
文件描述符傳遞方法聲明:
static int SendChannelFd(int iSocketFd, int iSendFd, int iCodecType, std::shared_ptr pLogger);
static int RecvChannelFd(int iSocketFd, int& iRecvFd, int& iCodecType, std::shared_ptr pLogger);
文件描述符發(fā)送方法實現(xiàn):
/**
* @brief 發(fā)送文件描述符
* @param iSocketFd 由socketpair()創(chuàng)建的UNIX域套接字,用于傳遞文件描述符
* @param iSendFd 待發(fā)送的文件描述符
* @param iCodecType 通信通道編解碼類型
* @param pLogger 日志類指針
* @return errno 錯誤碼
*/
int SocketChannel::SendChannelFd(int iSocketFd, int iSendFd, int iCodecType, std::shared_ptr 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);
}
文件描述符接收方法實現(xiàn):
/**
* @brief 接收文件描述符
* @param iSocketFd 由socketpair()創(chuàng)建的UNIX域套接字,用于傳遞文件描述符
* @param iRecvFd 接收到的文件描述符
* @param iCodecType 接收到的通信通道編解碼類型
* @param pLogger 日志類指針
* @return errno 錯誤碼
*/
int SocketChannel::RecvChannelFd(int iSocketFd, int& iRecvFd, int& iCodecType, std::shared_ptr 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進(jìn)程的void Manager::CreateWorker()方法創(chuàng)建用于傳遞文件描述符的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進(jìn)程發(fā)送文件描述符:
int iCodec = m_stManagerInfo.eCodec; // 將編解碼方式和文件描述符一同發(fā)送給Worker進(jìn)程
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); // 發(fā)送完畢,關(guān)閉文件描述符
Worker進(jìn)程接收文件描述符:
``` C++
int iAcceptFd = -1;
int iCodec = 0; // 這里的編解碼方式在RecvChannelFd方法中獲得
int iErrno = SocketChannel::RecvChannelFd(m_stWorkerInfo.iManagerDataFd, iAcceptFd, iCodec, m_pLogger);
??至此,Nebula框架的文件描述符傳遞分享完畢,下面再看看nginx中的文件描述符傳遞實現(xiàn)。
### 6. Nginx文件描述符傳遞代碼實現(xiàn)
??Nginx的文件描述符傳遞代碼在os/unix/ngx_channel.c文件中。
nginx中發(fā)送文件描述符代碼:
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框架系列技術(shù)分享__ 之 《通過UNIX域套接字傳遞文件描述符》。 如果覺得這篇文章對你有用,如果覺得Nebula框架還可以,幫忙到Nebula的[__Github__](https://github.com/Bwar/Nebula)或[__碼云__](https://gitee.com/Bwar/Nebula)給個star,謝謝。Nebula不僅是一個框架,還提供了一系列基于這個框架的應(yīng)用,目標(biāo)是打造一個高性能分布式服務(wù)集群解決方案。
參考資料:
* 《UNIX網(wǎng)絡(luò)編程》
* 《UNIX環(huán)境高級編程》
* [進(jìn)程間傳遞文件描述符](https://pureage.info/2015/03/19/passing-file-descriptors.html)
* [linux網(wǎng)絡(luò)編程之socket(十六):通過UNIX域套接字傳遞描述符和 sendmsg/recvmsg 函數(shù)](https://blog.csdn.net/jnu_simba/article/details/9079627)