前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >socket接口api的深度探究

socket接口api的深度探究

原创
作者头像
mariolu
发布2019-08-04 23:37:45
2.7K0
发布2019-08-04 23:37:45
举报

一、socket接口使用

1.1 socket抽象层

Linux内核net/socket.c定义了一套socket的操作api。图1展示了socket层所处与TCP/IP协议栈之上和应用层之下。

图1 socket层的位置
图1 socket层的位置

1.2 一些需要预先知道的内核操作api

socket层大量使用了这些内核操作api,完成协议栈的调用入口。在深度探究socket层实现之前,先来了解下这些内核api。

  • fget_light()和fput_light():轻量级的文件查找入口。多任务对同一个文件进行操作,所以需要对文件做引用计数。fget_light在当前进程的struct files_struct中根据所谓的用户空间文件描述符fd来获取文件描述符。另外,根据当前fs_struct是否被多各进程共享来判断是否需要对文件描述符进行加锁,并将加锁结果存到一个int中返回, fput_light则根据该结果来判断是否需要对文件描述符解锁。fget_light()/fput_light是fget/fput的变形,不用考虑多进程共享同一个文件表而导致的竞争避免锁。fget/fput是指在文件表的引用计数+1/-1
  • sockfd_lookup_light根据fd找到相应的socket object(内核真正操作的对象)。
  • so_xxx: 内核相关socket操作接口。socket object操作协议栈的api入口。
  • in_pcballoc()。分配内核内存,内存名字叫Internet protocol control block。
  • in_pcbbind(), 绑定IN_PCB到指定的地址,如果不指定地址,那么会寻找一个可用的端口进行绑
  • in_pcblookup(): 指定的端口是否可用。
  • sbappend()追加数据到发送缓冲区。
  • so->so_proto->pr_usrreq 是socket object操作协议栈的函数
  • tcp_ursreq()是tcp 协议栈操作的入口函数,支持以下操作类型:PRU_ATTACH,PRU_BIND,PRU_LISTEN, PRU_ACCEPT, PRU_CONNECT, PRU_SHUTDOWN,PRU_ABORT, PRU_DETACH,PRU_SEND,PRU_SENDOOB,PRU_RCVD,PRU_RCVOOB
  • tcp_newtcpcb()。TCP control block被分配,socket描述符指向的正是这个TCP control block。
  • tcp_attach().
  • tcp_xxx: tcp_close(), tcp_disconect(),tcp_drop()
  • pr_xxx: 一套socket层和协议栈通信的接口,包括pr_usrreq(),pr_input(),pr_output(),pr_ctlinput(),pr_ctloutput()。

1.3 socket函数api

1.3.1 socket函数

  • 功能:在内核创建一个socket对象,并返回引用的操作fd。通过这个fd操作这个socket
  • 实现:通过in_pcballoc() 分配Internet control block的内存。接着调用tcp_newtcpcb()分配TCP control block的内存。初始化TCP定时器等。链接Internet control block的内存和TCP control block的内存
  • 注意:此时TCP为CLOSED状态
图2 创建socket函数的流程
图2 创建socket函数的流程

1.3.2 bind函数

  • 功能:指派一个ip地址给socket,并且分配一个fd返回
  • 实现:检查端口没有被重复绑定过。网卡有带ip。如果没有指定端口,则会先尝试从reserverd port(<1024)开始找,如果还没有可用会继续从ephemeral port (比如1024~5000)找。
  • 注意:客户端不需要显示bind,因为connect在内部已经自动实现了bind的逻辑。
图3 bind的流程
图3 bind的流程

1.3.3 connect函数

  • 功能:通过该函数建立和对端的有状态连接。connect成功调用意味着TCP三次握手成功
  • 实现:在用户态校验服务端地址(要connect的对端地址)是合法的,移入到内核空间,进入内核态进行connect操作。检查是否以及bind,如果没有bind,进行bind。接着soisconnecting会置tcp状态为SYN_SENT,调用tcp_output会发送SYN包。进入睡眠直到协议栈唤醒,如果成功置ESTABLISHED。
  • 注意:connect之前的显示调用bind不是必须的,当然有也可以。
图4 connect流程
图4 connect流程

1.3.4 listen函数

  • 功能:listen表示协议栈可以接收新的连接请求,同时正常处理连接中的请求数量是有限的(backlog=521)。
  • 实现:迁移TCP状态从CLOSED迁移到LISTEN
  • 注意:
