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)

  1. socket():创建套接字。
  2. bind():绑定本机的 IP 地址和端口号(告诉系统我监听哪个门)。
  3. listen():将套接字设为监听模式,准备接收连接请求。
  4. accept():阻塞等待,直到有客户端连接过来,返回一个新的专门用于和该客户端通信的 Socket。
  5. recv() / send():通过新的 Socket 接收和发送数据。
  6. close():通信完毕,关闭套接字断开连接。

TCP 客户端流程 (Client)

  1. socket():创建套接字。
  2. connect():向服务端的 IP 和端口发起连接请求(触发三次握手)。
  3. send() / recv():连接成功后,发送和接收数据。
  4. 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);
    }
}