【Linux网络编程】socket基础
🔥个人主页:Quitecoder
🔥专栏:linux笔记仓
目录
- 01.理解源IP地址和目的IP地址
- 02.端口号
- 03.传输层典型代表
- 04.网络字节序
- 05.socket编程接口
- sockaddr结构
- 1. IPv4 地址结构 (`sockaddr_in`)
- 2. IPv6 地址结构 (`sockaddr_in6`)
- 3. 本地套接字地址 (`sockaddr_un`)
- 关键点:类型转换
01.理解源IP地址和目的IP地址
IP在网络中,互联网上每台联网设备(电脑、手机、服务器、路由器等)的唯一逻辑标识符(mac地址只用来表明自己在局域网的唯一性)
源IP地址:发起网络通信请求或发送数据包的发送方设备的IP地址
目的IP地址:网络通信请求或数据包最终想要到达的设备的IP地址
路由: 数据包离开你的电脑,经过你的家庭路由器、你的ISP(互联网服务提供商)的路由器、互联网骨干网上的众多路由器。
- 每个路由器查看数据包的目的IP地址 。
- 路由器根据其内部的路由表,决定将数据包发送到哪个“下一跳”路由器(离目的地更近的路由器)。
- 这个过程(称为“路由”)不断重复,直到数据包到达目标服务器所在的网络
数据传输到主机不是目的,而是手段,到达主机内部,再交给主机内的进程才是目的
系统中同时会存在非常多的进程,当数据到达目标主机后,怎么转发给目标进程?
02.端口号
端口号是传输层(TCP/UDP)的逻辑标识符,用于区分同一台设备上的不同网络应用或服务。
核心作用:
- 应用寻址:设备收到网络数据包后,根据端口号决定交给哪个程序处理。
- 多路复用:单台设备可同时运行多个网络服务(如Web服务+邮件服务),端口号避免数据混乱。
特点:
- 范围固定:端口号是16位整数(0-65535),分为三类:
类型 范围 用途 示例 知名端口 0-1023 系统级服务(需管理员权限) HTTP: 80, HTTPS: 443 注册端口 1024-49151 用户级应用/服务 MySQL: 3306, Redis: 6379 动态端口 49152-65535 客户端临时使用(操作系统自动分配) 浏览器访问网站时临时端口 - 全局约定:知名端口由IANA统一分配(如80→HTTP),确保跨平台一致性。
- 网络通信必需:IP地址定位设备,端口号定位设备上的具体服务。
A主机和B主机的通信,就转换为A应用层的进程和B应用层的进程进行通信
协作流程示例(浏览器访问网站)
假设你在电脑(IP: 192.168.1.100
)上访问 https://www.example.com
(IP: 93.184.216.34
):
-
浏览器发起请求:
- 操作系统为浏览器分配:源IP
192.168.1.100
+ 源端口(临时端口如52000
)。 - 目标地址:目的IP
93.184.216.34
+ 目的端口443
(HTTPS服务)。
- 操作系统为浏览器分配:源IP
-
数据包传输:
- 路由器根据目的IP将数据包送往目标服务器。
- 服务器收到后,根据目的端口号
443
将数据交给正在监听443端口的Web服务进程(如Nginx,其PID可能是1234
)。
-
服务器响应:
- Web服务生成响应数据包,源端口变为
443
,目的端口变为浏览器的临时端口52000
。 - 数据包返回你的电脑,操作系统根据目的端口
52000
将数据交给浏览器进程(其PID假设为5678
)。
- Web服务生成响应数据包,源端口变为
一个端口号只能被一个进程使用
同一时刻,一个端口只能被一个进程监听。否则会导致冲突(Linux报错 Address already in use
)。
一个进程可以绑定多个端口
例如Nginx可同时监听80(HTTP)和443(HTTPS)端口。
查看端口与进程的对应关系
- Linux/macOS:
netstat -tunlp | grep 端口号
- Windows:
netstat -ano | findstr :端口号
(结果中显示占用该端口的PID)
为什么需要端口号?直接用PID通信不行吗?
- 网络不可见PID:外部设备不知道目标机器的进程内部标识。
- 动态性:PID重启会变,而服务端口(如80)需长期固定。
- 分层设计:端口属于网络传输层,PID属于操作系统层,职责分离。
综上,IP地址用来标识互联网中唯一的一台主机,port用来标识该主机上唯一的一个网络进程,IP+Port就能表示互联网中唯一的一个进程
所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srcIP,srcPort,dstIP,dstPort}这样的4元组就能标识互联网中唯二的两个进程
所以,网络通信的本质,也是进程间通信,我们把IP+Port叫做套接字socket
03.传输层典型代表
TCP(Transmission Control Protocol 传输控制协议):
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP(User Datagram Protocol 用户数据报协议):
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
04.网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个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位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP- 地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
05.socket编程接口
// 创建 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);
sockaddr 是 套接字编程(Socket Programming)中的核心数据结构,用于表示网络地址信息(如IP地址和端口号)。它本质上是一个通用的地址结构,设计目的是统一处理不同协议族(如IPv4、IPv6、本地套接字等)的地址格式
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6, 然而, 各种网络协议的地址格式并不相同
未来在使用套接字时,既可以完成网络通信,又可以完成本地通信
网络通信中可能使用多种协议(IPv4/IPv6/Unix域套接字等),每种协议有自己的地址结构。为了简化接口设计,操作系统定义了一个“通用地址结构” sockaddr
,所有套接字函数(如 bind()
, connect()
, accept()
)都使用它作为参数类型。
sockaddr
的结构(C语言定义)
#include <sys/socket.h>// 通用地址结构(通常16字节)
struct sockaddr {sa_family_t sa_family; // 地址族(协议族标识符)char sa_data[14]; // 协议相关的地址数据(如IP+端口)
};
sa_family
:标识地址类型,常见值:AF_INET
:IPv4 协议AF_INET6
:IPv6 协议AF_UNIX
:本地套接字(进程间通信)
sa_data
:存放具体协议的地址信息(二进制格式)。
专用地址结构(实际使用)
由于 sa_data[14]
无法容纳复杂地址(如IPv6需要28字节),实际编程中不会直接使用 sockaddr
,而是用以下专用结构体,使用时强制转换为 sockaddr*
:
1. IPv4 地址结构 (sockaddr_in
)
struct sockaddr_in {sa_family_t sin_family; // 固定为 AF_INETin_port_t sin_port; // 16位端口号(如 80)struct in_addr sin_addr; // 32位IPv4地址char sin_zero[8]; // 填充字节(保持与sockaddr大小一致)
};struct in_addr {uint32_t s_addr; // IPv4地址(二进制形式,如 0x7F000001 = 127.0.0.1)
};
2. IPv6 地址结构 (sockaddr_in6
)
struct sockaddr_in6 {sa_family_t sin6_family; // 固定为 AF_INET6in_port_t sin6_port; // 16位端口号uint32_t sin6_flowinfo; // 流标签(通常为0)struct in6_addr sin6_addr; // 128位IPv6地址uint32_t sin6_scope_id; // 接口作用域ID
};struct in6_addr {unsigned char s6_addr[16]; // IPv6地址(二进制形式)
};
3. 本地套接字地址 (sockaddr_un
)
struct sockaddr_un {sa_family_t sun_family; // 固定为 AF_UNIXchar sun_path[108]; // 套接字文件路径(如 "/tmp/my_socket")
};
关键点:类型转换
所有套接字函数都接收 struct sockaddr*
类型参数。使用时需将专用结构体指针强制转换:
// IPv4 示例
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 端口号(主机字节序→网络字节序)
inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr); // IP字符串→二进制// 强制转换为 sockaddr* 并绑定套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));