0%

TCP、UDP通信模型

本文主要位通信协议、网络体系结构、TCP/IP协议、常见包头等分析介绍使用。

前置

计算机网络的本质作用其实就是让IT设备之间能够进行数据传输和信息交互(通信)
最简单的通信模型如下:
Alt text

其中通信协议的作用就是 让通信的双方能够识别并能接收到数据信息。
Alt text

这种层次结构使得不同的协议可以在不同的层次上进行交互和搭配,而彼此之间不会产生不必要的干扰,提高了网络管理的效率和可靠性。

TCP/IP协议簇的体系结构

TCP/IP协议族是Internet中最重要的一组协议,并且广泛应用于各种计算机网络和系统之间的通信。
TCP/IP协议族(簇)的体系结构其实也是现实版的OSI体系,是现实的工业互联标准,共有四层:
Alt text

网络的体系结构

  • 网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。
  • 每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的,每层向上一层提供服务,同时使用下层提供的服务。
  • 网络体系结构即指网络层次和每层所使用的协议的集合。

常见的TCP/IP协议

按照四层模型,TCP/IP协议族具有以下四层:

  1. 应用层(Application Layer):应用层提供了与特定应用程序和网络服务的通信接口。任何需要从网络中获取数据的应用程序,如Web浏览器、邮件客户端、FTP客户端等,都是在应用层实现的。常见的应用层协议包括HTTP、FTP、SMTP、POP3等。

    • HTTP(HyperText Transfer Protocol)协议:用于Web服务器和Web浏览器之间互相传输超文本信息,实现浏览器访问万维网的功能。
    • SMTP(Simple Mail Transfer Protocol)协议:用于邮件传输协议,使邮箱能够发送和接收邮件。
    • POP3(Post Office Protocol version 3)协议:用于收取邮件,将服务器中的邮件下载到客户端计算机。
    • FTP(File Transfer Protocol)协议:用于在网络上传输文件,实现文件共享及远程文件传输等功能。
    • TFTP简单文件传输协议:使用UDP传输。
    • DNS解析协议:把url域名解析成ip地址的一种协议。
    • NFS文件共享协议:一般使用UDP传输。
  2. 传输层(Transport Layer):传输层为应用程序提供了端到端的通信,为不同主机的通信进程设定端口号,并将数据在不同主机之间传输。常见的传输层协议包括TCP、UDP等

    • TCP(Transmission Control Protocol)协议:一种可靠的、面向连接的协议,用于确保数据的可靠传输,保障数据的准确(数据无误,无丢失,无失序,无重复到达的通信)。
      • 优点:准确,缺点:传输效率不高。
      • 场景:数据要求严格的传输。
    • UDP(User Datagram Protocol)协议:一种不可靠的、无连接的协议,用于在不需要可靠数据传输时,提高数据传输效率。
      • 优点:传输效率高,缺点:可能会有丢包现象
      • 场景:对数据要求不高的场景。
  3. 网络层(Internet Layer):网络层用于实现IP协议,对数据包的传输进行路由选择和寻址,并通过向下一层提供不可靠的数据报送程序来实现传输。常见的网络层协议为IP(Internet Protocol)协议以及相关协议例如ICMP、ARP等。

    • IP(Internet Protocol)协议:IP协议使得不同的计算机可以互相通信,并能够在不同的网络之间传递数据包,实现了整个互联网的路由功能。是Internet中发送数据的关键协议之一。
    • ICMP(Internet Control Message Protocol)协议:用于在IP主机,路由器之间的传递控制消息,比如ping命令、网络是否可达等等。
    • IGMP (Internet Group Message Protocol)协议:互联网组管理协议,是一个组播协议,用于主机和组播路由之间的通信。
  4. 物理层和数据链路层(Link Layer):物理层和数据链路层用于实现与物理媒介的接口,将比特流转换为实际可传输的信号。它们是一组紧密相连的层,负责处理数据帧、网络接口卡(NIC)、帧的检错、流量控制等相应的操作。常见的物理层和数据链路层协议包括以太网、PPP、SLIP等。

    • ARP(Address Resolution Protocol)协议:将IP地址转换成物理地址,使一个通信节点(如计算机)能够定位另一个通信节点(如路由器)的物理地址。
    • RARP(Reverse Address Resolution Protocol) 协议:通过MAC地址获取对方的ip地址。

