【Linux网络】Socket编程预备
在上篇文章中,我们了解到网络传输的基本流程,从局域网传输到跨网传输,也了解到在传输时需要封装和解包,接下来我们就要为下篇文章Socket编程作预备,我们需要通过理论加实战来输入了解,所以这篇文章会对Socket编程前铺垫一些知识概念
文章目录
- 1. 理解源IP地址和目的IP地址
- 2. 认识端口号
- 2.1 端口号范围划分
- 2.2 理解 "端口号" 和 "进程ID"
- 2.3 理解源端口号和目的端口号
- 2.4 理解socket
- 3. 传输层的典型代表
- 3.1 认识TCP协议
- 3.2 认识UDP协议
- 4. 网络字节序
- 5. socket编程接口
- 5.1 socket 常见API
- 5.2 sockaddr结构
1. 理解源IP地址和目的IP地址
IP
在网络中,用来标识主机的唯一性
注意:后面我们会讲IP
的分类,会详细阐述IP
的特点
但是这里要思考一个更深层次的问题:数据传输到主机真的是最终目的吗?
其实并不是。因为数据最终都是为人服务的。比如:聊天是人在聊天,下载是人在下载,浏览网页是人在浏览。数据只是载体,人才是主体
但是人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?人是如何与这些数据交互的呢?
通过启动的 qq,迅雷,浏览器。而启动的 qq,迅雷,浏览器都是进程。换句话说,进程是人在系统中的代表,只要把数据给进程,人就相当于是拿到了数据。
所以:数据传输到主机不是目的,而是手段。到达主机内部,在交给主机内的进程,才是目的。
但是系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程?在复杂的网络环境中,如何确保数据不仅能到达目标主机,还能准确送达目标进程?
显然,仅靠IP地址(标识了主机)是远远不够的。这需要在两个层面建立标识体系:
- 网络层:通过IP地址等标识主机的唯一性
- 系统层:通过端口号等标识进程的唯一性
2. 认识端口号
端口号( port )
是传输层协议的内容。
- 端口号是一个 2 字节 16 位的整数;
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
IP地址
+端口号
能够标识网络上的某一台主机的某一个进程;- 一个端口号只能被一个进程占用
2.1 端口号范围划分
- 0 - 1023 : 知名端口号,这些端口号由IANA(互联网数字分配机构)统一分配和管理,HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的.
- 常见协议及其默认端口:
- HTTP: 80
- HTTPS: 443
- FTP: 21(控制端口)/20(数据端口)
- SSH: 22
- Telnet: 23
- SMTP: 25
- 常见协议及其默认端口:
- 1024 - 65535 : 操作系统动态分配的端口号,客户端程序的端口号, 就是由操作系统从这个范围分配的
就像是我们现实中的电话号码,例如那些110、119、120、10086等是固定特殊的号码,而我们普通人则是动态分配的随机号码
2.2 理解 “端口号” 和 “进程ID”
我们之前在学习系统编程的时候, 学习了 pid 表示唯一的一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
一个比喻:10086客服热线
-
端口号 (Port):就像是10086这个客服热线号码。
-
这个号码是公开的、稳定的、对外提供服务的入口。任何用户想办理业务,都知道要打这个号码。
-
它不关心电话那头是张三、李四还是王五接听了电话。
-
-
进程PID:就像是客服中心里某一个具体的客服人员,比如工号12345的张三。
-
这个工号是内部管理的标识。用于公司内部进行考勤、发工资、绩效考核。
-
客服人员可能会离职、换班、休息(进程会退出、重启),但10086这个热线号码是永远不变的。
-
-
操作系统内核:就像是客服中心的呼叫分配系统(ACD)。
-
当客户拨打10086(请求某个端口)时,分配系统会找到一个空闲的客服人员(一个正在监听该端口的进程),比如把电话转接给工号12345的张三(PID=12345的进程)。
-
如果张三今天休息了(进程崩溃或退出),分配系统会把来电转给工号67890的李四(另一个进程,PID=67890)。对于客户来说,他始终拨打的是10086,完全不知道背后接电话的人是谁,也不会受到影响。
-
核心结论:目的和上下文不同
-
端口号 (Port Number):是网络协议栈用来在网络通信中标识一个进程的ID。它的作用域是网络连接。 → 就像 10086这个客服热线号码。
-
PID (Process ID):是操作系统内核用来在系统内部唯一标识和管理一个进程的ID。它的作用域是本地主机。 → 就像 客服中心里某一个具体的客服人员的工号(比如工号12345的张三)。
另外:
一个进程可以绑定多个端口号
就像不能有两个不同的热线号码都叫10086,否则客户就不知道该打哪个了。系统(内核)会保证一个端口在同一时间只能被一个进程监听。
但是一个端口号不能被多个进程绑定
就像一个超级客服可以同时负责接听10086、10085等多个热线。一个复杂的网络服务(如Nginx)可以同时监听80端口(HTTP)和443端口(HTTPS)。
进程 PID 属于系统概念,技术上也具有唯一性,确实可以用来标识唯一的一个进程,但是这样做,会让系统进程管理和网络强耦合,实际设计的时候,并没有选择这样做。
2.3 理解源端口号和目的端口号
传输层协议( TCP 和 UDP )的数据段中有两个端口号,分别叫做源端口号和目的端口号.,就是在描述 “数据是谁发的, 要发给谁”。
目的端口号:回答了 “数据要交给对方主机的哪个进程?” 这是通信的目标。
源端口号:回答了 “数据是来自对方主机的哪个进程?” 并更重要的是,它提供了 “请将回复发送到我的哪个端口?” 的返回地址。
2.4 理解socket
综上, IP 地址用来标识互联网中唯一的一台主机,port 用来标识该主机上唯一的一个网络进程
-
故,IP+Port 就能表示互联网中唯一的一个进程
-
所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srcIp,srcPort,dstIp,dstPort}这样的4元组就能标识互联网中唯二的两个进程
- {源IP,源端口,目标IP,目标端口} 例如:{192.168.1.2 : 54321,203.179.25.16 : 80}表示从内网主机192.168.1.2的54321端口访问公网服务器203.179.25.16的Web服务
-
所以,网络通信的本质,也就是全网唯二的两个进程,在进程间通信。
-
我们把 ip+port 叫做套接字 socket
注意:我们之前学习了
System V IPC
进程间通信,其实POSIX
也有一套更标准的通信方式,它不仅可以进行进程通信,也能进行网络通信。而网络通信的方式就可以通过网络套接字Socket来实现,当然也有本地套接字Socket可以实现进程间通信,这里就不过多赘述,感兴趣可以自行了解
3. 传输层的典型代表
如果我们了解了系统,也了解了网络协议栈,我们就会清楚,传输层是属于内核的,那么我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用,来进行的网络通信。
3.1 认识TCP协议
此处我们先对 TCP ( Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
3.2 认识UDP协议
此处我们也是对 UDP ( User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
因为我们暂时还没有深入了解 tcp 、 udp 协议,此处我们下面只需对概念做下了解即可
什么是有连接和无连接?
指:通信双方在交换数据之前是否需要先建立一个专门的通道。
可靠和不可靠又是指什么?
指:协议是否保证数据能够准确无误、完整地送达对方。从字面意思其实就不难看出来,但是这里要提一嘴,不可靠不一定就不好,它其实是一个中性词,存在即合理,既然不可靠还会留下来说明肯定有它的独到之处,就比如有时候我们看直播之类的,突然卡了(比如数据掉包),但是它卡了之后并不会继续刚刚的画面,有可能就是在那之后的画面了,但是这也并不会有什么影响,如果要保证可靠的话,实现肯定就要复杂很多,有时候我们不追求这种,就可以使用UDP
那什么又是面向字节流,什么又是面向数据报呢?
-
面向字节流 (TCP):像用吸管喝水。你喝到的是一个连续的水流,你不知道一开始倒进杯子里的是哪几口水。
-
面向数据报 (UDP):像用手递手拿糖果。每次传递都是一颗完整的、独立的糖果。
4. 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分。磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议
规定,网络数据流应采用大端字节序,即低地址高字节.- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可
为什么要有“网络字节序”?
不同主机可能有不同的字节序(大端机或小端机)。如果它们直接按照自己内存中的格式发送数据,就会导致严重的通信问题。
网络字节序的提出,就是为了给网络通信提供一个统一的、标准化的数据表示格式,从而消除不同平台字节序差异带来的歧义,实现异构系统之间的可靠通信
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>// 主机字节序 -> 网络字节序 (Host to Network)
uint32_t htonl(uint32_t hostlong); // 转换32位长整数(常用于IP地址)
uint16_t htons(uint16_t hostshort); // 转换16位短整数(常用于端口号)// 网络字节序 -> 主机字节序 (Network to Host)
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
- 这些函数名很好记,h 表示
host
, n 表示network
, l 表示 32 位长整数, s 表示 16 位短整数。 - 例如
htonl
表示将 32 位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。 - 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
📌 网络规定:所有发送到网络上的数据,都必须是大端的!
5. socket编程接口
5.1 socket 常见API
// 创建 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);
这里我们先了解一下这些接口,后面Socket编程时会详细介绍这些接口。
仔细观察一下,我们可以发现大部分接口都有sockaddr结构体
5.2 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket.。然而,,各种网络协议的地址格式并不相同.
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址.
- IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6.。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性,可以接收IPv4, IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
而上图中:
-
sockaddr_in
用于网络通信,使用IP地址和端口号作为寻址方式。 -
sockaddr_un
用于本地进程间通信,使用文件系统路径作为寻址方式。
socket会有很多的种类,来满足不同的应用场景,socket未来的接口,会有不同的通信接口规范,socket的设计者,只想提供一种通信接口!
可以自行区分,你到底是网络通信,还是本地通信。
if(address->sin_family ==AF_INET)
else if(address->sin_family == AF_UNIX)
所以,说白了这不就是继承和多态嘛,
struct sockaddr
是基类,而struct sockaddr_in
,struct sockaddr_un
以及struct sockaddr_in6
(IPv6)等都是派生类
sockaddr 结构
sockaddr_in 结构
虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in,这个结构里主要有三部分信息:地址类型、端口号、IP地址。
in_addr结构
、
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
我们现在已经铺垫了一些知识,下一篇文章我们就来写代码,通过理论+代码能更好帮助我们理解网络通信,当然关于具体的协议,例如传输层的UDP和TCP协议,网络层的IP协议等,我们后面也会详细探讨