图5 listen流程
图5 listen流程

1.3.5 accept流程

  • 功能:从listen socket创建新的通信socket
  • 实现:默认accept是阻塞,直到accept到连接,创建新的socket,唤醒客户端,返回这个socket,并且把外网ip和端口从内核协议栈拷贝给用户态应用层。
  • 注意:除了accpet,还有accept4(为什么叫4,因为有4个形参)比accept多了一个参数,可以传flag到系统调用。可以看到两者的区别仅仅在于accept4()有第四个参数flags,这个参数如果为0,就跟accept()一样;下面的两个参数可以用按位OR来获取不同的行为。SOCK_NONBLOCK:为新打开的文件描述符设置O_NONBLOCK标志位,如果是accept需要和fcntl()搭配使用,这样设置的效果和accept4是一样的,区别就是用accept的话需要多调用个fcntl函数。SOCK_CLOEXEC: 为新打开的文件描述符设置FD_CLOEXEC标志位,该标志位的作用是在进程使用fork()加上execve()的时候自动关闭打开的文件描述符。其实使用fcntl()设置FD_CLOEXEC标志位(也就是用open()的时候设置的O_CLOEXEC标志位)也能达到同样的效果,但跟fcntl()有什么不同呢?在多线程环境中,如果使用fcntl()会多出一步操作,这样就可能形成竞争。而使用accept4()就可以直接在打开的文件描述符上设置,可以消除竞争的问题。(原则上该竞争在那些新建文件描述符的调用中都存在,所以很多linux的系统调用都做了类似的处理)
图6 accept流程
图6 accept流程

1.3.6 send/write函数

  • 功能:发送数据
  • 实现:验证socket和connection状态,分配空间,拷贝消息到内核
  • 注意:发送函数有4个api:sendto,sendmsg,write,writev。send只能操作网络fd,而write更通用,可用处理任意通用fd。另外send允许您为实际操作指定某些选项。读/写是“通用”文件描述符函数,而recv / send稍微更专门化(例如,您可以设置一个标志忽略SIGPIPE,或者发送带外消息…)。

图7 send流程
图7 send流程

1.3.7 recv/read函数

  • 功能:接收数据
  • 实现:除了拷贝内核接收区的数据到应用层,还发送窗口更新信息给网络对端
  • 注意:recv和send一样也提供了4套接口:recvfrom,recvmsg,read,readv。recv和read的区别如同send和write的区别
图8 recv流程
图8 recv流程

1.3.8 close函数

  • 功能:关闭连接
  • 实现:如果是listening socket:遍历两个保持正在连接的pending conection的队列。阻止进一步的accept导致的tcp连接下一步状态迁移。比如说连接处于SYN_RCVD,那么会发送RST包。如果是其他socket,那么会置detach 连接在socket的control block。并且检查linger选项和linger time。如果linger time为0,那么会立即drop掉这个tcp连接。否则则有可能会发送FIN包关闭连接
  • 注意:
图9 close流程
图9 close流程

1.3.9 shundown函数

拒绝新的网络读数据,释放资源,丢弃读缓冲区,并且关闭读端连接,协议栈将写端缓冲区buff发送出去,并且关闭写端。写端关闭有可能会发送FIN包。

二、深入理解过程

2.1 tcp的三次握手

图10 tcp三次连接(上半部分)
图10 tcp三次连接(上半部分)
图11 tcp连接的下半部分
图11 tcp连接的下半部分

2.2 为什么是3次,而不是2次

此时已经客户端已经显示ESTABLISHED,是否可代表只需要两次握手。

为什么不是两次握手,而是三次握手,会有什么问题

还有一种失效连接的处理,客户端向服务端发出的SYN包延迟了,服务端没收到,客户端再重新发一个SYN包,然后服务端新建了这个连接。那么此时如果之前由于网络节点的延迟又达到了B,那么B会以为是A发起的新连接。于是B同意,并向A发起确认,但是此时A根本不会理会。B一直等A发送应用层数据,但是A并没有这个连接的发送任务。

三、异常情况

3.1 accept过程的异常

3.1.1 SYN没成功的重试次数

服务端会根据/proc/sys/net/ipv4/tcp_synack_retries(我的机器设置为5)设置的重试次数,重发SYN+ACK,

