网络编程套接字

理解源IP地址和目的IP地址

在 IP 数据包头部中,有两个 IP 地址,分别叫做源 IP 地址和目的 IP 地址。

源 IP 地址是指发送数据的主机的 IP 地址,它标识了数据来源。当数据从源主机发送到目的主机时,数据包中会包含源 IP 地址,以便于目的主机响应数据。
目的 IP 地址是指数据传输的目的主机的 IP 地址。当数据包到达时目的主机时,目的主机会根据目的 IP 来接收和处理数据。

在互联网中,每台计算机都有一个唯一的 IP 地址。当一台主机上的数据需要传送到另外一个主机时,目的主机的 IP 地址被用作数据传输的目的 IP 地址。然后,仅知道自己的 IP 地址是不够的。一旦目标主机接收到数据,它需要对该主机做出响应,因此目标主机也需要将数据发送回改主机。因此,目标主机需要包含源 IP 地址和目的 IP 地址。

关于端口号

端口号(port)是传输层协议的内容。

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程,它告诉操作系统当前数据应该交给哪个进程来处理;
  • IP地址+端口号 能够标识网路上的某一台主机的某一个进程,且一个端口只能被一个进程占用;

当数据到达目标主机后,需要通过一种方法来找到目标主机上对应的服务进程,并将数据交给该进程进行处理。这个方法就是使用个目标主机上的端口号。每个进程都会绑定一个特定的端口号,当数据到达目标主机后,网络设备回根据数据包中的目的端口找到对应的进程,并将数据交给它处理。

在网络通信中,源IP地址、源端口号、目的IP地址和目的端口号共同使用,可以确保数据能够准确地发送到目标进程,并且目标进程能够向发送端响应。

在学习系统编程的时候,学习了 pid 表示唯一一个进程,此处我们的端口号也是唯一表示一个进程,那么这两者之间有什么关系吗❓

进程ID(PID)是操作系统内核为每个进程分配的唯一标识符。它是一个整数,用于在操作系统中识别和管理进程。PID 用于在操作系统内部进行进程管理、资源分配等;端口号是用于标识需要对外进行网络数据请求的进程的唯一性。每个网络进程可以绑定一个特定的端口号,以便于其它进程或主机可通过该端口号找到它并与之通信。端口号在网络中起作用,用于标识网络上的进程。

在一台主机上可能存在着大量的进程,但并不是所有的进程都需要进行网络通信。对于不需要进行网络通信的本地进程,使用 PID 来标识它们的唯一性更合适。而对于需要进行网络通信的进程,使用端口号来标识它们的唯一性更恰当。

类比于身份证号、学号等。它们在不同的场景下都可以标识我们的唯一性,只是在不同的场景中使用不同的管理方式更加恰当、方便。

TCP协议和UDP协议初识

TCP(传输控制协议 - Transmission Control Protocol)和 UDP(用户数据报协议 - User Datagram Protocol)是互联网协议族中的两个主要传输层协议,它们在数据传输方式、可靠性和连接性等方便有所区别。

连接性:

  • TCP 是面向连接的协议,通信双方在数据传输之前需要先建立一个连接。连接的建立包括三次握手的过程,确保双方都能够正常通信。TCP 提供可靠的数据传输,通过序号、确认和重传机制来保证数据的完整性和可靠性。
  • UDP 是无连接的协议,通信双方不需要建立连接。UDP 协议不提供可靠保证,他只是简单的将数据传输出去,不关心目的主机是否收到或者丢失。

可靠性:

  • TCP 是可靠传输。如果发送方在一定时间内没有收到确认消息,就会向目标主机重新发送对应的数据。
  • UDP 是不可靠传输。UDP 适用于实时性要求较高的应用,例如:音视频传播、赛事直播等。

数据传输方式:

  • TCP 是面向字节流的协议。它将数据报看作是一连串的字节流,而不是分割成固定大小的数据块。TCP 会将发送的任意大小的数据分割成适当大小的数据段进行传输。
  • UDP 是面向数据报的协议。它将数据划分为小的数据包(数据报),每个数据包携带自己的有效信息(报头),独立发送和接收。

既然UDP协议是不可靠的,那为什么还要有UDP协议的存在❓

首先,TCP 虽然是可靠的,但是在保证可靠性上就在底层做了更多的工作。因此,TCP 相较于 UDP 的实现更为复杂。UDP 协议虽然是不可靠的,但是在一些特定的场景下,它仍然有存在的道理:

  • 相对于 TCP 协议,UDP 协议没有建立连接和维护状态的开销。因此传输速度更快,延迟更低。
  • 相较于 TCP 协议的复杂的连接和可靠性机制,UDP 协议的设计更简单。
  • UDP 协议支持广播和多播功能。这些功能在一些特定的场景中特别有用,比如:视频直播、实时通信等。
  • UDP 协议允许应用程序自定义数据包的格式和处理方式。开发人员可以根据特定的需求设计出更加适合的协议。

即 UDP 协议的存在是为了满足一些特定的需求。尽管它没有 TCP 协议那样的可靠性保证,但在特定场景下能够提供更好的性能和灵活性。

