1. 网络分层

ARP协议

  • 按照OSI分层的话从上到下分为7层:物理层、链路层、网络层、传输层、会话层、表示层、应用层

  • 一般来说会话层、表示层、应用层 统一称为 应用层

  • 每张网卡都有一个唯一确定的地址,被称为MAC地址,通过这个全球唯一的MAC地址,就能标识不同的网络设备,MAC地址是一个48bit的值

2. Ethernet 封包格式

2.1 以太网封包格式

img
字段 字段长度(字节) 说明
前导码(preamble) 7 0和1交替变换的码流
帧开始符(SFD) 1 帧起始符
目的地址(DA) 6 目的设备的MAC物理地址
源地址(SA) 6 发送设备的MAC物理地址
长度/类型(Length/Type) 2 帧数据字段长度/帧协议类型
数据及填充(data and pad) 46~1500 帧数据字段
帧校验序列(FCS) 4 数据校验字段
  • 以太网帧大小必须在64-1518字节(不包含前导码和定界符),即包括目的地址(6B)、源地址(6B)、类型(2B)、数据、FCS(4B)在内,其中数据段大小在46~1500字节之间。

  • 以太网的前导码是一串交替的0和1,用于在网络流中区分一帧帧数据,前导码的最后一个字节是帧开始定界符

image-20240221170858451

2.2 IP协议封包格式

IP

2.2 ARP协议封包格式

ARP

2.3 TCP协议封包格式

tcp

在Tcp协议中,比较重要的字段有:

  • 源端口:表示发送端端口号,字段长 16 位,2个字节

  • 目的端口:表示接收端端口号,字段长 16 位,2个字节

  • 序列号(sequence number):字段长 32 位,占4个字节,序列号的范围为 [0,4284967296]。

    • 由于TCP是面向字节流的,在一个TCP连接中传送的字节流中的每一个字节都按顺序编号
    • 首部中的序号字段则是指本报文段所发送的数据的第一个字节的序号,这是随机生成的。
    • 序号是循环使用的,当序号增加到最大值时,下一个序号就又回到了0
  • 确认序号(acknowledgement number):占32位(4字节),表示收到的下一个报文段的第一个数据字节的序号,如果确认序号为N,序号为S,则表明到序号N-S为止的所有数据字节都已经被正确地接收到了。

  • 8个标志位(Flag):

    • CWR:CWR 标志与后面的 ECE 标志都用于 IP 首部的 ECN 字段,ECE 标志为 1 时,则通知对方已将拥塞窗口缩小;
    • ECE:若其值为 1 则会通知对方,从对方到这边的网络有阻塞。在收到数据包的 IP 首部中 ECN 为 1 时将 TCP 首部中的 ECE 设为 1.;
    • URG:该位设为 1,表示包中有需要紧急处理的数据,对于需要紧急处理的数据,与后面的紧急指针有关;
    • ACK:该位设为 1,确认应答的字段有效,TCP规定除了最初建立连接时的 SYN 包之外该位必须设为 1;
    • PSH:该位设为 1,表示需要将收到的数据立刻传给上层应用协议,若设为 0,则先将数据进行缓存;
    • SYN:用于建立连接,该位设为 1,表示希望建立连接,并在其序列号的字段进行序列号初值设定;
    • FIN:该位设为 1,表示今后不再有数据发送,希望断开连接。
  • 窗口尺寸:该字段长 16 位,表示从确认序号所指位置开始能够接收的数据大小,TCP 不允许发送超过该窗口大小的数据。

2.4 UDP协议封包格式

udp

2.套接字通信

图片
  • 在Linux的套接字编程中分为标准套接字和原始套接字,而标准套接字又分为流式套接字和数据报套接字
    • 流式套接字:TCP
    • 数据报套接字:UDP

2.1 UDP编程

udp
  • sendto函数是非阻塞的,recvfrom函数是阻塞的

udp客户端的实现:

//udp客户端的实现
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>

int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}

int sockfd; //文件描述符
struct sockaddr_in serveraddr; //服务器网络信息结构体
socklen_t addrlen = sizeof(serveraddr);

//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}

//客户端自己指定自己的ip地址和端口号,一般不需要,系统会自动分配
#if 0
struct sockaddr_in clientaddr;
clientaddr.sin_family = AF_INET;
clientaddr.sin_addr.s_addr = inet_addr(argv[3]); //客户端的ip地址
clientaddr.sin_port = htons(atoi(argv[4])); //客户端的端口号
if(bind(sockfd, (struct sockaddr *)&clientaddr, addrlen) < 0)
{
perror("fail to bind");
exit(1);
}
#endif

