草庐IT

2021-01-20 wayland 协议的实现

LinuxDE 2023-09-28 原文

如何扩展 wayland 协议

为了能够扩展 wayland 协议,首先需要理解 wayland 协议,并且知道怎么样在server和client端实现协议中定义的接口。看了一堆文档,试着按照自己的理解来整理文档,并动手写简单的代码来加深理解。【希望一个月之后再读这篇文章不会觉得是一坨shit】

wayland 协议是什么

wayland核心协议是一个 xml 文件,如果我们安装了 wayland 开发包,这个文件在一般在系统的 /usr/share/wayland/wayland.xml。核心协议的内容有限,不满足我们平常对窗口的一些操作,所以为了实现一些窗口管理的功能,还有很多扩展的协议,比如 xdg-shell 就是为了实现桌面窗口而扩展的协议。协议有稳定版本和不稳定版本,在这篇文档中我们主要看 /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml,这是一个稳定的版本,它的 xdg_surface 对象有一个 request 接口是 set_window_geometry,从注释来看,如果实现这个接口,应该能满足我们的需求。

可以通过 wayland-scanner 工具解析这个 xml 生成Server、Client的头文件以及 glue code一个 C 文件。


wayland-scanner server-header < xdg-shell.xml > xdg_shell_server_header.h

wayland-scanner client-header < xdg-shell.xml > xdg_shell_client_header.h

wayland-scanner private-code < xdg-shell.xml > xdg_shell_protocol.c

wayland 协议生成 gluecode.png

上面这张图是网上找到的,开发的时候基本上就是这么个流程。写一个server.c 文件,用到生成的 server-protocol.h 和 protocol.c ,编译命令:

gcc -o server server.c xdg_shell_protocol.c -lwayland-server

编译Client程序:

gcc -o xdg_client xdg_client.c xdg_shell_protocol.c -lwayland-client

理解 wayland 协议

wayland 协议其实就是我们预先定义好一个 object 的接口,包括它的 request 和 event,Server实现 request 接口,Client实现对 event 的监听和响应。当Client把对这个对象的 request 封装成消息发给Server,Server收到消息后,根据对象id的和操作码执行对应的响应函数;event 也是类似的流程,Server把对这个对象的 event 发到了Client,Client会作出响应。

在Server和Client之间,对象是一一对应的,互相知道这个对象的状态。Client是 wl_proxy,与之对应的,在Server就会有一个 wl_resource。Server程序需要知道这个 resource 属于哪个Client程序。这个对象之间映射就是 wayland 的 wl_map 来维护的。


struct wl_map {

  struct wl_array  client_entries;

  struct wl_array server_entries;

  uint32_t side;

  uint32_t free_list;

};

side 分WL_MAP_CLIENT_SIDE 和 WL_MAP_SERVER_SIDE两种,表明当前map保存的是Client还是 Server对象。

wayland Server运行之后,一般会有多个 wayland Client程序连接。每当Client程序调用 wl_display_connect 连接到wayland, 对应就会为它维护一个 wl_map结构:display->objects[代码wayland-client.c ];如果Server监听到Client连接,在 wl_client_create 的时候,也会为之创建一个 wl_map:client->objects[代码wayland-server.c]。

在映射表中,Client proxy 对象从 0 开始,Server resource 对象从 0xff000000 开始存放,display->objects只用了 client_entries,client->objects 只用了 server_entries。wl_map在Client和Server端各有一个,它们分别存了wl_proxy和wl_resource的数组,且是一一对应的。这些对象在这个数组中的索引作为它们的id。这样,参数中的对象只要传id,这个id被传到目的地后会通过查找这个wl_map表来得到本地相应的对象。

映射表创建后,就可以插入数据了。比如我们客户端创建wl_proxy,设置interface等信息,然后将该wl_proxy插入到display->objects的wl_map中,返回值为id,其实就是在wl_map中数组中的索引值。这个值是会被发到Server端的,这样Server端就可以创建 wl_resource,并把它插入到Server端的wl_map数组的相同索引值的位置。这样逻辑上,就创建了wl_proxy和wl_resource的映射关系。以后,Client和Server间要相互引用对象只要传这个id就可以了。


