You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Winsock的SMTP客户端:发送数据后卡在recv()函数处

问题:SMTP客户端发送HELO命令后卡在recv调用

我正在开发简易SMTP客户端学习SMTP协议,基于微软《创建基础Winsock应用程序》指南编写,目前实现了交互式终端功能,后续计划自动化发邮件、读收件箱。当前代码能正常接收服务器初始问候,但发送HELO 1.2.3.4这类命令后,会卡在*iResult = recv(*ConnectSocket, out, 200, 0);语句,直到服务器超时返回错误。

运行输出如下:

Bytes received: 111
220 CP6P284CA0098.outlook.office365.com Microsoft ESMTP MAIL Service ready at Tue, 28 Feb 2023
18:17:15 +0000

$> HELO 1.2.3.4
(some time later)
Bytes received: 127
451 4.7.0 Timeout waiting for client input [CP6P284CA0098.BRAP284.PROD.OUTLOOK.COM 2023-02-28T18:22:16.228Z 08DB18B4E5C7B417]

$>

以下是当前代码:

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#define _WIN32_WINNT 0x0501

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <stdio.h>
#include <stdlib.h>

#define DEFAULT_PORT "25"

char data[500];

int initWinsock(int *, WSADATA *);
int makeSocket(int *, struct addrinfo *, struct addrinfo **, struct addrinfo **, SOCKET *);
int connSocket(int *, SOCKET *, struct addrinfo *, struct addrinfo *);
int clientReceive(int *, SOCKET *, char *);
int clientSend(int *, char *, SOCKET *);
int closeSocketListener(int *, SOCKET *);
int closeSocket(SOCKET *);

int main() {
    SOCKET ConnectSocket = INVALID_SOCKET;
    WSADATA wsaData;
    int iResult;
    struct addrinfo *result = NULL,
                    *ptr = NULL,
                    hints;
    ZeroMemory( &hints, sizeof(hints) );
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    
    initWinsock(&iResult, &wsaData);

    makeSocket(&iResult, &hints, &result, &ptr, &ConnectSocket);

    connSocket(&iResult, &ConnectSocket, ptr, result);

    while (1){
        char prompt[200] = {0};
        clientReceive(&iResult, &ConnectSocket, data);
        printf("%s\n$> ", data);
        fgets(prompt, sizeof prompt, stdin);
        clientSend(&iResult, prompt, &ConnectSocket);
    }
    

    closeSocketListener(&iResult, &ConnectSocket);

    closeSocket(&ConnectSocket);

    system("pause");
    return 0;
}

int initWinsock(int *iResult, WSADATA *wsaData){
    // Initialize Winsock
    *iResult = WSAStartup(MAKEWORD(2,2), wsaData);
    if (*iResult != 0) {
        printf("WSAStartup failed: %d\n", *iResult);
        return 1;
    }
    return 0;
}

int makeSocket(int *iResult, struct addrinfo *hints, struct addrinfo **result, struct addrinfo **ptr, SOCKET *ConnectSocket){
    // Resolve the server address and port
    *iResult = getaddrinfo("52.97.26.134", DEFAULT_PORT, hints, result);
    if (*iResult != 0) {
        printf("getaddrinfo failed: %d\n", *iResult);
        WSACleanup();
        return 1;
    }

    // Attempt to connect to the first address returned by
    // the call to getaddrinfo
    *ptr = *result;

    // Create a SOCKET for connecting to server
    *ConnectSocket = socket((*ptr)->ai_family, (*ptr)->ai_socktype, (*ptr)->ai_protocol);

    if (*ConnectSocket == INVALID_SOCKET) {
        printf("Error at socket(): %d\n", WSAGetLastError());
        freeaddrinfo(*result);
        WSACleanup();
        return 1;
    }
    return 0;
}