//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制字符串ip地址转化为整形数据
//htons:将主机字节序转化为网络字节序
//atoi:将数字型字符串转化为整形数据
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));

//第三步:进行通信
char buf[32] = "";
while(1)
{
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = '\0';

if(sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("fail to sendto");
exit(1);
}

char text[32] = "";
if(recvfrom(sockfd, text, sizeof(text), 0, (struct sockaddr *)&serveraddr, &addrlen) < 0)
{
perror("fail to recvfrom");
exit(1);
}
printf("from server: %s\n", text);
}
//第四步:关闭文件描述符
close(sockfd);

return 0;
}

udp服务器的实现:

//udp服务器的实现
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //htons inet_addr
#include <unistd.h> //close
#include <string.h>

int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}

int sockfd; //文件描述符
struct sockaddr_in serveraddr; //服务器网络信息结构体
socklen_t addrlen = sizeof(serveraddr);

//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("fail to socket");
exit(1);
}

//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制字符串ip地址转化为整形数据
//htons:将主机字节序转化为网络字节序
//atoi:将数字型字符串转化为整形数据
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));

//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
{
perror("fail to bind");
exit(1);
}

while(1)
{
//第四步:进行通信
char text[32] = "";
struct sockaddr_in clientaddr;
if(recvfrom(sockfd, text, sizeof(text), 0, (struct sockaddr *)&clientaddr, &addrlen) < 0)
{
perror("fail to recvfrom");
exit(1);
}
printf("[%s - %d]: %s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), text);

strcat(text, " *_*");

if(sendto(sockfd, text, sizeof(text), 0, (struct sockaddr *)&clientaddr, addrlen) < 0)
{
perror("fail to sendto");
exit(1);
}
}

//第四步:关闭文件描述符
close(sockfd);

return 0;
}

2.2 TCP编程

tcp

3. ARP协议分析

ARP协议是链路层的协议,用于拿到目标IP主机的MAC地址,例如假设我现在主机A(192.168.1.1)B(192.168.1.2) 发送一个数据包,那么A必须知道B主机的ipport,使用的协议(TCP/UDP,此外还需要MAC地址,在第一次发送时A并不知道BMAC地址,因此需要先拿到BMAC地址,这就需要使用到ARP协议

ARP(Address Resolution Protocol,地址解析协议)

  • 1、是 TCP/IP 协议族中的一个

  • 2、主要用于查询指定 ip 所对应的的 MAC

  • 3、请求方使用广播来发送请求

  • 4、应答方使用单播来回送数据

  • 5、为了在发送数据的时候提高效率在计算中会有一个 ARP 缓存表,用来暂时存放 ip 所对应的 MAC,在 linux中使用 ARP 即可查看,在 xp 中使用 ARP -a

考虑如下的局域网通信流程,各个主机通过交换机相连接

image-20240221151031897

先说一下交换机的功能:

  • 交换机中有一个缓存表,这个表中保存了每一端口相连设备的MAC地址,将端口和MAC地址相互对应好保存在这个缓存表中
  • 当一个设备向另外一个设备发送数据时,发送的数据先到达交换机,交换机会根据发送数据包中的目标主机的MAC地址去缓存表中索引目标设备的端口,然后将数据转发给此端口
  • 交换机一般工作在数据链路层

ARP获取目标MAC的流程:

  • 主机A不知道B的MAC地址,那么主机A会先发送一个ARP广播,交换机第一次收到ARP广播时,会把ARP广播数据包转发给所有连接上的端口(除来源端口);此时也会根据A发送的ARP数据去拿到A的MAC地址,将A插入的端口何其MAC地址对应起来保存到交换机自己的缓存表中

  • 这样所有的主机拿到这个ARP广播后,会比对IP地址,判断A主机是否想要自己的MAC地址,此时B主机比对后发现是,就会将自己的MAC地址写入到回复的ARP数据包中,并以单播的形式先发送给交换机,然后交换机再转发给对应的主机。

  • 为了避免下次A主机向B主机发送数据时还要再次去发送ARP数据包去向B主机获取它的MAC地址,因此在第一次获取到B的MAC地址后会将它放在一张表中存起来,这张表用来暂时存放 ip 所对应的 MAC,在 linux中使用 ARP 即可查看,在 xp 中使用 ARP -a

    image-20240221152640111

现在假设各主机之间通过路由器相连:

image-20240221161712911

先说一下路由器的功能:

  • 交换机用于实现同网段之间设备的通信,路由器用于不同网段之间的设备的通信,路由器工作在网络层

  • 路由器中存在两个网卡,一个用于内部局域网通信,一个用于和另外一个路由器建立连接

  • 假设A主机想要向D主机发送数据,由于此时不在同一网段中,因此A主机会将数据包先发送给路由器,由路由器进行转发。而A主机是如何知道谁是路由器呢,这就到了默认网关出场了,每台主机在配置网络的时候需要将默认网关配置成连接的路由器的IP地址,比如A主机的默认网关地址就是192.168.1.1,D主机的默认网关地址就是192.168.3.1

  • 现在A发送的数据包到达了路由器,A想要向192.168.3.0这个网段发送数据,此时路由器会进行判断说将这个数据包转发到和他连接的哪一个路由器,因此需要去查表,这个表就叫做路由表,比如

    Network: 192.168.3.0
    Mask: 255.255.255.0
    Next Hop: 192.168.2.3

    对于路由器1来说,路由表就如上,意味着如果路由器1要向192.168.3.0这个网段发送数据,那么它的下一跳的路由器的地址为192.168.2.3,可以看见192.168.2.3就代表了路由器2,而通过路由器2就能访问到D主机了

4. 使用原始套接字进行网络数据分析

#include <sys/socket.h>
#include <sys/types.h> //socket
#include <netinet/ether.h> //ETH_P_ALL
#include <unistd.h> //close
#include <stdlib.h> //exit
#include <stdio.h> //printf
#include <arpa/inet.h> //htons

#define ERRLOG(errmsg) do{\
perror(errmsg);\
exit(1);\
}while(0)

