在 IP 数据包头部中,有两个 IP 地址,分别叫做源 IP 地址和目的 IP 地址。
源 IP 地址是指发送数据的主机的 IP 地址,它标识了数据来源。当数据从源主机发送到目的主机时,数据包中会包含源 IP 地址,以便于目的主机响应数据。
目的 IP 地址是指数据传输的目的主机的 IP 地址。当数据包到达时目的主机时,目的主机会根据目的 IP 来接收和处理数据。
在互联网中,每台计算机都有一个唯一的 IP 地址。当一台主机上的数据需要传送到另外一个主机时,目的主机的 IP 地址被用作数据传输的目的 IP 地址。然后,仅知道自己的 IP 地址是不够的。一旦目标主机接收到数据,它需要对该主机做出响应,因此目标主机也需要将数据发送回改主机。因此,目标主机需要包含源 IP 地址和目的 IP 地址。
端口号(port)是传输层协议的内容。
当数据到达目标主机后,需要通过一种方法来找到目标主机上对应的服务进程,并将数据交给该进程进行处理。这个方法就是使用个目标主机上的端口号。每个进程都会绑定一个特定的端口号,当数据到达目标主机后,网络设备回根据数据包中的目的端口找到对应的进程,并将数据交给它处理。
在网络通信中,源IP地址、源端口号、目的IP地址和目的端口号共同使用,可以确保数据能够准确地发送到目标进程,并且目标进程能够向发送端响应。
在学习系统编程的时候,学习了 pid 表示唯一一个进程,此处我们的端口号也是唯一表示一个进程,那么这两者之间有什么关系吗❓
进程ID(PID)是操作系统内核为每个进程分配的唯一标识符。它是一个整数,用于在操作系统中识别和管理进程。PID 用于在操作系统内部进行进程管理、资源分配等;端口号是用于标识需要对外进行网络数据请求的进程的唯一性。每个网络进程可以绑定一个特定的端口号,以便于其它进程或主机可通过该端口号找到它并与之通信。端口号在网络中起作用,用于标识网络上的进程。
在一台主机上可能存在着大量的进程,但并不是所有的进程都需要进行网络通信。对于不需要进行网络通信的本地进程,使用 PID 来标识它们的唯一性更合适。而对于需要进行网络通信的进程,使用端口号来标识它们的唯一性更恰当。
类比于身份证号、学号等。它们在不同的场景下都可以标识我们的唯一性,只是在不同的场景中使用不同的管理方式更加恰当、方便。
TCP(传输控制协议 - Transmission Control Protocol)和 UDP(用户数据报协议 - User Datagram Protocol)是互联网协议族中的两个主要传输层协议,它们在数据传输方式、可靠性和连接性等方便有所区别。
连接性:
可靠性:
数据传输方式:
既然UDP协议是不可靠的,那为什么还要有UDP协议的存在❓
首先,TCP 虽然是可靠的,但是在保证可靠性上就在底层做了更多的工作。因此,TCP 相较于 UDP 的实现更为复杂。UDP 协议虽然是不可靠的,但是在一些特定的场景下,它仍然有存在的道理:
即 UDP 协议的存在是为了满足一些特定的需求。尽管它没有 TCP 协议那样的可靠性保证,但在特定场景下能够提供更好的性能和灵活性。
✴️某些网站和应用程序在设计网络通信算法时会同时使用 TCP 协议和 UDP 协议,并根据网络状况动态选择合适的协议进行数据传输。这种策略被称为混合传输或自适应传输。
✴️同时使用 TCP 和 UDP 协议,可以充分的利用它们的优势。当网络流畅时,可以使用 UDP 协议进行数据传输,以提高传输速率和降低延迟。当网络质量不好时,UDP 协议可能会导致数据丢失或乱序。为了保证数据的可靠性,可以切换到 TCP 协议进行数据传输。动态调整后台数据通信算法可以根据网络状况实时选择合适的协议进行数据传输。
我们知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样也有大断和小端之分。
大端存储(Big Endian)是指将高位字节存储在低地址,低位字节存储在高地址。
小端存储(Little Endian)是指将低位字节存储在低地址,高位字节存储在高地址。
在本地机器上运行的程序通常不需要考虑大小端的问题。而不同主机可能采用不同的存储方式模式。因此,涉及到网络通信时,就必须考虑大小端的问题,因为网络通信涉及数据在不同主机之间的传输和解析。若发送端和接收端数据存储方式不一致,那么接收端就可能错误的解析数据,导致数据错误。
那么如何定义网络数据流的地址呢?
示例:现在又两台主机需要进行网路通信,发送端的存储方式为小端字节序,接收端的存储方式为大端字节序。发送端将数据(0x1234abcd)转化为大端字节序放入发送缓冲区中,网络进行数据传输后,数据到达接收端的数据缓冲区,接收端上层提取数据,并将数据转化为自己主机的存储模式。
为什么 TCP/IP 协议规定,网络数据流需要采用大端字节序,而不用小端字节序❓
TCP/IP 协议规定网络数据流需要采用大端字节序,而不用小端字节序,主要有以下原因:
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上都能够正常的运行,可以调用以下的库函数做网络字节序和主机字节序之间的转换:
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h
表示主机字节序(host),n
表示网络字节序(network),l
表示32位长整数(long),s
表示16位短整数(short);socket(套接字)编程是一种连接网络上的两个节点以相互通信的方法。一个套接字(节点)监听 IP 上的特殊端口,而另一个套接字通过连接到另一个套接字来建立连接。服务器创建监听套接字,而客户端则连接到服务器。
创建 socket 文件描述符(TCP/UDP,客户端+服务器)
int socket(int domain, int type, int protocol);
创建套接字时的参数说明:
sockfd
:套接字描述符,是一个整数,类似于文件句柄。domain
:整数,指定通信域(Communication Domain)。在同一主机上的进程之间进行通信时,我们使用 POSIX 标准定义的 AF_LOCAL。在不同的主机通过 IPv4 连接的进程之间通信时,我们使用 AF_INET,对于通过 IPv6 连接的进程之间进行通信时,我们使用 AF_INET6。type
:通信类型(Communication Type)。SOCK_STREAM:TCP(可靠的、面向连接的);SOCK_DGRAM:UDP(不可靠的、无连接的)。protocol
:互联网协议的协议值,通常为0。这与数据包的 IP 头中的协议字段中显示的数字相同。绑定端口号(TCP/UDP,服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
bind 函数将套接字绑定到 addr(自定义数据结构)指定的地址和端口号。在套接字创建后,我们使用 bind 函数将服务器绑定到本地主机,因此我们使用 INADDR_ANY 来指定 IP 地址。
开始监听 socket(TCP,服务器)
int listen(int socket, int backlog);
listen 函数将服务器套接字置于被动模式下,在此模式下,它等待客户端与服务器建立连接。backlog 参数定义了 sockfd 的等待连接队列的最大长度。如果连接请求在队列已满时到达,客户端可能会收到一个带有 ECONNREFUSER 指示的错误。
接收请求(TCP,服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
accept 函数从监听套接字的等待连接队列中提取第一个连接请求,创建一个新的已连接套接字,并返回一个新的文件描述符,引用该套接字。此时,客户端和服务器之间建立了连接,并且准备好传输数据。
建立连接(TCP,客户端)
int connect(int sockfd, const struct sockaddr* addr, socketlen_t addrlen);
connect 函数用于在文件描述符为 sockfd 的套接字与 addr 指定的地址之间建立连接。在客户端应用程序中,addr 指定了服务器的地址和端口。
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6、UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。
套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)。在进程跨网络通信时我们需要传送 IP 地址和端口号,本地通信则不需要,因为此套接字提供了 sockaddr_in
结构体和 sockaddr_un
结构体,其中 sockaddr_in
结构体用于跨网路通信,而 sockaddr_un
结构用于本地通信。
为了让套接字的网络通信和本地通信能使用同样的接口,引入了 sockaddr
结构体。虽然 sockaddr
、sockaddr_in
和 sockaddr_un
这三个结构体在整体上下不相同,但是它们的头部16个比特位是相同的,该字段被称为协议家族(protocol family)。
在 API 内部,可以提取 sockaddr 结构体的头部16位来判断通信类型,并执行相应的操作。这样,通过统一使用 sockaddr 结构体作为参数类型,实现了套接字网络通信和本地通信参数的统一性。
在实际进行网络通信时,我们仍然需要定义 sockaddr_in 等特定的结构体,只是在传参时需要将该结构体的地址类型强制转换为 sockaddr* 类型。这样做减少了参数传递的复杂性,同时保持了对特定套接字结构体的使用。
早期的通信标准和协议存在多个不同的实现和方案,不同的实验室或组织可能采用自己的通信方式。这导致了套接字结构体的多样性,包括 System V 标准的通信方式和 POSIX 标准的通信方式。
netinet/in.h
中,IPv4 地址用 sockaddr_in
结构体表示,包括16位地址类型,16位端口号和32位 IP 地址;AF_INET
和 AF_INET6
。通过获取 sockaddr
结构体的首地址,无需知道具体哪种类型的 sockaddr
结构体,就可以根据地址类型字段确定结构体中的内容;struct sockaddr*
类型表示,在使用时需要强制转化成 sockaddr_in
。这样做的好处是提高程序的通用性,可以接受 IPv4、IPv6 以及 UNIX Domain Socket 各种类型的 sockaddr
结构体指针作为参数。sockaddr 结构:
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
sockaddr_in 结构:
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* 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 API 的接口是 sockaddr,但是真正在基于 IPv4 编程时,使用的数据结构是 sockaddr_in,这个结构里主要有三部分信息:地址类型、端口号、IP地址。
in_addr 结构:
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
in_addr 用来表示一个 IPv4 的IP地址,其实就是一个32位的整数。