wayland.png

上面这张图被多次引用,它很清楚的描述了Server和Client之间的对象映射和事件调用。

object

为了Server和Client之间的通信,第一步就是创建对象,wayland中默认第一个对象就是 wl_display,object id 为1,这个对象之外的所有其他对象,都是需要通信来创建的。

在 wayland 中,wl_display 是第一个对象,wl_registry 是第二个对象,因为有了 wl_registry 之后,我们才能注册绑定其他所有的 global object。在运行Server程序前,开启 WAYLAND_DEBUG=1,可以看到Client连接后,Server的 debug 如下:

[1008582.564] wl_display@1.get_registry(new id wl_registry@2)

[1008582.581] -> wl_registry@2.global(1, "wl_output", 1)

[1008582.654] wl_registry@2.bind(1, "wl_output", 1, new id [unknown]@4)

request 和 event

在生成的 glue code代码中,主要定义了一些对象,以及这写对象的request和event接口。Server头文件中定义 request 的接口结构,因为Server需要响应Client请求,所以我们在开发Server程序时需要实现 request接口;在Client头文件中定义 event 的 listener,因为Client需要监听Server传过来的事件,执行对应的回调。所以在开发Client程序时,需要去 add_listener,实现收到事件之后要做的工作。

以 xdg_surface 为例,它在生成的xdg_shell_protocol.c 文件中定义如下:


WL_PRIVATE const struct wl_interface xdg_surface_interface = {

        "xdg_surface", 3,

        5, xdg_surface_requests,

        1, xdg_surface_events,

};

这个 xdg_surface_interface 是一个全局变量,数据类型是 wl_interface。这个结构中成员组成:name、version、request个数、request签名、event个数以及event签名。也就是说 xdg_surface 这个对象有 5 个请求和 1 个事件。

request 和 event 签名是 wl_message 结构的,不管是 request 和 event 都会被封装成 MESSAGE 在 server 和 client 之间传递。


static const struct wl_message xdg_surface_requests[] = {

        { "destroy", "", xdg_shell_types + 0 },

        { "get_toplevel", "n", xdg_shell_types + 7 },

        { "get_popup", "n?oo", xdg_shell_types + 8 },

        { "**set_window_geometry**", "iiii", xdg_shell_types + 0 },

        { "ack_configure", "u", xdg_shell_types + 0 },

};

message

当Client程序拿到一个对象了,就可以给Server发对这个对象的request,Server收到这个请求去执行对应的工作。比如Client的窗口中有个entry,我们输入文字时,窗口内容需要更新,那么就可以在Client调用 wl_surface_damage 告知Server这块区域无效需要重新绘制了,其实 wl_surface_damage 这个函数其实就是把就是wl_surface 提供的一个 request“damage”封装成一条操作消息,通过 socket从Client发到Server,Server收到了这条消息,解析message,找到操作码去执行对应的操作;同样Server也会给Client发event,Client监听到event去执行对应的回调。

对于一条 message,我们需要了解它的基本结构:

object id + messeage size + opcode + 其他各个参数组成

其中 opcode 就是我们请求或者事件的操作码,这个码其实是由协议文件 xml 中它的出现顺序决定的(从0开始计数),比如 damage 在 wl_surface_request 中是第三个,它的 opcode 就是 2,下面这张图来自 wayland protocol book:

2021-01-16_13-25.png

其实我想看 set_window_geometry 的,但是不知道怎么样打印出上面的数据。xdg_surface 的 set_window_geometry 是第4个,所以它的 opcode 应该是 3,在生成的客户端头文件中定义:#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3

协议消息打包

Client请求

接下来以我们最关心的xdg-shell 扩展协议中的 xdg_surface_set_window_geometry 为例来介绍消息是如何从Client发到Server的。这个函数是 wayland-scanner 扫描协议文件xdg-shell-unstable-v5.xml,为xdg_surface 的请求 set_window_geometry 自动生成的供Client使用的函数,函数里面只是简单地执行了下面这个语句:

wl_proxy_marshal((struct wl_proxy *) xdg_surface, XDG_SURFACE_SET_WINDOW_GEOMETRY, x, y, width, height);

wl_proxy_marshal 在我们这几个生成文件中找不到定义,它是 wayland 库提供的,需要在 wayland 代码中去找,src/wayland-client.c 文件中,通过层层调用,在函数 wl_proxy_marshal_array_constructor_versioned 中开始构建 message 并发送:

构建 wl_closure 消息:

closure = wl_closure_marshal(&proxy->object, opcode, args, message);

发送消息:

wl_closure_send(closure, proxy->display->connection)

Server和Client的通信通过 socket 来实现,由于这部分跟我们扩展协议暂时没什么关系,所以没有深入去看,感兴趣可以自己去看代码。附录简单介绍了通信机制。

Server事件

流程其实差不多,只不过Server的函数是 post_event。比如创建对象时发出的global信号:

wl_resource_post_event(resource,
       WL_REGISTRY_GLOBAL,
       global->name,
       global->interface->name,
       global->version);

协议消息解包

消息解包就是把上面的 marshal 过程再 demarshal,因为在打包消息的时候知道 object id,interface的接口签名以及 opcode,这样就能根据这几个信息从 interface 中解析得到参数格式,从而把一整条消息解析出来。比如 set_window_geometry,从接口定义中找到它的 wl_message 格式:

{ "set_window_geometry", "iiii", xdg_shell_unstable_v5_types + 0 },

这是一条 wl_message 结构的数据:


struct wl_message {

    /** Message name */

    const char *name;

    /** Message signature */

    const char *signature;

    /** Object argument interfaces */

    const struct wl_interface **types;

};

其中消息签名是”iiii”表示这个 request 的参数是四个整形数据。第三个成员 types,需要从数组 xdg_shell_unstable_v5_types 中去找,这里加 0 表示数组第一条数据。这个请求不需要创建新的对象,所以签名为空。如果需要创建新对象,消息签名中会有“n”,表示 new id,types 就是这个新对象的接口定义。比如 wl_display_get_registry 需要返回 wl_registry 对象,Server和Client需要为这个新对象达成共识,好为后面的request、event传递打下基础,所以需要提前定好 interface,它的这个 types 就是 &wl_registry_interface。// { "get_registry", "n", wayland_types + 9 },

回到 set_window_geometry,协议中定义了这个请求,Client把这个请求组成 message 发送给Server。Server socket 监听机制监听到这条消息后,就会执行 socket 的回调函数 wl_client_connection_data[见附录说明]。这个函数里面反序列化消息,得到 wl_closure,找到目标对象对应的接口函数,利用 libffi 执行 server 端的 implementation 函数。

如何找到目标对象的接口函数?这就需要 server 端来实现了。wayland server 和 client 之间的对象是一一对应的,对于每个 object,它的 interface 定义,有几个 request,有几个 event,以及各自的参数是什么,它们相互之间都很清楚。所以每当Client申请 bind 一个对象,Server需要创建一个资源与之对应,如果这个对象有 request 需要实现,Server就需要去实现这些函数接口并且 set_implementation。

Server代码简述

为了响应Client的请求,Server需要创建对应的对象。首先我们需要去查协议中对象的接口定义,再去实现对象接口中定义的request。代码片段1:


display = wl_display_create ();

wl_display_add_socket_auto (display);

wl_global_create (display, &wl_compositor_interface, 3, NULL, &compositor_bind);

wl_global_create (display, &wl_shell_interface, 1, NULL, &shell_bind);

wl_global_create (display, &xdg_wm_base_interface, 1, NULL, &xdg_wm_base_bind);

wl_global_create (display, &wl_seat_interface, 1, NULL, &seat_bind);

