学习open62541 --- [79] 在docker中运行open62541工程

发布于:2025-07-10 ⋅ 阅读:(23) ⋅ 点赞:(0)

docker是非常流行的容器技术,解决了部署环境不一致的问题,open62541的工程也可以在docker容器中运行,本文讲述如何把open62541工程放到docker容器中运行。

本文使用WSL ubuntu 22.04作为宿主环境,其它linux也是一样。


一 拉取debian镜像

执行docker命令拉取debian镜像

docker pull debian:bookworm

这个debian镜像创建的容器非常干净,啥都没有,都需要自己安装,如果我想创建多个容器,那就要安装多次,所以这里基于debian:bookworm来制作自定义镜像。


二 编写Dockerfile

通过Dockerfile来制作自定义镜像,

# 使用 debian:bookworm 作为基础镜像
FROM debian:bookworm

# 设置环境变量以避免交互式配置提示
ENV DEBIAN_FRONTEND=noninteractive

# 确保 /etc/apt/sources.list 文件存在,并使用国内镜像源(以阿里云为例)
RUN if [ ! -f "/etc/apt/sources.list" ]; then echo "deb http://mirrors.aliyun.com/debian/ bookworm main non-free contrib" > /etc/apt/sources.list && \
    echo "deb-src http://mirrors.aliyun.com/debian/ bookworm main non-free contrib" >> /etc/apt/sources.list && \
    echo "deb http://mirrors.aliyun.com/debian/ bookworm-updates main non-free contrib" >> /etc/apt/sources.list && \
    echo "deb-src http://mirrors.aliyun.com/debian/ bookworm-updates main non-free contrib" >> /etc/apt/sources.list; fi

# 更新包列表并安装常用开发工具
RUN apt-get update && \
    apt-get install -y \
        git \
        curl \
        wget \
        cmake \
        vim \
        sudo \
        build-essential \
        python3 \
        python-is-python3 \
        python3-pip \
        python3.11-venv \
        openssh-client \
        net-tools \
        iproute2 \
        iputils-ping \
        dnsutils \
        ca-certificates \
        gnupg \
        lsb-release \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# 创建虚拟环境目录
RUN python3 -m venv /opt/venv

# 设置虚拟环境为当前环境
ENV PATH="/opt/venv/bin:$PATH"

# 设置容器启动时默认执行的命令
CMD ["bash"]

然后运行docker命令来制作镜像,

docker build -t myimage:0.0.1 .

执行完毕后,运行docker images来查看当前的本地镜像,
![[Pasted image 20250707215627.png]]

这个镜像可以推送到docker hub或者其他仓库里,然后分享给别人使用,这样就保证开发环境是一样的了。


三 创建open62541工程

在WSL ubuntu下创建一个简单的open62541工程,工程结构如下,包含server和client
![[Pasted image 20250708210916.png]]

open62541使用的版本是v1.4.12,
client.c内容如下,

// client.c,功能主要是从server那里获取时间
#include <stdlib.h>
#include "open62541.h"

int main(void) 
{
    UA_Client *client = UA_Client_new();
    UA_ClientConfig_setDefault(UA_Client_getConfig(client));
    UA_StatusCode retval = UA_Client_connect(client, "opc.tcp://localhost:4840");
    if(retval != UA_STATUSCODE_GOOD) {
        UA_Client_delete(client);
        return (int)retval;
    }
    
    /* Read the value attribute of the node. UA_Client_readValueAttribute is a
    * wrapper for the raw read service available as UA_Client_Service_read. */
    UA_Variant value; /* Variants can hold scalar values and arrays of any type */
    UA_Variant_init(&value);
    
    /* NodeId of the variable holding the current time */
    const UA_NodeId nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
    retval = UA_Client_readValueAttribute(client, nodeId, &value);
    
    if(retval == UA_STATUSCODE_GOOD && UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_DATETIME])) 
    {
        UA_DateTime raw_date = *(UA_DateTime *) value.data;
        UA_DateTimeStruct dts = UA_DateTime_toStruct(raw_date);
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "date is: %u-%u-%u %u:%u:%u.%03u\n",
            dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec);
    }
    
    /* Clean up */
    UA_Variant_clear(&value);
    UA_Client_delete(client); /* Disconnects the client internally */
    
    return EXIT_SUCCESS;
}

server.c内容如下,

// server.c
#include "open62541.h"

#include <signal.h>
#include <stdlib.h>

UA_Boolean running = true;

static void stopHandler(int sign) {
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
    running = false;
}

int main(void) 
{
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

    UA_Server *server = UA_Server_new();
    UA_ServerConfig_setDefault(UA_Server_getConfig(server));
    UA_StatusCode retval = UA_Server_run(server, &running);
    
    UA_Server_delete(server);
    
    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}


CMakeLists.txt内容如下,

cmake_minimum_required(VERSION 3.10)

project(demo)

add_definitions(-std=c99)

add_library(open62541 
    open62541/open62541.c
    open62541/open62541.h
)

add_executable(client src/client.c)
target_include_directories(client PRIVATE open62541)
target_link_libraries(client open62541 pthread)


add_executable(server src/server.c)
target_include_directories(server PRIVATE open62541)
target_link_libraries(server open62541 pthread)

最后,在工程根目录下添加.devcontainer.json,内容如下,

{
    "image":"myimage:0.0.1"
}

这个用于指导使用哪个镜像来生成容器。


四 在容器中运行工程

使用VSCode打开工程,然后Ctrl+Shift+p打开命令界面,执行“Dev Containers: Reopen in Container”
![[Pasted image 20250708221118.png]]

这个执行后会通过镜像创建容器,然后在容器里打开本工程,最后VScode左下角会变成下面这样,
![[Pasted image 20250709210120.png]]

表示已经在容器里打开了,默认是root用户。

工程在容器中的目录是/workspaces/prj_001/

在工程根目录下创建build目录,然后cd进入后执行下面命令进行编译,

cmake .. && make

这里需要创建终端执行命令,有2种方式,

  1. 使用VSCode的终端,因为VSCode已经和容器连接成功了
  2. 在WSL linux下打开终端,然后输入docker ps来查看容器id,最后执行docker exec -it 容器id bash,就可以进入容器里了

编译成功后运行server,默认端口是4840,VSCode里会自动转发,
![[Pasted image 20250709210954.png]]

这样我们在容器外面使用UaExpert也可以访问,非常方便,
![[Pasted image 20250709211035.png]]

这里解释一下为什么使用Reopen in container:因为可以把工程放在宿主机里,如果容器挂了,那么重新开个容器就可以了,如果把工程放到容器里,那么就存在一定的风险了。

还有一点,工程在容器里打开后,里面的文件权限都变成root的了,因为容器默认的用户就是root,当我们在宿主机里想修改工程文件时,就会提示权限不够,解决办法是在工程根目录下,执行下面的命令,把文件的owner再变回来,

sudo chown -R user_name .

user_name就是宿主机的登录用户名。


网站公告

今日签到

点亮在社区的每一天
去签到