TCP/IP四层模型中不同层次使用的协议之间的关系

  1. 应用层:应用层负责应用程序数据的格式或者说协议,使用的协议有HTTPFTPSMTPDNS等;
  2. 传输层:传输层负责端到端的通信,包括数据的分段和重新组装,使用的协议有TCPUDP
  3. 网络层:网络层负责网络上的寻址、路由和分组传输,使用的协议有IPICMPARP
  4. 数据链路层:数据链路层负责物理网络上的数据传输,包括数据的分帧、差错校验和流量控制等,使用的协议有以太网PPP

因此,TCP/IP四层模型中每层使用的协议是相互独立、功能分层的,每个层次上使用的协议都为上一层提供服务(比如,如果应用层使用TFTP,那么上一层的网络传递协议选择就一定要选择UDP,因为TFTP不支持TCP,只支持UDP)。最终,所有协议协同工作,实现了网络中各种应用程序之间的通信。
Alt text

封包与拆包过程

Alt text

发送端:

  1. 应用层 - 发送端的应用程序将数据进行封装,将数据转换为网络可传输的格式,例如HTTP协议、FTP协议、SMTP协议等。

  2. 传输层 - 这个阶段选用TCP或UDP协议,根据应用层选择对应的协议,如果是可靠传输会选择TCP协议进行传输,而无需可靠传输的数据则会选择UDP协议,并设置好SERVER IP和端口号。

  3. 网络层 - 进行IP地址的解析,将数据封装成IP数据包,进行路由选择,通过路由器或交换机将数据包发送到互联网上。

  4. 数据链路层 - 将IP数据包封装在数据帧中,设置好目的MAC地址和源MAC地址,通过本机所连的交换机转发到接入的路由器。

  5. 物理层 - 讲数字信号转换成模拟信号,在传输线上进行数据传输,利用调制、编码、解码、调幅等技术进行信号转换和传输。

接收端:

  1. 物理层 - 接收方收到模拟信号后,将模拟信号转换为数字信号。

  2. 数据链路层 - 将收到的数据帧中的MAC头和数据载荷分离,每一帧内找出自己的MAC地址,校验数据帧的数据完整性和正确性。

  3. 网络层 - 进行IP协议解析,从IP数据包中提取出源IP地址和目标IP地址。

  4. 传输层 - 进行TCP协议解析,判断数据是否为可靠传输,根据TCP头部信息中的端口号,将数据传输给对应的应用层进行处理。

  5. 应用层 - 最终数据将由接收方的应用程序进行处理,解码并完成相关操作。

  6. 回传信息 - 接收方应用程序处理过程中或处理完成后,可发送一条ACK信息表示数据已接收成功。

注意 在整个数据传输过程中,可能会遇到不同的网络问题,例如网络试验慢、数据包丢失等,因此需要利用相应的协议和技术进行性能优化和错误处理来确保数据传输的正确和稳定。

数据帧

TCP/IP通信中,一帧数据的大小是由底层传输介质的MTUMaximum Transmission Unit,最大传输单元)决定的。不同的网络设备和网络传输介质有不同的MTU,比如以太网的MTU一般为1500字节,而无线网络的MTU一般较小。

在发送数据时,TCP协议会将数据分割成多个大小相等的IP数据包(IP数据报文),每个数据包的大小不能超过MTU。在传输过程中,这些数据包在不同的网络设备之间被逐一传递,直到到达目的地,并在目的地重新组装成完整的数据。因此,TCP/IP通信中一帧数据的大小是不固定的,而是由网络设备的MTU所决定的。

传输每帧数据包的大小一般为:46~1500字节,在Linux中可以通过ifconfig进行查看,但传输过程中以整个传输过程中的最小通路为准。
Alt text

TCP段,如果是UDP则占8个字节。

所以:

UDP 包MTU的大小就应该是 1500 - IP头(20) - UDP头(8) = 1472(Bytes)
TCP 包MTU的大小就应该是 1500 - IP头(20) - TCP头(20) = 1460 (Bytes)

数据帧的封装

