文档管理中心

Native侧如何实现socket连接

问题现象

在ArkTS侧,声明了一个socket server并运行,然后调用Native侧方法进行socket连接。Native侧代码运行了,但是ArkTS侧未收到连接及消息。Native如何实现socket连接?

背景知识

  • int socket(int af, int type, int protocol)创建套接字。
    展开

    参数

    说明

    af

    地址族(Address Family):也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

    type

    数据传输方式/套接字类型:常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。

    protocol

    传输协议。

  • sockaddr_in:internet环境下套接字的地址形式。
  • htons:将主机的无符号短整形数转换成网络字节顺序。简单来说就是将一个数的高低位互换(如:1234-->3412)。
  • int inet_pton(int af, const character *src, void *dst):用于转换互联网地址,即IP地址,以文本形式包含数字二进制格式。该函数用于将人类可读的IP地址转换为二进制格式的地址。
    展开

    参数

    说明

    af

    地址族(Address Family)。

    sourc

    指的是传递给它的字符串。

    dst

    指向缓冲区,该缓冲区是inet_pton()在转换后存储的数字地址的存储器。系统调用者确保缓冲区的存储能力。它确保 "dst "所分配的缓冲区足够大,以容纳数字地址。

  • int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen)建立连接。
    展开

    参数

    说明

    sock

    socket 文件描述符。

    serv_addr

    sockaddr 结构体变量的指针。

    addrlen

    addr 变量的大小,可由 sizeof() 计算得出。

  • int send(SOCKET sock, const char *buf, int len, int flags)发送数据。
    展开

    参数

    说明

    sock

    要发送数据的套接字。

    buf

    要发送的数据的缓冲区地址。

    len

    要发送的数据的字节数。

    flags

    发送数据时的选项。

  • recv():接收数据。有阻塞模式和非阻塞模式两种工作方式,取决于套接字的设置。
    • 阻塞模式:默认模式。当调用recv函数时,如果套接字的接收缓冲区中没有数据可读,调用将一直阻塞,直到有数据到达或者发生错误。

      适用于数据到达频率较低或数据量较大的场景,简化编程逻辑。

    • 非阻塞模式:通过fcntl函数将套接字设置为非阻塞模式。调用recv时,如果套接字的接收缓冲区中没有数据可读,调用将立即返回,返回值为-1,并且errno被设置为EWOULDBLOCK或EAGAIN。

      适用于需要快速响应或处理多个连接的场景,提高程序的响应速度和处理能力。

  • 在非阻塞模式下,recv函数可能会立即返回EAGAIN或EWOULDBLOCK,表示没有数据可读。为了正确处理这种情况,需要使用select或poll等函数来等待数据可读。
  • 网络连接属于耗时任务,所以需要在Native侧创建子线程去执行连接任务,避免主线程因连接时间较长触发THREAD_BLOCK_6S。

