TCP协议详解

在TCP协议中客户端和服务端之间的交互主要分为3个部分:客户端与服务端建立一个TCP连接;客户端和服务端在该连接上接收和发送数据;关闭TCP连接。
当服务端调用ServerSocket的bind方法绑定并监听某个端口时,内核就会建立该端口的SYN队列和ACCEPT队列。当客户端调用Socket的connect方法向服务端发起连接建立请求,一个TCP连接的建立需要经过三次握手:首先客户端向服务端发送一个包含客户端主机选择的初始序号ISN的SYN报文段;服务端接收后会将其放到SYN队列中,并返回一个包含服务端主机选择的ISN的SYN报文段作为ACK;客户端接收后会发送一个对其的ACK确认报文,服务端接收该ACK后会从SYN队列中取出对应的监听句柄并放到ACCEPT队列中。最后当服务端应用进程调用ServerSocket的accept方法时会直接从ACCEPT队列中取出已成功建立连接的Socket套接字。

PS:
    SYN队列和ACCEPT队列即对应已完成握手队列和未完成握手队列,它们都不是无限长度的,其长度受限于调用bind绑定监听某个端口时传入的backlog参数。当SYN队列满则会导致直接丢弃客户端的连接建立请求,当ACCEPT队列满虽然不会导致丢弃连接请求但会加剧SYN队列满,所以对于应用服务器来讲,如果ACCEPT队列中有建立好的TCP连接而没有及时取出来,这样一旦两个队列满了,就会使客户端不能再建立新的连接。所以通常会使用一个单独的线程执行ServerSocket的accept方法取出新连接,否则应当将Socket置为非阻塞模式。
    监听句柄本身并不对应于某个TCP连接而是还未完成三次握手的半连接。

当TCP连接建立成功后,就对应着操作系统分配的一个套接字Socket。应用进程是通过调用内核提供的send/write系统调用来发送数据,并把当前socket作为参数传入进去,在其层层封装调用中最终会执行内核的tcp_sendmsg方法,该方法会把应用层传递过来的待发送数据字节流按照三次握手时通告的MSS划分成若干个分片报文段,同时依次拷贝到该socket对应的发送缓冲区中,当发送缓冲区已满,该方法就会阻塞等待滑动窗口释放出一些缓存(如果是非阻塞Socket直接返回一个错误)。另外send方法调用和内核tcp_sendmsg方法真正执行发送报文不是同步的,即send方法成功返回也并不意味着把数据真正发送到网络中,最终还会调用tcp_push来执行IP层方法完成数据的发送。在tcp_push方法中首先检查未经确认的报文段数量是否大于拥塞窗口大小,如果不大于则继续检查待发送报文段序号是否大于发送窗口大小,不大于的话根据Nagle算法检查是否发送该报文段,最后检查待发送报文段的大小是否大于Min(拥塞窗口,发送窗口)。

PS:
    最大报文段大小MSS定义了TCP连接上期望对端主机发送的单个报文的最大长度,主要用于避免IP分片(因为IP分片效率很低,即使丢失一片也要发送端重传整个报文)。MSS是在三次握手时确定的,该值是预估的,如果TCP连接上的两台主机处于不同的网络中,即可能有不同的MTU值,如果当前发送的IP报文大于中间某个数据链路的MTU,则依然会发送IP分片——此时可以把IP报文首部的DF标志位置1,这样会回送一个
携带当前链路MTU的ICMP报文,这样连接双方就可以重新确定MSS值。
    IP分片:数据链路层一般都要限制每次发送数据帧的最大长度,比如以太网上传输的数据帧长度限制为46B~1500B,802.3对数据帧的长度最大为1492B。任何时候当IP层发送数据时,首先判断向本地哪个接口发送数据,并查询该接口对应的MTU,如果发送的IP数据报大小大于MTU则进行IP分片。IP分片可以发生在发送端,也可以在中间路由器上,但是只能在目的端IP层才可以重新组装。

对于TCP连接接收消息分为两个阶段:第一阶段,当网卡接收到传过来的数据时,通过软中断内核拿到并进行解析,如果是TCP报文段则交给内核的TCP模块。此时TCP模块会执行tcp_v4_recvmsg方法,该方法会判断新接收到的TCP报文段是否与期望的序号一致:如果一致则将该TCP报文段放到内核的receive队列中,receive队列是用来存放已排好序 、去除了TCP头部的报文段,它可以被应用进程直接读取;如果不一致的话则将该TCP报文段放到out_of_order队列中,并且此后每次向reveive队列中插入新的报文段后都会检查out_of_order队列中是否有报文段的序号正是下一个期望的序号,若有则直接将该报文段移到receive队列中。第二阶段,当应用进程读取数据时,首先在进程内分配一块内存,然后调用内核提供的read/recv系统调用,并把当前Socket传入,内核中最终会执行tcp_recvmsg方法,该方法首先要锁定Socket(因为其由多个进程所共享 ),在从receive队列中将接收到的报文段依次拷贝到用户进程内存中,在拷贝前会检查用户进程剩余内存是否大于正在拷贝的报文段,如果小于则直接返回已拷贝的总字节数,此外此处还可以通过设置MSG_PEEK标志位来判断从reveive队列中拷贝报文段后是否要删除报文段,主要用于多进程读取同一个Socket场景。如果reveive队列读取完成后,会检查SO_RCVLOWAT的值,若已拷贝的总字节数小于SO_RCVLOWAT值则应用进程休眠等待拷贝更多的数据,默认该值为1,表明只要读取到数据就可以返回。在检查内核的backlog队列是否为空,backlog队列用于存放当Socket被锁住后接收到的TCP报文段。对于应用进程休眠拷贝更多的数据,其最长的休眠时间由SO_RCVTIMEO指定,此时会释放Socket锁——这样新接收到的报文段不会进入backlog队列而是放到prequeue队列中,当应用进程唤醒并重新获取Socket锁后,会检查prequeue队列,如果是期望的序号则直接拷贝到应用进程内存中。此外因为在休眠前接收到的乱序报文段会被放到backlog队列中,所以在转入休眠前需要遍历backlog队列并将乱序报文依次拷贝到reveive队列中。此外如果打开tcp_low_latency选项,意味着服务器希望应用进程能够及时读取新接收到的报文段——这样进程休眠后接收到的报文段不会进入prequeue队列中,而是如果是正确序号则拷贝到用户进程内存中,并且每次拷贝完成后都要检查out_of_order队列,最后再检查已拷贝的总字节数是否大于SO_RCVLOWAT值,如果大于则返回,并将控制权交给用户态。

