找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3076

积分

1

好友

425

主题
发表于 3 小时前 | 查看: 2| 回复: 0

在嵌入式开发中,TCP网络通信是一个常见且关键的需求。无论是物联网设备的数据上报、工业控制系统的指令交互,还是各种需要联网的应用程序,都离不开可靠的数据传输。然而,直接使用底层的socketbindlistenaccept这一套API进行开发时,你是否也感到过一丝繁琐?

那些重复的参数设置、复杂的结构体填充,还有稍不留神就可能出错的字节序转换,不仅拉低了开发效率,也让代码难以维护。每次写新项目,似乎都要把这一套流程再“复制粘贴”一遍。

今天,我们就来探讨一个实用的解决方案:封装一套简洁、健壮的TCP常用接口,告别重复代码,让嵌入式网络编程变得更高效。

TCP客户端与服务端通信示例

一、 为何需要封装TCP接口?

让我们先回顾一下标准的TCP通信流程。

对于服务端,需要经历:创建socketbind地址 → listen监听 → accept接受连接。
对于客户端,则是:创建socketconnect连接服务器。

这个过程可以用下图简要表示:

TCP通信基本流程

流程本身并不复杂,但每个步骤都涉及参数设置。例如bind函数,你需要填充sockaddr_in结构体,设置地址族、IP地址、端口号,并且必须记得做主机字节序到网络字节序的转换。如果每次都手动编写这些代码,不仅效率低下,而且容易因疏忽而出错。

因此,我们的封装目标非常明确:将复杂的参数设置和重复性的样板代码隐藏起来,对外提供一组简洁、易用且功能完整的接口。

二、 封装方案设计

我们的核心思路是,将TCP通信中的常用操作抽象为几个关键函数。每个函数只暴露最必要的参数,内部自动完成所有细节处理。整个封装层的架构如下图所示:

TCP接口封装架构图

我们主要封装了以下几个核心函数:

  • tcp_init:服务端初始化,一个函数完成socket创建、bindlisten全流程。
  • tcp_accept:接受客户端连接,简化参数传递,并可选择获取客户端信息。
  • tcp_connect:客户端连接服务器,只需提供IP和端口,支持超时控制。
  • tcp_send / tcp_send_all:发送数据,后者确保完整发送所有字节。
  • tcp_blocking_recv:阻塞方式接收数据。
  • tcp_nonblocking_recv:非阻塞方式接收数据,支持微秒级超时控制。
  • tcp_close:关闭连接。

三、 核心代码实现

下面我们来看看具体的实现。首先从头文件开始,它定义了接口和错误码。

3.1 头文件定义 (tcp_socket.h)

#ifndef TCP_SOCKET_H
#define TCP_SOCKET_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <stdint.h>

#define MAX_CONNECT_NUM         10      /* 最大连接队列长度 */
#define TCP_NO_TIMEOUT          0       /* 无超时 */

/*===========================================================================
 * 错误码定义
 *=========================================================================*/

#define TCP_SUCCESS             0       /* 成功 */
#define TCP_ERR_SOCKET         -1       /* socket创建失败 */
#define TCP_ERR_SETSOCKOPT     -2       /* setsockopt设置失败 */
#define TCP_ERR_BIND           -3       /* bind绑定失败 */
#define TCP_ERR_LISTEN         -4       /* listen监听失败 */
#define TCP_ERR_ACCEPT         -5       /* accept接受连接失败 */
#define TCP_ERR_CONNECT        -6       /* connect连接失败 */
#define TCP_ERR_TIMEOUT        -7       /* 连接超时 */
#define TCP_ERR_SEND           -8       /* 发送失败 */
#define TCP_ERR_RECV           -9       /* 接收失败 */

/*===========================================================================
 * API 接口
 *=========================================================================*/

int tcp_init(const char* ip, int port);
int tcp_accept(int server_fd, char *client_ip, int ip_len, int *client_port);
int tcp_connect(const char* ip, int port, int timeout_sec);
int tcp_nonblocking_recv(int conn_sockfd, void *rx_buf, int buf_len,
int timeval_sec, int timeval_usec);
int tcp_blocking_recv(int conn_sockfd, void *rx_buf, uint16_t buf_len);
int tcp_send(int conn_sockfd, uint8_t *tx_buf, uint16_t buf_len);
int tcp_send_all(int conn_sockfd, uint8_t *tx_buf, uint16_t buf_len);
void tcp_close(int sockfd);

#endif

头文件的设计体现了几个关键点:

  1. 明确的错误码系统:为每个可能出错的操作定义了唯一的错误码,便于上层进行精确的错误处理和调试。
  2. 精简的参数列表:每个函数只保留最核心的参数,大幅降低了使用的复杂度。
  3. 完整的功能覆盖:涵盖了从初始化、连接、数据收发到关闭的整个TCP/IP通信生命周期。

