网络编程TCP/IP详解

网络编程TCP/IP详解

1. 网络通信

中继器:信号放大器

集线器(hub):是中继器的一种形式,区别在于集线器能够提供多端口服务,多口中继器,每个数据包的发送都是以广播的形式进行的,容易阻塞网络。

网桥:局域网之间建立连接的桥梁,网桥是一种对帧进行转发的技术,根据MAC分区块,可隔离碰撞。网桥将网络的多个网段在数据链路层连接起来。

交换机(switch):工作在数据链路层,交换机与网桥的细微差别在于交换机常常用来连接独立的计算机,而网桥连接的目标是LAN,所以交换机的端口较网桥多。而且集线器是以广播形式发送数据包,交换机有一个智能化的功能,可以根据相应的地址发送数据包。

  • 转发过滤: 当一个数据帧的目的地址在MAC地址中有映射时,它被转发到连接目的节点的端口而不是所有端口
  • 学习功能:以太网交换机了解每一个端口相连设备的MAC地址,并将地址同相应的端口映射起来存放在交换机缓存中的MAC地址表中。

路由器:连接多个逻辑上分开的网络,能够判断网络地址和选择IP路径,内部存储路由表(配置路由“”),路由表可静态设置,亦可动态设置(根据RIP路由解析协议自动记录),每经过一次路由器,TTL值就会减1。

​ ping命令使用的是ICMP协议

​ ARP协议: 根据IP地址获取mac地址

​ RARP协议:根据mac地址获取IP地址

​ IP:标记逻辑上的地址

​ MAC:标记实际转发数据时的设备地址

​ netmask:和IP地址一起来确定网络号,

​ 默认网关:发送的IP不在同一个网段内,那么会把这个数据转发给默认网关。Mac地址,在两个设备之间通信时变化(路由器),IP地址在整个通信过程中不会发生任何变化。

​ DNS服务器:域名解析服务器,根据域名解析IP地址

通信领域的单工、半双工、全双工

  • 单工通信:传输数据只支持数据在一个方向上传输(收音机)
  • 半双工:传输允许在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,实际上是一种切换方向的单工通信放心,如:对讲机,单行道
  • 全双工:允许数据同时在两个方向上传输,同一时间,允许发送和接收数据。如:网卡,电话,手机,socket。软件开发领域实现TCP的全双工只能是通过多线程或者多进程来处理。

OSI模型

image.png

image.png

2. UDP 用户数据报协议

2.1 UDP用户数据报协议

无连接的简单的面向数据报的运输层协议。

  • 特点: UDP数据报文中包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内不可靠传输协议,发送方所发送的数据报并不一定以相同的次序到达接收方。传输速度快

  • 适用场景:UDP一般用于多点通信和实时数据的业务,注重速度流畅

    • 语音广播
    • 视频会议系统
    • QQ
    • TFTP SNMP RIP(路由信息协议,如报告股票市场,航空信息)
    • DNS(域名解释)
2.2 创建UDP网络程序流程:
  • 1.创建客户端套接字
  • 2.发送/接受数据
  • 3.关闭套接字

image.png

通信流程:

20200531124111.png

3 TCP 传输控制协议

面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。

  • 特性:
    • 面向连接,通信双方必须先建立连接,双方都必须为该连接分配一定的内核资源,以管理连接的状态和连接上的传输。
    • 可靠传输:
      • TCP采用发送应答机制
      • 超时重传:发送端发出一个报文段之后就会启动定时器,在定时时间内没收到应答就重发这个报文段,为了保证不发生丢包,就给每一个包一个序号,同时序号也保证了传送到接收端实体的包按序接收。然后接收端对已成功收到的包回一个 ACK包。如果发送端在合理的RTT内未收到确认,对应的数据包将被假设为已丢失,将会进行重传
      • 错误校验:TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
      • 流量控制和阻塞管理:流量控制用来避免主机发送得过快而使接收方来不及完全收下

tcpmode.png

