當前位置:
首頁 > 知識 > Socket實現-Socket I/O

Socket實現-Socket I/O

Socket層的核心是兩個函數:sosendsoreceive.這兩個函數負責處理所有Socket層和協議層之間的I/O操作

select系統調用的作用是監控文件描述符的狀態。一般用於Socket I/O操作,也可以用於其它文件I/O操作。


Socket緩存

我們知道,每一個Socket都有一個發送緩存和一個接收緩存。緩存的類型為sockbuf

struct sockbuf{
struct mbuf * sb_mb;//mbuf鏈,用於存儲用戶數據
u_long sb_mbcnt;//mbuf鏈的長度
u_long sb_mbmax;//mbuf鏈的最大長度。
u_long sb_cc;//緩存中的實際位元組數

u_long sb_hiwat;
long sb_lowat;
struct selinfo sb_sel;
short sb_flags;
short sb_timeo;//read/write超時時間
}

  • sb_mb

    指向存儲數據的mbuf鏈的指針

    ,指向的是mbuf鏈的第一個mbuf
  • sb_cc的值等於存儲在mbuf鏈中的數據位元組數,即

    mbuf鏈中存儲的有效數據的位元組數

  • sb_hiwatsb_lowat用來調整Socket的流控演算法(不是TCP的流量控制)
  • sb_mbcnt為分配給緩存中的所有mbuf的存儲器數量,即mbuf鏈的長度
  • sb_mbmax為分配給該Socket mbuf緩存的存儲器數量的上限,即mbuf鏈的最大長度。
    默認的上限在socket系統調用中發送PRU_ATTACH請求時由協議設置。只要內核要求的每個socket緩存的大小不超過262 144個位元組的限制(sb_max,這是一個全局變數),進程就可以修改緩存的上限和下限。

Internet協議的默認的Socket緩存限制

Socket實現-Socket I/O

Socket實現-Socket I/O

因為每一個收到的UDP數據報的源地址和其攜帶的數據一起排隊,所以UDP協議的sb_hiwat的默認值設置為能容納40個1K位元組長的數據報和相應的sockaddr_in結構(每個16個位元組)。

  • sb_sel是用來實現select系統調用的selinfo結構
  • sb_timeo用來限制一個進程在讀寫調用中被阻塞的時間。

下表列出了sb_flags的所有可能的值


sb-flags 說明
SB_LOCK 一個進程已經鎖定了socket緩存
SB_WANT 一個進程正在等待給socket緩存加鎖
SB_WAIT 一個進程正在等待接收數據或發送數據所需的緩存
SB_SEL 一個或多個進程正在選擇這個緩存
SB_ASYNC 為這個緩存產生非同步I/O信號
SB_NOINTR 信號不取消加鎖請求
SB_NOTIFY (SB_WAIT|SB_SEL|SB_ASYNC) 一個進程正在等待緩存的變化,如果緩存發送任何變化,用wakeup通知該進程


write、writev、sendto和sendmsg系統調用

我們將writewritevsendtosendmsg四個系統調用統稱為"寫系統調用",它們的作用是往網路連接上發送數據。相對於最一般的調用sendmsg而言,前三個系統調用是比較簡單的介面。

所有的寫系統調用都要直接或間接地調用sosendsosend的功能是將進程來的數據複製到內核,並將數據傳遞給與socket相關的協議。


函數 描述符類型 緩存數量 是否指明目的地址 標誌? 控制信息?
write 任何類型 1
writev 任何類型 [1..UIO_MAXIOV]
send socket 1 .
sendto socket 1 . .
sendmsg socket [1..UIO_MAXIOV] . . .

writewritev系統調用適用於任何描述符,而其它的系統調用只適用於socket描述符。

writevsendmsg系統調用可以接收從多個(應用層)緩存中來的數據。從多個緩存中寫數據稱為"收集(gathering)",同它相對應的讀操作稱為"分散(scattering)"。執行收集操作時,內核按序接收類型為iovec的數據中指定的緩存中的數據。數組最多有UIO_MAXIOV個單元。

struct iovec
{
char * iov_base;//基線地址,指向長度為iov_len個位元組的緩存的開始
size_t iov_len;//長度
};

如果沒有這種介面(writev),一個進程將不得不將多個緩存複製到一個大的緩存中,或調用多個寫系統調用來發送多個緩存中的數據。對於數據報協議而言,調用一次write就是發送一個數據報,數據報的發送不能用多個寫動作來實現。

數據報協議要求每一個寫調用必須指定一個目的地址。因為writewritevsend調用介面不支持對目的地址的指定,因此這些調用只能在調用connect將目的地址同一個無連接的socket聯繫起來後才能被調用。調用sendtosendmsg時必須提供目的地址,或在調用它們之前調用connect來指定目的地址。

sendmsg系統調用

只有通過sendmsg系統調用才能訪問到與socket API的輸出有關的所有功能。sendmsgsendit函數準備sosend系統調用所需的數據結構,然後由sosend系統調用將報文發送給相應的協議

對於SOCK_DGRAM協議而言,報文就是數據報。對於SOCK_STREAM協議而言,報文是一串位元組流。

sendmsg有三個參數:socket描述符、指向msghdr結構的指針、幾個控制標誌。函數copyinmsghdr結構從用戶空間複製到內核空間

struct msghdr
{
caddr_t msg_name;//可選的目的地址
u_int msg_namelen;//msg_name的長度
struct iovec * msg_iov;//分散/收集數組
u_int msg_iovlen;//msg_iov數組長度
caddr_t msg_control;//控制信息
u_int msg_controllen;//控制信息長度
int msg_flags;//接收標誌
};

控制信息(msg_control欄位)的類型為cmsghdr結構:

struct cmsghdr
{
u_int cmsg_len;
int cmsg_level;
int cmsg_type;
};

struct sendmsg_args
{
int s;
caddr_t msg;
int flags;
};