3.2 关键函数实现细节 (tcp_socket.c)

3.2.1 tcp_init - 服务端初始化

这个函数将服务端启动所需的四个步骤(创建socket、设置端口复用、地址绑定、开始监听)合并为一个调用。

int tcp_init(const char* ip, int port)
{
int optval = 1;
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0)
    {
        perror("socket");
return TCP_ERR_SOCKET;
    }

/* 解除端口占用 */
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
 {
  perror("setsockopt");
  close(server_fd);
return TCP_ERR_SETSOCKOPT;
 }

struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(struct sockaddr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
if (NULL == ip)
    {
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    }
else
    {
        server_addr.sin_addr.s_addr = inet_addr(ip);
    }

if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)) < 0)
    {
        perror("bind");
        close(server_fd);
return TCP_ERR_BIND;
    }

if(listen(server_fd, MAX_CONNECT_NUM) < 0)
    {
        perror("listen");
        close(server_fd);
return TCP_ERR_LISTEN;
    }

return server_fd;
}

要点:使用SO_REUSEADDR选项避免了服务器程序快速重启时可能遇到的“Address already in use”错误。当ip参数传入NULL时,自动绑定所有本地网卡地址(INADDR_ANY),使服务器能够接受来自任何本地IP的连接。

3.2.2 tcp_accept - 接受客户端连接

封装了accept调用,并增加了获取客户端地址信息的功能。

int tcp_accept(int server_fd, char *client_ip, int ip_len, int *client_port)
{
struct sockaddr_in client_addr = {0};
socklen_t addrlen = sizeof(struct sockaddr);
int new_fd = accept(server_fd, (struct sockaddr*) &client_addr, &addrlen);
if(new_fd < 0)
    {
        perror("accept");
return TCP_ERR_ACCEPT;
    }

/* 返回客户端IP和端口信息 */
if (client_ip != NULL && ip_len > 0)
    {
snprintf(client_ip, ip_len, "%s", inet_ntoa(client_addr.sin_addr));
    }
if (client_port != NULL)
    {
        *client_port = ntohs(client_addr.sin_port);
    }

return new_fd;
}

使用示例

/* 需要客户端信息 */
char client_ip[32];
int client_port;
int client_fd = tcp_accept(server_fd, client_ip, sizeof(client_ip), &client_port);
printf("客户端: %s:%d\n", client_ip, client_port);

/* 不需要客户端信息 */
int client_fd = tcp_accept(server_fd, NULL, 0, NULL);

3.2.3 tcp_connect - 客户端连接(支持超时)

实现了带超时控制的连接功能,这对于网络状况不稳定或需要快速失败响应的嵌入式场景非常有用。

int tcp_connect(const char *ip, int port, int timeout_sec)
{
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0)
    {
        perror("socket");
return TCP_ERR_SOCKET;
    }

struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(struct sockaddr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr.s_addr = inet_addr(ip);

/* 无超时,使用系统默认(阻塞模式) */
if (timeout_sec == 0)
    {
if (connect(server_fd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)) < 0)
        {
            perror("connect");
            close(server_fd);
return TCP_ERR_CONNECT;
        }
return server_fd;
    }

/* 有超时,使用非阻塞模式 */
int flags = fcntl(server_fd, F_GETFL, 0);
if (flags < 0 || fcntl(server_fd, F_SETFL, flags | O_NONBLOCK) < 0)
    {
        perror("fcntl");
        close(server_fd);
return TCP_ERR_SOCKET;
    }

int ret = connect(server_fd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
if (ret < 0)
    {
if (errno != EINPROGRESS)
        {
            perror("connect");
            close(server_fd);
return TCP_ERR_CONNECT;
        }

/* 使用select等待连接完成 */
        fd_set writeset;
struct timeval timeout;
        timeout.tv_sec = timeout_sec;
        timeout.tv_usec = 0;

        FD_ZERO(&writeset);
        FD_SET(server_fd, &writeset);

        ret = select(server_fd + 1, NULL, &writeset, NULL, &timeout);
if (ret <= 0)
        {
/* 超时或错误 */
            close(server_fd);
return TCP_ERR_TIMEOUT;
        }

/* 检查连接是否成功 */
int error = 0;
socklen_t len = sizeof(error);
if (getsockopt(server_fd, SOL_SOCKET, SO_ERROR, &error, &len) < 0 || error != 0)
        {
            close(server_fd);
return TCP_ERR_CONNECT;
        }
    }

/* 恢复阻塞模式 */
    fcntl(server_fd, F_SETFL, flags);

return server_fd;
}

逻辑解析:当timeout_sec为0时,使用默认阻塞连接。当设置超时时间后,函数会将socket设置为非阻塞模式发起连接,然后使用select系统调用在指定时间内等待连接完成。无论成功与否,最后都会将socket恢复为阻塞模式,便于后续的数据收发操作。