解决方案

  1. Native侧:服务端接口实现。
    /*
     * Copyright (c) Huawei Technologies Co., Ltd. 2025-2025. All rights reserved.
     */
    // napi_init.cpp
    #include "napi/native_api.h"
    #include <arpa/inet.h>
    #include <cstring>
    #include <sys/socket.h>
    #include <unistd.h>
    #include <thread>
    
    #include "hilog/log.h"
    #undef LOG_DOMAIN
    #undef LOG_TAG
    #define LOG_DOMAIN 0x3200        // 全局domain宏,标识业务领域
    #define LOG_TAG "Native侧的日志" // 全局tag宏,标识模块日志tag
    
    #include <fcntl.h>
    #include <sys/select.h>
    
    // 阻塞模式
    static napi_value ConnectJoin(napi_env env, napi_callback_info info)
    {
        napi_value result;
        std::thread th([]() {
            auto clientSocket = socket(AF_INET, SOCK_STREAM, 0);
            if (clientSocket == -1) {
                OH_LOG_ERROR(LOG_APP, "创建socket失败");
                return;
            }
    
            sockaddr_in serverAddr;
            serverAddr.sin_family = AF_INET;
            serverAddr.sin_port = htons(1234);
            if (inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr) <= 0) {
                OH_LOG_ERROR(LOG_APP, "无效地址");
                return;
            }
            // 连接到服务器
            int res = connect(clientSocket, (sockaddr *)&serverAddr, sizeof(serverAddr));
            if (res < 0) {
                OH_LOG_ERROR(LOG_APP, "连接失败:%{public}d", res);
                return;
            }
            // 发送消息
            const char *message = "I am blocking information from Client.";
            send(clientSocket, message, strlen(message), 0);
    
            // 接收服务器响应
            char buffer[1024] = {0};
            int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
            if (bytesReceived > 0) {
                buffer[bytesReceived] = '\0'; // 确保字符串结束
                OH_LOG_INFO(LOG_APP, "客户端接收到信息: %{public}s", buffer);
            } else {
                OH_LOG_ERROR(LOG_APP, "接收失败");
            }
    
            // 关闭socket
            close(clientSocket);
        });
        th.detach();
        napi_create_double(env, 1, &result);
        return result;
    }
    
    // 非阻塞模式
    static napi_value ConnectNotJoin(napi_env env, napi_callback_info info)
    {
        napi_value result;
        std::thread th([]() {
            auto clientSocket = socket(AF_INET, SOCK_STREAM, 0);
            if (clientSocket == -1) {
                OH_LOG_ERROR(LOG_APP, "创建socket失败");
                return;
            }
    
            // 设置为非阻塞模式
            int flags = fcntl(clientSocket, F_GETFL, 0);
            if (flags == -1) {
                OH_LOG_ERROR(LOG_APP, "获取文件描述符标志失败");
                close(clientSocket);
                return;
            }
            if (fcntl(clientSocket, F_SETFL, flags | O_NONBLOCK) == -1) {
                OH_LOG_ERROR(LOG_APP, "设置非阻塞模式失败");
                close(clientSocket);
                return;
            }
    
            sockaddr_in serverAddr;
            serverAddr.sin_family = AF_INET;
            serverAddr.sin_port = htons(1234);
            if (inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr) <= 0) {
                OH_LOG_ERROR(LOG_APP, "无效地址");
                close(clientSocket);
                return;
            }
    
            // 连接到服务器
            int res = connect(clientSocket, (sockaddr *)&serverAddr, sizeof(serverAddr));
            if (res < 0) {
                if (errno != EINPROGRESS) {
                    OH_LOG_ERROR(LOG_APP, "连接失败: %{public}d", errno);
                    close(clientSocket);
                    return;
                }
    
                // 使用select等待连接完成
                fd_set writefds;
                FD_ZERO(&writefds);
                FD_SET(clientSocket, &writefds);
    
                struct timeval timeout;
                timeout.tv_sec = 5; // 设置超时时间为5秒
                timeout.tv_usec = 0;
    
                res = select(clientSocket + 1, NULL, &writefds, NULL, &timeout);
                if (res <= 0) {
                    OH_LOG_ERROR(LOG_APP, "连接超时或失败: %{public}d", res);
                    close(clientSocket);
                    return;
                }
    
                // 检查连接是否成功
                int soError;
                socklen_t len = sizeof(soError);
                if (getsockopt(clientSocket, SOL_SOCKET, SO_ERROR, &soError, &len) < 0) {
                    OH_LOG_ERROR(LOG_APP, "获取连接状态失败: %{public}d", errno);
                    close(clientSocket);
                    return;
                }
                if (soError != 0) {
                    OH_LOG_ERROR(LOG_APP, "连接失败: %{public}d", soError);
                    close(clientSocket);
                    return;
                }
            }
    
            // 发送消息
            const char *message = "I am non-blocking information from Client. ";
            send(clientSocket, message, strlen(message), 0);
    
            // 接收服务器响应
            char buffer[1024] = {0};
            int bytesReceived = 0;
    
            // 使用select等待数据可读
            fd_set readfds;
            FD_ZERO(&readfds);
            FD_SET(clientSocket, &readfds);
    
            struct timeval timeout;
            timeout.tv_sec = 5; // 设置超时时间为5秒
            timeout.tv_usec = 0;
    
            res = select(clientSocket + 1, &readfds, NULL, NULL, &timeout);
            if (res > 0) {
                if (FD_ISSET(clientSocket, &readfds)) {
                    bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
                    if (bytesReceived > 0) {
                        buffer[bytesReceived] = '\0'; // 确保字符串结束
                        OH_LOG_INFO(LOG_APP, "客户端接收到信息: %{public}s", buffer);
                    } else if (bytesReceived == 0) {
                        OH_LOG_INFO(LOG_APP, "连接已关闭");
                    } else {
                        OH_LOG_ERROR(LOG_APP, "接收失败: %{public}d", errno);
                    }
                }
            } else if (res == 0) {
                OH_LOG_INFO(LOG_APP, "接收超时");
            } else {
                OH_LOG_ERROR(LOG_APP, "select 失败: %{public}d", errno);
            }
    
            // 关闭socket
            close(clientSocket);
        });
        th.detach();
        napi_create_double(env, 1, &result);
        return result;
    }
    
    EXTERN_C_START
    static napi_value Init(napi_env env, napi_value exports)
    {
        napi_property_descriptor desc[] = {
            {"connectJoin", nullptr, ConnectJoin, nullptr, nullptr, nullptr, napi_default, nullptr},
            {"connectNotJoin", nullptr, ConnectNotJoin, nullptr, nullptr, nullptr, napi_default, nullptr}};
        napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
        return exports;
    }
    EXTERN_C_END
    
    static napi_module demoModule = {
        .nm_version = 1,
        .nm_flags = 0,
        .nm_filename = nullptr,
        .nm_register_func = Init,
        .nm_modname = "entry",
        .nm_priv = ((void *)0),
        .reserved = {0},
    };
    
    extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
    {
        napi_module_register(&demoModule);
    }
  2. 桥接层:index.d.ts中声明接口。
    // index.d.ts
    export const connectJoin: () => number;
    export const connectNotJoin: () => number;
  3. ArkTS侧:服务端实现。
    // src/main/ets/pages/SCLocalSocket.ets
    import { socket } from '@kit.NetworkKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    
    let localSocketServer: socket.TCPSocketServer = socket.constructTCPSocketServerInstance();
    
    /**
     * 创建本地socket服务端
     * @param listenCallBack 回调是否创建成功及失败原因
     */
    function startListenSocket(listenCallBack: (isSuccess: boolean, error?: string) => void) {
    
      let listenAddr: socket.NetAddress = {
        address: '127.0.0.1',
        port: 1234,
        family: 1
      };
    
      localSocketServer.listen(listenAddr).then(() => {
        let tcpExtraOptions: socket.TCPExtraOptions = { keepAlive: true }; // 是否保持连接
        localSocketServer.setExtraOptions(tcpExtraOptions, (error: BusinessError) => {
          if (error != undefined || error != null) {
            console.error(`tcpExtraOptions error:${error.message}`);
            return;
          }
          startListenSocketEvent();
          listenCallBack(true);
        });
    
      }).catch((err: Object) => {
        listenCallBack(false, 'listen fail: ' + JSON.stringify(err));
      });
    }
    
    /**
     * 开始监听Socket事件
     */
    function startListenSocketEvent() {
    
      localSocketServer.getState((err: BusinessError, data: socket.SocketStateBase) => {
        if (err) {
          console.error(`getState fail`);
          return;
        }
        console.info(`监听Socket事件:getState success:${JSON.stringify(data)}`);
      });
    
      localSocketServer.on("connect", (client: socket.TCPSocketConnection) => {
        console.info(`localSocketServer - connect`);
        client.on('message', (value: socket.SocketMessageInfo) => {
          const uintArray = new Uint8Array(value.message);
          let messageView = '';
          for (let i = 0; i < uintArray.length; i++) {
            messageView += String.fromCharCode(uintArray[i]);
          }
          console.info(`Message total: ${JSON.stringify(value)}`);
          console.info(`Message message information: ${messageView}`);
          sendSocketMessage("I am the information sent from the server. ", client); // 立即回复一条消息
        });
    
      });
    }
    
    /**
     * socket发送消息
     * @param msg 消息
     */
    export function sendSocketMessage(msg: string, client: socket.TCPSocketConnection) {
      if (msg.trim().length <= 0) {
        return;
      }
      let msgObj: socket.TCPSendOptions = { data: msg };
      client.send(msgObj);
    }
    
    /**
     * 开始配置socket
     * @param socketCallBack
     */
    export function startLocalSocket(socketCallBack: (isSuccess: boolean, error?: string) => void) {
      startListenSocket((isSuccess, error) => {
        if (!isSuccess) {
          socketCallBack(isSuccess, error);
          return;
        }
        socketCallBack(isSuccess, error);
      });
    }
    
    /**
     * 关闭所有本地socket订阅信息
     */
    export function stopLocalSocket() {
      localSocketServer.off("connect");
    }
  4. ArkTS侧:接口调用。
    // src/main/ets/pages/Index.ets
    import testNapi from 'libentry.so';
    import { startLocalSocket, stopLocalSocket } from './SCLocalSocket';
    
    @Entry
    @Component
    struct Index {
    
      build() {
        Row() {
          Column() {
            Button('服务端-开启').onClick(() => {
              console.info(`开启Socket服务端`);
              startLocalSocket((isDone, error) => {
                console.error(`开启Socket服务端 = ${isDone ? "ok" : error?.toString()}`);
              });
            }).width('70%').fontSize(30).margin(16);
    
            Button('NDK客户端连接-阻塞').onClick(() => {
              console.info(`- 调用Native侧客户端接口 阻塞 -`);
              let aa = testNapi.connectJoin();
              console.info(`客户端连接:${aa.toString()}`);
            }).width('70%').fontSize(20).margin(16);
    
            Button('NDK客户端-非阻塞').onClick(() => {
              console.info(`- 调用Native侧客户端接口 非阻塞 -`);
              let aa = testNapi.connectNotJoin();
              console.info(`客户端连接-非阻塞:${aa.toString()}`);
            }).width('70%').fontSize(20).margin(16);
    
            Button('服务端-关闭').onClick(() => {
              console.info(`Socket服务端关闭`);
              stopLocalSocket();
            }).width('70%').fontSize(30).margin(16);
    
          }
          .width('100%');
        }
        .height('100%');
      }
    }