sendmsg(struct proc * p,struct sendmsg_args * uap,int * retval)
{
struct msghdr msg;
struct iovec aiov[UIO_SMALLOV],*iov;
int error;

/**
*一個有8個元素(UIO_SMALLIOV)的iovec數組從棧中自動分配。
*如果分配的數組不夠大,sendmsg將調用MALLOC分配更大的數組。如果進程指定的數組單元大於1024(UIO_MAXIOV),則返回EMSGSIZE。
*copyin將iovec數組從用戶空間複製到棧中的數組或一個更大的動態分配的數組中。
*這種技術避免了調用malloc帶來的高代價,因為大多數情況下,數組的單元數小於等於8
*/

//將msg數據從用戶空間複製到內核空間
if(error = copyin(uap->msg,(caddr_t)&msg,sizeof(msg))){
return (error);
}

if((u_int)msg.msg_iovlen >= UIO_SMALLIOV){
if((u_int)msg.msg_iovlen >= UIO_MAXIOV){
return (EMSGSIZE);
}

MALLOC(iov,struct iovec *,sizeof(struct iovec)*(u_int)msg.msg_iovlen,M_IOV,M_WAITOK);
}else{
iov = aiov;
}

if(msg.msg_iovlen && (error = copyin((caddr_t)msg.msg_iov,(caddr_t)iov,(unsigned)(msg.msg_iovlen * sizeof(struct iovec))))){
goto done;
}
msg.msg_iov = iov;

//如果sendit返回,則表明數據已經發送給相應的協議或出現差錯
error = sendit(p,uap->s,&msg,uap->flags,retval);
done:
if(iov != aiov){
FREE(iov,M_IOV);
}

return (error);
}

sendit系統調用

sendit(struct proc * p,int s,struct msghdr * mp,int flags,int * retsize)
{
struct file * fp;
struct uio auio;
struct iovec * iov;
int i;
struct mbuf * to, * control;
int len,error;

if(error = getsock(p->p_fd,s,&fp)){
return error;
}

/**
*初始化uio結構,並將應用層的輸出緩存中的數據收集到內核緩存中
*/
auio.uio_iov = mp->msg_iov;
auio.uio_iovcnt = mp->msg_iovlen;
auio.uio_segflg = UIO_USERSPACE;
auio.uio_rw = UIO_WRITE;
auio.uio_procp = p;
auio.uio_offset = 0;
auio.uio_resid = 0;

iov = mp->msg_iov;

/**
*將傳送的數據的長度通過一個for循環來計算
*/
for(i = 0;i < mp->msg_iovlen;i++,iov++){
/**
*保證緩存的長度非負
*/
if(iov->iov_len < 0){ return (EINVAL); } /** *保證uio_resid不溢出,因為uio_resid是一個**有符號的**整數,且iov_len要求非負 */ if((auio.uio_resid += iov->iov_len) < 0){ return (EINVAL); } } /** *如果進程提供了地址和控制信息,則sockargs將地址和控制信息複製到內核緩存中 */ if(mp->msg_name){//如果進程提供了地址
//將地址複製到內核中
if(error = sockargs(&to,mp->msg_name,mp->msg_namelen,MT_SONAME)){
return (error);
}
}else{
top = 0;
}

if(mp->msg_control){//如果進程提供了控制信息
if(mp->msg_controllen < sizeof(struct cmsghdr)){ error = EINVAL; goto bad;; } //將控制信息複製到內核中 if(error = sockargs(&control,mp->msg_control,mp->msg_controllen,MT_CONTROL)){
goto bad;
}
}else{
control = 0;
}

/**
*發送數據和清除緩存
*/

len = auio.uio_resid;//為了防止sosend不接受所有數據而無法計算傳送的位元組數,將uio_resid的值保存在len中

//將socket、目的地址、uio結構(包含了要發送的數據)、控制信息和標誌全部傳給函數sosend
if(error = sosend((struct socket *)fp->f_data,to,&auio,(struct mbuf*)0,control,fkags)){
if(auio.uio_resid != len && (error == ERESTART || error == EINTR || error == EWOULDBLOCK)){
error = 0;
}

if(error == EPIPE){
psignal(p,SIGPIPE);
}
}

if(0 == error){
/**
*如果沒有差錯出現(或差錯被丟棄),則計算傳送的位元組數,並將其保存在*retsize中。
*如果sendit返回0,syscall返回*retsize給進程而不是返回差錯代碼
*/
*retsize = len - auio.uio_resid;
}

bad:
//釋放包含目的地址的緩存。
if(to){
m_freem(to);
}

return (error);
}

sosend系統調用

sosend是socket層中最複雜的函數之一。前面提到的所有五個寫系統調用最終都要調用sosendsosend的功能就是:根據socket指明的協議支持的語義和緩存的限制,將數據和控制信息傳遞給socket指明的協議的pr_usrreq函數.sosend從不將數據放在發送緩存(輸出緩存)中,存儲和移走數據應由協議來完成。

sosend對發送緩存的sb_hiwatsb_lowat值的解釋,取決於對應的協議是否實現可靠或不可靠的數據傳送功能。

可靠的協議緩存

對於提供可靠的數據傳輸協議,例如TCP,發送緩存保存了還沒有發送的數據和已經發送但還沒有被確認的數據sb_cc等於發送緩存的數據的位元組數,且0 <= sb_cc <= sb_hiwat

如果有帶外數據發送,則sb_cc有可能暫時超過sb_hiwat

so_send應該確保在通過pr_usrreq函數將數據傳遞給協議層之前有足夠的發送緩存。

協議層會將數據放到發送緩存中。sosend通過下面兩種方式之一將數據傳送給協議層:

TCP應用程序對外出的TCP報文段的大小沒有控制。例如,在TCP Socket上發送一個長度為4096位元組的報文,假定發送緩存中有足夠的緩存,則Socket層將該報文分成兩部分,每一部分長度為2048個位元組,分別存放在一個帶外部簇的mbuf中。然後,在協議處理時,TCP將根據連接上的MSS將數據分段,通常情況下,MSS為2048個位元組。

當一個報文因為太大而沒有足夠的緩存時,協議允許報文被分成多段。但sosend仍然不將數據傳送給協議層知道發送緩存中的空閑空間大小大於sb_lowat。對於TCP而言,sb_lowat的默認值是2048,從而阻止Socket層在發送緩存快滿時用小塊數據干擾TCP.

不可靠的協議緩存

對於提供不可靠的數據傳輸的協議而言,例如UDP,發送緩存不需要保存任何數據,也不等待任何確認

每一個報文一旦被排隊等待發送到相應的網路設備,Socket層立即將它傳送到協議。在這種情況下,sb_cc總是等於0,sb_hiwat指定每一次寫的最大長度,間接指明數據報的最大長度

UDP協議的sb_hiwat的默認值為9216(9 x 1024)。如果進程沒有通過SO_SNDBUFSocket選項改變sb_hiwat的值,則發送長度大於9216個位元組的數據報將導致差錯。不僅如此,其它的協議限制也可能不允許一個進程發送大的數據報。


