socket编程基础

上一篇 --- 网络基础概念(下)https://blog.csdn.net/Small_entreprene/article/details/147320155?fromshare=blogdetail&sharetype=blogdetail&sharerId=147320155&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link

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

在我们当前的认识当中,IP地址是用来标识主机的唯一性的,后面我们会详细的对IP进行分类。

数据传输到主机并不是最终目的,因为数据是给人使用的。例如,聊天是人在聊天,下载是人在下载,浏览网页是人在浏览。那么,人是如何看到聊天信息、执行下载任务以及浏览网页信息的呢?答案是通过启动的 QQ、迅雷、浏览器等软件。而这些启动的 QQ、迅雷、浏览器等都是进程。换句话说,进程是人在系统中的代表,只要把数据交给进程,人就相当于拿到了数据。

因此,数据传输到主机只是手段,而不是目的。真正的目的是将数据传输到主机内部,并交给主机内的目标进程。

我们上网,其实可以概括成两种行为:

  1. 从远端服务器,获取数据(刷抖音,其实就是将抖音推送到手机端)
  2. 本地数据,上传到远端服务器 (登入,将账号密码推送到远端;上传文件到百度网盘)

不管我们的上网行为有多么丰富多样,在技术角度也就只有两种情况,一种上传,一种下载!因为我们的数据是通过进程来做的,进程又是在内存当中的,上网的时候,所有的数据都是要经过网卡的,而网卡需要将数据给网络。(是进程和网卡之间的关系,网络和网卡的关系)

我们将上面两者两者之间的关系叫做IO!!! 说白了:冯诺依曼体系结构规定,网卡只能进行IO操作,就决定了我们的应用层软件上,只能做获取信息和发送信息的行为。

说明了网络通信的本质就是两个不同主机的进程在进行数据交互,更本质就是进程间通信!

进程间通信的本质就是要看到同一份资源,那么两个不同主机的进程要看到的同一份资源又是谁呢?就是网络!!! (今天只是从在同一台主机内进行进程间通信换成了在不同的两台主机间进行进程间通信而已)

我们现在知道,数据传输到主机只是手段,而不是目的。真正的目的是将数据传输到主机内部,并交给主机内的目标进程。然而,在系统中,同时会存在非常多的进程。因为收到的数据是要分配给一个或多个进程的,哪些数据对应哪一个进程,那么当数据到达目标主机之后,如何将数据转发给目标进程呢?这需要在网络的背景下,通过某种方式在系统中标识主机的唯一性,从而确保数据能够准确地被转发到目标进程。

我们主机会收到来自远端主机发送来的各种数据,这些数据需要按照对应不同的数据分发到对应的进程当中,所以我们就必须要在系统层面上有一种办法来标识主机的唯一性,为了能够实现主机唯一性的标识,我们在网络的范畴当中,我们就引入了新的概念:端口号!

认识端口号

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

  • 端口号是一个2字节16位的整数。

  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。(也就是说,未来写的网络服务,比如说QQ,这个网络服务要进行启动的时候,需要通过操作系统提供的一些系统调用来让这个进行和对应的端口号(在传输层提取报文中的目的端口号)产生对应的关联)

  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。

  • 一个端口号只能被一个进程占用。(就是端口号可以用来标识系统中唯一的一个网络进程!!!)(其实反过来,一个进程是可以和多个端口号进行绑定的,因为我们要的是从端口号查进程的方向是唯一的)

网络通信的本质是全网范围内唯二的两个进程在进行进程间通信!!!我们用对方的IP和Port标识对方的唯一性。我们将IP+Port称为Socket(套接字)

不过,端口号端口号可以用来标识系统中唯一的一个网络进程,但是我们学习过,pid也是进程的唯一标识,那为什么不直接用进程pid呢?

不是所有的进程都需要进行网络通信,不过我们从技术角度上来说,使用pid不使用端口号,这是可行的,但是pid是一个系统的概念,如果未来pid这个概念变化了,伴随着网络就需要变,这就是耦合性差,使用端口号就可以进行解耦!!!


