将 libsmb2 集成到 HarmonyOS ArkTS 项目
本文记录在鸿蒙媒体播放器项目 hmplayer 中集成 libsmb2 实现 SMB 网络文件浏览与播放的完整过程。libsmb2支持smb3协议。能够查看macos上的文件夹分享。配置macos分享的时候需要把选项中的window共享创建一个账号和密码。之后使用使用此账号密码进行连接。
整体架构
scss
ArkTS 层 (Smb2Client.ets)
↓ NAPI 绑定 (libentry.so)
C/C++ 层 (napi_init.cpp)
↓ 静态链接
libsmb2 (thirdparty/libsmb2/arm64-v8a/lib/libsmb2.so.1)
ArkTS 通过 NAPI 调用 C++ 导出的函数,C++ 层直接调用 libsmb2 的 C API。整个模块编译为 libentry.so,在应用加载时自动注册。
第一步:放置预编译库
libsmb2 不需要在项目中从源码编译,直接放置预编译好的 arm64-v8a 架构的 .so 文件:
bash
entry/src/main/cpp/thirdparty/libsmb2/arm64-v8a/
├── include/smb2/
│ ├── libsmb2.h
│ ├── smb2.h
│ ├── smb2-errors.h
│ ├── libsmb2-raw.h
│ └── libsmb2-dcerpc*.h
├── lib/
│ ├── libsmb2.so.6.1.0
│ ├── libsmb2.so.1 -> libsmb2.so.6.1.0
│ ├── libsmb2.so -> libsmb2.so.1
│ └── cmake/libsmb2/
└── lib/pkgconfig/
第二步:配置 CMake
在 entry/src/main/cpp/CMakeLists.txt 中添加:
cmake
cmake_minimum_required(VERSION 3.4.1)
project(libsmb2project)
# 应用主库,包含 NAPI 绑定
add_library(entry SHARED napi_init.cpp)
# 链接 NAPI 运行时和日志库
target_link_libraries(entry PUBLIC libace_napi.z.so libhilog_ndk.z.so)
# 引入 libsmb2 预编译库
set(LIBSMB2_DIR ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/libsmb2/${OHOS_ARCH})
set(LIBSMB2_LIB ${LIBSMB2_DIR}/lib/libsmb2.so.1)
target_link_libraries(entry PRIVATE ${LIBSMB2_LIB})
target_include_directories(entry PRIVATE ${LIBSMB2_DIR}/include)
${OHOS_ARCH} 由 DevEco Studio 构建时自动注入,值为 arm64-v8a。
第三步:配置 ABI 过滤
在 entry/build-profile.json5 中指定只构建 arm64-v8a:
json5
{
externalNativeOptions: {
path: './src/main/cpp/CMakeLists.txt',
abiFilters: ['arm64-v8a'],
arguments: '',
cppFlags: '',
},
}
第四步:编写 NAPI 绑定
entry/src/main/cpp/napi_init.cpp 是核心绑定文件,将 libsmb2 的 C API 暴露给 ArkTS。
模块注册
cpp
#include <napi/native_api.h>
static napi_module entryModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = RegisterEntryModule, // 你的注册函数
.nm_modname = "entry",
.nm_priv = nullptr,
.reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
napi_module_register(&entryModule);
}
__attribute__((constructor)) 保证共享库加载时自动执行注册。
全局状态管理
Native 层使用全局变量维护单一连接:
cpp
static smb2_context* g_smb2_ctx = nullptr;
static smb2dir* g_smb2_dir = nullptr;
static smb2fh* g_smb2_fh = nullptr;
同一时间只允许一个 SMB 连接,打开新文件时自动关闭旧的文件句柄。
导出函数示例
每个 NAPI 函数都是 libsmb2 C API 的薄封装,负责 napi_value 与 C 类型之间的转换:
cpp
// 初始化上下文
static napi_value InitContext(napi_env env, napi_callback_info info)
{
// ...
g_smb2_ctx = smb2_init_context();
if (!g_smb2_ctx) {
napi_create_int32(env, -1, &result);
return result;
}
napi_create_int32(env, 0, &result);
return result;
}
// 连接共享
static napi_value ConnectShare(napi_env env, napi_callback_info info)
{
size_t argc = 4;
napi_value args[4];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
char server[256], share[256], user[256], password[256];
// ... 从 napi_value 提取字符串
int ret = smb2_connect_share(g_smb2_ctx, server, share, user, password);
// ...
}
// 读取文件(随机访问)
static napi_value ReadFile(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value args[2];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
int64_t offset, size;
// ... 提取参数
uint8_t* buf = new uint8_t[size];
int n = smb2_pread(g_smb2_ctx, g_smb2_fh, buf, size, offset);
// 将数据拷贝到 ArrayBuffer 返回给 ArkTS
void* data;
napi_create_arraybuffer(env, n, &data, &ab);
memcpy(data, buf, n);
delete[] buf;
return ab;
}
所有导出函数在 RegisterEntryModule 中统一注册:
cpp
static napi_value RegisterEntryModule(napi_env env, napi_value exports)
{
napi_property_descriptor desc[] = {
{ "initContext", nullptr, InitContext, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "destroyContext", nullptr, DestroyContext, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "setUser", nullptr, SetUser, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "setPassword", nullptr, SetPassword, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "setDomain", nullptr, SetDomain, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "setAuthentication", nullptr, SetAuthentication, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "connectShare", nullptr, ConnectShare, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "disconnectShare", nullptr, DisconnectShare, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "openDir", nullptr, OpenDir, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "closeDir", nullptr, CloseDir, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "readDir", nullptr, ReadDir, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "getError", nullptr, GetError, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "openFile", nullptr, OpenFile, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "closeFile", nullptr, CloseFile, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "getFileSize", nullptr, GetFileSize, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "readFile", nullptr, ReadFile, nullptr, nullptr, nullptr, napi_default, nullptr },
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
第五步:声明 TypeScript 类型
在 entry/src/main/cpp/types/libentry/index.d.ts 中声明类型,让 ArkTS 能获得完整的类型提示:
typescript
export interface Smb2DirEntry {
name: string
type: number
isDirectory: boolean
}
export const SMB2_SEC_UNDEFINED: number
export const SMB2_SEC_NTLMSSP: number
export const SMB2_SEC_KRB5: number
export function initContext(): number
export function destroyContext(): number
export function setUser(user: string): number
export function setPassword(password: string): number
export function setDomain(domain: string): number
export function setAuthentication(method: number): number
export function connectShare(
server: string,
share: string,
user: string,
password: string
): number
export function disconnectShare(): number
export function openDir(path: string): number
export function closeDir(): number
export function readDir(): Smb2DirEntry[]
export function getError(): string
export function openFile(path: string): number
export function closeFile(): number
export function getFileSize(): number
export function readFile(offset: number, size: number): ArrayBuffer
对应的 oh-package.json5:
json5
{
name: 'libentry.so',
types: './index.d.ts',
version: '1.0.0',
}
第六步:ArkTS 封装层
在 ArkTS 侧通过 import libentry from 'libentry.so' 导入原生模块,封装为易用的类。
Smb2Client
typescript
import libentry from 'libentry.so'
export class Smb2Client {
private connected: boolean = false
init(): number {
return libentry.initContext()
}
destroy(): number {
this.connected = false
return libentry.destroyContext()
}
setUser(user: string): number {
return libentry.setUser(user)
}
setPassword(password: string): number {
return libentry.setPassword(password)
}
setDomain(domain: string): number {
return libentry.setDomain(domain)
}
setAuthentication(method: number): number {
return libentry.setAuthentication(method)
}
connect(
server: string,
share: string,
user: string,
password: string
): number {
const ret = libentry.connectShare(server, share, user, password)
if (ret === 0) {
this.connected = true
}
return ret
}
disconnect(): number {
this.connected = false
return libentry.disconnectShare()
}
openDir(path: string): number {
return libentry.openDir(path)
}
closeDir(): number {
return libentry.closeDir()
}
readDir(): libentry.Smb2DirEntry[] {
return libentry.readDir()
}
getError(): string {
return libentry.getError()
}
openFile(path: string): number {
return libentry.openFile(path)
}
closeFile(): number {
return libentry.closeFile()
}
getFileSize(): number {
return libentry.getFileSize()
}
readFile(offset: number, size: number): ArrayBuffer {
return libentry.readFile(offset, size)
}
}
SmbFileCache(文件缓存下载)
SMB 文件不能直接作为视频播放器的数据源,需要先下载到本地缓存。SmbFileCache 通过分块读取 + 并发写入实现高效下载:
typescript
export class SmbFileCache {
private client: Smb2Client
private readonly CHUNK_SIZE = 4 * 1024 * 1024 // 4MB
async download(
remotePath: string,
localPath: string,
onProgress?: (progress: number, speed: string) => void
): Promise<string> {
// 1. 打开远程文件
this.client.openFile(remotePath)
const totalSize = this.client.getFileSize()
// 2. 分块读取,通过 ConcurrentFileDownloader 写入本地文件
let downloaded = 0
let startTime = Date.now()
while (downloaded < totalSize) {
const chunkSize = Math.min(this.CHUNK_SIZE, totalSize - downloaded)
const data = this.client.readFile(downloaded, chunkSize)
// 并发写入磁盘(不阻塞主线程)
await ConcurrentFileDownloader.writeChunk(localPath, downloaded, data)
downloaded += chunkSize
// 回调进度
if (onProgress) {
const elapsed = (Date.now() - startTime) / 1000
const speed = downloaded / elapsed / 1024
onProgress(downloaded / totalSize, `${speed.toFixed(1)} KB/s`)
}
}
// 3. 关闭文件句柄
this.client.closeFile()
return localPath
}
}
第七步:UI 层集成
SmbEntry 类型
目录条目类型与 NAPI 声明一致:
typescript
interface SmbEntry {
name: string
type: number
isDirectory: boolean
}
连接流程
在 pageSmbBrowser.ets 中,页面组件的生命周期管理 SMB 连接:
typescript
@Entry
@Component
struct PageSmbBrowser {
@State client: Smb2Client = new Smb2Client();
@State entries: SmbEntry[] = [];
@State currentPath: string = '/';
@State downloadProgress: number = 0;
@State downloadSpeed: string = '';
@State isDownloading: boolean = false;
aboutToAppear() {
this.client.init();
this.client.setUser('admin');
this.client.setPassword('123456');
this.client.setDomain('WORKGROUP');
const ret = this.client.connect('192.168.1.100', 'Public', 'admin', '123456');
if (ret === 0) {
this.loadDirectory('/');
}
}
aboutToDisappear() {
this.client.disconnect();
this.client.destroy();
}
loadDirectory(path: string) {
const ret = this.client.openDir(path);
if (ret === 0) {
const raw = this.client.readDir();
// 过滤 . 和 ..,目录优先排序
this.entries = raw
.filter(e => e.name !== '.' && e.name !== '..')
.sort((a, b) => {
if (a.isDirectory && !b.isDirectory) return -1;
if (!a.isDirectory && b.isDirectory) return 1;
return a.name.localeCompare(b.name);
});
this.client.closeDir();
}
}
}
文件操作
typescript
// 点击目录:进入子目录
onDirClick(entry: SmbEntry) {
const newPath = this.currentPath === '/' ? `/${entry.name}` : `${this.currentPath}/${entry.name}`;
this.currentPath = newPath;
this.loadDirectory(newPath);
}
// 点击媒体文件:下载到缓存后播放
onMediaClick(entry: SmbEntry) {
const remotePath = this.currentPath === '/' ? `/${entry.name}` : `${this.currentPath}/${entry.name}`;
const localPath = `/data/storage/el2/base/cache/smb/${entry.name}`;
this.isDownloading = true;
const cache = new SmbFileCache(this.client);
cache.download(remotePath, localPath, (progress, speed) => {
this.downloadProgress = progress;
this.downloadSpeed = speed;
}).then((path) => {
// 播放本地缓存文件(调用导航跳转到播放器页面)
this.isDownloading = false;
// pushPath({ name: PageName.videoPlayer, param: { uri: path } });
});
}
// 返回上级目录
onBack() {
if (this.currentPath === '/') return;
const parts = this.currentPath.split('/').filter(p => p.length > 0);
parts.pop();
this.currentPath = parts.length > 0 ? '/' + parts.join('/') : '/';
this.loadDirectory(this.currentPath);
}
API 列表
集成的 16 个 NAPI 函数覆盖了 SMB 文件浏览与读取的完整流程:
| 函数 | 对应 libsmb2 API | 说明 |
|---|---|---|
initContext() |
smb2_init_context() |
创建 SMB 上下文 |
destroyContext() |
smb2_destroy_context() |
销毁上下文 |
setUser(user) |
smb2_set_user() |
设置用户名 |
setPassword(password) |
smb2_set_password() |
设置密码 |
setDomain(domain) |
smb2_set_domain() |
设置域名 |
setAuthentication(method) |
smb2_set_authentication() |
认证方式(0=自动, 1=NTLM, 2=Kerberos) |
connectShare(server, share, user, password) |
smb2_connect_share() |
连接 SMB 共享 |
disconnectShare() |
smb2_disconnect_share() |
断开连接 |
openDir(path) |
smb2_opendir() |
打开远程目录 |
closeDir() |
smb2_closedir() |
关闭目录句柄 |
readDir() |
smb2_readdir() 循环至 NULL |
读取全部目录条目 |
getError() |
smb2_get_error() |
获取最近一次错误信息 |
openFile(path) |
smb2_open(path, O_RDONLY) |
打开文件(只读) |
closeFile() |
smb2_close() |
关闭文件句柄 |
getFileSize() |
smb2_fstat() |
获取文件大小 |
readFile(offset, size) |
smb2_pread() |
按偏移量读取指定字节,返回 ArrayBuffer |
关键注意事项
-
单一连接限制 :Native 层使用全局变量
g_smb2_ctx、g_smb2_dir、g_smb2_fh,同一时间只能维持一个 SMB 连接、一个目录句柄、一个文件句柄。对于单页面浏览场景足够,如需并发连接需重构为句柄池模式。 -
只读文件访问 :
openFile固定使用O_RDONLY标志,不支持写操作。 -
同步 API :绑定只使用了 libsmb2 的同步接口(
smb2_connect_share、smb2_pread等),未接入 libsmb2 的异步事件循环(smb2_get_fd/smb2_service)。在鸿蒙的TaskPool中执行可避免阻塞 UI 主线程。 -
权限声明 :在
module.json5中需要声明ohos.permission.INTERNET和ohos.permission.GET_NETWORK_INFO。 -
ABI 支持 :当前仅构建
arm64-v8a。如需支持armeabi-v7a或x86_64,需交叉编译对应架构的 libsmb2 并添加到abiFilters中。
参考文档
参考文档:参考文档