2450 字
6 分钟
TCP&IP网络编程学习札记_2
2026-06-26
浏览量 127 · 访客 9

第三章内容比较多所以单开了一章。

地址族与数据序列#

3.1 分配给套接字的IP地址和端口号#

IPIPInternetProtocolInternet Protocol(网络协议)的简写,是为了收发网络数据而分配给计算机的值。
端口号简单来说就是分配给套接字的序号。下面会详细讲。

网络地址(Internet Address)#

IPIP地址主要分为两类:
IPv4(Internet Protocol version 4) 4字节地址族
IPv6(Internet Protocol version 6) 16字节地址族
IPv4IPv4IPv6IPv6主要的区别是IPIP地址使用的字节数,目前主要使用的地址族还是IPv4IPv4
IPv4IPv4标准的44字节IPIP地址分为网络地址和主机地址。且分为ABCDEA、B、C、D、E等类型。一般不使用已被预约的EE类地址。

替换文本
网络地址(网络IDID)是为区分网络而设置的一部分IPIP地址。我们查找目标主机时,首先仅浏览44字节IPIP地址的网络地址,先把数据传到对应的网络,然后构成网络的路由器(或交换机)接收到数据之后,再通过浏览主机地址(主机IDID)讲数据传给目标计算机。

网络地址的分类与主机地址边界#

只通过IPIP地址的第一个字节即可判断网络地址占用的字节数:
AA类地址的首字节范围:01270-127
BB类地址的首字节范围:128191128-191
CC类地址的首字节范围:192223192-223
还有下述表示方式。
AA类地址的首位以00开始
BB类地址的前22位以1010开始
CC类地址的前33位以110110开始

用于区分套接字的端口号#

IPIP用以区分计算机,只要有IPIP地址就能向目标主机传输数据。但是不同的数据(比如视频数据和网页数据)接收需要不同的套接字,我们就需要端口号来区分不同的套接字。
计算机中一般配有NICNICNetworkInterfaceCardNetwork Interface Card,网络接口卡)数据传输设备。通过NICNIC可以传递IPIP和分配端口号,操作系统会参考该端口号把数据传给相应端口的套接字。
端口号一般由1616位构成,可分配的端口号范围是0655350-65535。但010230-1023是知名端口(WellknownPROTWell-known PROT),一般分给特定应用程序,应该要分配此范围之外的值。另外这里还有个特殊情况,TCPTCPUDPUDP可以重复使用一个端口,但是一个端口最多只能有一个TCPTCPUDPUDP套接字。
总结:数据传输目标地址必须包括IP地址和端口号数据才能被传输到最终的应用程序套接字。

3.2 地址信息的表示#

表示IPv4地址的结构体#

该结构体会作为地址信息传输给bindbind函数。

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]; //不使用
};

该结构体提到的另外一个结构体in_addrin\_addr定义如下,它用来存放32位IP地址。

struct in_addr{
In_addr_t s_addr; //32位IPv4地址
};

这里面的一些数据类型可以参考一下POSIX(Portable Operating System Interface,可移植操作系统接口)。它是为UNIXUNIX系列操作系统设立的标准。

数据类型数据类型说明声明的头文件
int8_t
uint8_t
int16_t
uint16_t
int32_t
uint32_t
signed 8-bit int
unsigned 8-bit int (unsigned char)
signed 16-bit int
unsigned 16-bit int (unsigned short)
signed 32-bit int
unsigned 32-bit int (unsigned long)
sys/types.h
sa_family_t
socklen_t
地址族(address family)
长度(length of struct)
sys/socket.h
in_addr_t
in_port_t
IP地址,声明为uint32_t
端口号、声明为uint16_t
netinet/in.h

额外定义这写数据类型的原因同样是为了保证扩展性。

结构体sockaddr_in的成员分析#

成员family,也就是地址族。

地址族(Address Family)含义
AF_INETIPv4网络通信协议中使用的地址族
AF_INET6IPv6网络通信协议中使用的地址族
AF_LOCAL本地通信中采用得的UNIX协议的地址族

成员sin_port以网络字节序保存1616位端口号。
成员sin_addr以网络字节序保存3232IPIP地址信息,数据类型为前面的in_addrin\_addr,其实也就是uint32_tuint32\_t
成员sin_zero无特殊含义,只是为使结构体sockaddr_insockaddr\_in的大小与sockaddrsockaddr结构体保持一致。
从之前的代码也可以看出,sockaddr_insockaddr\_in结构体变量的地址将以如下的方式传递给bindbind函数。

