网际传输协议和传输控制协议(IP,TCP,IP)

1 简介

  网络协议是网络上所有设备(网络服务器、计算机及交换机、路由器、防火墙等)之间通信规则的集合,它规定了通信时信息必须采用的格式和这些格式的意义。网络设备中,不同层一般实现有不同的协议,层层包装。网络分层一般为OSI的七层模型和TCP/IP五层模型(如下图所示)。发送数据时,自上而下层层包装,每一层加上当前层的头(header);接收时,自下而上,层层解包,得到最终的数据。
在这里插入图片描述

  IP(Internet Protocal,网际传输协议)是在网络层的协议,IP提供一种简单的数据传输协议,不保证数据可达和正确性;TCP/UDP是在传输层的协议,TCP和UDP都是传输控制协议,TCP能够保证数据的可达性和正确性,还会进行流量控制和拥塞控制之类的机制,UDP无法保证数据的可达性。

2 IP

2.1 简介

  IP(Internet Protocal,网际传输协议)是在TCP/IP协议族中网络层的主要协议,任务仅仅是根据源主机和目的主机的地址来传送数据。为此目的,IP定义了寻址方法和数据报的封装结构。
  数据在IP互联网中传送时会被封装为数据包。IP协议的独特之处在于:在报文交换网络中主机在传输数据之前,无须与先前未曾通信过的目的主机预先创建好一条特定的“通路”。互联网协议提供了一种“不可靠的”数据包传输机制(也被称作“尽力而为”或“尽最大努力交付”);也就是说,它不保证数据能准确的传输。数据包在到达的时候可能已经损坏,顺序错乱(与其它一起传送的报文相比),产生冗余包,或者全部丢失。如果应用需要保证可靠性,一般需要采取其他的方法,例如利用IP的上层协议控制。

  IP一般涉及的主要是路由寻址和IP地址分配(比如DHCP),以及如何划分和组合子网。IP地址指主机在网络中的地址,一般为IPV4地址(如下图为IPv4地址范围),现如今也在开始推广IPV6。IPV4和IPV6的区别是地址长度不同,IPV6比IPV4能够支持更多的用户。
在这里插入图片描述

2.2 IP的可靠性

  互联网协议的设计原则,假定网络基础设施本身就是不可靠的单一网络元素或传输介质,并且它使用的是动态的节点和连接。不存在中央监测和性能衡量机制来跟踪和维护网络的状态。为了减少网络的复杂性,大部分网络只能故意地分布在每个数据传输的终端节点。传输路径中的路由器只是简单地将数据包发送到下一个匹配目的地址的路由前缀的本地网关。
  基于这种设计院色,IP协议只提供尽可能的送达,其是一种无连接的不可靠的协议,在传输过程中可能出现以下错误:

  • 数据包损坏;
  • 数据包丢失;
  • 数据包重复送达;
  • 数据包传递乱序。

  一般来说可靠的数据传输需要上层协议的支持,一般就是指TCP。

2.1 IP Header

