计算机网络——网络模型

1. 数据传输过程

1.1 数据包首部

每个分层中,都会对发送的数据附加一个首部。网络中传输的数据包包含两部分:协议所要用到的首部、上层传输的数据。首部规定了协议如何读取数据,比如应该从包的哪一位开始读取,读取多少个比特,如何校验,插入到哪一位等。

在这里插入图片描述

1.2 发送数据包

在这里插入图片描述

在这里插入图片描述

2. 数据链路层

2.1 MAC地址

MAC地址以16进制表示,每一位占4比特,共12位,例:08:00:20:74:CE:E5

在这里插入图片描述

3. 网络层

3.1 IP地址

IP地址(IPv4地址)由32位正整数表示,将32位地址分为4组,每8位为一组,每组用“.”隔开,再转换为10进制数。IPv6的IP地址长度为128位。

在这里插入图片描述

IP地址分类:

  • A类地址:以0开头,第1位到第8位是网路标识,后24位是主机标识。该网段内容纳的主机地址上限为16777214个。0.0.0.0 ~ 127.0.0.0
  • B类地址:以10开头,第1位到第16位是网路标识,后16位是主机标识。该网段内容纳的主机地址上限为65534个。128.0.0.0 ~ 191.255.0.0
  • C类地址:以110开头,第1位到第24位是网路标识,后8位是主机标识。该网段内容纳的主机地址上限为254个。192.0.0.0 ~ 223.255.255.0
  • D类地址:以1110开头,第1位到第32位是网路标识,无主机标识。224.0.0.0 ~ 239.255.255.255

IP地址主机标识不可全为0或全为1。全为0表示对应的网络地址不可获知,全为1表示广播地址。

广播地址用于在同一个链路中相互连接的主机之间发送数据包。

  • IP地址:172.20.0.0(10101100.00010100.00000000.00000000)
  • 对应的广播地址为:172.20.255.255(10101100.00010100.11111111.11111111)

随着互联网的普及,一个IP地址的网络标识与主机标识不再受限于地址的类别,而是通过子网掩码来细分出比A类、B类等更小粒度的分类。子网掩码对应的IP地址网络标识部分全为1,对应的主机标识部分全为0。

  • IP地址:172.20.100.52(前26位为网络地址)
  • 子网掩码:255.255.255.192(将26位网络地址置1,主机地址置0)
  • 广播地址:172.20.100.63(将后6位主机地址全置1)
  • 子网掩码:255.255.255.192

简化表示方式:

  • IP地址:172.20.100.52/26(前26位为网络地址,用/追加网络地址的位数)
  • 广播地址:172.20.100.63/26(将后6位主机地址全置1)

IP包首部:

在这里插入图片描述

3.2 已知IP地址求网络号

已知IP地址为192.168.1.180

1. 属于C类地址:192.0.0.0 ~ 223.255.255.0
2.1位到第24位是网路标识,后8位是主机标识,默认子网掩码:网络标识全为1、主机标识全为0255.255.255.0
3.1位到第24位是网路标识,构成网络号:192.168.1
4.8位是主机标识,构成主机号:180
已知IP地址190.20.223.100,子网掩码为255.255.128.0

1. 将IP地址转为二进制:10111110.00010100.11011111.01100100
2. 将子网掩码转为二进制:11111111.11111111.10000000.00000000,子网掩码中连续0的个数 = n = 15
3. 【主机号】 = 2 ^ n - 2 = 2 ^ 15 - 2 = 32766
4. 【网络地址】:将IP地址与子网掩码与运算:10111110.00010100.10000000.00000000(全11,其余为0),转十进制:190.20.128.0
5. 【广播地址】:将网络地址中连续为0的部分改成110111110.00010100.11111111.11111111】,转十进制:190.20.255.255
6. 【IP地址范围】:网络地址 + 1 ~ 广播地址 - 1,即190.20.128.1 ~ 190.20.255.254
7. 【掩码位】:子网掩码中连续为1的部分:【11111111.11111111.10000000.00000000,连续的1个数为17,掩码位即17,即190.20.223.100/17

4. 运输层

4.1 UDP用户数据报协议

无连接的,不可靠的服务。

UDP客户与服务器之间不必存在任何长期的关系。举例来说,一个UDP客户可以创建一个套接字并发送一个数据报给一个给定的服务器,然后立即用同一个套接字发送另一个数据报给另一个服务器。同样地,一个UDP服务器可以用同一个UDP套接字从若干个不同的客户接收数据报,每个客户一个数据报。

4.2 TCP传输控制协议

参考:UNIX网络编程 卷1:套接字联网 API

4.2.1 特性

  • 面向连接的

TCP客户先与某个给定服务器建立连接,再通过连接与服务器交换数据,最后终止连接。

  • 可靠的