3.2.4 数据收发函数

我们封装了两种发送和两种接收函数,以适应不同场景。

发送函数
tcp_send是基础发送,使用MSG_NOSIGNAL标志避免在连接已断开时收到SIGPIPE信号导致进程意外退出(如果系统不支持该标志,则回退到普通发送)。
tcp_send_all则通过循环发送,确保指定长度的数据全部被送出,并处理了被信号中断(EINTR)的情况,保证了数据传输的完整性。

int tcp_send(int conn_sockfd, uint8_t *tx_buf, uint16_t buf_len)
{
#ifdef MSG_NOSIGNAL
return send(conn_sockfd, tx_buf, buf_len, MSG_NOSIGNAL);
#else
return send(conn_sockfd, tx_buf, buf_len, 0);
#endif
}

int tcp_send_all(int conn_sockfd, uint8_t *tx_buf, uint16_t buf_len)
{
uint16_t total_sent = 0;
int sent = 0;

while (total_sent < buf_len)
    {
#ifdef MSG_NOSIGNAL
        sent = send(conn_sockfd, tx_buf + total_sent, buf_len - total_sent, MSG_NOSIGNAL);
#else
        sent = send(conn_sockfd, tx_buf + total_sent, buf_len - total_sent, 0);
#endif
if (sent < 0)
        {
if (errno == EINTR)
            {
/* 被信号中断,继续发送 */
continue;
            }
            perror("send");
return TCP_ERR_SEND;
        }
else if (sent == 0)
        {
/* 连接已关闭 */
return TCP_ERR_SEND;
        }

        total_sent += sent;
    }

return total_sent;
}

接收函数
tcp_blocking_recv是简单的阻塞接收,会一直等待直到有数据到达或连接关闭。
tcp_nonblocking_recv则利用select实现超时控制,可以在指定的秒和微秒时间内等待数据,避免了无限期阻塞,适合需要同时处理多个连接或实现超时逻辑的场景。

int tcp_blocking_recv(int conn_sockfd, void *rx_buf, uint16_t buf_len)
{
return recv(conn_sockfd, rx_buf, buf_len, 0);
}

int tcp_nonblocking_recv(int conn_sockfd, void *rx_buf, int buf_len, int timeval_sec, int timeval_usec)
{
 fd_set readset;
struct timeval timeout = {0, 0};
int recv_bytes = 0;
int ret = 0;

 timeout.tv_sec = timeval_sec;
 timeout.tv_usec = timeval_usec;
 FD_ZERO(&readset);
 FD_SET(conn_sockfd, &readset);

 ret = select(conn_sockfd + 1, &readset, NULL, NULL, &timeout);
if (ret > 0 && FD_ISSET(conn_sockfd, &readset))
    {
  recv_bytes = recv(conn_sockfd, rx_buf, buf_len, MSG_DONTWAIT);
if (recv_bytes == -1)
        {
   perror("recv");
return -1;
  }
 }
else
    {
return -1;
 }

return recv_bytes;
}

3.3 封装库核心特性总结

通过上述实现,这套TCP封装库具备了以下突出特点:

  1. 简洁易用:接口参数极少,学习成本低,上手快。
  2. 功能完整:覆盖了连接超时、客户端信息获取、数据完整发送等实际开发中的常见需求。
  3. 健壮性强:内置了错误码系统、资源自动管理(如连接失败自动关闭socket)、以及防止SIGPIPE等机制。
  4. 灵活性好:像tcp_accept这样的函数,参数支持传入NULL,可按需获取信息,不强制占用资源。

四、 实战应用示例

我们用一个经典的“回声服务器”(Echo Server)和其客户端来演示这套封装库的使用。服务端监听连接,收到任何数据后原样发回给客户端。

4.1 服务端实现 (tcp_server.c)

TCP服务端处理流程图

#include "tcp_socket.h"

int main(int argc, char **argv)
{
printf("==================tcp server==================\n");

/* 初始化服务器,监听4321端口 */
int server_fd = tcp_init(NULL, 4321);
if (server_fd < 0)
    {
printf("tcp_init error! code: %d\n", server_fd);
exit(EXIT_FAILURE);
    }
printf("Server listening on port 4321...\n");

/* 接受客户端连接并获取客户端信息 */
char client_ip[32] = {0};
int client_port = 0;
int client_fd = tcp_accept(server_fd, client_ip, sizeof(client_ip), &client_port);
if (client_fd < 0)
    {
printf("tcp_accept error! code: %d\n", client_fd);
        tcp_close(server_fd);
exit(EXIT_FAILURE);
    }
printf("Client connected: %s:%d\n", client_ip, client_port);

/* 循环接收数据并回显 */
while (1)
    {
char buf[128] = {0};

int recv_len = tcp_blocking_recv(client_fd, buf, sizeof(buf));
if (recv_len <= 0)
        {
printf("Client disconnected\n");
            tcp_close(client_fd);
            tcp_close(server_fd);
exit(EXIT_FAILURE);
        }
printf("Received: %s\n", buf);

/* 使用tcp_send_all确保完整发送 */
int send_len = tcp_send_all(client_fd, (uint8_t*)buf, strlen(buf));
if (send_len < 0)
        {
printf("Send error! code: %d\n", send_len);
            tcp_close(client_fd);
            tcp_close(server_fd);
exit(EXIT_FAILURE);
        }
printf("Echo sent: %d bytes\n", send_len);
    }

    tcp_close(server_fd);
return 0;
}

