目录

一、前言

二、主机字节序和网路字节序

补充知识:cpu,内存,硬盘之间的关系

一、一个比喻:厨房、桌子和厨师

二、CPU里面存东西吗?

三、为什么不能把所有东西都放CPU里,或者全用最快的技术?

总结

1.大端序和小端序

2.网络字节序

3.ip地址和通讯端口

4.如何处理大小端序

三、sockaddr结构体

四、sockaddr_in结构体

孩子们,你收否有很多问号:

五、下面来看:IP地址相关的结构体

问题:IP地址是字符串,怎么把它转变为32位的大端序整数?

方案一:gethostbyname函数

方案二:字符串IP与大端序IP的转换(在嵌入式用的极多)


注:本文章内容均来自本人的学习笔记为个人学习总结,禁止转载。

参考自B站课程:码农论坛《c++网络编程》。由于当时方便记笔记,笔记中少部分图片(仅涉及部分代码以及相关运行结果展示,不涉及重要笔记、资料等)来源于原课程视频截图,版权归原作者“码农论坛”及相关权利人所有。本笔记无任何商业用途(除开csdn官方操作),仅供个人学习交流。感谢原up主的课程分享!


一、前言

在我之前的一篇博客tcp网络编程以及socket函数详解中,讲到了socket网络编程的基本概念和代码示例。以下是代码,方便查看。

客户端client:

/*
 * 程序名:demo_client.cpp,此程序用于演示socket的客户端
 * kaizy
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;

int main(int argc, char* argv[])
{
	if (argc != 3)
	{
		cout << "Using:./demo_client 服务端的IP 服务端的端口\nExample:./demo_client 192.168.101.139 5005\n\n";
		return -1;
	}

	//第1步:创建客户端的socket(套接字)
	// 类比——准备电话
	// 创建客户端的socket,返回套接字描述符(类似文件句柄)
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd == -1)// 创建失败返回-1
	{
		perror("socket");// 打印具体错误原因(如权限、资源不足)
		return -1;
	}
	/*关键参数解释:
	socket 就是让两台电脑上的程序能互相传数据的 “网络管道”
	AF_INET:使用 IPv4 协议(主流网络协议);
	SOCK_STREAM:使用 TCP 协议(面向连接、可靠传输);
	0:默认协议(TCP 协议下可省略);
	sockfd:Socket 描述符,后续所有操作(连接、发送)都基于这个 “句柄”。
	作用:相当于给客户端创建了一个 “网络通讯的通道”,没有这个通道就无法和服务端通信。*/

	//第二步:向服务器发起连接请求。(解析服务端ip并发起连接)   
	// 类比——打电话拨号
	struct hostent* h;   //用于存放服务端ip的结构体
	// 把字符串格式的IP(如"192.168.101.139")转换成网络可识别的结构体
	if ((h = gethostbyname(argv[1])) == NULL) //把字符串格式的ip转换成结构体
	{
		cout << "gethostbyname failed.\n" << endl;
		close(sockfd);// 失败时关闭Socket,释放资源
		return -1;
	}

	//注:gethostbyname:不仅能解析 IP,还能解析域名(如 "www.baidu.com"),返回的h包含 IP 的二进制形式。
	//////填充服务端地址结构体
	struct sockaddr_in servaddr; //用于存放服务端ip和端口结构体
	memset(&servaddr, 0, sizeof(servaddr));// 初始化结构体,避免脏数据
	servaddr.sin_family = AF_INET;// 协议族:IPv4
	// 把解析后的IP复制到结构体中(二进制形式)
	memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);   //指定服务端的ip地址
	// 把端口转换成网络字节序(大端序),赋值给结构体
	servaddr.sin_port = htons(atoi(argv[2]));			  //指定服务端的通信端口
	/* atoi(argv[2]):把字符串端口(如 "5005")转换成整数;
	htons:把主机字节序(小端序,x86 架构)转换成网络字节序(大端序),网络通讯必须统一字节序。*/

	///////向服务端发起连接
	if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) != 0)//向服务端发起连接请求
	{
		perror("connect");// 打印连接失败原因(如服务端未启动、IP/端口错误)
		close(sockfd);
		return -1;
	}
	/* connect:TCP 的 “三次握手” 核心函数,成功返回 0,失败返回 - 1;
	失败场景:服务端未启动、IP 错误、端口被占用、网络不通等。*/

	//第三步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文
	// 打电话->通话
	char buffer[1024];// 数据缓冲区,存储要发送的内容
	for (int i = 0; i < 3; i++)//循环3次,将与服务端进行3次通讯
	{
		int iret;
		memset(buffer, 0, sizeof(buffer));//清空缓冲区
		sprintf(buffer, "这是第%d个超级女生,编号%03d。", i + 1, i + 1);  // 生成请求报文内容。
		// 向服务端发送请求报文:参数=Socket句柄 + 数据缓冲区 + 数据长度 + 标志(0=默认)
		if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0)
		{
			perror("send");
			break;
		}
		cout << "发送:" << buffer << endl;
		/*sprintf:把格式化内容写入缓冲区(类似 cout,但写入字符数组);
		strlen(buffer):获取有效数据长度(不含字符串结束符 '\0');
		send:发送数据,返回值iret是实际发送的字节数;
		sleep(1):每秒发送 1 条,避免发送过快。*/

		memset(buffer, 0, sizeof(buffer));//把 buffer 数组的所有字节置为 0,清空上一次发送的数据(比如 “这是第 1 个超级女生...”),
		//避免残留数据干扰接收结果。
		//接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待.。
		if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0)
		{
			cout << "iret=" << iret << endl;
			break;
		}
		// recv() 是从 Socket 连接里 “读取” 对方发过来的数据的函数,像打电话时 “听对方说话”,没数据就等(阻塞),
		// 拿到数据存到缓冲区,拿不到就返回错误 / 断连信号。
		// 返回值:>0 = 收到的字节数,=0 = 对方正常断连,<0 = 接收失败;
		cout << "接收:" << buffer << endl;

		sleep(1);
	}

	//第四步,关闭socket,释放资源
	close(sockfd);
	//作用:断开与服务端的 TCP 连接(四次挥手),释放系统分配的 Socket 资源,必须执行否则会造成资源泄漏。
}