對於NFS寫而言,9216已足夠大,NFS寫的數據加上協議首部的長度一般默認為8192個位元組。

實現

sosend(struct socket * so,struct mbuf * addr,struct uio * uio,struct mbuf * top,struct mbuf * control,int flags)
{
/**
*初始化
*/

struct proc * p = curproc;
struct mbuf **mp;
struct mbuf * m;
long space,len,resid;
int clen = 0,error,s,dontroute,mlen;

/**
* 如果sosendallatonce等於true(任何設置了PR_ATOMIC的協議)或數據已經通過top中的mbuf鏈傳送給sosend,則將設置atomic。這個標誌[控制數據是作為一個mbuf鏈還是作為獨立的mbuf傳送給協議].
*/

int automic = sosendallatonce(so) || top;

/**
*resid等於iovec緩存中的數據位元組數或top中的mbuf鏈中的數據位元組數
*/
if(uio){
resid = uio->uio_resid;
}else{
resid = top->m_pkthdr.len;
}

if(resid < 0){ return (EINVAL); } /** *如果僅僅要求對這個報文不通過路由表進行路由選擇,則設置dontroute */ dontroute = (flags & MSG_DONTROUTE) && (so->so_options & SO_DONTROUTE) == 0 && (so->so_proto->pr_flags & PR_ATOMIC);
p->p_stats->p_ru.ru_msgsnd++;

if(control){
/**
* clen等於在可選的控制緩存中的位元組數
*/
clen = control->m_len;
}

#define snderr(errno) {error = errno;splx(s);goto release;}

/**
*sosend的主循環從restart開始,在循環的開始調用sblock給發送緩存加鎖。通過加鎖確保多個進程按序互斥訪問socket緩存
*/
restart:
if(error = sblock(&so->so_snd,SBLOCKWAIT(flags)))
{
goto out;
}

/**
*主循環直到[將所有數據都傳送給協議層](即resid = 0)時才會退出
*/
do{

/**
* 等待發送緩存有空閑空間
*/

s = splnet;

/**
* 如果Socket輸出被禁止,即TCP連接的寫通道已經關閉,則返回EPIPE
*/
if(so->so_state & SS_CANTSENDMORE){
snderr(EPIPE);
}

/**
* 如果Socket正處於差錯狀態(例如,前一個數據報可能已經產生了一個ICMP不可達的差錯),則返回so->so_error
*/
if(so->so_error){
snderr(so->so_error);
}

/**
* 如果協議請求連接且連接還沒有建立或連接請求還沒有啟動,則返回EMOTCONN.
* sosend允許只有控制信息但沒有數據的寫操作
*/
if((so->so_state & SS_ISCONNECTED) == 0){
if(so->so_proto->pr_flags & PR_CONNREQUIRED){
if((so->so_state & SS_ISCONFIRMING) == 0 && !(resid == 0 && clen != 0)){
snderr(ENOTCONN);
}
}else if(addr == 0){
snderr(EDESTADDRREQ);
}
}

/**
* 獲取發送緩存中剩餘的空閑空間位元組數
* 這是一個基於緩存高水位標記的管理上的限制,但也是sb_mbmax對它的限制,其目的是[防止太多的小豹紋消耗太多的mbuf緩存]。sosend通過放寬緩存限制到1024個位元組來給予帶外數據更高的優先順序
*/
space = sbspace(&so->so_snd);

if(flags & MSG_OOB){
space += 1024;
}

/**
* 如果atomic被置位,並且報文大於高水位標記(high-water mark),則返回EMSGSIZE;
* 報文因為太大而不被協議接受,即使緩存是空的。如果控制信息的長度大於高水位標記,同樣返回EMSGSIZE
*/
if(atomic && resid > so->so_snd.sb_hiwat || clen > so->so_snd.sb_hiwat){
snderr(EMSGSIZE);
};

/**
* 如果發送緩存中的空間不夠,數據來源於進程(而不是來源於內核中的top),並且下列條件之一成立,則sosend必須等待更多的空間
* 報文必須一次傳送給協議(atomic為真);或
* 報文可以分段傳送(即atomic為假),但閑置空間大小低於低水位標記;或
* 報文可以分段傳送(即atomic為假),閑置空間大小大於低水位標記,但閑置空間存放不下控制信息
* 當數據通過top傳送給sosend(即uio為NULL)時,數據已經在mbuf緩存中。因此sosend忽略緩存高、低水位標記限制,因為不需要附加的緩存來保存數據。
*/
if(space < resid + clen && uio && (atomic || space < so->so_snd.sb_lowat || space < clen)){ /** * 如果sosend必須等待緩存且socket是非阻塞的,則返回EWOULDBLOCK */ if(so->so_state & SS_NBIO){
snderr(EWOULDBLOCK);
}

sbunlock(&so->so_snd);

/**
* sosend調用sbwait等待,直到發送緩存的狀態發生變化.
* 當sbwait返回後,sosend重新進行協議處理,並且跳轉到restart獲取緩存鎖,檢查差錯和緩存空間。如果條件滿足,則繼續執行。
* 默認情況下,sbwait阻塞直到可以發送數據。通過`SO_SNDTIMEO` Socket選項改變緩存中的sb_timeo,進程可以設置等待時間的上限
*/

error = sbwait(&so->so_snd);

splx(s);
if(error){
goto out;
}

goto restart;
}
splx(s);
mp = ⊤
space -= clen;//在sosend從進程複製任何數據之前,可用緩存的數量需減去控制信息的大小

do{

/**
*一旦有了足夠的空間並且sosend也獲得了發送緩存上的鎖,則準備傳送給協議層的數據。
*/

if(NULL == uio){
/**
*如果uio為空,則說明mbuf鏈中並沒有數據或者調用出錯
*/
resid = 0;
if(flags & MSG_EOR){
top->m_flags |= M_EOR;
}
}else{
do{
/**
*如果uio不為空,則sosend必須從進程間複製數據。當PR_ATOMIC被設置時(例如,UDP),循環繼續,直到所有數據都被複制到一個mbuf鏈中。
*當sosend從進程得到所有數據後,通過循環中的break跳出循環。跳出循環後,sosend將整個數據鏈一次傳送給相應協議
*/

if(0 == top){
MGETHDR(m,M_WAIT,MT_DATA);
mlen = MHLEN;
m->m_pkthdr.len = 0;
m->m_pkthdr.rcvif = (struct ifnet*)0;
}else{
MGET(m,M_WAIT,MT_DATA);
mlen = MLEN;
}

if(resid >= MINCLSIZE && space >= MCLBYTES){
MCLGET(m,M_WAIT);

if((m->m_flags & M_EXT) == 0){
goto nopages;
}

mlen = MCLBYTES;

if(atomic && top == 0){
len = min(MCLBYTES - max_hdr,resid);
m->m_data += max_hdr;
}else{
len = min(MCLBYTES,resid);
}

space -= MCLBYTES;
}else{
nopages:
len = min(min(mlen,resid),space);
space -= len;

/**
* For datagram protocols,level room for protocol headers in first mbuf
*/

if(atomic && top == 0 && len < mlen){ MH_ALIGN(m,len); } } /** * [使用uiomove從進程複製len個位元組的數據到mbuf中]。傳送完後更新mbuf的長度 */ error = uiomove(mtod(m,caddr_t),(int)len,uio); resid = uio->uio_resid;
m->m_len = len;
*mp = m;

top->m_pkthdr.len += len;

if(error){
goto release;
}

mp = &m->next;
if(resid <= 0){ if(flags & MSG_EOR){ top->m_flags != M_EOR;
}
break;
}
}while(space > 0 && automic);

/**
* pass mbuf chain to protocol
*/

if(dontroute){
so->so_options != SO_DONTROUTE;
}

s= splnet;

/**
* 向協議層發送PRU_SEND請求或PRU_SENDOOB,協議層收到請求後會發送數據
*/
error = (*so->so_proto->pr_usrreq)(so,(flags & MSG_OOB) ? PRU_SENDOOB : PRU_SEND,top,addr,control);
splx(s);

if(dontroute){
so->so_options &= ~SO_DONTROUTE;
}

clen = 0;
control = 0;
top = 0;
mp = ⊤
if(error){
goto release;
}
}
}while(resid && space > 0);

}while(resid);

release:

/**
* 當所有數據都傳送給協議後,給Socket緩存解鎖,釋放多餘的mbuf緩存
*/
sbunlock(&so->so_snd);
out:
if(top){
m_freem(top);
}

if(control){
m_freem(control);
}

return (error);
}

