文章目录
🌈 一、IP 地址概念
⭐ 1. IP 地址的作用
- 互连网上的每台主机都有一个唯一的 IP 地址,总的来说就是,IP 地址在网络中用来标识主机的唯一性。
⭐ 2. 源 IP 地址和目的 IP 地址
- 如果想将一台主机 A 的数据传输到另一台主机 B 上,发送数据的 A 主机的 IP 地址就是源 IP 地址,而接受数据的对端主机 B 的 IP 地址就应该作为该数据传输时的目的 IP 地址。
- 例:唐僧取经时,会根据他所持有的源 IP 地址 (东土大唐) 以及目的 IP 地址 (西天),在取经路上不停的问路,中间走过的每一站都是为了更加靠近目的 IP 地址。
- 只要有了 IP 地址,数据报文就不会走进不在源 IP 地址和目的 IP 地址之间的其他局域网。
- 在数据进行传输前,会先自定向下贯穿网络协议栈完成数据的封装,其中在网络层封装的 IP 报头中就包含了该数据的源 IP 地址和目的 IP 地址。
🌈 二、端口号概念
⭐ 1. 源端口号和目的端口号
- 主机之间进行通信并不仅是为了将数据发送给对端主机而已,那没有任何意义。主机之间进行通信是为了访问对端主机上的某个服务。
- 用户 a 在 A 主机上使用 qq 给用户 b 的主机 B 中的 qq 发送数据,不是就将这段数据发给主机 B 就完了。
- 通信的根本目的是为了实现人与人之间的通信,用户 a 从 qq 上发送给用户 b 的消息,需要让用户 b 也能在 qq 上看到才行。
- 但是用户手里的主机上有那么多进程,没人知道发送的数据应该交给主机上的哪个进程。
- 同时,如果用户 b 想要给用户 a 回消息,发送的数据也需要能够找到用户 a 手里的主机中的 qq 这个程序才行。
- 因此,端口号是用来唯一标识主机中的进程。
- 真正的通信是两个主机上的两个进程在进行通信,而源端口号和目的端口号就是用来找到通信双方的主机中的那个用来通信的进程。
⭐ 2. 端口号范围划分
- 知名端口号 (0 ~ 1023),像 HTTP、FTP、SSH 等广泛使用的应用层协议的端口号都是固定的。
- 这些端口号和其提供的服务基本上已经算是一个东西了,只要知道这些端口号,就能知道对应提供的是什么服务。
- 就像 120、110、119 这些电话和与其绑定的服务一样,看到这些电话就知道对应的是啥服务。
- 操作系统动态分配端口号 (1024 ~ 65535),由操作系统动态分配的端口号,客户端进程的端口号由操作系统从这个范围分配。
⭐ 3. 端口号 VS 进程 ID
- 端口号和进程 IP 都能用来唯一标识一台主机上的某个进程,但在网络通信中,并不能用进程 ID 来替代端口号。
1. 为什么不能用进程 PID 替代网络端口号 port
- 端口号属于网络的概念,进程 ID 用来标识系统内进程的唯一性,它属于系统级的概念;而端口号用来标识需要对外进行网络数据请求的进程的唯一性,它属于网络的概念。
- 不是所有的进程都要进行网络通信,一台主机上存在着 n 个进程,但不是所有的进程都要进行网络通信的,但每个进程都要有自己的 PID。这种情况下就不太适合使用 PID 来标识网络进程的唯一性了。
- 专事专办,在不同的场景下可能需要不同的编号来标识某种事物的唯一性,某些编号会更加适用于某种场景。
- 如:身份证号已经足够标识身份的唯一性了,但还是有学号和工号这种特殊编号用来标识在不同场景下的唯一身份。
- 实现系统和网络的解耦 (最重要),进程在每次启动时,进程 PID 都会发生变化。如果使用 PID 代替端口号,会直接导致网络部分也需要作出调整。
2. 如何通过端口号 port 找到对应的进程
- 在底层中,采用哈希的方式建立了端口号和进程 PID 之间的映射关系。
- 当底层拿到端口号时,就可以执行对应的哈希算法,然后拿到与该端口号对应的进程 PID,从而找到对应进程。
⭐ 4. 套接字 socket 的概念
- IP 地址用来标识网络中唯一的一台主机,而端口号 port 则用来标识该主机上唯一的一个网络进程。
- 因此,使用 ip + port 就能标识互联网中唯一的一个进程。
- 网络通信,本质上是在用两个互联网进程代替人来进行通信,通过 { 源 ip,源 port,目的 ip,目的 port } 即可标识互联网中唯二的两个进程。
- 因此,网络通信的本质就是进程间通信。
- 将 ip + port 的组合叫做 socket 套接字。
理解 socket 这个名词
- socket 翻译成中文有 ⌈ 插座 ⌋ 的意思,插座上有不同规格的插孔,将插头插入到对应的插孔当中就能够实现电流的传输。
- 在进行网络通信时,客户端就相当于插头,服务端就相当于一个插座,但服务端上可能会有多个不同的服务进程 (多个插孔)。因此当访问服务时需要指明服务进程的端口号 (对应规格的插孔),才能享受对应服务进程的服务。
🌈 三、传输层的典型代表协议
- 网络协议栈贯穿整个网络体系结构,在应用层中,操作系统层和驱动层各自占有一部分网络协议。
- 传输层写在操作系统中,当使用系统提供的接口实现网络通信时,必须要面对的就是传输层的协议,传输层最典型的协议是 TCP 和 UDP 。
⭐ 1. TCP 协议
- 传输控制协议 TCP (Transmission Control Protocol) 是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP 协议的特点
- TCP 协议是面向连接的:当两台主机之间想要进行数据传输时,需要先建立连接。只有在连接建立成功后才可以进行数据传输。
- TCP 协议是保证可靠的:数据在传输过程中如果出现了丢包、乱序等情况,TCP 协议都有对应的解决办法。
⭐ 2. UDP 协议
- 用户数据报协议 UDP (User Datagram Protocol) 是一种无需建立连接的、不可靠的、面向数据包的传输层通信协议。
UDP 协议的特点
- UDP 协议不需要建立连接:当两台主机想要进行数据传输时,直接将数据包发送给对端主机即可。
- UDP 协议是不保证可靠的:由于特点 1,UDP 协议是不可靠的。UDP 协议不知道数据在传输过程中是否出现了丢包、乱序等情况。
⭐ 3. 如何选择 TCP 还是 UDP
1. 不应将 UDP 协议的特点当作缺陷
- UDP 不保证可靠并不是缺陷,而是特点。
- UDP 不保证可靠意味着要做的工作比 TCP 少,实现起来比 TCP 简单,且数据传输速度更快。
2. 如何选择通信协议
- 编写网络通信代码时,应根据上层的应用场景选择 TCP / UDP 协议。
- 当应用场景严格要求数据在传输过程中的可靠性时,选择 TCP 协议。
- 当应用场景允许数据传输出现少量丢包时,优先选择简单的 UDP 协议。
🌈 四、网络字节序
⭐ 1. 网络中的大小端
- 大端模式:数据的高位字节处的内容存放在内存的低地址处,而数据的低位字节处的内容存放在内存的高地址处。
- 小端模式:数据的高位字节处的内容存放在内存的高地址处,而数据的低位字节处的内容存放在内存的低地址处。
⭐ 2. 网络字节序采用大端方式存储
- 如果程序只在本地机器上运行,由于同一台机器的数据的存储方式一致,因此无需考虑数据的大小端存储问题。
- 如果程序涉及到网络通信,则需要考虑大小端的转换问题,否则接收端主机识别出的数据可能与发送端发送的数据不一致。
- 由于不能保证通信双方存储数据的方式一致,那么就只能统一网络字节序,TCP / IP 协议规定,网络数据流采用大端字节序。
- 如果 发送端 是 小 端存储,需要先将数据转换成大端,然后发送到网络中。
- 如果 发送端 是 大 端存储,可以直接将数据发送到网络中。
- 如果 接收端 是 小 端存储,需要先将接收到的数据转换成小端,然后进行识别。
- 如果 接收端 是 大 端存储,可以直接识别通过网络传输过来的数据。
⭐ 3. 网络字节序与主机字节序之间的转换
- 为了让网络程序具备可移植性,使得同样的 C 代码在 大端 / 小端 机上都能运行,系统提供了4 个函数用于实现网络字节序和主机字节序之间的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // 将 32 位的 主机字节序 转换为 32 位的 网络字节序
uint16_t htons(uint16_t hostshort); // 将 16 位的 主机字节序 转换为 16 位的 网络字节序
uint32_t ntohl(uint32_t netlong); // 将 32 位的 网络字节序 转换为 32 位的 主机字节序
uint16_t ntohs(uint16_t netshort); // 将 16 位的 网络字节序 转换为 16 位的 主机字节序
- 函数名看着挺混乱,但还是有规律的,h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数。
- 例:
htonl
表示的就是将 32 位的主机字节序转换成 32 位的网络字节序,其余同理。
- 例:
- 如果主机采用的是小端存储方式,这些函数就会将提供参数做相应的大小端转换,然后返回转换后的大端字节序。
- 如果主机采用的是大端存储方式,这些函数就不会进行转换,而是直接将参数返回。
🌈 五、socket 编程接口
⭐ 1. socket 常见函数
- 创建套接字
int socket(int domain, int type, int protocol);
- 绑定端口号
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
- 监听套接字
int listen(int sockfd, int backlog);
- 接受请求
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
- 建立连接
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
⭐ 2. sockaddr 结构介绍
- socket 不仅支持跨网络的进程间通信,还支持本主机的进程间通信。
- 在创建套接字时,需要选择创建的是用于网络通信的网络套接字,还是用于本地通信的域间套接字。
- 由于在进行网络通信时,需要传递 ip + port,而本地通信则不需要。因此套接字就提供了用于网络通信的
sockaddr_in
结构体,以及用于本地通信的sockaddr_un
结构体。 - 而为了让网络通信和本地通信都能使用同一个函数,又出现了一种新的结构体
sockaddr
,这 3 种结构体的前面 16 个比特位相同,都叫做协议家族。
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IP 地址
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
- 在使用 socket 相关函数时,不管要进行的是网络通信还是本地通信,统一传入
sockaddr
结构体作为 socket 相关函数的参数。 - 通过设置
sockaddr
的协议家族来决定进行的是网络通信还是本地通信,socket 相关函数会提取出sockaddr
的前 16 个比特位来判断要进行的是本地还是网络通信。 - 在使用 socket 相关函数时,不管要进行的是网络通信还是本地通信,统一传入
sockaddr
结构体作为 socket 相关函数的参数。 - 通过设置
sockaddr
的协议家族来决定进行的是网络通信还是本地通信,socket 相关函数会提取出sockaddr
的前 16 个比特位来判断要进行的是本地还是网络通信。 - 编写网络通信代码时,定义的依旧是
sockaddr_in
结构体;传参时,需要将定义的sockaddr_in
结构体变量的地址类型强转为sockaddr*
。