服务端server:

/*
 * 程序名:demo_server.cpp,此程序用于演示socket通信的服务端
 * kaizy
*/
#include <iostream>       // C++标准输入输出(cout)
#include <cstdio>         // C标准输入输出(perror)
#include <cstring>        // 内存操作(memset、strcpy、strlen)
#include <cstdlib>        // 字符串转数字(atoi)
#include <unistd.h>       // 系统调用(close)
#include <netdb.h>        // 网络相关结构体(sockaddr_in)
#include <sys/types.h>    // 系统类型定义(socket相关)
#include <sys/socket.h>   // Socket核心函数(socket/bind/listen/accept/recv/send)
#include <arpa/inet.h>    // 网络字节序转换(htons/htonl)
using namespace std;      // 简化C++标准库调用

//和客户端头文件基本一致,都是 Linux 下 Socket 编程的标配,覆盖 IO、内存、网络核心能力。

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        cout << "Using:./demo_server 通讯端口\nExample:./demo_server 5005\n\n";   // 端口大于1024,不与其它的重复。
        cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";
        cout << "      如果是云服务器,还要开通云平台的访问策略。\n\n";
        return -1;
    }
    /*服务端只需要指定 “监听的端口”(如 5005),不需要指定 IP(后续会绑定到所有网卡);
    关键提醒:端口需用 1024 以上(1024 以下是系统保留端口,普通用户无权使用),且要开放防火墙 / 云平台策略,否则客户端连不上。*/

    //第一步:创建服务端的socket
    //打电话——准备电话机
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1)
    {
        perror("socket");
        return -1;
    }
    /*AF_INET:IPv4 协议;
    SOCK_STREAM:TCP 协议(面向连接、可靠传输);
    0:默认协议;
    lisenfd:监听套接字描述符,专门用于 “监听客户端连接请求”,不负责实际收发数据。*/

    //第二步:把服务器用于通信的ip和端口绑定到socket上
    // 打电话——分配电话号码
    //服务端需要把 “监听的 IP + 端口” 和套接字绑定,客户端才能找到它。
    struct sockaddr_in servaddr;          // 用于存放服务端IP和端口的数据结构,IPv4专用地址结构体,存储IP+端口
    memset(&servaddr, 0, sizeof(servaddr));// 清空结构体,避免脏数据
    servaddr.sin_family = AF_INET;        // 指定协议。
    // 绑定到所有网卡(0.0.0.0),即服务器的任意IP都能接收连接
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 服务端任意网卡的IP都可以用于通讯。
    // 把端口转换成网络字节序,赋值给结构体
    servaddr.sin_port = htons(atoi(argv[1]));     // 指定通信端口,普通用户只能用1024以上的端口。
    /*
    关键参数:
    INADDR_ANY:宏定义,值为 0,代表 “绑定到服务器所有网卡的 IP”(比如服务器有内网 IP、外网 IP,都能接收连接);
    htonl:把主机字节序转换成网络字节序(针对 32 位整数,如 IP);
    htons:把主机字节序转换成网络字节序(针对 16 位整数,如端口);
    atoi(argv[1]):把字符串端口(如 "5005")转成整数。
    
    */

    //绑定服务端的ip和端口
    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) != 0)
    {
        perror("bind");
        close(listenfd);
        return -1;
    }

    // 第三步:把socket设置为可连接(监听)的状态。
    /*
    isten 就是让服务端 socket 开始 “监听连接”,进入可以被客户端连接的状态。
    极简版:
    服务端调用 listen 后,就开始等着客户端来 connect
    同时会维护一个等待连接的队列,避免并发连接丢失
    一句话总结:把 socket 设为监听模式,开启接客模式。
    */
    if (listen(listenfd, 5) != 0)
    {
        perror("listen");
        close(listenfd);
        return -1;
    }

    //第四步:受理客户端的连接请求,如果没有客户端连上来,accept()函数将阻塞等待。
    int clientfd = accept(listenfd, 0, 0);

    if (clientfd == -1)
    {
        perror("accept");
        close(listenfd);
        return -1;
    }

    cout << "客户端已连接。\n";
    //第五步:与客户端通信,接收客户端发过来的报文后,回复ok
    char buffer[1024];
    while (true)
    {
        int iret;
        memset(buffer, 0, sizeof(buffer));
        //接收客户端的请求报文,如果客户端没有请求报文,recv()函数将阻塞等待
        //如果客户端已经断开连接,recv()函数将返回0
        if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0)
        {
            cout << "iret=" << iret << endl;
            break;
        }
        cout << "接收:" << buffer << endl;
        strcpy(buffer, "ok");//生成回应报文内容
        //向客户端发送回应报文
        if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0)
        {
            perror("send");
            break;
        }
        cout << "发送:" << buffer << endl;
    }
    //第六步:关闭socket,释放资源
    close(listenfd);   // 关闭服务端用于监听的socket。
    close(clientfd);   // 关闭客户端连上来的socket。
}