read、readv、recvfrom和recvmsg系統調用

我們將readreadvrecvfromrecvmsg系統調用稱為"讀系統調用",從網路連接上接收數據。同recvmsg相比,前三個系統調用比較簡單。recvmsg因為比較通用而複雜得多。


函數 描述符類型 緩存數量 返回發送者的地址嗎? 標誌? 返回控制信息?
read 任何類型 1
readv 任何類型 [1..UIO_MAXIOV]
recv socket 1 .
recvfrom socket 1 . .
recvmsg socket [1..UIO_MAXIOV] . . .

只有readreadv系統調用適用於各類描述符,其它的系統調用只適用於socket描述符。

同寫調用一樣,通過iovec結構數組來指定多個緩存。

  • 對數據報協議,recvfromrecvmsg返回每一個收到的數據報的源地址
  • 對於面向連接的協議,getpeername返回連接對方的地址

recvmsg系統調用

recvmsg函數是最通用的讀系統調用。如果一個進程使用任何一個其它的讀系統調用,且地址、控制信息和接收標誌的值還未定,則系統可能在沒有任何通知的情況下丟棄它們。

struct recvmsg_args
{
int s;//socket描述符
struct msghdr * msg;
int flags;//控制標誌
};

recvmsg(struct proc * p,struct recvmsg_args * uap,int * retval)
{
struct msghdr msg;

struct iovec aiov[UIO_SMALLIOV], *uiov, *iov;

int error;

/**
* 同sendmsg一樣,recvmsg將msghdr結構複製到內核。
* 如果自動分配的數組aiov太小,則分配一個更大的iovec數組,並且將數組單元從進程複製到由iov只想的內核數組。
* 並將第三個參數複製到msghdr結構中
*/

if(error = copyin((caddr_t)uap->msg,(caddr_t)&msg,sizeof(msg))){
return (error);
}

if((u_int)msg.msg_iovlen >= UIO_SMALLIOV){
if((u_int)msg.msg_iovlen >= UIO_MAXIOV){
return (EMSGSIZE);
}
MALLOC(iov,struct iovec * ,sizeof(struct iovec) * (u_int)msg.msg_iovlen,M_IOV,M_WAITOK);
}else{
iov = aiov;
}

msg.msg_flags = uap->flags;
uiov = msg.msg_iov;
msg.msg_iov = iov;

if(error = copyin((caddr_t)uiov,(caddr_t)iov,(unsigned)(msg.msg_iovlen * sizeof(struct iovec)))){
goto done;
}

/**
* recvit收完數據後,將更新過的緩存長度和標誌的msghdr結構再複製到進程。
*/

if((error = recvit(p,uap->s,&msg,(caddr_t)0,retval)) == 0){
msg.msg_iov = uiov;
error = copyout((caddr_t)&msg,(caddr_t)uap->msg,sizeof(msg));
}

done:
/**
* 如果分配了一個更大的iovec結構,則釋放它
*/
if(iov != aiov)
FREE(iov,M_IOV);
return (error);
}

recvit函數

recvit函數被recvrecvfromrecvmsg調用。

基於recv xxx調用提供的msghdr結構,recvit函數為soreceive的處理準備了一個uio結構。