✴️某些网站和应用程序在设计网络通信算法时会同时使用 TCP 协议和 UDP 协议,并根据网络状况动态选择合适的协议进行数据传输。这种策略被称为混合传输或自适应传输。

✴️同时使用 TCP 和 UDP 协议,可以充分的利用它们的优势。当网络流畅时,可以使用 UDP 协议进行数据传输,以提高传输速率和降低延迟。当网络质量不好时,UDP 协议可能会导致数据丢失或乱序。为了保证数据的可靠性,可以切换到 TCP 协议进行数据传输。动态调整后台数据通信算法可以根据网络状况实时选择合适的协议进行数据传输。

网络字节序

我们知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样也有大断和小端之分。

大端存储(Big Endian)是指将高位字节存储在低地址,低位字节存储在高地址。
小端存储(Little Endian)是指将低位字节存储在低地址,高位字节存储在高地址。

在本地机器上运行的程序通常不需要考虑大小端的问题。而不同主机可能采用不同的存储方式模式。因此,涉及到网络通信时,就必须考虑大小端的问题,因为网络通信涉及数据在不同主机之间的传输和解析。若发送端和接收端数据存储方式不一致,那么接收端就可能错误的解析数据,导致数据错误。

那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把网络上接收到的字节一次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网路数据流的地址应该这样规定:先发出的数据是低地址,后发出的数据是高地址。
  • TCP/IP 协议规定了网络数据流应采用大端字节序。无论发送主机是大端机还是小端机,都必须按照大端字节序发送和接收数据。
  • 如果当前发送主机是小端,就需要在发送之前将数据转化为大端;否则就忽略,直接发送即可。

示例:现在又两台主机需要进行网路通信,发送端的存储方式为小端字节序,接收端的存储方式为大端字节序。发送端将数据(0x1234abcd)转化为大端字节序放入发送缓冲区中,网络进行数据传输后,数据到达接收端的数据缓冲区,接收端上层提取数据,并将数据转化为自己主机的存储模式。
在这里插入图片描述

为什么 TCP/IP 协议规定,网络数据流需要采用大端字节序,而不用小端字节序❓

TCP/IP 协议规定网络数据流需要采用大端字节序,而不用小端字节序,主要有以下原因:

  1. 在 TCP/IP 协议的设计之初,大部分主机都采用了大端字节序。因此,为了与当时存在的主机和网络设备兼容,TCP/IP 协议选择了大端字节序作为网络字节序。
  2. 大端字节序更符合现代人的阅读习惯。在大端字节序中,高位字节在前,低位字节在后,与人们的阅读习惯一直。这种方式更便于人们理解。
  3. 尽管如今的计算机体系结构中,小端字节序已经成为了主流,但为了保持既有网络设备和协议的兼容性,TCP/IP 协议在数据传输中使用大端字节序。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上都能够正常的运行,可以调用以下的库函数做网络字节序和主机字节序之间的转换:

#include <arpa/inet.h>

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);
  • 例如,htonl() 函数在小端字节序的主机上会将32位长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送;
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,函数将不做转换,将参数原封不动的返回;
  • 通过这些函数,可以确保在不同主机上编写的网络程序都能在各种字节序的主机上正确运行。

socket编程

什么是socket编程

socket(套接字)编程是一种连接网络上的两个节点以相互通信的方法。一个套接字(节点)监听 IP 上的特殊端口,而另一个套接字通过连接到另一个套接字来建立连接。服务器创建监听套接字,而客户端则连接到服务器。

socket常见API

创建 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 指定了服务器的地址和端口。

sockaddr 结构

socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6、UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。

套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)。在进程跨网络通信时我们需要传送 IP 地址和端口号,本地通信则不需要,因为此套接字提供了 sockaddr_in 结构体和 sockaddr_un 结构体,其中 sockaddr_in 结构体用于跨网路通信,而 sockaddr_un 结构用于本地通信。

为了让套接字的网络通信和本地通信能使用同样的接口,引入了 sockaddr 结构体。虽然 sockaddrsockaddr_insockaddr_un 这三个结构体在整体上下不相同,但是它们的头部16个比特位是相同的,该字段被称为协议家族(protocol family)。

在这里插入图片描述
在 API 内部,可以提取 sockaddr 结构体的头部16位来判断通信类型,并执行相应的操作。这样,通过统一使用 sockaddr 结构体作为参数类型,实现了套接字网络通信和本地通信参数的统一性。

在实际进行网络通信时,我们仍然需要定义 sockaddr_in 等特定的结构体,只是在传参时需要将该结构体的地址类型强制转换为 sockaddr* 类型。这样做减少了参数传递的复杂性,同时保持了对特定套接字结构体的使用。

早期的通信标准和协议存在多个不同的实现和方案,不同的实验室或组织可能采用自己的通信方式。这导致了套接字结构体的多样性,包括 System V 标准的通信方式和 POSIX 标准的通信方式。

  • IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括16位地址类型,16位端口号和32位 IP 地址;
  • IPv4 和 IPv6 地址类型分别定义为常量 AF_INETAF_INET6 。通过获取 sockaddr 结构体的首地址,无需知道具体哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容;
  • socket API 可以使用 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位的整数。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风&57

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值