【Linux网络编程】网络通信初步认识 重要套接字接口
目录
一、网络通信的本质
二、端口号:
socket:
端口号 vs pid:
三、UDP与TCP协议:
tcp——传输控制协议
udp——用户数据报协议
四、网络字节序列:
五、套接字编程的种类:
六、套接字接口
socket:
bind:
recvfrom:
sendto :
struct sockaddr:
处理字节序函数:
IP地址转换:
inet_aton:
inet_addr
inet_ntoa:
inet_pton:
inet_ntop:
一、网络通信的本质
网络通信并不是两台主机进行通信的
理解:当我们下载抖音,B站等APP,在我们不启动他们的客户端的时候是不会消耗流量的,也就是不会通信,只有当我们启动了后客户端和服务器之间才会进行通信
这说明真正通信的是应用层,应用层想要通信,进而推动下层一步一步地交互数据,封装报头,通过局域网,路由器而发送给目标主机
所以我们得到结论:
- 网络协议中的下三层(传输层,网络层,数据链路层),主要解决的是将数据安全可靠地传送到远端机器
- 用户使用应用层软件,完成数据发送和接收
因为是要使用软件,就需要将这个软件启动起来,当这个软件启动起来后就是一个进程了
所以日常通信的本质就是进程间通信
如上,当进行网络通信的时候本质就是进程间通信,我们之前学过进程,了解进程间通信的本质是让不同进程看到同一份资源,只不过以前学习的是在同一台主机中,而现在是在网络中的,那么现在网络中的进程间通信所看到的同一份资源是什么呢? ----- 就是网络,这里和以前的管道,共享内存一样,需要给我们用户提供系统调用接口,这些接口就是网络协议栈,通过这些接口,让一个进程往网络里面写,一个进程往网络里面读,完成进程间通信
二、端口号:
我们手机上的软件是客户端的,在开发软件的厂商那里还有一个服务端,当两台客户端的进程进行通信的本质是通过服务器进程作为中间媒介完成通信,即 进程A--->服务器进程--->进程 B 的间接交互
这时就引申出了一个问题:为什么手机上的客户端软件能够准确地到达对应的服务端呢?会不会微信客户端QQ服务端呢?反过来又为什么微信服务端能够准确地到达我们手机上的客户端呢?毕竟我们手机上又这么多客户端,会不会微信的服务端到我们QQ的客户端呢?这也对应着在网络协议栈中的传输层要交给哪一个客户端或者服务端?
为了解决上述问题,于是引入了一个概念:端口号
端口号是一个16位(2字节)的无符号整数,范围是0~65535,其核心作用就是在网络通信中,IP 地址标识目标主机,而端口号标识目标主机上的具体进程,确保数据能准确交付给对应的应用程序
端口号无论对于客户端还是服务端,都能够唯一的标识该主机上的一个网络应用层的进程
端口号的使用:
- 在从客户端到服务端的时候,在传输层的报头中会有始末端口号,比如[1234,4321],将这个端口号通过在网络协议栈中进行传输,依次封装报头再在网络中进行传输,传输到服务端的应用层
- 当服务端的传输层收到客户端的数据后,首先就会通过解析该层的报头中的端口号,然后在将有效载荷传输给应用层,接着在将数据返回给客户端的时候就会在服务端的传输层重新封装报头,将端口号反过来,像上述一样就会改变成[4321,1234],然后在传回客户端,这样就能够保证数据能准确交付给对应的文件了
一个进程可以绑定多个端口号, 但是一个端口号不可以被多个进程绑定
那么传输层是怎么通过端口号找到应用层中的软件的?在OS内部会有一张哈希表,也可以理解为数组,其中每一个元素都是进程PCB指针,在这个哈希表中,将端口号作为key值,绑定的对应进程的PCB指针作为value值,这样进行映射,这就完成了进程和端口号之间的绑定,未来在查找的时候也就能够直接通过端口号作为key值进行查找到对应的进程即可
socket:
通过上述的理解:
在公网上,IP地址能够表示唯一的一台主机,端口号port,用来表示该主机上的唯一的一个进程,所以IP地址+端口号就能够表示全网唯一的一个进程,所以客户端和服务端之间的进程通信就能够通过这种方式保证其唯一性
像上述这种基于IP地址+端口号的通信方式称为socket
端口号 vs pid:
在之前系统的学习过程中,我们已经知道了进程是有pid的,并且这个pid也能够保证其唯一性,那么为什么还需要端口号呢?
这就像在公司中,我们可以使用身份证号来表示各个员工之间的唯一性,但是为什么还要搞员工号,这是为了解耦,比如当有一天身份证号被取消了,那么是不是所有依赖于身份证号所写的程序就要重新修改,但是如果有公司自己所搞的员工号,那么就能够不依赖于外部的干预
所以需要端口号的意义在于:
- 不是所有的进程都要网络通信,但所有进程都要有pid
- 端口号能够保证系统和网络的解耦
三、UDP与TCP协议:
这里只是对这两个协议有个直观的认识,在后来会对这两个协议进行详细的讨论的
tcp——传输控制协议
- 有连接
- 可靠传输
- 面向字节流
udp——用户数据报协议
- 无连接
- 不可靠传输
- 面向数据报
注意:
上述的所有词都是中性词的意思,并没有褒贬含义,就像化学中的惰性金属一样,并不是说这个金属不好怎么的,比如在上述的可靠和不可靠传输中,可靠传输就意味着设计更复杂,时间成本更大,维护更加复杂,在特定的场景如银行的系统中使用;不可靠传输意味着更加简单,时间成本快,维护简单等等,所以上述词在技术层面上是中性的
四、网络字节序列:
跨主机通信的字节序规范:
字节序列指多字节数据在内存或网络中存储 / 传输时,各个字节的排列顺序
- 大端序:高位字节存于低地址,低位字节存于高地址
- 小端序:低位字节存于低地址,高位字节存于高地址
在早期,很多人为进行存储的方式是大端还是小端争论,知道现在也没个结果,在网络出现后,为了解决不同主机字节序差异的通信,毕竟若直接传输多字节数据,接收方可能因字节序不同导致解析错误,所以规定网络字节序列均采用大端的存储方式
所以发送方需将本地字节序(大端或小端)的数据转换为网络字节序(大端)后再传输;接收方收到数据后,需将网络字节序转换为本地字节序后再处理
五、套接字编程的种类:
套接字编程的种类:
- 遇见套接字编程--->同一个机器内的进程间通信
- 原始套接字编程--->绕过传输层直接访问网络层,用来编写网络工具
- 网络套接字编程--->用户间的网络通信
有多种不同的套接字种类,设计者想要将网络接口统一,就需要保证接口的参数类型是一样的,所以套接字就统一使用struct sockaddr类型
sockaddr的前16个比特位其实也是一种类型,和右边的struct sockaddr_in和struct sockaddr_un是一种类型的,在函数内部实现中,就可以通过判断前16位的类型来表示是走网络套接字还是走域间套接字
if (address->type == AF_INET)
{//走网络的代码
}
else
{//走域间的代码
}
六、套接字接口
socket:
socket是网络编程的核心,它代表一个网络端点(IP 地址 + 端口号),通过套接字,程序可以发送和接收数据,支持 TCP,UDP等多种协议
参数解析:
- domain:指定网络层使用的协议族,常用取值:
值 | 含义 | 说明 |
---|---|---|
AF_INET | IPv4 协议 | 最常用的 IPv4 地址族 |
AF_INET6 | IPv6 协议 | 下一代互联网协议 |
AF_LOCAL | Unix 域套接字 | 本地进程间通信(IPC) |
AF_UNIX | 同 AF_LOCAL | 历史兼容别名 |
AF_PACKET | 底层数据包接口 | 用于实现网络驱动或抓包工具 |
- type:指定传输层使用的通信模式,常用取值:
值 | 含义 | 说明 |
---|---|---|
SOCK_STREAM | 面向连接的 TCP 协议 | 提供可靠、有序、双向的字节流 |
SOCK_DGRAM | 无连接的 UDP 协议 | 不保证可靠性,支持广播和多播 |
SOCK_RAW | 原始套接字 | 直接访问底层协议(如 IP、ICMP) |
SOCK_SEQPACKET | 有序数据包协议 | 类似 TCP,但保留消息边界 |
SOCK_RDM | 可靠数据报协议 | 保证交付,但不保证顺序 |
protocol:通常为 0
,表示根据type自动选择默认协议:
值 | 对应 type | 说明 |
---|---|---|
0 | 任意 | 默认协议(如 TCP/UDP) |
IPPROTO_TCP | SOCK_STREAM | 显式指定 TCP 协议 |
IPPROTO_UDP | SOCK_DGRAM | 显式指定 UDP 协议 |
IPPROTO_ICMP | SOCK_RAW | ICMP 协议(用于 ping 等) |
返回值:
- 成功:返回非负整数(套接字描述符,类似于文件系统中的文件描述符),用于后续操作
- 失败:返回-1,错误码被设置
套接字的本质其实就相当于在底层打开了一个文件,指向底层的网卡设备
bind:
bind函数和socket函数是一起使用的,其主要功能是把一个套接字和特定的网络地址(IP 地址与端口号,也就是socket的返回值)关联起来
参数解析:
- socket:由socket()函数返回的套接字描述符,代表要绑定的套接字
- addr:指向包含IP地址和端口号的地址结构体,也就是套接字结构体
- addrlen:表示addr结构的字节长度
返回值:
成功返回0;失败返回-1,错误码被设置
recvfrom:
其主要作用是:用于接收UDP数据报,同时获取发送方的地址信息,也就是说要知道收到的消息是谁给你发的,因为也要把收到的消息给别人也要返回
参数解析:
- sockfd:由socket()函数返回的套接字描述符
- buf:指向用于存储接收数据的缓冲区
- len:buf的最大容量(字节数)
- flags:控制接收行为的标志位,常用值有0,MSG_DONTWAIT,MSG_PEEK,一般用0即可,表示默认行为,阻塞等待,直到有数据到达;MSG_DONTWAIT非阻塞模式,若无数据立即返回-1,错误码被设置;MSG_PEEK查看数据但不将其从接收队列中移除,下次调用仍会返回相同数据
- src_addr:指向用于存储发送方地址信息的结构体,作用是把对端的套接字信息保存到给指针指向的内存空间里面,一般传入struct sockaddr_in为IPv4
- addrlen:传入时指定src_addr的大小,返回时存储实际地址结构的长度
返回值:
- 成功:返回实际接收的字节数
- 失败返回-1,错误码被设置
注意:在udp套接字中,不能够使用write,read函数,因为write,read是面向字节流的,而udp面向的是一个数据报,所以需要使用recvfrom接口从指定的套接字中得到指定的报文
sendto :
这个函数主要用于向指定目标发送 UDP 数据报
参数解析:
- sockfd:由socket()函数返回的套接字描述符
- buf:你要发送的字符串
- len:buf的最大容量(字节数)
- flags:控制接收行为的标志位,一般用0即可,表示默认行为,阻塞等待,直到有数据到达;
- dest_addr:指向目标地址的结构体,包含接收方的 IP 地址和端口号
- addrlen:这也就是dest_addr的字节数
返回值:
- 成功返回实际发送的字节数
- 失败返回-1,错误码被设置
struct sockaddr:
在上述的众多接口中可以看到有一种类型是struct sockaddr,那么接下来看看这个结构体
在 C++ 网络编程中,struct sockaddr是一个核心数据结构,用于表示网络地址,它是所有网络地址结构的通用基类
其源码就是上述4行
第一个__SOCKADDR_COMMON就是一个宏,其定义如下,##作用是连接左右
经过上述的一系列转化,其实就是unsigned short int sa_family
sa_data:
- 存储具体的地址信息(如 IP、端口),长度固定为 14 字节。
- 不同协议族有不同的解释方式,直接操作易出错,因此使用特定的派生结构
struct sockaddr_in
struct sockaddr_in {short int sin_family; // 地址族(Address Family),固定为 AF_INET(IPv4)unsigned short int sin_port; // 端口号(网络字节序)struct in_addr sin_addr; // IPv4 地址结构体unsigned char sin_zero[8]; // 填充字节,确保与 struct sockaddr 大小相同
};struct in_addr {unsigned int s_addr; // 32 位 IPv4 地址(网络字节序)
};
处理字节序函数:
由于不同计算机体系结构可能采用不同的字节序,这些函数确保网络通信中数据的正确传输
- htnol:将32位整数从主机字节序转换为网络字节序(大端序)
- htnos:将16位整数从主机字节序转换为网络字节序(大端序)
- ntohl:将32位整数从网络字节序转换为主机字节序
- ntohs:将16位整数从网络字节序转换为主机字节序
IP地址转换:
如上的接口涉及:IPv4地址在点分十进制字符串和网络字节序二进制之间的转换,以及网络地址结构的操作
inet_aton:
这是将IP地址从 ASCII 到 网络地址(二进制)
功能:将 点分十进制字符串(如192.168.1.1)转换为 网络字节序的二进制 IP,然后在存入struct in_addr结构体中
参数:
cp:这就是点分十进制的IP字符串
inp:这是一个输出型参数,其作用是存储转换后的二进制IP(网络字节序)
返回值:成功返回非0,失败返回0
inet_addr
ASCII 到网络地址(简洁版)
功能:将点分十进制字符串直接转为网络字节序的32位整数
这里返回的in_addr_t本质就是uint32_t进行封装了的,这个接口在转换失败的时候的返回值是0xFFFFFFFF,这个也对应着255.255.255.255,无法区分
inet_ntoa:
网络地址(二进制)转 ASCII
功能:将struct in_addr中的网络字节序二进制IP转为点分十进制字符串
注意:返回的字符串是静态缓冲区(内部共享),多次调用会覆盖之前结果
inet_pton:
功能:将点分十进制格式的 IP 字符串(如“192.168.1.1”或 IPv6 的字符串形式 )转换成网络字节序的二进制形式
参数解析:
af:指定地址族,决定处理 IPv4 还是 IPv6 地址常见取值:AF_INET处理 IPv4 地址,AF_INET6处理IPV6地址
src:指向以空字符结尾的字符串,内容是待转换的 IP 地址
dst:转换后的二进制结果存储地址
- 若af = AF_INET,dst需指向struct in_addr类型变量(存储 IPv4 二进制地址,网络字节序)
- 若af = AF_INET6,dst需指向struct in6_addr类型变量(存储 IPv6 二进制地址,网络字节序 )
返回值:
成功返回1,失败返回0(若af不支持(非AF_INET或AF_INET6),或src格式非法)或-1(若出现系统错误(如参数传递异常 ))
inet_ntop:
功能:把网络字节序的二进制 IP转换成可读的点分十进制
参数解析:
af:指定地址族,决定处理 IPv4 还是 IPv6 地址常见取值:AF_INET处理 IPv4 地址,AF_INET6处理IPV6地址
src:指向二进制 IP 地址的指针:
- IPv4 时,src需指向struct in_addr
- IPv6 时,src需指向struct in6_addr
dst:转换后的字符串存储缓冲区,需提前分配足够空间,函数会把点分十进制字符串写入这里
size:dst缓冲区的大小限制,防止溢出
返回值:
- 成功:返回dst的指针(即转换后的字符串首地址,方便直接使用 )
- 失败:返回NULL,错误码被设置