数据从应用层向下传递,到达传输层时,为应用层数据,如果采用TCP传输则添加TCP报头,如果采用UDP传输则添加UDP报头,关于TCP报头及UDP报头在第二节与第三节均进行了介绍。当数据到达网络层时,则要再添加IP头。当到过数据链路层与物理层时,则要再添加MAC头。

IP 头

Alt text

  1. **版本**:IP协议的版本。通信双方使用过的IP协议的版本必须一致,目前最广泛使用的IP协议版本号为34(即IPv4)
  2. **首部长度**:单位是4倍(共4bit)比如表示IP头为20个字节,那么就是5. 即0101四位。
  3. **服务类型**:一般不使用,取值为0。前3位:优先级,第4-7位:延时,吞吐量,可靠性,花费。第8位保留
  4. **总长度**:指首部加上数据的总长度,单位为字节。最大长度为65535字节。
  5. 标识(identification):用来标识主机发送的每一份数据报。IP软件在存储器中维持一个计数器,每产生一个数据报,计数器就加1,并将此值赋给标识字段。
  6. **标志**(flag):目前只有两位有意义。标志字段中的最低位记为MF。MF=1即表示后面“还有分片”的数据报。MF=0表示这已是若干数据报片中的最后一个。标志字段中间的一位记为DF,意思是“不能分片”,只有当DF=0时才允许分片
  7. **片偏移**:指出较长的分组在分片后,某片在源分组中的相对位置,也就是说,相对于用户数据段的起点,该片从何处开始。片偏移以8字节为偏移单位。
  8. **生存时间**:TTL,表明是数据报在网络中的寿命,即为“跳数限制”,由发出数据报的源点设置这个字段。路由器在转发数据之前就把TTL值减一,当TTL值减为零时,就丢弃这个数据报。通常设置为32、64、128。
  9. **协议**:指出此数据报携带的数据时使用何种协议,以便使目的主机的IP层知道应将数据部分上交给哪个处理过程,常用的ICMP(1),IGMP(2),TCP(6),UDP(17),IPv6(41)
  10. **首部校验和**:只校验数据报的首部,不包括数据部分。
  11. **源地址**:发送方IP地址
  12. **目的地址**:接收方IP地址
  13. **选项**:用来定义一些任选项;如记录路径、时间戳等。这些选项很少被使用,同时并不是所有主机和路由器都支持这些选项。一般忽略不计

MAC 头

Alt text

CRC即循环冗余校验码:是数据通信领域中最常用的一种查错校验码,其特征是信息字段和校验字段的长度可以任意选定。循环冗余检查是一种数据传输检错功能,对数据进行h多项式计算,并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证数据传输的正确性和完整性。

在这个数据帧中,默认一帧数据最大为为1518。

TCP

TCP 封装格式

在应用层的数据在发生传递会在传输层,如果选择的TCP传输,将会为这个应用层数加前加上TCP报头,TCP报头如下:
Alt text

  1. **源端口号**:发送方端口号
  2. **目的端口号**:接收方端口号
  3. **序列号**:本报文段的数据的第一个字节的序号,使用小写seq表示
  4. **确认序号**:期望收到对方下一个报文段的第一个数据字节的序号,使用小写ack表示
  5. **首部长度(数据偏移)**:4个bit,TCP报文段的数据起始处距离TCP报文段的起始处有多远,即首部长度。即以4字节为计算单位,取值通常为 5~15
  6. **保留**:占6位,保留为今后拓展使用,目前应置为0.
  7. 紧急URG: 此位置1,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送
  8. 确认ACK: 仅当ACK=1时确认号字段才有效,TCP规定,在连接建立后所有传达的报文段都必须把ACK置1
  9. **推送PSH**:当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作,这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去,接收方收到PSH=1的报文段,就尽快地(即“推送”向前)交付给接收应用进程,而不再等到整个缓存都填满后再向上交付
  10. 复位RST: 用于复位相应的TCP连接时会置1
  11. 同步SYN: 仅在三次握手建立TCP连接时有效。当SYN=1而ACK=0时,表明这是一个连接请求报文段,对方若同意建立连接,则应在相应的报文段中使用SYN=1和ACK=1.因此,SYN置1就表示这是一个连接请求或连接接受报文。
  12. **终止FIN**:用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已经发送完毕,并要求释放运输连接。
  13. **窗口**:用于控制数据传输的速度和流量控制。窗口大小表示接收端当前可用的缓存大小,即接收端还可以接收多少字节的数据。这个值是一个16位的整数,可以表示的窗口大小为 0~65535 字节。当发送方发送数据时,它会根据接收方的窗口大小动态地调整发送的数据量,以避免接收方缓存区满了而导致数据丢失。发送方会根据接收方发送过来的窗口大小调整发送的数据量,如果窗口大小变小,发送方会减少发送的数据量,如果窗口大小变大,发送方则可以增加发送的数据量。TCP 的窗口大小控制可以防止数据包过多拥塞网络,也可以保证数据传输的流畅性和可靠性。在数据传输过程中,如果接收方的窗口大小为 0,发送方就会停止发送数据,等待接收方重新通知其窗口大小。这种流量控制的机制可以使得 TCP 在高速、高负载的网络中稳定运行,并有效地避免了数据的丢失和拥塞。
  14. **校验和**:校验和字段用于检验 TCP 报头和数据段的数据是否正确,以保证数据传输的正确性和可靠性。这个字段是 16 位的整数。
  15. **紧急指针**:仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据),即指出了紧急数据的末尾在报文中的位置,注意:即使窗口为零时也可发送紧急数据
  16. **选项**:长度可变,最长可达40字节,当没有使用选项时,TCP首部长度是20字节