在这里插入图片描述
  不同字段的解释:

  • 版本(Version):占4bit,通信双方使用的版本必须一致。对于IPV4,该字段值为4;

  • 首部长度(Internet Header Length, IHL):占4bit,首部长度说明首部有多少32位字(4字节)。由于IPv4首部可能包含数目不定的选项,这个字段也用来确定数据的偏移量。这个字段的取值范围为[5,15]即[20,60]字节;

  • 区分服务(Differentiated Services,DS):占6bit,最初被定义为服务类型字段,实际上并未使用。只有在使用区分服务时,这个字段才起作用,在一般的情况 下都不使用这个字段。例如需要实时数据流的技术会应用这个字段,一个例子是VoIP;

  • 显式拥塞通告( Explicit Congestion Notification,ECN):占2bit,在RFC 3168中定义,允许在不丢弃报文的同时通知对方网络拥塞的发生。ECN是一种可选的功能,仅当两端都支持并希望使用,且底层网络支持时才被使用;

  • 全长(Total Length):占16bit,定义了报文总长,包含首部和数据,单位为字节。取值范围为[20,65535]字节。IP规定所有主机都必须支持最小576字节的报文,这是假定上层数据长度512字节,加上最长IP首部60字节,加上4字节富裕量,得出576字节,但大多数现代主机支持更大的报文。当下层的数据链路协议的最大传输单元(MTU)字段的值小于IP报文长度时,报文就必须被分片;

  • 标识符(Identification)占16bit,这个字段主要被用来唯一地标识一个报文的所有分片,因为分片不一定按序到达,所以在重组时需要知道分片所属的报文。每产生一个数据报,计数器加1,并赋值给此字段。一些实验性的工作建议将此字段用于其它目的,例如增加报文跟踪信息以协助探测伪造的源地址;

  • 标志 (Flags):3个bit被用来控制和识别分片(如果DF标志被设置为1,但路由要求必须分片报文,此报文会被丢弃。这个标志可被用于发往没有能力组装分片的主机。当一个报文被分片,除了最后一片外的所有分片都设置MF为1。最后一个片段具有非零片段偏移字段,将其与未分片数据包区分开,未分片的偏移字段为0。):

    • 位0:保留,必须为0;
    • 位1:禁止分片(Don’t Fragment,DF),当DF=0时才允许分片;
    • 位2:更多分片(More Fragment,MF),MF=1代表后面还有分片,MF=0 代表已经是最后一个分片。
  • 分片偏移 (Fragment Offset):13bit,表示每个分片相对于原始报文开头的偏移量,以8字节作单位;

  • 存活时间(Time To Live,TTL):占8bit,避免报文在互联网中永远存在(例如陷入路由环路)。存活时间以秒为单位,但小于一秒的时间均向上取整到一秒。在现实中,这实际上成了一个跳数计数器:报文经过的每个路由器都将此字段减1,当此字段等于0时,报文不再向下一跳传送并被丢弃,最大值是255;

  • 协议 (Protocol)占8bit,这个字段定义了该报文数据区使用的协议;

  • 首部检验和 (Header Checksum):占16bit,只对首部查错,不包括数据部分。在每一跳,路由器都要重新计算出的首部检验和并与此字段进行比对,如果不一致,此报文将会被丢弃;

  • 源地址:占32bit,因为NAT的存在,这个地址并不总是报文的真实发送端,因此发往此地址的报文会被送往NAT设备,并由它被翻译为真实的地址;

  • 目的地址:占32bit,指出报文的发送地址;

  • 选项:附加的首部字段可能跟在目的地址之后,但这并不被经常使用,从1到40个字节不等。请注意首部长度字段必须包括足够的32位字来放下所有的选项(包括任何必须的填充以使首部长度能够被32位整除)。当选项列表的结尾不是首部的结尾时,EOL(选项列表结束,0x00)选项被插入列表末尾。下表列出了可能的情况:
    在这里插入图片描述

  • 数据:用户数据部分。

2.3 IP分片和重组

  互联网协议(IP)是整个互联网架构的基础,可以支持不同的物理层网络,即IP层独立于链路层传输技术。不同的链路层不仅在传输速度上有差异,还在帧结构和大小上有所不同,不同MTU参数描述了数据帧的大小。为了实现IP数据包能够使用不同的链路层技术,需要将IP数据包变成适合链路层的数据格式,IP报文的分片即是IP数据包为了满足链路层的数据大小而进行的分割。
  当设备收到IP报文时,分析其目的地址并决定要在哪个链路上发送它。MTU决定了数据载荷的最大长度,如IP报文长度比MTU大,则IP数据包必须进行分片。每一片的长度都小于等于MTU减去IP首部长度。接下来每一片均被放到独立的IP报文中,并进行如下修改:

  • 总长字段被修改为此分片的长度;
  • 更多分片(MF)标志被设置,除了最后一片;
  • 分片偏移量字段被调整为合适的值;
  • 首部检验和被重新计算。

  当一个接收者发现IP报文的下列项目之一为真时:
当一个接收者发现IP报文的下列项目之一为真时:

  • DF标志为0;
  • 分片偏移量字段不为0。

  它便知道这个报文已被分片,并随即将数据、标识符字段、分片偏移量和更多分片标志一起储存起来。当接受者收到了更多分片标志未被设置的分片时,它便知道原始数据载荷的总长。一旦它收齐了所有的分片,它便可以将所有片按照正确的顺序(通过分片偏移量)组装起来,并交给上层协议栈。

3 TCP

