1.IP地址
IP地址是Internet Protocol(网络协议)的缩写,是为了收发网络数据而分配给计算机的值,分为两类:IPv4和IPv6,差别主要是表示IP地址所用的字节数,前者用四字节地址族,后者用16字节地址族。IPv6是为了应对IPv4地址耗尽而提出的标准,2019 年 11 月 25 日已分配完公网 IPv4 地址,但本书还是以IPv4为例子进行展示。
下图展示IPv4地址族,分为A、B、C、D、E(忽略)等类型:
可以通过IP地址的第一个字节即可判断网络地址占用的字节数,进而区分网络地址的类型:
- A类地址:
首字节范围:0到127
首位:0
网络ID占用:1个字节
主机ID占用:3个字节 - B类地址:
首字节范围:128到191
首位:10
网络ID占用:2个字节
主机ID占用:2个字节 - C类地址:
首字节范围:192到223
首位:110
网络ID占用:3个字节
主机ID占用:1个字节
例如,如果一个IP地址的首字节是192(二进制为11000000),我们立即知道这是一个C类地址,网络ID占用3个字节,主机ID占用1个字节.
网络ID是用来标识一个特定网络的。它告诉路由器和其他网络设备,数据应该被发送到哪个网络。
主机ID是用来标识网络中具体设备的。它必须在网络中是唯一的,以确保数据能够被正确地发送到正确的设备。
2.端口号
端口号是计算机为了区分程序中创建的不同套接字,而分配给套接字的序号,由16位组成,端口号唯一,可配分的范围在0~ 65535,其中0~10223是知名端口,一般分配给特定应用程序,所以应当分配范围之外的值。
其次TCP套接字和UDP套接字不会共用端口号,所以允许重复。
3.数据传输过程
下面是基于IP地址的数据传输过程图:
- 主机向203.211.217.202和203.211.172.103传输数据。
- 其中203.211.217和203.211.172是网络ID,通过网络ID可以把数据传输到指定的网络(路由器或交换机)
- 202和103是主机ID,网络(路由器或交换机)通过主机ID将数据传输到指定的设备上
- 操作系统收到数据后,根据数据包里的端口号,将数据传输到对应的程序上
4.IPv4的地址结构体表示
结构体定义如下:
struct sockaddr_in ipv4_address {sa_family_t sin_family; // 地址族,对于IPv4通常是AF_INETin_port_t sin_port; // 端口号,网络字节序struct in_addr sin_addr; // IPv4地址,具体结构见下char sin_zero[8]; // 填充字符,用于与sockaddr结构体兼容
};
//其中的struct 定义如下
struct in_addr {in_addr_t s_addr; // 存储IPv4地址的32位整数,网络字节序
};
①sin_family
表示地址族,对于IPv4地址,它通常被设置为AF_INET,具体分类如下:
AF_LOCAL是为了说明具有多种地址族而添加的
②sin_port
是一个网络字节序的端口号,用于标识特定的服务或应用程序。
③sin_addr
是一个32位的无符号整数,用来存储IPv4地址,且必须以网络字节序(大端序)表示。
④sin_zero
是一个填充字段,用于确保sockaddr_in结构体的大小与sockaddr结构体兼容,因为某些系统调用可能同时处理IPv4和IPv6地址。
5.网络字节序
字节序: 是指计算机处理器在内存中存储多字节数据类型(如整数、长整型、双精度浮点数等)时所采用的字节排列顺序。分为两种:
-
大端序: 高位字节存放到低位地址
0x1234567中,0x12是最高位字节,0x67是最低位字节,大端序中先保存最高位。 -
小端序: 高位字节存放到高位地址
0x1234567中,0x12是最高位字节,0x67是最低位字节,小端序中先保存最低位。
网络字节序:
是一种在互联网上进行通信时使用的字节序,它保证了不同计算机架构之间数据的一致性。网络字节序遵循大端序),即最高有效字节(MSB)存储在最低的内存地址处。
6.字节序转换
为了统一标准,在网络传输前,得先把主机数据数组转化为大端序的网络字节序格式,下面是四种转换字节序的函数:
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
//s指short,l指long,h指主机(host)字节序,n指网络(network)字节序
//htons指,把short类型数据从主机字节序转换为网络字节序
//ntohl指,把long类型数据从网络字节序转换为主机字节序
7.将字符串信息转化为网络字节序
①因为在IPv4的结构体定义中,IPv4地址为32位整数型,所以我们可以通过inet_addr()
函数进行转化,而且该函数还可以检测无效的IP地址,代码如下:
in_addr_t inet_addr(const char *cp);
//cp 是一个指向包含 IPv4 地址点分十进制字符串的字符指针,例如 "192.168.1.1"。
Linux系统下运行代码:
#include <arpa/inet.h> // 包含inet_addr函数的头文件
#include <stdio.h>int main() {const char *ip_str = "1.2.3.4"; // 这是要转换的IP地址字符串in_addr_t ip_addr; // 存储转换后的网络字节序整数型// 使用inet_addr函数将字符串转换为网络字节序的32位整数ip_addr = inet_addr(ip_str);// 检查转换是否成功if (ip_addr == INADDR_NONE) {fprintf(stderr, "Invalid IP address\n");return 1;}// 打印转换后的网络字节序整数printf("The IP address in network byte order is: %u\n", (unsigned int)ip_addr);//输出结果为0x4030201return 0;
}
Windows系统下运行代码:
因为inet_addr()
是Windows Sockets的一部分,所以需要初始化Winsock
#include <winsock2.h> // 包含Winsock的头文件
#include <stdio.h>
#include <stdlib.h>int main() {// 因为inet_addr()是Windows Sockets的一部分,所以需要初始化WinsockWSADATA wsaData;int result = WSAStartup(MAKEWORD(2, 2), &wsaData);if (result != 0) {fprintf(stderr, "WSAStartup failed: %d\n", result);return 1;}const char *ip_str = "1.2.3.4"; // 要转换的IP地址字符串unsigned long ip_addr; // 存储转换后的网络字节序整数型// 使用inet_addr函数将字符串转换为网络字节序的32位整数ip_addr = inet_addr(ip_str);// 检查转换是否成功if (ip_addr == INADDR_NONE) {fprintf(stderr, "Invalid IP address\n");WSACleanup(); // 清理Winsockreturn 1;}// 打印转换后的网络字节序整数printf("The IP address in network byte order is: %#x\n", ip_addr);//输出结果为0x4030201system("pause");// 清理WinsockWSACleanup();return 0;
}
②也可以使用inet_aton()
函数,与inet_addr()
函数不同,inet_aton()
可以将转化后的32位网络字节序自动代入in_addr结构体,所以使用更加频繁
int inet_aton(const char *cp, struct in_addr *inp);
//cp:是一个指向点分十进制IPv4地址字符串的指针。
//inp:是一个指向 in_addr 结构的指针,该结构用于接收转换后的网络字节序的32位整数。
8.将网络字节序转化为字符串信息
使用inet_aton()
函数,将网络字节序ip地址转化为字符串形式,语法如下:
char *inet_ntoa(struct in_addr in);
//in 是一个 struct in_addr 类型的变量,它包含了要转换的网络字节序的IPv4地址。
函数返回值是一个指向静态分配的字符数组的指针,该数组包含了转换后的点分十进制IPv4地址字符串。注意, 返回的字符串是一个指向静态存储的指针,这意味着该字符串不应被修改,并且在每次 inet_ntoa
调用后都可能改变,如果你需要保留这个字符串,应该立即复制它到安全的存储位置。
Linux系统下运行代码:
#include <arpa/inet.h>
#include <stdio.h>int main() {struct in_addr ip_addr;// 假设我们有一个网络字节序的32位整数ip_addr.s_addr = htonl(0x4030201);// 将网络字节序的IPv4地址转换为点分十进制的字符串char *ip_str = inet_ntoa(ip_addr);// 打印转换后的IP地址字符串printf("The IP address is: %s\n", ip_str);//输出结果为4.3.2.1return 0;
}
Windows系统下运行代码:
因为inet_aton()
是Windows Sockets的一部分,所以需要初始化Winsock
#include <winsock2.h> // 包含Winsock的头文件
#include <stdio.h>
#include <stdlib.h>int main() {// 初始化WinsockWSADATA wsaData;int result = WSAStartup(MAKEWORD(2, 2), &wsaData);if (result != 0) {fprintf(stderr, "WSAStartup failed: %d\n", result);return 1;}// 十六进制数转换为网络字节序的整数struct sockaddr_in addr;addr.sin_addr.s_addr= htonl(0x4030201);// 将网络字节序的整数转换回点分十进制的IPv4地址字符串char *ip_str = inet_ntoa(addr.sin_addr);// 打印转换后的IP地址printf("The IP address is: %s\n", ip_str);system("pause");// 清理WinsockWSACleanup();return 0;
}
Windows系统补充说明: WSAStringToAddress()
和WSAAddressToString()
函数,是Windows特有的函数
WSAStringToAddress()
:是 Windows Sockets的一部分,它用于将字符串形式的网络地址转换为相应的 socket 地址结构,且自动填充到sockaddr 中
int WSAStringToAddress(const char *AddressString,int AddressFamily,const struct sockaddr *lpProtocolInfo,struct sockaddr *lpSocketAddress,int *lpAddressLength
);
//AddressString:指向包含地址字符串的指针,例如 "192.168.1.1" 或 "example.com"。
//AddressFamily:指定地址族,对于 IPv4 使用 AF_INET,对于 IPv6 使用 AF_INET6。
//lpProtocolInfo:指向协议特定信息的可选指针,默认为 NULL。
//lpSocketAddress:指向接收转换结果的 sockaddr 结构的指针。
//lpAddressLength:在调用时,指向 sockaddr 结构的大小的指针。在函数返回后,它表示实际填充到结构中的数据的大小。
WSAAddressToString()
:是 Windows Sockets API (Winsock) 的一部分,自动读取sockaddr 结构体,将其中的 socket 地址转换为它的字符串表示形式。
int WSAAddressToStringA(LPSOCKADDR lpsaAddress,DWORD dwAddressLength,LPWSAPROTOCOL_INFOW lpProtocolInfo,LPSTR lpszAddressString,LPDWORD lpdwAddressStringLength
);
//lpsaAddress:指向 sockaddr 结构的指针,包含要转换的地址信息。
//dwAddressLength:sockaddr 结构的大小。
//lpProtocolInfo:指向 WSAPROTOCOL_INFOW 结构的指针,可以为 NULL。
//lpszAddressString:指向缓冲区的指针,该缓冲区接收转换后的地址字符串。
//lpdwAddressStringLength:指向一个 DWORD 的指针,该 DWORD 指定 lpszAddressString 缓冲区的长度。函数返回时,它表示实际存储在缓冲区中的字符数,包括空终止符。
9.套接字初始化
当学习之后,我们再来看之前的代码,就会发现分为几个部分,这里展示套接字初始化部分,,Linux系统和Windows系统初始化部分代码几乎完全一致:
PS: 使用INADDR_ANY
可以自动获取本地计算机的IP地址。
Linux系统
int serv_sock; // 定义服务器套接字
struct sockaddr_in serv_addr; // 定义服务器地址结构体
char* serv_port="9190";//定义端口号//创建套接字
serv_sock=socket(PF_INET, SOCK_STREAM, 0);//地址信息初始化
memset(&servAddr, 0, sizeof(servAddr));// 清空servAddr结构
//设置结构体
servAddr.sin_family = AF_INET;// 设置地址族为IPv4
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置监听的IP地址
servAddr.sin_port = htons(atoi(serv_port));// 设置监听端口//把设置好的地址信息分配给套接字,使用bind函数绑定
if (bind(serv_sock, (struct sockadd* )&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)error_handling("bind() error");//绑定失败则返回异常
Windows系统
SOCKET serv_sock; // 定义服务器套接字
struct sockaddr_in serv_addr; // 定义服务器地址结构体
char* serv_port="9190";//定义端口号//创建套接字
serv_sock=socket(PF_INET, SOCK_STREAM, 0);//地址信息初始化
memset(&servAddr, 0, sizeof(servAddr));// 清空servAddr结构
//设置结构体
servAddr.sin_family = AF_INET;// 设置地址族为IPv4
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置监听的IP地址
servAddr.sin_port = htons(atoi(serv_port));// 设置监听端口//把设置好的地址信息分配给套接字,使用bind函数绑定
if (bind(serv_sock, (struct sockadd* )&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)error_handling("bind() error");//绑定失败则返回异常
10.回顾运行过程
回顾第一章里面的代码运行部分,Linux平台和Windows平台,我们便可以理解为什么那样运行。
上图是开启服务器命令,意味着通过9190
端口创建服务器套接字并运行程序,这里之所以没有输入IP地址,是因为通过INADDR_ANY
已经自动获取了本机的IP地址。
上图是开启客户端命令,意味着尝试连接IP地址127.0.0.1
(这里是本机地址,因为运行程序的服务器和客户端在一台电脑上,正常情况下应该是服务器端IP地址),并连接到服务器端的9190
端口