TCP 三次握手

TCP 使用三次握手(three-way handshake)的方式来建立一个连接,以确保通信双方都能够正确地接收和发送数据。
发生在客户端connect函数 与 服务器的 accepet 函数之前:

三次握手的过程如下:

  1. 客户端发送一个带有 SYN(同步序列号Synchronize Sequence Numbers)标志的数据包,表示客户端请求建立连接。客户端选择一个随机的序列号(序列号 A),并将该序列号放在 SYN 标志的字段中,然后向服务器端发送这个数据包。此时客户端进入 SYN_SENT 状态。
  2. 服务器收到客户端的请求后,会回复一个带有 SYN/ACK 标志的数据包,表示服务器已经收到客户端的请求,并同意建立连接。服务器选择一个随机的序列号(序列号 B),并将该序列号放在 SYN 标志的字段中,将自己的初始序列号(序列号 A + 1)放在 ACK 标志的字段中,然后将数据包发送给客户端。此时服务器进入 SYN_RCVD 状态。
  3. 客户端收到服务器的回复后,会发送一个带有 ACK 标志的数据包,表示客户端确认已经收到了服务器的响应,并同意建立连接。客户端将服务器的初始序列号(序列号 B + 1)放在 ACK 标志的字段中,然后将数据包发送给服务器。此时客户端和服务器都进入 ESTABLISHED 状态,连接建立完成。

三次握手的过程可以确保客户端和服务器都已经准备好建立连接,并可以正确地发送和接收数据。如果服务器没有收到客户端的请求,或者客户端没有收到服务器的响应,则会重新发送请求,直到建立连接成功。三次握手的机制可以有效地保证数据的可靠性和安全性,同时也避免了重复的连接请求和资源浪费。

Alt text

TCP 四次挥手

Alt text

  1. 客户端发送一个带有 FIN(结束标志)的数据包,表示客户端已经完成了所有的数据发送任务,并且准备关闭连接。客户端选择一个随机的序列号(序列号 C),并将该序列号放在 FIN 标志的字段中,然后向服务器端发送这个数据包。此时客户端进入 FIN_WAIT_1 状态。
  2. 服务器收到客户端的请求后,会回复一个带有 ACK 标志的数据包,表示服务器已经收到客户端的请求,并同意关闭连接。服务器将客户端的序列号(序列号 C + 1)放在 ACK 标志的字段中,然后将数据包发送给客户端。此时服务器进入 CLOSE_WAIT 状态,客户端收到服务器的回复后,进入 FIN_WAIT_2 状态。
  3. 服务器准备好关闭连接时,会发送一个带有 FIN 标志的数据包,表示服务器已经完成了所有的数据发送任务,并且准备关闭连接。服务器选择一个随机的序列号(序列号 D),并将该序列号放在 FIN 标志的字段中,将客户端的序列号(序列号 C + 1)放在 ACK 标志的字段中,然后将数据包发送给客户端。此时服务器进入 LAST_ACK 状态。
  4. 客户端收到服务器的回复后,会回复一个带有 ACK 标志的数据包,表示客户端已经收到服务器的回复,并同意关闭连接。客户端将服务器的序列号(序列号 D + 1)放在 ACK 标志的字段中,然后将数据包发送给服务器。此时客户端进入 TIME_WAIT 状态,等待 2MSL(最长报文段寿命)后,客户端和服务器都进入 CLOSED 状态,连接关闭完成。
  5. 服务器收到客户端的回复后,进入 CLOSED 状态,连接关闭完成。

