分配给套接字的 IP 地址与端口号
(一) IP 地址
定义
IP地址是分配给网络上每个设备的唯一标识符,用于在网络中定位和通信。
类型
- IPv4 (Internet Protocol version 4):4字节地址族
- IPv6 (Internet Protocol version 6):16字节地址族
IPv4 标准的4字节 IP 地址分为网络地址和主机地址,且分为 A、B、C、D、E 等类型:
网络地址是为了区分网络而设置的一部分 IP 地址。
假如要向 M 网络中的某个主机传输数据,并非一开始就浏览所有 IP 地址,进而找到目标主机;而是首先浏览网络地址,找到 M 网络,先把数据传输到 M 网络,M 网络接收到数据后,浏览传数据的主机地址并将数据传给目标计算机:
主机地址边界(IPv4)
- A类地址的首字节范围:
00000000
(0)到01111111
(127) - B类地址的首字节范围:
10000000
(128)到10111111
(191) - C类地址的首字节范围:
11000000
(192)到11011111
(223)
(二)用于区分套接字的端口号
IP 用于区分计算机,只要有 IP 地址就能向目标主机传输数据,但仅凭这些无法传输给最终的应用程序,即仅凭 IP 地址无法区分传输到计算机的网络数据发给哪个应用程序。
计算机中一般配有 NIC (Network Interface Card,网络接口卡)数据传输设备。通过 NIC 向计算机内部传输数据时会用到 IP。操作系统负责把传递到内部的数据适当分配给套接字,此时就需要利用端口号(port)。通过 NIC 接收的数据内有端口号,操作系统正是参考此端口把数据传输给相应端口的套接字:
地址信息的表示
应用程序中使用的 IP 地址和端口号以结构体的形式给出了定义。
(一)表示 IPv4 地址的结构体
c
struct sockaddr_in
{
sa_family_t sin_family; // 地址族(Address Family)
uint16_t sin_port; // 16位 TCP/UDP 端口号
struct in_addr sin_addr; // 32位 IP 地址
char sin_zero[8]; // 不使用
};
struct in_addr
{
In_addr_t s_addr; // 32位 IPv4 地址
};
(二)结构体成员分析
-
sin_family
:- 类型 :
sa_family_t
- 说明 :表示地址族。对于 IPv4 地址,通常设置为
AF_INET
。这告诉系统该结构体包含的是 IPv4 地址信息。
- 类型 :
-
sin_port
:- 类型 :
uint16_t
- 说明 :表示16位的端口号。端口号用于区分同一台计算机上的不同服务。端口号的值是网络字节序(大端序),因此在赋值时需要使用
htons()
函数来转换主机字节序到网络字节序。
- 类型 :
-
sin_addr
:- 类型 :
struct in_addr
- 说明 :表示32位的 IPv4 地址。
struct in_addr
中的字段是s_addr
,它存储了 IP 地址的实际值,通常也是网络字节序。
- 类型 :
-
sin_zero
:- 类型 :
char[8]
- 说明 :这是一个填充字段,大小为8字节。它用于确保
struct sockaddr_in
与struct sockaddr
结构体对齐,保持结构体的大小一致。这个字段在实际使用中不需要设置或使用。
- 类型 :
c
struct sockaddr_in serv_addr;
if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1)
error_handling("bind() error");
sockaddr_in
结构体变量的地址值将以如上方式传递给 bind
函数,可以看到第二个参数期望得到 sockaddr
结构体变量地址值,包括地址族、端口号、IP 地址等。sockaddr
结构体如下:
c
struct sockaddr
{
sa_family_t sin_family; //地址族
char sa_data[14]; // 地址信息
};
此结构体成员 sa_data
保存的地址信息中需要包含 IP 地址和端口号,剩余部分应填充0。这对于包含地址信息来讲非常麻烦,所以有了新的结构体 sockaddr_in
。
网络字节序与地址变换
(一)字节序
不同 CPU 中,4字节整数型值1在内存空间中保存的方式是不同的。4字节整数型值1可用二进制表示如下:
00000000 00000000 00000000 00000001
有些 CPU 以这种顺序保存到内存,另外一些 CPU 则以倒序保存:
00000001 00000000 00000000 00000000
CPU 向内存保存数据的方式有两种,所以 CPU 解析数据的方式也分为两种:
- 大端序(Big Endian):高位字节存放到低位地址。
在大端序中,整数 0x12345678
的高位字节(0x12
)存储在最低地址,低位字节(0x78
)存储在最高地址。
地址 | 值 |
---|---|
0x00 | 0x12 |
0x01 | 0x34 |
0x02 | 0x56 |
0x03 | 0x78 |
- 小端序(Little Endian):高位字节存放到高位地址。
在小端序中,整数 0x12345678
的高位字节(0x12
)存储在最高地址,低位字节(0x78
)存储在最低地址。
地址 | 值 |
---|---|
0x00 | 0x78 |
0x01 | 0x56 |
0x02 | 0x34 |
0x03 | 0x12 |
(二)网络字节序
由于大端序和小端序的存在,字节序不同的计算机之间传输数据过程中可能会出现问题,所以,在通过网络传输数据时约定统一方式,这种约定称为网络字节序(Network Byte Order)------------统一为大端序。
(三)字节序转换
字节序转换函数:
c
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
htons
中的h
代表主机(host)字节序。htons
中的n
代表网络(network)字节序。s
指的是short
。l
指的是long
。
所以 htons
解释为 "把 short
型数据从主机字节序转化为网络字节序"。
网络地址的初始化与分配
(一)将字符串信息转换为网络字节序的整数型
对于 IP 地址的表示,较为熟悉的是点分十进制表示法(Dotted Decimal Notation)例如 201.211.214.36,而非整数型数据表示法。幸运的是,有函数会帮助我们将字符串形式的 IP 地址转换成32位整数型数据,这些函数在转换类型的同时进行网络字节序转换。
(1) inet_addr
函数
c
#include <arpa/inet.h>
// 成功时返回32位大端序整数型值,失败时返回 INAADDR_NONE
in_addr_t inet_addr(const char* string);
Linux 平台调用过程
c
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
char *addr1="127.212.124.78";
// 错误的 IP 地址,验证函数的错误检测能力
char *addr2="127.212.124.256";
unsigned long conv_addr=inet_addr(addr1);
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n", conv_addr);
// 调用出现异常
conv_addr=inet_addr(addr2);
if(conv_addr==INADDR_NONE)
printf("Error occureded \n");
else
printf("Network ordered integer addr: %#lx \n\n", conv_addr);
return 0;
}
(2) inet_aton
函数
c
#include <arpa/inet.h>
// 成功时返回1(true),失败时返回0(false)
int inet_aton(const char* string, struct in_addr* addr);
string
:待转换的字符串地址值。addr
:转换结果的结构体变量的地址值。
Linux 平台调用过程
c
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
char *addr="127.232.124.79";
struct sockaddr_in addr_inet;
if(!inet_aton(addr, &addr_inet.sin_addr))
error_handling("Conversion error");
else
printf("Network ordered integer addr: %#x \n", addr_inet.sin_addr.s_addr);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
有一个与 inet_aton
函数正好相反的函数 inet_ntoa
,这个函数可以把网络字节序整数型 IP 地址转化为我们熟悉的字符串形式。
c
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
struct sockaddr_in addr1, addr2;
char *str_ptr;
char str_arr[20];
addr1.sin_addr.s_addr=htonl(0x1020304);
addr2.sin_addr.s_addr=htonl(0x1010101);
str_ptr=inet_ntoa(addr1.sin_addr);
strcpy(str_arr, str_ptr);
printf("Dotted-Decimal notation1: %s \n", str_ptr);
inet_ntoa(addr2.sin_addr);
printf("Dotted-Decimal notation2: %s \n", str_ptr);
printf("Dotted-Decimal notation3: %s \n", str_arr);
return 0;
}
/*
root@com:/home/swyoon/tcpip# gcc inet_ntoa.c -o ntoa
root@com:/home/swyoon/tcpip# ./ntoa
Dotted-Decimal notation1: 1.2.3.4
Dotted-Decimal notation2: 1.1.1.1
Dotted-Decimal notation3: 1.2.3.4
*/
(3)inet_addr
和 inet_ntoa
函数在 Windows 平台的调用过程
c
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
void ErrorHandling(char* message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
/* inet_addr 函数调用过程 */
{
char *addr="127.212.124.78";
unsigned long conv_addr=inet_addr(addr);
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n", conv_addr);
}
/* inet_ntoa 函数调用过程 */
{
struct sockaddr_in addr;
char *strPtr;
char strArr[20];
addr.sin_addr.s_addr=htonl(0x1020304);
strPtr=inet_ntoa(addr.sin_addr);
strcpy(strArr, strPtr);
printf("Dotted-Decimal notation3 %s \n", strArr);
}
WSACleanup();
return 0;
}
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
(二)网络地址初始化
c
struct sockaddr_in addr;
char* serv_ip = "211.217.168.13"; // 声明IP地址字符串
char* serv_port= "9190"; // 声明端口号字符串
memset(&addr, 0, sizeof(addr)); // 初始化为0
addr.sin_family = AF_INET; // 指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); // 基于字符串的IP地址初始化
addr.sin_port = htons(atoi(serv_port)); // 基于字符串的端口号初始化
每次创建服务器端套接字都要输入 IP 地址会有些繁琐,可如下初始化地址信息:
c
struct sockaddr_in addr;
char* serv_port= "9190";
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));
INADDR_ANY
可自动获取服务器端的计算机 IP 地址。当你使用 INADDR_ANY
绑定套接字时,套接字将被绑定到计算机的所有网络接口。这意味着服务器会接收来自任何网络接口的流量。
问答
① 通过 IP 可以区分哪些对象?通过端口号可以区分哪些对象?
-
IP 地址:用于唯一标识网络中的设备或网络接口,并可以区分不同的设备、子网和 ISP。
-
端口号:用于在单一设备上区分不同的应用程序、服务实例和协议类型。它确保数据包能够正确地传递到对应的服务。
② 什么是知名端口?其范围是多少?知名端口中具有代表性的 HTTP 和 FTP 端口号各是多少?
-
知名端口:是为特定协议和服务预定义的端口号,用于确保网络服务的标准化和一致性。
-
范围:0 到 1023。
-
HTTP 端口号:80。
-
FTP 端口号:21。
③ 怎样表示回送地址?其含义是什么?如果向回送地址传输数据将发生什么情况?
-
回送地址(Loopback Address) 是网络协议中用于测试和调试的一个特殊地址。它代表了本机自身,用于验证网络协议栈和网络接口的工作状况。
-
IPv4 回送地址:
- 表示 :
127.0.0.1
,以及127.0.0.0/8
地址段中的所有地址。 - 含义 :
127.0.0.1
是回送地址的最常用形式。IPv4 的回送地址范围是127.0.0.0
到127.255.255.255
,其中127.0.0.1
是最常见的地址。
- 表示 :
-
IPv6 回送地址:
- 表示 :
::1
- 含义 :
::1
是 IPv6 中的回送地址。IPv6 的回送地址是::1
,并且整个::/128
地址段中的所有地址都可以用作回送。
- 表示 :
-
测试和调试:回送地址用于测试本地网络协议栈和应用程序,而无需实际的网络连接。通过向回送地址发送数据,可以检查网络协议栈是否正常工作,而不涉及外部网络。
-
本地通信:它允许网络服务和应用程序在同一台计算机上相互通信,而不需要通过物理网络接口。
-
向回送地址传输数据的结果
- 当数据包发送到回送地址时,数据包不会被发送到外部网络。相反,数据包会被直接处理并返回给发送者。
- 这意味着发送到回送地址的数据不会离开计算机,所有的数据包都在本地计算机上进行处理和响应。