每当Server调用 wl_global_create 创建一个对象,就会将 global 事件打包发给Client,Client收到 global 事件,在回调函数中需要 bind 这个对象。上面创建 wl_compositor 对象时,传递了 &compositor_bind 函数指针,也就是说,如果Client执行 wl_registry_bind 绑定 wl_compositor对象,Server就会执行这个 compositor_bind。在绑定的时候,Server创建与Client对象相对应的资源,并且设置它的实现函数。比如 wl_compositor,它的 request 有两个:


static const struct wl_message wl_compositor_requests[] = {
        { "create_surface", "n", wayland_types + 10 },
        { "create_region", "n", wayland_types + 11 },
};

那么Server代码中就需要定义这两个请求对应的处理函数:


static struct wl_compositor_interface compositor_implementation =

{
    &compositor_create_surface,
    &compositor_create_region
};

在 compositor_bind 中,主要做这两步工作:


struct wl_resource *resource = wl_resource_create (client, &wl_compositor_interface, 1, id);
wl_resource_set_implementation (resource, &compositor_implementation, NULL, NULL);

如果我们定义的 compositor_create_surface没有问题,Client调用 wl_compositor_create_surface 函数的时候,Server就能执行到 compositor_create_surface,从而打通从Client到Server的请求流程。

但是,由于 wl_compositor,wl_surface 这些对象要实现的接口太太多了,所以在测试代码中,这些对象的 request 我就只定义了空的函数体,保证Client请求能拿到对象,但是没有实际工作。代码片段如下:


static const struct **wl_output_interface** wl_output_implementation = {

   .release = wl_output_handle_release,

};

在绑定对象时,设置 request 接口的实现


struct wl_resource *resource = wl_resource_create (client, &wl_output_implementation, wl_output_interface.version, id);

wl_resource_set_implementation(resource, &wl_output_implementation, client_output, wl_output_handle_resource_destroy);

wayland 中有两个 wl_output_interface,千万不要混淆了。其中一个是wl_interface 类型的变量,指明了 name version request 和 event,在生成的glue code文件中定义:


WL_PRIVATE const struct wl_interface ***wl_output_interface*** = {

        "wl_output", 3,

        1, wl_output_requests,

        4, wl_output_events,

};

另一个是结构体,成员是待实现的函数指针,在生成的server 头文件中定义:


struct ***wl_output_interface*** {

        void (*release)(struct wl_client *client,

                        struct wl_resource *resource);

};

每个对象都会有这两个让人迷糊的 wl_xxx_interface,一定要注意区分。结构体 wl_output_interface 里面的函数指针就对应是这个对象需要实现的 request。

实现request

qtwayland 无法设置 Client 窗口的坐标,理论上来说,可能是下面两种原因:

1、客户端程序的 set_window_geometry 没有给 wayland Server 发请求

2、虽然客户端发了请求,但是 wayland Server 端实际上没有实现它。

很多现有的合成器都没有实现 set_window_geometry 接口,因为 wayland 设计理念就是这样。它明确表示不希望客户端程序自己设置坐标,而是觉得客户端的坐标应该由合成器去做决定。

我找到了一个非常非常简单的开源合成器代码,它没有实现 xdg-shell 的 set_window_geometry 请求。一开始我在运行 weston-flower 客户程序时,不管怎么调用 xdg_surface_set_window_geometry尝试设置坐标都无效,一直都显示在位置0,0.
补充:开源代码地址:https://github.com/eyelash/tutorials.git

通过修改合成器代码,实现 xdg-shell 的协议来达到设置客户端窗口的目标。

首先我们需要维护资源的状态,可以定义一个结构体,维护当前 xdg_surface,当前坐标 x,y 等。

第一步:在创建对象时,绑定我们的实现接口:


struct surface *surface = wl_resource_get_user_data (_surface);

surface->xdg_surface = wl_resource_create (client, &xdg_surface_interface, 1, id);

wl_resource_set_implementation (surface->xdg_surface, &xdg_surface_implementation, surface, NULL);