int connSocket(int *iResult, SOCKET *ConnectSocket, struct addrinfo *ptr, struct addrinfo *result){
    // Connect to server.
    *iResult = connect( *ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
    if (*iResult == SOCKET_ERROR) {
        closesocket(*ConnectSocket);
        *ConnectSocket = INVALID_SOCKET;
    }

    // Should really try the next address returned by getaddrinfo
    // if the connect call failed
    // But for this simple example we just free the resources
    // returned by getaddrinfo and print an error message

    freeaddrinfo(result);

    if (*ConnectSocket == INVALID_SOCKET) {
        printf("Unable to connect to server!\n");
        WSACleanup();
        return 1;
    }
    return 0;
}

int clientReceive(int *iResult, SOCKET *ConnectSocket, char *out){
    *iResult = recv(*ConnectSocket, out, 200, 0);
    if (iResult > 0) {
        printf("Bytes received: %d\n", *iResult);
        return 0;
    } else if (iResult == 0) {
        printf("Connection closed\n");
        return 1;
    } else {
        printf("recv failed: %d\n", WSAGetLastError());
        return 2;
    }

    return -1;
}

int clientSend(int *iResult, char *sendbuf, SOCKET *ConnectSocket){
    *iResult = send(*ConnectSocket, sendbuf, (int) strlen(sendbuf), 0);
    if (*iResult == SOCKET_ERROR) {
        printf("send failed: %d\n", WSAGetLastError());
        closesocket(*ConnectSocket);
        WSACleanup();
        return 1;
    }
    return 0;
}

int closeSocketListener(int *iResult, SOCKET *ConnectSocket){
    // shutdown the send half of the connection since no more data will be sent
    *iResult = shutdown(*ConnectSocket, SD_SEND);
    if (*iResult == SOCKET_ERROR) {
        printf("shutdown failed: %d\n", WSAGetLastError());
        closesocket(*ConnectSocket);
        WSACleanup();
        return 1;
    }
    return 0;
}

int closeSocket(SOCKET *ConnectSocket){
    // cleanup
    closesocket(*ConnectSocket);
    WSACleanup();
    return 0;
}

问题原因排查

1. SMTP协议命令格式错误

SMTP要求所有命令必须以\r\n(回车+换行)作为结束标记,但你用fgets读取输入时,只会保留用户输入的\n换行符。服务器未收到完整的命令,因此一直等待直到超时,返回451错误。

2. clientReceive函数逻辑错误

函数中判断if (iResult > 0)时,iResult是指针变量,实际应该判断解引用后的*iResult > 0。当前条件永远为真(指针地址不可能小于等于0),导致recv返回0或错误时,代码无法正确处理。

3. 接收缓冲区未清空

每次调用clientReceive时,data缓冲区未被清空,旧数据会与新数据叠加,可能导致后续解析混乱。

修复方案

修复命令换行问题

在发送前将fgets获取的\n替换为SMTP要求的\r\n:

// 在调用clientSend前处理prompt
size_t len = strlen(prompt);
if (len > 0 && prompt[len-1] == '\n') {
    prompt[len-1] = '\0'; // 移除原换行符
    strcat(prompt, "\r\n"); // 添加CRLF结束标记
}

修复clientReceive判断逻辑

修改函数内的条件判断,正确解引用指针:

int clientReceive(int *iResult, SOCKET *ConnectSocket, char *out){
    memset(out, 0, 500); // 每次接收前清空缓冲区
    *iResult = recv(*ConnectSocket, out, 200, 0);
    if (*iResult > 0) {
        printf("Bytes received: %d\n", *iResult);
        return 0;
    } else if (*iResult == 0) {
        printf("Connection closed\n");
        return 1;
    } else {
        printf("recv failed: %d\n", WSAGetLastError());
        return 2;
    }

    return -1;
}

补充:Office365 SMTP端口建议

Office365的25端口常被ISP拦截,建议改用587(STARTTLS)或465(SSL/TLS)端口,后续还需实现TLS加密逻辑,否则服务器可能拒绝执行命令。

内容的提问来源于stack exchange,提问作者fpp

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 12:57:26