深度剖析網絡協議棧中的 socket 函數,可以說是把前面介紹的串聯起來,將網絡協議棧各層關聯起來。
![徒手在Linux上實現一個網絡協議棧《源碼筆記》](http://p2.ttnews.xyz/loading.gif)
一、應用層——recvfrom 函數
對於這個函數有必要分析一下,先看看這個dup例子。服務器端中調用recvfrom函數,並未指定發送端(客戶端)的地址,換句話說這個函數是一個被動函數,有點類似於tcp協議中服務器listen 之後阻塞,等待客戶端connect。這裡則是服務器端recvfrom後,等待客戶端sendto,服務器端recvfrom接收到客戶端的數據包,也順便知道了發送端的地址,於是將其填充到recvfrom的最後兩個參數中,這樣服務器端就獲得了客戶端的地址,然後服務器端就可sendto數據給客戶端。(TCP同理)
想想也是,服務器怎麼可能實現知道全球這麼多客戶的地址呢?但服務器採用的是大家廣為人知的地址,比如你訪問谷歌搜索,你知道谷歌的網址,但谷歌事先肯定不知道它眾多訪問者的地址,所以是客戶端先主動訪問,發送數據之後,谷歌才知道該客戶端的地址,然後返回訪問信息。
#include
ssize_t recvfrom(int sockfd, const void *buff, size_t nbytes, int flags,
const struct sockaddr *from, socklen_t *addrlen);
//若成功返回讀到的字節數,否則返回-1
/*參數解析。這裡sockfd是接收,from那邊是發送
前面三個參數分別表示:套接字描述符,指向寫出緩衝區的指針和寫字節數。
與sendto不同是後面的參數,recvfrom的最後兩個參數類似於accept的最後兩個參數,返回時其中套接字地址結構的內容告訴我們是誰發送了數據報
*/
二、BSD Socket 層——sock_recvfrom 函數
/*
*Receive a frame from the socket and optionally record the address of the
*sender. We verify the buffers are writable and if needed move the
*sender address from kernel to user space.
*/
//從指定的遠端地址接收數據,主要用於UDP協議
//從addr指定的源端接收len大小的數據,然後緩存到buff緩衝區
//該函數還要返回遠端地址信息,存放在addr指定的地址結構中
static int sock_recvfrom(int fd, void * buff, int len, unsigned flags,
struct sockaddr *addr, int *addr_len)
{
struct socket *sock;
struct file *file;
char address[MAX_SOCK_ADDR];
int err;
int alen;
//參數有效性檢查
if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL))
return(-EBADF);
//通過文件描述符找到對應socket結構
if (!(sock = sockfd_lookup(fd, NULL)))
return(-ENOTSOCK);
if(len<0)
return -EINVAL;
if(len==0)
return 0;
//檢查緩衝區域是否可寫
err=verify_area(VERIFY_WRITE,buff,len);
if(err)
return err;
//調用下層函數inet_recvfrom
len=sock->ops->recvfrom(sock, buff, len, (file->f_flags & O_NONBLOCK),
flags, (struct sockaddr *)address, &alen);
if(len<0)
return len;
//對比可知,這裡是sock_recvfrom相比sock_sendto多出來的一部分
//它的作用便是將發送端(客戶端)的地址信息填充到addr中,就是獲取客戶端的地址信息
if(addr!=NULL && (err=move_addr_to_user(address,alen, addr, addr_len))<0)
return err;
return len;
}
三、INET Socket 層——inet_recvfrom 函數
/*
*The assorted BSD I/O operations
*/
//其功能與inet_sendto函數類似
static int inet_recvfrom(struct socket *sock, void *ubuf, int size, int noblock,
unsigned flags, struct sockaddr *sin, int *addr_len )
{
//獲取對應sock結構
struct sock *sk = (struct sock *) sock->data;
if (sk->prot->recvfrom == NULL)
return(-EOPNOTSUPP);
if(sk->err)
return inet_error(sk);
/* We may need to bind the socket. */
//檢查是否綁定了端口,沒有的話就自動綁定一個,就服務器端而言,肯定是有的
if(inet_autobind(sk)!=0)
return(-EAGAIN);
//調用下層udp_recvfrom函數
return(sk->prot->recvfrom(sk, (unsigned char *) ubuf, size, noblock, flags,
(struct sockaddr_in*)sin, addr_len));
四、傳輸層——udp_recvfrom 函數
/*
* This should be easy, if there is something there we\\
* return it, otherwise we block.
*/
//接收數據包,並返回對端地址(如果需要的話)
int udp_recvfrom(struct sock *sk, unsigned char *to, int len,
int noblock, unsigned flags, struct sockaddr_in *sin,
int *addr_len)
{
int copied = 0;
int truesize;
struct sk_buff *skb;
int er;
/*
*Check any passed addresses
*/
if (addr_len)
*addr_len=sizeof(*sin);
/*
*From here the generic datagram does a lot of the work. Come
*the finished NET3, it will do _ALL_ the work!
*/
//從接收隊列中獲取數據包
skb=skb_recv_datagram(sk,flags,noblock,&er);
if(skb==NULL)
return er;
//數據包數據部分(數據報)長度
truesize = skb->len;
//讀取長度檢查設置,udp是面向報文的,其接收到的每個數據包都是獨立的
//如果用戶要求讀取的小於可讀取的,那麼剩下的將被丟棄(本版本協議棧就是這麼幹的)
copied = min(len, truesize);
/*
*FIXME : should use udp header size info value
*/
//拷貝skb數據包中的數據負載到to緩衝區中
//這裡就是數據轉移的地方,將數據從數據包中轉移出來到緩存區
skb_copy_datagram(skb,sizeof(struct udphdr),to,copied);
sk->stamp=skb->stamp;//記錄時間
/* Copy the address. */
//如果要求返回遠端地址的話,這裡就是拷貝遠端地址信息了,含端口號和ip地址
if (sin)
{
sin->sin_family = AF_INET;//地址族
sin->sin_port = skb->h.uh->source;//端口號
sin->sin_addr.s_addr = skb->daddr;//ip地址,這裡是目的ip地址,有點困惑?
}
//釋放該數據包
skb_free_datagram(skb);
release_sock(sk);
return(truesize);//返回讀取(接收)到的數據的大小
}
上面在數據處理方面,調用了三個數據報文處理函數(net\\inet\\Datagram.c):skb_recv_datagram()、skb_copy_datagram()、skb_free_datagram()
skb_recv_datagram()
/*
*Get a datagram skbuff, understands the peeking, nonblocking wakeups and possible
*races. This replaces identical code in packet,raw and udp, as well as the yet to
*be released IPX support. It also finally fixes the long standing peek and read
*race for datagram sockets. If you alter this routine remember it must be
*re-entrant.
*/
//從接收隊列中獲取數據包
//需要注意的是,這些函數(非udp.c文件下)或沒有明確指明只與udp協議相關的函數則都是通用的
//在tcp和udp協議下都可被調用
struct sk_buff *skb_recv_datagram(struct sock *sk, unsigned flags, int noblock, int *err)
{
struct sk_buff *skb;
unsigned long intflags;
/* Socket is inuse - so the timer doesn't attack it */
save_flags(intflags);
restart:
sk->inuse = 1;//加鎖
//檢查套接字接收隊列中是否有數據包
//如果沒有,則睡眠等待,在睡眠等待之前必須檢查等待的必要性
while(skb_peek(&sk->receive_queue) == NULL)/* No data */
{
/* If we are shutdown then no more data is going to appear. We are done */
//檢查套接字是否已經被關閉接收通道,已經關閉通道了就沒必要盲目等待了
if (sk->shutdown & RCV_SHUTDOWN)
{
release_sock(sk);//對於udp無用,因為udp沒有采用back_log暫存隊列
*err=0;
return NULL;
}
//發生錯誤,則需要首先處理錯誤,返回
if(sk->err)
{
release_sock(sk);
*err=-sk->err;
sk->err=0;
return NULL;
}
/* Sequenced packets can come disconnected. If so we report the problem */
//狀態檢查,如果不符合則置錯誤標誌並返回
if(sk->type==SOCK_SEQPACKET && sk->state!=TCP_ESTABLISHED)
{
release_sock(sk);
*err=-ENOTCONN;
return NULL;
}
/* User doesn't want to wait */
//不阻塞,即調用者要求不進行睡眠等待,則直接返回
if (noblock)
{
release_sock(sk);
*err=-EAGAIN;
return NULL;
}
//系列篇前面介紹過該函數的一個主要功能是重新接收back_log緩存隊列中的數據包
//由於udp協議不會使用back_log隊列(用於tcp超時重發),所以該函數不會對套接字接收隊列造成影響
release_sock(sk);
/* Interrupts off so that no packet arrives before we begin sleeping.
Otherwise we might miss our wake up */
cli();
//經過前面的一系列檢查,這裡再次判斷是否隊列中沒有數據包
//因為很有可能在上面檢查過程中,有數據包到達
if (skb_peek(&sk->receive_queue) == NULL)
{
interruptible_sleep_on(sk->sleep);//進入睡眠等待
/* Signals may need a restart of the syscall */
if (current->signal & ~current->blocked)
{
restore_flags(intflags);;
*err=-ERESTARTSYS;
return(NULL);
}
if(sk->err != 0)/* Error while waiting for packet
eg an icmp sent earlier by the
peer has finally turned up now */
{
*err = -sk->err;
sk->err=0;
restore_flags(intflags);
return NULL;
}
}
sk->inuse = 1;//該套接字目前正在被本進程使用,不能被其餘場所使用
restore_flags(intflags);//恢復現場
}//end while
/* Again only user level code calls this function, so nothing interrupt level
will suddenly eat the receive_queue */
//如果接收隊列中存在數據包
//處理正常讀取的情況
if (!(flags & MSG_PEEK))
{
skb=skb_dequeue(&sk->receive_queue);//從隊列中獲取數據包
if(skb!=NULL)
skb->users++;//使用該數據包的模塊數+1
else
goto restart;/* Avoid race if someone beats us to the data */
}
//如果設置了MSG_PEEK標誌,允許查看已可讀取的數據
//處理預先讀取的情況
else
{
cli();
skb=skb_peek(&sk->receive_queue);
if(skb!=NULL)
skb->users++;
restore_flags(intflags);
if(skb==NULL)/* shouldn't happen but .. */
*err=-EAGAIN;
}
return skb;//返回該數據包
}
skb_copy_datagram()
//將內核緩衝區中數據複製到用戶緩衝區
//拷貝size大小skb數據包中的數據負載(由offset偏移定位)到to緩衝區中
void skb_copy_datagram(struct sk_buff *skb, int offset, char *to, int size)
{
/* We will know all about the fraglist options to allow >4K receives
but not this release */
//函數原型:memcpy_tofs(to,from,n) :功能一目瞭然
memcpy_tofs(to,skb->h.raw+offset,size);
}
skb_free_datagram()
//釋放一個數據包
//先判斷該數據包是否還有其餘模塊使用,再判斷該數據包是否還處於系統的某個隊列中,
//換句話說,這兩個判斷的目的就是看該數據包是否還有用,沒有用了就釋放
void skb_free_datagram(struct sk_buff *skb)
{
unsigned long flags;
save_flags(flags);//保存現場
cli();
skb->users--;//使用該數據包的模塊數-1
if(skb->users>0)//如果還有模塊使用該數據包,則直接返回
{
restore_flags(flags);
return;
}
/* See if it needs destroying */
//如果沒有其餘模塊使用該數據包,表示這是一個遊離的數據包
//下面檢查數據包是否仍處於系統某個隊列中,如果還處於某個隊列中則不可進行釋放
if(!skb->next && !skb->prev)/* Been dequeued by someone - ie it's read */
kfree_skb(skb,FREE_READ);//否則釋放該數據包所佔用的內存空間
restore_flags(flags);//恢復現場
}
對比數據包的發送與接收,發送過程就是把數據從緩衝區拷貝到數據包的數據部分,由於需要經過協議棧,所以對於數據部分區域還需要進行數據封裝,添加各層的協議頭。對於數據包的接收,由於本來已經處於傳輸層了,不需要進行數據包的解封裝,直接獲取套接字接收隊列中的數據包(如果有),然後再將數據包中的數據部分拷貝到緩衝區。
總結:今天就分享到這吧,需要的相關資料+學習+問題的小夥伴可以私信;資料;免費領取,
資料內容:包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,Golang,TCP/IP,協程,DPDK等等。。。
![徒手在Linux上實現一個網絡協議棧《源碼筆記》](http://p2.ttnews.xyz/loading.gif)
閱讀更多 編程資料庫 的文章