这个surface 就是自定义的结构体,把它设置为服务端资源的user_data:resource->data = surface,当接收到客户端请求的时候,这个数据会作为 wl_resource 的参数一起传递过来,用于为维护处于不断变化中的资源的状态。

第二步:定义各个 request 实现:


static struct xdg_surface_interface xdg_surface_implementation = {
        &xdg_surface_destroy,
        &xdg_surface_get_toplevel,
        &xdg_surface_get_popup,
        &xdg_surface_set_window_geometry,
        &xdg_surface_ack_configure
};

第三步:实际的实现接口:


static void xdg_surface_set_window_geometry (struct wl_client *client, struct wl_resource *resource,
                                                          int32_t x, int32_t y, int32_t width, int32_t height)
{
        struct surface *surface = wl_resource_get_user_data (resource);

        surface->x = x;
        surface->y = y;
        redraw_needed = 1;
}

由于我们想要更新服务端 resource 的位置,首先需要拿到服务端维护的xdg_surface,在绑定实现接口时,传入的 surface 参数,可以取出来。redraw_needed 是为了触发下一帧画面时重新渲染客户端窗口,要不然不会更新。

附录

wayland通信

在开发 wayland Client程序时,第一步工作就是连接到wayland Server拿到资源对象,一般是wl_display_connect 再wl_display_get_registry,拿到 wl_registry 对象后,就会监听 global 信号,通过调用 wl_registry_add_listener 来监听,注册信号回调函数。

Server会把可用对象挨个发出 global 信号,Client程序在 global 信号回调函数中就可以 wl_registry_bind 这些对象,生成Client的可用对象。[参考 weston 的客户程序代码 window.c] 但是信号机制一般是对同一个进程来说的,我们可以监听某个对象的某个信号,当收到信号时执行对应的回调函数;而这里其实是两个程序,Server和Client,这种跨进程的信号不是简单地 connect 就可以的,而是需要通过 socket 来传递。 要让两个进程通过 socket 进行函数调用,首先需要将调用抽象成数据流的形式,这些信息通过 wl_closure_marshal 写入 wl_closure 结构,再由 serialize_closure 变成数据流;等到了目标进程,从数据流中通过 wl_connection_demarshal 转回 wl_closure 结构。

1、wayland server 启动时会创建一个 socket(_wl_display_add_socket),并将这个 socket fd 加入 epoll 中,这样一旦有有Client程序连接,epoll 就会通知Server,从而执行回调函数 socket_data。Client可能会有很多,用 epoll 会比较有效率。

2、当有Client程序通过调用wl_display_connect 连接到 server, 在回调函数 socket_data 中会调用 accept 创建新的 socket fd,紧接着创建 wl_client并在创建 wl_client 的时候,wl_client_create (display, fd) 将这个 socket fd 加入 epoll,继续监听新的 socket,为的是响应从Client发过来的请求,回调函数是 wl_client_connect_data。

一个 socket 负责监听Client连接,对于每个Client还有一个socket负责监听Client的请求。

有关2021-01-20 wayland 协议的实现的更多相关文章

  1. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  2. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  3. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  4. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  5. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  6. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  7. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  8. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

  9. ruby - "public/protected/private"方法是如何实现的,我该如何模拟它? - 2

    在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定

  10. ruby - 实现k最近邻需要哪些数据? - 2

    我目前有一个reddit克隆类型的网站。我正在尝试根据我的用户之前喜欢的帖子推荐帖子。看起来K最近邻或k均值是执行此操作的最佳方法。我似乎无法理解如何实际实现它。我看过一些数学公式(例如k表示维基百科页面),但它们对我来说并没有真正意义。有人可以推荐一些伪代码,或者可以查看的地方,以便我更好地了解如何执行此操作吗? 最佳答案 K最近邻(又名KNN)是一种分类算法。基本上,您采用包含N个项目的训练组并对它们进行分类。如何对它们进行分类完全取决于您的数据,以及您认为该数据的重要分类特征是什么。在您的示例中,这可能是帖子类别、谁发布了该项

随机推荐