Files
STM32F4-Base/IEC60870-Slave/.build/command-v1/before/104/IEC104_Net_Windows.c
T
STF cd42b4d61d
CI / L0 静态门禁 (pre-commit) (push) Failing after 2m51s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 18s
添加遥控业务代码
2026-10-30 20:03:19 -07:00

378 lines
17 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Windows网络后端:所有Winsock类型、错误码和系统计时依赖集中在本文件。
* 实现IEC104_Net.h约定的同步非阻塞操作;上层无需包含Windows系统头文件。
* 不保存连接、不执行协议回调、不实现收发循环:这些调度职责由TcpServer承担。
* 本文件只在Windows目标上编译,最终链接时需加入Winsock库ws2_32。
*/
#ifndef _WIN32
#error "Select a network backend for this platform; IEC104_Net_Windows requires Windows"
#endif
#ifndef _WIN32_WINNT
/* 为InetPtonA/InetNtopA和GetTickCount64启用至少Windows Vista级别的系统接口声明。 */
#define _WIN32_WINNT 0x0600
#endif
#ifndef WIN32_LEAN_AND_MEAN
/* 缩减windows.h引入的内容,配合先包含winsock2.h,避免旧版Winsock头文件冲突。 */
#define WIN32_LEAN_AND_MEAN
#endif
#include "IEC104_Net.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <limits.h>
#include <string.h>
/* Windows SOCKET是指针宽度的数值句柄;编译时检查跨层转换不会丢失句柄位。 */
_Static_assert(sizeof(IEC104_NetSocket_t) >= sizeof(SOCKET), "Network handle must hold a SOCKET");
/**
* 功能:返回通用状态,并按需写出已经取得的平台错误码。
* 参数:result为调度状态,error为诊断码,native_error可为NULL。
* 说明:本函数不调用WSAGetLastError;调用方应先保存原始错误,再执行资源清理。
*/
static IEC104_NetResult_t Result_With_Error(
IEC104_NetResult_t result, int error, int *native_error)
{
if (native_error != NULL) {
*native_error = error;
}
return result;
}
/**
* 功能:将常见Winsock失败原因转换为通用操作状态。
* 返回值:暂不可读写映射为WOULD_BLOCK,操作被中断映射为RETRY,其余映射为ERROR。
* 说明:只转换状态,不丢弃原始错误码;Accept特有的客户端失效及资源暂缺另行处理。
*/
static IEC104_NetResult_t Socket_Error_Result(int error)
{
if (error == WSAEWOULDBLOCK) {
return IEC104_NET_WOULD_BLOCK;
}
return error == WSAEINTR ? IEC104_NET_RETRY : IEC104_NET_ERROR;
}
/* 只用于接入路径;收发及监听初始化仍保留各自原有的错误分类。 */
static int Accept_Resource_Error(int error)
{
return error == WSAENOBUFS || error == WSAEMFILE;
}
/* 构造一次收发结果;这里只打包参数,字节数与状态的一致性由调用位置保证。 */
static IEC104_NetIoResult_t Io_Result(IEC104_NetResult_t result, size_t transferred, int error)
{
IEC104_NetIoResult_t output;
output.result = result;
output.transferred = transferred;
output.native_error = error;
return output;
}
/**
* 功能:把已经创建的Windows socket切换到非阻塞模式。
* 返回值:非0表示成功,0表示失败;失败后调用方须立即取得WSAGetLastError。
* 说明:FIONBIO取1只改变I/O模式,本函数不会关闭句柄,也不会创建后台任务。
*/
static int Set_Nonblocking(SOCKET socket_handle)
{
u_long enabled = 1UL;
return ioctlsocket(socket_handle, FIONBIO, &enabled) == 0;
}
/**
* 功能:请求Winsock 2.2初始化,成功后取得一次需配对释放的引用。
* 返回值:将WSAStartup直接返回的错误码转为OK/ERROR,并按需写入native_error。
* 说明:WSAStartup的错误不能改用WSAGetLastError读取;本层不缓存“已初始化”标志。
*/
IEC104_NetResult_t IEC104_Net_Init(int *native_error)
{
WSADATA startup;
int error = WSAStartup(MAKEWORD(2, 2), &startup);
return Result_With_Error(error == 0 ? IEC104_NET_OK : IEC104_NET_ERROR, error, native_error);
}
/* 释放一次成功Init取得的Winsock引用;调用方应先关闭自己持有的全部socket。
* 与Init一一配对,本函数不检查重复释放,接口也不对外返回WSACleanup的结果。 */
void IEC104_Net_Cleanup(void)
{
(void)WSACleanup();
}
/* 向上层提供Windows参数错误码,使WSAEINVAL不出现在通用调度代码中。 */
int IEC104_Net_Invalid_Argument_Error(void)
{
return WSAEINVAL;
}
/**
* 功能:完成数字IPv4地址解析、socket创建、非阻塞设置、绑定及监听。
* 输出:成功才交付新句柄和实际端口;失败关闭本次创建的socket,保留原始错误码。
* 说明:本函数只负责监听socket;失败不释放调用方由Net_Init取得的平台引用。
* 参数及资源所有权的完整约定见IEC104_Net.h。
*/
IEC104_NetResult_t IEC104_Net_Listen(
const char *bind_address, uint16_t port, IEC104_NetSocket_t *listener,
uint16_t *bound_port, int *native_error)
{
struct sockaddr_in address;
int address_length = (int)sizeof(address);
int enabled = 1;
int error;
SOCKET socket_handle;
/* 先统一失败输出;只清空输出值,不会替调用方关闭原来保存在其中的句柄。 */
if (listener != NULL) {
*listener = IEC104_NET_INVALID_SOCKET;
}
if (bound_port != NULL) {
*bound_port = 0U;
}
if (bind_address == NULL || listener == NULL || bound_port == NULL) {
return Result_With_Error(IEC104_NET_ERROR, WSAEINVAL, native_error);
}
/* 只解析数字IPv4,不做可能阻塞的域名查询;系统sockaddr中的端口使用网络字节序。 */
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
if (InetPtonA(AF_INET, bind_address, &address.sin_addr) != 1) {
return Result_With_Error(IEC104_NET_ERROR, WSAEINVAL, native_error);
}
socket_handle = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (socket_handle == INVALID_SOCKET) {
return Result_With_Error(IEC104_NET_ERROR, WSAGetLastError(), native_error);
}
/* 独占地址选项须在bind前设置,保留原有监听地址占用策略。
* 各步骤按短路顺序执行,任一步骤失败即停止;getsockname读取实际绑定端口,
* 因而port=0时也能将系统分配的端口交回上层。 */
if (setsockopt(socket_handle, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
(const char *)&enabled, sizeof(enabled)) == SOCKET_ERROR ||
!Set_Nonblocking(socket_handle) ||
bind(socket_handle, (const struct sockaddr *)&address, sizeof(address)) == SOCKET_ERROR ||
listen(socket_handle, SOMAXCONN) == SOCKET_ERROR ||
getsockname(socket_handle, (struct sockaddr *)&address, &address_length) == SOCKET_ERROR) {
/* closesocket可能改变线程错误状态,必须先保存最初失败的错误码。 */
error = WSAGetLastError();
(void)closesocket(socket_handle);
return Result_With_Error(IEC104_NET_ERROR, error, native_error);
}
/* 到此才转移句柄所有权;公共接口交付的端口恢复为主机字节序。 */
*listener = (IEC104_NetSocket_t)socket_handle;
*bound_port = ntohs(address.sin_port);
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/**
* 功能:尝试接入一个客户端,设置非阻塞和TCP_NODELAY并转换对端地址后一起交付。
* 返回值:没有待接入客户端时返回WOULD_BLOCK;本次可恢复失败返回RETRY,资源暂缺返回RETRY_LATER。
* 说明:不在后端循环accept,额外连接及失败重试均受TcpServer本轮接入预算约束。
* 任何失败都不会在本函数内关闭listener或修改上层连接状态。
*/
IEC104_NetResult_t IEC104_Net_Accept(
IEC104_NetSocket_t listener, IEC104_NetSocket_t *client, IEC104_NetPeer_t *peer, int *native_error)
{
SOCKET accepted;
struct sockaddr_in address;
int address_length = (int)sizeof(address);
int enabled = 1;
IEC104_NetPeer_t prepared;
int error;
IEC104_NetResult_t result;
if (client != NULL) {
*client = IEC104_NET_INVALID_SOCKET;
}
if (client == NULL || peer == NULL || listener == IEC104_NET_INVALID_SOCKET) {
return Result_With_Error(IEC104_NET_ERROR, WSAEINVAL, native_error);
}
/* 接入时取得不可变的TCP对端信息;上层后续只读缓存,不再查询可能已关闭或复用的socket。 */
memset(&address, 0, sizeof(address));
accepted = accept((SOCKET)listener, (struct sockaddr *)&address, &address_length);
if (accepted == INVALID_SOCKET) {
error = WSAGetLastError();
/* 资源暂缺交给上层定时重试;单个客户端复位或中止仍可在本轮预算内重试。 */
if (Accept_Resource_Error(error)) {
result = IEC104_NET_RETRY_LATER;
} else {
result = error == WSAECONNRESET || error == WSAECONNABORTED ?
IEC104_NET_RETRY : Socket_Error_Result(error);
}
return Result_With_Error(result, error, native_error);
}
/* 显式设置非阻塞并关闭Nagle,减少104短报文的合并等待;任一步骤失败均回收新客户端。 */
if (!Set_Nonblocking(accepted) ||
setsockopt(accepted, IPPROTO_TCP, TCP_NODELAY,
(const char *)&enabled, sizeof(enabled)) == SOCKET_ERROR) {
error = WSAGetLastError();
(void)closesocket(accepted);
return Result_With_Error(Accept_Resource_Error(error) ? IEC104_NET_RETRY_LATER : IEC104_NET_RETRY,
error, native_error);
}
if (address_length < (int)sizeof(address) || address.sin_family != AF_INET) {
(void)closesocket(accepted);
return Result_With_Error(IEC104_NET_RETRY, WSAEAFNOSUPPORT, native_error);
}
memset(&prepared, 0, sizeof(prepared));
if (InetNtopA(AF_INET, &address.sin_addr, prepared.address, sizeof(prepared.address)) == NULL) {
error = WSAGetLastError();
(void)closesocket(accepted);
return Result_With_Error(Accept_Resource_Error(error) ? IEC104_NET_RETRY_LATER : IEC104_NET_RETRY,
error, native_error);
}
prepared.port = ntohs(address.sin_port);
/* 所有准备均成功才发布地址及句柄;失败不泄漏新socket,也不覆盖调用方原地址。 */
*peer = prepared;
*client = (IEC104_NetSocket_t)accepted;
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/**
* 功能:执行一次同步非阻塞接收,将字节数、EOF和错误分别交付上层。
* 说明:不循环读取、不识别104帧边界,也不因EOF主动关闭客户端。
* recv返回0表示对端发送方向关闭,上层仍可决定排空本地待发送数据。
*/
IEC104_NetIoResult_t IEC104_Net_Recv(
IEC104_NetSocket_t socket_handle, void *data, size_t capacity)
{
int received;
int error;
/* Winsock长度参数为int,先校验再转换,避免大size_t被截断成负数。
* 排除零容量请求,使recv返回0可以明确表示EOF。 */
if (socket_handle == IEC104_NET_INVALID_SOCKET || data == NULL || capacity == 0U || capacity > INT_MAX) {
return Io_Result(IEC104_NET_ERROR, 0U, WSAEINVAL);
}
received = recv((SOCKET)socket_handle, (char *)data, (int)capacity, 0);
if (received > 0) {
return Io_Result(IEC104_NET_OK, (size_t)received, 0);
}
if (received == 0) {
return Io_Result(IEC104_NET_EOF, 0U, 0);
}
/* 只在recv失败后读取错误;WOULD_BLOCK和RETRY都没有新增接收字节。 */
error = WSAGetLastError();
return Io_Result(Socket_Error_Result(error), 0U, error);
}
IEC104_NetResult_t IEC104_Net_Pending_Input(
IEC104_NetSocket_t socket_handle, uint32_t *available, int *native_error)
{
u_long count = 0UL;
if (socket_handle == IEC104_NET_INVALID_SOCKET || available == NULL) {
return Result_With_Error(IEC104_NET_ERROR, WSAEINVAL, native_error);
}
if (ioctlsocket((SOCKET)socket_handle, FIONREAD, &count) == SOCKET_ERROR) {
return Result_With_Error(IEC104_NET_ERROR, WSAGetLastError(), native_error);
}
*available = (uint32_t)count;
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/**
* 功能:执行一次同步非阻塞发送,准确报告本次交给本机网络栈的字节数。
* 说明:不代替上层循环补发;调用方须保留未发送的后缀,并按实际进度推进发送计划。
* 使用普通send,不发起需在返回后继续借用data的异步重叠I/O。
*/
IEC104_NetIoResult_t IEC104_Net_Send(
IEC104_NetSocket_t socket_handle, const void *data, size_t length)
{
int written;
int error;
/* 与Recv一致,长度须能无损转换为Winsock要求的正int。 */
if (socket_handle == IEC104_NET_INVALID_SOCKET || data == NULL || length == 0U || length > INT_MAX) {
return Io_Result(IEC104_NET_ERROR, 0U, WSAEINVAL);
}
written = send((SOCKET)socket_handle, (const char *)data, (int)length, 0);
/* 允许部分写入及零进度;send返回0不同于recv返回0,不转成EOF。 */
if (written >= 0) {
return Io_Result(IEC104_NET_OK, (size_t)written, 0);
}
error = WSAGetLastError();
return Io_Result(Socket_Error_Result(error), 0U, error);
}
/**
* 功能:通过一次select等待socket就绪并转换事件位;空集合改作有限定时等待。
* 返回值:正常就绪或超时均为OK;中断为RETRY,其他等待失败为ERROR。
* 说明:最多关注一个监听和一个客户端;不执行实际收发,也不在内部重试select。
* 等待期限及中断后的剩余时长由TcpServer重新计算,避免反复等待延长期限。
*/
IEC104_NetResult_t IEC104_Net_Wait(
IEC104_NetSocket_t listener, IEC104_NetSocket_t client, uint32_t interest,
uint32_t timeout_ms, uint32_t *ready_events, int *native_error)
{
fd_set read_set, write_set;
struct timeval timeout;
int ready;
/* 清除上一轮结果;参数错误、等待失败及正常超时均不会残留旧就绪位。 */
if (ready_events != NULL) {
*ready_events = 0U;
}
/* 只校验本轮实际关注的句柄;空集合用于接入暂停等纯定时等待。 */
if (ready_events == NULL ||
(interest & ~(IEC104_NET_EVENT_ACCEPT | IEC104_NET_EVENT_READ | IEC104_NET_EVENT_WRITE)) != 0U ||
((interest & IEC104_NET_EVENT_ACCEPT) != 0U && listener == IEC104_NET_INVALID_SOCKET) ||
((interest & (IEC104_NET_EVENT_READ | IEC104_NET_EVENT_WRITE)) != 0U && client == IEC104_NET_INVALID_SOCKET)) {
return Result_With_Error(IEC104_NET_ERROR, WSAEINVAL, native_error);
}
if (interest == 0U) {
/* Winsock的select不接受空集合;0直接返回,UINT32_MAX不能传成Sleep(INFINITE)。 */
if (timeout_ms != 0U) {
Sleep((DWORD)(timeout_ms == UINT32_MAX ? UINT32_MAX - 1U : timeout_ms));
}
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/* select会改写集合,每次都重新构建;监听接入和客户端接收都使用读集合。 */
FD_ZERO(&read_set);
FD_ZERO(&write_set);
if ((interest & IEC104_NET_EVENT_ACCEPT) != 0U) {
FD_SET((SOCKET)listener, &read_set);
}
if ((interest & IEC104_NET_EVENT_READ) != 0U) {
FD_SET((SOCKET)client, &read_set);
}
if ((interest & IEC104_NET_EVENT_WRITE) != 0U) {
FD_SET((SOCKET)client, &write_set);
}
/* 将毫秒拆成秒和微秒;二者均为0时select只查询,不等待后续事件。 */
timeout.tv_sec = (long)(timeout_ms / 1000U);
timeout.tv_usec = (long)((timeout_ms % 1000U) * 1000U);
/* Winsock忽略nfds,固定传0;fd_set和timeval等平台细节不进入公共头文件。 */
ready = select(0, &read_set, &write_set, NULL, &timeout);
if (ready == SOCKET_ERROR) {
int error = WSAGetLastError();
return Result_With_Error(error == WSAEINTR ? IEC104_NET_RETRY : IEC104_NET_ERROR, error, native_error);
}
/* select返回就绪数量,不能直接作为事件掩码;逐项还原本轮关注的事件位。 */
if (ready > 0) {
if ((interest & IEC104_NET_EVENT_ACCEPT) != 0U && FD_ISSET((SOCKET)listener, &read_set)) {
*ready_events |= IEC104_NET_EVENT_ACCEPT;
}
if ((interest & IEC104_NET_EVENT_READ) != 0U && FD_ISSET((SOCKET)client, &read_set)) {
*ready_events |= IEC104_NET_EVENT_READ;
}
if ((interest & IEC104_NET_EVENT_WRITE) != 0U && FD_ISSET((SOCKET)client, &write_set)) {
*ready_events |= IEC104_NET_EVENT_WRITE;
}
}
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/**
* 功能:关闭单个socket,将closesocket失败转换为ERROR及原始诊断码。
* 说明:INVALID_SOCKET直接返回OK,便于调用方清理尚未完整初始化的资源。
* 参数按值传入,不会清空调用方字段;也不会释放Init取得的Winsock引用。
*/
IEC104_NetResult_t IEC104_Net_Close(IEC104_NetSocket_t socket_handle, int *native_error)
{
if (socket_handle != IEC104_NET_INVALID_SOCKET && closesocket((SOCKET)socket_handle) == SOCKET_ERROR) {
return Result_With_Error(IEC104_NET_ERROR, WSAGetLastError(), native_error);
}
return Result_With_Error(IEC104_NET_OK, 0, native_error);
}
/* 读取系统启动以来的64位毫秒计数;供相对期限计算,不受用户修改日历时间影响。
* 不在此处换算为UTC或本地时间,避免将协议超时与日历时钟混用。 */
uint64_t IEC104_Net_Now_Ms(void)
{
return (uint64_t)GetTickCount64();
}