struct sockaddr_in serv_addr;
....
if(bind(serv_sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
....

不过实际上bindbind函数的第二个参数期望得到sockaddrsockaddr结构体的变量地址,包括地址族,端口号,IPIP地址等。

struct sockaddr{
sa_family_t sin_family; //地址族(Adress Family)
char sa_data[14]; //地址信息
};

此结构体成员中sa_datasa\_data保存的地址需要保存IPIP地址和端口号,剩余部分应该填充00。因而我们引入了sockaddr_insockaddr\_in来生成符合bindbind函数要求的字节流,最后转化为sockaddrsockaddr型的结构体,再传递给bindbind函数即可。

3.3 网络字节序与地址变换#

字节序(Order)与网络字节序#

不同CPUCPU向内存中保存数据的方式有两种:
大端序BigEndianBig Endian):高位字节存放到低位地址。
小端序LittleEndianLittle Endian):高位字节存放到高位地址。
拿整数0x123456780x12345678举个例子:

端序0x20号0x21号0x22号0x23号
大端序0x120x340x560x78
小端序0x780x560x340x12

因此,代表CPUCPU数据保存方式的主机字节序(Host By Order)在不同CPUCPU之间也各不相同。
为了统一在传输过程中的字节序保证数据正确传输,我们在用网络传输数据的时候统一使用大端序,即网络字节序(Network Byte Order)。

字节序转换(Endian Conversions)#

以下为一些帮助转换字节序的函数:

unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);

我们可以通过函数名了解它们的功能:
htonshtonshh代表主机(hosthost)字节序,htonshtonsnn代表网络(networknetwork)字节序。
ss代表shortshort(22个字节),ll指的是longlong(LinuxLinux里面longlong类型占用44个字节)。

3.4 网络地址的初始化与分配#

将字符串信息转换为网络字节序的整数型#

sockaddrinsockaddr_in中保存地址信息的成员为3232位整数型。但我们的IPIP地址一般是用点分十进制表示法(Dotted Decimal Notation),比如说201.211.214.36201.211.214.36。不过我们有将地址字符串转化为大端序整数的函数:

#include <arpa/inet.h>
in_addr_t inet_addr(const char * string);
//成功时返回32位大端序整数型值,失败是返回INADDR_NONE。

inet_aton函数与inet_addr函数在功能上完全相同,也将字符串形式IPIP地址转化为3232位网络字节序整数并返回。只不过该函数还利用了in_addr结构体,且使用频率更高。

#include <arpa/inet.h>
//string:需要转换的IP地址信息字符串地址,addr:将保存转换信息结果的in_addr结构体变量地址值
int inet_aton(const char * string, struct in_addr * addr);
//成功时返回1(true),失败时返回0(false)。

若要调用inet_addr函数,需将转换后的IP地址信息代人sockaddr_in结构体中声明的in_addr结构体变量。而inet_aton函数会自动把结果填入该结构体变量,不需要此过程。
然后还有一个相反的inet_aton函数,它可以把网络字节序整数型IPIP地址转换成我们熟悉的字符串。

#include <arpa/inet.h>
char * inet_ntoa(struct in_addr adr);
//成功时返回转换的字符串地址值,失败时返回-1。

这里要注意一点,由于这里的返回值是charchar指针,也就是系统自动分配了内存来存储这个字符串,当我们下次调用这个函数的时候就会覆盖掉上一个字符串,如果需要保存字符串就要在第二次调用前复制这个字符串。

网络地址初始化#

套接字创建过程中常见的网络地址信息初始化方法。

struct sockaddr_in addr;
char * serv_ip = "211.217.168.13"; //声明IP地址字符串
char * serv_port = "9190"; //声明端口号字符串
memset(&addr, 0, sizeof(addr)); //结构体变量addr的所有成员初始化为0
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); //基于字符串的IP地址初始化
addr.sin_port = htons(atoi(serv_port)); //基于字符串的端口号初始化

客户端地址信息初始化#

上述网络地址的初始化主要针对服务器端,给套接字分配IPIP地址和端口号的原因主要是要传输数据,而对于客户端来说则是要连接到服务器相应的网络地址。所以服务器端的准备工作是通过bindbind函数完成的,而客户端主要是通过connectconnect函数完成的。因此,函数调用前需准备的地址值类型也不同。服务器端声明sockaddr_insockaddr\_in结构体变量,将其初始化为赋予服务器端IPIP和套接字的端口号,然后调用bindbind函数。而客户端则声明sockaddr_insockaddr\_in结构体,并初始化为要与之连接的服务器端套接字的IPIP和端口号,然后调用connectconnect函数。