3.1 简介

  传输控制协议(英语:Transmission Control Protocol,缩写:TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,是处于传输层的网络协议。TCP能够提供面向连接的、可靠的数据传输。

  TCP协议的运行一般分为三个阶段:连接创建(connection establishment)、数据传送(data transfer)和连接终止(connection termination)。建立链接一般就是我们说的三次握手,链接终止便是四次挥手。

3.2 TCP Header

在这里插入图片描述

  • 来源连接端口(16bit):识别发送连接端口;
  • 目的连接端口(16bit):识别接收连接端口;
  • 序列号(seq,32bit):
    • 如果含有同步化标志(SYN),则此为最初的序列号;第一个数据比特的序列码为本序列号加一;
    • 如果没有同步化标志(SYN),则此为第一个数据比特的序列码;
  • 确认号(ack,32bit):期望收到的数据的开始序列号,即已经收到的数据的字节长度加1;
  • 数据偏移(4bit):以4字节为单位计算出的数据段开始地址的偏移值;
  • 保留(3bit):须置0;
  • 标志符(9bit):
    • NS:ECN-nonce。ECN显式拥塞通知(Explicit Congestion Notification)是对TCP的扩展,定义于RFC 3540(2003)。ECN允许拥塞控制的端对端通知而避免丢包。ECN为一项可选功能,如果底层网络设施支持,则可能被启用ECN的两个端点使用。在ECN成功协商的情况下,ECN感知路由器可以在IP头中设置一个标记来代替丢弃数据包,以标明阻塞即将发生。数据包的接收端回应发送端的表示,降低其传输速率,就如同在往常中检测到包丢失那样;
    • CWR:Congestion Window Reduced,定义于RFC 3168(2001);
    • ECE:ECN-Echo有两种意思,取决于SYN标志的值,定义于RFC 3168(2001);
    • URG:为1表示高优先级数据包,紧急指针字段有效;
    • ACK:为1表示确认号字段有效;
    • PSH:为1表示是带有PUSH标志的数据,指示接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满;
    • RST:为1表示出现严重差错。可能需要重新创建TCP连接。还可以用于拒绝非法的报文段和拒绝连接请求;
    • SYN:为1表示这是连接请求或是连接接受请求,用于创建连接和使顺序号同步;
    • FIN:为1表示发送方没有数据要传输了,要求释放连接;
  • 窗口(WIN,16bit):表示从确认号开始,本报文的发送方可以接收的字节数,即接收窗口大小。用于流量控制;
  • 校验和(Checksum,16bit):对整个的TCP报文段,包括TCP头部和TCP数据,以16位字进行计算所得。这是一个强制性的字段;
  • 紧急指针(16bit):本报文段中的紧急数据的最后一个字节的序号;
  • 选项字段:(<=40bit):每个选项的开始是1字节的kind字段,说明选项的类型:
    • 0:选项表结束(1byte)
    • 1:无操作(1byte)用于选项字段之间的字边界对齐。
    • 2:最大报文段长度(4byte,Maximum Segment Size,MSS)通常在创建连接而设置SYN标志的数据包中指明这个选项,指明本端所能接收的最大长度的报文段。通常将MSS设置为(MTU-40)字节,携带TCP报文段的IP数据报的长度就不会超过MTU(MTU最大长度为1518字节,最短为64字节),从而避免本机发生IP分片。只能出现在同步报文段中,否则将被忽略。
    • 3:窗口扩大因子(4byte,wscale),取值0-14。用来把TCP的窗口的值左移的位数,使窗口值乘倍。只能出现在同步报文段中,否则将被忽略。这是因为现在的TCP接收数据缓冲区(接收窗口)的长度通常大于65535字节。
    • 4:sackOK:发送端支持并同意使用SACK选项。
    • 5:SACK实际工作的选项。
    • 8:时间戳(10字节,TCP Timestamps Option,TSopt)
      • 发送端的时间戳(Timestamp Value field,TSval,4byte)
      • 时间戳回显应答(Timestamp Echo Reply field,TSecr,4byte)

3.3 TCP 三次握手

在这里插入图片描述
  TCP三次握手过程为:

  1. 客户端发送SYN=1,seq=X;
  2. 服务端接收到客户端的请求,发送SYN=1,ACK=1,ack=X+1,seq=Y;
  3. 客户端接收到服务端的ACK,发送ACK=1,ack=Y+1,seq=X+1;
  4. 服务端接收到客户端的ACK,建立链接。

  以上过程总共互相交互了三次数据,即三次握手。三次握手中设置的标志位和序列号在TCP Header总能够清晰的看到。

3.3.1 为什么是三次握手

TCP 为什么是三次握手,而不是两次或四次?

  上面这个回答里面说的很明白,简单说下,网络传输一般要考虑到效率和性能,TCP链接的目的肯定是在使用最少网络资源的情况下进行目标任务即完成双向可靠的面向连接的传输链接,即双方都要有发送和接受数据的能力。那么便可以简单的从1开始枚举:

  • 1次握手过程:客户端向服务端发送建立链接请求,SYN,Seq,服务端接收到请求,链接建立发送数据。但是如果服务端不可达或者服务端完全拒绝建立连接,即便链接建立完成,双方也无法保证互相的传输能力;
  • 2次握手:客户端向服务器发送SYN,SEQ,服务端接收到客户端的请求向客户端发送ACK,SYN,SEQ客户端接收到服务端的ACK建立连接。两次握手双方只针对客户端的SEQ进行了确认,确保了服务端的接受和发送数据的可靠性,而无法确保客户端发送数据的可靠性;
  • 4次握手:将三次握手中间两步拆分成服务端分别发送ACK+SEQ和SYN+SEQ,那同理为什么不合并呢?

  另外需要注意的是三次握手并无法在理论上完全保证连接的可靠性,理论上无论多少次握手连接都是不可靠的,而三次握手只是验证了双方的数据接受和发送能力。

3.4 TCP四次挥手

在这里插入图片描述
  TCP四次挥手的过程是:

  1. 其中一方(暂指客户端)发送FIN和seq=X;
  2. 服务端接收到请求后发送ACK,ack=X+1,seq=Y;
  3. 等待一段时间,服务端再次发送FIN,ACK,ack=X+1,seq=Z;
  4. 客户端接收到服务端的ACK后发送ACK,seq=X+1,ack=Z+1,客户端进入TIME-WAIT,一般为2*MSL,MSL不同系统不同,超时之后断开连接;
  5. 服务端接收到后断开连接。

  为什么客户端要进入TIME-WAIT等待2MSL?

  1. 保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失;
  2. 防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

3.5 TCP 流量控制

  流量控制(flow control (data))用来避免主机分组发送得过快而使接收方来不及完全收下,一般由接收方通告给发送方进行调控。
  TCP使用滑动窗口协议(Sliding Window Protocol)实现流量控制。接收方在“接收窗口”域指出还可接收的字节数量。发送方在没有新的确认包的情况下至多发送“接收窗口”允许的字节数量。接收方可修改“接收窗口”的值。
  当接收方宣布接收窗口的值为0,发送方停止进一步发送数据,开始了“保持定时器”(persist timer),以避免因随后的修改接收窗口的数据包丢失使连接的双侧进入死锁,发送方无法发出数据直至收到接收方修改窗口的指示。当“保持定时器”到期时,TCP发送方尝试恢复发送一个小的ZWP包(Zero Window Probe),期待接收方回复一个带着新的接收窗口大小的确认包。一般ZWP包会设置成3次,如果3次过后还是0的话,有的TCP实现就会发RST把链接断开。
  如果接收方以很小的增量来处理到来的数据,它会发布一系列小的接收窗口。这被称作愚蠢窗口综合症,因为它在TCP的数据包中发送很少的一些字节,相对于TCP包头是很大的开销。解决这个问题,就要避免对小的window size做出响应,直到有足够大的window size再响应:

  • 接收端使用David D Clark算法:如果收到的数据导致window size小于某个值,可以直接ack把window给关闭了,阻止了发送端再发数据。等到接收端处理了一些数据后windows size大于等于了MSS,或者接收端buffer有一半为空,就可以把window打开让发送端再发数据过来。
  • 发送端使用Nagle算法来延时处理,条件一:Window Size>=MSS 且 Data Size >=MSS;条件二:等待时间或是超时200ms,这两个条件有一个满足,才会发数据,否则就是在积累数据。Nagle算法默认是打开的,所以对于一些需要小包场景的程序——比如像telnet或ssh这样的交互性程序,需要关闭这个算法。可以在Socket设置TCP_NODELAY选项来关闭这个算法。

3.6 TCP 拥塞控制

  拥塞控制是发送方根据网络的承载情况控制分组的发送量,以获取高性能又能避免拥塞崩溃(congestion collapse,网络性能下降几个数量级)。这在网络流之间产生近似最大最小公平(max-min fairness)分配。
  发送方与接收方根据确认包或者包丢失的情况,以及定时器,估计网络拥塞情况,从而修改数据流的行为,这称为拥塞控制或网络拥塞避免。
  TCP的现代实现包含四种相互影响的拥塞控制算法:慢启动、拥塞避免、快速重传、快速恢复。
  拥塞控制本身是基于拥塞窗口(cwbd)进行发送数据窗口大小调节的方法。

  流量控制和拥塞控制的区别:

  • 流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的,是局部的;
  • 拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况,是全局的。

3.6.1 慢启动

  慢启动(Slow Start),是传输控制协议(TCP)使用的一种阻塞控制机制。慢启动也叫做指数增长期。慢启动是指最初以一个比较小的cwnd开始,每次TCP接收窗口收到确认时都会增长,增加的大小就是已确认段的数目。这种情况一直保持到要么没有收到一些段即超时,要么窗口大小到达预先定义的阈值。

在这里插入图片描述

3.6.2 拥塞避免

  如果慢启动阶段发生timeout或者cwnd达到阈值sstresh,cwnd就进入线性增长阶段,每个RTT只增长较小的值直到达到最大cwnd的阈值。
  拥塞避免和慢启动之间的切换如下:

  1. c w n d < s s t h r e s h cwnd<ssthresh cwnd<ssthresh,使用慢启动算法;
  2. c w n d = s s t h r e s h cwnd=ssthresh cwnd=ssthresh,使用二者任意一个;
  3. c w n d > s s t h r e s h cwnd>ssthresh cwnd>ssthresh,使用拥塞避免算法;

  当出现分组丢失时即timeout,即判定为网络阻塞,此时将ssthresh设为当前cwnd的一半(不能小于2),cwnd重新设为1,执行慢启动算法。这样可以减少网络中的分组数量,减小网络压力。

在这里插入图片描述

  图中的乘法减小是指:当发生网络阻塞时,ssthresh设置为当前cwnd的一半,cwnd重新增长。加法增大是通过加法线性增大。

3.6.2 快速重传

  快速重传(Fast retransmit)是对TCP发送方降低等待重发丢失分段用时的一种改进。TCP发送方每发送一个分段都会启动一个超时计时器,如果没能在特定时间内接收到相应分段的确认,发送方就假设这个分段在网络上丢失了,需要重发。这也是 TCP 用来估计 RTT 的测量方法。
  快速重传算法有两个版本:Tahoe和Reno。两者算法大致一致,对于丢包事件判断都是以重传超时(retransmission timeout,RTO)和重复确认为条件,但是对于重复确认的处理,两者有所不同:

  • Tahoe:如果收到三次重复确认——即第四次收到相同确认号的分段确认,并且分段对应包无负载分段和无改变接收窗口——的话,Tahoe算法则进入快速重传,将慢启动阈值改为当前拥塞窗口的一半,将拥塞窗口降为1个MSS,并重新进入慢启动阶段;
  • Reno:如果收到三次重复确认,Reno算法则进入快速重传,只将拥塞窗口减半来跳过慢启动阶段,将慢启动阈值设为当前新的拥塞窗口值,进入一个称为“快速恢复”的新设计阶段。

  对于RTO,两个算法都是将拥塞窗口降为1个MSS,然后进入慢启动阶段。
在这里插入图片描述

3.6.3 快速恢复

  快速恢复(Fast recovery)是Reno算法新引入的一个阶段,在将丢失的分段重传后,启动一个超时定时器,并等待该丢失分段包的分段确认后,再进入拥塞控制阶段。如果仍然超时,则回到慢启动阶段。

  快速恢复算法有很多版本:TCP Vegas,TCP Hybla,TCPBIC和CUBIC等等。

3.7 TCP Socket编程

3.7.1 过程

  TCP Scoket编程中一般有两个角色服务端和客户端,服务端通过打开scoket监听端口,等待客户端的链接,客户端通过socket连接服务端已经打开的socket进行网络通信。
  scoket的基本过程如下:
在这里插入图片描述

3.7.2 socket API

//创建网络端点,返回socket文件描述符,失败返回-1设errno
int socket(int domain, int type, int protocol);
/*  domain表示协议族,一般使用AF_INET
    AF_UNIX, AF_LOCAL   Local communication
    AF_INET             IPv4 Internet protocols
    AF_INET6            IPv6 Internet protocols
    AF_IPX              IPX - Novell protocols
    AF_NETLINK          Kernel user interface device
    AF_X25              ITU-T X.25 / ISO-8208 protocol
    AF_AX25             Amateur radio AX.25 protocol
    AF_ATMPVC           Access to raw ATM PVCs
    AF_APPLETALK        AppleTalk
    AF_PACKET           Low level packet interface
    AF_ALG              Interface to kernel crypto API
*/
/* type表示协议类型(TCP/UDP)TCP使用SOCK_STREAM,UDP使用SOCK_DGRAM
SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission mechanism may be supported.
SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
SOCK_SEQPACKET  Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is required to read an entire packet with  each  input system call.
SOCK_RAW        Provides raw network protocol access.
SOCK_RDM        Provides a reliable datagram layer that does not guarantee ordering.
SOCK_PACKET     Obsolete and should not be used in new programs;
/*
/*protocol表示特殊协议,一般给0
*/
//把通信地址和socket文件描述符绑定,用在服务器端,成功返回0,失败返回-1设errno
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
sockfd  通过socket()创建的socket文件描述符
*/
/*addr
struct sockaddr{	//主要用于函数的形参类型, 很少定义结构体变量使用, 叫做通用的通信地址类型//$man bind
	sa_family_t 	sa_family;
	char        	sa_data[14];
}
struct sockaddr_in{	//准备网络通信的通信地址	//$man in.h
	sa_family_t	sin_family;   	//协议族, 就是socket()的domain的AF_INET
	in_port_t       sin_port;   //端口号
	struct in_addr	sin_addr;   //IP地址,
                    //当前网段的最大ip地址是广播地址,即,xxx.xxx.xxx.255。
                    //255.255.255.255在所有网段都是广播地址
}
struct in_addr{
    in_addr_t	s_addr;		//整数类型的IP地址
}
*/
/*addrlen addr的长度
*/
//创建侦听socket,把sockfd标记的socket标记为被动socket,被动socket表示这个socket只能用来接收即将到来的连接请求,不再用于读写操作和通信,接收连接请求的是accept()
//成功返回0,失败返回-1设errno
int listen(int sockfd, int backlog);
/*sockfd监听的socket文件描述符
*/
/*backlog
排队等待“被响应”连接请求队列的最大长度 eg: 接待室的最大长度
*/
//创建连接socket,返回连接socket的文件描述符,成功返回文件描述符,失败返回-1设errno
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*sockfd监听的socket文件描述符
*/
/*addr  客户端的信息,包含ip,端口号等
*/
/*addrlen   addr的长度
*/
//向指定的socket发送指定的数据,成功返回实际发送数据的大小,失败返回-1设errno
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//sockfd    发送数据的socket描述符,一般为accept得到的
//buf       发送的数据
//len       发送的数据大小
//flags     一般设为0等同write,详情见info send
//从指定的socket接收数据,成功返回接收的数据的大小,失败返回-1设errno
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//sockfd    目标socket描述符,一般为accpet返回值
//buf       接收缓冲区
//len       接收的缓冲区大小
//flags     一般为0等同read
//初始化一个socket的连接,用在客户端,成功返回0,失败返回-1设errno
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd    客户端的scoket描述符
//addr      服务端的信息
//addrlen   addr的长度
//关闭socket文件描述符
int close(int sockfd);

3.7.3 简单示意

  服务端:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/select.h>

#define BUFF_SIZE 100
#define MAX_FD 1024

//程序本身并不严谨,为了演示只演示过程
int main()
{
    //创建socket
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sock_fd)
    {
        perror("socket create failed!\n");
        exit(-1);
    }

    //绑定服务器地址和端口
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8000);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int ret = bind(sock_fd, (struct sockaddr *)(&addr), sizeof(addr));
    if (ret == -1)
    {
        perror("bind server address failed!\n");
        exit(-1);
    }

    //监听socket
    ret = listen(sock_fd, 100);
    if (ret == -1)
    {
        perror("bind server address failed!\n");
        exit(-1);
    }

    char buffer[BUFF_SIZE] = {0};

    while (1)
    {
        struct sockaddr_in recv_addr;
        socklen_t len = sizeof(recv_addr);
        int client_fd = accept(sock_fd, (struct sockaddr *)&recv_addr, &len);
        if (client_fd == -1)
        {
            perror("client connect server failed!\n");
            exit(-1);
        }

        int ret = read(client_fd, buffer[fd], sizeof buffer[0]);
        if (0 == ret)
        { //客户端链接已经断开
            close(client_fd);
            continue;
        }

        int ret = write(fd, buffer[fd], sizeof buffer[0]);
        if(ret == 0)
        {
            perror("send failed!\n");
        }

        close(client_fd);
    }

    close(sock_fd);
    return 0;
}

  客户端:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>