int main(int argc, char const *argv[])
{
//创建原始套接字
int sockfd;
if((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0)
{
ERRLOG("fail to socket");
}

//printf("sockfd = %d\n", sockfd);

//接收数据并分析
unsigned char msg[1600] = "";
while(1)
{
//recvfrom recv read 都可以使用
if(recvfrom(sockfd, msg, sizeof(msg), 0, NULL, NULL) < 0)
{
ERRLOG("fail to recvfrom");
}

//分析接收到的数据包
unsigned char dst_mac[18] = "";
unsigned char src_mac[18] = "";
unsigned short type;
sprintf(dst_mac, "%x:%x:%x:%x:%x:%x", msg[0], msg[1], msg[2], msg[3], msg[4], msg[5]);
sprintf(src_mac, "%x:%x:%x:%x:%x:%x", msg[6], msg[7], msg[8], msg[9], msg[10], msg[11]);
type = ntohs(*(unsigned short *)(msg + 12));

printf("源mac:%s --> 目的mac:%s\n", src_mac, dst_mac);
printf("type = %#x\n", type);
if(type == 0x0800)
{
printf("ip数据报\n");
//头部长度、总长度
unsigned char ip_head_len;
unsigned short ip_len;
((*(unsigned char *)(msg + 14)) & 0x0f) * 4;
ip_len = ntohs(*(unsigned short *)(msg + 16));
printf("ip头部:%d, ip数据报总长度: %d\n", ip_head_len, ip_len);
//目的ip地址、源IP地址
unsigned char dst_ip[16] = "";
unsigned char src_ip[16] = "";
sprintf(src_ip, "%u.%u.%u.%u", msg[26], msg[27], msg[28], msg[29]);
sprintf(dst_ip, "%u.%u.%u.%u", msg[30], msg[31], msg[32], msg[33]);
printf("源ip地址:%s --> 目的ip地址:%s\n", src_ip, dst_ip);
//协议类型
unsigned char ip_type;
ip_type = *(msg + 23);
printf("ip_type = %d\n", ip_type);
//icmp、igmp、tcp、udp
if(ip_type == 1)
{
printf("icmp报文\n");
}
else if(ip_type == 2)
{
printf("igmp报文\n");
}
else if(ip_type == 6)
{
printf("tcp报文\n");
unsigned short src_port;
unsigned short dst_port;
src_port = ntohs(*(unsigned short *)(msg + 34));
dst_port = ntohs(*(unsigned short *)(msg + 36));
printf("源端口号:%d --> 目的端口号: %d\n", src_port, dst_port);
}
else if(ip_type == 17)
{
printf("udp报文\n");
//目的端口号、源端口号
unsigned short src_port;
unsigned short dst_port;
src_port = ntohs(*(unsigned short *)(msg + 34));
dst_port = ntohs(*(unsigned short *)(msg + 36));
printf("源端口号:%d --> 目的端口号: %d\n", src_port, dst_port);
}
}else if(type == 0x0806){
printf("arp数据报\n");
//源ip地址
//目的ip地址
unsigned char dst_ip[16] = "";
unsigned char src_ip[16] = "";
sprintf(src_ip, "%u.%u.%u.%u", msg[28], msg[29], msg[30], msg[31]);
sprintf(dst_ip, "%u.%u.%u.%u", msg[38], msg[39], msg[40], msg[41]);
printf("源ip地址:%s --> 目的ip地址:%s\n", src_ip, dst_ip);

}else if(type == 0x8035)
{
printf("rarp数据报\n");
}
}

return 0;
}

代码运行结果如下,在root模式下运行:

image-20240221210039770

5. 使用原始套接字剖析TCP协议

为了更好的理解TCP协议中的三次握手与四次挥手,我们使用原始套接字来抓取一下TCP数据报,逐步分析每次握手和挥手之间的TCP数据报的变化:

我现在有两台主机,主机之间通过路由器连接:

image-20240222155851459

  • A作为客户端,IP为:192.168.3.9,端口为系统默认分配,我们将会在A主机上运行一个客户端程序使用TCP去连接B主机
  • B作为服务器,IP为:192.168.3.31,端口设置为10000,我们会在B主机上运行一个服务器程序使用TCP去监听TCP连接
  • 额外在A主机上运行一个网络抓包程序,用来抓取A主机发送给B主机和B主机发送给A主机的TCP数据报

服务器代码:

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

#define N 128

int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s [ip] [port]\n", argv[0]);
exit(1);
}

