目录
一、计算机网络分层模型
1.1 概念
计算机网络分层模型是网络通信的基础框架,它将复杂的网络通信过程划分为若干层次,每一层都执行特定的功能,并为上一层提供服务。这种分层的目的是简化网络设计,确保不同网络技术之间的兼容性和互操作性。与分层模型相关的概念包括分层、实体、协议、接口和服务。
1)分层(Layer)
分层是将网络通信过程划分为多个层次的过程。每一层关注网络通信的一个特定方面,比如物理传输、数据链路、网络路由、传输可靠性等。最著名的分层模型是OSI(开放系统互连)模型,它包含七层,以及TCP/IP模型,通常被认为包含四层。前者是学术和法律上的国际标准,后者是事实上的国际标准,即现实生活中被广泛遵循的分层模型。
2)实体(Entity)
在分层模型中,每一层都有实体,这些实体指的是执行特定层次功能的硬件或软件组件。同一层内的实体可以在不同的机器上,通过遵循相同层的协议进行通信。例如,两台计算机上的传输层实体可以是负责建立端到端连接的软件。同一层次的实体为对等实体。
3)协议(Protocol)
协议是一套规则和标准,用于控制同一层次内的实体如何相互通信。协议定义了通信的格式、时序、错误处理等。例如,TCP(传输控制协议)定义了如何在网络中的两个点之间可靠地传输数据。
4)接口(Interface)
接口是网络分层模型中,定义相邻两层之间如何交互的规范。它规定了一层如何向另一层提出服务请求,以及这些请求怎样被另一层接收和响应。接口包含了一系列的规则、命令、数据格式和过程,确保不同网络层之间的有效通信和数据交换。
服务访问点(Service Access Point,SAP)是接口概念的一个组成部分,具体化了接口在实现层次服务中的作用。
接口定义了相邻层之间交互的规范和方式,而SAP则是这种交互发生的具体逻辑位置。换句话说,SAP为接口提供了一个具体的实施机制,使得上层能够访问下层提供的服务。
5)服务(Service)
服务是指下层为紧邻上层提供的功能调用,是垂直方向的。描述了上层可以利用的具体功能和操作,但不涉及这些功能是如何实现的。服务强调的是功能性的提供,而不是实现细节。服务描述了“做什么”(功能),而不是“如何做”(实现细节)。服务是抽象的,隐藏了下层如何完成这些任务的具体细节,使得上层可以不依赖于下层的具体实现来进行设计和开发。
上层是通过SAP访问下层服务的。
1.2 OSI 七层模型
OSI(Open Systems Interconnection,开放系统互联)模型是由国际标准化组织(ISO)在1984年提出的一个网络架构模型,它将网络通信分为七个层次,每层都定义了特定的网络功能。自下而上分别为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
1)物理层(Physical Layer)
负责传输原始比特流。它涉及的是物理设备及介质,如电缆类型、电信号传输和接收等。
2)数据链路层(Data Link Layer)
确保物理链路上的无误传输。它提供了如帧同步、流量控制和错误检测等功能。
3)网络层(Network Layer)
负责数据包从源到目的地的传输和路由选择。它定义了地址和路由的概念,如IP协议。
4)传输层(Transport Layer)
提供端到端的数据传输服务,保证数据的完整性。它定义了如TCP和UDP协议。
5)会话层(Session Layer)
管理会话,控制建立、维护和终止会话。
6)表示层(Presentation Layer)
处理数据的表示、编码和解码,如加密和解密。
7)应用层(Application Layer)
提供网络服务给终端用户的应用程序,如HTTP、FTP、SMTP等协议。
1.3 五层模型
五层模型是将OSI模型的应用层、表示层和会话层合并为应用层,其它层不变。是为了教学而设计的一种网络分层模型。
1.4 TCP/IP四层模型
TCP/IP四层模型,亦称互联网协议套件(Internet Protocol Suite),是一种按照功能标准组织互联网及类似计算机网络中使用的一系列通信协议的框架。该套件中的基础协议包括传输控制协议(TCP)、用户数据报协议(UDP)和互联网协议(IP)。
TCP/IP协议栈(Protocol Stack)是指TCP/IP协议套件的软件实现。
1.应用层(Application Layer)
这层是用户最直接交互的部分,是软件应用通过网络进行通信的地方。应用层使用下层提供的服务来创建用户数据,将这些数据传输给同一台机器上或远程机器上的其他应用。
该层包括了SMTP、FTP、SSH、HTTP等协议,这些协议都在应用层上实现,以实现客户端与服务器之间的通信和数据交换。
2.传输层(Transport Layer)
传输层管理着端到端的通信,即主机到主机的通信。它的主要职责是为不同主机上的应用程序提供数据传输,同时保证这些数据的完整性和可靠性。
在传输层,主要的协议有TCP(Transmission Control Protocol),它提供顺序的、可靠的、双向的连接流,并管理报文段的发送,确保无错误、不丢失、不重复、按序到达;以及UDP(User Datagram Protocol),它提供一种无连接的服务,数据以数据报的形式发送,不保证顺序或响应。
3.互联网层(Internet Layer)
互联网层处理跨网络界限的数据包交换,负责将数据报文段从源地址路由到目的地址。这层抽象了实际的物理网络拓扑结构,并且定义了如何在各种网络结构中发送和接收数据包。
网络层的IP协议是构成Internet的基础。Internet上的主机都是通过IP地址来互相识别。IP协议不保证传输数据的可靠性,可能出现丢包等情况。
主要协议是IP(Internet Protocol),它定义了数据包的路由方式和网络地址。其他重要的协议包括ICMP(Internet Control Message Protocol),用于错误报告和网络诊断。
链路层(Link Layer)
链路层涉及到在物理网络上的数据通信。链路层确保网络层传来的IP数据报可以在网络的物理链接上进行传输,不管是通过有线还是无线媒介。它还负责处理与物理网络链接相关的问题,例如MAC(Media Access Control)地址寻址、帧同步、错误检测和校正。
它包括了在物理网络链接中使用的所有协议,如以太网(Ethernet)、Wi-Fi以及PPP(Point-to-Point Protocol)。
二、传输层-TCP协议
2.1 什么是TCP协议?
TCP(传输控制协议,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,广泛应用于互联网中。它旨在提供可靠的端到端通信,在发送数据之前,需要在两个通信端点之间建立连接。TCP通过一系列机制确保数据的可靠传输,这些机制包括序列号、确认应答、重传控制、流量控制和拥塞控制。
我这边重点围绕TCP报文段格式去叙述。
1)源端口号(Source Port)和目标端口号(Destination Port): 每个字段都是16位,用于标识发送和接收数据的应用层服务。端口号允许操作系统区分不同的网络服务。
2)序列号(sequence number field) 一个32位的数,用于标识从TCP源到目标的数据字节流,它用于确保数据的有序传输和重组。 TCP把数据看成一个无结构的、有序的字节流。TCP的序号是建立在传送的字节流之上,而不是建立在传送的报文段的序列之上。一个报文段的序号是该报文段首字节的字节流编号。举例来说,假设主机A上的一个进程想通过一条TCP连接向主机B上的一个进程发送一个数据流。主机A中的TCP将隐式地对数据流中的每一个字节编号。假定数据流是一个包含1000字节的序列,每次传输的报文段长度为100个字节,数据流的首字节编号是0。将为该数据流构建10个报文段。给第一个报文段分配序号0,第二个报文段分配序号100,第三个报文段分配序号200,以此类推。每一个序号被写入相应TCP报文段首部的序列号字段中。
3)确认号(acknowledgement number field) 也是一个32位的数,如果设置了ACK标志,报文段的确认号是发送方期望从对方收到的下一字节的序号。它是对之前接收到的数据的确认。假设主机A上的一个进程通过一条TCP连接和主机B上的一个进程通信,主机A已经收到了来自主机B的序号为0~122的所有字节,同时假设主机A接下来将要发送一个报文段给主机B,主机A等待主机B的数据流中序号为123及之后的所有字节,因此主机A将要发送给主机B的报文段中确认号应为123。 4)数据偏移(Data Offset) 一个4位字段,表示TCP头部的长度(以32位字为单位)。TCP头部可能包含可变数量的选项,因此长度是不确定的,需要这个字段记录长度信息。
5)保留(Reserved) 这是一个预留的6位字段,当前必须设置为0。
6)控制位(Control Bits) 标志字段共有六个比特,每个比特对应一个标志:URG、ACK、PSH、RST、SYN、FIN。这些标志用于控制TCP的不同行为,例如建立连接(SYN),终止连接(FIN),指示数据急迫性(URG)等。
URG:紧急标志,为1表示当前报文段存在被发送端上层实体置为“紧急”的数据。接收方应当优先处理这些数据。紧急指针字段指出了这部分数据的结束位置。
ACK:确认标志,为1指示确认字段的值是有效的。该报文段包含对已被成功接收报文段的确认。连接建立后,直至释放前传输的所有报文段ACK标志均为1。
PSH:为1指示接收方应立即将数据交给上层。
RST:为1表示链接出现错误,要求接收方终止连接,并重新建立连接。
SYN:该标志位用于建立连接。TCP连接建立时的前两次握手SYN为1。
FIN:为1表示发送方已经没有数据发送,想要断开连接。
7)窗口大小(Window Size) 一个16位的数值,用于控制对方发送的数据量,以避免接收方缓冲区溢出。窗口大小是流量控制的一部分,可以动态调整。
8)校验和(Checksum) 一个16位的字段,用于检查整个TCP报文段的错误。校验和确保数据在传输过程中的完整性。
9)紧急指针(Urgent Pointer) 一个16位的字段,如果URG标志被设置则为正数,表明从当前序列号开始的紧急数据的字节偏移量。
10)选项(Options) TCP头部可能包含各种可选字段,这些字段不是固定的,可能包括如最大报文段大小(MSS)、窗口缩放选项、选择性确认(SACK)等,用于优化TCP连接的性能。
11)填充(Padding) TCP是在32位体系结构上设计的,为了内存对齐以提升处理效率,需要确保它的头部长度为32位的整数倍。选项长度可变,填充的目的是确保TCP头部的长度按照32位字对齐,填充位于TCP头部的末尾。 注意:TCP数据部分不要求按照32位字对齐。
2.2 TCP的连接的建立和释放
1)TCP连接建立过程 TCP的连接建立过程又称为三次握手。 我们用小写的seq表示TCP报文头部的序号,用小写的ack表示确认号。未提到的标志位均为0。
(1)TCP服务器准备好接受连接,进入LISTEN状态,这一过程称为被动打开。
(2)第一次握手:客户端发送SYN标志为1(表示这是一个同步报文段),且seq随机的报文段,请求建立连接。此时的seq记为ISN(c)(Initial Sequence Number,初始序列号),括号中的c表示这是和客户端的序列号。客户端发送后变为SYN-SENT状态。
(3)第二次握手:服务端收到客户端的第一次握手信号,变为SYN-RCVD状态。随即确认客户端的SYN报文段,发送一个ACK和SYN标志均为1的报文段。该报文段中ack=ISN(c)+1,seq随机,标记为ISN(s),此处的s表示这是服务端的序列号。服务端变为SYN-RCVD状态。
(4)第三次握手:客户端收到服务端的第二次握手信号,变为ESTABLISHED状态,随即确认服务端的报文段,发送ACK标志为1的报文段。该报文段中ack=ISN(s)+1,seq=ISN(c)+1。服务端收到客户端的第三次握手信号之后变为ESTABLISHED状态。 下面的PPT中,大写字母组合表示相应标志位为1。
2)TCP连接释放过程 TCP的连接释放过程又称为四次挥手。 四次挥手可以由客户端发起,也可以由服务端发起。此处假设连接请求的断开操作由客户端发起。连接断开前,双方都处于ESTABLISHED状态。需要注意的是,连接建立后,即客户端和服务端处于ESTABLISHED时,双方发送的报文段ACK标志均为1。 我们用小写的seq表示TCP报文头部的序号,用小写的ack表示确认号。未提到的标志位均为0。
(1)第一次挥手:客户端发送FIN标志为1(即FINISH,表示通信结束)的报文段,请求断开连接,执行主动关闭(active close)。此时,报文段中包含对于服务端数据的确认,ACK为 1,假设ack=V。连接断开前已经历了一系列的数据传输,seq取决于之前已发送的报文段,假设seq=U。客户端状态变为FIN-WAIT-1。
(2)第二次挥手:服务端接收到第一次挥手信息,切换为CLOSE-WAIT状态,随即发送ACK标志为1,ack=U+1的报文段,此时seq=V。客户端接收到服务端的第二次挥手信号,变为FIN-WAIT-2状态。第二次挥手后,服务端仍可发送数据,客户端仍可接收。
(3)第三次挥手:服务端完成数据传送后,发送FIN标志和ACK标志均为1的报文段,ack=U+1,seq大于V,假设为W,请求断开连接,这一过程称为被动关闭。服务端发送第三次挥手信号后,变为LAST-ACK状态。
(4)第四次挥手:客户端收到第三次挥手信号,随即发送ACK标志为1,seq=U+1,ack=W+1的报文段,变为TIME-WAIT状态。服务端收到第四次挥手信号,变为CLOSED状态。客户端从变为TIME-WAIT状态开始计时,等待2MSL(2倍最大报文时长,约定值)后进入CLOSED状态。四次挥手结束。
综上就是一些理论基础知识的支撑,但是我并不想大章篇幅的去概述这些理论,最终都是要实践,接下来我会着重在代码操作上,谢谢。
2.3 基于TCP协议-只接受一个连接的范例程序
1)服务端(single_conn_server.c)
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#define handle_error(cmd, result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
}
void *read_from_client(void *argv)
{
int client_fd = *(int *)argv;
char *read_buf = NULL;
ssize_t count = 0;
read_buf = malloc(sizeof(char) * 1024);
if (!read_buf)
{
perror("malloc server read_buf");
return NULL;
}
while ((count = recv(client_fd, read_buf, 1024, 0)))
{
if (count < 0)
{
perror("recv");
}
fputs(read_buf, stdout);
}
printf("客户端请求关闭连接......\n");
free(read_buf);
return NULL;
}
void *write_to_client(void *argv)
{
int client_fd = *(int *)argv;
char *write_buf = NULL;
ssize_t send_count = 0;
write_buf = malloc(sizeof(char) * 1024);
if (!write_buf)
{
printf("写缓存分配失败,断开连接\n");
shutdown(client_fd, SHUT_WR);
perror("malloc server write_buf");
return NULL;
}
while (fgets(write_buf, 1024, stdin) != NULL)
{
send_count = send(client_fd, write_buf, 1024, 0);
if (send_count < 0)
{
perror("send");
}
}
printf("接收到命令行的终止信号,不再写入,关闭连接......\n");
shutdown(client_fd, SHUT_WR);
free(write_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
int sockfd, temp_result, client_fd;
pthread_t pid_read, pid_write;
struct sockaddr_in server_addr, client_addr;
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
// 声明IPV4通信协议
server_addr.sin_family = AF_INET;
// 我们需要绑定0.0.0.0地址,转换成网络字节序后完成设置
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 端口随便用一个,但是不要用特权端口
server_addr.sin_port = htons(6666);
// 创建server socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
handle_error("socket", sockfd);
// 绑定地址
temp_result = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
handle_error("bind", temp_result);
// 进入监听模式
temp_result = listen(sockfd, 128);
handle_error("listen", temp_result);
// 接受第一个client连接
socklen_t cliaddr_len = sizeof(client_addr);
client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &cliaddr_len);
handle_error("accept", client_fd);
printf("与客户端 from %s at PORT %d 文件描述符 %d 建立连接\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd);
// 启动一个子线程,用来读取客户端数据,并打印到 stdout
pthread_create(&pid_read, NULL, read_from_client, (void *)&client_fd);
// 启动一个子线程,用来从命令行读取数据并发送到客户端
pthread_create(&pid_write, NULL, write_to_client, (void *)&client_fd);
// 阻塞主线程
pthread_join(pid_read, NULL);
pthread_join(pid_write, NULL);
printf("释放资源\n");
close(client_fd);
close(sockfd);
return 0;
}
2)客户端(single_conn_client.c)
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
// 192.168.10.150 IP 地址的16进制表示
#define INADDR_LOCAL 0xC0A80A96
#define handle_error(cmd, result) \
if (result < 0) \
{ \
perror(cmd); \
return -1; \
}
void *read_from_server(void *argv)
{
int sockfd = *(int *)argv;
char *read_buf = NULL;
ssize_t count = 0;
read_buf = malloc(sizeof(char) * 1024);
if (!read_buf)
{
perror("malloc client read_buf");
return NULL;
}
while (count = recv(sockfd, read_buf, 1024, 0))
{
if (count < 0)
{
perror("recv");
}
fputs(read_buf, stdout);
}
printf("收到服务端的终止信号......\n");
free(read_buf);
return NULL;
}
void *write_to_server(void *argv)
{
int sockfd = *(int *)argv;
char *write_buf = NULL;
ssize_t send_count = 0;
write_buf = malloc(sizeof(char) * 1024);
if (!write_buf)
{
printf("写缓存分配失败,断开连接\n");
shutdown(sockfd, SHUT_WR);
perror("malloc client write_buf");
return NULL;
}
while (fgets(write_buf, 1024, stdin) != NULL)
{
send_count = send(sockfd, write_buf, 1024, 0);
if (send_count < 0)
{
perror("send");
}
}
printf("接收到命令行的终止信号,不再写入,关闭连接......\n");
shutdown(sockfd, SHUT_WR);
free(write_buf);
return NULL;
}
int main(int argc, char const *argv[])
{
int sockfd, temp_result;
pthread_t pid_read, pid_write;
struct sockaddr_in server_addr, client_addr;
memset(&server_addr, 0, sizeof(server_addr));
memset(&client_addr, 0, sizeof(client_addr));
server_addr.sin_family = AF_INET;
// 连接本机 127.0.0.1
server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
// 连接端口 6666
server_addr.sin_port = htons(6666);
client_addr.sin_family = AF_INET;
// 连接本机 192.168.10.150
client_addr.sin_addr.s_addr = htonl(INADDR_LOCAL);
// 连接端口 8888
client_addr.sin_port = htons(8888);
// 创建socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
handle_error("socket", sockfd);
temp_result = bind(sockfd, (struct sockaddr *)&client_addr, sizeof(client_addr));
handle_error("bind", temp_result);
// 连接server
temp_result = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
handle_error("connect", temp_result);
// 启动一个子线程,用来读取服务端数据,并打印到 stdout
pthread_create(&pid_read, NULL, read_from_server, (void *)&sockfd);
// 启动一个子线程,用来从命令行读取数据并发送到服务端
pthread_create(&pid_write, NULL, write_to_server, (void *)&sockfd);
// 阻塞主线程
pthread_join(pid_read, NULL);
pthread_join(pid_write, NULL);
printf("关闭资源\n");
close(sockfd);
return 0;
}
在上述例程中,我们将客户端绑定到了192.168.10.150的8888端口,192.168.10.150实际上是本机IP,此处等价于localhost或127.0.0.1。此外,通常服务端不需要绑定到具体的IP和端口,如果不绑定,启动后会操作系统会随机为客户端分配本机的某个端口。我们这里将客户端绑定至指定的IP和端口,主要是为了在分析时便于区分客户端和服务端,实际的客户端程序完全可以省去这一步。
未完待续。。。2024-08-23.。。。