专业编程基础技术教程

网站首页 > 基础教程 正文

常用网络协议整理笔记(一)

ccvgpt 2025-01-11 10:50:18 基础教程 3 ℃

本文索引:

1. 网络协议整体框架

常用网络协议整理笔记(一)

2. 以太网帧/IP/UDP/TCP帧格式

3. ARP/RARP

4. ICMP/IGMP

5. PING

6. trace

7. BOOTP/DHCP

8. DNS


一、网络协议整体框架:

1. 定义:

网络协议的作用是使不同类型的网络能够相互通信。它提供了一个通用的框架,允许各种设备和应用程序在不同的网络环境中进行数据交换。 其核心在于其分层结构,每一层都负责特定的任务,从而简化了网络的设计和管理。 共有多种不同的分层方法。 本文重点说明下TCP/IP四层协议体系结构。

2. 内容:

TCP/IP协议栈并非单一协议,而是一组协议的集合,构成网络通信的基础。它是一个分层模型,通常被描述为四层模型,虽然实际实现可能更复杂。包括:链路层、网络层、传输层和应用层;分层结构如下:

1)链路层 (Network Access Layer / Link Layer):

a. 作用: 负责在物理网络上传输数据。它处理物理介质的细节,例如电缆或无线信号。

b. 内容: 这一层与具体的物理网络技术相关,例如:

---Ethernet: 局域网技术

---Wi-Fi: 无线局域网技术

---PPP (Point-to-Point Protocol): 点到点连接技术。

2) 网络层 (Internet Layer):

a.作用: 负责在网络之间路由数据包。它使用IP地址来寻址,并决定数据包如何从源主机到达目标主机。

b. 内容:

主要协议有

---IP (Internet Protocol): 定义了数据包的格式和寻址方式,负责数据包在网络中的路由。 IPv4和IPv6是两种主要的IP版本。

---ICMP (Internet Control Message Protocol): 用于网络诊断,例如ping命令

---ARP (Address Resolution Protocol): 将IP地址转换为MAC地址。

---RARP (Reverse Address Resolution Protocol): 将MAC地址转换为IP地址。

3) 传输层 (Transport Layer)

a. 作用: 提供端到端的可靠数据传输。它负责将数据分割成数据包,并确保这些数据包按顺序到达目的地。

b. 内容: 主要包含两个协议:

---TCP (Transmission Control Protocol): 面向连接、可靠的传输协议,提供流量控制、拥塞控制和错误校验。确保数据包的顺序和完整性

---UDP (User Datagram Protocol): 无连接、不可靠的传输协议,速度快但没有错误校验和顺序保证。常用于实时应用,例如视频流和在线游戏。

4) 应用层 (Application Layer):

a. 作用: 提供网络应用程序使用的接口。 它负责与用户交互,并决定数据如何被处理和传输。

b. 内容: 包含各种网络应用协议,例如:

---HTTP (Hypertext Transfer Protocol): 用于网页浏览。

---HTTPS (Hypertext Transfer Protocol Secure): 安全版本的HTTP,使用SSL/TLS加密。

---FTP (File Transfer Protocol): 用于文件传输。

---SMTP (Simple Mail Transfer Protocol): 用于电子邮件发送。

---POP3 (Post Office Protocol version 3) / IMAP (Internet Message Access Protocol): 用于电子邮件接收。

---DNS (Domain Name System): 将域名转换为IP地址。

---DHCP (Dynamic Host Configuration Protocol): 自动分配IP地址。


二、以太网帧/MAC帧/IP/UDP/TCP包格式

1.以太网帧/MAC帧:

1) 作用:

以太网帧定义了如何在以太网网络中传输数据。以太网帧主要由数据链路层(Data Link Layer)物理层(Physical Layer)负责,它封装了从上层协议传来的数据,并在局域网中传输。具体作用如下:

a. 数据封装与传输: 以太网帧将网络层(如IP层)传来的数据进行封装,形成数据帧,并通过网络媒介(如电缆或无线信号)进行传输。

b. 寻址与路由: 以太网帧包含了源和目标的MAC地址,用于在局域网内找到正确的接收设备。

