Socket 支持跨不同主机通信,Socket 既支持本机(本地域套接字 AF_UNIX),也支持跨主机网络通信(AF_INET / AF_INET6)
OSI 七层模型(开放式系统互联参考模型)
自上而下一共 7 层 ,记口诀:应表会传网数物
应用层 → 表示层 → 会话层 → 传输层 → 网络层 → 数据链路层 → 物理层
每层作用 + 典型协议 / 设备 + 数据单元
第 7 层:应用层(Application)
- 作用:为应用程序提供网络服务,面向用户,定义应用交互规则
- 典型协议:HTTP、HTTPS、FTP、TFTP、DNS、SMTP、POP3
- 数据单元:数据(Data)
第 6 层:表示层(Presentation)
- 作用:数据格式转换、加密解密、压缩解压;解决不同主机数据编码差异
- 工作:编码转换、SSL/TLS 加密、JPEG/ASCII 解析
第 5 层:会话层(Session)
- 作用:建立、管理、断开通信会话(建立连接、保持会话、同步、断点恢复)
- 协议:RPC、NetBIOS
第 4 层:传输层(Transport)
- 作用:端到端进程通信,端口寻址;流量控制、差错校验
- 协议:TCP、UDP
- 数据单元:段(Segment,TCP) / 报文(Datagram,UDP)
- 核心:区分同一主机上不同进程(端口号)
第 3 层:网络层(Network)
- 作用:主机寻址、路由选择,跨子网转发
- 协议:IP(IPv4/IPv6)、ICMP、ARP (部分划分二层)、IGMP
- 设备:路由器、三层交换机
- 数据单元:分组 / 包(Packet)
第 2 层:数据链路层(Data Link)
- 作用:相邻节点传输;MAC 寻址、帧封装、差错检测(CRC)
- 协议:以太网 Ethernet、PPP
- 设备:网卡、交换机、网桥
- 数据单元:帧(Frame)
第 1 层:物理层(Physical)
- 作用:传输比特流,定义电气 / 机械接口、电平、线缆、信号
- 介质:光纤、双绞线、无线电波
- 设备:集线器 Hub、中继器
- 数据单元:比特(Bit)

在实际使用中TCP/IP 是现实互联网真正在用的标准,对 OSI 七层做了合并简化 自上而下:
- 应用层
- 传输层
- 网际层(网络层 / Internet 层)
- 网络接口层(主机 - 网络层)
Linux 编程实现网络通信,Socket 编程,基于 TCP/IP
Linux 下网络编程核心:Socket(套接字)API ,遵循 POSIX 标准,使用 AF_INET(IPv4)实现跨主机通信,分为 TCP(面向连接) 和 UDP(无连接) 两套流程。
TCP(Transmission Control Protocol,传输控制协议)
核心特点
- 面向连接:通信前必须先建立连接(三次握手),通信结束正常断开(四次挥手)
- 可靠传输 :保证数据完整、不乱序、不丢包
- 序列号 + 确认应答 ACK
- 超时重传:丢包自动补发
- 流量控制(滑动窗口):防止接收方过载
- 拥塞控制:防止网络拥堵
- 面向字节流:数据看成连续字节流,没有天然边界
- 开销更大:协议头至少 20 字节,握手、确认、重传带来额外延迟
- 适用场景: 文件传输、网页浏览、邮件、远程登录(SSH),要求数据不能出错、不能丢
TCP 完整通信流程(C/S 模型)
✅ TCP 服务端流程
- socket () 创建套接字
- bind () 绑定 IP + 端口
- listen () 设置监听队列
- accept () 阻塞等待客户端接入
- read/recv 接收、write/send 发送数据
- close 套接字
✅ TCP 客户端流程
- socket () 创建套接字
- connect () 连接服务端 IP + 端口
- send /recv 读写数据
- close 关闭

