美文网首页
套接字 socket

套接字 socket

作者: 守拙圆 | 来源:发表于2018-11-07 16:30 被阅读23次

    1 socket 简介

    socket 是处于传输层之上封装的网络接口,方便应用层、会话层等使用。socket 的本意是“插口或插槽”,顾名思义,可以将 socket 想象成一根网线,一头插在客户端,一头插在服务端,然后进行通信,故客户端与服务端在通信之前必须要创建一个 socket。

    由于此 socket 对于 tcp/ip 各种协议通用,故 socket 必须提供各种选项,例如使用 IPv4 和 IPv6 分别对应选项 AF_INETAF_INET6。另外,还要指定到底是 TCP 还是 UDP,由于 TCP 是基于数据流的,所以设置为 SOCK_STREAM,而 UDP 是基于数据报的,因而设置为 SOCK_DGRAM

    从上述 socket 描述中可以看出,socket 是一个端到端的通信,关于中间经过多少局域网,多少路由器,因而能够设置的参数,也只是端到端协议之上的网络层和传输层。

    2 基于 TCP 协议的 socket 介绍

    2.1 创建一个 TCP 服务的步骤

    1. 创建一个 socket。
    2. 调用 bind 函数绑定一个特定的服务地址与端口。其中端口是用来标定系统中对应的应用程序,IP地址是用来指定监听网卡,当然机器上可能存在多个网卡,可以选择监听所有网卡或某一个网卡。
    3. 调用 listen 指定网口进行监听。绑定 IP 后就可以对该服务进行监听了,服务端开启监听后,则客户端就可以发起连接,与服务端进行 三次握手创建连接。
    4. 调用 accept 函数,拿出一个已经完成的连接进行(已连接 socket)数据处理。accept 调用是从 监听 socket 的已连接队列中取一个 establish 状态的 socket 返回。
      Note:此处在内核中,OS 为每个监听 socket 维护了两个队列,一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于 establish 状态;一个是还没有完全建立连接的队列,此时三次握手还没完成,处于 syn_rcvd 的状态。
    5. 连接成功建立后,就进行读写操作。

    2.2 socket 的实现

    说 TCP 的 socket 是一个文件流是非常准确的,因为在 Linux 内核中,socket 就是一个文件描述符,可以承载对于文件的大部分操作。

    众所周知,在内核中,每一个进程都对应着一个 task_struct,在此结构体中存在一个文件描述符数组,来列出次进程的所有打开文件的文件描述符。文件描述符是一个整数,是这个数组的下标。这个数据的元素是一个指针,指向内核中所有打开的文件列表中对应的元素,既然是文件,就会有一个 inode,只是 socket 的 iNode 不像文件系统中 iNode,保存在硬盘中;这个 inode 是在 socket 调用时,在内存中创建的,指向内核中的 socket 结构。

    在 socket 结构中,存在两个队列,一个发送队列,一个接收队列。这两个队列里面保存的是一个缓存 sk_buff。这个缓存里面能够看到完整包的结构。

    2.3 基于 UDP 协议的 socket 程序函数调用过程

    UDP 是无连接的,所以不需要三次握手,也不需要调用 listen 和 connect。所以也不存在连接状态,sendto 和 recvfrom 的每一次调用都创建一对 socket。

    3 服务器如何处理多客户连接

    一个服务很显然是一对多的,通常一个服务都需要服务多个客户,但是服务的客户数肯定也是有限的,首先就让我们来看看理论上一个服务能够服务的客户数量是多少。如我们所知,任何一个连接都是由以下一个四元组构成:

    {本机 ip,本机端口,对端 ip,对端端口}
    

    对于特定服务来说,以上四元组中本机 ip 和本机端口是保持不变的,而对端 ip 和对端端口是可变的,根据 ip 和端口的数据类型可以知道最大可能对端数为2的32次方乘以2的16次方,为2的48次方。

    当然,这是服务端理论上的最大可能并发数,实际上由于内存容量、文件描述符数量的限制,实际远远小于理论数量。从而需要我们尽可能提高服务器所能服务的客户端的数量。

    3.1 多进程方式

    从上述 tcp socket 的介绍中可以看出,每个客户端都会和服务端创建一对 socket,对于服务端称为此 socket 为已连接 socket,与监听 socket 是完全分离的。基于父子进程能够继承文件描述符的特性,可以考虑创建一个子进程专门来处理已连接 socket 与对应客户端的通信。子进程处理完成后就自行退出该进程,并通知父进程。

    3.2 多线程方式

    线程为轻量级进程,线程共享所在进程的所有资源。可并行执行。显然可以新建线程来处理已连接 socket。

    3.3 多路复用,轮询方式

    以上两种方式,对于解决 10K 以下的用户都是够用的,但是对于 10K 以上的用户,其服务性能就急剧下降了。为了解决著名的 C10K 问题,提出多路复用技术。

    由于 socket 是文件描述符,故我们考虑在某一个特定的线程中来管理所有的已连接 socket,将其放在一个文件描述符集合 fd_set 中,这就是进度墙,然后调用 select 函数来监听文件描述符集合是否变化。一旦变化,就会依次查看每个文件描述符。那些发生变化的文件描述符会在 fd_set 对应的位置 1,表示对应的 socket 可读或可写,从而进行读写操作,然后调用 select,监控下一轮变化。

    3.4 多路复用,事件通知方式

    上面 select 函数还是存在问题的,因为每次 socket 所在文件描述符集合有 socket 发生变化时,都需要通过轮询的方式,也就是需要过一遍所有的已连接 socket 看其是否有数据读或写。而文件描述符集合的大小FD_SIZE 就成了能监听 socket 的瓶颈。同时在轮询的过程中,对于不存在变化的描述符同样需要遍历,就读写数据本身来说也是无用功。

    如果改为事件通知的方式,那就方便多了,如果某个文件描述符有数据读或写,自行通知线程进行读写操作。能完成这种事件通知的函数叫 epoll,它在内核中不是通过轮询的方式来实现的,而是通过注册 callback 函数的方式,当某个文件描述符发生变化的时候,就会主动通知。

    select 与 epoll方式实现比较:

    • select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
    • select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次。而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个用于epoll文件描述符的等待队列)。这也能节省不少的开销。

    如上图,假设进程打开了 socket m n x等多个文件描述符,现在需要通过 epoll 来监听是否这些 socket 都有事件发生。其中 epoll_create创建一个 epoll 对象,也是一个文件,也对应一个文件描述符,同样对应着打开文件列表中的一项。在这项里面有一个红黑树,在红黑树中,要保存这个 epoll 所监听的所有套接字。

    当 epoll_ctl 添加一个 socket 时,其实就是添加一个 socket 到该红黑树中,同时红黑树里面的节点指向一个结构,该结构在被监听的 socket 的事件列表中。当一个 socket 来了一个事件的时候,可以从这个列表中得到 epoll 对象,并用 call_back 通知它。

    注意:epoll 也是现在解决 C10K 问题的利器。

    相关文章

      网友评论

          本文标题:套接字 socket

          本文链接:https://www.haomeiwen.com/subject/wvwxzftx.html