3.1 创建TCP网络程序流程
  • 服务端

    • # coding: utf-8
      
      import socket
      
      # 创建tcp套接字
      tcpserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      
      addr = ('localhost', 7777)
      
      # 绑定ip
      tcpserver.bind(addr)
      # 开启监听
      tcpserver.listen(5)
      # 接收客户端请求
      print(f'TCP 服务器已开启:{addr}')
      while True:
          newSocket, clientAddr = tcpserver.accept()
          while True:
              data = newSocket.recv(1024)
              if len(data) > 0:
                  print('receive from [%s]:%d, data: %s' % (*clientAddr, data.decode('utf-8')))
              else:
                  break
              newSocket.send('thank you!'.encode('utf-8'))
          newSocket.close()
      
      tcpserver.close()
      
  • 客户端

    • # coding: utf-8
      
      import socket
      
      tcpclient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      
      dest = ('localhost', 7777)
      
      tcpclient.connect(dest)
      
      while True:
          sendData = input('send: #some msg#')
      
          if len(sendData) > 0:
              tcpclient.send(sendData.encode('utf-8'))
          else:
              break
      
          recvData = tcpclient.recv(1024)
          print(recvData.decode('utf-8'))
      
      tcpclient.close()
      
3.2 TCP的数据包格式

image.png

  • 源端口和目的端口:各占16bit=2字节

  • 序列号(Seq):占32位=4字节 range=[0:2^32] 表示数据的第一个字节的序列号,TCP的数据交互式基于序列号(控制华东窗口),发送方通过序列号控制发送的数据,以及超时重传,接收方通过序列号控制乱序重排。

    接收方根据三次握手后确认的首字节序列号+数据长度,计算得到最后一个字节的序列号,并将其加1作为ack应答。

  • 确认号(ACK):占4个字节,表示期望下次收到的序列号。比如服务器收到客户端发来的报文段,其序列号字段值为501,并通过计算可知数据长度为200,所以服务器可以算出最后一个字节的序列号为700。这表明服务器正确收到了客户端发送的序列号到700为止的数据,因此,服务器期望下次收到的序列号为701,并将其作为确认号放入应答报文段中

    确认号和序列号范围相同,当溢出时从0开始

  • 数据偏移:占4bit, 表示TCP报文段的第一个数据距离报文段起始处有多远。数据偏移代表的是4字节的倍数,由于4位二进制最大的可以表示15, 所以数据偏移最大值为4*15=60字节,即TCP报文首部最大长度。最小为20字节,偏移值=5。

  • 保留 占6位,占未使用,可能是预留其他控制标志位,或者对齐字节位

  • 控制位,用于说明报文段的性质。每个控制字段占1位

    • 紧急URG:开启时表示此数据包处于紧急状态应优先处理

    • 确认标志位ACK:开启表明确认号有效,TCP规定连接建立后发送的所有报文段ACK位都必须置1

    • 推送PSH:该控制位很少使用,因为TCP会自己决定什么时候应该使用PUSH操作。

    • 复位RST:用于复位,表示连接出现错误,应当立即关闭。当TCP接收到复位报文段后会通知应用程序连接被复位,随后关闭连接

    • 同步SYN:连接建立的过程中用于同步序列号,告知对方自己的起始序列号。可以根据对方的序列号初始化缓冲区起点(滑动窗口)

      SYN=1,ACK=0时表示一个连接请求报文段,SYN=1,ACK=1表示一个连接接收报文段

    • 终止FIN:用于释放连接,报文段中FIN控制位为1表示已经将数据发送完毕。等待关闭连接

    • 窗口:占2个字节,表示发送该报文段的一方能够接收的字节数,表明期望接受到的数据包字节数,用于拥塞控制。窗口值范围为[0:2^16−1]

    • 校验和:占2个字节,用于检验报文段是否出错。发送方根据发送的报文段计算检验和填入报文段首部,接收方根据接收的报文段重新计算,如果不匹配,表明报文段出错

    • 紧急指针:占2个字节,表示紧急数据的个数。在紧急状态下(URG打开),指出窗口中紧急数据的位置(末端)。

    • 选项:用于支持一些特殊的变量,比如最大分组长度(MSS),MSS指的是数据的最大长度而不是TCP报文段长度。在将数据发送之前,会根据MSS将数据进行合理的切分,即单次发送的报文段中的数据不能超过MSS,所以MSS应该适当调大一些以降低网络中的报文段个数

    查缺补漏

    MSS(Maximum Segment Size):MSS 是TCP选项中最经常出现,也是最早出现的选项。MSS选项占4byte。MSS是每一个TCP报文段中数据字段的最大长度,注意:只是数据部分的字段,不包括TCP的头部。TCP在三次握手中,每一方都会通告其期望收到的MSS(MSS只出现在SYN数据包中)如果一方不接受另一方的MSS值则定位默认值536byte。
    MSS值太小或太大都是不合适。太小,例如MSS值只有1byte,那么为了传输这1byte数据,至少要消耗20字节IP头部+20字节TCP头部=40byte,这还不包括其二层头部所需要的开销,显然这种数据传输效率是很低的。MSS过大,导致数据包可以封装很大,那么在IP传输中分片的可能性就会增大,接受方在处理分片包所消耗的资源和处理时间都会增大,如果分片在传输中还发生了重传,那么其网络开销也会增大。因此合理的MSS是至关重要的。MSS的合理值应为保证数据包不分片的最大值,对于以太网MSS可以达到1460byte,在IP层中有一个类似的概念,MTU(Maximum Transfer Unit)

    MTU=MSS+TCP Header + IP Header

    image.png

    为什么需要MSS?

    主要是为了最大程度的保证传输的高效和稳定性

    那么MTU和MSS又有什么必然联系呢?虽然MTU限制了IP层的报文大小,但分层网络模型本来不就是为了对上层提供透明的服务么?即使一个很大的TCP报文传递给IP层,IP层也应该可以经过分段等手段成功传输报文才对。

    理论上来说是没错的,UDP中就不存在MSS,UDP生成任意大的UDP报文,然后包装成IP报文根据底层网络的MTU分段进行发送。MSS存在的本质原因就是TCP和UDP的根本不同:TCP提供稳定的连接。假设生成了很大的TCP报文,经过IP分段进行发送,而其中一个IP分段丢失了,则TCP协议需要重发整个TCP报文,造成了严重的网络性能浪费,而相对的由于UDP无保证的性质,即使丢失了IP分段也不会进行重发。所以说,MSS存在的核心作用,就是避免由于IP层对TCP报文进行分段而导致的性能下降

    通常将MSS设置为MTU-40(20字节IP头部+20字节TCP头部),在TCP建立连接时由连接双方商定,双方得到的MSS值可能并不相同,建立MSS所基于MTU的值基于路径MTU发现机制获取。

    参考:

    TCP报文段首部格式

    TCP Maximum Segment Size (MSS)

    TCP Maximum Segment Size (MSS) and Relationship to IP Datagram Size