3.1.2 backlog已满的状态怎么办

  • 服务端发送SYN+ACK,客户端收到后会回复ACK,如果此时ACCEPT队列仍处于已满状态,退避2^n后再次重试,直到超过重试次数超过/proc/sys/net/ipv4/tcp_synack_retries设置的次数,服务端链接状态SYNC_RECV->CLOSED,客户端链接状态为ESTABLISHED。
  • 如果内核参数/proc/sys/net/ipv4/tcp_abort_on_overflow 是0,服务端会忽略最后一个ACK,此时服务端的TCP链接处于SYN_RECV半连接状态,客户端的TCP链接处于ESTABLISHED状态,客户端以为链接创建成功,服务端却处于半连接状态,状态不一致!其实这种不一致在TCP/IP协议里经常出现,处理方式一般都是重试和退避。
  • 如果内核参数/proc/sys/net/ipv4/tcp_abort_on_overflow 是1,则服务端会回复一个RST包,由SYN_RECV->CLOSED,客户端链接 ESTABLISHED->CLOSED。

3.2 send过程中

3.2.1 进程退出

先用kill -9方法,其实kill -9不能模拟服务器断电的情况。进程退出总共有8中情况:

有8种方式使进程终止,其中前5种为正常终止,它们是

  • 从 main 返回
  • 调用 exit
  • 调用 _exit 或 _Exit
  • 最后一个线程从其启动例程返回
  • 最后一个线程调用 pthread_exit

  异常终止有3种,它们是

  • 调用 abort
  • 接到一个信号并终止
  • 最后一个线程对取消请求做出响应
图12 进程正常退出
图12 进程正常退出

通过tcp抓包发现,有正常的四次挥手过程

3.2.2 拔电源、拔网线、交换机瘫痪的办法

那如果进程是由于服务器断电,导致的失连,服务器会怎么样。

操作步骤如下:设备B监听端口。设备A通过connect设备B的监听端口。设备A的进程睡眠,此时断掉设备B的网卡。拔网卡的命令是

代码语言:javascript
复制
ip link set eth0 down; 
ip link set eth0 up; 

设备A停止睡觉,send数据,返回值正是这个数据的长度,如果在继续send,会返回成功吗,会接受到对方RST包吗,为什么。这里看到进程发送完退出,会进入一段次数的退避重传(15次,共924秒,哪里配置的),然后没有FIN挥手过程。

send为什么成功的解释是,send只会探测到本地的错误,而不会探测到网络错误。

重试次数的配置:

  • /proc/sys/net/ipv4/tcp_retries1

?? 这个值影响由于某些错误引起的没有ACK的RTO重传和上报这些错误给网路层的时间。

??? RFC 1122推荐至少3次重传,这是个默认值。

  • /proc/sys/net/ipv4/tcp_retries2

这个值影响当RTO重传仍没收到ACK的TCP连接的超时时间。

?给定一个值N,假定一个TCP连接带有TCP_RTO_MIN的初始RTO的指数值会重传N次,在第(N+1)个RTO时杀死这个连接。默认值是15,生成一个假想的超时时间是924.6秒,和一个有效超时的下限。当超过这个假设的超时时间,TCP会在第一个RTO就会超时.RFC1122推荐至少超时时间有100秒,相当于这个值等于8.

图13 断电掉线,send成功
图13 断电掉线,send成功

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、socket接口使用
    • 1.1 socket抽象层
      • 1.2 一些需要预先知道的内核操作api
        • 1.3 socket函数api
          • 1.3.1 socket函数
          • 1.3.2 bind函数
          • 1.3.3 connect函数
          • 1.3.4 listen函数
          • 1.3.5 accept流程
          • 1.3.6 send/write函数
          • 1.3.7 recv/read函数
          • 1.3.8 close函数
          • 1.3.9 shundown函数
      • 二、深入理解过程
        • 2.1 tcp的三次握手
          • 2.2 为什么是3次,而不是2次
          • 三、异常情况
            • 3.1 accept过程的异常
              • 3.1.1 SYN没成功的重试次数
              • 3.1.2 backlog已满的状态怎么办
            • 3.2 send过程中
              • 3.2.1 进程退出
              • 3.2.2 拔电源、拔网线、交换机瘫痪的办法
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
          http://www.vxiaotou.com