通信需要用到的结构体类型定义
struct sockaddr_in {
sa_family_t sin_family; // 地址族:AF_INET
in_port_t sin_port; // 端口号(网络大端序)
struct in_addr sin_addr; // IPv4地址
unsigned char sin_zero[8]; // 占位,填充0,对齐sockaddr
};
struct in_addr {
uint32_t s_addr; // IPv4整数,网络大端
};
struct sockaddr { // 通用地址结构体,用于函数参数强转
sa_family_t sa_family;
char sa_data[14];
};
端口号一般填写的都是十进制的整数数字,而传入sin_port类型不符,因此需要使用htons进行转换
将端口号 转换为网络字节序的端口号
htons
uint16_t htons(uint16_t hostshort);
功能:
将hostshort值转换为网络字节序 返回
就是把十进制数转化成点分十进制表示法的字符串的网络字节序地址
50000----->>>>>------192 .168 .1 .1
类似这样的效果,数据仅做例子,不是对应的转化
同样传入的ipv4的ip地址,也需要进行转化,结构体里的是字符串类型的地址,而我们一般填写的都是点分十进制法的一个ip地址。一台服务器只有一个 IP,但上面可以同时跑 Web、数据库、SSH 等多个网络程序; 内核依靠 端口号 区分收到的数据包该交给哪个应用程序。端口本质:16 位无符号整数,范围 0 ~ 65535
1. 知名端口(公认端口):0 ~ 1023
特权端口 特点:系统预留、标准服务占用;Linux 下绑定 0~1023 需要 root 权限
2. 注册端口:1024 ~ 49151
分配给第三方应用、自定义服务,普通用户即可绑定 我们自己写 socket 服务器,推荐选这个区间,例如:8080、8888、9000、12345 很多中间件默认端口都在这里:Redis 6379
3. 动态 / 私有端口(临时端口):49152 ~ 65535
客户端发起连接时,内核自动随机分配 的临时端口 客户端不需要手动 bind 端口,内核自动从这里拿端口做源端口; 一般不作为服务端监听端口。
将ip地址 转换为网络字节序的ip地址
inet_addr
in_addr_t inet_addr(const char *cp);
功能:
将字符串形式ip地址cp(点分十进制) 转换为 网络字节序形式的IP地址返回
创建套接字
在Linux下一切皆文件,因此socket本质也是创建了一个套接字文件并打开,返回的是文件描述符,此后的通信过程同样可以使用系统调用的read/write进行操作
int socket(int domain, int type, int protocol);
功能:
创建通信的一端
参数:
@domain:
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
@type:
SOCK_STREAM //流式套接字
Provides sequenced, reliable, two-way, connection-based byte streams.
An out-of-band data transmission mechanism may be supported.
SOCK_DGRAM //数据报套接字
Supports datagrams
(connectionless,
unreliable messages of a fixed maximum length).
@protocol
协议 --- tcp
--- upd
给 0 表示默认协议
返回值:
成功 返回socket的文件描述符
失败 -1 && errno
套接字文件绑定地址信息
将 socket fd 和【本机 IP + 端口号】绑定,让外界可以通过这个 IP 端口找到当前进程,客户端一般不用 bind,内核自动分配随机端口,只有服务器端需要使用bind进行绑定。
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:
给sockfd 绑定一个名字 (地址信息)
参数:
@sockfd --- 要当定的socket socket 返回的套接字 fd
@addr --- 绑定服务器自己的地址 (struct sockaddr *)&sockaddr_in 强制转换
@addrlen --- 服务器地址大小 传入地址结构体大小 sizeof(struct sockaddr_in)
返回值:
成功 0
失败 -1 && errno
TCP 服务端开启监听
仅 TCP 使用!把 socket 设置为被动监听状态,开始等待客户端发起连接,UDP 不需要 listen
int listen(int sockfd, int backlog);
功能:
将sockfd设置为监听套接字(被动套接字)
参数:
@sockfd --- 要设置的socket !!已经 bind 完成的套接字!!
@backlog --- 表示等待队列的大小
返回值:
成功 0
失败 -1 && errno
TCP 阻塞接收客户端连接
阻塞函数!从内核已完成连接队列取出一个客户端连接,返回连接后的cfd进行后续的通信操作
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:
从listen的等待队列中提取第一个连接请求,完成连接,
同时创建一个socket 表示已经连接好的socket
最终,返回这个 已连接好的socket的fd
参数:
@sockfd --- 监听套接字 的 fd
@addr --- 用来保存对端地址信息 也就是保存接入的客户端的 IP 和端口,不需要可传 NULL
@addrlen --- 值结果参数
必须初始化 --- sizeof(客户端地址类型)
返回值:
成功 返回已经连接好的socket的fd
失败 -1 && errno被设置
使用过程中,可以定义一个客户端的地址信息的结构体,以及结构体长度的变量,传给accept便可以接收到客户端的地址信息,用于日志、鉴权、额外控制、多协议配合等附加业务场景,如果不需要可以直接全传NULL。
struct sockaddr_in cliaddr;
socklen_t addrlen = sizeof(cliaddr);
// 阻塞等待客户端连接,不保存客户端地址信息
int connfd = accept(fd,(struct sockaddr *)&cliaddr,&addrlen);
TCP 客户端发起连接
客户端专用,向服务端 IP + 端口发起三次握手,建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
发起连接请求
参数:
@sockfd --- 用到的socket的fd
@addr --- 要通信地址 --- 对于客户端来说,就是服务器的地址(ip+端口号)
@addrlen --- 表示的式 addr 参数地址的大小
返回值:
成功 0
失败 -1 && errno
实际使用中,第二个参数传入的就是已经定义好的,本客户端的地址信息的结构体变量的地址,第三个参数就是地址参数这个结构体变量的字节长度大小,一般使用sizeof进行计算。
// 定义客户端地址信息结构体
struct sockaddr_in seraddr;
// 将结构体内存全部置0
bzero(&seraddr,sizeof(seraddr));
seraddr.sin_family = AF_INET; // 地址协议族:IPv4
seraddr.sin_port = htons(50000); // 要连接的服务器端口,转为网络字节序
seraddr.sin_addr.s_addr = inet_addr("192.168.31.94"); // 要连接的服务器IP地址
// 向服务端发起TCP连接
if(connect(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0)
{
perror("connect fail");
return -1;
}