int main(int argc, char *argv[])
{
    int sockfd;
    char sendbuffer[200];
    char recvbuffer[200];
    //  char buffer[1024];
    struct sockaddr_in server_addr;
    struct hostent *host;
    int portnumber, nbytes;
    if (argc != 3)
    {
        fprintf(stderr, "Usage :%s hostname portnumber\a\n", argv[0]);
        exit(1);
    }

    if ((host = gethostbyname(argv[1])) == NULL)
    {
        herror("Get host name error\n");
        exit(1);
    }

    if ((portnumber = atoi(argv[2])) < 0)
    {
        fprintf(stderr, "Usage:%s hostname portnumber\a\n", argv[0]);
        exit(1);
    }

    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        fprintf(stderr, "Socket Error:%s\a\n", strerror(errno));
        exit(1);
    }

    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(portnumber);
    server_addr.sin_addr = *((struct in_addr *)host->h_addr);
    if (connect(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) == -1)
    {
        fprintf(stderr, "Connect error:%s\n", strerror(errno));
        exit(1);
    }

    while (1)
    {
        printf("Please input your word:\n");
        scanf("%s", sendbuffer);
        printf("\n");
        if (strcmp(sendbuffer, "quit") == 0)
            break;
        send(sockfd, sendbuffer, sizeof(sendbuffer), 0);
        recv(sockfd, recvbuffer, 200, 0);
        printf("recv data of my world is :%s\n", recvbuffer);
    }

    close(sockfd);
    exit(0);
}