/*
listen函数:
#include <sys/socket.h>
int listen(int sockfd, int backlog);

两个参数:
sockfd
你用 socket() 创建出来的文件描述符
就是服务端自己的 socket
backlog
等待连接的队列长度
简单说:最多能同时排队等待处理的客户端数量
一般写 5、10、128 都行
你写 5 就够了:listen(fd, 5);
*/
/*
accept 与 connect 超直白对比(实习面试必考)
一句话分清
connect:客户端主动去找服务端
accept:服务端等着客户端来找它

1. 谁在用?
connect → 客户端调用
accept → 服务端调用
2. 干什么?
connect
作用:主动连接指定 IP 和端口的服务器
结果:成功后得到一个 socket,可以 send /read
accept
作用:阻塞等待客户端来连接
结果:来一个客户端,就返回一个新的 socket 专门跟它聊天
3. 阻塞表现
connect:
连不上就一直等,直到超时或成功。
accept:
一直死等,没有客户端连接就卡在这里不动。

 面试标准答案(背这个)
connect 是客户端函数,用于主动向服务端发起连接请求。
accept 是服务端函数,用于阻塞等待客户端连接,成功后返回用于通信的客户端 socket。
*/

但是,这里最复杂的结构体部分还没有讲,今天把它给拿下!

啥结构体?就是存放协议端口,和ip地址的结构体。

但是在此之前要先了解几个概念——主机字节序和网络字节序

二、主机字节序和网路字节序

补充知识:cpu,内存,硬盘之间的关系

一、一个比喻:厨房、桌子和厨师

把计算机想象成一个厨房:

硬盘 (SSD/HDD):是冰箱和储物柜。容量巨大,但速度慢。你的菜谱、食材(程序和数据)在不做菜时都放在这里,断电后也不会消失