可以看到,服务端的主逻辑非常清晰:初始化、接受连接、然后进入接收-回显循环。所有底层的网络细节都被封装函数隐藏了。

4.2 客户端实现 (tcp_client.c)

#include "tcp_socket.h"

int main(int argc, char **argv)
{
printf("==================tcp client==================\n");
if (argc < 3)
    {
printf("Usage: ./tcp_client <ip> <port>\n");
exit(EXIT_FAILURE);
    }

char ip_buf[32] = {0};
int port = 0;
memcpy(ip_buf, argv[1], strlen(argv[1]));
    port = atoi(argv[2]);

/* 连接服务器,5秒超时 */
printf("Connecting to %s:%d ...\n", ip_buf, port);
int server_fd = tcp_connect(ip_buf, port, 5);
if (server_fd < 0)
    {
if (server_fd == TCP_ERR_TIMEOUT)
        {
printf("Connection timeout!\n");
        }
else
        {
printf("tcp_connect error! code: %d\n", server_fd);
        }
exit(EXIT_FAILURE);
    }
printf("Connected successfully!\n");

/* 循环发送和接收数据 */
while (1)
    {
char buf[128] = {0};
printf("\nInput message: ");
if (scanf("%s", buf))
        {
/* 使用tcp_send_all确保完整发送 */
int send_len = tcp_send_all(server_fd, (uint8_t*)buf, strlen(buf));
if (send_len < 0)
            {
printf("tcp_send error! code: %d\n", send_len);
                tcp_close(server_fd);
exit(EXIT_FAILURE);
            }
printf("Sent: %d bytes\n", send_len);

            bzero(buf, sizeof(buf));
int recv_len = tcp_blocking_recv(server_fd, buf, sizeof(buf));
if (recv_len <= 0)
            {
printf("Server disconnected\n");
                tcp_close(server_fd);
exit(EXIT_FAILURE);
            }
printf("Received: %s (%d bytes)\n", buf, recv_len);
        }
    }

    tcp_close(server_fd);
return 0;
}

客户端同样简洁:解析参数、带超时连接服务器,然后进入发送-接收循环。完善的错误处理确保了程序的健壮性。

4.3 运行效果

编译并运行上述代码,你将会看到类似下图的交互过程,直观地展示了封装后接口的易用性和可靠性。

TCP客户端与服务端运行结果

五、 项目代码获取

本文涉及的完整代码,包括头文件、实现以及示例程序,均已开源:

  • Gitee 仓库:https://gitee.com/EmbeddedLinuxZn/tcp_socket
  • GitHub 仓库:https://github.com/EmbeddedLinuxZn/tcp_socket

你可以直接克隆仓库,快速集成到你的项目中,或以此为基础进行二次开发。

六、 总结与展望

这套TCP接口封装方案源于实际嵌入式项目的开发经验,旨在解决网络编程中的重复劳动和易错问题。通过将底层的、易出错的细节封装起来,它显著提升了开发效率,并使得网络通信相关的代码更加模块化和可维护。

实际上,这套封装只是一个起点。你可以根据自己项目的具体需求,在此基础上进行丰富的扩展:

  • 增加安全传输:集成SSL/TLS库(如Mbed TLS),为数据通信增加加密层。
  • 实现连接管理:封装连接池,管理多个客户端连接,适用于需要高并发的服务器场景。
  • 加入健康检测:实现心跳机制(Heartbeat),自动检测并清理失效的连接。
  • 适配不同平台:当前实现针对Linux/POSIX系统,你可以补充Windows或RTOS(如FreeRTOS+lwIP)下的实现,形成跨平台库。

在网络成为嵌入式系统标配的今天,拥有一套自己熟悉的、可靠的网络通信基础库,无疑能让你在物联网或任何联网设备开发中更加得心应手。希望这套封装思路和代码能为你带来启发,如果你有更好的想法或改进,也欢迎在云栈社区等技术社区进行交流分享。




上一篇:AI智能体部署架构选型:批量、流式、实时与边缘模式深度解析
下一篇:Python设计模式实战:10个可维护大项目必备架构方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-29 23:16 , Processed in 0.279000 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表