端口号的范围划分是【0,65535】,因为是一个两字节(16比特位)的整数

  • 0 - 1023:知名端口号,HTTP、FTP、SSH 等这些广为使用的应用层协议,它们的端口号都是固定的。
  • 1024 - 65535:操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的。

传输层协议(TCP 和 UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号,就是在描述"数据是哪台主机的哪一个进程发的,要发给哪台主机上的哪一个进程"。


理解socket

综上,IP 地址用来标识互联网中唯一的一台主机,port 用来标识该主机上唯一的一个网络进程。

  • IP + Port 就能表示互联网中唯一的一个进程。

  • 所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srcIp,srcPort,dstIp,dstPort}这样的 4 元组就能标识互联网中唯二的两个进程。

  • 所以,网络通信的本质,也是进程间通信。

  • 我们把 ip + port 叫做套接字 socket。


传输层的典型代表

如果我们了解了系统,也了解了网络协议栈,我们就会清楚,传输层是属于内核的。那么我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用,来进行网络通信。

传输层有两个重要协议:TCB和UDP。

认识 TCP 协议

此处我们先对 TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;后面我们再详细讨论 TCP 的一些细节问题。

  • 传输层协议

  • 有连接:在数据传输之前,需要先建立连接。(打电话,你喂我喂的过程就是建立连接的过程)

  • 可靠传输:保证数据的完整性和顺序性,通过确认和重传机制确保数据可靠传输。

  • 面向字节流:数据以字节流的形式传输,不保留消息边界。(自来水,怎么接,接多少,都是自己自主决定的;文件打开也叫文件流,字节流和文件流没有区别,都是流式的;学习完自定义协议后我们就能理解了)

认识 UDP 协议

此处我们也是对 UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;后面再详细讨论。

  • 传输层协议

  • 无连接:不需要建立连接,直接发送数据。(对讲机)

  • 不可靠传输:不保证数据的完整性和顺序性,数据可能丢失或乱序到达。

  • 面向数据报:数据以数据报的形式传输,保留消息边界。(发快递,发几个就只能几个)

TCP是可靠的,丢包了可以再发,但是UDP不可靠,那为什么还要保留UDP呢?(属于同层协议,但是还保留一个不可靠的???)

我们要注意,这里的可靠和不可靠不可以将其视为贬义词,而是一种中性词,是一种特点。TCP保证可靠性,意味着他一定要做更多的工作,也就是意味着TCP协议会更加复杂一些,复杂带来的就是占有资源会比较多。UDP协议就会很简单,简单的话就是代表开发周期短,可维护性好。

因为我们暂时还没有深入了解 TCP 和 UDP 协议,此处只做了解即可。


网络字节序

我们以前学过,计算机在存储数据的时候,是有大端和小端的,大小端是按照字节为单位的。

大端(Big-Endian)

定义:大端字节序是指高位字节存放在内存的低地址端,低位字节存放在内存的高地址端。

假设有一个32位的整数 0x12345678,在大端字节序下,它在内存中的存储顺序如下:

cpp 复制代码
内存地址: 0x00  0x01  0x02  0x03
存储内容: 12    34    56    78
  • 0x12 存储在最低地址 0x00

  • 0x78 存储在最高地址 0x03

小端(Little-Endian)(小-小-小)

定义:小端字节序是指低位字节存放在内存的低地址端,高位字节存放在内存的高地址端。

假设有一个32位的整数 0x12345678,在小端字节序下,它在内存中的存储顺序如下:

cpp 复制代码
内存地址: 0x00  0x01  0x02  0x03
存储内容: 78    56    34    12
  • 0x78 (低权值位:就是16的几次方)存储在最低地址 0x00

  • 0x12 存储在最高地址 0x03

cpp 复制代码
#include <stdio.h>

// 函数:将整数从当前字节序转换为网络字节序(大端)
unsigned int htonl(unsigned int hostlong) {
    unsigned char *bytes = (unsigned char*)&hostlong;
    return ((unsigned int)(bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]);
}

int main() {
    unsigned int x = 1;
    if (*((char *)&x) == 0) {
        printf("大端(Big-Endian)\n");
    } else {
        printf("小端(Little-Endian)\n");
        printf("转换前的值:%u\n", x);
        // 调用 htonl 函数将整数转换为大端字节序
        x = htonl(x);
        printf("转换后(大端)的值:%u\n", x);
    }
    return 0;
}