c. 错误检测: 以太网帧还包含了错误检查字段(CRC),用于检测数据在传输过程中是否发生了错误。

2) 以太网帧格式:

各字段的具体说明如下:

字段

长度

说明

前导码(Preamble)

7字节

用于同步发送方和接收方的时钟,确保数据的正确接收。前导码由固定的“10101010...”组成。

帧起始定界符(SFD)

1字节

标识帧的开始,紧接着前导码后面的一字节10101011。

目标MAC地址

6字节

目标设备的MAC地址

源MAC地址

6字节

发送设备的MAC地址

类型字段(Type)

2字节

表示上层协议类型。常见的有:

0x0800: 表示封装IPv4数据。
0x0806: 表示封装的是ARP(地址解析协议)数据。
0x86DD: 表示封装的是IPv6数据。

数据和填充

46-1500字节

以太网帧的主要部分,包含了上层协议的数据。数据部分的最小长度为46字节,最大长度为1500字节。如果数据部分小于46字节,则需要通过填充(Padding)来填补,直到达到最小长度。

CRC校验

4字节

用于错误检测


3) 以太网、802.3和802.2的关系

---以太网协议:是一个广义的术语,指代局域网中使用的技术,是数据链路层的一种方式。包括媒体访问控制(MAC层)和逻辑链路控制(LLC层);

---IEEE 802.3: 是以太网的具体标准,定义了物理层和MAC子层的实现。主要定义了局域网的物理层和数据链路层的MAC(媒体访问控制)子层。它描述了如何在物理介质上进行数据传输,包括帧的格式、传输速率、介质类型等。

---IEEE 802.2:定义了数据链路层的逻辑链路控制(LLC)子层。它位于MAC子层之上,提供了对上层协议的支持,使得不同的网络协议可以在同一物理网络上共存。

几者间的关系即:

4)使用场景:

MAC帧用于在局域网内的设备之间传输数据。每个设备都有一个唯一的MAC地址,MAC帧使用这些地址来确保数据到达正确的目的地。

网络协议栈中的数据在传输前被封装成MAC帧,并在接收时被解封装。MAC帧提供了数据链路层的封装格式。

5)示例代码:

下面是一个使用C语言构建和发送以太网MAC帧的示例代码。这个示例使用原始套接字(Raw Socket)来创建和发送MAC帧。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <net/ethernet.h>
#include <net/packet.h>
#include <net/if.h>
#include <sys/ioctl.h>

#define ETH_FRAME_LEN 1514  // 最大以太网帧长度


int main (int arc, char **argv) {
    int sockfd;
    struct sockaddr_ll sa_ll;
    unsigned char frame[ETH_FRAME_LEN];
    unsigned char dest_mac[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};  // 广播地址
    unsigned char src_mac[6] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};  // 源MAC地址
    const char *data = "Hello, Ethernet!";
    
    // 创建原始套接字
    sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sockfd < 0) {
        perror("Socket creation failed");
        return 1;
    }
    
    // 获取网络接口索引(比如eth0的索引)
    struct ifreq ifr;
    strncpy((char *)ifr.ifr_name, "eth0", IFNAMSIZ);  // "eth0"可以修改为实际的网络接口
    if (ioctl(sockfd, SIOCGIFINDEX, &ifr) < 0) {
        perror("ioctl failed");
        close(sockfd);
        return 1;
    }
    int ifindex = ifr.ifr_ifindex;

    // 填充以太网帧
    memcpy(frame, dest_mac, 6);    // 目的MAC地址
    memcpy(frame + 6, src_mac, 6); // 源MAC地址
    frame[12] = 0x08;              // Type字段(IPv4)
    frame[13] = 0x00;
    memcpy(frame + 14, data, strlen(data)); // 数据负载

    // 填充 sockaddr_ll 结构
    memset(&sa_ll, 0, sizeof(struct sockaddr_ll));
    sa_ll.sll_protocol = htons(ETH_P_IP); // 设置协议为IPv4
    sa_ll.sll_ifindex = ifindex;          // 设置接口索引
    sa_ll.sll_halen = ETH_ALEN;           // 硬件地址长度(6字节)
    memcpy(sa_ll.sll_addr, dest_mac, 6);  // 设置目的MAC地址

    // 发送数据
    if (sendto(sockfd, frame, 14 + strlen(data), 0, (struct sockaddr*)&sa_ll, sizeof(struct sockaddr_ll)) < 0) {
        perror("Send failed");
        close(sockfd);
        return 1;
    }

    printf("Frame sent successfully\n");

    close(sockfd);
    return 0;
}