recvit(struct proc * p,int s,struct msghdr * mp,caddr_t namelen,int * retsize)
{
struct file * fp;
struct uio auio;
struct iovec * iov;
int i;
int len,error;
struct mbuf * from = 0,*control = 0;

if(error = getsock(p->p_fd,s,&fp)){
return (error);
}

auio.uio_iov = mp->msg_iov;
auio.uio_iovcnt = mp->msg_iovlen;
auio.uio_segflg = UIO_USERSPACE;
auio.uio_rw = UIO_READ;
auio.uio_procp = p;
auio.uio_offset = 0;
auio.uio_resid = 0;
iov = mp->msg_iov;

for(i = 0;i < mp->msg_iovlen;i++;iov++){
if(iov->iov_len < 0){ return (EINVAL); } if((auio.uio_resid += iov->iov_len) < 0){ return (EINVAL); } } len = auio.uio_resid; /** * soreceive實現從socket緩存中接收數據的最複雜的功能。 * 傳送的位元組數保存在*retisze中,並返回給進程。 * 如果有些數據已經複製到進程後信號出現或阻塞出現(len不等於uio_resid),則忽略差錯,並返回已經傳送的位元組 */ if(error = soreceive((struct socket*)fp->f_data,&from,&auio,(struct mbuf**)0,mp->msg_control ? &control : (struct mbuf**)0,&mp->msg_flags)){
if(auio.uio_resid != len && (error == ERESTART || error == EINTR || error == EWOULDBLOCK)){
error = 0;
}
}

if(error){
goto out;
}

*retsize = len - auio.uio_resid;

/**
* 如果進程傳入了一個存放地址或控制信息或者兩者都有的緩存,則recvit將結果寫入該緩存,並且根據soreceive返回的結果調整它們的長度。
* 如果緩存太小,則地址信息可能被截掉。如果進程在發送讀請求之前保存緩存的長度,將該長度同內核返回的namelenp變數(或sockaddr結構的長度域)相比較就可能發現這個錯誤。通過設置msg_flags中的MSG_CTRUNC標誌來報告這種差錯。
*/

if(mp->msg_name){
len = mp->msg_namelen;

if(len <= 0 || from == 0){ len = 0; }else{ if(len > from->m_len){
len = from->m_len;
}

if(error = copyout(mtod(from,caddr_t),(caddr_t)mp->msg_name,(unsigned)len)){
goto out;
}

mp->msg_namelen = len;
if(namelenp && (error = copyout((caddr_t)&len,namelenp,sizeof(int)))){
goto out;
}
}

if(mp->msg_control){
len = mp->msg_controllen;

if(len <= 0 || control == 0){ len = 0; }else{ if(len >= control->m_len){
len = control->m_len;
}else{
mp->msg_flags |= MSG_CTRUNC;
}

error = copyout((caddr_t)mtod(control,caddr_t),(caddr_t)mp->msg_control,(unsigned)len);
}

mp->msg_controllen = len;

}

out:
/**
* 從out開始,釋放存儲源地址和控制信息的mbuf緩存
*/
if(from){
m_freem(from);
}

if(control){
m_freem(control);
}

return (error);
}

soreceive函數

soreceive函數將數據從socket的接收緩存傳送到進程指定的緩存。某些協議還提供發送者的地址,地址可以同可能的附加控制信息一起返回。

在討論它的代碼之前,先來討論接收操作,帶外數據和socket接收緩存的組織的含義。

recv xxx系統調用,傳遞給內核的標誌值:


flags 描述
MSG_DONTWAIT 在調用期間不等待資源
MSG_OOB 接收帶外數據而不是正常的數據
MSG_PEEK 接收數據的副本而不取走數據
MSG_WAITALL 在返回之前等待數據寫緩存

recvmsg是唯一返回標誌欄位給進程的讀系統調用。在其它的系統調用中,控制返回給進程之前,這些信息被內核丟棄。

在msghdr中recvmsg能設置的標誌


msg_flags 描述
MSG_CTRUNC 控制信息的長度大於提供的緩存長度
MSG_EOR 收到的數據標誌一個邏輯記錄的結束
MSG_OOB 緩存中包含帶外數據
MSG_TRUNC 收到的報文的長度大於提供的緩存長度

其它的接收操作選項:

  • 進程能夠通過設置標誌MSG_PEEK來查看是否有數據到達,而數據仍然留在接收隊列中,被下一個不設置MSG_PEEK的讀調用讀出。
  • 標誌MSG_WAITALL指示讀調用只有在讀到指定數量的數據後才返回。即使soreceive中有一些數據可以返回給進程,但它仍然要等待收到剩餘的數據後才返回。但是如果出現如下情況,即使沒有讀完指定長度的數據也會返回:

    • 連接的讀通道被關閉
    • socket的接收緩存小於所讀數據的大小
    • 在進程等待生於的數據時差錯出現
    • 帶外數據到達
    • 在讀緩存被寫滿之前,一個邏輯記錄的結尾出現

接收緩存的組織:報文邊界

對於支持報文邊界的協議,例如UDP,每一個報文存放在一個mbuf鏈中。

接收緩存中的多個報文通過m_nextpk指針鏈接成一個mbuf隊列。協議處理層添加數據到接收隊列,Socket層從接受隊列中移走數據。接收緩存的高水位標記(so_hiwat)限制了存儲在緩存中的數據量。

如果PR_ATOMIC沒有被置位,協議層儘可能多地在緩存中存放數據,丟棄輸入數據中的不合要求的部分。對於TCP,這就意味著到達的任何數據如果在接收窗口之外都將被丟棄。

如果PR_ATOMIC被置位,緩存必須能容納整個報文,否則協議層將丟棄整個報文。對於UDP而言,如果接收緩存已滿,則進入的數據報都將被丟棄,緩存滿的原因可能是進程讀數據報的速度不夠快。

包含三個數據報的UDP接收緩存

Socket實現-Socket I/O

對於PR_ATOMIC協議,當收到數據時,sb_lowat被忽略。當沒有設置PR_ATOMIC時,sb_lowat的值等於讀系統調用返回的最小位元組數。

接收緩存的組織:沒有報文邊界

當協議不需要維護報文邊界(即SOCK_STREAM協議,如TCP)時,通過sbappend將進入的數據駕到緩存中的最後一個mbuf鏈的尾部。如果進入的數據長度大於緩存的長度,則數據將被截掉,sb_lowat為一個讀系統調用返回的位元組樹設置了一個下限。

Socket實現-Socket I/O

實現


/**
* @param so: 指向socket
* @param paddr: 只想存放接收地址信息的mbuf緩存
* @param mp0: 如果mp0指向一個mbuf鏈,則soreceive將接收緩存中的數據傳送到*mp0指向的mbuf緩存鏈.在這種情況下uio結構中只有用來記數的uio_resid欄位是有意義的。如果mp0為空,則soreceive[將數據傳送到uio結構指定的緩存]。
* @param control : 指向包含控制信息的mbuf緩存。
* @param flagsp : 存放標誌
*
*/

soreceive(struct socket * so,struct mbuf ** paddr,struct uio * uio,struct mbuf **mp0,struct mbuf **controlp,int * flagsp)
{
struct mbuf *m,**mp;
int flags,len,error,s,offset;
//將pr指向socket協議的交換結構
struct protosw * pr = so->so_proto;
struct mbuf * nextrecord;
int moff,type;

/**
* 將uio_resid(接收請求的大小)保存在orig_resid
* 如果將控制或地址信息從內核複製到進程,則將orig_resid清0.soreceive函數的最後處理要利用這一事實。
*/
int orig_resid = uio->uio_resid;

mp = mp0;

if(paddr){
*paddr = 0;
}

if(controlp){
*controlp = 0;
}

if(flags){
flags = *flagsp & ~MSG_EOR;
}else{
flags = 0;
}

/**
* MSG_OOB processing and implicit connection confirmation
* 處理帶外數據
*/

if(flags & MSG_OOB){
/**
* 因為OOB數據不存放在接收緩存中,所以soreceive為其分配一塊標準的mbuf
*/
m = m_get(M_WAIT,MT_DATA);

/**
*給協議發送PRU_RCVOOB請求,並通過循環將協議返回的數據複製到uio指定的緩存中。複製完成後,soreceive返回0或差錯代碼
*/

error = (*pr->pr_usrreq)(so,PRU_RCVOOB,m,(struct mbuf*)(flags & MSG_PEEK),(struct mbuf*)0);//給協議發送PRU_RCVOOB請求,將帶外數據複製到m中

if(error){
goto bad;
}

do{

//將m中的數據(即協議返回的帶外數據)複製到uio指定的緩存中
error = uiomove(mtod(m,caddr_t),(int)min(uio->uio_resid,m->m_len),uio);

}while(uio->uio_resid && error == 0 && m);

bad:
if(m){
m_freem(m);
}

return (error);
}

/**
* 如果mp不為空,則表明將數據存放在其指向的mbuf鏈中。這裡我們首先將mbuf鏈中的第一個mbuf清空
*/
if(mp){
*mp = (struct mbuf*)0;
}

/**
* 如果socket處於SO_ISCONFIRMING狀態,PRU_RCVD請求告知協議進程想要接收數據
*/
if(so->so_state & SS_ISCONFORMING && uio->uio_resid){
(*pr->pr_usrreq)(so,PRU_RCVD,(struct mbuf*)0,(struct mbuf*)0,(struct mbuf*)0);
}

restart:

/**
* 在訪問接收緩存之前,調用sblock給緩存加鎖。如果flags中沒有設置MSG_DONTWAIT標誌,則soreceive必須等待加鎖成功
*/
if(error = sblock(&so->so_rcv,SBLOCKWAIT(flags))){
return (error);
}

/**
* 掛起協議處理,使得在檢查緩存過程中soreceive不被中斷
*/
s = splnet;

/**
* m是接收緩存中的第一個mbuf鏈上的第一個mbuf
*/
m = so->so_rcv.sb_mb;

/**
* if necessary ,wait for data to arrive
* 讀調用的請求能滿足嗎?如果不能則等待更多的數據
*/

if(m == 0 || ((flags & MSG_DONTWAIT) == 0 && so->so_rcv.sb_cc < uio->uio_resid) && (so->so_rcv.sb_cc < so->so_rcv.sb_lowat || ((flags & MSG_WAITALL) && uio->uio_resid <= so->so_rcv.sb_hiwat)) && m->m_nextpkt == 0 && (pr->pr_flags & PR_ATOMIC) == 0){
if(so->so_error){
if(m){
goto dontblock;
}

error = so->so_error;

if((flags & MSG_PEEK) == 0){
so->so_error = 0;
}

goto release;
}

if(so->so_state & SS_CANTRCVMORE){
if(m){
goto dontblock;
}else{
doto release;
}
}

for(;m;m->next){
if(m->m_type == MT_OOBDATA || (m->m_flags & M_EOR)){
m = so->so_rcv.sb_mb;
goto dontblock;
}
}

if((so->so_state & (SS_ISCONNECTED | SS_ISCONNECTING)) == 0 && (so->so_proto->pr_flags & PR_CONNREQUIRED)){
error = ENOTCONN;
goto release;
}

if(uio->uio_resid == 0){
goto release;
}

if((so->so_state & SS_NBIO) || (flags & MSG_DONTWAIT)){
error = EWOULDBLOCK;
goto release;
}

sbunlock(&so->so_rcv);

/**
* 等待更多的數據
* 同sosend中一樣,進程能夠利用SO_RCVTIMEO Socket選項為sbwait設置一個接收定時器。如果在數據到達之前定時器超時,則sbwait返回EWOULDBLOCK
*/
error = sbwait(&so->so_rcv);

splx(s);

if(error){
return (error);
}

goto restart;

}
dontblock:
if(uio->uio_procp){
uio->uio_procp->p_stats->p_ru.ru_msgrcv++;
}

/**
* nextrecord 指向接收緩存的下一條記錄。
* 在soreceive的後面,當第一個鏈被丟棄後,該指針被用來將剩餘的mbuf放入socket緩存
*/
nextrecord = m->m_nextpkt;

/**
* 處理地址和控制信息
*/

/**
* 如果協議提供地址信息,例如UDP,則將從mbuf鏈中刪除含地址的mbuf,並通過*paddr返回。如果,paddr為空,則地址信息被丟棄
*/
if(pr->pr_flags & PR_ADDR){
orig_resid = 0;

if(flags & MSG_PEEK){//接收數據的副本而不取走
if(paddr){
*paddr = m_copy(m,0,m->m_len);
}

m = m->m->m_next;
}else{
sbfree(&so->so_rcv,m);

if(paddr){
*paddr = m;
so->so_rcv.sb_mb = m->m_next;
m->m_next = 0;
m = so->so_rcv.sb_mb;
}else{
MFREE(m,so->so_rcv.sb_mb);
m = so->so_rcv.sb_mb;
}
}
}

while(m && m->m_type == MT_CONTROL && error == 0){

if(flags & MSG_PEEK){
if(controlp){
*controlp = m_copy(m,0,m->m_len);
}

m = m->m_next;
}else{
sbfree(&so->so_rcv,m);
if(controlp){
if(pr->pr_domain->dom_externalize && mtod(m,struct cmsghdr *)->cmsg_type == SCM_RIGHTS){
error = (*pr->pr_domain->dom_externalize)(m);
}

*controlp = m;
so->so_rcv.sb_mb = m->m_next;
m->m_next = 0;
m = m->so_rcv.sb_mb;
}
}

if(controlp){
orig_resid = 0;
controlp = &(*controlp)->m_next;
}

}

/**
* 處理完所有的控制mbuf後,m指向鏈中的下一個mbuf.
* 如果在地址或控制信息的後面,鏈中沒有其它的mbuf,則m為空
*
*/

if(m){
if((flags & MSG_PEEK) == 0){
m->m_nextpkt = nextrecord;
}

type = m->m_type;

if(MT_OOBDATA == type){
flags |= MSG_OOB;
}
}

/**
* 傳送數據
* 當MSG_PEEK被置位時,將為傳送的下一個位元組的偏移位置
* 當MSG_PEEK被置位時,OOB標記的偏移位置
* uio_resid表示還未傳送的位元組數
* len表示從本mbuf中將要傳送的位元組數;如果uio_resid比較小或靠OOB標記比較近,則len可能小於m_len
*/

moff = 0;
offset = 0;

while(m && uio->uio_resid > 0 && error == 0){
if(m->m_type == MT_OOBDATA){
if(type != MT_OOBDATA){
break;
}
}else if(type == MT_OOBDATA){
break;
}

so->so_state &= ~SS_RCVATMARK;

len = uio->uio_resid;

if(so->so_oobmark && len > so->so_oobmark - offset){
len = so->so_oobmark - offset;
}

if(len > m->m_len - moff){
len = m->m_len - moff;
}

/**
* 如果將數據傳送到uio緩存,則調用uiomove。如果數據是作為一個mbuf鏈返回的,則更新uio_resid的值,使其等於傳送的位元組數
*/
if(mp == 0){
splx(s);
//將數據從接收緩存傳送到uio緩存中
error = uiomove(mtod(m,caddr_t)+moff,(int)len,uio);
s = splnet;
}else{
uio->uio_resid -= len;
}

/**
* 調整指針和偏移準備傳送下一個mbuf
*/

if(len == m->m_len - moff){
if(m->m_flags & M_EOR){
flags != MSG_EOR;
}

if(flags & MSG_PEEK){
m = m->m_next;
moff = 0;
}else{
nextrecord = m->m_nextpkt;
sbfree(&so->so_rcv,m);

if(mp){
*mp = m;
mp = &m->m_next;
so->so_rcv.sb_mb = m = m->m_next;
*mp = (struct mbuf*)0;
}else{
MFREE(m,so->so_rcv.sb_mb);
m = so->so_rcv.sb_mb;
}

if(m){
m->m_nextpkt = nextrecord;
}
}else{
if(mp){
//將數據從接收緩存拷貝到mp指向的mbuf鏈
*mp = m_copy(m,0.len,M_WAIT);
}

m->m_data += len;
m_m_len -= len;
so->so_rcv.sb_cc -= len;
}
}

if(so->so_oobmark){
if((flags & MSG_PEEK) == 0){
so->so_oobmark -= len;
if(so->so_oobmark == 0){
so->so_state |= SS_RCVATMARK;
break;
}
}else{
offset += len;
if(offset == so->so_oobmark){
break;
}
}
}

if(flags & MSG_EOR){
BREAK;
}

/**
* 當設置了MSH_WAITALL標誌,並且讀請求還沒有完成,則循環將等待更多的數據到達
* If the MSG_WAITALL flag is set (for non-atomic socket)
* we must not quit util "uio->uio_resid == 0"or an error termination.
* If a signal/timeout occurs ,return with a short count but without error
* Keep sockbuf locked against other readers
*/

while(flags & MSG_WAITALL && m == 0 && uio->uio_resid > - && !sosendallatonce(so) && !nextrecord){
if(so->so_error || so->so_state & SS_CANTRCVMORE){
break;
}

//等待
error = sbwait(&so->so_rcv);

if(error){
sbunlock(&so->so_rcv);
splx(s);
return (0);
}

if(m = so->so_rcv.sb_mb){
nextrecord = m->m_nextpkt;
}
}
}

/**
*cleanup
*/

if(m && pr->pr_flags & PR_ATOMIC){
flags != MSG_TRUNC;
if((flags & MSG_PEEK) == 0){
(void)sbdroprecord(&so->so_rcv);
}
}

if((flags & MSG_PEEK) == 0){
if(m == 0){
so->so_rcv.sb_mb = nextrecord;
}

if(pr->pr_flags & PR_WANTRCVD && so->so_pcb){
(*pr->pr_usrreq)(so,PRU_RCVD,(struct mbuf*)0,(struct mbuf*)flags ,(struct mbuf*)0,(struct mbuf *)0);
}
}

if(orig_resid == uio->uio_resid && orig_resid && (flags & MSG_EOR) == 0 && (so->so_state & SS_CANTRCVMORE) == 0){
sbunlock(&so->so_rcv);
splx(s);
goto restart;
}

if(flagsp){
*flagsp |- flags;
}

release:
sbunlock(&so->so_rcv);
splx(s);

return (error);
}

select系統調用

下表列出了能夠監控的socket狀態。

Socket實現-Socket I/O

struct select_args{
u_int nd;
fd_set * in,*ou,*ex;
struct timeval * tv;
};

select(struct proc * p,struct select_args * uap,int * retval)
{
/**
* 在棧中分配兩個數組:ibits和obits,每個數組有三個單元,每個單元為一個描述符集合。
*/
fd_set = ibits[3],obits[3];

struct timeval atv;

int s,ncoll,error = 0,timo;

/**
* ni等於用來存放nd個比特(1個描述符佔1個比特)的比特掩碼所需的位元組數
*/
u_int ni;

bzero((caddr_t)ibits,sizeof(ibits));
bzero((caddr_t)obits,sizeof(obits));

/**
* select系統調用的第一個參數nd,必須不大於進程的描述符的最大數量
*/
if(uap->nd > FD_SETSIZE){
return (EINVAL);
}

/**
* 如果nd大於當前分配給進程的描述符個數,則將其減少到當前分配給進程的描述的個數
*/
if(uap->nd > p->p_fd->fd_nfiles){
uap->nd = p->p_fd->fd_nfiles;
}

/**
* ni等於用來存放nd個比特(1個描述符佔1個比特)的比特掩碼所需的位元組數
*/
ni = howmany(uap->nd,NFDBITS) * sizeof(fd_mask);

/**
* getbits宏copyin從進程那裡將文件描述符集合傳遞到ibits中的三個文件描述符集合.
* 如果文件描述符集合指針為空,則不需要複製
*/
#define getbits(name,x)
if(uap->name &&
(error = copyin((caddr_t)uap->name,(caddr_t)&ibits[x],ni)))
goto done;
}
getbits(in,0);
getbits(ou,1);
getbits(ex,2);
#undef getbits

/**
* 設置超時值
* 如果tv為空,則將timeo設置成0,select將無限期等待
*/
if(uap->tv){

/**
* 如果tv非空,則將超時值複製到內核
*/
error = copyin((caddr)t)uap->tv,(caddr_t)&atv,sizeof(atv));

if(error)[
goto done;
}

/**
* 調用itimerfix將超時值按硬體時鐘的解析度取整
*/
if(itimerfix(&atv)){
error = EINVAL;
goto done;
}

s = splclock;

/**
* 調用timevaladd將當前時間加到超時值中
*/
timevaladd(&atv,(struct timeval*)&time);
/**
* 調用hzto計算[從啟動到超時之間]的時鐘滴答數,並保存在timo中
*/
timo = hzto(&atv);

/**
* 如果計算出來的時鐘滴答數為0,則將timo設置為1,從而防止select無限期等待,[實現利用全0的timeval結構來實現非阻塞操作]
* Avoid inadvertently sleeping forever
*/

if(0 == timo){
timo = 1;
}

splx(s);

}else{
timo = 0;//如果tv為空,則將timeo設置成0,select將無限期等待
}