四次挥手的过程可以确保客户端和服务器都已经完成了所有的数据发送任务,并且准备好关闭连接。如果客户端没有收到服务器的回复,则会重新发送请求,直到关闭连接成功。

粘包问题

由于TCP是基于字节流的,因此在发送端,只要把数据放入到发送缓冲区,就会发送出去,而不考虑数据包的大小。这就可能导致接收端收到的数据包大于发送端的数据包,也可能导致接收端收到的数据包小于发送端的数据包。这种情况就称为粘包。

字节流可以理解为一个双向的通道里流淌的数据,这个数据其实就是我们常说的二进制数据,简单来说就是一大堆 01 串。这些 01 串之间没有任何边界。

内核中的缓冲区
Socket 内核缓冲区是指在操作系统内核中维护的一块内存,用于存储网络数据的临时缓存区。当应用程序通过 Socket 发送数据时,数据先被写入内核缓冲区,然后再由操作系统将数据发送到网络上。类似地,当应用程序从 Socket 接收数据时,数据也是先被读取到内核缓冲区中,然后再由应用程序读取。

内核缓冲区的作用是为了提高网络传输的效率和可靠性。当应用程序发送数据时,内核缓冲区可以将多个小数据包合并成一个大的数据包,从而减少网络传输中的开销。而当应用程序接收数据时,内核缓冲区可以对接收到的数据进行拆分和重组,以保证数据的完整性和正确性。

操作系统为每个 Socket 连接分配了两个内核缓冲区,一个用于发送数据,另一个用于接收数据。这些缓冲区的大小可以通过系统参数进行配置,一般情况下,应该根据实际情况合理调整缓冲区大小,以达到最佳的网络传输性能。

Alt text

应用层传到 TCP 协议的数据,不是以消息报为单位向目的主机发送,而是以字节流的方式发送到下游,这些数据可能被切割和组装成各种数据包,接收端收到这些数据包后没有正确还原原来的消息,因此出现粘包现象。比如:在传输过程中,如果一端短段时间内发送多个数据包,为了提高传输效率内核中的Naggle算法,会将多个小的数据合并为一个较大的数据进行传输,可能就会数据粘连的问题。

产生的原因

关于Nagle算法导致的粘包问题

MTU: Maximum Transmit Unit,最大传输单元。 由网络接口层(数据链路层)提供给网络层最大一次传输数据的大小;一般 MTU=1500 Byte。
假设IP层有 <= 1500 byte 需要发送,只需要一个 IP 包就可以完成发送任务;假设 IP 层有> 1500 byte 数据需要发送,需要分片才能完成发送,分片后的 IP Header ID 相同。
MSS:Maximum Segment Size 。 TCP 提交给 IP 层最大分段大小,不包含 TCP Header 和 TCP Option,只包含 TCP Payload ,MSS 是 TCP 用来限制应用层最大的发送字节数。

假设 MTU= 1500 byte,那么 MSS = 1500- 20(IP Header) -20 (TCP Header) = 1460 byte,如果应用层有 2000 byte 发送,那么需要两个切片才可以完成发送,第一个 TCP 切片 = 1460,第二个 TCP 切片 = 540。
在 Nagle 算法开启的状态下,数据包在以下两个情况会被发送:

如果包长度达到MSS(或含有Fin包),立刻发送,否则等待下一个包到来;如果下一包到来后两个包的总长度超过MSS的话,就会进行拆分发送;
等待超时(一般为200ms),第一个包没到MSS长度,但是又迟迟等不到第二个包的到来,则立即发送。