3.8 TCP黏包问题

  tcpip协议使用"流式"(套接字)进行数据的传输,就是说它保证数据的可达以及数据抵达的顺序,但并不保证数据是否在你接收的时候就到达,特别是为了提高效率,充分利用带宽,底层会使用缓存技术,具体的说就是使用Nagle算法将小的数据包放到一起发送,但是这样也带来一个使用上的问题——黏包,黏包就是说一次将多个数据包发送出去,导致接收方不能进行正常的解析。
  发生黏包一般有两种原因:

  • 发送方进行了不该缓冲的缓冲,比如上图中,收发双方协议好按照一定的规则进行编写/解析报文,但是由于Nagle算法,可能出现发送方一次发送了1.5个数据包,而接收方只解析了前面的1个包,后面的0.5个由于数据不完整而解析失败,造成数据的丢失或错位,很可能会影响之后所有的数据解析工作。由于发送方导致的黏包问题可以使用setsockopt()来解决,该api可以禁止发送方使用Nagle算法;
  • 接收方处理不当也可能导致黏包问题,如果发送方将4个包发送到接收方的缓冲区,但是由于频繁的存取,可能有一次只取了2.5个包,就会导致黏包问题。接收方的黏包问题可以使用recv(sockfd,buf,sizeof(buf),MSG_WAITALL)来解决,MSG_WAITALL可以强制接收方收到sizeof(buf)那么多的数据才返回,而buf的大小可以是收发双方约定好的大小。

