前提知识
IP 在全网范围内,唯一标识一台主机。 源 IP:标识数据从哪一台主机发出 目的 IP:标识数据要交付到哪一台主机
但是!数据送到主机,并不是最终目的
数据最终是给人使用的:聊天、下载、浏览网页。 而电脑里,QQ、迅雷、浏览器本质都是进程,进程是人在操作系统里的代表。
真正最终目标:把数据交付给目标主机上对应的进程。
问题来了:一台主机上同时运行几十上百个进程,数据抵达主机后,操作系统如何区分该交给哪一个进程?只依靠 IP 地址不够,还需要端口号(port)!
同一台电脑上同时跑浏览器、QQ、迅雷,都会收发网络数据包。 如果没有端口号区分:操作系统收到网络数据,根本不知道该交给哪个软件。 端口号就相当于进程的门牌号。
pid不就可以标识进程的唯一性???为什么还需要端口号??
| 概念 | 是什么 | 谁在用 | 作用范围 |
|---|---|---|---|
| PID(进程 ID) | 操作系统给进程分配的编号 | 操作系统内核用 | 仅在单台主机内部有效 |
| 端口号(Port) | 网络通信中标识进程的编号 | 网络协议栈(传输层)用 | 在网络通信中有效 |
pid是系统观念,有端口号可以对其进行解耦
pid每个进程都有,但不是所有进程都需要进行通行
原因 1:PID 是动态的,每次启动都变,不稳定
同一个程序(比如 QQ),今天启动 PID 是 1234,明天重启可能就是 5678。
如果网络通信靠 PID 寻址,对方主机怎么知道你这次 QQ 的 PID 是多少?总不能每次启动都通知全世界吧?
而端口号是程序自己指定 / 绑定的,可以固定。比如 HTTP 服务永远绑定 80 端口,不管进程 PID 怎么变,客户端只要访问 80 端口就能找到服务。
原因 2:PID 是操作系统内部概念,不同系统格式不一样
- Linux 的 PID 是整数
- Windows 的进程 ID 格式又不同
- 其他操作系统可能还有别的实现
网络是跨平台的,Linux 要和 Windows 通信,如果用 PID 寻址,两边操作系统对 PID 的理解都不一样,根本无法统一。
而端口号是 TCP/IP 协议标准规定的,全世界所有操作系统统一:16 位无符号整数,0~65535。不管什么系统,端口号含义完全一致。
原因 3:不是所有进程都需要网络通信
pid 每个进程都有,但不是所有进程都需要进行通信。
一台主机上同时运行几百个进程:桌面进程、输入法进程、系统后台服务…… 绝大多数进程根本不收发网络数据。
如果用 PID 作为网络标识,等于给所有进程都分配了一个 "网络地址",但大部分根本用不上,浪费且混乱。
端口号是按需分配的:只有需要网络通信的进程才会绑定端口,不需要通信的进程不占端口,干净高效。
原因 4:一个进程可以绑定多个端口,一个端口只能被一个进程占用
- 一个 Web 服务器进程,可以同时绑定 80 端口(HTTP)和 443 端口(HTTPS)
- 反过来,80 端口同一时刻只能被一个进程占用
如果用 PID 代替端口号,一个进程只有一个 PID,怎么同时提供多种网络服务?做不到。
端口号机制更灵活:进程可以按需绑定多个端口,提供多种服务。
ip vs port
比如我要投诉一个,先拨打,在说明工号
10086 ---> 4321
| 概念 | 电话类比 | 作用 | 所属分层 |
|---|---|---|---|
| IP 地址 | 总机号码 (10086) | 定位主机(哪台电脑) | 网络层 |
| Port 端口 | 员工工号 (4321) | 定位进程(哪个软件) | 传输层 |
- 10086(客服总机号码) ≈ IP 地址先拨打总机 10086:用来定位整个客服中心(一台主机),保证电话可以送到这个机构。
- 4321(员工工号) ≈ 端口号 port接通总机之后,再报工号 4321:用来定位客服中心里具体的工作人员(主机内进程)。
OS是如何讲收到的数据转发给特定端口的进程????
端口哈希表
操作系统内核会维护一张哈希表(图上横向数组就是哈希桶)
- key:端口号(示例:8080)
- value:对应进程的 PCB /socket 缓冲区
流程:拿到目的端口 → 哈希运算定位桶 → 查表找到绑定该端口的 socket → 将报文放入 socket 接收缓冲区,唤醒对应进程读取数据
找到进程如何把网络数据,交给这个进程
Linux 一切皆文件,socket 本质是文件描述符 fd,内核通过缓冲区完成内核态 → 用户态的数据交付
端口号范围划分
• 0 - 1023 : 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都 是固定的.
• 1024 - 65535 : 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范 围分配的.
- IP 地址用来标识互联网中唯一的一台主机, port 用来标识该主机上唯一的一个网络进程
- IP+Port 就能表示互联网中唯一的一个进程
- 所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srcIp,srcPort,dstIp,dstPort} 这样的4元组就能标识互联网中唯二的两个进程
- 所以,网络通信的本质,也是进程间通信
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于 文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地 址呢?
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下 库函数做网络字节序和主机字节序的转换。
网络字节序转换函数(htonl/htons/ntohl/ntohs)
| 函数 | 作用 | 长度 |
|---|---|---|
htonl() | host‑to‑network‑long:主机字节序 → 网络字节序,32 位 (IP 地址) | 4 字节 |
htons() | host‑to‑network‑short:主机字节序 → 网络字节序,16 位 (端口号) | 2 字节 |
ntohl() | network‑to‑host‑long:网络字节序 → 主机字节序,32 位 | 4 字节 |
ntohs() | network‑to‑host‑short:网络字节序 → 主机字节序,16 位 | 2 字节 |
- 这些函数名很好记, h 表示 host , n 表示 network , l 表示 32 位长整数, s 表示 16 位短整 数。
- 例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发 送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
socket编程接口
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);- 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结构 体指针做为参数;