解决方案

  1. **定长消息**:在发送数据之前,将数据按照固定长度分割成多个数据包发送,接收方每次接收一个固定长度的数据包,从而避免了粘包问题。(缺点:虽然这种方式可以解决粘包和拆包的问题,但这种固定缓冲区大小的方式增加了不必要的数据传输;当这种方式当发送的数据比较小时会使用空字符来弥补,所以这种方式就大大的增加了网络传输的负担,所以它也不是最佳的解决方案。)
  2. **特定字符分割**:在发送数据时,在每个数据包的结尾添加特定的字符或标记,接收方在接收数据时,根据特定字符或标记切分数据包,从而避免了粘包问题。(缺点是报文中不能有相同的字符或标记,不常用)
  3. **消息长度**:在发送数据时,在每个数据包的开头添加表示消息长度的字段,接收方先读取消息长度,然后根据消息长度读取数据包,从而避免了粘包问题。(网络框架Netty所用的解决方案)
  4. **利用应用层协议**:在应用层协议中定义消息格式和消息长度,从而避免TCP传输中的粘包问题。

注意

  1. 发送方和接收方需要使用相同的方法,以保证数据能够正确地发送和接收。
  2. 在发送数据时,需要注意数据包的大小,不宜过大或过小,以避免传输效率低下或造成拥堵。
  3. 在接收数据时,需要及时读取数据,不要等待所有数据都接收完成再进行处理,以避免缓存溢出或处理延迟过大。

Netty中的解决方案 - 消息长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>

int write_N(int fd, char* buf, int newMsgLen)
{
if(buf == NULL)
{
return -1;
}
int n = newMsgLen;
while(n > 0)
{
int len = write(fd,buf,n);
if(len == -1)
{
perror("write err:");
return -1;
}
buf += len;
n -= len;
}
return newMsgLen;
}

int writeToSocket(int fd,char* buf, int len)
{
if(buf == NULL)
{
return -1;
}
//1.获取要发送的报文的长度。
int MsgLen = strlen(buf);
//2.把报文件长度设置为网络字节序:
int MsgLen_netOrder = htonl(MsgLen);

char senderBuf[4 + MsgLen];
memset(senderBuf,0,sizeof(senderBuf));
//3.通过内存拷贝的方式把数据放在有消息长度的用户缓冲区:
memcpy(senderBuf,&MsgLen_netOrder,4);
//4.拷贝报文:
memcpy(senderBuf+4,buf,MsgLen);
int nbytes = write_N(fd,senderBuf,4 + MsgLen);
return nbytes;
}