4 UDP

  用户数据报协议(User Datagram Protocol,缩写:UDP;又称用户数据包协议)是一个简单的面向数据报的通信协议,位于OSI模型的传输层。该协议由David P. Reed(英语:David P. Reed)在1980年设计且在RFC 768中被规范。典型网络上的众多使用UDP协议的关键应用在一定程度上是相似的。

  在TCP/IP模型中,UDP为网络层以上和应用层以下提供了一个简单的接口。UDP只提供数据的不可靠传递,它一旦把应用程序发给网络层的数据发送出去,就不保留数据备份(所以UDP有时候也被认为是不可靠的数据报协议)。UDP在IP数据报的头部仅仅加入了复用和数据校验字段。

4.1 UDP Header

在这里插入图片描述

  • 报文长度:该字段指定UDP报头和数据总共占用的长度。取值范围为[8, 65535]字节;
  • 校验和:校验和字段可以用于发现头部信息和数据中的传输错误。该字段在IPv4中是可选的,在IPv6中则是强制的。如果不使用校验和,该字段应被填充为全0。

  当UDP运行在IPv4之上时,为了能够计算校验和,需要在UDP数据包前添加一个“伪头部”。伪头部包括了IPv4头部中的一些信息,但它并不是发送IP数据包时使用的IP数据包的头部,而只是一个用来计算校验和而已。