//第一步:创建套接字
int sockfd;
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("fail to socket");
exit(1);
}

//第二步:将套接字与服务器网络信息结构体绑定
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
//配置本地服务器的Ip和端口号
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
perror("fail to bind");
exit(1);
}

//第三步:将套接字设置为被动监听状态
if(listen(sockfd, 10) == -1)
{
perror("fail to listen");
exit(1);
}

//第四步:阻塞等待客户端的链接请求
int acceptfd;
struct sockaddr_in clientaddr;
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) == -1)
{
perror("fail to accept");
exit(1);
}

//打印连接的客户端的信息
printf("ip:%s, port:%d\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

//第五步:进行通信
//tcp服务器与客户端通信时,需要使用accept函数的返回值
char buf[N] = "";
if(recv(acceptfd, buf, N, 0) == -1)
{
perror("fail to recv");
}

printf("from client: %s\n", buf);

strcat(buf, " *_*");
if(send(acceptfd, buf, N, 0) == -1)
{
perror("fail to send");
exit(1);
}

//关闭套接字文件描述符
close(acceptfd);
close(sockfd);

return 0;
}

客户端代码:

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


#define N 128

int main(int argc, char const *argv[])
{

if(argc < 3)
{
fprintf(stderr, "Usage: %s [ip] [port]\n", argv[0]);
exit(1);
}

int sockfd;
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("FAIL to socket");
exit(1);
}

struct sockaddr_in serveraddr;
socklen_t addrlen =sizeof(serveraddr);

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

if(connect(sockfd, (struct sockaddr*)&serveraddr ,addrlen) == -1)
{
perror("fail to connect");
exit(1);
}

char buf[N] = "";
fgets(buf , N ,stdin);
buf[strlen(buf) - 1] = '\0';

if(send(sockfd , buf , N , 0) == -1)
{
perror("faild to send");
exit(1);
}

char text[N] = "";
if(recv(sockfd , text, N , 0) == -1)
{
perror("fail to recv");
exit(1);
}

printf("from server: %s\n", text);
close(sockfd);
return 0;

}

网络抓包代码

#include <sys/socket.h>
#include <sys/types.h> //socket
#include <netinet/ether.h> //ETH_P_ALL
#include <unistd.h> //close
#include <stdlib.h> //exit
#include <stdio.h> //printf
#include <arpa/inet.h> //htons
#include <string.h>

#define ERRLOG(errmsg) do{\
perror(errmsg);\
exit(1);\
}while(0)