如果今天主机A是小端存储,主机B是大端存储,两台主机间要进行网络通信,A将数据发送给B的话,主机B就解释反了。所以在网络当中,两台主机,如果主机间的存储序列不同的话,经过网络通信,会导致对方将接收到的数据解释错了!

所以在网络中就有规定:凡是将数据发送到网络当中的话,一定是要按照大端的形式发送!

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据。如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。

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

  • 这些函数名很好记,h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数。

  • 例如,htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送。

  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。

  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

注意:所有发送到网络上的数据,都必须是大端的!

socket编程接口

socket常见API

cpp 复制代码
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

这些API的详细细节我们会在后续写代码的时候进行说明。

我们发现大部分的接口参数中都有const struct sockaddr*的结构体指针(其他暂时不关心)

下面,我们来聊一聊这个sockaddr这个结构体。

sockaddr结构体

我们现在清楚,网络通信的本质其实是进程间通信,我们之前学习的system V这个标准属于本地之间进行进程间通信,我们后面还会见到的POSIX标准,这个是主要用于网络通信的,网络通信也是进程通信,也能进行本地通信的,也就是为什么我们之前说System V版本的进程间通信被淘汰了,因为POSIX这套标准,我们直接就可以进行了,还顺带了网络通信。

socket套接字会有许多不同的种类来满足各种各样的不同的应用场景--网络socket/本地socket(unix域间通信)/原始socket,原始socket我们不需要考虑,未来我们只需要学懂网络socket,本地socket我们自然而然也就清楚了。也是正因为有不同的场景的socket,我们socket未来的接口,也是会有不同的通信接口规范(网络的一套,本地的一套......)但是socket的设计者并不想这么干,只想要提供一套通信接口!!!(这一套既可以做网络通信,也可以做本地通信)

所以就需要对接口进行设计,为了能够支持设计出来的接口可以进行不同种类的通信,就设计了一个结构体---sockaddr结构体!

  • 通用性struct sockaddr 提供了一个通用的接口,适用于各种网络协议。

  • 专用性struct sockaddr_instruct sockaddr_un 分别针对IPv4网络通信和本地通信进行了优化,提供了必要的信息和灵活性。

在网络编程中,sockaddr 结构体及其相关的结构体如 sockaddr_in 和 sockaddr_un 经常需要进行强制类型转换。这是因为 sockaddr 是一个通用的地址结构体,设计用来支持多种不同的网络协议和地址类型。而 sockaddr_in 和 sockaddr_un 是针对特定协议(如IPv4、IPv6和UNIX域套接字)的具体实现。

当你需要将 sockaddr_in 或 sockaddr_un 结构体传递给需要 sockaddr 类型参数的函数时,通常需要进行强制类型转换。例如,在调用 bind() 或 connect() 函数时,你需要将 sockaddr_in 结构体的地址转换为 sockaddr 类型的指针。

这样做的原因是因为 sockaddr 结构体定义了一个通用的接口,它包含了一个地址族字段(sa_family),用于指示地址的具体类型。sockaddr_in 和 sockaddr_un 结构体都以这个地址族字段开始,但它们包含的地址信息不同。通过将它们转换为 sockaddr 类型,你可以确保函数能够正确识别和处理不同类型的地址。

至于为什么不将参数设置为 void*,原因在于 void* 类型虽然可以指向任何类型的数据,但它不提供足够的信息来处理不同协议的地址。使用 sockaddr 类型及其派生的结构体可以提供必要的语义信息,使函数能够根据地址类型字段来正确处理地址数据。此外,原始套接字API是在1983年发布的,早于1989年的ANSI C标准,其前身------K&R C------根本没有 void *,所以您无论如何都必须将其转换为某个东西。

其实本质就是继承和多态:(C语言实现的)



相关推荐
AlfredZhao1 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao16 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
小宇宙Zz2 天前
Maven依赖冲突
java·服务器·maven
网络研究院2 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展