retry:

/**
* 掃描進程指示的文件描述符,當一個或多個文件描述符處於就緒狀態或者定時器超時或信號出現時返回
*/

ncoll = nselcoll;
p->p_flag |= P_SELECT;
error = selscan(p,ibits,obits,uap->nd,retval);

if(error || *retval){
goto done;
}

s = splhigh;

/* this should be timercmp(&time,&atv,>=) */

if(uap->tv && (time.tv_sec > atv.tv_sec || time.tv_sec == atv.tv_sec && time.tv_tv_usec >= atv.tv_usec)){
splx(s);
goto done;
}

if((p->p_flag & P_SELECT) == 0 || nselcoll != ncoll){
splx(s);
goto retry;
}

p->p_flag &= ~P_SELECT;

error = tsleep((caddr_t)&selwait,PSOCK|PCATCH,"select",timo);

splx(s);

if(0 == error){
goto retry;
}

done:
p->p_flag &= ~P_SELECT;
/* select is not restarted after signals ... */

if(ERESTART == error){
error = EINTR;
}

if(EWOULDBLOCK == error){
error = 0;
}

#define putbits(name,x)
if(uap->name &&
(error2 = copyout((caddr_t)&bits[x],(caddr_t)uap->name,nil))){
error = error2;
}

if(0 == error){
int error2;

putbits(in,0);
putbits(ou,1);
putbits(ex,2);
}
#undef putbits

