TCP三次握手

TCP报文格式

  • 源端口(16位):发送端口
  • 目的端端口(16位):接收端口
  • 序列号(32位) :在建立连接时由计算机生成的随机数作为其初始值,通过SYN包传给接收端主机,每发送一次数据,就累加一次该数据字节数的大小。用来解决网络乱序问题
  • 确认应答号(32位) :下一次期望收到的数据的序列号,发送端收到这个确认应答以后可以认为这个序号以前的数据都已经被正常接收。用来解决丢包的问题
  • 数据偏移:是TCP报头中的一个重要字段,它也被称为首部长度。主要作用是指示TCP数据载荷从哪里开始,也就是确定TCP报头的总长度。
  • 保留:供未来使用,预留字段。
  • 校验和:由发送端填充,接收端对TCP报文段执行CRC算法以校验TCP报文段在传输过程中是否损坏,这个校验不仅包括TCP头部,也包括数据部分。这是TCP实现可靠传输的一个重要保障。
  • 窗口:TCP流量控制的一种手段。通过窗口告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方可以控制发送数据的速度,从而达到流量控制。窗口大小为16bit字段,因而窗口大小最大为65535。
  • 控制位

ACK:1,确认应答的字段变为有效。

RST:1,表示强制断开连接。

SYN:1,表示希望建立连接,并在序列号的字段进行序列号的初始值设定。

FIN:1,表示希望断开连接。

PSH:允许发送方应用指示接收应用立即消费数据,以减少延迟。

TCP三次握手过程

三次握手的过程如图,这个是最基础的,面试过程中会拓展很多。

  • 一开始客户端和服务器都处于close状态。先是服务器主动监听某个端口,处于listen状态。
  • 客户端会随机初始化序列号(client_isn),将此序列号置于TCP首部的序号字段中,同时把SYN标志位置为1,表示SYN报文。接着把第一个SYN报文发送给服务端,表示客户端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT状态。
  • 服务器收到客户端的SYN报文后,首先服务器也随机初始化自己的序号(server_isn),将此序号填入TCP首部序列号字段中,其次把TCP首部的确认应答号字段填入client_isn + 1,接着把SYN和ACK标志位置为1,最后把该报文发给客户端,该报文也不包含应用层数据,之后服务器处于SYN-RCVD状态。
  • 客户端收到服务端报文后,还要向服务端回应一个应答报文,首先应答报文TCP首部ACK标志位置为1,其次确认应答号字段填入server_isn + 1,最后把报文发送给服务器,这次报文可以携带客户端到服务端的数据,之后客户端处于eatablished状态。
  • 服务端收到客户端的应答报文后,也进入eatablished状态。

三次握手原理

什么是Socket?

客户端:首先创建一个socket,然后向服务某个端口发送connect请求。也就是发起三次握手建立连接

服务端:首先也是创建一个socket,和客户端不一样的是,它需要进行bind,紧接着进行一次listen,当bind和listen都处理完之后可以调用accept,进行三次握手连接请求的接收。

在操作系统中,通常会为应用程序提供一组应用程序接口,称为套接字接口(Socket),应用程序可以通过套接字接口,来使用网络套接字,以进行资料交换。

Socket内核数据结构如图所示。Socket在Linux内核中其实就是一组结构体。struct socket就是Socket在内核中最上层的一个封装。通过struct socket封装了操作对象给应用暴露出来。

bind函数作用

bind具体调用位置如下源码所示。

比如我们一台机器上要运行很多服务,nginx、redis等,bind函数的作用就是通知内核,每一个socket监听哪一个ip和端口对。简单来说就是,bind就是给socket设置上ip和端口。

具体执行过程:

  1. 将要绑定的IP地址设置到了socket的inet->inet_rcv_saddr成员中
  2. 将要绑定的端口设置到socket的inet->inet_sport成员上

listen系统调用

客户端作为发送方,发送第一次握手和第三次握手,那么如何区分呢?