3.3 TCP/IP协议族详解之IP协议
3.3.1 IP协议的功能:
  • 路由寻址
  • 传递服务,有两个特点:不可靠,可靠性由上层协议提供,如TCP协议,无连接(IP并不维护任何关于后续数据报的状态信息。每个数据报的处理是相互独立的,这也就是说IP数据报可以不按发送顺序接收)
  • 数据包分段(Segment)和重组
3.3.2 IP协议头部格式

image.png

可根据Wireshark抓包工具分析数据包含义 参考:TCP/IP协议族详解(二)

应用程序使用TCP/IP协议传输数据时,数据要被送入协议栈经过逐层封装,最后作为比特流在媒体上传送,其过程示意图如下所示:

image.png

注:从上图可以看到以太网帧的数据长度是有大小限制的,这个最大值称为 MTU,所以当 IP 数据包长度大于 MTU 时会被拆成多个帧传输,称为 “IP分片”——-Mr.su Blog

3.4 TCP 三次握手

抛出疑惑:为什么是三次握手而不是二次或者四次握手?

TCP作为一种可靠传输控制协议,核心思想:既要保证数据可靠传输,又要提高传输的效率,而用三次就可以满足以上两方面的需求。

TCP的可靠性就是通过三次握手就是确认通信双方数据原点的初始序列号 (Initial Sequence Number)

通俗的描述:客户端A发出连接请求,由操作系统动态随机选取一个32位长的序列号(Initial Sequence Number), 假设A的初始序列号是1000, 以该序列号为原点,对自己将要发送的每个字节进行编号,1001, 1002…, 并把自己的初始序列号INS告诉B, 什么样的编号的数据是合法的,方便服务端B对A的每一个编号的字节数据进行确认。如:如果A收到B确认编号为2001,则意味着字节编号为1001-2000,共1000个字节已经安全到达。

同理B也是类似的操作,假设B的初始序列号ISN为2000,以该序列号为原点,对自己将要发送的每个字节的数据进行编号,2001,2002,2003…,并把自己的初始序列号ISN告诉A,以便A可以确认B发送的每一个字节。如果B收到A确认编号为4001,则意味着字节编号为2001-4000,共2000个字节已经安全到达。

image.png

第一次握手:

客户端向服务端发送连接请求报文段,报文段的头部中SYN=1, ACK=0, seq=x。请求发送后,客户端进入SYN-SENT状态

  • SYN=1, ACK=0 标识该报文段为连接请求报文
  • seq=x, 标识本次TCP通信客户端数据字节流的初始序列号
  • TCP规定:SYN=1的报文段不能有数据部分,但要消耗掉一个字节,一个序号