int main(int argc, char const *argv[])
{
//创建原始套接字
int sockfd;
if((sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0)
{
ERRLOG("fail to socket");
}

//接收数据并分析
unsigned char msg[1600] = "";
while(1)
{
//recvfrom recv read 都可以使用
if(recvfrom(sockfd, msg, sizeof(msg), 0, NULL, NULL) < 0)
{
ERRLOG("fail to recvfrom");
}

//分析接收到的数据包
unsigned char dst_mac[18] = "";
unsigned char src_mac[18] = "";
unsigned short type;
sprintf(dst_mac, "%x:%x:%x:%x:%x:%x", msg[0], msg[1], msg[2], msg[3], msg[4], msg[5]);
sprintf(src_mac, "%x:%x:%x:%x:%x:%x", msg[6], msg[7], msg[8], msg[9], msg[10], msg[11]);
type = ntohs(*(unsigned short *)(msg + 12));


if(type == 0x0800)
{

//头部长度、总长度
unsigned char ip_head_len;
unsigned short ip_len;
((*(unsigned char *)(msg + 14)) & 0x0f) * 4;
ip_len = ntohs(*(unsigned short *)(msg + 16));

//目的ip地址、源IP地址
unsigned char dst_ip[16] = "";
unsigned char src_ip[16] = "";
sprintf(src_ip, "%u.%u.%u.%u", msg[26], msg[27], msg[28], msg[29]);
sprintf(dst_ip, "%u.%u.%u.%u", msg[30], msg[31], msg[32], msg[33]);

//协议类型
unsigned char ip_type;
ip_type = *(msg + 23);
if(ip_type == 6)
{

unsigned short src_port;
unsigned short dst_port;
src_port = ntohs(*(unsigned short *)(msg + 34));
dst_port = ntohs(*(unsigned short *)(msg + 36));
if(dst_port == 10000 || src_port==10000){
unsigned char flag = msg[47] & 0x3f;
int ACK = flag >> 4 & 0x1;
int SYN = flag >> 1 & 0x1;
int FIN = flag & 0x1;
uint32_t seq = ntohl(*(uint32_t *)(msg + 38));
uint32_t actual_seq = ntohl(*(uint32_t *)(msg + 42));
printf("----------------tcp报文----------------\n");
printf("源端口号:%d --> 目的端口号: %d\n", src_port, dst_port);
printf("源ip地址:%s --> 目的ip地址:%s\n", src_ip, dst_ip);
printf("ip头部:%d, ip数据报总长度: %d\n", ip_head_len, ip_len);
printf("ACK: %d SYN: %d SEQ: %u ASEQ: %u FIN: %d\n ",ACK,SYN,seq ,actual_seq,FIN);
}
}
}
}

return 0;
}

对于A主机来说,我们抓取目标为目标端口为10000或者发送端口为10000的TCP数据报,其他的全部舍弃,目标端口为10000的TCP数据报是A发送给B的,发送端口为10000的是B主机发送给A的

先启动服务器程序,再启动客户端程序,三次握手的结果如下,

image-20240222195612633

很清晰的说明了三次握手的过程:上图中的ASEQ就是ack确认序号

image-20240222195914907

  • 在理解三次握手,四次挥手之前,我们先理解两个很重要的概念:

    在下面这张图中代表了一帧TCP数据包

    tcp

    • seq,序列号:代表了发送的数据字节数,在一帧TCP数据包中发送的每一个字节都会计数;这里的数据指的是上图中给哪个进程中的数据,不包含TCP头中的字节,在我们进行握手或者挥手时,发送的字节数据数为0,因此seq理论上是不会增加的,但是由于我们在握手和挥手的过程中FINSYN状态码是会发生改变的,因此TCP协议规定这两个状态码发生改变时就当多了一字节的数据,因此seq+1;seq代表的本机发送的字节数,seq的增加与否只与本机发送的数据有关,和接收到的数据无关
    • ack,确认序列号:这个ack和状态码中全部大写的ACK不一样,这里代表确认号,确认号只与接收到的数据的字节数有关,如果对方发送的TCP数据包中有数据,则这个ack的值等于上次ack的值加上收到的数据的字节数,同样如果接收到的数据包的FINSYN状态码发生了改变ack会相应的加一,
  • 第一次握手:

    • 客户端请求建立连接,将SYN置为1ACK0,然后随机生成一个seq=1490486468ack=0
  • 第二次握手:服务器给客户端回复数据

    • 服务器在收到客户端发送的第一次握手信息后,会先检查SYN是否为1,如果为1,将ACK=1,向客户端请求建立连接,因此SYN=1,然后随机生成一个seq=4013990024,将收到的seq+1放在ack中,此时ack=1490486469,然后打包发给客户端
    • 客户端检测服务器发送来的请求信息,先判断ACKSYN是否等于1,ACK=1代表服务器同意了客户端的连接请求,SYN=1代表服务器想要同客户端建立连接。此时对于客户端来说由于上次的seq=1490486468,然后上次发送了SYN,上面提到SYN代表发送了一个字节,因此下次发送的序列号为seq = 1490486468 + 1 = 1490486469
  • 第三次握手:客户端发送数据给服务器

    • 客户端将ACK标志位置为1,代表同意了服务器的连接请求,服务器发送过来的数据的seq = 4013990024,同样由于服务器改变了SYN状态码,代表发送了一个字节过来,因此客户端发送给服务器的确认码为ack 4013990024+1=4013990025
    • 服务器端查看ACK对应的标志位是否为1, 如果是1代表, 客户端同意了服务器的连接请求,然后校验确认序号是否等于生成的随意序号+1,然后拿到客户端的发送序号

三次握手完成之后,客户端和服务器都变成了同一种状态,这种状态叫:ESTABLISHED,表示双向连接已经建立, 可以通信了。在通过过程中,正常的通信状态就是 ESTABLISHED。在我们的程序中,客户端会先向服务器写入数据,服务器拿到数据后会回复客户端确认收到数据,然后再向客户端发送数据,发送完毕后,就直接关闭连接

观察数据发送和四次挥手我们使用Wireshark抓包来看一下:

image-20240223111923750

  • 前三帧数据就是三次握手的过程,然后客户端向服务器发送了数据,看第四帧的数据,发送的数据的长度为128,此时客户端的seq = 1 , ack = 1,然后服务器接收到了数据,此时服务器接收到了发送来的128字节数据,
  • TCP协议规定接收方在接收到数据后,需要应答发送方,发送发如果一直没收到接收方的应答,则代表此次发送数据出现了丢包,那发送方会重发数据,因此在第四帧客户端向无服务发送数据后,需要应答客户端,即第五帧数据,回复的确认号ack = 128 + 1 =129,发送方根据这个确认号就知道接收方成功接收到数据了

image-20240223115646850

  • 然后第六帧数据,服务器向客户端发送数据,数据长度为128,发送给客户端后理论上客户端应该回复一帧说我已经收到了的数据包,但是由于服务器在发送完毕这128字节数据后立马执行了close操作去断开连接,导致了一些很奇怪的操作,因此好像看着并没有回复此帧

接下来看四次挥手的过程:

  • 服务器和客户端都可以主动发起断开连接,在我们的代码中是服务器发起的断开连接

  • 第一次挥手:服务器会将FIN置为1,然后发送给客户端去告诉客户端我想要断开连接了

  • 第二次挥手:客户端发现了服务器此次请求,因此FIN的改变占用一个字节因此回复的ack = 129 + 1 = 130,然后发送给服务器,服务器得到此数据包检查确认序号知道客户端同意了断开连接,进入等待

  • 第三次挥手:客户端将FIN置为1去发送给服务器告诉服务器它想要断开连接

  • 第四次挥手:服务器检查客户端发送的FIN的值,然后同意客户端断开连接,此时是服务器第二次发送数据因此seq = 129 + 1 =130,客户端的发送序号为129,然后客户端将FIN置为了1,因此此时ack = 129 + 1 =130

    image-20240223121923843

  • 根据Wireshark的抓包结果显示,最后四帧明显是四次挥手的过程,但实际上不然,我们看见似乎是少了一帧数据,三次握手三帧,客户端发送 + 服务器应答 2帧,服务器发送+客户端应答 2 帧,四次挥手四帧,总共应该是11帧,但实际上只有10帧

  • 理论上第8帧中客户端执行第2次挥手时回复的ack应该是130才对,但是确回复的ack129 = 128 +1,因此我们可以猜测第九帧实际上是用于回复上次服务器发送来的128自己的数据,而不是四次挥手中的第二次挥手,而客户端将第二次挥手和第三次挥手合并成了一帧数据发送,即第9帧。

  • 这是由什么原因引起的,问题在于服务器在第6帧向客户端发送数据后,立刻执行了close操作,去发起断开连接,即第7帧,客户端先收到了这两帧数据,因此客户端先回复服务器收到了数据,然后再去应答断开连接的请求

  • 我们修改一下服务器的代码,在close之前添加一个延时操作,再使用Wireshark,抓包

    image-20240223120541936

image-20240223123938843

  • 可以看见此时就有11帧数据了,此时是由客户端先发起的断开连接请求,因此服务器再向客户端发送数据后没有立即执行断开连接操作,因此客户端有时间去先执行应答,然后去向服务器申请断开连接