既然要区分就要记录下来第一次是不是来过,客户端发起第一次连接的时候,服务端会把第一次握手SYN包放进半连接队列中。第三次握手包,从半连接队列中取出,插入全连接队列。

对于服务器来说,也是分为两个角色,一个是内核态的进程,一个是用户态的进程,内核需要通知用户进程,通知的时候如果有一个连接就通知一下,难免效率会很低,Linux内核的做法是把连接先放到全连接队列里面,缓存起来,等待用户进程把它取走。

listen函数最主要的工作就是创建这两个连接队列。

初始化连接队列的源码如下。

connect函数工作原理

准备工作已经做完了,进程已经起来了。那么下面就该连接了,建立连接最最主要的函数就是connect。

connect调用的时候就是触发三次握手的一个时机。

动态选择一个端口,填充源端口信息。源端口必须在connect的时候进行初始化。

connect关键源码如下。

从本地读取端口配置,遍历查找。

客户端一个端口号只能用一次?一个端口号可以被用多次,能被多次使用的核心原因是客户端和服务器在建立连接的时候,每一条连接在逻辑上是由四元组来组成的。假设客户端在50000端口上和建立服务器建立连接,对于客户端来讲,它有自己的ip、端口号,对于服务器它也有自己的ip、端口号。这个50000端口可以和另外一个服务器建立连接,可以通过四元组查找到正确的连接,不会串线。具体是基于下面match函数实现的。

三次握手在内核中的实现

面试回答

  1. 在握手开始之前,假设服务器和客户端都处于closed状态,客户端和服务器启动,首先会调用bind函数,将socket和对应的ip和端口初始化好。服务器应用程序调用listen函数,内核会申请全连接队列和半连接队列,通过Hash表初始化半连接队列,链表初始化全连接队列。
  2. 第一次握手,客户端调用connect函数,选择一个可用端口,然后发出SYN握手请求,这时需要启动重传定时器,如果超时没有收到ACK启动重传机制,然后客户端会设置为SYN-SENT状态。
  3. 第二次握手,服务器收到SYN请求之后,会先进行半连接队列的溢出判断,如果不溢出,则根据客户端传来的SYN序列号,将上对应的数据偏移,设置对应的ACK,然后将SYN请求放入到半连接队列中,启动定时器,设置为SYN_RCVD状态。
  4. 第三次握手,客户端收到来自服务器的SYN-ACK请求之后,会先清除重传定时器,然后将SYN请求设置为已连接状态,然后根据服务器序列号发送的ACK确认,然后客户端设置为ESTABLISHED。
  5. 服务端收到ACK之后,内核会先创建Socket,然后请求从半连接队列删除,加入到全连接队列,然后状态设置为ESTABLISHED,然后后面服务端使用accpet调用全连接队列取走socket,这里三次握手的连接才正式确认建立。

参考:

小林Coding:

4.1 TCP 三次握手与四次挥手面试题

相关推荐
谷粒.1 小时前
云原生时代的测试策略:Kubernetes环境下的测试实践
运维·网络·云原生·容器·kubernetes
濊繵1 小时前
Linux网络--传输层协议 TCP
linux·网络·tcp/ip
_dindong1 小时前
Linux网络编程:Reactor反应堆模式
linux·服务器·网络·设计模式·php
盛满暮色 风止何安2 小时前
负责均衡的理解
运维·服务器·网络·网络协议·系统安全·安全架构
代码不停2 小时前
HTTP / HTTPS详细介绍
网络协议·http·https
澄岚明雪2 小时前
八股复习之计算机网络中TCP与UDP的区别
网络·计算机网络
vortex52 小时前
渗透测试红队快速打点策略的思考
网络·安全·web安全
董世昌412 小时前
HTTP 核心:GET 与 POST 深度解析(区别、原理与实战场景)
网络·网络协议·http
小熊哥^--^2 小时前
基于TCP全双工特性,HTTP、SSE与WebSocket通信模式差异解析
websocket·网络协议·http