第二次握手:

服务端处于监听状态LISTEN,收到连接请求报文后,如果同意连接,返回一个应答 SYN=1, ACK=1, seq=y, ack=x+1, 进入SYN-RCVD状态

第三次握手:

当客户端收到服务器的应答后,还要向服务端发送一个确认报文段,表示服务端发来的连接同意应答已经成功收到,且收到服务端的出示序列号y

确认报文为:ACK=1, seq=x+1, ack=y+1。

为什么连接建立需要三次握手,而不是2次握手?

防止失效的连接请求报文段被服务端接收,从而产生错误,失效的连接请求:若客户端向服务端发送的连接请求丢失,客户端等待应答超时后就会再次发送连接请求,此时,上一个连接请求就是『失效的』—《计算机网络》谢希仁版

三次握手中存在的漏洞:SYN flood!,攻击者通过向服务器发起大量的SYN报文,把服务器的SYN报文连接的队列生生耗尽,导致正常的连接请求得不到处理,目前只能进行减缓,别没有解决补丁

  1. 在web应用程序中可以使用安全的CSRF令牌环节问题。CSRF攻击将在服务器造成持久的变化而没有处理要求,除非使用了有效的CSRF令牌。

  2. 首保丢弃:可通过丢弃客户端的第一个SYN报文来达到防御的目的,TCP是一种可靠的协议,为了确保所有的数据包都能到达服务器,设计了一个重传机制。真实的客户端访问,在一定的时间内如果没有收到服务器的回复,将会再次发送SYN报文。

  3. 内核层面进行缓解:

    • 增大tcp_max_syn_backlog
    • 减小tcp_synack_retries
    • 启用tcp_syncookies: 当启用tcp_syncookies时,backlog满了后,linux内核生成一个特定的n值,而不并把客户的连接放到半连接的队列backlog里(即没有存储任何关于这个连接的信息,不浪费内存)。当客户端提交第三次握手的ACK包时,linux内核取出n值,进行校验,如果通过,则认为这个是一个合法的连接。(加密的INS)

    注:tcp_max_syn_backlog 在 syn_cookies 开启时是无效的,这两个选项存在冲突

3.5 TCP四次挥手

image.png

第一挥手

若A认为数据发送完成,就会向B发送连接释放请求,该请求只有报文头,头重携带的主要参数为:FIN=1, seq=u, 此时A进入FIN-WAIT-1状态

  • FIN=1即TCP报文段中的控制位FIN置1表示该数据报为连接释放请求
  • seq=u, u-1是A向B发送的最后一个字节的序号

第二次挥手

B收到连接释放请求后,会通知相应的应用程序,告诉它连接已经释放,此时B进入CLOSE_WAIT状态, 报文头:ACK=1, seq=v, ack=u+1

  • ACK=1, 除了TCP连接请求报文段以外,TCP通信过程中数据报的ACK控制为都为1
  • seq=v, v-1表示B向A发送的最后一个字节的序号
  • ack=u+1 表示希望收到第u+1个字节开始的报文段,已经成功接收了签u个字节数据

A收到该应答后进入 FIN_WAIT_2状态,等待B发送连接释放请求

第二次挥手后,A->B方向的连接已经释放,A不会再发送数据,但B->A方向的连接仍然存在。

第三次挥手

当B向A发送完所有数据后,向A发送连接释放请求,请求头: FIN=1, ACK=1, seq=w, ack=u+1 B进入 LAST_ACK状态

第四次挥手

A收到释放请求后,向B发送确认应答,A进入TIME_WAIT状态。该状态会持续2MSL(Maximum Segment Lifetime)时间,(报文最大生存时间),若该时间段内B没有发送请求的话,就进入CLOSED状态,关闭TCP。当B收到确认应答后,也进入CLOSED状态, 关闭TCP。

为什么A要先进入TIME-WAIT状态,等待2MSL时间后才进入CLOSED状态?

为了保证B能收到A的确认应答。
若A发完确认应答后直接进入CLOSED状态,那么如果该应答丢失,B等待超时后就会重新发送连接释放请求,但此时A已经关闭了,不会作出任何响应,因此B永远无法正常关闭。在模拟tcpserver的时候,如果是服务器先close的时候,在2MSL中(也就是2-4分钟之内并不会马上释放端口)不过在实际应用中可以通过设置 SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

参考TCP 为什么是三次握手,而不是两次或四次?-[大闲人柴毛毛]