内存 (RAM):是料理台。容量比冰箱小,但厨师操作很快。你想做的任何菜,都必须先把食材和菜谱从冰箱里拿出来,放在料理台上。料理台上的东西,一断电(或者收拾厨房)就没了。

CPU:是厨师。他不存任何东西,只负责从料理台上拿起食材,按照菜谱(指令)进行切菜、翻炒等操作,再把做好的菜放回料理台上

所以,你的问题“计算机的所有数据都是存放在内存吗?”的答案是:不,所有正在运行的程序和正在处理的数据,必须存放在内存中。 而长久保存的数据(比如你没打开的文档、没运行的游戏),是存放在硬盘里的。

二、CPU里面存东西吗?

CPU内部有非常小极快的存储空间,叫做 寄存器

寄存器:相当于厨师手里的调料盒或者锅铲。厨师不可能把整块肉(数据)一直拿在手里,但他可以把几粒盐(一个数字)、酱油(一个内存地址)这种最紧要的东西放在手边最方便的位置。寄存器的数量极少(一个CPU只有几十个),但速度是计算机里最快的。

数据搬运的完整流程:

  1. 程序运行时,操作系统会把程序代码和数据从硬盘复制到内存

  2. CPU 执行一条指令,比如“把内存地址1000的数值+1”。

  3. CPU 会先把内存地址1000的数值读到自己的寄存器里。

  4. CPU 在寄存器里完成 +1 操作。

  5. CPU 再把寄存器里计算好的新数值写回到内存地址1000。

三、为什么不能把所有东西都放CPU里,或者全用最快的技术?

这是一个经典的工程权衡问题,核心在于 速度和成本

  • 为什么数据不能全放CPU里?

    成本:CPU里的寄存器(以及L1/L2/L3缓存)技术,速度极快,但成本也极高、功耗大。1MB的CPU缓存比16GB的内存条贵得多。所以只能做到KB或MB级别。
  • 为什么不用内存替代硬盘?

    断电即失:你关掉电脑,内存里的所有数据就会消失。我们需要硬盘来持久化存储。成本:内存(SRAM/DRAM)的成本依然远高于硬盘(闪存/机械),且单位容量的耗电量也更大。

这个速度、成本、容量的金字塔结构是计算机体系的基础:

最快、最贵、最小:CPU寄存器

很快、较贵、较小:CPU缓存 (L1/L2/L3)

快、便宜、较大:内存 (RAM)

慢、很便宜、巨大:硬盘 (SSD/HDD)

总结

  1. 内存是CPU的“工作台”:所有正在运行的程序和数据,必须放在内存里。

  2. CPU自己不存数据:它只通过寄存器这种极小的“手边工具”,来读取、计算、写回内存中的数据。

  3. 硬盘是“仓库”:用来长久保存暂时不用的数据。

1.大端序和小端序

如果数据类型占用的内存空间大于1字节,CPU把数据存放在内存中的方式有两种:

大端序(Big Endian):低位字节存放在高位,高位字节存放在低位。

小端序(Little Endia):低位字节存放在低位,高位字节存放在高位。

假设从内存地址0x00000001处开始存储十六进制数0x12345678,那么:

Bit-endian(按原来顺序存储)

0x00000001           0x12

0x00000002           0x34

0x00000003           0x56

0x00000004           0x78

Little-endian(颠倒顺序储存)

0x00000001           0x78

0x00000002           0x56

0x00000003           0x34

0x00000004           0x12

简单说就是:多字节数据在内存里怎么排队。一个是“顺着排”,一个是“倒着排”。

大端序:高字节存低地址(高位在前,像写数字一样从左到右)

小端序:低字节存低地址(低位在前,倒着存)

不同 CPU 设计者选择的哲学不同:

大端序(网络序):符合人类读写习惯(高位在前),主要用于网络协议、ARM 默认也可配

小端序(主机序):x86、x86_64 强制使用,目前 PC、服务器、多数嵌入式设备的主流

Intel系列的CPU以小端序方式保存数据,其它型号的CPU不一定。

操作文件的本质是把内存中的数据写入磁盘,在网络编程中,传输数据的本质也是把数据写入文件(socket也是文件描述符)。

(文件、网络 socket 都是“字节流”,把内存里的二进制数据直接写进去 → 另一端读出来时,字节顺序相反 → 数字解析错误)

这样的话,字节序不同的计算机之间传输数据,可能会出现问题:

因此,引入了网络字节序来解决这个问题:

2.网络字节序

为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序(大端序)。

主机序(Host Byte Order):取决于 CPU 架构,可以是大端(如 PowerPC、旧版 Mac),也可以是小端(如 x86、x86_64、ARM 默认小端模式)。ARM 架构支持大小端切换,但绝大多数嵌入式 Linux 系统都运行在小端模式

网络序(Network Byte Order):TCP/IP 协议栈明确规定使用 大端(Big-Endian),这是网络协议中的硬性标准,所有厂商、所有平台都必须遵守,以保证不同架构的设备能正常通信。

一句话总结:主机序可以是大小端,但是网络序一定是大端

都转成网络序,就避免了传输的数据解析问题!!!

C语言提供了四个库函数,用于在主机字节序和网络字节序之间转换:

以下是函数的声明:

#include <arpa/inet.h>

uint16_t htons(uint16_t hostshort);   // 主机序 → 网络序(16位)

uint32_t htonl(uint32_t hostlong);    // 主机序 → 网络序(32位)

uint16_t ntohs(uint16_t netshort);    // 网络序 → 主机序(16位)

uint32_t ntohl(uint32_t netlong);     // 网络序 → 主机序(32位)

注:// uint16_t  2字节的整数 unsigned short

// uint32_t  4字节的整数 unsigned int

1byte=8bit

函数详解:

主机序 ↔ 网络序 转换函数

函数

含义

参数类型

返回值类型

htons()

host to network short

16位主机序

16位网络序

htonl()

host to network long

32位主机序

32位网络序

ntohs()

network to host short

16位网络序

16位主机序

ntohl()

network to host long

32位网络序

32位主机序

命名规则拆解;

缩写

全称

含义

h

host

主机字节序(x86 是小端)

n

network

