【Linux 学习指南】网络编程基础:从 IP、端口到 Socket 与 TCP/UDP 协议详解
文章目录
- 📝理解源IP地址和目的IP地址
- 🌠 认识端口号
- 🌉端口号范围划分
- 🌉理解"端口号"和"进程ID"
- 🌉理解源端口号和目的端口号
- 🌉理解socket
- 🌠传输层的典型代表
- 🌉认识TCP协议
- 🌉 认识UDP协议
- 🌠网络字节序
- 🌠Socket 编程接口
- 🌉Socket 常见API
- 🌉sockaddr 结构
- 🌉`in_addr` 结构
- 🚩总结
📝理解源IP地址和目的IP地址
- IP在网络中,用来标识主机的唯一性
- 注意:后面我们会讲IP的分类,后面会详细阐述IP的特点
但是这里要思考一个问题:数据传输到主机是目的吗?不是的。因为数据是给人用的。比如:聊天是人在聊天,下载是人在下载,浏览网页是人在浏览?
但是人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?通过启动的qq,迅雷,浏览器。
而启动的qq,迅雷,浏览器都是进程。换句话说,进程是人在系统中的代表,只要把数据给进程,人就相当于就拿到了数据。
所以:数据传输到主机不是目的,而是手段。到达主机内部,在交给主机内的进程,才是目的。
但是系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程?这就要在网络的背景下,在系统中,标识主机的唯一性。
🌠 认识端口号
端口号(port)是传输层协议的内容
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
IP
地址+端口号能够标识网络上的某一台主机的某一个进程;- 一个端口号只能被一个进程占用.
🌉端口号范围划分
- 0-1023: 知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的.
- 1024-65535: 操作系统动态分配的端口号.客户端程序的端口号,就是由操作系统从这个范围分配的
🌉理解"端口号"和"进程ID"
我们之前在学习系统编程的时候,学习了pid表示唯一一个进程;此处我们的端口号也是唯一表示一个进程.那么这两者之间是怎样的关系?
在Linux系统中,进程ID(PID)和端口号都可用于标识进程,但二者有着不同的概念和用途,它们之间存在一定的关联关系。具体如下:
- 概念及用途不同:
- 进程ID(PID):是系统为每个运行中的进程分配的唯一标识符,属于系统级的概念。无论进程是否参与网络通信,系统都会为其分配PID,用于在内核中管理和区分各个进程,是操作系统进行进程调度、资源分配等操作的重要依据。
- 端口号:是传输层协议的内容,是一个16位的整数,用于在网络通信中标识主机中的进程。它是网络概念,主要用于区分同一台主机上不同的网络服务或应用程序,当数据通过网络传输到主机时,操作系统根据端口号将数据交给对应的进程处理。
- 对应关系:
- 一个进程可以绑定多个端口号。例如,一个Web服务器进程可能同时监听80端口(用于HTTP协议)和443端口(用于HTTPS协议),就像一个人可以有多个身份标识,在不同场景下使用不同标识一样。
- 一个端口号只能被一个进程占用(特殊情况除外,如支持多进程监听的情况,但也是在特定机制下),以保证标识进程的唯一性,否则会导致网络数据传输混乱。
- 查询方法:
- 已知PID查询端口号,可以使用
netstat -tulnp | grep <PID>
或lsof -i -P -n | grep <PID>
命令。 - 已知端口号查询PID,可以使用
netstat -nap | grep <端口号>
或lsof -i :<端口号>
命令。
- 已知PID查询端口号,可以使用
以10086为例,如果有一个进程号为10086的进程,你想知道它占用了哪些端口,可以使用sudo netstat -tulnp | grep 10086
命令来查看。反之,如果已知某个网络服务使用的端口号是10086,想知道是哪个进程在占用该端口,则可以使用sudo lsof -i :10086
命令,命令执行后会显示占用该端口的进程PID及相关信息。
因此:一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定;
进程ID
属于系统概念,技术上也具有唯一性,确实可以用来标识唯一的一个进程,但是这样做,会让系统进程管理和网络强耦合,实际设计的时候,并没有选择这样做。
🌉理解源端口号和目的端口号
传输层协议(TCP
和UDP
)的数据段中有两个端口号,分别叫做源端口号和目的端口号.就是在描述"数据是谁发的,要发给谁"
🌉理解socket
综上,IP地址用来标识互联网中唯一的一台主机,port用来标识该主机上唯一的
一个网络进程
• IP+Port
就能表示互联网中唯一的一个进程
• 所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srcIp
,srcPort
,dstIp
,dstPort
}这样的 4元组就能标识互联网中唯二的两个进程
• 所以,网络通信的本质,也是进程间通信
• 我们把ip+port
叫做套接字socket
C++
socket
n.
(电源)插座;(电器上的)插口,插孔,管座;槽;窝;托座;臼;孔穴
vt.
把…装入插座;给…配插座
🌠传输层的典型代表
如果我们了解了系统,也了解了网络协议栈,我们就会清楚,传输层是属于内核的,那么我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用,来进行的网络通信。
🌉认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;后面我们再详细讨论TCP的一些细节问题.
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
🌉 认识UDP协议
此处我们也是对UDP(UserDatagramProtocol 用户数据报协议)有一个直观的认识;后面再详细讨论.
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
🌠网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP
协议规定,网络数据流应采用大端字节序,即低地址高字节.- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
🌠Socket 编程接口
🌉Socket 常见API
C// 创建 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()
- 创建网络端点
int socket(int domain, int type, int protocol);
- 功能:创建一个网络通信的端点(socket描述符),类似于文件描述符,用于后续的网络操作。
- 参数:
domain
:协议族(地址族),指定网络通信的地址类型,常见值:AF_INET
:IPv4协议AF_INET6
:IPv6协议AF_UNIX
:本地进程间通信(Unix域socket)
type
:套接字类型,决定通信方式:SOCK_STREAM
:流式套接字,对应TCP协议(可靠、面向连接)SOCK_DGRAM
:数据报套接字,对应UDP协议(不可靠、无连接)SOCK_RAW
:原始套接字,用于直接访问底层协议(如ICMP)
protocol
:具体协议,通常设为0表示根据前两个参数自动选择默认协议
- 返回值:成功返回非负的socket描述符,失败返回-1并设置
errno
bind()
- 绑定地址和端口
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
- 功能:将socket与特定的IP地址和端口号绑定(主要用于服务器端)。
- 参数:
socket
:socket()
返回的描述符address
:指向struct sockaddr
(或其衍生结构,如struct sockaddr_in
)的指针,包含要绑定的IP和端口address_len
:address
结构体的长度
- 典型用法(服务器端):
struct sockaddr_in serv_addr; serv_addr.sin_family = AF_INET; // IPv4 serv_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有本地网卡 serv_addr.sin_port = htons(8080); // 端口号(需转换为网络字节序) bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
- 返回值:成功返回0,失败返回-1
listen()
- 监听连接请求
int listen(int socket, int backlog);
- 功能:将流式套接字(TCP)转为被动监听状态,准备接收客户端连接(仅服务器端使用)。
- 参数:
socket
:已绑定的socket描述符backlog
:未处理连接的最大队列长度(超过此数的连接会被拒绝)
- 注意:仅用于TCP协议,UDP无需监听
- 返回值:成功返回0,失败返回-1
accept()
- 接收客户端连接
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
- 功能:从监听队列中取出一个客户端连接请求,创建新的socket用于与该客户端通信(仅TCP服务器使用)。
- 参数:
socket
:处于监听状态的socket描述符(监听套接字)address
:输出参数,用于存储客户端的地址信息address_len
:输入输出参数,传入缓冲区长度,返回实际地址长度
- 特点:
- 阻塞调用,若无连接会一直等待
- 返回新的socket描述符(连接套接字),与客户端的通信通过该描述符进行
- 原监听 socket继续监听新的连接
- 返回值:成功返回新的socket描述符,失败返回-1
connect()
- 建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:客户端主动向服务器发起连接(仅TCP客户端使用)。
- 参数:
sockfd
:客户端的socket描述符addr
:指向服务器地址结构的指针(包含服务器IP和端口)addrlen
:地址结构的长度
- 特点:
- 触发TCP三次握手过程
- 阻塞调用,直到连接建立或失败
- 返回值:成功返回0,失败返回-1
典型工作流程
- TCP服务器:
socket()
→bind()
→listen()
→accept()
→ 读写数据 - TCP客户端:
socket()
→connect()
→ 读写数据 - UDP(无连接):通常只需
socket()
和bind()
(服务器),无需listen()
/accept()
/connect()
🌉sockaddr 结构
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIXDomainSocket.然而,各种网络协议的地址格式并不相同.
IPv4
和IPv6
的地址格式定义在netinet/in.h
中,IPv4
地址用sockaddr_in
结构体表示,包括16
位地址类型,16
位端口号和32
位IP
地址.IPv4、IPv6
地址类型分别定义为常数AF_INET
、AF_INET6
.这样,只要取得某种sockaddr
结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.socketAPI
可以都用struct sockaddr
*类型表示, 在使用的时候需要强制转化成sockaddr_in
; 这样的好处是程序的通用性,可以接收IPv4, IPv6, 以及UNIXDomainSocket
各种类型的sockaddr
结构体指针做为参数;
sockaddr
结构
/* Structure describing a generic socket address. */
struct __attribute_struct_may_alias__ 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 __attribute_struct_may_alias__ 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位的整数