对于TCP连接的关闭提供了两个方法:close方法和 shutdown方法,通常使用close来实现全关闭,shutdown用来实现半关闭。对于调用shutdown方法关闭连接与Socket被多线程/多进程共享是没有关系的,而close在Socket处于多进程的情况下表现的行为不一致。具体来说,因为创建一个进程只能是从父进程创建子进程,子进程会复制父进程中的资源,并把父进程中所有打开的fd的引用计数都+1,当引用进程调用close关闭Socket时,在其层层封装调用中最执行fput方法,在该方法中维护了当前Socket的引用计数,每次调用fput仅仅只是将该引用计数-1,因此只有当共享该Socket的所有进程都调用了close方法才会执行真正的连接关闭。在使用close来关闭Socket时,首先判断该Socket对应的是一个正常的连接还是监听句柄,如果是监听句柄的话,则移除keepAlive定时器并发送一个RST报文来关闭连接,否则,检查该连接上是否有未读消息,若有则直接丢弃并发送RST报文异常关闭连接,否则检查SO_LINGER选项是否打开,如果打开则检查 l_linger参数是否大于0,如果大于则接着检查是否有未发送消息,如果有则将消息发送队列中的最后一个报文段的FIN标志位置1,没有的话则单独发送一个FIN报文段,如果 l_linger参数为0则不会触发FIN报文段的发送而是直接发送RST,最后关闭用于减少网络中微小分组的Nagle算法,向对端发送消息。如果未打开SO_LINGER选项的话,与上述一致,只是向对端发送消息后会直接返回,而打开了话则阻塞等待接收到对方的确认或超时。对于shutdown方法有一个参数,表示只关闭读/写,同时关闭读写:对于监听句柄,若只关闭写则不需要做任何事,只关闭读与close一致;对于正常连接,若shutdown参数包含读关闭——设置读关闭标志位并把接收到的消息全部丢弃,这样当应用进程调用read/write就读取不到任何数据,若包含写关闭——发送FIN报文段。

PS:
    SO_LINGER选项用来确保close关闭时发出的消息被对方接收到,它是通过调用close时阻塞应用进程直到确认对方接收到消息再返回,此外还提供了一个 l_linger参数用来控制close阻塞进程的最长时间。慎用SO_LINGER,因为它会在不经意间降低应用代码的执行速度。

在Socket中提供了SO_SNDBUF/SO_RCVBUF分别用来设置TCP连接的发送缓冲区和接收缓冲区,其只会影响到当前连接,并且实际上内核会使用其2倍大小作为缓冲区的上限。发送/接收缓冲区的大小是动态变化的,实际用到多少才会分配多少内存,在使用时如果缓冲区内存未达到上限的话,两者是不起任何作用的。当接收到TCP连接对端的报文段时会导致接收缓冲区内存大小增加,并且当新接收到的报文段会导致接收缓冲区大小超过其上限SO_RCVBUF,则会直接丢弃该报文段——这样就可以防止TCP连接不会消耗太多的服务器资源;当应用进程调用read/recv方法会导致接收缓冲区内存大小减少。对于发送缓冲区,当应用进程调用send/write发送消息时会导致发送缓冲区增大,如果发送缓冲区大小超过上限SO_SNDBUF则向引用进程返回一个一个错误;每当接收到TCP连接对端的ACK确认时,发送缓冲区大小会减小(用于实现超时重传)。接收缓冲区大小与滑动窗口有关,但不是一一对应的,当引用进程读取速度过慢时,接收滑动窗口缩小,此时连接对端就会降低发送速度(TCP连接双方都会通告对方自己的接收窗口的大小,而对方的接收窗口就是自己的发送窗口)。一般会以带宽时延积BDP来设置最大接收窗口,BDP代表网络的承载能力,而最大接收窗口则表示在网络承载能力内可以有多少不经确认报文段。因为OS内存是有限的,因此在并发连接数较少时,可以把接收/发送缓冲区设置得大一些,并发连接数较多时,设置得小一些以容纳更多的连接。另外设置了SO_SNDBU/SO_RCVBUF会使得Linux内核的连接自动内存分配功能失效。






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值