网络字节序(固定为大端

to

to

转换方向

s

short

16位(2字节)

l

long

32位(4字节)

3.ip地址和通讯端口

在计算机中,IPv4的地址用4字节的整数存放,通讯端口用2字节的整数(0-65535)存放。

例如:192.168.190.134      3232284294    255.255.255.255

           192           168           190          134

大端:11000000 10101000 10111110 10000110

小端:10000110 10111110 10101000 11000000

说明:IP地址在内存里怎么存,和网络传输时怎么排,是两回事。

IP 地址 192.168.190.134 对应十六进制:C0 A8 BE 86(192=C0, 168=A8, 190=BE, 134=86)。

视角

排列顺序

十六进制表示

人看

点分十进制

192.168.190.134

内存中(x86小端)

低地址存低字节

86 BE A8 C0

网络传输(大端)

先发高字节

C0 A8 BE 86

所以小端机器在内存里看到的是 86 BE A8 C0,但用 htonl 转成网络序后,就会变成 C0 A8 BE 86。

为什么网络传输要用大端???

网络协议是跨架构的,必须规定一个“通用语言”。

规定 大端序(网络序) 作为传输标准,这样:

小端机器(x86)发前用 htonl 转成大端

大端机器(某些ARM、PowerPC)发前不用转

接收端用 ntohl 转回自己的主机序

所以代码里永远写 htonl / ntohl,不要假设自己机器是什么端。

为什么“255.255.255.255”是特殊的

255.255.255.255 的十六进制是 FF FF FF FF:

大端:FF FF FF FF

小端:FF FF FF FF

这是唯一一个“不分端序”的IP地址,因为所有字节都一样。

广播地址天然避开字节序问题。

P地址(4字节)和端口(2字节)在内存中是主机序(x86小端),但网络传输必须是大端(网络序)。

因此发送前必须用 htonl / htons 转换,接收后用 ntohl / ntohs 转回。

这是跨平台通信的“铁律”。

4.如何处理大小端序

在网络编程中,数据收发的时候有自动转换机制,不需要程序员手动转换,只有向sockaddr_in结体成员变量填充数据时,才需要考虑字节序的问题。

三、sockaddr结构体

存放协议族、端口和地址信息,客户端和connect()函数和服务端的bind()函数需要这个结构体。

struct sockaddr 
{
  unsigned short sa_family;	// 协议族,与socket()函数的第一个参数相同,填AF_INET。
  unsigned char sa_data[14];	// 14字节的端口和地址。
};

四、sockaddr_in结构体

sockaddr结构体是为了统一地址结构的表示方法,统一接口函数,但是,操作不方便,所以定义了等价的sockaddr_in结构体,它的大小与sockaddr相同,可以强制转换成sockaddr。

struct sockaddr_in 
{  
  unsigned short sin_family;	// 协议族,与socket()函数的第一个参数相同,填AF_INET。
  unsigned short sin_port;		// 16位端口号,大端序。用htons(整数的端口)转换。
  struct in_addr sin_addr;		// IP地址的结构体。192.168.101.138
  unsigned char sin_zero[8];	// 未使用,为了保持与struct sockaddr一样的长度而添加。
};

struct in_addr // IP地址的结构体。
{				
  unsigned int s_addr;		// 32位的IP地址,大端序。
};

注意观察,这两个结构体的大小是相同的。第一个结构体,端口和地址14字节,第二个结构体包含了另一个结构体,端口号,sin_zero,ip地址,加起来也是14字节。

在程序中,创建的是下方的结构体,调用时才转为上面的结构体。

client:

server:

孩子们,你收否有很多问号:

产生了疑问:_in结构体用的好好的,为什么还要强转为上面的?

1. 系统调用的接口设计

connect()、bind()、accept() 这些函数的原型是这样设计的:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

第二个参数要求是 struct sockaddr*(通用地址结构体指针)

它要兼容 IPv4、IPv6、Unix 域套接字等多种地址格式

所以它不能只写死 struct sockaddr_in*(只兼容 IPv4)

咋看出来它只兼容ipv4?

in = Internet

特指 IPv4 协议

看里面的字段:

struct in_addr sin_addr;    // ← 这个就是 IPv4 的 IP!

struct in_addr 是 纯 IPv4 地址结构,只有 4 字节

它只能存 192.168.1.100 这种 IPv4,存不下 IPv6!

IPv6 有专门的结构体:

struct sockaddr_in6   // IPv6 专用

struct in6_addr sin6_addr;   // 128位 IPv6 地址

关键:内存布局兼容!struct sockaddr_in 的大小和 struct sockaddr 完全一样,sin_family 对应 sa_family,后面的 sin_port+sin_addr 正好填满 sa_data[14]。

2.为什么要强制转换?

C 语言是强类型语言,struct sockaddr_in* 和 struct sockaddr* 是不同的指针类型,编译器不会自动转换;

强转只是告诉编译器:“我知道我在做什么,把这个 IPv4 地址当成通用地址传给系统调用”;

实际内存里的数据一点没变,只是换了个 “类型标签”。

那为什么不直接用struct sockaddr* ?

直接用 struct sockaddr* 太不方便、太容易出错!struct sockaddr_in 是为了方便你手动填写 IPv4 地址和端口而设计的“友好版”,而 struct sockaddr 只是给系统调用用的“通用壳子”。

sa_data[14] 是一个无意义的字节数组,里面混着 IP、端口、填充位,没有任何字段名;

如果你直接用它,就得手动拼字节:

struct sockaddr servaddr;

servaddr.sa_family = AF_INET;

// 手动把端口和IP塞到 sa_data 里,非常容易写错!

servaddr.sa_data[0] = 0x13; // 端口高8位

servaddr.sa_data[1] = 0x89; // 端口低8位

servaddr.sa_data[2] = 192;  // IP第一段

servaddr.sa_data[3] = 168;  // IP第二段

// ... 剩下的字节还要手动填,完全是自找麻烦

这不仅难写,可读性极差,还极易因为字节顺序搞错而 Bug 满天飞。

2. struct sockaddr_in 是给人用的友好封装

它把 IPv4 地址拆成了有名字的字段,写起来一目了然:

struct sockaddr_in {

    sa_family_t    sin_family;  // 地址族

    in_port_t      sin_port;    // 端口号(直接用 htons() 赋值)

    struct in_addr sin_addr;    // IP地址(直接用 inet_pton() 或 memcpy)

    unsigned char  sin_zero[8];// 填充到和 struct sockaddr 一样大

};

你可以按名字赋值:

servaddr.sin_family = AF_INET;

servaddr.sin_port = htons(5005);       // 端口直接赋值

inet_pton(AF_INET, "192.168.1.1", &servaddr.sin_addr); // IP直接赋值

代码清晰、不易出错,这才是给开发者用的接口。

我前面定义的是 struct sockaddr_in(IPv4),那调用 connect 时怎么可能存得下 IPv6?根本没定义 IPv6 结构体啊

connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

 那为什么接口要写成 struct sockaddr*?

不是因为你现在在用 IPv6,而是这个接口设计成 “通用接口”,未来可以支持 IPv6。

就像:

插座设计成万能插座

但你现在只插了两脚插头(IPv4)你根本没有调用 IPv6

你现在写的代码,从头到尾都是 IPv4。

// 1. 定义 IPv4 专用结构体

struct sockaddr_in serv_addr;

// 2. 填 IPv4 内容

serv_addr.sin_family = AF_INET;

serv_addr.sin_port   = htons(8080);

inet_pton(AF_INET, "192.168.1.1", &serv_addr.sin_addr);

// 3. 强转成通用指针 → 传给 connect

不代表你现在在用三脚插头(IPv6)

如果你真要用 IPv6,才需要换结构体

IPv6 必须换结构体:

// IPv6 专用结构体

struct sockaddr_in6 serv_addr6;

接口还是同一个:

connect(sockfd, (struct sockaddr *)&serv_addr6, sizeof(serv_addr6));

回归正题:

struct sockaddr_in {  

  unsigned short sin_family; // 协议族,与socket()函数的第一个参数相同,填AF_INET。

  unsigned short sin_port; // 16位端口号,大端序。用htons(整数的端口)转换。

  struct in_addr sin_addr; // IP地址的结构体。192.168.101.138

  unsigned char sin_zero[8]; // 未使用,为了保持与struct sockaddr一样的长度而添加。

};

struct in_addr { // IP地址的结构体。

  unsigned int s_addr; // 32位的IP地址,大端序。

};

五、下面来看:IP地址相关的结构体

问题:IP地址是字符串,怎么把它转变为32位的大端序整数?

方案一:gethostbyname函数

可以把字符串,域名转为ip地址

根据域名/主机名/字符串IP获取大端序IP,用于网络通讯的客户端程序中。

函数原型:

struct hostent *gethostbyname(const char *name);

输入:www.baidu.com 这种字符串

输出:装满 IP 信息的结构体指针

这个结构体就是:

struct hostent { 
  char *h_name;     	// 主机名。
  char **h_aliases;    	// 主机所有别名构成的字符串数组,同一IP可绑定多个域名。 
  short h_addrtype; 	// 主机IP地址的类型,例如IPV4(AF_INET)还是IPV6。
  short h_length;     	// 主机IP地址长度,IPV4地址为4,IPV6地址则为16。
  char **h_addr_list; 	// 最重要:主机的ip地址,以网络字节序存储。 
};
#define h_addr h_addr_list[0] 	// for backward compatibility.

使用函数gethostbyname之前要声明这个结构体,gethostbyname函数返回的信息就在这个结构体里。

struct hostent * (指针)

→ 意思是:我会给你返回一个指针,你必须用同类型指针接住它!

Gethostbyname的返回值: struct hostent *

是啥?

它就是一个指针,指向一块装满了 “域名解析出来的 IP 信息” 的内存。

注意这个宏定义:

#define h_addr h_addr_list[0]

在结构体中:

char **h_addr_list;

它是一个IP 地址数组,一个域名可能对应多个 IP:

h_addr_list[0] = 192.168.1.10(二进制网络序)

h_addr_list[1] = 192.168.1.11

h_addr_list[2] = NULL(表示结束)

那 h_addr 干嘛用?

绝大多数场景,我们只需要用第一个 IP 就够了。

所以系统给你整个快捷方式:

h->h_addr

就等于:

h->h_addr_list[0]

代码里看作用:

memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);