代码说明:

a. socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)):

创建一个原始套接字,使用 AF_PACKET 地址族,SOCK_RAW 类型,协议为 ETH_P_ALL(表示所有以太网协议类型)。

b. struct ifreq ifr:

使用 ioctl 获取指定网络接口(如 eth0)的接口索引 (ifindex),这个索引需要填充到 sockaddr_ll 结构中。

c. 填充 MAC 地址和数据帧:

设置帧的目的MAC地址、源MAC地址、类型字段(例如IPv4类型 0x0800)以及数据负载。

d. sendto() 函数:

使用 sendto 发送以太网帧,传递给 sockaddr_ll 结构体,其中包括接口索引、协议类型和目标MAC地址。

struct sockaddr_ll 定义在 <net/packet.h> 中,通常用于原始套接字(raw socket)来发送和接收数据链路层的帧。其定义如下:

 struct sockaddr_ll {
    unsigned short sll_protocol;   // 协议类型,通常是ETH_P_ALL或特定的以太网类型
    int             sll_ifindex;   // 网络接口的索引
    unsigned short sll_hatype;     // 硬件类型(一般是ETH_P_ETHER表示以太网)
    unsigned char  sll_pkttype;    // 包类型(例如,广播、单播、组播)
    unsigned char  sll_halen;      // 硬件地址长度
    unsigned char  sll_addr[8];    // 目标硬件地址(如MAC地址)
};

---sll_protocol:

这是协议类型字段,表示该帧的协议类型。通常它会设置为ETH_P_ALL(表示所有协议类型的以太网帧),或者特定协议的值,如ETH_P_IP(表示IPv4协议)。 ETH_P_ALL 是 0x0003,表示所有协议的数据帧。

---sll_ifindex:

这是网络接口的索引,表示发送帧的网络接口。这个值通过命令 ifconfig 或 ip link show 可以获得。例如,eth0 或 eth1 等网络接口的索引值。

---sll_hatype:

这个字段表示硬件类型,通常设置为 ETH_P_ETHER,表示标准的以太网类型。该字段可以用于更低级别的协议,但在大多数情况下它是设置为以太网类型(ETH_P_ETHER)。

--- sll_pkttype:

表示包的类型。常见的包类型有:

.PACKET_HOST:指向主机的数据包。

.PACKET_BROADCAST:广播数据包。

.PACKET_MULTICAST:多播数据包。

.PACKET_OTHERHOST:其他主机的数据包。

.PACKET_OUTGOING:发送的数据包。

---sll_halen: 这个字段表示硬件地址的长度(通常是6字节,即MAC地址的长度)。

---sll_addr:

存储目标硬件地址(如MAC地址)的数组,通常是6字节。对于以太网帧,MAC地址的长度是6字节。如果该字段设置为广播地址(ff:ff:ff:ff:ff:ff),表示数据包将发送到所有设备。


2.IP帧数据:

1) 作用:

在网络协议栈中,IP帧主要作用是在网络层提供无连接、不可靠的数据报服务,实现主机间的端到端数据传输。 它并不负责数据的可靠传输,也不保证数据包的顺序到达,这些工作留给更高层协议(如TCP)来完成。

