赞
踩
IP地址是Internet Protocol(网络协议)的缩写,是为了收发网络数据而分配给计算机的值,分为两类:IPv4和IPv6,差别主要是表示IP地址所用的字节数,前者用四字节地址族,后者用16字节地址族。IPv6是为了应对IPv4地址耗尽而提出的标准,2019 年 11 月 25 日已分配完公网 IPv4 地址,但本书还是以IPv4为例子进行展示。
下图展示IPv4地址族,分为A、B、C、D、E(忽略)等类型:
可以通过IP地址的第一个字节即可判断网络地址占用的字节数,进而区分网络地址的类型:
例如,如果一个IP地址的首字节是192(二进制为11000000),我们立即知道这是一个C类地址,网络ID占用3个字节,主机ID占用1个字节.
网络ID是用来标识一个特定网络的。它告诉路由器和其他网络设备,数据应该被发送到哪个网络。
主机ID是用来标识网络中具体设备的。它必须在网络中是唯一的,以确保数据能够被正确地发送到正确的设备。
端口号是计算机为了区分程序中创建的不同套接字,而分配给套接字的序号,由16位组成,端口号唯一,可配分的范围在0~ 65535,其中0~10223是知名端口,一般分配给特定应用程序,所以应当分配范围之外的值。
其次TCP套接字和UDP套接字不会共用端口号,所以允许重复。
下面是基于IP地址的数据传输过程图:
结构体定义如下:
struct sockaddr_in ipv4_address {
sa_family_t sin_family; // 地址族,对于IPv4通常是AF_INET
in_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地址。
字节序: 是指计算机处理器在内存中存储多字节数据类型(如整数、长整型、双精度浮点数等)时所采用的字节排列顺序。分为两种:
大端序: 高位字节存放到低位地址
0x1234567中,0x12是最高位字节,0x67是最低位字节,大端序中先保存最高位。
小端序: 高位字节存放到高位地址
0x1234567中,0x12是最高位字节,0x67是最低位字节,小端序中先保存最低位。
网络字节序:
是一种在互联网上进行通信时使用的字节序,它保证了不同计算机架构之间数据的一致性。网络字节序遵循大端序),即最高有效字节(MSB)存储在最低的内存地址处。
为了统一标准,在网络传输前,得先把主机数据数组转化为大端序的网络字节序格式,下面是四种转换字节序的函数:
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类型数据从网络字节序转换为主机字节序
①因为在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); //输出结果为0x4030201 return 0; }
Windows系统下运行代码:
因为inet_addr()
是Windows Sockets的一部分,所以需要初始化Winsock
#include <winsock2.h> // 包含Winsock的头文件 #include <stdio.h> #include <stdlib.h> int main() { // 因为inet_addr()是Windows Sockets的一部分,所以需要初始化Winsock WSADATA 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(); // 清理Winsock return 1; } // 打印转换后的网络字节序整数 printf("The IP address in network byte order is: %#x\n", ip_addr); //输出结果为0x4030201 system("pause"); // 清理Winsock WSACleanup(); 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位整数。
使用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.1 return 0; }
Windows系统下运行代码:
因为inet_aton()
是Windows Sockets的一部分,所以需要初始化Winsock
#include <winsock2.h> // 包含Winsock的头文件 #include <stdio.h> #include <stdlib.h> int main() { // 初始化Winsock WSADATA 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"); // 清理Winsock WSACleanup(); 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 缓冲区的长度。函数返回时,它表示实际存储在缓冲区中的字符数,包括空终止符。
当学习之后,我们再来看之前的代码,就会发现分为几个部分,这里展示套接字初始化部分,,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");//绑定失败则返回异常
回顾第一章里面的代码运行部分,Linux平台和Windows平台,我们便可以理解为什么那样运行。
上图是开启服务器命令,意味着通过9190
端口创建服务器套接字并运行程序,这里之所以没有输入IP地址,是因为通过INADDR_ANY
已经自动获取了本机的IP地址。
上图是开启客户端命令,意味着尝试连接IP地址127.0.0.1
(这里是本机地址,因为运行程序的服务器和客户端在一台电脑上,正常情况下应该是服务器端IP地址),并连接到服务器端的9190
端口
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。