INADDR_ANY#

每次创建服务器端套接字都要输入IPIP地址会有些繁琐,此时可如下初始化地址信息:

struct sockaddr_in addr;
char * serv_addr = "9190";
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
add.sin_port = htons(atoi(serv_addr));

这里利用常数INADDR_ANY自东分配服务器端的IPIP地址,可自动获取运行服务器端的计算机IPIP地址。若同一计算机中已分配多个IPIP地址(多宿主(MultihomedMulti-homed)计算机,一般路由器属于这一类),则只要端口号一致,就可以从不同IPIP接收数据。
这里还要提到一点,127.0.0.1127.0.0.1是回送地址(loopback address),指的是计算机自身IPIP地址,所以在第一章的示例中目标服务器的地址就是这个,如果是在不同计算机就需要输入正确的服务器IPIP地址。

向套接字分配网络地址#

#include <sys/socket.h>
//sockfd:对应的文件描述符,myaddr:存有地址信息的结构体变量地址,addrlen:第二个结构体变量的长度
int bind(int sockfd, struct sockaddr * myaddr, socklen_t addrlen);
//成功时返回0,失败时返回-1。

下面给出服务器端常见套接字初始化过程:

int serv_sock;
struct sockaddr_in serv_addr;
char * serv_port = "9190";
/* 创建服务器端套接字(监听套接字) */
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
/* 地址信息初始化 */
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(serv_port));
/* 分配地址信息 */
bind(serv_sock, (struct sockaddr * )&serv_addr, sizeof(serv_addr));
....

3.5 基于Windows的实现#

诸如htonshtonlinet_addrinet_ntoahtons、htonl、inet\_addr、inet\_ntoa等函数均可以在WindowsWindows下使用,故不赘述。唯一不同的是WindowsWindows不存在inet_atoninet\_aton函数。

WSAStringToAddress & WSAAddressToString#

此外WindowsWindows还有两个特殊的转换函数,不过它们在功能上与inet_ntoainet\_ntoainet_addrinet\_addr完全相同,但优点在于支持多种协议,在IPv4IPv4IPv6IPv6中均适用。

#include <winsock2.h>
INT WSAStringToAddress(
LPTSTR AddressString, INT AddressFamily, LPWSAPROTOCOL_INFO lpProtocolInfo,
LPSOCKADDR lpAddress, LPINT lpAddressLength
);
//成功时返回0,失败时返回SOCKET_ERROR。
//AddressString 含有IP和端口号的字符串地址值。
//AddressFamily 第一个参数中地址所属的地址族信息。
//IpProtocolInfo 设置协议提供者(Provider) ,默认为NULL。
//IpAddress 保存地址信息的结构体变量地址值。
//IpAddressLength 第四个参数中传递的结构体长度所在的变量地址值。

WSAAddressToStringWSAAddressToStringWSAStringToAddressWSAStringToAddress在功能上正好相反,它将结构体中的地址信息转换成字符串形式。

#include <winsock2.h>
INT WSAAddressToString(
LPSOCKADDR lpsaAddress, DWORD dwAddressLength,
LPWSAPROTOCOL_INFO lpProtocolInfo, LPSTR lpszAddressString,
LPDWORD lpdwAddressStringLength
);
//成功时返回0,失败时返回SOCKET_ERROR。
//IpsaAddress 需要转换的地址信息结构体变量地址值
//dwAddressLength 第一个参数中结构体的长度。
//IpProtocolInfo 设置协议提供者(Provider),默认为NULL。
//IpszAddressString 保存转换结果的字符串地址值。
//IpdwAddressStringLength 第四个参数中存有地址信息的字符串长度。

WindowsWindows中出现的SOCKADDR_INSOCKADDR\_INlinuxlinuxsockaddr_insockaddr\_in本质一样,只是用了typedeftypedef声明。

typedef struct sockaddr_in SOCKADDR_IN;
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

TCP&IP网络编程学习札记_2
https://mkrari.cn/posts/tcp_ip_learn_2/
作者
Mkrari
发布于
2026-06-26
许可协议
CC BY-NC-SA 4.0