作用体现在以下几个方面:

  • 寻址: IP数据报包含源IP地址和目的IP地址,这使得它能够在互联网上路由,找到正确的目的地。IP地址是互联网上设备的逻辑地址,用于标识网络上的主机或设备。
  • 分片与重组: 当IP数据报过大无法通过某个网络链路传输时,IP层会将它分割成多个更小的片段(fragment),并在到达目的地后重新组装成完整的IP数据报。这保证了数据包能够在不同网络环境下传输。
  • 路由: IP数据报在网络中传输时,会经过多个路由器。每个路由器都会根据IP数据报的目的IP地址,选择最佳路径转发数据报,最终将数据报传递到目的地。 IP路由协议(如RIP、OSPF、BGP)负责维护路由信息。
  • 差错检测: 虽然IP层本身不保证可靠传输,但IP数据报包含校验和(checksum),用于检测数据报在传输过程中是否发生错误。如果检测到错误,数据报会被丢弃。但是,IP层不会重传丢失的数据包。
  • 服务质量 (QoS): 在一些网络应用中,需要对数据包进行优先级划分,保证某些类型的流量优先传输。IP层可以通过一些机制(如DiffServ)来实现服务质量控制。
  • 隧道技术: IP隧道技术允许在IP网络中创建虚拟的IP网络,用于安全传输数据或跨越不同的网络类型。IP数据报可以封装在IP隧道中进行传输。
  • 封装和解封装: IP数据报是网络层的数据单元,它封装了来自上层协议(如TCP、UDP)的数据,并在到达目的地后解封装,将数据传递给上层协议处理。

2)IP帧格式(IPv4和IPv6):

a. IPv4 数据报格式

IPv4数据报分为多个字段,以下是每个字段的说明:

---版本(Version, 4 bits): 指定IP的版本号,IPv4使用值4。

---首部长度(IHL, Internet Header Length, 4 bits):

使用32位字作为单位,IP头的长度(不包括数据部分)。最短是5(表示20字节),如果有选项字段则更长。

---服务类型(Type of Service, 8 bits):

现在被称为区分服务(Differentiated Services),用于标识数据包的优先级和QoS(服务质量)。

---总长度(Total Length, 16 bits):

整个IP数据报的长度(以字节为单位),包括首部和数据。

---标识(Identification, 16 bits):

唯一标识数据报。用于分片情况下,接收方可以通过此字段和标识数据片属于哪个数据报。

---标志(Flags, 3 bits):

控制分片:

      • 第一个位保留位,必须为0。
      • 第二个位为DF(Don't Fragment),如果设为1,数据报不能分片。
      • 第三个位为MF(More Fragments),如果设为1,表示后面还有更多分片。

---片偏移(Fragment Offset, 13 bits):

数据报分片时,标识每个分片在原数据报中的位置(以8字节为单位)。

---生存时间(Time to Live, TTL, 8 bits):

数据报在网络中能经过的最大跳数(路由器数量)。每通过一个路由器,此值减1,为0时数据报被丢弃。

---协议(Protocol, 8 bits):

指定数据部分使用的协议类型(如6表示TCP,17表示UDP)。

---首部校验和(Header Checksum, 16 bits):

仅对IP头进行校验,确保传输过程中没有损坏。

---源IP地址(Source Address, 32 bits): 发送方的IP地址。

---目的IP地址(Destination Address, 32 bits):接收方的IP地址。

---选项(Options, 可变长): 可选字段,提供控制和调试功能,如记录路径、时间戳等。

---数据(Payload, 可变长):实际传输的数据,由协议字段指明的上层协议来解释和处理。


b. IPv6 数据报格式

IPv6是为了解决IPv4地址枯竭和一些协议局限性而设计的,以下是IPv6数据报的基本格式:



---版本(Version, 4 bits): IPv6的版本号为6。

---流量类别(Traffic Class, 8 bits): 类似于IPv4的服务类型字段,用于标识数据包的优先级。

---流标签(Flow Label, 20 bits): 用于标识数据包流,提高对特定流的处理效率。

---有效负载长度(Payload Length, 16 bits):不包括IP头的负载长度。

---下一个头部(Next Header, 8 bits):类似于IPv4的协议字段,在标准IPv6数据报内,指定紧随其后的扩展报头或数据的类型(如TCP或UDP)。

---跳限制(Hop Limit, 8 bits): 类似于IPv4的TTL,限制数据包在网络中的寿命。

---源地址(Source Address, 128 bits):发送方的IP地址。

---目的地址(Destination Address, 128 bits):接收方的IP地址。

---数据(Payload, 可变长):包含传输数据,实现中的上层协议处理。

3)使用场景:

选择IPv4还是IPv6取决于具体的应用场景和需求。 对于小型网络、现有基础设施或需要与旧系统兼容的场景,IPv4仍然是首选。 对于大型网络、物联网、移动网络以及需要高安全性和更大地址空间的场景,IPv6是更好的选择。 在许多情况下,IPv4和IPv6的双协议栈部署是过渡阶段的最佳解决方案。

4) 代码示例:

以下是一个简单的示例,展示如何手动封装IP帧并发送数据。

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

	// 计算校验和
unsigned short checksum(void *b, int len) {    
    unsigned short *buf = b;
    unsigned int sum = 0;
    unsigned short result;

    for (sum = 0; len > 1; len -= 2)
        sum += *buf++;
    if (len == 1)
        sum += *(unsigned char *)buf;
    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    result = ~sum;
    return result;
}

int main (int argc, char **argv) {
    int sockfd;
    char buffer[4096];
    struct sockaddr_in dest_addr;
    struct iphdr *iph = (struct iphdr *)buffer;

    // 创建原始套接字
    sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 设置目的地址
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(0); // 端口不需要设置
    dest_addr.sin_addr.s_addr = inet_addr("192.168.1.1"); // 目的IP地址

    // 填充IP头部
    iph->ihl = 5; // IP头部长度
    iph->version = 4; // IPv4
    iph->tos = 0; // 服务类型
    iph->tot_len = sizeof(struct iphdr) + strlen("Hello"); // 总长度
    iph->id = htonl(54321); // ID
    iph->frag_off = 0; // 分段偏移
    iph->ttl = 255; // 生存时间
    iph->protocol = IPPROTO_ICMP; // 协议类型(这里使用ICMP作为示例)
    iph->check = 0; // 校验和
    iph->saddr = inet_addr("192.168.1.100"); // 源IP地址
    iph->daddr = dest_addr.sin_addr.s_addr; // 目的IP地址

    // 计算IP校验和
    iph->check = checksum((unsigned short *)iph, sizeof(struct iphdr));

    // 填充数据
    strcpy(buffer + sizeof(struct iphdr), "Hello");

    // 发送数据包
    if (sendto(sockfd, buffer, iph->tot_len, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
        perror("Send failed");
    } else {
        printf("Packet sent\n");
    }

    close(sockfd);
    return 0;
}

补充说明:

---原始套接字:使用SOCK_RAW创建原始套接字,以便手动构建IP头部。

---IP头部:构造IP头部,设置版本、头部长度、服务类型、总长度、ID、生存时间、协议类型、源IP和目的IP等。

---校验和:计算IP校验和,以确保数据完整性。

---数据发送:使用sendto函数发送构造好的数据包。

---IPv6和IPv4的区别:IPv4使用 AF_INET 创建TCP和UDP套接字,IPv6使用 AF_INET6 创建TCP和UDP套接字,其它接口使用都是一样的。



3.UDP帧数据:

1) 作用:

a. 无连接传输: UDP是无连接的协议,发送数据之前不需要建立连接。这使得UDP在传输数据时延迟较低,适合实时应用。

b. 简单性: UDP的协议头部较小(8字节),相较于TCP的头部(20字节及以上),UDP的开销更小,适合需要快速传输的场景。

c. 不保证可靠性: UDP不提供数据包的重传、顺序控制或完整性检查。数据包可能会丢失、重复或乱序到达,适合对可靠性要求不高的应用。

d. 广播和多播支持: UDP支持广播(向网络上的所有主机发送数据)和多播(向特定组的主机发送数据),适合需要同时向多个接收者发送数据的场景。

2)帧格式:

UDP头部为固定的8字节(64位),包含以下四个字段:

a. 源端口(Source Port,2字节):

指示发送方的应用程序的端口号。对于不需要响应的通信,这是可选的。如果不使用,可以设置为0。

