Socket套接字
概念
Socket(套接字)是计算机网络数据结构,它是TCP/IP网络环境下应用程序与底层通信驱动程序之间运行的开发接口。
- 本质:它是对 TCP/IP 协议族的封装,提供了一套调用接口(API)。可以把它看作是网络通信的“端点”。
- 标识:一个 Socket 通常由
IP地址 + 端口号唯一确定,代表着网络上的一个特定进程。 - 作用:将应用程序与具体的 TCP/IP 隔离开发,使得程序员不需要深究网络协议的复杂细节,通过调用 Socket API 就能实现网络传输。
方式与类型
根据底层协议的不同,Socket 开发接口可以提供面向连接和无连接两种服务方式。在 Socket 中,主要分为以下三种类型:
流式套接字 (Stream Socket) → 对应 SOCK_STREAM
- 底层协议:基于 TCP 协议。
- 特点:
- 面向连接:通信前必须先建立连接(三次握手)。
- 可靠性高:保证数据按顺序、无重复、无丢失地到达。
- 无记录边界:数据像流水一样传输,接收端可能一次接收多个包的数据,也可能半个包(需要开发者自己处理粘包/分包问题)。
- 开销大:因为要维持连接状态和保证可靠性。
- 适用场景:文件传输、HTTP网页访问、SSH 远程登录等对数据完整性要求高的场景。
数据报式套接字 (Datagram Socket) → 对应 SOCK_DGRAM
- 底层协议:基于 UDP 协议。
- 特点:
- 无连接:发送数据前不需要建立连接,直接把数据包扔到网络上。
- 不可靠:不保证数据是否到达,不保证到达顺序,可能会丢包。
- 保留记录边界:发送端发几次,接收端就收几次,每次接收都是一个完整的数据报(不存在粘包问题)。
- 开销小,速度快:没有握手和重传机制的负担。
- 适用场景:视频/音频流媒体直播、在线游戏、DNS查询等对实时性要求高、能容忍偶尔丢包的场景。
原始套接字 (Raw Socket) → 对应 SOCK_RAW
- 工作层次:前两者工作在传输层(应用层直接调用),而原始套接字工作在网络层。
- 特点:
- 允许绕过传输层(TCP/UDP),直接访问底层协议(如 IP、ICMP、IGMP)。
- 可以用来构造自定义的网络报文。
- 通常需要管理员/Root 权限才能创建。
- 适用场景:网络抓包工具(如 Wireshark 的底层实现)、网络扫描工具(如 Nmap)、自定义网络协议开发、Ping 命令(基于 ICMP)等。(注:这在逆向和安全攻防中非常常用)
核心对比:流式(TCP) vs 数据报式(UDP)
| 特性 | 流式套接字 (SOCK_STREAM / TCP) | 数据报式套接字 (SOCK_DGRAM / UDP) |
|---|---|---|
| 连接性 | 面向连接 (需建立连接) | 无连接 (直接发送) |
| 可靠性 | 可靠 (保证送达) | 不可靠 (可能丢包) |
| 数据顺序 | 保证顺序 | 不保证顺序 |
| 数据边界 | 无边界 (字节流,需处理粘包) | 有边界 (一个个独立的数据报) |
| 传输速度 | 相对较慢 | 快 |
| 资源开销 | 大 | 小 |
Socket 编程基本流程
网络编程通常分为服务端(Server)和客户端(Client)。以下是基于最常用的 TCP (流式套接字) 的标准编程流程:
📡 TCP 服务端流程 (Server)
socket():创建套接字。bind():绑定本机的 IP 地址和端口号(告诉系统我监听哪个门)。listen():将套接字设为监听模式,准备接收连接请求。accept():阻塞等待,直到有客户端连接过来,返回一个新的专门用于和该客户端通信的 Socket。recv()/send():通过新的 Socket 接收和发送数据。close():通信完毕,关闭套接字断开连接。
💻 TCP 客户端流程 (Client)
socket():创建套接字。connect():向服务端的 IP 和端口发起连接请求(触发三次握手)。send()/recv():连接成功后,发送和接收数据。close():通信完毕,关闭套接字。
网络字节序 (大端与小端)
- 主机字节序:我们个人电脑(x86架构)通常是小端模式(Little-Endian),即低位字节存放在低地址。
- 网络字节序:TCP/IP 协议规定,网络上传输的数据必须是大端模式(Big-Endian),即高位字节存放在低地址。
- 编程注意:在绑定 IP 和端口时,必须使用系统提供的转换函数(如
htons()将主机端口转为网络端口,inet_addr()转换IP地址),否则网络通信会失败。
使用方式
初始化与清理 WinSock
在 Windows 操作系统中,网络通信依赖于 ws2_32.dll。在调用任何 Socket API 之前,必须调用 WSAStartup 函数来加载这个库并初始化其内部数据结构;程序结束时,需要调用 WSACleanup 来释放相关资源。
(注:WSA 代表 Windows Sockets API)
1. 核心代码模板
通常在 main() 函数的最开始进行初始化:
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib") // 告诉链接器链接 winsock 库
int main() {
// 1. 定义一个 WSADATA 结构体,用于接收系统返回的 Winsock 实现信息
WSADATA wsaData;
// 2. 指定需要的 Winsock 版本,通常使用 2.2 版本 (主版本号2,副版本号2)
WORD wVersionRequested = MAKEWORD(2, 2);
// 3. 调用 WSAStartup 初始化
int err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) {
std::cout << "WSAStartup 初始化失败,错误码: " << err << std::endl;
return -1; // 初始化失败,程序退出
}
/*
* ==========================================
* 这里开始你正常的 Socket 编程逻辑
* socket() -> bind() -> listen() -> ...
* ==========================================
*/
// 4. 程序结束前,清理 Winsock 释放资源
WSACleanup();
return 0;
}2. 函数详解
MAKEWORD(2, 2):这是一个宏,用来把两个数字拼成一个WORD(16位无符号整数)。它告诉操作系统:“我想使用 Winsock 的 2.2 版本”。WSAStartup(版本号, &wsaData):- 如果成功,返回
0。 - 如果失败,返回错误码(不用调用
WSAGetLastError(),直接看返回值)。 wsaData参数会被操作系统填入当前系统支持的 Winsock 版本信息。
- 如果成功,返回
WSACleanup():解除与ws2_32.dll的绑定,释放系统分配的资源。有几次WSAStartup就应该对应有几次WSACleanup。