这里:

h->h_addr = 拿到解析出来的第一个 IP

格式是网络字节序的二进制 IP

刚好可以直接 memcpy 塞进 servaddr.sin_addr

终极总结:

h_addr_list:存放一个域名对应的所有 IP

h_addr:取第一个 IP,方便你写代码

作用:给 socket 地址结构提供 IP 地址

char **h_addr_list; 它为啥是一个数组???

因为一个域名(比如 www.baidu.com)可以对应好多个 IP 地址!为了把所有 IP 都存下来,所以必须用 数组。

为什么一个域名能对应多个 IP?

这叫 DNS 轮询(DNS Load Balance)大厂为了不让服务器崩掉,会给一个域名绑很多 IP。

你电脑每次解析,可能拿到不同的 IP。系统函数 gethostbyname 会把所有解析出来的 IP 全部带回给你!

所以它必须是数组!

以下是C语言知识:

这就是一个指针数组:char *[]

先回忆两个最基础的东西

char * → 一串字符 / 一段二进制数据

char *[] / char ** → 一串指针,也就是数组

现在有好几个ip:

IP1 → char*

IP2 → char*

IP3 → char*

Char*原本指的是一串字符串,但是注意在这里:

h->h_addr_list[0]

它是4 字节原始二进制 IP,不是字符串,不是 "192.168.1.1",是 0xC0 0xA8 0x01 0x01 这种裸二进制,不能直接 printf("%s", ...) 打印