int main(int argc, char const *argv[])
{
//1.创建流式套接字:
int socket_fd = socket(AF_INET,SOCK_STREAM,0);
if(socket_fd == -1)
{
perror("socket err:");
return -1;
}
//2.创建一个网络信息结构体:
struct sockaddr_in serverInfo = {0};
serverInfo.sin_family = AF_INET;
serverInfo.sin_port = htons(6666);
serverInfo.sin_addr.s_addr = inet_addr("192.168.250.100");

//使用connect函数连接主机:
int ret = connect(socket_fd,(const struct sockaddr*)&serverInfo,sizeof(serverInfo));
if(ret == -1)
{
perror("connect err:");
return -1;
}
const char* str = "hellowrold";
for(int i = 0; i < 30; i++)
{
int nbytes = writeToSocket(socket_fd,str,strlen(str));
if(nbytes == -1)
{
return -1;
}
printf("%s \n",str);
}
close(socket_fd);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>

int read_N(int fd, char* buf, int MsgLen)
{
if(buf == NULL)
{
return -1;
}
int n = MsgLen;
while (n > 0)
{
int nbytes = read(fd,buf,n);
if(nbytes == -1)
{
perror("read err");
return -1;
}
if(nbytes == 0)
{
printf("对端关闭\n");
return 0;
}
buf += nbytes;
n -= nbytes;
}
return MsgLen;

}

int readFromSocket(int fd, char* buf)
{
if(buf == NULL)
{
return -1;
}
int msgLen = 0;
//1.先读取报文件前4个字节,报文件的长度:
int nbytes = read(fd,&msgLen,4);
//2.把读取网络字节序转成主机字节序
msgLen = ntohl(msgLen);
nbytes = read_N(fd,buf,msgLen);
//返回读取字节数:
return nbytes;
}

int main(int argc, char const *argv[])
{
// 1.创建流式监听套接字类型:
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1)
{
perror("socket() err:");
return -1;
}
// 2.定义一个网络地址信息结构体:
struct sockaddr_in serverInfo;
memset(&serverInfo, 0, sizeof(serverInfo));
serverInfo.sin_family = AF_INET;
// 端口号都是2字节,short类型,所以一定要注意字节序的问题:
serverInfo.sin_port = htons(6666);
serverInfo.sin_addr.s_addr = inet_addr("192.168.250.100");
// 3.绑定网络地址信息结构体:
int ret = bind(listen_fd, (const struct sockaddr *)&serverInfo, sizeof(serverInfo));
if (ret == -1)
{
perror("bind err:");
return -1;
}
// 4.设置监听的状态:
ret = listen(listen_fd, 1);
if (ret == -1)
{
perror("listen err:");
return -1;
}
// struct sockaddr clientInfo = {0};//如果关于客户端地址信息就写一个客户端的信息结构体。
// int clientlen;
// 5.阻塞等待客户端的连接:
printf("循环服务器启动\n");
while (true)
{
int connect_fd = accept(listen_fd, NULL, NULL);
if (connect_fd == -1)
{
perror("accept err:");
return -1;
}
while (true)
{
// 数据的收发:
// 定义用户数据的缓冲区buf;
char buf[128] = {0};
int nbtyes = readFromSocket(connect_fd, buf);
if (nbtyes == -1)
{
perror("read err:");
return -1;
}
if(nbtyes == 0)
{
printf("对端断开了连接\n");
close(connect_fd);
break;
}
printf("客户端发来的数据为 %s \n",buf);
}
}

return 0;
}

注意 在TCP链接之后,如果断开之后,会出现短时间无法链接的情况

TCP是一个面向连接的协议,它使用三次握手建立连接,并使用四次挥手断开连接。在断开连接后,TCP会将连接的端口号等相关信息保存在一个连接表中,并将其保留一段时间。这个时间被称为TIME_WAIT状态,它通常持续几分钟,这段时间内,连接表中的信息仍然存在,以确保网络中任何延迟的数据包到达时,TCP可以正确处理。

当TCP连接关闭后,客户端和服务器端的TCP实现都会进入TIME_WAIT状态。在此状态下,这些连接表项不能被重新使用,因为TCP实现正在等待确保另一端确认连接已关闭。如果尝试在TIME_WAIT状态下重新建立TCP连接,会导致建立连接失败,因为TCP实现不能使用相同的端口和连接信息。

另外,如果连接的一方关闭了连接,而另一方尝试向已关闭的连接发送数据,那么TCP实现会返回一个RST数据包,指示连接已经关闭。因此,在关闭TCP连接后,如果尝试立即重新连接,可能会由于RST数据包而导致连接失败。

因此,要成功重新建立TCP连接,需要等待足够的时间使TIME_WAIT状态结束,或者在关闭连接后等待一段时间后再尝试重新连接。

UDP

无连接的,固定数据大小,不可靠的用户数据报协议。

Alt text

收发数据

recv / recvfrom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <sys/types.h>
#include <sys/socket.h>

//read函数的升级版,因为read默认是一个阻塞函数。
//recv函数多了一个flags 是指使用阻塞读取,还是使用非阻塞读取。如果是阻塞则为0,非阻塞则为:MSG_DONTWAIT
//recvfrom函数多了源地址结构体,类似来电显示的功能。可以获取源地址。

/*
* @功能:接收数据
* @参数:
* @sockfd: 套接字描述符
* @buf: 接收数据的缓冲区
* @len: 接收数据的长度
* @flags: 0:阻塞读取,MSG_DONTWAIT:非阻塞读取
* @返回值:成功:返回读取的字节数,失败:-1
*/
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

/*
* @功能:接收数据
* @参数:
* @sockfd: 套接字描述符
* @buf: 接收数据的缓冲区
* @len: 接收数据的长度
* @flags: 0:阻塞读取,MSG_DONTWAIT:非阻塞读取
* @src_addr: 源地址结构体
* @addrlen: 源地址结构体的长度
* @返回值:成功:返回读取的字节数,失败:-1
*/
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