b. 目的端口(Destination Port,2字节):

指示接收方的应用程序的端口号,此字段在数据报到达目的地时用于识别目标应用程序。

c. 长度(Length,2字节):

表示整个UDP数据报的长度(包括头部和数据)。该值的最小值为8(仅头部长度),最大值为65,535字节。

d. 校验和(Checksum,2字节):

校验和用于错误检测,对UDP头部和数据部分进行检验。计算时还包括一个伪头部(来自IP层信息),该字段在IPV4中是可选的,但在IPv6中是强制校验的。

3)使用场景:

适合对速度要求高、对可靠性要求低的应用,具有较低的延迟和开销。

a.实时应用: 如视频会议、在线游戏、语音通话等,这些应用对延迟敏感,能够容忍一定的数据丢失。

b. DNS查询: DNS使用UDP进行域名解析,因为请求和响应数据包较小,且不需要建立连接。

c.简单的请求-响应协议: 如SNMP(简单网络管理协议)和TFTP(简单文件传输协议)。

4)代码示例:

以下是一个简单的示例,展示如何手动封装UDP帧并发送。

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

// UDP伪头部结构体,用于校验和计算
struct pseudo_header {
    u_int32_t source_address;
    u_int32_t dest_address;
    u_int8_t placeholder;
    u_int8_t protocol;
    u_int16_t udp_length;
};

// 计算校验和
unsigned short checksum(void *b, int len) {    
    unsigned short *buf = b;
    unsigned int sum = 0;
    unsigned short result;

    for (sum = 0; len > 1; len -= 2)
        sum += *buf++;
    if (len == 1)
        sum += *(unsigned char *)buf;
    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    result = ~sum;
    return result;
}

int main() {
    int sockfd;
    char buffer[4096];
    struct sockaddr_in dest_addr;
    struct udphdr *udph = (struct udphdr *)(buffer + sizeof(struct iphdr));
    struct iphdr *iph = (struct iphdr *)buffer;
    struct pseudo_header psh;

    // 创建原始套接字
    sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 设置目的地址
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(8080);
    dest_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 填充IP头部
    iph->ihl = 5; // IP头部长度
    iph->version = 4; // IPv4
    iph->tos = 0; // 服务类型
    iph->tot_len = sizeof(struct iphdr) + sizeof(struct udphdr) + strlen("Hello");
    iph->id = htonl(54321); // ID
    iph->frag_off = 0; // 分段偏移
    iph->ttl = 255; // 生存时间
    iph->protocol = IPPROTO_UDP; // 协议
    iph->check = 0; // 校验和
    iph->saddr = inet_addr("127.0.0.1"); // 源IP
    iph->daddr = dest_addr.sin_addr.s_addr; // 目的IP

    // 填充UDP头部
    udph->source = htons(12345); // 源端口
    udph->dest = htons(8080); // 目的端口
    udph->len = htons(sizeof(struct udphdr) + strlen("Hello")); // UDP长度
    udph->check = 0; // 校验和

    // 填充伪头部
    psh.source_address = iph->saddr;
    psh.dest_address = iph->daddr;
    psh.placeholder = 0;
    psh.protocol = IPPROTO_UDP;
    psh.udp_length = udph->len;

    int psize = sizeof(struct pseudo_header) + sizeof(struct udphdr) + strlen("Hello");
    char *pseudogram = malloc(psize);

    memcpy(pseudogram, (char *)&psh, sizeof(struct pseudo_header));
    memcpy(pseudogram + sizeof(struct pseudo_header), udph, sizeof(struct udphdr) + strlen("Hello"));

    // 计算UDP校验和
    udph->check = checksum(pseudogram, psize);

    // 填充数据
    strcpy(buffer + sizeof(struct iphdr) + sizeof(struct udphdr), "Hello");

    // 发送数据包
    if (sendto(sockfd, buffer, iph->tot_len, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
        perror("Send failed");
    } else {
        printf("Packet sent\n");
    }

    close(sockfd);
    free(pseudogram);
    return 0;
}

说明:

  • 原始套接字:使用SOCK_RAW创建原始套接字,以便手动构建IP和UDP头部。
  • IP头部:构造IP头部,设置版本、头部长度、协议等。
  • UDP头部:构造UDP头部,设置源和目的端口、长度等。
  • 伪头部:用于计算UDP校验和,包含源IP、目的IP、协议和UDP长度。
  • 校验和:计算UDP校验和,以确保数据完整性。
  • 数据发送:使用sendto函数发送构造好的数据包。

--------------------------------------------------------------------


4.TCP帧数据:

1) 作用:

a.面向连接: TCP是面向连接的协议,在发送数据之前需要建立连接,确保双方都准备好进行数据传输。

b. 可靠性: TCP提供可靠的数据传输,确保数据包按顺序到达,并且在丢失时会进行重传。它使用序列号、确认应答和重传机制来实现这一点。

c.流量控制: TCP使用流量控制机制(如滑动窗口)来防止发送方发送过多数据,导致接收方处理不过来。

d.拥塞控制: TCP具有拥塞控制机制,能够根据网络状况动态调整数据传输速率,避免网络拥塞。

e. 数据完整性: TCP提供数据完整性检查,确保数据在传输过程中没有被损坏。

2) 帧格式:

TCP帧(或称为TCP段)由TCP头部和数据部分组成。以下是TCP帧的详细格式解析。

---源端口(Source Port)(16 bits):发送方的端口号。

---目的端口(Destination Port)(16 bits):接收方的端口号。

---序列号(Sequence Number)(32 bits):用于标识发送的数据字节流的序列号。对于第一个数据段,序列号是初始序列号(ISN),后续段的序列号是前一个段的序列号加上数据字节数。

---确认号(Acknowledgment Number)(32 bits):表示期望接收的下一个字节的序列号。只有在ACK标志位被设置时,这个字段才有效。

---数据偏移(Data Offset)(4 bits):TCP头部的长度,以32位字(4字节)为单位。这个字段指示TCP头部的结束位置,通常是5(表示20字节)到15(表示60字节)。

---保留(Reserved)(3 bits):保留供将来使用,通常设置为0。

---控制标志(Flags)(9 bits)

包含多个控制位:

  • URG(Urgent Pointer):紧急指针标志。
  • ACK(Acknowledgment):确认标志。
  • PSH(Push):推送标志,表示接收方应立即将数据传递给应用层。
  • RST(Reset):重置连接。
  • SYN(Synchronize):同步序列号,用于建立连接。
  • FIN(Finish):结束连接。

---窗口大小(Window Size)(16 bits):表示接收方的缓冲区大小,告知发送方可以发送多少字节的数据。

---校验和(Checksum)(16 bits):用于错误检测,确保数据在传输过程中未被损坏。校验和是对TCP头部和数据部分的计算结果。

---紧急指针(Urgent Pointer)(16 bits):仅在URG标志位被设置时有效,指示紧急数据的结束位置。

---选项(Options)(可变长度):可选字段,允许TCP进行一些扩展功能,如最大报文段大小(MSS)、时间戳等。

---数据(Data)(可变长度):实际传输的数据部分,长度可变,取决于应用层发送的数据。

以下为使用tcpdump抓的TCP数据包相关信息,可对比上面的TCP头帧格式来看。

3)使用场景:

适合对可靠性和数据完整性要求高的应用,提供可靠的、顺序的、面向连接的数据传输。

a. 文件传输: 如FTP(文件传输协议)和HTTP(超文本传输协议),这些应用需要确保数据的完整性和顺序。

b.电子邮件: 如SMTP(简单邮件传输协议)和IMAP(互联网消息访问协议),需要可靠的邮件传输。

c. 远程登录: 如SSH(安全外壳协议)和Telnet,确保命令和响应的可靠传输

4)代码示例:

以下是一个简单的示例, 展示手动封装TCP帧涉及到构建TCP头部和IP头部,并通过套接字发送。

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

