最近在看Socket的相关知识,看的知识比较碎,所以想着梳理一下从客户端发起请求到服务端处理请求的流程来把学过的知识串在一起,以我们比较常用的TCP协议举例。知识整理过程中,查阅了很多资料,这部分的内容其实非常复杂,涉及很多的linux内核知识,为了不陷入到细节中,我只写了主要流程,后面会再深入学习这部分知识,到时候再到这篇文章中进行补充。如果哪个地方写的不对,还请路过的大佬帮忙指正。
1、关于Socket
Socket的中文含义是插口或者插槽,客户端和服务端可以利用Socket进行数据的传输工作。按照linux"一切皆文件"的设计思想,Socket也被设计为了一个文件,也会有文件描述符,会有inode结构,这个inode指向了Socket在内核中的Socket结构struct Socket。这样linux就可以用操作文件的方式来操作Socket,比如调用read和write和读取和写入数据。
2、服务端启动
TCP服务端启动后,首先绑定ip和端口,这一步,是通过调用bind函数完成,为什么bind函数需要ip呢?因为一个服务器可以有多张网卡,有多张网卡就会有多个ip,所以需要通过指定ip来指定监听哪张网卡的消息。之后,服务端调用listen函数来监听端口,等待接收客户端的连接,TCP的连接是一个五元组,{服务端ip,服务端端口,客户端ip,客户端端口,协议类型}
3、客户端连接服务端
客户端生成一个Socket,使用一个临时端口,调用connect函数发起对服务端的连接请求,和服务端进行三次握手。服务端有两个队列,分别存放未握手完成的连接和已握手完成的连接。服务端的accept函数会从已握手完成的连接队列中获取一个连接进行处理,同时返回给客户端一个Socket,客户端和服务端用这个新生成的Socket进行数据传输工作。这里有一个知识点,就是连接的Socket和数据传输的Socket是2个不同的Socket。
4、服务端使用多路复用技术监听多个Socket(epoll举例)
服务端会调用epoll_create API创建一个eventpoll对象,后续的epoll_ctl的相关API都是操作这个eventpoll对象。eventpoll的结构为:
/*
* This structure is stored inside the "private_data" member of the file
* structure and represents the main data structure for the eventpoll
* interface.
*/
struct eventpoll {
......
......
/* Wait queue used by sys_epoll_wait() */
// 这个队列里存放的是执行 epoll_wait 从而等待的进程队列
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
// 这个队列里存放的是该 eventloop 作为 poll 对象的一个实例,加入到等待的队列
// 这是因为 eventpoll 本身也是一个 file, 所以也会有 poll 操作
wait_queue_head_t poll_wait;
/* List of ready file descriptors */
// 这里存放的是事件就绪的 fd 列表,链表的每个元素是下面的 epitem
struct list_head rdllist;
/* RB tree root used to store monitored fd structs */
// 这是用来快速查找 fd 的红黑树的根节点
struct rb_root_cached rbr;
......
......
// 这是 eventloop 对应的匿名文件,充分体现了 Linux 下一切皆文件的思想,创建eventpoll实例后,会将对应文件的fd返回给调用者,调用者可以通过fd操作eventpoll实例
struct file *file;
......
......
};
这里面比较关键的几个属性都标了注释,其中最关键的是rbr和rdllist,前者是存放Socket的红黑树,后者是就绪Socket的链表。内核会将已握手完成的Socket封装为一个epitem对象通过epoll_ctl函数加入eventepoll实例的红黑树中,epitem对象的结构如下:
/*
* Each file descriptor added to the eventpoll interface will
* have an entry of this type linked to the "rbr" RB tree.
* Avoid increasing the size of this struct, there can be many thousands
* of these on a server and we do not want this to take another cache line.
*/
struct epitem {
union {
/* RB tree node links this structure to the eventpoll RB tree */
struct rb_node rbn;
/* Used to free the struct epitem */
struct rcu_head rcu;
};
/* List header used to link this structure to the eventpoll ready list */
// 将这个 epitem 连接到 eventpoll 里面的 rdllist 的 list 指针,rdllist是就绪fd的链表
struct list_head rdllink;
/*
* Works together "struct eventpoll"->ovflist in keeping the
* single linked chain of items.
*/
struct epitem *next;
/* The file descriptor information this item refers to */
//这个结构,其实就是epitem封装的那个Socket对象,epoll_filefd内部有两个值,一个file,一个fd,前者是Socket在linux中对应的文件,后者是这个文件的描述符
//通过这个ffd结构,可以在红黑树中快速查找到这个epitem对象,进而找到Socket
struct epoll_filefd ffd;
/* Number of active wait queue attached to poll operations */
// 一个文件可以被多个 epoll 实例所监听,这里就记录了当前文件被监听的次数
int nwait;
/* List containing poll wait queues */
struct list_head pwqlist;
/* The "container" of this item */
// 当前 epollitem 所属的 eventpoll
struct eventpoll *ep;
};
当一个Socket和Epoll实例关联后,内核还会再构建一个eppoll_entry对象,eppoll_entry对象和epitem对象有关联关系,同时eppoll_entry和Socket接收/发送缓冲区的等待队列有关联关系,所以epoll_entry是Socket接收/发送缓冲区等待队列和红黑树epitem之间关系的关联对象,其关键结构如下:
/* Wait structure used by the poll hooks */
struct eppoll_entry {
/* List header used to link this structure to the "struct epitem" */
struct list_head llink;
/* The "base" pointer is set to the container "struct epitem" */
//指向红黑树中的epitem结构体
struct epitem *base;
//关联了Socket接收/发送缓冲等待队列,这个结构中有一个回调函数ep_poll_callback.当内核监听到Socket对应的缓冲区有变化时,会通过该回调函数通知到Socket
wait_queue_t wait;
/* The wait queue head that linked the "wait" wait queue item */
wait_queue_head_t *whead;
};
光看代码有点乱,我画了一张图,描述一下eventpoll结构、红黑树、epitem、eppoll_entry的关系,看着更清晰一点
epoll函数实现的关键就是wait_queue_t中的回调函数,正是这个回调函数架起了linux内核事件和Epoll之间的关联
设置回调的地方是在ep_ptable_queue_proc函数中,给新加入到Epoll实例的Socket的fd设置回调函数
/*
* This is the callback that is used to add our wait queue to the
* target file wakeup lists.
*/
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,poll_table *pt)
{
struct epitem *epi = ep_item_from_epqueue(pt);
struct eppoll_entry *pwq;
if (epi>nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
//设置回调函数
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
pwq->whead = whead;
pwq->base = epi;
if (epi->event.events & EPOLLEXCLUSIVE)
add_wait_queue_exclusive(whead, &pwq->wait);
else
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
} else {
/* We have to signal that an error occurred */
epi->nwait = -1;
}
}
ep_poll_callback的实现如下:
/*
* 如果文件类型支持epoll并且有事件发生,发生的事件通过
* 参数key来传送,参见tcp_prequeue()函数中对wake_up_interruptible_poll()
* 的调用。
* @wait: 调用ep_ptable_queue_proc()加入到文件中的唤醒队列时分配的
* eppoll_entry实例的wait成员的地址
* @mode:该参数在回调函数ep_poll_callback()中没有使用,其值为进程
* 睡眠时的状态
* @sync: 唤醒等待进程的标志
*/
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int pwake = 0;
unsigned long flags;
//通过wait_queue_t找到epitem对象。有了epitem对象,就可以找到红黑树中的Socket的文件描述符fd。所以回调函数是连接事件和eventpoll的重要桥梁
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
......
......
/* If this file is already in the ready list we exit soon */
/*
* 如果当前没有在向用户空间传递事件,用户
* 关心的事件已经发生,并且还没有加入到就绪
* 队列中,则将当前的epitem实例加入到就绪队列中。
*/
if (!ep_is_linked(&epi->rdllink))
list_add_tail(&epi->rdllink, &ep->rdllist);
/*
* Wake up ( if active ) both the eventpoll wait list and the ->poll()
* wait list.
*/
/*
* 唤醒调用epoll_wait()函数时睡眠的进程。
*/
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
/*
* 唤醒等待eventpoll文件状态就绪的进程
*/
if (waitqueue_active(&ep->poll_wait))
pwake++;
......
......
}
通过 init_waitqueue_func_entry(&pwq->wait, ep_poll_callback)初始化时,wait_queue_t中的func引用被显式设置为 ep_poll_callback
5、客户端给服务端发送数据
经过以上流程,客户端和服务端建立了连接,同时通过epoll机制来监听客户端发送数据。接下来,我们看下客户端发送数据的流程。客户端会使用三次握手成功后服务端返回的Socket和服务端进行数据传输工作,首先将数据封装成一个sk_buffer对象,然后加入到sk_buffer双向链表中,这个双向链表其实就是发送缓冲区,协议栈会将待发送的数据根据最大报文段大小分成多个数据段,然后将数据段发送出去。我这里写的比较简单,实际上这个过程很复杂,流程很多。想要更细致的了解,可以参考这篇文章。https://news.sohu.com/a/517940515_121124376
6、服务端处理数据
数据到达服务端后,linux内核感知到Socket发送/接收缓冲区等待队列中的数据变化,会唤醒阻塞的对应进程,然后执行wait_queue_t结构中的ep_poll_callback回调函数,找到eppoll_entry结构,进而找到红黑树的epitem对象,然后通过Socket文件对象和Socket文件描述符从红黑树中找到这个epitem,将其移动到epoll对象的就绪链表中
应用可以通过调用ctl_wait函数来获取到已就绪链表中的Socket文件描述符fd,然后对这个fd执行相关的操作,来获取到客户端发送的数据
参考资料
1、https://news.sohu.com/a/517940515_121124376
2、https://zhuanlan.zhihu.com/p/667412830
3、https://cloud.tencent.com/developer/article/1844168
4、https://www.cnblogs.com/zhongqifeng/p/15972623.html
5、极客时间网络专栏<趣谈网络协议>