在这里插入图片描述

4.2 UDP和TCP的区别

  1. TCP面向连接,UDP无连接;
  2. TCP结构复杂,需要更多计算机资源,UDP结构简单,开销小;
  3. TCP流模式,UDP数据报模式;
  4. TCP能够保证数据的准确性,可达性和有序性,UDP无法保证;
  5. TCP只支持点对点,UDP同时支持点对点,点对多,多对多,多对一。

4.2 UDP Socket编程

4.2.1 过程

在这里插入图片描述

4.2.2 Socket API

  socket,bind,api,closeapi相同。

//从指定的socket和相应的地址接受消息,并提供来电显示的功能,成功返回实际接收的数据大小,失败返回-1设errno
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
//sockfd    当前socket文件描述符
//buf       接收缓冲区
//len       接收缓冲区大小
//flags
//src_addr  返回接收方的信息
//addrlen      src_addr的长度
//向指定的socket和相应的地址发送消息,成功返回实际发送数据的大小,失败返回-1设errno
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
//sockfd    当前socket文件描述符
//buf       发送缓冲区
//len       发送缓冲区大小
//flags
//dest_addr  发送方的信息
//addrlen      dest_addr的长度

4.2.3 简单的例子

  服务端:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>

//打印输入提示选项
static void *usage(const char *port)
{
    printf("usage: %s [local_ip] [local_port]\n", port);
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        return 1;
    }
    //创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        perror("socket");
        exit(1);
    }
    //将套接字与ip地址和端口号进行绑定
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[2]));
    local.sin_addr.s_addr = inet_addr(argv[1]);
    if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        perror("bind");
        exit(2);
    }
    char buf[1024];

    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    char *msg = "Have a goog day";
    while (1)
    {
        //读取数据
        int r = recvfrom(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&client, &len);
        if (r < 0)
        {
            perror("recvfrom");
            exit(3);
        }
        else
        {
            buf[r] = 0;
            printf("[%s : %d]#  %s\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), buf);

            //回送数据
            if (sendto(sock, msg, strlen(msg), 0, (struct sockaddr *)&client, len) < 0)
            {
                perror("sendto");
                exit(4);
            }
            break;
        }
    }
    return 0;
}

  客户端:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>