// 计算校验和
unsigned short checksum(void *b, int len) { unsigned short *buf = b;
    unsigned int sum = 0;
    unsigned short result;

    for (sum = 0; len > 1; len -= 2)
        sum += *buf++;
    if (len == 1)
        sum += *(unsigned char *)buf;
    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    result = ~sum;
    return result;
}

int main() {
    int sockfd;
    char buffer[4096];
    struct sockaddr_in dest_addr;
    struct iphdr *iph = (struct iphdr *)buffer;
    struct tcphdr *tcph = (struct tcphdr *)(buffer + sizeof(struct iphdr));

/***1. 创建原始套接字 ***/
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    /*** 2. 设置目的地址 ***/
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(8080); // 目的端口
dest_addr.sin_addr.s_addr = inet_addr("192.168.1.1"); // 目的IP地址 

/***3.填充TCP头部 ***/
tcph->source = htons(12345); // 源端口 
tcph->dest = htons(8080); // 目的端口
    tcph->seq = 0; // 序列号
tcph->ack_seq = 0; // 确认号 
tcph->doff = 5; // TCP头部长度 
tcph->fin = 0; // FIN标志 
tcph->syn = 1; // SYN标志 
tcph->rst = 0; // RST标志
    tcph->psh = 0; // PSH标志
    tcph->ack = 0; // ACK标志
tcph->urg = 0; // URG标志 
tcph->window = htons(5840); // 窗口大小 
tcph->check = 0; // 校验和 
tcph->urg_ptr = 0; // 紧急指针 

/***4. 填充IP头部 ***/
iph->ihl = 5; // IP头部长度 
iph->version = 4; //ipv4版本
IPv4 iph->tos = 0; // 服务类型 
iph->tot_len = sizeof(struct iphdr) + sizeof(struct tcphdr); // 总长度 iph->id = htonl(54321); // ID
iph->frag_off = 0; // 分段偏移 
iph->ttl = 255; // 生存时间
    iph->protocol = IPPROTO_TCP; // 协议类型
iph->check = 0; // 校验和 
iph->saddr = inet_addr("192.168.1.100"); // 源IP地址 
iph->daddr = dest_addr.sin_addr.s_addr; // 目的IP地址

/***5.计算IP校验和 ***/
iph->check = checksum((unsigned short *)iph, sizeof(struct iphdr));

/***6.计算TCP校验和**/
	int psize = sizeof(struct pseudo_header) + sizeof(struct tcphdr);
    char *pseudogram = malloc(psize);
    struct pseudo_header {
        u_int32_t source_address;
        u_int32_t dest_address;
        u_int8_t placeholder;
        u_int8_t protocol;
        u_int16_t tcp_length;
    };

    struct pseudo_header psh;
    psh.source_address = iph->saddr;
    psh.dest_address = iph->daddr;
    psh.placeholder = 0;
    psh.protocol = IPPROTO_TCP;
    psh.tcp_length = htons(sizeof(struct tcphdr));

    memcpy(pseudogram, (char *)&psh, sizeof(struct pseudo_header));
    memcpy(pseudogram + sizeof(struct pseudo_header), tcph, sizeof(struct tcphdr));

    // 计算TCP校验和
    tcph->check = checksum((unsigned short *)pseudogram, psize);

// 发送数据包 
if (sendto(sockfd, buffer, iph->tot_len, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
        perror("Send failed");
    } else {
        printf("Packet sent\n");
    }

    close(sockfd);
    free(pseudogram);
    return 0;
}

说明:

  • 原始套接字:使用SOCK_RAW创建原始套接字,以便手动构建IP和TCP头部。
  • TCP头部:构造TCP头部,设置源端口、目的端口、序列号、确认号、标志位、窗口大小等。
  • IP头部:构造IP头部,设置版本、头部长度、服务类型、总长度、ID、生存时间、协议类型、源IP和目的IP等。
  • 伪头部:用于计算TCP校验和,包含源IP、目的IP、协议和TCP长度。
  • 校验和:计算IP和TCP的校验和,以确保数据完整性。
  • 数据发送:使用sendto函数发送构造好的数据包。

<<常用网络协议整理笔记(二)>>待续......

最近发表
标签列表