return (error);

selscan函數

select系統調用的核心是selscan函數.

對於任意一個文件描述符集合中設置的每一個比特,selscan找出同它相關聯的文件描述符,並且將控制分散給與描述符相關聯的so_select函數。對於socket而言,就是soo_select函數

selscan(struct proc * p,fd_set * ibits,fd_set * obits,int nfd,int * retval)
{
struct filedesc * fdp = p->p_fd;
int j,fd;

fd_mask bits;

int n = 0;

static int flag[3] = {FREAD,FWRITE,0};

/**
* 第一個for循環依次查看三個文件描述符集合:讀、寫、例外
*/
for(int msk = 0;msk < 3;msk++){ /** * 第二個for循環在每個文件描述符集合(讀、寫、例外)內部循環,這個循環在集合中每個32bit(NFDBITS)循環依次 */ for(int i = 0;i < nfd;i+= NFDBITS){ bits = ibits[msk].fds_bits[i / NFDBITS]; while((j = ffs(bits)) && (fd = i + --j) < nfd){ bits &= ~(1 << j); struct file *fp = fdp->fd_ofiles[fd];

if(NULL == fp){
return (EBADF);
}

/**
* 當發現某個文件描述符的狀態為準備就緒時,設置輸出文件描述符集合中相對應的比特位。並將n(狀態就緒的描述符個數)加1
*/
if((*fp->f_ops->fo_select)(fp,flag[msk],p)){
FD_SET(fd,&obits[msk]);
n++;
}
}
}
}

&retval = n;
return (0);
}

