网络基本常识
国际标准化组织
- ISO:国际标准化组织
开放系统互联7层网络模型
- OSI:开放系统互联(Open System Interconnect)
TCP/IP是一个实际标准,国际标准组织ISO制定了一个OSI七层网络模型。
OSI协议
由低到高:
物理层
- 设备:集线器
-
- 放大信号
-
- 扩展网络物理接口
-
- 作用:进测物理线路,确保数据发送
数据链路层
- 设备:交换机、网桥
-
- 具备自动寻址能力
-
- 交换作用
-
- 作用:确保数据在设备上的可靠的传输
数据链路层包括设备物理编址,网络拓扑结构,错误校验,帧序列和流控。
- 设备物理编址:定义设备在数据链路层的编址方法。
- 案例:内存编址:
- arm:
- 一般用的是统一编址:数据和代码保存到一块内存中,把寄存器当内存来使用。
- x86:
- 一般用的是独立编址:数据和代码分开存储。
- arm:
- 案例:内存编址:
- 网络拓扑结构:定义设备的物理连接方法。
- 总线型网络结构
- 优点:节约成本,结构简单
- 缺点:网络故障,则所有设备异常
- 环型网络结构
- 星型网络结构
- 总线型网络结构
- 错误校验:向上层数据传输错误的警告。
- 和校验:把所有数据累加,判断某位为0或者为1。
- 奇偶校验:检测数据每一位1的个数是奇数还是偶数。
- 如果是奇数:就是奇校验
- 反之,则为偶校验
- 练习:输入一个数,判断是奇校验还是偶校验
- 1 => 奇校验
- 2 => 奇校验
- 3 => 偶校验
- 帧序列:重新整理帧序列及以外帧
- 流控:数据可能发生延迟,则导致数据丢失,要么接受不到。作为数据缓冲。
网络层
- 网络识别设备的唯一标识是ip地址
- 网络硬件识别标识叫物理地址(MAC地址)
- 作用:将网络ip地址转化成对应物理地址
- 设备:路由器
- 有地址翻译、协议转换和数据格式转换等功能
- 地址解析协议:IP => MAC
- 逆地址解析协议:MAC => IP
传输层
认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
认识UDP协议
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;后面再详细讨论
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
- 协议:指的是数据在网络中处理不同方式或者格式。
会话层
- 保证数据的链接
- 会话层提供服务可使应用的建立和保持连接。并使数据获得同步。
表示层
- 表示数据一种体现方式
- 压缩和解压缩
- 加密和解密
应用层
功能
- 提供网络服务给最终用户的应用程序。
- 这些服务通过以下几种协议实现:
常见协议
- HTTP (Hypertext Transfer Protocol):用于在Web服务器和客户端浏览器之间传输超文本数据。
- HTTPS (HTTP Secure):HTTP的安全版本,通过SSL/TLS加密数据传输。
- FTP (File Transfer Protocol):用于文件传输。
- SMTP (Simple Mail Transfer Protocol):用于电子邮件传输。
- DNS (Domain Name System):将域名转换为IP地址。
其他
- 应用层还包含许多其他服务和协议,如Telnet、SSH、NFS等,它们为用户提供特定的网络应用服务。
TCP/IP五层(或四层)模型
TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇.
TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求
- 物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wififi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等. 集线器(Hub)工作在物理层.
- 数据链路层: 负责设备之间的数据帧的传送和识别. 例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作. 有以太网、令牌环网, 无线LAN等标准. 交换机(Switch)工作在数据链路层.
- 网络层: 负责地址管理和路由选择. 例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由). 路由器(Router)工作在网路层.
- 传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机.
- 应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等. 我们的网络编程主要就是针对应用层
帧的大小
类型
- Ethernet II类型
大小
- 最小长度:64字节(6+6+2+46+4)
- 最大长度:1518字节(6+6+2+1500+4)
包含信息
- 目标MAC地址:6字节
- 源MAC地址:6字节
- 类型:2字节
- 校验位:4字节
- 数据:46 ~ 1500字节
- 数据帧:帧头 + IP数据包 + 帧尾
- 帧头:源和目标MAC地址及类型
- 帧尾:校验
- IP数据包:IP头部 + IP数据信息
- IP包头:包括源和目标IP地址,类型,生存周期等
- IP数据信息:TCP包头 + 实际数据
- TCP包头:源和目标主机端口号,顺序号,确认号,校验位
- 数据帧:帧头 + IP数据包 + 帧尾
查看网络配置
- Linux:
ifconfig device_name
- Windows:
ipconfig
IP地址概述
应用区域
- 私有IP:局域网
- 公有IP:因特网
IP地址分类
A类
- 网络号:8位
- 主机号:24位
- 网段:0.0.0.0 ~ 127.255.255.255
- 子网掩码:255.0.0.0
B类
- 网络号:16位
- 主机号:16位
- 网段:128.0.0.0 ~ 191.255.255.255
- 子网掩码:255.255.0.0
C类
- 网络号:24位
- 主机号:8位
- 网段:192.0.0.0 ~ 223.255.255.255
- 子网掩码:255.255.255.0
D类
- 多播地址:1110
- **地址的范围:**224.0.0.0到239.255.255.255
- 用途:用于多播
E类
- 保留地址:11110
- **地址范围:**240.0.0.0到255.255.255.255
- 用途:保留,未分配
同一网段判定
- 如果网络号一致,则表示同一网段
IP数据位数
- IPv4:32位
- IPv6:128位
请注意,上述信息中有一些不准确之处。例如,IP地址的分类和应用区域并不是严格按照地理位置来划分的,而是根据IP地址的分配规则来划分。此外,IP地址的分类中的网段范围也有误,例如A类地址的实际范围是1.0.0.0到126.0.0.0(0.0.0.0和127.0.0.0有特殊用途),B类地址的实际范围是128.0.0.0到191.255.255.255,C类地址的实际范围是192.0.0.0到223.255.255.255。
三次握手机制实现原理
三次握手机制是TCP协议中用于建立连接的过程,其目的是确保两个通信端点都准备好数据交换,并且同步序列号以避免数据包的混淆。以下是三次握手的详细步骤:
- 客户端发送SYN包
- 客户端向服务器发送一个SYN(同步序列编号)包。
- 包含一个序列号
X
,并将SYN标志设置为1。 - 此时,客户端进入SYN_SENT状态。
- 服务器响应ACK和SYN包
- 服务器收到客户端的SYN包后,会发送一个ACK(确认)包作为响应。
- 服务器将ACK标志设置为1,SYN标志也设置为1。
- 服务器发送自己的序列号
Y
,并将确认序号设置为客户端的序列号X
加1。 - 此时,服务器进入SYN_RCVD状态。
- 客户端发送ACK包
- 客户端收到服务器的ACK和SYN包后,会再次发送一个ACK包。
- 客户端将ACK标志设置为1,SYN标志设置为0。
- 客户端发送的确认序号为服务器的序列号
Y
加1。 - 此时,客户端和服务器都进入ESTABLISHED状态,表示连接已经建立。
TCP 客户端流程
1. 创建套接字
int socket(int domain, int type, int protocol);
- 第一个参数:表示地址家族
PF_INET
: 表示IP4协议地址家族
- 第二个参数:表示套接子类型
SOCK_STREAM
:表示流式套接子 TCPSOCK_DGRAM
:表示数据报套接子 UDP
- 第三个参数:表示协议类型 0
IPPROTO_TCP
: 表示tcp类型IPPROTO_UDP
: 表示udp类型
- 返回值:
- 成功:文件描述符
- 失败:-1
2. 请求连接
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
-
第一个参数:表示套接子文件描述符
socket
返回值 -
第二个参数:表示套接子结构体信息
- ipv4结构体信息
man 7 ip
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ u_int16_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { u_int32_t s_addr; /* address in network byte order */ };
- ipv4结构体信息
-
客户端需要调用connect()连接服务器;
-
connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
字节序转换:
-
主机字节序:数据在pc上存放方式
-
网络字节序:数据在网络上存储方式
-
把主机字节序转换成网络字节序
uint32_t htonl(uint32_t hostlong);
长整形uint16_t htons(uint16_t hostshort);
短整形
-
是网络字节序转换成主机字节序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
IP地址转换
- 把ip转成网络字节序ip
int inet_aton(const char *cp, struct in_addr *inp);
- 把网络ip地址转换成主机ip
char *inet_ntoa(struct in_addr in);
- 把主机ip地址转换成网络ip地址
in_addr_t inet_addr(const char *cp);
- 把主机ip 转换成网络二进制地址
in_addr_t inet_network(const char *cp);
- 第三个参数:表示结构体大小
- 返回值:
- 成功:0
- 失败:-1
TCP 服务器流程
1. 创建套接字
int socket(int domain, int type, int protocol);
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
- 应用程序可以像读写文件一样用read/write在网络上收发数据;
- 如果socket()调用出错则返回-1;
- 对于IPv4, family参数指定为AF_INET;
- 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
- protocol参数的介绍从略,指定为0即可
2. 绑定端口和IP
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
-
第一个参数:表示套接子文件描述符
-
第二个参数:表示绑定自己结构体信息
-
第三个参数:结构体大小
-
返回值:
- 成功:0
- 失败:-1
-
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
-
bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
-
前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
3. 监听用户连接
int listen(int sockfd, int backlog);
-
第一个参数:表示套接子文件描述符
-
第二个参数:表示监听用户个数
-
返回值:
- 成功:0
- 失败:-1
-
listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节同学们课后深入研究;
4. 接受用户连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
第一个参数:表示建立连接用户套接子的
socket
返回值 -
第二个参数:表示连接用户结构体信息
-
第三个参数:表示结构体大小
-
返回值:
- 成功:表示接受用户套接子文件描述符
- 失败:-1
-
三次握手完成后, 服务器调用accept()接受连接;
-
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
-
addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
-
如果给addr 参数传NULL,表示不关心客户端的地址;
-
addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
端口分配规则
服务端口
- 来源:用户指定
- 条件:如果用户指定的服务端口被其他服务所占用,则不可以使用。
数据端口
- 来源:系统随机分配
- 影响:如果数据端口被占用,则用户指定的服务端口依然不可以使用。
相关函数
int getsockname(int s, struct sockaddr *name, socklen_t *namelen);
int main() {
int sockfd;
struct sockaddr_in my_addr;
socklen_t my_addr_len;
// 创建一个socket
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务端地址结构
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET; // IPv4
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听任何地址
my_addr.sin_port = htons(0); // 系统随机分配端口
// 绑定socket到地址
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 获取绑定后的地址信息
my_addr_len = sizeof(my_addr);
if (getsockname(sockfd, (struct sockaddr *)&my_addr, &my_addr_len) == -1) {
perror("getsockname");
exit(EXIT_FAILURE);
}
// 打印本地地址和端口
printf("Local IP address is: %s\n", inet_ntoa(my_addr.sin_addr));
printf("Local port is: %d\n", ntohs(my_addr.sin_port));
// 关闭socket
close(sockfd);
return 0;
}
UDP客户端操作流程
-
创建套接字
- 使用
socket
函数。
- 使用
-
发送数据
- 使用
sendto
函数。ssize_t sendto(int s, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
- 第一个参数:
s
- 表示套接字文件描述符。 - 第二个参数:
buf
- 表示发送数据空间的地址。 - 第三个参数:
len
- 表示发送数据的大小。 - 第四个参数:
flags
- 表示标志位,通常为 0。 - 第五个参数:
to
- 表示目标地址结构体。 - 第六个参数:
tolen
- 表示目标结构体的大小。 - 返回值:
- 成功:表示发送的字节数。
- 失败:返回 -1。
- 第一个参数:
- 使用
UDP服务器端操作流程
-
创建套接字
- 使用
socket
函数。
- 使用
-
绑定
- 使用
bind
函数。
- 使用
-
接收数据
- 使用
recvfrom
函数。ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
- 第一个参数:
s
- 表示socket
函数的返回值。 - 第二个参数:
buf
- 表示接收数据空间的地址。 - 第三个参数:
len
- 表示空间的大小。 - 第四个参数:
flags
- 表示标志位,通常为 0。 - 第五个参数:
from
- 表示连接用户套接字信息结构体。 - 第六个参数:
fromlen
- 表示结构体的大小。 - 返回值:
- 失败:返回 -1。
- 成功:返回接受数据的个数(字节)。
- 第一个参数:
- 使用
广播地址设置
广播地址 192.168.2.255
对应于子网 192.168.2.0/24
。
实现广播
1. 发送广播(客户端)
要实现广播,客户端需要设置套接字为广播方式。
使用 setsockopt
函数来设置套接字选项:
int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
int main() {
int sockfd;
struct sockaddr_in broadcast_addr;
int broadcast_enable = 1;
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置套接字选项以允许广播
if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast_enable, sizeof(broadcast_enable)) == -1) {
perror("setsockopt (SO_BROADCAST)");
exit(EXIT_FAILURE);
}
// 设置广播地址
memset(&broadcast_addr, 0, sizeof(broadcast_addr));
broadcast_addr.sin_family = AF_INET;
broadcast_addr.sin_addr.s_addr = inet_addr("192.168.2.255"); // 广播地址
broadcast_addr.sin_port = htons(12345); // 广播端口
// 发送广播消息
const char *message = "Hello, this is a broadcast message!";
if (sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&broadcast_addr, sizeof(broadcast_addr)) == -1) {
perror("sendto");
exit(EXIT_FAILURE);
}
printf("Broadcast message sent.\n");
// 关闭套接字
close(sockfd);
return 0;
}
- 第一个参数:
s
- 表示套接字文件描述符(socket
函数的返回值)。 - 第二个参数:
level
- 表示选项所在的协议层。SOL_SOCKET
- 表示基本套接字类型。IPPROTO_IP
- 表示 IPv4 级别类型。IPPROTO_IPV6
- 表示 IPv6 级别类型。
- 第三个参数:
optname
- 表示要操作的选项。SO_BROADCAST
- 允许发送广播数据包。SO_REUSEADDR
- 允许重用本地地址和端口。
- 第四个参数:
optval
- 指向存放选项值的缓冲区。- 通常设置为
int
值,表示布尔开关(1 表示启用,0 表示禁用)。
- 通常设置为
- 第五个参数:
optlen
- 表示optval
缓冲区的大小。 - 返回值:
- 成功:返回 0。
- 失败:返回 -1。
接收广播(服务器端)
服务器端在接收广播之前需要设定端口重用。
同样使用 setsockopt
函数:
int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
int main() {
int sockfd;
struct sockaddr_in server_addr;
int reuse_addr = 1;
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置套接字选项以允许地址重用
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse_addr, sizeof(reuse_addr)) == -1) {
perror("setsockopt (SO_REUSEADDR)");
exit(EXIT_FAILURE);
}
// 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听任何地址
server_addr.sin_port = htons(12345); // 监听端口
// 绑定套接字
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 接收广播消息
char buffer[1024];
struct sockaddr_in sender_addr;
socklen_t sender_addr_len = sizeof(sender_addr);
ssize_t num_bytes;
if ((num_bytes = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&sender_addr, &sender_addr_len)) == -1) {
perror("recvfrom");
exit(EXIT_FAILURE);
}
buffer[num_bytes] = '\0'; // 确保消息以空字符结尾
printf("Received message: %s\n", buffer);
// 关闭套接字
close(sockfd);
return 0;
}
参数与发送广播时相同,但在此场景中,你需要设置 SO_REUSEADDR
选项,以便多个进程可以绑定到同一个端口。
level
应设置为SOL_SOCKET
。optname
应设置为SO_REUSEADDR
。optval
应指向一个int
值,设置为 1 表示启用端口重用。optlen
应设置为sizeof(int)
。