为什么还用 char*?

因为 C 语言里:char* 最适合表示 “一段任意二进制数据”所以系统函数就用它来存 IP。

要把这些ip放在一起,就变成:

char *addr_list[] = { IP1, IP2, IP3, NULL };

数组名在传参时会退化成指针:

char **h_addr_list

再看最后一步:

memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);

1. &servaddr.sin_addr

目标地址:把 IP 复制到 sockaddr_in 的 IP 字段里

2. h->h_addr

源地址:来自 gethostbyname 解析出来的 4 字节网络 IP

3. h->h_length

复制长度:IPv4 = 4 字节IPv6 = 16 字节自动适配,不用你写死 4!

它做了啥:把“gethostbyname 解析出来的 裸二进制IP” 直接复制到 “socket 需要的 sin_addr 里面”

等同于:servaddr.sin_addr = 解析出来的IP

为什么不能直接赋值?

因为:

h->h_addr 是 char* 类型

servaddr.sin_addr 是 struct in_addr 类型

类型不一样,不能直接 =所以必须用 memcpy 内存拷贝。

哦!!!!!!!!!!!!!懂了吧!!!!!!

方案二:字符串IP与大端序IP的转换(在嵌入式用的极多)

C语言提供了几个库函数,用于字符串格式的IP和大端序IP的互相转换,用于网络通讯的服务端程序中。

typedef unsigned int in_addr_t;    // 32位大端序的IP地址。

// 把字符串格式的IP转换成大端序的IP,转换后的IP赋给sockaddr_in.in_addr.s_addr。
in_addr_t inet_addr(const char *cp); 

// 把字符串格式的IP转换成大端序的IP,转换后的IP将填充到sockaddr_in.in_addr成员。
int inet_aton(const char *cp, struct in_addr *inp);	

// 把大端序IP转换成字符串格式的IP,用于在服务端程序中解析客户端的IP地址。
char *inet_ntoa(struct in_addr in);

嵌入式设备通常有固定的服务器IP,直接写死在代码或配置文件里。用 inet_addr() 转换一下就能用,简单、可靠、无依赖。而 gethostbyname() 需要DNS,会增加代码复杂度、网络依赖、启动时间,还可能因DNS问题导致连接失败。

在嵌入式Linux里,稳定性比灵活性重要一万倍。能用IP解决的问题,绝不用域名。

注意图片的最后两行:

servaddr.sin_addr.s_addr=htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯。

  //servaddr.sin_addr.s_addr=inet_addr("192.168.101.138"); // ③指定服务端用于通讯的IP(大端序)。

服务器,如果有多个网卡,就可以有多个ip地址

假设服务器有多个ip地址:

这两个ip地址属于不同的网段,对于左右两端的客户端来说,只能访问相应的网段。

如果服务端的程序采用的是第一种写法:

servaddr.sin_addr.s_addr=htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯。

那么左右两端的客户端都可以和服务端通信。

但如果采用的是第二种写法:

 //servaddr.sin_addr.s_addr=inet_addr("192.168.101.138"); // ③指定服务端用于通讯的IP(大端序)。

那么,只有左边的客户端能与服务端通信。

在客户端,也可以把gethostbyname函数改为int_addr,这是在嵌入式中更常见的做法:

编译运行服务端客户端程序,依然可以正常运行:

但是,gethostbyname函数还可以用域名和主机名,而int_addr函数只能用ip

演示:代码demo5(见linux,或vs)

Ping一下新浪微博,查看其ip地址:

运行客户端:

网站是http协议,用的是80端口

显示:

接受成功,虽然不合法

把IP地址改为域名:

重新启用gethostbyname函数:

就可以成功接收了。

创作不易,大家点赞收藏+关注呀O(∩_∩)O哈哈~

更多推荐