send / sendto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <sys/types.h>
#include <sys/socket.h>
//write函数的升级版,因为write默认是阻塞函数,即写满时,阻塞等待对方的读取之后再解除阻塞。
//send多了一个flags:阻塞为0,非阻塞为:MSG_DONTWAIT
//sendto双多了一个目的地址,即,数据将要发向哪一个ip地址的主机。

/*
* @功能:发送数据
* @参数:
* @sockfd: 套接字描述符
* @buf: 发送数据的缓冲区
* @len: 发送数据的长度
* @flags: 0:阻塞发送,MSG_DONTWAIT:非阻塞发送
* @返回值:成功:返回发送的字节数,失败:-1
*/
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

/*
* @功能:发送数据
* @参数:
* @sockfd: 套接字描述符
* @buf: 发送数据的缓冲区
* @len: 发送数据的长度
* @flags: 0:阻塞发送,MSG_DONTWAIT:非阻塞发送
* @dest_addr: 目的地址结构体
* @addrlen: 目的地址结构体的长度
* @返回值:成功:返回发送的字节数,失败:-1
*/
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);

UDP 服务器

Alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

int main(int argc, char const *argv[])
{
//构建UDP服务器:
//1.构建用户数据报套接字:
int udp_fd = socket(AF_INET,SOCK_DGRAM,0);
if(udp_fd == -1)
{
perror("socket err:");
return -1;
}
//构建网络地址信息结构体:
struct sockaddr_in udp_serverInfo = {0};
udp_serverInfo.sin_family = AF_INET;
udp_serverInfo.sin_port = htons(8888);
udp_serverInfo.sin_addr.s_addr = INADDR_ANY;
//2.绑定udp网络地址信息结构体:
int ret = bind(udp_fd,(struct sockaddr*)&udp_serverInfo,sizeof(udp_serverInfo));
if(ret == -1)
{
perror("bind err:");
return -1;
}

//定义一个用户数据缓冲区:
char buf[128] = {0};

//定义一个udp客户端的地址信息结构体:
struct sockaddr_in udp_clientInfo = {0};
int len = sizeof(udp_clientInfo);
printf("udp服务器启动\n");
while(true)
{
memset(buf,0,sizeof(buf));
//接收数据:
int nbytes = recvfrom(udp_fd,buf,sizeof(buf)-1,0,(struct sockaddr*)&udp_clientInfo,&len);
if(nbytes == -1)
{
perror("recvfrom err:");
return -1;
}
printf("udp客户端发来的数据:%s \n",buf);
//回显客户端:发出数据:
nbytes = sendto(udp_fd,buf,strlen(buf),0,(struct sockaddr*)&udp_clientInfo,len);
if(nbytes == -1)
{
perror("write err:");
return -1;
}

}
return 0;
}

UDP 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

int main(int argc, char const *argv[])
{
//构建UDP服务器:
//1.构建用户数据报套接字:
int udp_fd = socket(AF_INET,SOCK_DGRAM,0);
if(udp_fd == -1)
{
perror("socket err:");
return -1;
}
//2.构建网络地址信息结构体:
struct sockaddr_in udp_serverInfo = {0};
udp_serverInfo.sin_family = AF_INET;
udp_serverInfo.sin_port = htons(8888);
udp_serverInfo.sin_addr.s_addr = inet_addr("192.168.250.100");

//3.定义一用户数据缓冲区:
char buf[128] = {0};
while(true)
{
memset(buf,0,sizeof(buf));
printf("请输入你要发送的udp数据:\n");
fgets(buf,sizeof(buf),stdin);
//发送数据:sendto:
int nbytes = sendto(udp_fd,buf,strlen(buf),0,(struct sockadd*)&udp_serverInfo,sizeof(udp_serverInfo));
if(nbytes == -1)
{
perror("sendto err:");
return -1;
}
memset(buf,0,sizeof(buf));
//接收数据:recvfrom:
nbytes = recvfrom(udp_fd,buf,sizeof(buf)-1,0,NULL,NULL);
if(nbytes == -1)
{
perror("recvfrom err:");
return -1;
}
printf("对端发来的数据为:%s \n",buf);
}
return 0;
}