static void usage(const char *proc)
{
    printf("Usage: %s [locaal_ip] [local_port]\n", proc);
    exit(1);
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        return 1;
    }

    int sock = socket(AF_INET, SOCK_DGRAM, 0); //IPV4  SOCK_DGRAM 数据报套接字(UDP协议)
    if (sock < 0)
    {
        perror("socket\n");
        return 2;
    }

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);

    socklen_t len = sizeof(server_addr);
    char buf[1024];
    char *msg = "hello world";
    while (1)
    {
        if (sendto(sock, msg, strlen(msg), 0, (struct sockaddr *)&server_addr, len) < 0)
        {
            perror("send:");
            exit(3);
        }
        struct sockaddr_in tmp;
        len = sizeof(tmp);
        int ret = recvfrom(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&tmp, &len);
        if (ret > 0)
        {
            buf[ret] = 0;
            printf("server echo#:%s\n", buf);
            break;
        }
    }
    close(sock);
    return 0;
}

参考

维基-网际协议
维基-IPV4
维基-传输控制协议
TCP 为什么是三次握手,而不是两次或四次?
TCP三次握手四次挥手详解
TCP为什么需要3次握手与4次挥手
两张动图-彻底明白TCP的三次握手与四次挥手
用户数据报协议
TCP和UDP的最完整的区别
Ho sockets works
Linux IPC tcp/ip socket 编程
Linux IPC udp/ip socket 编程
TCP流量控制、拥塞控制
维基-TCP拥塞控制
Linux tcp黏包解决方案

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值