soo_select函數


/**
* 出現如下情況是,可以認為socket可讀:
* - 接收緩存中的數據高於低水位
* - 接收緩存中不會再有數據進入了(對方發送了FIN)
* - 處於監聽狀態的socket中有新的連接
* - socket出現差錯
*/
#define soreadble(so)
(so->so_rcv.sb_cc >= so->so_rcv.sb_lowat || so->so_state & SS_CANTRCVMORE || so->so_qlen || so->so_error)

/**
* 當出現如下情況是,可以認為socket可寫:
* - 發送緩存中的可用空間大於低水位
*/
#define sowriteable(so)
(sbspace(&so->so_snd) >= so_snd.sb_lowat && (so->so_state & SS_ISCONNECTED) || ((so->so_proto->pr_flags & PR_CONNREQURED) == 0 || so->so_state & SS_CANTSENDMORE || so->error))

soo_select(struct file * fp,int which,struct proc * p)
{
struct socket * so = (struct socket *)fp->f_data;

int s = splnet;

switch(which){
case FREAD:{
if(soreadable(so)){
splx(s);
return (1);
}

selrecord(p,&so->so_rcv.sb_sel);
so->so_rcv.sb_flags |= SB_SEL;
break;
}

case FWRITE:{
if(sowriteable(so)){
splx(s);
return (1);
}

selrecord(p,&so->so_snd.sb_sel);
so->so_snd.sb)flag |= SB_SEL;
break;
}

case 0:{
if(so->so_oobmark || (so->so_state & SS_RCVATMARK)){
splx(s);
return (1);
}

selrecord(p,&so->so_rcv.sb_sel);
so->so_rcv.sb_flags |= SB_SEL;
break;
}
}

splx(s);

return (0);
}

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 科技優家 的精彩文章:

Backbone中父子view之間的值傳遞

TAG:科技優家 |

您可能感興趣

Socket.io+canvas實現實時繪畫
Perl Socket 編程
Django Channel處理Websocket鏈接
Swoole實現基於WebSocket的群聊私聊
Python的Socket知識1:入門
快速搭建CentOS+ASP.NET Core環境支持WebSocket
SpringBoot | 第十九章:web 應用開發之 WebSocket
App Engine彈性環境開始提供WebSockets協議
Python如何爬取實時變化的WebSocket數據
JMeter測試WebSocket的經驗總結
Python的Socket知識2:粘包處理
websocket與爬蟲
ajax,long poll,websocket連接的區別原理
Python 中最全面的 Socket 編程指南
springboot websocket後台主動推送消息
DDoS攻擊新玩法之WebSocket
手機配件市場觀察:PopSocket手機指環和Fundraising
python 基礎之 socket介面與web介面
AWS語音轉文本服務開始支持WebSockets
ubuntu環境下,apache更改默認埠80,以其他埠作為socket的方式