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)
#include <iostream>
#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib") // 告诉链接器链接 winsock 库
#include <iphlapi.h>
#pragma comment(lib, "iphlapi.lib")通常在 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;
}MAKEWORD(2, 2):这是一个宏,用来把两个数字拼成一个WORD(16位无符号整数)。它告诉操作系统:“我想使用 Winsock 的 2.2 版本”。WSAStartup(版本号, &wsaData):- 如果成功,返回
0。 - 如果失败,返回错误码(不用调用
WSAGetLastError(),直接看返回值)。 wsaData参数会被操作系统填入当前系统支持的 Winsock 版本信息。
- 如果成功,返回
WSACleanup():解除与ws2_32.dll的绑定,释放系统分配的资源。有几次WSAStartup就应该对应有几次WSACleanup。
获取信息
主机名
//获取主机名
char szHostName[MAX_PATH]{0};
if (gethostname(szHostName,MAX_PATH)==0){
printf("主机名:%s\r\n",szHostName);
}
适配器IP
//获取本机IP
//AF_IENT(ipv4) AF_IENT6(ipv6)
hostent * phost = gethostbyname(szHostName)
for (size_t i=0;i != NULL&&phcst->h_addr_list[i] != NULL;i++){
//int_ntoa 网络字节转换成点分十进制的字符串
char * szHostip=int_ntoa(*(in_addr *)phost->h_addr_list[i]);
printf("主机IP:%s\r\n",szHostip);
}
通过网址获取IP
char * hostname = (char*)"www.vultop.com"
ADDINFOA hints;
ADDINFOA * result;
ADDINFOA * node;
in_addr addr;
memset(&hints,0,sizeof(ADDINFOA));
//运输层协议设置 SOCK_STEAM(TCP) SOCK_DGRAM(UDP)
hints.ai_socktype=SOCK_STREAM;
//IP协议版本
hint.ai_family=AF_INET;
//匹配所有IP地址
hints.ai_flags=AI_PASSIVE;
//匹配所有协议
hint.ai_protocol=NULL;
//获取网站信息
if (getaddinfo(hostname,&hints,&result)==0){
for(node=result;node!=NULL;node=node->ai_next)
{
addr.s_add=((sockeadr_in*)(node->ai_addr)).sin_addr.s_addr;
printf("网站地址:%s\r\n",inet_ntoa(addr));
}
}
freeaddrinfo(result);
获取网卡(适配器)信息
PIP_ADAPTER_INFO AdapterInfo=NULL;
ULONG ulLength = sizeof(PIP_ADAPTER_INFO);
ULONG ulRet = GetAdaptersInfo(NULL,&ulLength);
if (ulRet == ERROR_BUFFER_OVERFLOW){
//开辟结构空间
AdapterrInfo = (IP_ADAPTER_INFO*)malloc(ulLength);
//获取适配器信息
ulRet = GetAdaptersInfo(AdapterInfo,&ulLength);
}
if (ulRet == NO_ERROR){
while(AdapterInfo){
printf("网卡名称:%s\r\n",AdapterInfo->AdapterName);
printf("网卡描述:%s\r\n",AdapterInfo->Description);
printf("MAC地址:");
for(size_t i = 0;i<AdapterInfoLength;i++){
printf("%02X",AdapterInfo->Address[i]);
if(i<5){
printf("-");
}
}
printf("\r\n");
printf("适配器类型:%d\r\n",AdapterInfo->Type);
printf("IP地址:%s\r\n",AdapterInfo->IpAddressList.IpAddress.String);
printf("子网掩码:%s\r\n",AdapterInfo->IpAddressList.IpMask.String);
//指向下一个适配器
AdapterInfo=AdapterInfo->Next;
}
}
获取DNS信息
#include <windows.h>
#include <iphlpapi.h>
#include <stdio.h>
#pragma comment(lib, "IPHLPAPI.lib")
void GetDnsInformation() {
DWORD ulLength = 0;
// 第一次调用 GetNetworkParams 获取所需缓冲区大小
DWORD dwRet = GetNetworkParams(NULL, &ulLength);
if (dwRet == ERROR_BUFFER_OVERFLOW) {
// 分配所需的内存空间
PFIXED_INFO pFixedInfo = (PFIXED_INFO)HeapAlloc(GetProcessHeap(), 0, ulLength);
if (pFixedInfo == NULL) {
printf("Memory allocation failed.\n");
return;
}
// 第二次调用 GetNetworkParams 获取具体的网络参数
dwRet = GetNetworkParams(pFixedInfo, &ulLength);
if (dwRet == NO_ERROR) {
printf("== 本地计算机 DNS 信息 ==\n");
printf("Host Name: %s\n", pFixedInfo->HostName);
printf("Domain Name: %s\n", pFixedInfo->DomainName);
// 遍历 IP_ADDR_STRING 链表获取所有 DNS 服务器
PIP_ADDR_STRING pCurrentDnsServer = &pFixedInfo->DnsServerList;
int count = 1;
while (pCurrentDnsServer != NULL) {
// 判断地址是否非空
if (pCurrentDnsServer->IpAddress.String[0] != '\0') {
printf("DNS Server %d: %s\n", count++, pCurrentDnsServer->IpAddress.String);
}
// 指向下一个 DNS 服务器节点
pCurrentDnsServer = pCurrentDnsServer->Next;
}
} else {
printf("GetNetworkParams failed with error: %d\n", dwRet);
}
// 释放已分配的内存空间
HeapFree(GetProcessHeap(), 0, pFixedInfo);
} else {
printf("GetNetworkParams failed initially with error: %d\n", dwRet);
}
}