向对端发送数据时,需要对端返回一个确认。如果超过RTT(TCP使用算法估计数据在客户与服务器之间的往返时间:RTT)时间,没有收到确认,自动重传。
TCP将数据中的每个字节关联一个序列号,并依据序列号对数据进行排序。假设一个应用写2048字节到一个TCP套接字,导致TCP发送2个分节(分节是TCP传递给IP的数据单元):第一个分节所含数据的序列号为1 ~ 1024,第二个分节所含数据的序列号为1025 ~ 2048。如果这些分节非顺序到达,接收端TCP将先根据它们的序列号重新排序,再把结果数据传递给接收应用。如果接收端TCP接收到来自对端的重复数据(对端认为已丢失并因此重传,而这个分节并没有真正丢失,只是网络通信过于拥挤),它可以(根据序列号)判定数据是重复的,从而丢弃重复数据。

  • 流量控制

TCP总是告知对端在任何时刻它一次能够从自己这端接收多少字节的数据,这称为通告窗口。在任何时刻,该窗口指出接收缓冲区中当前可用的空间量,从而确保发送端发送的数据不会使接收缓冲区溢出。该窗口时刻动态变化:当接收到来自发送端的数据时,窗口大小就会减小;当接收端应用从缓冲区中读取数据时,窗口大小就会增大。当TCP对应某个套接字的接收缓冲区已满,通告窗口大小减小到0,导致它必须等待应用从该缓冲区读取数据时,方能从对端再接收数据。

  • 全双工

这意味着在一个给定的连接上,应用可以在任何时刻,在进出两个方向上既发送数据又接收数据。因此,TCP必须为每个数据流方向跟踪诸如序列号和通告窗口大小等状态信息。

4.2.2 状态转换

  • 三次握手,数据交换需要3个分组(分节)。客户端初始序列号x,发送SYN到服务器,服务器返回的ACK序列号为x + 1。服务器初始序列号y,发送SYN到客户端,客户端返回ACK序列号为y + 1。
  • 每个SYN包含多个TCP选项:MSS选项(对端接受的最大分节大小)、窗口规模(影响吞吐量)、时间戳(防止数据损坏)
  • 四次挥手,数据交换需要4个分组。
    在这里插入图片描述
  • 状态转换图
    在这里插入图片描述
  • TIME_WAIT

客户端停留在该状态的时间是2MSL(最长分节生命期),有2个存在理由:

  1. 可靠实现TCP全双工连接的终止
  2. 允许老的重复分节在网络中消失

4.2.3 缓冲区

TCP协议是作用是用来进行端对端数据传送的,那么就会有发送端和接收端,在操作系统有两个空间即user space和kernal space。

每个Tcp socket连接在内核中都有一个发送缓冲区和接收缓冲区,TCP的全双工的工作模式以及TCP的流量(拥塞)控制便是依赖于这两个独立的buffer以及buffer的填充状态。

  • 单工:只允许甲方向乙方传送信息,而乙方不能向甲方传送 ,如汽车单行道。

  • 半双工:半双工就是指一个时间段内只有一个动作发生,甲方可以向乙方传送数据,乙方也可以向甲方传送数据,但不能同时进行,如一条窄马路同一时间只能允许一个车通行。

  • 全双工:同时允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合。

一个socket的两端,都会有send和recv两个方法,如client发送数据到server,那么就是客户端进程调用send发送数据,而send的作用是将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。

4.2.4 套接字

参考链接:socket是什么?套接字是什么?
在这里插入图片描述
图源:UNIX网络编程 卷Ⅰ:套接字联网API

  • IPV4套接字地址结构(POSIX定义)
#include <netinet/in.h>
struct in_addr {
    in_addr_t       s_addr;             // 32位IPv4地址,网络字节序
}; 
struct sockaddr_in {
    uint8_t         sin_len;			// 结构长度:16
    sa_family_t     sin_family;         // 协议类型
    in_port_t       sin_port;           // 端口号
 
    struct in_addr  sin_addr;           
 
    char            sin_zero[8];
}
  • 通用套接字地址结构:套接字地址结构以引用(指针)形式作为参数传递到套接字函数中,而函数需要处理来自所有支持的任何协议族的套接字地址结构。所以需要将其转换为通用套接字地址结构
struct sockaddr{
	uint8_t      	sa_len;				// 结构长度
    sa_family_t  	sa_family;   		// 地址族(Address Family),也就是地址类型
    char         	sa_data[14];  		// IP地址和端口号
};
  • IPV6套接字地址结构
struct in6_addr {
    uint8_t			s6_addr[16];        // 128位IPv6地址,网络字节序
}; 
#define SIN6_LEN
struct sockaddr_in {
    uint8_t         sin6_len;			// 结构长度:28
    sa_family_t     sin6_family;        // 协议类型
    in_port_t       sin6_port;          // 端口号
 	
 	uint32_t		sin6_flowonfo;
    struct in6_addr sin6_addr;           
 
    uint32_t        sin6_scope_id;
}
(1)socket函数

客户+服务器

#include <sys/socket.h>
int socket(int af, int type, int protocol);
// 成功返回非负描述符、出错返回-1
  • af:地址族(Address Family),常用的有 AF_INET (IPV4,最典型的是本机地址:127.0.0.1)和 AF_INET6(IPV6)
  • type:套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字)和 SOCK_DGRAM(数据报套接字/无连接的套接字)
  • protocol:传输协议,常用的有 IPPROTO_TCP(TCP协议) 和 IPPTOTO_UDP(UDP协议)

通常情况下,已知af、type就可以确定protocol。但是特殊情况,有两种protocol会使用相同的af、type,这时就必须指定protocol

// 标准
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 简化,在IPV4下,使用面向连接的只有TCP,protocol使用0代替
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
  • socket函数成功后返回int,表示套接字描述符sockfd。此时,只指定协议族(IPV4、IPV6、Unix)、套接字类型,并没有指定本地或远程协议地址
(2)connect函数

客户用connect函数执行三次握手,在连接建立成功或失败后才会返回结果

#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
// 成功返回0、出错返回-1
  • sockfd为 socket 文件描述符
  • *serv_addr为指向套接字地址结构的指针,包含服务器的IP地址和端口号
  • addrlen 为 addr 变量的大小,可由 sizeof() 计算得出
(3)bind函数

服务器用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理

#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);
// 成功返回0、出错返回-1
  • sockfd为 socket 文件描述符
  • *serv_addr为指向套接字地址结构的指针,包含服务器的IP地址和端口号
  • addrlen 为 addr 变量的大小,可由 sizeof() 计算得出

实际使用中,使用 sockaddr_in 结构体,然后再强制转换为 sockaddr 类型:

// 创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

// 创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));  // 每个字节都用0填充
serv_addr.sin_family = AF_INET;  // 使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 具体的IP地址
serv_addr.sin_port = htons(1234);  // 端口

// 将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

注意这里的sockaddr 结构,占16字节,其中sa_data占14字节,表明IP地址和端口号。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“

struct sockaddr{						// 通用套接字地址结构
	uint8_t      	sa_len;				// 结构长度:16
    sa_family_t  	sa_family;   		// 地址族(Address Family),也就是地址类型
    char         	sa_data[14];  		// IP地址和端口号
};

而sockaddr_in结构:

struct sockaddr_in{						// IPV4套接字地址结构
	uint8_t      	sin_len;			// 结构长度:16
    sa_family_t     sin_family;   		// 地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     		// 16位的端口号,占2字节
    struct in_addr  sin_addr;     		// 32位IP地址
    char            sin_zero[8];  		// 不使用,一般用0填充,8字节
};
struct in_addr{
    in_addr_t  		s_addr;  			//32位的IP地址
};

in_addr_t 在头文件 <netinet/in.h> 中定义,等价于 unsigned long,长度为4个字节。也就是说,s_addr 是一个整数,而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换,例如:

unsigned long ip = inet_addr("127.0.0.1");
printf("%ld\n", ip);

如果TCP服务器没有bind就调用listen,内核需要为相应的套接字选定一个临时端口

(4)listen函数

服务器调用listen将未连接的套接字转换为被动监听套接字,指示内核接受指向该套接字的连接请求。即从CLOSED状态转换为LISTEN状态。

int listen(int sockfd, int backlog); 
// 成功返回0、出错返回-1

内核会为套接字维护两个队列:

  • 未完成连接队列:SYN分节已由客户发出到达服务器,服务器正在等待完成三次握手建立连接的过程。套接字处于SYN_RCVD状态
  • 已完成连接队列:已完成三次握手建立连接,处于ESTABLISHED状态
  • backlog 为两队列长度之和的最大值

在这里插入图片描述

(5)accept函数

服务器,当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 成功返回非负描述符、出错返回-1

accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sockfd 是服务器端的套接字

(6)write

客户+服务器:调用write,内核从应用进程的缓冲区中复制所有数据到所写套接字的发送缓冲区

(7)read

客户+服务器:调用read,内核从套接字的接收缓冲区读取数据

  • read请求
  • 系统调用 —— 内核进程切换
  • 初始化【读IO】
  • 此时read结果为空,返回异常:EAGAIN(error:again,再试一次)、EWOULDBLOCK(error:operation would block,操作阻塞)
  • IO复用模型:read响应,返回结果
  • 再次read请求
  • 系统调用 —— 内核进程切换
  • 数据从内核空间移到用户空间

在这里插入图片描述

(8)close函数

客户+服务器:关闭套接字,终止TCP连接

(9)并发服务器

实际使用中,一个服务器会同时服务好几个客户,需要fork子进程

pid_t 	pid;
int 	listenfd,connfd;

// 1. socket
listenfd = socket(...);
// 2. bind
bind(listenfd, ...);
// 3. listen
listen(listenfd, ...);
for ( ; ; ) {
	// 4. accept
    connfd = accept(listenfd,...);
    // 5. fork子进程
    if ((pid = fork()) == 0) {
        close(listenfd);//子进程关闭listening socket
        doit(connfd);	//处理请求
        close(connfd);	//客户执行完毕,关闭已经连接的socket
        exit(0);		//子进程结束
    }
    close(connfd);		//父进程关闭已经连接的socket
}

5. IO多路复用模型

5.1 select

5.2 poll

5.3 epoll

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值