From 9def287dd9d1e57507ac2626daa249632d6439a3 Mon Sep 17 00:00:00 2001 From: donneyluck Date: Fri, 18 Oct 2024 10:24:10 +0800 Subject: [PATCH] fix --- archive.html | 3 +- .../20230917T140000--server-notes__notes.org | 919 ------------------ posts/20240826T174745--log-level__notes.org | 33 - .../20240826T175853--emacs-themes__emacs.org | 277 ------ ...0240826T180231--manjaro-install__notes.org | 81 -- posts/20240924T094433--gitworkflow__notes.org | 70 -- ...74\217\347\274\226\347\250\213__notes.org" | 549 ----------- ...5\225\344\272\244\344\272\222__notes.html" | 372 +++++++ ...75\225\344\272\244\344\272\222__notes.org" | 379 -------- 9 files changed, 374 insertions(+), 2309 deletions(-) delete mode 100644 posts/20230917T140000--server-notes__notes.org delete mode 100644 posts/20240826T174745--log-level__notes.org delete mode 100644 posts/20240826T175853--emacs-themes__emacs.org delete mode 100644 posts/20240826T180231--manjaro-install__notes.org delete mode 100644 posts/20240924T094433--gitworkflow__notes.org delete mode 100644 "posts/20241017T202935--\346\226\207\345\274\217\347\274\226\347\250\213__notes.org" create mode 100644 "posts/20241018T094654--lua-\345\222\214-c-\345\246\202\344\275\225\344\272\244\344\272\222__notes.html" delete mode 100644 "posts/20241018T094654--lua-\345\222\214-c-\345\246\202\344\275\225\344\272\244\344\272\222__notes.org" diff --git a/archive.html b/archive.html index dd9bf6f..6b9f46d 100644 --- a/archive.html +++ b/archive.html @@ -5,7 +5,8 @@ stuff -
  • [17.10.2024 20:29 CST] 文式编程
    认识并了解什么是文式编程
    (notes)
  • +
  • [18.10.2024 09:46 CST] Lua 和 C 如何交互
    Lua 和 C 交互的教程
    (notes)
  • +
  • [17.10.2024 20:29 CST] 文式编程
    认识并了解什么是文式编程
    (notes)
  • [24.09.2024 09:44 CST] GitWorkFlow
    How to use git collaboration in daily development
    (notes)
  • [26.08.2024 18:02 CST] Manjaro Install
    How to build your linux work environment from scratch
    (notes)
  • [26.08.2024 17:58 CST] Game server interview questions
    Common server interview questions and answers collections very helpful
    (notes)
  • diff --git a/posts/20230917T140000--server-notes__notes.org b/posts/20230917T140000--server-notes__notes.org deleted file mode 100644 index 64f0331..0000000 --- a/posts/20230917T140000--server-notes__notes.org +++ /dev/null @@ -1,919 +0,0 @@ -#+title: Game server interview questions -#+date: [2024-08-26 Mon 17:58] -#+filetags: :notes: -#+description: Common server interview questions and answers collections very helpful -* 游戏服务器面试题合集 -** TCP 的核心意涵是什么? -TCP 是面向连接的可靠的传送协议。核心意涵就是面向连接与可靠,对于使用TCP socket而言我们要管理好socket的建立连接,断开连接等。同时对于业务逻辑而言TCP socket是可靠的不会丢包的,比如我发送ABCDE,这些数据包,不会出现丢包(ABDE)与乱序的情况(ACBED)。 -** 为什么TCP需要封包拆包协议? -应用层发送数据的时候,每次发送会被开发人员认为是一个独立的数据包,可是在底层,由于TCP是可靠的传送协议,每次发送数据都要收到确认。所以底层有可能把应用层的两个数据包合并在一起发送,发送到另外一段的时候,可能一次收到两个应用层的数据包,而我们解析这些数据包的时候需要分成两个,所以我们在发送TCP命令包的时候要用标识能分开这两个数据包。所以就需要我们加一个封包拆包的协议。 -*** TCP 协议传输过程 -TCP 协议是面向流的协议,是流式的,没有业务上的分段,只会根据当前套接字缓冲区的情况进行拆包或者粘包: -[[file:assets/notes/tcp_pack/1.jpg]] -*** TCP 粘包、拆包图解 -由于 TCP 传输协议面向流的,没有消息保护边界。一方发送的多个报文可能会被合并成一个大的报文进行传输,这就是粘包;也可能发送的一个报文,可能会被拆分成多个小报文,这就是拆包。 -下图演示了粘包、拆包的过程,client 分别发送了两个数据包 D1 和 D2 给 server,server 端一次读取到字节数是不确定的,因此可能可能存在以下几种情况: -[[file:assets/notes/tcp_pack/2.png]] - -关于这几种情况说明如下: -server 端分两次读取到了两个独立的数据包,分别是 D1 和 D2,没有粘包和拆包 -server 一次接受到了两个数据包,D1 和 D2 粘合在一起,称之为 TCP 粘包 -server 分两次读取到了数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这称之为 TCP 拆包 -server 分两次读取到了数据包,第一次读取到了 D1 包的部分内容 D1_1,第二次读取到了 D1 包的剩余部分内容 D1_2 和完整的 D2 包。 -由于发送方发送的数据,可能会发生粘包、拆包的情况。这样,对于接收端就难于分辨出来了,因此必须提供科学的机制来解决粘包、拆包问题,这就是协议的作用。 -在介绍协议之前,我们先了解一下粘包、拆包产生的原因。 -*** 粘包、拆包产生的原因 -粘包、拆包问题的产生原因归纳为以下 3 种: -**** socket 缓冲区与滑动窗口 -每个 TCP socket 在内核中都有一个发送缓冲区(SO_SNDBUF )和一个接收缓冲区(SO_RCVBUF),TCP 的全双工的工作模式以及 TCP 的滑动窗口便是依赖于这两个独立的 buffer 的填充状态。 -***** SO_SNDBUF: -进程发送的数据的时候假设调用了一个 send 方法,最简单情况(也是一般情况),将数据拷贝进入 socket 的内核发送缓冲区之中,然后 send 便会在上层返回。 -换句话说,send 返回之时,数据不一定会发送到对端去(和 write 写文件有点类似),send 仅仅是把应用层 buffer 的数据拷贝进 socket 的内核发送 buffer 中。 -***** SO_RCVBUF: -把接受到的数据缓存入内核,应用进程一直没有调用 read 进行读取的话,此数据会一直缓存在相应 socket 的接收缓冲区内。再啰嗦一点,不管进程是否读取 socket,对端 -发来的数据都会经由内核接收并且缓存到 socket 的内核接收缓冲区之中。read 所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的 buffer 里面,仅此而已。 -***** 滑动窗口: -TCP 连接在三次握手的时候,会将自己的窗口大小(window size)发送给对方,其实就是 SO_RCVBUF 指定的值。之后在发送数据的时,发送方必须要先确认接收方的窗口没有被填充满,如果没有填满,则可以发送。 -每次发送数据后,发送方将自己维护的对方的 window size 减小,表示对方的 SO_RCVBUF 可用空间变小。 -当接收方处理开始处理 SO_RCVBUF 中的数据时,会将数据从 socket 在内核中的接受缓冲区读出,此时接收方的 SO_RCVBUF 可用空间变大,即 window size 变大,接受 -方会以 ack 消息的方式将自己最新的 window size 返回给发送方,此时发送方将自己的维护的接受的方的 window size 设置为 ack 消息返回的 window size。 -此外,发送方可以连续的给接受方发送消息,只要保证对方的 SO_RCVBUF 空间可以缓存数据即可,即 window size>0。当接收方的 SO_RCVBUF 被填充满时,此时 -window size=0,发送方不能再继续发送数据,要等待接收方 ack 消息,以获得最新可用的 window size。 -***** 现在来看一下 SO_RCVBUF 和滑动窗口是如何造成粘包、拆包的? -粘包:假设发送方的每 256 bytes 表示一个完整的报文,接收方由于数据处理不及时,这 256 个字节的数据都会被缓存到 SO_RCVBUF 中。如果接收方的 SO_RCVBUF 中缓存了多个报文,那么对于接收方而言,这就是粘包。 -拆包:考虑另外一种情况,假设接收方的 window size 只剩了 128,意味着发送方最多还可以发送 128 字节,而由于发送方的数据大小是 256 字节,因此只能发送前 128 字节,等到接收方 ack 后,才能发送剩余字节。这就造成了拆包。 -**** MSS/MTU 限制 -MTU (Maxitum Transmission Unit,最大传输单元) 是链路层对一次可以发送的最大数据的限制。 -MSS (Maxitum Segment Size,最大分段大小) 是 TCP 报文中 data 部分的最大长度,是传输层对一次可以发送的最大数据的限制。 -要了解 MSS/MTU,首先需要回顾一下 TCP/IP 五层网络模型模型: - -[[file:assets/notes/tcp_pack/3.png]] -数据在传输过程中,每经过一层,都会加上一些额外的信息: -1. 应用层:只关心发送的数据 DATA,将数据写入 socket 在内核中的缓冲区 SO_SNDBUF 即返回,操作系统会将 SO_SNDBUF 中的数据取出来进行发送。 -2. 传输层:会在 DATA 前面加上 TCP Header(20 字节) -3. 网络层:会在 TCP 报文的基础上再添加一个 IP Header,也就是将自己的网络地址加入到报文中。IPv4 中 IP Header 长度是 20 字节,IPV6 中 IP Header 长度是 40 字节。 -4. 链路层:加上 Datalink Header 和 CRC。会将 SMAC(Source Machine,数据发送方的 MAC 地址),DMAC(Destination Machine,数据接受方的 MAC 地址 )和 Type 域加入。SMAC+DMAC+Type+CRC 总长度为 18 字节。 -5. 物理层:进行传输 - -在回顾这个基本内容之后,再来看 MTU 和 MSS。MTU 是以太网传输数据方面的限制,每个以太网帧最大不能超过 1518bytes。 -刨去以太网帧的帧头(DMAC+SMAC+Type 域)14Bytes 和帧尾(CRC 校验)4Bytes,那么剩下承载上层协议的地方也就是 Data 域最大就只能有 1500Bytes 这个值 我们就把它称之为 MTU。 - -MSS 是在 MTU 的基础上减去网络层的 IP Header 和传输层的 TCP Header 的部分,这就是 TCP 协议一次可以发送的实际应用数据的最大大小。 - -MSS = MTU(1500) -IP Header(20 or 40)-TCP Header(20) - -由于 IPV4 和 IPV6 的长度不同,在 IPV4 中,以太网 MSS 可以达到 1460byte;在 IPV6 中,以太网 MSS 可以达到 1440byte。 -发送方发送数据时,当 SO_SNDBUF 中的数据量大于 MSS 时,操作系统会将数据进行拆分,使得每一部分都小于 MSS,也形成了拆包,然后每一部分都加上 TCP Header,构成多个完整的 TCP 报文进行发送,当然经过网络层和数据链路层的时候,还会分别加上相应的内容。 -另外需要注意的是:对于本地回环地址(lookback)不需要走以太网,所以不受到以太网 MTU=1500 的限制。linux 服务器上输入 ifconfig 命令,可以查看不同网卡的 MTU 大小,如下: - -[[file:assets/notes/tcp_pack/4.jpg]] -上图显示了 2 个网卡信息: -eth0 需要走以太网,所以 MTU 是 1500; -lo 是本地回环,不需要走以太网,所以不受 1500 的限制。 -**** Nagle 算法 -TCP/IP 协议中,无论发送多少数据,总是要在数据(DATA)前面加上协议头(TCP Header+IP Header),同时,对方接收到数据,也需要发送 ACK 表示确认。 -即使从键盘输入的一个字符,占用一个字节,可能在传输上造成 41 字节的包,其中包括 1 字节的有用信息和 40 字节的首部数据。这种情况转变成了 4000%的消耗,这样的情况对于重负载的网络来是无法接受的。称之为"糊涂窗口综合征"。 -为了尽可能的利用网络带宽,TCP 总是希望尽可能的发送足够大的数据。(一个连接会设置 MSS 参数,因此,TCP/IP 希望每次都能够以 MSS 尺寸的数据块来发送数据)。Nagle 算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。 -Nagle 算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于 MSS 尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的 ACK 确认该数据已收到。 -Nagle 算法的规则: -1. 如果 SO_SNDBUF 中的数据长度达到 MSS,则允许发送; -2. 如果该 SO_SNDBUF 中含有 FIN,表示请求关闭连接,则先将 SO_SNDBUF 中的剩余数据发送,再关闭; -3. 设置了 TCP_NODELAY=true 选项,则允许发送。TCP_NODELAY 是取消 TCP 的确认延迟机制,相当于禁用了 Negale 算法。正常情况下,当 Server 端收到数据之后,它并不会马上向 client 端发送 ACK, - 而是会将 ACK 的发送延迟一段时间(假一般是 40ms),它希望在 t 时间内 server 端会向 client 端发送应答数据,这样 ACK 就能够和应答数据一起发送,就像是应答数据捎带着 ACK 过去。 - 当然,TCP 确认延迟 40ms 并不是一直不变的,TCP 连接的延迟确认时间一般初始化为最小值 40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。 - 另外可以通过设置 TCP_QUICKACK 选项来取消确认延迟。 -4. 未设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于 MSS)均被确认,则允许发送; -5. 上述条件都未满足,但发生了超时(一般为 200ms),则立即发送。 -*** 通信协议 -在了解了粘包、拆包产生的原因之后,现在来分析接收方如何对此进行区分。道理很简单,如果存在不完整的数据(拆包),则需要继续等待数据,直至可以构成一条完整的请求或者响应。 -通过定义通信协议(protocol),可以解决粘包、拆包问题。协议的作用就定义传输数据的格式。这样在接受到的数据的时候: -如果粘包了,就可以根据这个格式来区分不同的包 -如果拆包了,就等待数据可以构成一个完整的消息来处理。 -**** 定长协议 -定长协议:顾名思义,就是指定一个报文的必须具有固定的长度。例如,我们规定每 3 个字节,表示一个有效报文,如果我们分 4 次总共发送以下 9 个字节: -|---+----+------+----| -| A | BC | DEFG | HI | -|---+----+------+----| -那么根据协议,我们可以判断出来,这里包含了 3 个有效的请求报文,如下: -|-----+-----+-----| -| ABC | DEF | GHI | -|-----+-----+-----| -在定长协议中: -发送方,必须保证发送报文长度是固定的。如果报文字节长度不能满足条件,如规定长度是 1024 字节,但是实际需要发送的内容只有 900 个字节,那么不足的部分可以补充 0。因此定长协议可能会浪费带宽。 -接收方,每读取到固定长度的内容时,则认为读取到了一个完整的报文。 -**** 特殊字符分割协议 -在包尾部增加回车或者空格符等特殊字符进行分割 。例如,按行解析,遇到字符\n、\r\n 的时候,就认为是一个完整的数据包。对于以下二进制字节流: -|--------------| -| ABC\nDEF\r\n | -|--------------| -那么根据协议,我们可以判断出来,这里包含了 2 个有效的请求报文 -|-----+-----| -| ABC | DEF | -|-----+-----| -在特殊字符分隔符协议中: -发送方,需要在发送一个报文时,需要在报文尾部添加特殊分割符号; -接收方,在接收到报文时,需要对特殊分隔符进行检测,直到检测到一个完整的报文时,才能进行处理。 - -在使用特殊字符分隔符协议的时候,需要注意的是,我们选择的特殊字符,一定不能在消息体中出现,否则可能会出现错误的拆包。 -例如,发送方希望把”12\r\n34”,当成一个完整的报文,如果是按行拆分,那么就会错误的拆分为 2 个报文。一种解决策略是,发 -送方对需要发送的内容预先进行 base64 编码,由于 base64 编码只包含 64 个字符:0-9、a-z、A-Z、+、/,我们可以选择这 64 个字 -符之外的特殊字符作为分隔符。 -**** 变长协议 -在解析时,先读取内容长度 Length,其值为实际消息体内容(Content)占用的字节数,之后必须读取到这么多字节的内容,才认为是一个完整的数据报文。 -|--------+---------| -| header | body | -|--------+---------| -| Length | Content | -|--------+---------| -在变长协议中: -发送方,发送数据之前,需要先获取需要发送内容的二进制字节大小,然后在需要发送的内容前面添加一个整数,表示消息体二进制字节的长度。 -接收方,在解析时,先读取内容长度 Length,其值为实际消息体内容(Content)占用的字节数,之后必须读取到这么多字节的内容,才认为是一个完整的数据报文。 -**** 序列化 -序列化本质上已经不是为了解决粘包和拆包问题,而是为了在网络开发中可以更加的便捷。 -在变长协议中,我们看到可以在实际要发送的数据之前加上一个 length 字段,表示实际要发送的数据的长度。 -这实际上给我们了一个很好的思路,我们完全可以将一个对象转换成二进制字节,来进行通信,例如使用一个 Request 对象表示请求,使用一个 Response 对象表示响应。 -|----------+---------------------------------+-------------------------------------------------------| -| frame | support language | web | -|----------+---------------------------------+-------------------------------------------------------| -| jdk | Java | | -|----------+---------------------------------+-------------------------------------------------------| -| hessian | Support multiple not include go | http://hessian.caucho.com/ | -|----------+---------------------------------+-------------------------------------------------------| -| fst | Java | https://github.com/RuedigerMoeller/fast-serialization | -|----------+---------------------------------+-------------------------------------------------------| -| protobuf | Almost all languages | https://developers.google.cn/protocol-buffers/ | -|----------+---------------------------------+-------------------------------------------------------| -| msgpack | Almost all languages | https://msgpack.org/ | -|----------+---------------------------------+-------------------------------------------------------| -提示:xml、json 也属于序列化框架的范畴,上面的表格中并没有列出。 - -一些网络通信的 RPC 框架通常会支持多种序列化方式,例如 dubbo 支持 hessian、json、kyro、fst 等。 -在支持多种序列化框架的情况下,在协议中通常需要有一个字段来表示序列化的类型,例如,我们可以将上述变长协议的格式改造为: -|--------+------------+---------| -| Length | serializer | Content | -|--------+------------+---------| -这里使用 1 个字节表示 Serializer 的值,使用不同的值代表不同的框架。 - -发送方,选择好序列化框架后编码后,需要指定 Serializer 字段的值。 -接收方,在解码时,根据 Serializer 的值选择对应的框架进行反序列化 -**** 压缩 -通常,为了节省网络开销,在网络通信时,可以考虑对数据进行压缩。常见的压缩算法有 lz4、snappy、gzip 等。在选择压缩算法时,我们主要考虑压缩比以及解压缩的效率。 -我们可以在网络通信协议中,添加一个 compress 字段,表示采用的压缩算法: -|--------+------------+----------+---------| -| Length | serializer | compress | Content | -|--------+------------+----------+---------| -通常,我们没有必要使用一个字节,来表示采用的压缩算法,1个字节可以标识 256 种可能情况,而常用压缩算法也就那么几种,因此通常只需要使用 2~3 个 bit 来表示采用的压缩算法即可。 - -另外,由于数据量比较小的时候,压缩比并不会太高,没有必要对所有发送的数据都进行压缩,只有再超过一定大小的情况下,才考虑进行压缩。 -如 rocketmq,producer 在发送消息时,默认消息大小超过 4k,才会进行压缩。因此,compress 字段,应该有一个值,表示没有使用任何压缩算法,例如使用 0。 -**** 查错校验码 -一些通信协议传输的数据中,还包含了查错校验码。典型的算法如 CRC32、Adler32 等。java 对这两种校验方式都提供了支持,java.util.zip.Adler32、java.util.zip.CRC32 -|--------+------------+----------+---------+-------| -| Length | serializer | compress | Content | CRC32 | -|--------+------------+----------+---------+-------| -这里并不对 CRC32、Adler32 进行详细说明,主要是考虑,为什么需要进行校验? -有人说是因为考虑到安全,这个理由似乎并不充分,因为我们已经有了 TLS 层的加密,CRC32、Adler32 的作用不应该是为了考虑安全。 -一位同事的观点,我非常赞同:二进制数据在传输的过程中,可能因为电磁干扰,导致一个高电平变成低电平,或者低电平变成高电平。这种情况下,数据相当于受到了污染,此时通过 CRC32 等校验值,则可以验证数据的正确性。 -另外,通常校验机制在通信协议中,是可选的配置的,并不需要强制开启,其虽然可以保证数据的正确,但是计算校验值也会带来一些额外的性能损失。如 Mysql 主从同步,虽然高版本默认开启 CRC32 校验,但是也可以通过配置禁用。 -**** 小结 -本节通过一些基本的案例,讲解了在 TCP 编程中,如何通过协议来解决粘包、拆包问题。在实际开发中,通常我们的协议会更加复杂。 -例如,一些 RPC 框架,会在协议中添加唯一标识一个请求的 ID,一些支持双向通信的 RPC 框架,如 sofa-bolt,还会添加一个方向信息等。 -当然,所谓复杂,无非是在协议中添加了某个字段用于某个用途,只要弄清楚这些字段的含义,也就不复杂了。 -** TCP 如何设计 封包与拆包协议? -设计TCP封包拆包协议主要有两种方式,一种是大小+内容模式+校验模式,一种是特殊的分割符号的模式,比如\r\n, http协议就采用的是\r\n来进行分割, 还有一种固定长度模式。 -** UDP的优点与缺点分别是什么? -UDP传送数据速度快,性能好,缺点是UDP发送完数据就不管了,数据传送中有可能丢包,同时数据包走的网络路径可能不一样,会导致先发的数据包后到,后发的数据包先到,这样就没有正确的时序性。 -** Redis 在游戏服务器开发中有什么作用? -Redis 在游戏开发中主要作用有:作为mem cache 数据库,将数据缓存到内存里面。Redis的订阅与发布系统可以作为多游戏服务器之间通讯的工具。Redis的有序集合等可以作为游戏的排行榜(zscore)。 -** 游戏服务器开发采用什么样的编程语言好? -目前市面上找平游戏服务器的主流的变成语言分别如下。 -第一档: C++ 与Java。占据了企业招聘里面的绝大部分; -第二档: Go, Python, C#, PHP, Node.js, Lua。 -** 什么是弱联网游戏? -弱联网游戏指的是玩家游戏的时候只是自己一个人完,不涉及多人同时交互,这种我们叫做弱联网游戏,同时也提供一些联网的功能,比如购买道具,社交,公告,邮件,排行等等。 -** 游戏服务器开发主流的高并发方式有哪些? -游戏服务器开发对性能要求非常的高,同时要支持高并发,充分发挥硬件性能,提升高并发发挥硬件性能,游戏服务器有两种模式的架构,一种是多进程单线程架构,一种是多进程多线程架构 -** 游戏服务器用Linux操作系统还是Windows操作系统? -目前主流的游戏服务器都基于Linux操作系统的,因为Linux操作系统一直做服务器,并且很多主流的代码模块框架都是优先基于Linux的,比如Redis等,所以一般游戏服务器都用Linux作为服务器的操作系统。 -** 游戏服务器开发如何调试? -游戏服务器开发对开发人员的要求非常的高, 特别是线上环境,处理的数据量比较大,所以断点调试这种方法,不大适合服务器。 -服务器一般采用的调试就是打印查看日志 -通过日志来分析对应的问题 -所以一个好的日志系统对于服务器来说是非常重要的,当然没有断点调试就对开发人员要求更好,对程序把控的能力更强。 -** 游戏服务器中主流的同步方式有哪些? -游戏服务器开发中主流的同步方式有状态同步和帧同步 -*** 帧同步 -帧同步是每帧同步玩家的操作,把所有的业务逻辑放到客户端计算,大家同样的操作,同样的代码得到同样的结果,帧同步的有点是性能好,缺点是容易作弊。 -*** 状态同步 -状态同步,就是服务器上跑游戏,各个客户端把操作输入发送给服务器,服务器决定处理的结果,把结果广播给客户端,然后客户端播放动画。状态同步的优点是不容易作弊,缺点是实时性不如帧同步。 -** 游戏服务器如何能承载大量的玩家在线? -当我们分析一个服务器能承载多大量的时候,一般我们要配置好单服(一个服务器组,可能是一台机器,也可能是几台)最多可带多少人,什么样的配置带多少人,这个需要我们把代码写好。提前设定好对应的承载量,单服设置好以后,我们再来通过扩展物理机器,来吃掉流量。 -** MMO RPG里面的AOI是什么意思? -MMORPG游戏可能有好几千人在同时玩游戏,如果一个人的状态改变了,要通知所有其他的好几千人,这个其实服务器是很难承受的,那么当我们一个玩家的状态有变化的时候,只要通知他周围对这个玩家感兴趣的人,这个叫做AOI,这样可以减少数据的传递,提升服务器的性能。 -** 网络游戏如何做世界排行榜? -排行榜是服务器经常需要用的一个功能,这个是为了增强top玩家的荣誉感,世界排行榜是非常重要的功能,Redis 对全服的玩家进行排序,使用的是有序队列,当我们更新玩家战绩的时候Redis就会帮我们更新排序好,请求排行榜的时候,只要获取就可以了。 -** 网路游戏如何对接第三方的支付? -目前第三方的支付都非常的成熟,比如微信支付,支付宝支付,那么游戏服务器如何与这些来对接呢?一般的思路是游戏服务器搭建一个http server, 提供一个地址,给第三方的支付服务器回调,当我们的客户端调用第三方SDK来支付一个商品的时候,第三方支付就会调用我们的回调地址通知我们,用户购买了某个商品,收到通知以后,我们在服务器上给玩家发货,把玩家的货物信息更新到数据库。 -** 服务器开发中同步IO与异步IO的区别是什么? -IO操作指的是当我们从外部设备(磁盘,网卡)读写数据的时候,CPU要等待外部设备处理完,如果是同步IO,那么这个线程就同步的等在这个IO请求上,直到处理完成,这样这个线程就会被挂起,而不可以做别的,异步IO是发完IO请求以后,我们不等结果立马返回做其他的事情,等IO结果完成了以后再来处理。同步IO会导致线程挂起,异步IO可以使线程做其他的一些事情,具体使用同步IO,还是异步IO要从服务器的整体架构上去考虑。 -** 各大编程语言的高性能的网络库主要有哪些? -Java服务器高性能的网络库有 netty, Mina等。 -C/C++ 服务器高性能的网络库,可以直接使用EPOLL或IOCP,也可以使用第三方的库如libevent, libuv等。 -C# 服务器开发高性能网络库可以使用SuperSocket等。 -每个服务器开发语言对会有对应的高性能的网络库。 -** 游戏服务器序列化/反序列化用什么样的技术? -目前服务器序列化与反序列化主要分成两种模式二进制模式与文本模式,文本模式的序列化与反序列化主要有json与xml, 二进制模式的序列化与反序列化主要有自定义的协议和google的protobuf协议。 -** BASE64编码解码在服务器开发中有何作用? -BAS64编码解码,使用可打印字符(文本)来表示二进制数。在游戏开发中,如果是用文本协议,比如http, 我们要传递一个二进制数据,可以将二进制数据编码从BASE64的文本编码,然后在传递,传递完成后,再解码出来得到二进制数据,这样文本模式下传递二进制使用BASE64就成为了一个处理的方式。 -** 游戏服务器如何避免内存碎片? -不管是C++服务器还是如Java这样带来垃圾回收的编程语言开发的服务器。避免内存碎片和减少GC的开销都是必不可少的。这两个其实解决问题的手段都是一样的,手段就是使用缓存池的模式,比如我这组服务器,准备负载N个玩家,那么可以为这个N问题的规模分配好对应的内存池,把那些经常要分配和释放的对象用内存池管理起来。很多人可能会问,内存池管理不就一直站内存么?其实这个问题很好理解,因为服务器和客户端App不一样,服务器所有的资源,都是为游戏服务器服务的,所以我们可以吧最大设计的负载所需要的内存一次性的开出来这样能大大节约GC开销或内存碎片。 -** 如何查看游戏服务器程序是否已经发挥了机器的最大性能? -当我们很衡量一个服务器程序能带多少负载,我们可以规定一个机器的CPU, IO, 网卡等,然后看这个服务器能同时支持多少玩家不卡,等到了卡的临界点的时候,这个时候应该就是我们这个服务器程序的最大的灵界点了,如何分析这个程序是否发挥了机器的最大性能呢?这个时候我们要看各个硬件的参数,比如CPU占用率, IO, 网卡等数据,如果CPU, IO,网卡都没有达80%以上,而玩家无法再增加了,说明了我们写的代码没有完全发挥机器的性能,要去思考我们的架构和部署。 -** 服务器如何做热更新? -服务器做热更新是在不关闭服务器的情况下直接热更新代码来修正代码逻辑。而游戏里面分为两类,一类为代码逻辑,一类为数据实体。当我们有成千上万的玩家同时在线的时候就有很多的数据实体,如果我们修改了数据实体的内容,肯定是无法热更新上去的,因为这些实体都存在,你添加了新的数据除非重新启动或生成数据实体否则无法热更,我们一般服务器的热更新指的不是热更新数据实体,而是更新代码逻辑,比如有个代码有bug要修正,修正以后数据实体不用改,只要更新好逻辑,这样不用重启机器,后续再掉这个逻辑的时候就已经被修正过来了。 -** 服务器数据库的字符编码一般采用什么? -一般我们开发游戏服务器的时候,字符编码一般都采用Utf8, 因为Linux上UTF8的标准支持的非常好。 -** 服务器守护进程有什么作用? -一般我们上线部署服务器的时候,时候有可能由于代码的错误等到只进程异常退出,当出现这样的情况是,我们要用守护进程把游戏进程重启,保证能从新开始游戏。这个就是守护进程的作用。 -** Linux 如何查看服务器的内存占用等信息? -Linux有命令可以查看内存占用相关的信息,不同的Linux发型版本,可能会有一些小的差异,我们可以通过命令cat /proc/meminfo, 查看内存的整体使用情况,也可以通过top等命令来查看各个进程的一些详细信息 -** 如何做服务器管理后台? -一般我们在服务器上架设一个HttpServer,HttpServer来做服务器的管理后台,通过HttpServer来操作游戏的数据库。来做为管理的后台。也可以通过访问服务器的数据库来显示当前游戏中的一些情况,方便我们对整个游戏服务器的情况做一个综合的了解。 -** new和malloc的区别 -| 特征 | new/delete | malloc/free | -| 分配内存的位置 | 自由存储区 | 堆 | -| 内存分配成功的返回值 | 完整类型指针 | void* | -| 内存分配失败的返回值 | 默认抛出异常 | 返回NULL | -| 分配内存的大小 | 由编译器根据类型计算得出 | 必须显式指定字节数 | -| 处理数组 | 有处理数组的new版本new[] | 需要用户计算数组的大小后进行内存分配 | -| 已分配内存的扩充 | 无法直观地处理 | 使用realloc简单完成 | -| 是否相互调用 | 可以,看具体的operator new/delete实现 | 不可调用new | -| 分配内存时内存不足 | 客户能够指定处理函数或重新制定分配器 | 无法通过用户代码进行处理 | -| 函数重载 | 允许 | 不允许 | -| 构造函数与析构函数 | 调用 | 不调用 | -** [[https://zhuanlan.zhihu.com/p/51898119][如何避免内存泄露]] -*** 使用智能指针 std::string 替代 char* -*** HOLD [[https://blog.csdn.net/okiwilldoit/article/details/110138697][Arena内存池简介]] -** HOLD 十大经典排序算法 -*** 冒泡排序 -比较相邻的元素。如果第一个比第二个大,就交换他们两个。 -对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。 -针对所有的元素重复以上的步骤,除了最后一个。 -持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 -#+begin_src c++ -#include -void bubble_sort(int arr[], int len) { - int i, j, temp; - for (i = 0; i < len - 1; i++) - for (j = 0; j < len - 1 - i; j++) - if (arr[j] > arr[j + 1]) { - temp = arr[j]; - arr[j] = arr[j + 1]; - arr[j + 1] = temp; - } -} -int main() { - int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 }; - int len = (int) sizeof(arr) / sizeof(*arr); - bubble_sort(arr, len); - int i; - for (i = 0; i < len; i++) - printf("%d ", arr[i]); - return 0; -} -#+end_src -*** 快速排序 -从数列中挑出一个元素,称为 "基准"(pivot); -重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作; -递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序; -#+begin_src c++ -//递归法 -template -void quick_sort_recursive(T arr[], int start, int end) { - if (start >= end) - return; - T mid = arr[end]; - int left = start, right = end - 1; - while (left < right) { //在整个范围内搜寻比枢纽元值小或大的元素,然后将左侧元素与右侧元素交换 - while (arr[left] < mid && left < right) //试图在左侧找到一个比枢纽元更大的元素 - left++; - while (arr[right] >= mid && left < right) //试图在右侧找到一个比枢纽元更小的元素 - right--; - std::swap(arr[left], arr[right]); //交换元素 - } - if (arr[left] >= arr[end]) - std::swap(arr[left], arr[end]); - else - left++; - quick_sort_recursive(arr, start, left - 1); - quick_sort_recursive(arr, left + 1, end); -} -template //整數或浮點數皆可使用,若要使用物件(class)時必須設定"小於"(<)、"大於"(>)、"不小於"(>=)的運算子功能 -void quick_sort(T arr[], int len) { - quick_sort_recursive(arr, 0, len - 1); -} -#+end_src -*** 归并排序 -** [[https://blog.csdn.net/weixin_43222324/article/details/112858929][小白都能看懂的TCP三次握手四次挥手]] -** [[https://www.cioage.com/article/623158.html][大量的 TCP 连接是 TIME_WAIT 状态,有什么影响?怎么处理?]] -*** 大量的 TIME_WAIT 状态 TCP 连接存在,其本质原因是什么? -大量的短连接存在 -特别是 HTTP 请求中,如果 connection 头部取值被设置为 close 时,基本都由「服务端」发起主动关闭连接 -而,TCP 四次挥手关闭连接机制中,为了保证 ACK 重发和丢弃延迟数据,设置 time_wait 为 2 倍的 MSL(报文最大存活时间) -~TIME_WAIT~ 状态: -TCP 连接中,主动关闭连接的一方出现的状态;(收到 FIN 命令,进入 TIME_WAIT 状态,并返回 ACK 命令) -保持 2 个 MSL 时间,即,4 分钟;(MSL 为 2 分钟) -*** 解决上述 time_wait 状态大量存在,导致新连接创建失败的问题,一般解决办法: -(1) 客户端,HTTP 请求的头部,connection 设置为 keep-alive,保持存活一段时间:现在的浏览器,一般都这么进行了 -(2) 服务器端 - -允许 time_wait 状态的 socket 被重用 -缩减 time_wait 时间,设置为 1 MSL(即,2 mins) -结论:几个核心要点 - -(1) time_wait 状态的影响: - -TCP 连接中,「主动发起关闭连接」的一端,会进入 time_wait 状态 -time_wait 状态,默认会持续 2 MSL(报文的最大生存时间),一般是 2x2 mins -time_wait 状态下,TCP 连接占用的端口,无法被再次使用 -TCP 端口数量,上限是 6.5w(65535,16 bit) -大量 time_wait 状态存在,会导致新建 TCP 连接会出错,address already in use : connect 异常 -(2) 现实场景: - -服务器端,一般设置:不允许「主动关闭连接」 -但 HTTP 请求中,http 头部 connection 参数,可能设置为 close,则,服务端处理完请求会主动关闭 TCP 连接 -现在浏览器中, HTTP 请求 connection 参数,一般都设置为 keep-alive -Nginx 反向代理场景中,可能出现大量短链接,服务器端,可能存在 -(3) 解决办法: -#+begin_src conf -vi /etc/sysctl.conf -net.ipv4.tcp_keepalive_time = 1200 -#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟。 -net.ipv4.ip_local_port_range = 1024 65000 -#表示用于向外连接的端口范围。缺省情况下很小:32768到61000,改为1024到65000。 -net.ipv4.tcp_max_syn_backlog = 8192 -#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。 -net.ipv4.tcp_max_tw_buckets = 5000 -#表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。 -默认为180000,改为5000。对于Apache、Nginx等服务器,上几行的参数可以很好地减少TIME_WAIT套接字数量,但是对于 Squid,效果却不大。此项参数可以控制TIME_WAIT套接字的最大数量,避免Squid服务器被大量的TIME_WAIT套接字拖死。 -#+end_src -服务器端允许 time_wait 状态的 socket 被重用 -缩减 time_wait 时间,设置为 1 MSL(即,2 mins) -** CLOSE_WAIT 什么情况出现?怎么处理? -四次挥手中可以得知 -client 发送 fin server 回应 ack 就会进入 close wait -如果server一直不发送 fin 就会保持在close wait状态 -有可能是由于服务器bug导致的 需要查 -** linux常用命令 -+ linux 文本去重命令 uniq 一般配合 sort cut 一起使用 -+ linux 查看cpu占用率命令 top -+ linux 查看硬盘情况 df -h -+ linux 查看进程命令 ps -x -+ linux 查看内存 free or cat /proc/meminfo -+ linux 查看端口 netstat -tnlp or lsof -** HOLD [[https://zhuanlan.zhihu.com/p/260450151][一文懂网络io模型]] -单线程异步 redis -多线程异步 memcache -多进程异步 nginx -** HOLD IO多路复用 -*** select -*** poll -*** epoll -+ epoll_create -+ epoll_ctrl -+ epoll_wait -** HOLD [[https://zhuanlan.zhihu.com/p/30007037][字节对齐]] -** 链表如何判环 -*** 快慢指针 龟兔算法 -#+begin_src c++ -#include - -struct ListNode { - int val; - ListNode *next; - ListNode(int x) : val(x), next(nullptr) {} -}; - -bool hasCycle(ListNode *head) { - if (!head || !head->next) { - return false; - } - - ListNode *slow = head; - ListNode *fast = head->next; - - while (slow != fast) { - if (!fast || !fast->next) { - return false; - } - slow = slow->next; - fast = fast->next->next; - } - - return true; -} - -int main() { - // 构建一个有环的链表示例 - ListNode *head = new ListNode(1); - head->next = new ListNode(2); - head->next->next = new ListNode(3); - head->next->next->next = new ListNode(4); - head->next->next->next->next = head; // 尾节点指向头节点,形成环 - - std::cout << "The linked list has cycle? " << (hasCycle(head) ? "Yes" : "No") << std::endl; - - return 0; -} - -#+end_src -** 大小端 -大端Big Endian模式:即把数据的高字节放到低地址中 -小端Little Endian模式:高字节放到高地址中 -网络序 网络传输一般采用大端序 -怎么测试我的电脑是小端模式还是大端模式呢 -+ 将int 48存起来,然后取得其地址,再将这个地址转为char* 这时候,如果是小端存储,那么char*指针就指向48;48对应的ASCII码为字符‘0’; -#+begin_src c++ -int i = 48; -int *p = &i; -char c = 0; -c = *((char*)p) -if(c == '0') - printf("little"); -else - printf("big"); -#+end_src -+ 定义变量int i=1;将 i 的地址拿到,强转成char*型,这时候就取到了 i 的低地址,这时候如果是1就是小端存储,如果是0就是大端存储 -#+begin_src c++ -int i = 1; -char c = *(char*(&i)) -if(c) - printf("little"); -else - printf("big"); -#+end_src -+ 定义联合体,一个成员是多字节,一个是单字节,给多字节的成员赋一个最低一个字节不为0,其他字节为0 的值,再用第二个成员来判断,如果第二个字节不为0,就是小端,若为0,就是大端。 -#+begin_src c++ -union { - int i; - char c; -}un; -un.i = 1; - -if(un.c == 1) - printf("little"); -else - printf("big"); -#+end_src -htons —— 把unsigned short类型从主机序转成网络字节序 -ntohs —— 把unsigned short类型从网络字节序转成主机序 -htonl —— 把unsigned long类型从主机序转成网络字节序 -ntohl —— 把unsigned long类型从网络字节序转成主机序 -** 定时器的实现 -*** 用途 -心跳检测 缓存数据定时刷盘 技能冷却 被动冷却 定时活动 -*** 目标 -统一协调处理 -*** 接口设计 -+ 初始化定时器 init_timer() -+ 添加定时任务 add_timer(expire_time, callback) -+ 删除定时任务 del_timer(int id) del_timer(tnode*) -+ 检测处理定时任务 handle_timer -*** 实现方式 -单线程下 红黑树 最小堆 (最小堆优于红黑树) 跳跃表 -多线程环境下 考虑锁的粒度 时间轮 -+ 时间轮 -空推进 增加层级 -数组大小必须要足够大 -** [[https://zhuanlan.zhihu.com/p/439331952][服务发现]] -** HOLD 树 -*** 树的遍历 深度 广度 前序 中序 -*** 二叉搜索树 -*** 说一下最小生成树 -*** 行为树 -** HOLD 数据库常见问题 -*** mysql -**** mysql 读写分离 -**** mysql 索引用途 -**** mysql B+树 -**** mysql 事务 -**** mysql 视图 -**** mysql 锁 -**** mysql 扩表方案 -三种,预留字段,写成kv的形式再进行,行转列。例如 uid,key,value的表。然后进行行转列即可。还有看服务器开发大佬们常用的方法,写个新表,写三个触发器,然后闲暇时间将原表的内容插入新表,然后改名字就好了 -**** 优化注册流程 -**** 慢查询优化 -**** mysql 为何选择b+树 -*** redis -**** redis有多少种类型 -Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合) -**** [[最全面的Redis缓存雪崩、击穿、穿透问题解决方案][redis 缓存雪崩 击穿 穿透]] -**** redis设计与实现 -**** Redis 落地的两种方式 -***** RDB(Redis DataBase): -RDB 是 Redis 的快照(Snapshot)持久化方式,它通过周期性地将内存中的数据集快照保存到磁盘上的一个二进制文件(.rdb 文件)中。 -RDB 持久化可以通过配置文件中的 save 指令来设置保存的触发条件和频率。 -RDB 文件通常用于备份和全量恢复,因为它是一个紧凑且经过压缩的二进制文件,可以在需要时快速地恢复到某个时间点的数据状态。 -***** AOF(Append-Only File): -AOF 是 Redis 的日志(Log)持久化方式,它以追加的方式记录每个写操作,将写操作以命令的形式追加到一个日志文件(appendonly.aof)中。 -AOF 持久化可以通过配置文件中的 appendonly 指令来启用,并且可以选择不同的同步策略(如 always、everysec、no)来控制日志的刷写频率。 -AOF 文件通常用于灾难恢复和增量恢复,因为它包含了所有的写操作记录,可以确保数据的完整性。 -*** mongo -** TODO cplusplus -*** 友元是什么 -*** 智能指针如何实现 -**** shareptr -**** uniqueptr -**** weakptr -*** 虚函数 -**** 虚函数怎么实现 -C++中的虚函数通过虚函数表(vtable)和虚函数指针(vptr)来实现。 -***** 虚函数表(vtable): -每个包含虚函数的类都会生成一个虚函数表,用于存储该类的虚函数地址。 -虚函数表是一个数组,其中存储了指向每个虚函数的函数指针。 -每个类的对象都会包含一个指向其对应虚函数表的指针。 -***** 虚函数指针(vptr): -每个包含虚函数的类的对象都会包含一个虚函数指针(vptr),用于指向其对应的虚函数表。 -当调用一个虚函数时,实际上是通过对象的虚函数指针找到对应的虚函数表,然后通过虚函数表中的函数指针调用实际的虚函数。 -**** 虚函数表在哪 -虚函数表位于静态存储区,在程序编译时就已经确定,因此对于每个类来说,其虚函数表是唯一的。 -**** 虚函数怎么做替换的 -虚函数的替换是通过派生类中重新定义(override)基类中的虚函数来实现的。当派生类中重新定义了基类的虚函数时,派生类中的虚函数会覆盖(替换)基类中的同名虚函数,从而改变了虚函数的行为。 -**** 纯虚函数的作用 -接口定义:纯虚函数定义了一个接口,强制所有派生类实现该函数。 -实现多态:纯虚函数是实现多态的重要手段之一。 -抽象基类:包含纯虚函数的类通常被称为抽象基类(Abstract Base Class,ABC)。 -**** 为什么析构函数用虚函数 -析构函数通常使用虚函数的主要原因是为了正确地释放派生类对象的资源。 -当基类指针指向一个派生类对象,并且通过基类指针调用析构函数时,如果析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这样可能导致派生类对象的资源无法正确释放,造成内存泄漏或其他问题。 -**** 构造函数用虚函数会怎么样 -将构造函数声明为虚函数是不推荐的,因为在对象构造期间,虚函数的多态性机制尚未建立。 -*** 多态 -**** 编译时多态性(静态多态性): -编译时多态性是通过函数重载(Overloading)和模板(Template)来实现的。在编译时,根据函数参数的类型、个数和顺序来确定调用哪个函数。 -函数重载允许在同一作用域内定义多个同名函数,它们的参数列表必须不同,编译器根据调用时的参数类型来决定调用哪个函数。 -模板允许编写通用的函数或类,使其可以接受不同类型的参数。 -**** 运行时多态性(动态多态性): -运行时多态性是通过虚函数(Virtual Function)和继承(Inheritance)来实现的。在运行时,根据对象的实际类型来确定调用哪个函数。 -虚函数是在基类中声明为虚函数的函数,派生类可以重新定义(Override)这些虚函数,通过基类指针或引用调用虚函数时,会根据对象的实际类型调用相应的派生类函数。 -*** 多线程 -C++提供了多种机制来支持多线程编程,其中最常用的是标准库中的头文件提供的线程类。以下是C++多线程编程的基本概念和常用技术: -**** 创建线程: -使用std::thread类来创建线程,通常需要提供一个可调用对象(如函数、函数对象或lambda表达式)作为线程的执行体。 -**** 线程同步: -多个线程并发执行时可能会涉及共享资源的访问,为了避免竞态条件(Race Condition)和数据竞争(Data Race),需要使用同步机制来保护共享资源,如互斥量(std::mutex)、条件变量(std::condition_variable)等。 -**** 线程池: -线程池是一种管理线程的技术,它可以重用线程以提高性能,并可以灵活地控制线程的数量。C++标准库中没有提供线程池,但可以使用第三方库(如boost::asio)或手动实现线程池。 -*** 静态变量 -在C++中,关键字static用于声明静态变量。静态变量具有以下特点: -**** 生命周期 -静态变量的生命周期与程序的整个运行周期相同。它们在程序启动时初始化,在程序结束时销毁。因此,它们在程序的所有函数调用之间保持其值。 -**** 作用域 -静态变量可以具有函数作用域、文件作用域或类作用域,取决于它们的声明位置。 -在函数内部声明的静态变量具有函数作用域,只能在声明它们的函数内部访问。 -在文件中或类中声明的静态变量具有文件作用域或类作用域,可以在整个文件或类的范围内访问。 -**** 初始化 -静态变量在程序启动时进行初始化。如果没有显式初始化,静态变量将被默认初始化为0(对于基本数据类型)或nullptr(对于指针类型)。 -对于函数内部的静态变量,初始化只会在第一次函数调用时进行,之后的调用不会再次初始化。 -对于文件或类作用域的静态变量,初始化只会在程序启动时进行一次。 -**** 存储位置 -静态变量通常存储在静态存储区(静态数据区)中,这是一块特殊的内存区域,用于存储全局变量、静态变量和常量。 -**** 作为类成员 -在类中声明的静态成员变量属于类本身,而不是类的实例。它们只有一份副本,被所有该类的对象所共享。 -*** 深浅拷贝 -深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在面向对象编程中用于复制对象的两种不同方式。它们的区别在于复制对象时是否复制对象的内容。 -**** 浅拷贝(Shallow Copy): -浅拷贝是将一个对象的数据成员的值复制到另一个对象中,而不复制数据成员所指向的内容。 -如果对象的数据成员是基本数据类型,浅拷贝会将其值直接复制到新对象中。 -如果对象的数据成员是指针类型,则浅拷贝只会复制指针的值,而不会复制指针指向的内容。因此,新对象和原对象会共享同一块内存区域,可能会导致浅拷贝对象的析构函数重复释放同一块内存,引发内存错误。 -**** 深拷贝(Deep Copy): -深拷贝是将一个对象的数据成员的值以及数据成员所指向的内容全部复制到另一个对象中,即在新对象中重新分配内存,与原对象完全独立。 -对于指针类型的数据成员,深拷贝会为新对象分配一块新的内存,将原对象所指向的内容复制到新的内存区域中。 -深拷贝避免了浅拷贝可能出现的问题,每个对象都有自己独立的内存空间,不会因为一个对象的改变而影响到另一个对象。 -*** const/volatile -const 和 volatile 都是C++中用于修饰变量的关键字,它们分别表示常量和易变性。它们的作用是告诉编译器如何对待被修饰的变量,以便更好地进行代码优化或确保程序的正确性。 -**** const: -const用于声明常量,表示变量的值在程序执行期间不可修改。 -声明为const的变量必须在声明时进行初始化,且一旦初始化后,其值不能再被修改。 -声明为const的指针或引用可以指向不可变对象,但不能通过它们修改对象的值。 -const还可以用于成员函数中,表示该成员函数不会修改对象的状态。 -**** volatile: -volatile用于声明易变变量,表示变量的值在程序执行期间可能会被意外改变,如硬件寄存器、多线程环境中的共享变量等。 -声明为volatile的变量告诉编译器不要对其进行优化,每次访问时都要从内存中读取或写入其值。 -volatile变量的值可以在未经通知的情况下被外部因素改变,因此编译器不会对其进行优化。 -*** RTTI (Run-Time Type Identification) -RTTI(Run-Time Type Identification)是C++语言的一项特性,用于在运行时确定对象的实际类型。它允许程序在运行时检查对象的类型信息,包括对象的类属关系、类的层次结构等。 -在C++中,RTTI主要通过两种方式来实现: -**** typeid运算符: -typeid运算符用于获取对象的类型信息,返回一个std::type_info对象的引用,该对象包含有关类型的信息。 -typeid运算符的语法为:typeid(expression),其中expression可以是对象、类型或表达式。 -**** dynamic_cast运算符: -dynamic_cast运算符用于在继承层次结构中进行安全的向下转型(downcasting)。 -当向下转型失败时,dynamic_cast返回空指针(对于指针类型),或抛出std::bad_cast异常(对于引用类型)。 - -*** c++强制转换运算符 -**** const_cast (expr) -const_cast 运算符用于修改类型的 const / volatile 属性。除了 const 或 volatile 属性之外,目标类型必须与源类型相同。这种类型的转换主要是用来操作所传对象的 const 属性,可以加上 const 属性,也可以去掉 const 属性。 -**** dynamic_cast (expr) -dynamic_cast 在运行时执行转换,验证转换的有效性。如果转换未执行,则转换失败,表达式 expr 被判定为 null。dynamic_cast 执行动态转换时,type 必须是类的指针、类的引用或者 void*,如果 type 是类指针类型,那么 expr 也必须是一个指针,如果 type 是一个引用,那么 expr 也必须是一个引用。 -**** reinterpret_cast (expr) -reinterpret_cast 运算符把某种指针改为其他类型的指针。它可以把一个指针转换为一个整数,也可以把一个整数转换为一个指针。 -**** static_cast (expr) -static_cast 运算符执行非动态转换,没有运行时类检查来保证转换的安全性。例如,它可以用来把一个基类指针转换为派生类指针。 -*** [[http://shaoyuan1943.github.io/2016/03/26/explain-move-forward/][std::move std::forward]] -*** c++11 -*** delete[]时如何知道数组长度 -[[file:assets/notes/cplusplus/1.png]] -[[file:assets/notes/cplusplus/2.png]] -*** map unordermap 区别 -[[file:assets/notes/cplusplus/map.jpg]] -*** [[https://www.runoob.com/note/27755][sizeof 和 strlen区别]] -sizeof 运算符 strlen 函数 -sizeof用来计算类型的大小 strlen用来计算字符串长度 -*** define 和 const的区别 -#define 和 const 都可以用于定义常量,但是 const 更安全、更具有类型和作用域,并且能够避免 #define 可能引发的一些问题。因此,在C++中,建议优先使用 const 来定义常量。 -**** 预处理器宏 (#define) -#define 是一个预处理指令,用于创建符号常量或简单的替换文本。 -#define 不会分配内存,它只是简单地将代码中的标识符替换为指定的文本。 -#define 定义的常量没有类型,编译器不会对其进行类型检查,也不会进行作用域检查。 -#define 适用于简单的常量定义,例如宏函数和条件编译等。 -由于是简单的文本替换,#define 可能会引发一些意外的副作用,例如多次计算或重复替换。 -**** 常量 (const) -const 是C++关键字,用于定义具有类型的常量。 -const 定义的常量在内存中有自己的存储空间,可以被编译器优化,并具有类型安全性。 -const 常量具有作用域,可以根据其定义的位置访问,也可以在不同的作用域中重新定义。 -const 可以定义任何类型的常量,包括基本类型、类对象和指针等。 -** TODO golang -*** [[https://zhuanlan.zhihu.com/p/323271088][gpm模型]] -*** TODO Go的GC怎么做到并发的 -** 设计模式 -*** 单例模式 -保证一个类只有一个实例,并提供一个访问它的全局访问点。 -*** 工厂模式 -#+begin_src c++ -#include -#include -#include - -// 基类:怪物 -class Monster { -public: - virtual void attack() = 0; -}; - -// 具体怪物类:狼 -class Wolf : public Monster { -public: - void attack() override { - std::cout << "Wolf attacks with claws!" << std::endl; - } -}; - -// 具体怪物类:巨魔 -class Troll : public Monster { -public: - void attack() override { - std::cout << "Troll attacks with club!" << std::endl; - } -}; - -// 工厂接口:怪物工厂 -class MonsterFactory { -public: - virtual std::unique_ptr createMonster() = 0; -}; - -// 具体工厂类:狼工厂 -class WolfFactory : public MonsterFactory { -public: - std::unique_ptr createMonster() override { - return std::make_unique(); - } -}; - -// 具体工厂类:巨魔工厂 -class TrollFactory : public MonsterFactory { -public: - std::unique_ptr createMonster() override { - return std::make_unique(); - } -}; - -int main() { - // 创建狼工厂 - std::unique_ptr wolfFactory = std::make_unique(); - // 使用狼工厂创建狼怪物 - std::unique_ptr wolf = wolfFactory->createMonster(); - // 狼怪物攻击 - wolf->attack(); - - // 创建巨魔工厂 - std::unique_ptr trollFactory = std::make_unique(); - // 使用巨魔工厂创建巨魔怪物 - std::unique_ptr troll = trollFactory->createMonster(); - // 巨魔怪物攻击 - troll->attack(); - - return 0; -} -#+end_src -*** 访问者模式 -#+begin_src c++ -#include -#include - -// 前向声明被访问元素类 -class ElementB; - -// 访问者接口 -class Visitor { -public: - virtual void visit(ElementA& element) = 0; - virtual void visit(ElementB& element) = 0; -}; - -// 具体访问者类:打印访问者 -class PrintVisitor : public Visitor { -public: - void visit(ElementA& element) override { - std::cout << "Printing ElementA" << std::endl; - } - - void visit(ElementB& element) override { - std::cout << "Printing ElementB" << std::endl; - } -}; - -// 具体访问者类:计算访问者 -class CalculationVisitor : public Visitor { -public: - void visit(ElementA& element) override { - std::cout << "Calculating something based on ElementA" << std::endl; - } - - void visit(ElementB& element) override { - std::cout << "Calculating something based on ElementB" << std::endl; - } -}; - -// 被访问元素接口 -class Element { -public: - virtual void accept(Visitor& visitor) = 0; -}; - -// 具体被访问元素类:元素A -class ElementA : public Element { -public: - void accept(Visitor& visitor) override { - visitor.visit(*this); - } -}; - -// 具体被访问元素类:元素B -class ElementB : public Element { -public: - void accept(Visitor& visitor) override { - visitor.visit(*this); - } -}; - -int main() { - std::vector> elements; - elements.push_back(std::make_unique()); - elements.push_back(std::make_unique()); - - PrintVisitor printVisitor; - CalculationVisitor calculationVisitor; - - for (const auto& element : elements) { - element->accept(printVisitor); - element->accept(calculationVisitor); - } - - return 0; -} -#+end_src -** [[https://www.jianshu.com/p/c1015f5ffa74][进程通信]] -常用的socket方式 或者 共享内存 -** TLV格式的协议 -** 编译型语言和解释型语言的区别 - + 编译型语言 -通过专门的编译器,将所有源代码一次性转换成特定平台(Windows、Linux 等)执行的机器码(以可执行文件的形式存在)。 -编译一次后,脱离了编译器也可以运行,并且运行效率高。 -可移植性差,不够灵活。 -+ 解释型语言 -由专门的解释器,根据需要将部分源代码临时转换成特定平台的机器码。 -跨平台性好,通过不同的解释器,将相同的源代码解释成不同平台下的机器码。 -一边执行一边转换,效率很低。 -** C++编译器有哪些,区别在哪 -历史和背景:GCC 是一个成熟的传统编译器,而 LLVM/Clang 是基于 LLVM 架构的新一代编译器。Clang 借助 LLVM 提供了更好的性能和更先进的特性。 -编译速度和优化能力:Clang 倾向于提供更快的编译速度和更好的错误诊断能力,而 GCC 则提供了更丰富的代码优化能力。 -错误诊断:Clang 通常提供更详细、更准确的错误信息,帮助开发者更快地定位和解决问题。 -标准支持:两者都在不断更新以支持最新的 C++ 标准,但 Clang 通常更快地更新并提供更好的支持。 -** [[https://www.cnblogs.com/i80386/p/4362720.html][protobuf 如何实现协议兼容]] -** TODO grpc -*** TODO grpc原理 -*** TODO 如何实现rpc -** aoi -常用算法 9宫格 主城中依然消耗很大 十字链表 -9宫格设计: -+ 根据地图大小初始化 aoimgr aoimgr.instance.init(mapsize, blocksize) 块大小由视野决定 -+ aoiblock 每个块内部一个map player_id player_entity 映射 -+ AddToAOIMgr(player) 把玩家加入aoi管理 -+ AOIUpdate 根据玩家位置 更新AOI -** HOLD [[https://halfrost.com/lru_lfu_interview/][lru缓存淘汰算法]] -用途: -如果游戏的用户很多,例如超过50万,内存就会不够,可使用LRU算法来淘汰一些数据。 -流程:收到用户请求 - 在内存查找用户对象 - 如果不存在就从数据库中加载- 放入内存cache-如果cache中的用户超过20万 - 用LRU算法淘汰最古老的用户数据。 -** 实现压缩算法的方法 -*** 无损压缩算法: -无损压缩算法是一种可以完全还原原始数据的压缩算法,即压缩后的数据可以通过解压缩算法还原为原始数据而不损失信息。常见的无损压缩算法包括: -霍夫曼编码:根据字符出现的频率来构建不等长的编码,频率高的字符用短编码,频率低的字符用长编码,以实现数据压缩。 -Lempel-Ziv 系列算法:如 LZ77、LZ78 和 LZW 等,基于字典的算法,通过查找重复出现的字符串来实现压缩。 -算术编码:将整个消息编码为一个数值,根据消息的概率分布来进行编码,以实现高效的数据压缩。 -*** 有损压缩算法: -有损压缩算法是一种在压缩数据时会丢失部分信息的压缩算法,压缩后的数据不能完全还原为原始数据。常见的有损压缩算法包括: -JPEG:一种用于图像压缩的有损压缩算法,主要用于压缩彩色图像。 -MP3:一种用于音频压缩的有损压缩算法,主要用于压缩音乐文件。 -视频编码标准:如 MPEG、H.264、H.265 等,用于视频压缩的有损压缩算法。 -*** 字典压缩算法: -字典压缩算法是一种通过构建字典来实现数据压缩的算法,通常用于处理重复性较高的数据。常见的字典压缩算法包括 LZ 系列算法和 LZW 算法等。 -*** 基于熵编码的压缩算法: -基于熵编码的压缩算法利用信息熵的概念来实现数据压缩,通过减少数据的冗余性来实现压缩。霍夫曼编码和算术编码都属于这类算法。 -** hash -*** [[https://zhuanlan.zhihu.com/p/45430524][什么是hash表]] -*** 如何解决冲突 -Hash冲突就是,不同的数据元素关键字K,计算出的哈希值相同,此时两个或多个数据,对应同一个存储地址,即产生冲突。 -*** 如何优化 -+ 开放定址法 - 使用某种探测算法在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。就是即使key产生hash冲突,也不会形成链表,而是将所有元素都存入哈希表里。发生hash冲突时,就以当前地址为基准,进行再寻址的方法去寻址下一个地址,直到找到一个为空的地址为止。 - 实现方式有: - 1.线性探查:发生hash冲突时,顺序查找下一个位置,直到找到一个空位置(固定步长1探测) - 2.二次探查:在发生hash冲突时,在表的左右位置进行按一定步长跳跃式探测(固定步长n探测) - 3.伪随机探测:在发生hash冲突时,根据公式生成一个随机数,作为此次探测空位置的步长(随机步长n探测)。 -+ 再哈希法 - 这种方式是同时构造多个哈希函数,当产生冲突时,计算另一个哈希函数的值。 - 这种方法不易产生聚集,但增加了计算时间。 -+ 链地址法(拉链法) - 使用链表来保存发生hash冲突的key,即不同的key有一样的hash值,将这些发生冲突的 value 组成一个单向链表 -+ 建立公共溢出区 - 将哈希表分为基本表和溢出表两部分,为所有发生hash冲突的关键字记录一个公共的溢出区来存放。在查找的时候,先与哈希表的相应位置比较,如果查找成功,则返回。否则去公共溢出区按顺序一一查找。在冲突数据少时性能好,冲突数据多的时候耗时 - 优缺点比较: - 开放散列(open hashing)/ 拉链法(针对桶链结构) - 优点: - 在总数频繁变动的时候可以节省开销,避免了动态调整; - 记录存储在节点里,动态分布,避免了指针的开销 - 删除时候比较方便 - 缺点: - 因为存储是动态的,所以在查询的时候跳转需要更多的时间的开销 - 在key-value可以预知,以及没有后续增改操作时候,封闭散列性能优于开放散列 - 不容易序列化 - 封闭散列(closed hashing)/ 开放定址法 - 优点: - 容易序列化 - 如果可以预知数据总数,可以创建完美哈希数列 - 缺点: - 存储的记录数目不能超过桶组数,在交互时候会非常麻烦 - 使用探测序列,计算时间成本过高 - 删除的时候比较麻烦 -*** 拉链法怎么优化 -红黑树 -** 网络同步 -网络同步是指多个网络节点之间协调数据和状态,以确保它们在共享的环境中保持一致。在游戏开发中,常见的同步方式包括状态同步和帧同步。 -*** 状态同步的实现方法 -状态同步是将每个玩家的状态信息发送给所有其他玩家,以便它们在各自的客户端上进行渲染。实现方法包括: -客户端-服务器模式:所有状态更新都由服务器进行,客户端只接收服务器的状态更新。客户端通过发送用户输入(如移动、攻击)给服务器,并接收服务器返回的其他玩家的状态信息。 -点对点模式:每个客户端直接发送状态信息给所有其他客户端。这种模式适用于较小规模的游戏,减少了对服务器的依赖,但需要更多的带宽和处理能力。 -*** 帧同步的实现方法 -帧同步是指将游戏世界的状态按照时间顺序分成一帧一帧进行同步。实现方法包括: -客户端-服务器-客户端模式:所有状态更新都由服务器进行,服务器将状态以一定的频率发送给所有客户端。客户端根据接收到的状态进行渲染,并将用户输入发送给服务器。这种方式保证了游戏的一致性,但需要额外的服务器带宽和计算资源。 -去中心化帧同步:每个客户端都有自己的逻辑和状态,通过交换状态信息来保持同步。客户端之间通过对等连接进行通信,共同决定游戏状态。这种方式减轻了对服务器的依赖,但需要解决一致性和延迟等问题。 -*** 两种同步方案的优缺点 各自的重连方法 -**** 状态同步: -优点: -简单易实现,适用于游戏状态较为简单的情况。 -降低了对带宽和服务器资源的需求。 -缺点: -需要保证所有玩家收到的状态信息保持一致,容易受到延迟和丢包的影响。 -客户端之间无法直接通信,需要通过服务器转发状态信息,增加了通信延迟。 -重连方法:玩家重新加入游戏时,从服务器获取当前游戏状态并进行同步。 -**** 帧同步: -优点: -游戏状态的同步更加精确,玩家之间的交互更加真实。 -降低了对服务器的依赖,减少了通信延迟。 -缺点: -对带宽和服务器资源的需求较大,特别是在大规模多人游戏中。 -需要解决客户端之间的同步问题,可能会引入复杂的逻辑。 -重连方法:玩家重新加入游戏时,根据当前帧状态进行同步,可能需要额外的校准机制以确保同步正确。 -选择适合的同步方案需要考虑游戏的性质、规模和网络环境等因素。常见的做法是在实际开发中根据需求综合考虑各种因素,并结合状态同步和帧同步的优点来设计网络同步方案。 -** [[https://lifan.tech/2020/03/08/game/game-ranking/][跳跃表排行榜]] -*** 1000人以下直接map 原理红黑树 -*** 跨服排行榜 直接redis做 原理跳跃表 -** TODO 应用题 -*** 我有一个很大的文本,我要去除重复行(内存存的下 和 内存存不下) -*** 假设,我发一个比较大的UDP的包,一个40K的包,请问对端,收到这个40K的包,会乱序吗? -请问是什么原理?乱序的UDP,为何它的单个报文,会顺序正确?它是基于网络的哪一层,来保证报文不会乱序的?是IP层,还是哪一层?一个mpu的大小,也就1.5K吧,那底层肯定要拆包,那具体是哪一层,保证你mpu拆包完之后,再重组,还有序呢? -*** 基于刚刚的问题,请问TCP的粘包,又是怎么回事? -Nagle算法?不对不对不对。那我问你,Nagle算法去掉之后,他就不粘包了吗?Nagle算法只是一个参数,告诉Tcp的底层,尽快将业务包,往外推,而不是说,保证不粘包 -*** 现memcpy拷贝函数: void memcpy(void* psrc, void* pdst, size_t length) diff --git a/posts/20240826T174745--log-level__notes.org b/posts/20240826T174745--log-level__notes.org deleted file mode 100644 index 011caea..0000000 --- a/posts/20240826T174745--log-level__notes.org +++ /dev/null @@ -1,33 +0,0 @@ -#+title: LogLevel -#+date: [2024-08-26 Mon 17:47] -#+filetags: :notes: -#+identifier: 20240826T174745 -#+description: Teach you how to use log levels correctly -* 正确的选择 log 级别 - 开发一个应用,日志的重要性不言而喻。然而有时会发现日志中会出现大量的垃圾日志。 - 所谓垃圾日志,就是不需要知道的日志,或者这些日志对于应用查看、跟踪没有什么作用。 - 也正是(但不仅仅是)出于这些问题的考量,常用的日志框架都设置了日志级别。 - 但是在写程序时,这些日志级别该选择哪一种呢,这点并没有一个统一的标准,也没有人教你怎么做。 - 下面就来说说我在开发中是怎么使用这些日志级别的。 - 一般来说,日志级别有以下几个: - FATAL(CRITICAL) - ERROR - WARN - INFO - DEBUG - 它们的权重从大到小。当我们设置好 log 级别后,比它权重低的其他 log 都会被忽略。 - 不同的语言,不同的库有不同的 log 实现,使用方法也比较简单。但运用好 log 的关键不在库本身,而是在恰当的地方使用合适的 log 级别。 - 在不同的场景下,应该选择相应的 log 级别。 -*** FATAL(CRITICAL) -代表发生了最严重的错误,会导致整个服务停止(或者需要整个服务停止)。简单地说就是服务死掉了。 -*** ERROR -代表发生了必须马上处理的错误。此类错误出现以后可以允许程序继续运行,但必须马上修正,如果不修正,就会导致不能完成相应的业务。 -*** WARN -代表存在潜在的错误,或者触发了容易引起错误的操作。程序可以继续运行,但必须多加注意。 -*** INFO -此输出级别常用语业务事件信息。例如某项业务处理完毕,或者业务处理过程中的一些信息。 -此输出级别也常用于输出一些对系统有比较大的影响的需要被看到的 message,例如数据库更新,系统发送了额外的请求等。 -*** DEBUG (或者 TRACE、FINE) -此输出级别用于开发阶段的调试,可以是某几个逻辑关键点的变量值的输出,或者是函数返回值的验证等等。 -另外,如果是你写的一些 util 工具类,在需要加日志的情况下,也可以使用 debug。 -如果你写的是 Helper(业务的辅助类),这应该算是业务处理相关的,所以应该用 info。 diff --git a/posts/20240826T175853--emacs-themes__emacs.org b/posts/20240826T175853--emacs-themes__emacs.org deleted file mode 100644 index 31d989e..0000000 --- a/posts/20240826T175853--emacs-themes__emacs.org +++ /dev/null @@ -1,277 +0,0 @@ -#+title: Emacs Themes -#+date: [2024-08-26 Mon 17:58] -#+filetags: :emacs: -#+identifier: 20240826T175853 -#+description: It's important to feel at home in your beloved editor, so this post is about creating custom themes in Emacs. -* Creating Emacs custom themes - - -#+attr_html: :class tldr -#+begin_div -[TLDR] It's important to feel at home in your beloved editor, so this post is -about creating custom themes in Emacs. -#+end_div - -** Why? - -Apart from the fact that of course we want Emacs to look pretty the -right theme can help to increase your productivity and optimise your -coding workflow: in the best case a theme makes it easy to read your code and -spot mistakes while being friendly for your eyes. - -** How? - -*** Creating the theme - -Creating themes in Emacs is really easy :) -Just type M-x customize-create-theme (for Emacs newbies: "M" means the alt key). -You'll be asked if you want to have common faces inserted into your theme, -that's pretty handy. Then you can choose a creative title and write a description -for your new theme: - -\\ - -#+begin_export html -
    -create theme screenshot - -
    Figure 1: A screenshot of the customize theme buffer
    -
    -#+end_export -\\ -Probably you also want to add additional faces for the modes you use, -you can do so with the "Insert Additional Face" option close to the -end of the buffer: - -\\ - -#+begin_export html -
    -create theme screenshot - -
    Figure 1: A screenshot of the customize theme buffer
    -
    -#+end_export - - -Themes don't only contain faces but also variables, you can set them in -this bufffer, too. - -*** Selecting the theme - - -When your outburst of creativity is over and you're done with customizing your -theme save it by either using the "Save Theme" button or typing C-x C-s (for Emacs -newbies: "C" means the control key). -The theme is saved as "your-creative-theme-name"-theme.el in the directory -specified by the variable custom-theme-directory. So if you haven't changed -that variable you'll probably find your new theme in the ~/.emacs.d/ directory. - -To select the eyecandy you just created as your new theme type M-x customize-themes -or (if your prefer to use the Menu Bar) click Options->Customize Emacs->Custom Themes. -You'll find yourself in the Custom Themes buffer where you can select one or more -themes. - -Emacs looks for the themes displayed in this buffer in custom-theme-directory and -etc/themes of your Emacs installation (that's where the themes that come with Emacs -are located). If you want to add additional directories just add them to the -list "custom-theme-load-path" (for example sth. like (add-to-list -'custom-theme-load-path "~/.my-special-theme-dir"...). - -Save the theme(s) with the "Save Theme Settings" button or by typing C-x C-s. - -\\ - -#+begin_export html -
    -create theme screenshot - -
    Figure 1: A screenshot of the customize theme buffer
    -
    -#+end_export - - -\\ - -Alternatively you can load a theme in your current session by typing M-x load-theme. -(or if it's been loaded before with M-x enable-theme, guess the command for disabling -it ;)). - -Note that loading a theme executes Lisp code, so make sure you know what you're loading. - -** I'm lazy - - -The method described above is not that much effort ;) But if it's -still too much effort (and I totally understand that) for you you could -use the Emacs Theme Creator by Martin Haesler: - -\\ - -#+begin_export html -
    -create theme screenshot - -
    Figure 1: A screenshot of the customize theme buffer
    -
    -#+end_export - - -\\ - -And a [[https://emacs-theme-creator.appspot.com/][link]] to it. - - - -** What else? - - -Since themes in Emacs are simply lisp files you can also edit them directly. -You should have a call to deftheme at the beginning of your file and provide-theme -at the end. -The function custom-theme-set-faces contains your face settings, if you -have custom variables you can set them in custom-theme-set-variables. - -As an example, here's my cactus-theme (feel free to use it and add the faces you -need): - -#+begin_src emacs-lisp -(deftheme cactus - "Cactus Theme") - -(custom-theme-set-faces - 'cactus - '(cursor ((t (:background "cadet blue")))) - '(border ((t (:foreground "gray6")))) - '(default ((t (:background "gray20" :foreground "light sea green")))) - '(error ((t (:foreground "goldenrod" :weight bold)))) - '(match ((t (:background "gray31")))) - '(mouse ((t (:foreground "goldenrod")))) - '(region ((t (:background "cadet blue")))) - '(scroll-bar ((t (:background "black" :foreground "cadet blue")))) - '(tool-bar ((t (:background "cadet blue" :foreground "black" :box (:line-width 1 :style released-button))))) - '(tooltip ((t (:inherit variable-pitch :background "pale goldenrod" :foreground "black")))) - '(warning ((t (:foreground "deep sky blue" :weight bold)))) - - - '(beacon-fallback-background ((t (:background "goldenrod")))) - '(bold ((t (:foreground "light sea green" :weight bold)))) - - '(comint-highlight-input ((t (:foreground "dark gray" :weight bold)))) - '(comint-highlight-prompt ((t (:foreground "dark goldenrod")))) - - '(company-echo-common ((t (:foreground "white smoke")))) - '(company-preview ((t (:background "light sky blue" :foreground "dim gray")))) - '(company-preview-common ((t (:inherit company-preview :foreground "goldenrod")))) - '(company-preview-search ((t (:inherit company-preview :background "sky blue")))) - '(company-scrollbar-bg ((t (:background "light gray")))) - '(company-scrollbar-fg ((t (:background "dim gray")))) - '(company-template-field ((t (:background "royal blue" :foreground "white")))) - '(company-tooltip ((t (:background "royal blue" :foreground "white smoke")))) - '(company-tooltip-annotation ((t (:foreground "light blue")))) - '(company-tooltip-common ((t (:foreground "cyan2")))) - '(company-tooltip-selection ((t (:background "light blue")))) - - '(compilation-mode-line-exit ((t (:inherit compilation-info :foreground "blue3" :weight bold)))) - '(compilation-mode-line-fail ((t (:inherit compilation-error :foreground "dark cyan" :weight bold)))) - - '(custom-button ((t (:background "cadet blue" :foreground "black" :box (:line-width 2 :style released-button))))) - '(custom-button-pressed-unraised ((t (:inherit custom-button-unraised :foreground "dark gray")))) - '(custom-invalid ((t (:background "blue1" :foreground "white smoke")))) - '(custom-rogue ((t (:background "black" :foreground "white")))) - '(custom-state ((t (:foreground "cornflower blue")))) - '(custom-modified ((t (:background "steel blue" :foreground "white")))) - '(custom-themed ((t (:background "cornflower blue" :foreground "white")))) - - '(diary ((t (:foreground "goldenrod")))) - - '(escape-glyph ((t (:foreground "medium aquamarine")))) - '(font-lock-builtin-face ((t (:foreground "medium aquamarine")))) - '(font-lock-comment-face ((t (:foreground "gray40")))) - '(font-lock-constant-face ((t (:foreground "aquamarine")))) - '(font-lock-function-name-face ((t (:foreground "cyan3")))) - '(font-lock-keyword-face ((t (:foreground "dark goldenrod")))) - '(font-lock-negation-char-face ((t (:foreground "cyan")))) - '(font-lock-string-face ((t (:foreground "dark gray")))) - '(font-lock-type-face ((t (:foreground "dark goldenrod")))) - '(font-lock-variable-name-face ((t (:foreground "medium aquamarine")))) - '(font-lock-warning-face ((t (:foreground "deep sky blue")))) - '(fringe ((t (:background "gray12")))) - '(haskell-constructor-face ((t (:foreground "DarkGoldenrod3")))) - '(header-line ((t (:inherit mode-line :background "grey20" :foreground "light gray" :box nil)))) - '(helm-action ((t (:foreground "cyan4" :underline t)))) - '(helm-buffer-archive ((t (:foreground "goldenrod")))) - '(helm-buffer-directory ((t (:background "LightGray" :foreground "gray15")))) - '(helm-buffer-not-saved ((t (:foreground "goldenrod")))) - '(helm-buffer-process ((t (:foreground "dark goldenrod")))) - '(helm-buffer-saved-out ((t (:background "black" :foreground "dark gray")))) - - '(helm-candidate-number ((t (:background "gray" :foreground "black")))) - '(helm-ff-denied ((t (:background "black" :foreground "gold")))) - '(helm-ff-directory ((t (:background "LightGray" :foreground "orange4")))) - '(helm-ff-executable ((t (:foreground "gainsboro")))) - '(helm-ff-invalid-symlink ((t (:background "steel blue" :foreground "black")))) - '(helm-ff-prefix ((t (:background "dark goldenrod" :foreground "black")))) - '(helm-ff-socket ((t (:foreground "gold")))) - '(helm-ff-suid ((t (:background "dark goldenrod" :foreground "white")))) - '(helm-grep-file ((t (:foreground "cyan" :underline t)))) - '(helm-grep-finish ((t (:foreground "gainsboro")))) - '(helm-header-line-left-margin ((t (:background "dark goldenrod" :foreground "black")))) - '(helm-locate-finish ((t (:foreground "gainsboro")))) - '(helm-mode-prefix ((t (:background "gold" :foreground "black")))) - '(helm-prefarg ((t (:foreground "gainsboro")))) - '(helm-resume-need-update ((t (:background "gainsboro")))) - '(helm-selection ((t (:background "cadet blue" :distant-foreground "black")))) - '(helm-separator ((t (:foreground "dark goldenrod")))) - '(helm-visible-mark ((t (:background "cadet blue" :foreground "black")))) - '(highlight ((t (:background "dark cyan" :foreground "black")))) - '(info-node ((t (:foreground "light gray" :slant italic :weight bold)))) - '(isearch ((t (:background "light sea green" :foreground "white")))) - '(lazy-highlight ((t (:background "dim gray")))) - '(link ((t (:foreground "deep sky blue" :underline t)))) - '(link-visited ((t (:inherit link :foreground "steel blue")))) - '(minibuffer-prompt ((t (:foreground "dark cyan")))) - '(mode-line ((t (:background "cadet blue" :foreground "black" :box (:line-width -1 :style released-button))))) - '(mode-line-buffer-id ((t (:foreground "pale goldenrod" :weight bold)))) - '(mode-line-highlight ((t (:box (:line-width 2 :color "medium aquamarine" :style released-button))))) - '(mode-line-inactive ((t (:inherit mode-line :background "PaleTurquoise4" :foreground "grey80" :box (:line-width -1 :color "grey40") :weight light)))) - '(org-agenda-calendar-event ((t (:foreground "dark cyan")))) - '(org-agenda-calendar-sexp ((t (:foreground "dark cyan")))) - '(org-agenda-clocking ((t (:background "dim gray" :foreground "black")))) - '(org-agenda-column-dateline ((t (:background "dark cyan" :foreground "gray")))) - '(org-agenda-current-time ((t (:foreground "goldenrod")))) - '(org-agenda-date-weekend ((t (:foreground "deep sky blue" :weight bold)))) - '(org-agenda-done ((t (:foreground "goldenrod")))) - '(org-checkbox ((t (:foreground "cyan" :weight bold)))) - '(org-checkbox-statistics-done ((t (:foreground "gold" :weight bold)))) - '(org-checkbox-statistics-todo ((t (:foreground "gainsboro" :weight bold)))) - '(org-date-selected ((t (:foreground "dark gray" :inverse-video t)))) - '(org-done ((t (:foreground "cyan" :weight bold)))) - '(org-drawer ((t (:foreground "dark gray")))) - '(org-formula ((t (:foreground "gold3")))) - '(org-level-1 ((t (:foreground "light sea green")))) - '(org-level-2 ((t (:foreground "cyan")))) - '(org-level-3 ((t (:foreground "goldenrod")))) - '(org-level-4 ((t (:foreground "medium aquamarine")))) - '(org-mode-line-clock-overrun ((t (:inherit mode-line :background "gold")))) - '(org-scheduled ((t (:foreground "medium spring green")))) - '(org-scheduled-today ((t (:foreground "turquoise1")))) - '(org-todo ((t (:foreground "gold" :weight bold)))) - - '(scroll-bar ((t (:background "black" :foreground "cadet blue")))) - '(show-paren-mismatch ((t (:background "dark goldenrod" :foreground "white")))) - '(success ((t (:foreground "dark turquoise" :weight bold)))) - - '(trailing-whitespace ((t (:background "dark goldenrod")))) - '(tty-menu-disabled-face ((t (:background "dark cyan" :foreground "lightgray")))) - '(tty-menu-enabled-face ((t (:background "dark cyan" :foreground "yellow" :weight bold)))) - '(tty-menu-selected-face ((t (:background "DarkSlateGray3" :foreground "black")))) - '(vimish-fold-overlay ((t (:background "dim gray" :foreground "cadet blue")))) - '(widget-button-pressed ((t (:foreground "gray")))) - '(widget-documentation ((t (:foreground "cyan")))) - '(window-divider ((t (:foreground "gray15"))))) - -(provide-theme 'cactus) - -#+end_src -\\ diff --git a/posts/20240826T180231--manjaro-install__notes.org b/posts/20240826T180231--manjaro-install__notes.org deleted file mode 100644 index a3874e4..0000000 --- a/posts/20240826T180231--manjaro-install__notes.org +++ /dev/null @@ -1,81 +0,0 @@ -#+title: Manjaro Install -#+date: [2024-08-26 Mon 18:02] -#+filetags: :notes: -#+identifier: 20240826T180231 -#+description: How to build your linux work environment from scratch -* Manjaro-i3wm -** download =manjaro-i3wm.iso= -[[https://manjaro.org/products/download/][manjaro-download]] -** install iso file -vmware or virtualbox -** change mirror to china -sudo pacman-mirrors -c China -** resolve garbled codes -sudo pacman -Sy wqy-zenhei -* Software -** chezmoi -#+begin_src shell -sudo pacman -S chezmoi -chezmoi init --apply donneyluck -#+end_src -** yay -#+begin_src shell -sudo pacman -S yay -#+end_src -** vim -#+begin_src shell -sudo pacman -S vim -#+end_src -** thefuck -sudo pacman -S thefuck -** fish -sudo pacman -S fish -chsh -S fish -** oh-my-fish -curl -L https://get.oh-my.fish | fish -** fisher -sudo pacman -S fisher -** picom -sudo pacman -S picom -** feh -sudo pacman -S feh -** alacritty -sudo pacman -S alacritty -** rofi -sudo pacman -S rofi -** clash-verge -yay -S clash-verge-bin -** lightdm-webkit2-greeter -sudo pacman -S lightdm-webkit2-greeter -** lightdm-theme -sudo pacman -S lightdm-webkit2-theme-tty-git -** google-chrome -#+begin_src shell -yay -S google-chrome -#+end_src -add "export BROWSER=/usr/bin/google-chrome-stable" to .profile -** mongodb -yay -S mongodb-bin -** dbeaver -** navicat -* Font -#+begin_src shell -yay -S ttf-jetbrains-mono-git -sudo pacman -S ttf-font-awesome -#+end_src -* Time -#+begin_src shell -sync time -sudo timedatectl set-ntp true -#+end_src -* Chinese Input -#+begin_src shell -sudo pacman -S fcitx5 fcitx5-configtool fcitx5-qt fcitx5-gtk fcitx5-chinese-addons fcitx5-material-color -#+end_src - -add to /etc/environment @@html:
    @@ -GTK_IM_MODULE=fcitx@@html:
    @@ -QT_IM_MODULE=fcitx@@html:
    @@ -XMODIFIERS=@im=fcitx@@html:
    @@ -SDL_IM_MODULE=fcitx@@html:
    @@ -GLFW_IM_MODULE=ibus@@html:
    @@ diff --git a/posts/20240924T094433--gitworkflow__notes.org b/posts/20240924T094433--gitworkflow__notes.org deleted file mode 100644 index c2c3b12..0000000 --- a/posts/20240924T094433--gitworkflow__notes.org +++ /dev/null @@ -1,70 +0,0 @@ -#+title: GitWorkFlow -#+date: [2024-09-24 Tue 09:44] -#+filetags: :notes: -#+identifier: 20240924T094433 -#+description: How to use git collaboration in daily development -* GitWorkFlow -Git 是一个分布式版本控制系统,它支持多种工作流程。以下是一些常见的 Git 工作流程: -** 集中式工作流: -适用于小型团队或项目。 -通常只有一个主分支(如 main 或 master)和几个长期分支(如 develop、feature)。 -开发者直接在 develop 分支上工作,完成后合并到 main。 - -** 功能分支工作流: -适用于大多数团队 -从 main 或 develop 分支创建一个新分支来开发新功能。 -开发完成后,通过 Pull Request (PR) 或者直接合并到 develop 分支。 -然后从 develop 分支合并到 main。 - -** Gitflow 工作流: -适用于发布周期固定的项目 -有 main、develop、feature、release、hotfix 等分支 -feature 分支用于开发新功能,完成后合并到 develop -release 分支用于准备发布,完成后合并到 main 和 develop -hotfix 分支用于修复 main 分支上的紧急问题,完成后合并到 main 和 develop - -** Forking 工作流: -适用于开源项目 -每个开发者都有自己的仓库。 -开发者在自己的仓库上创建分支进行开发。 -通过 Pull Request 将更改提交到原始仓库。 - -** Pull Request 工作流: -适用于团队协作 -开发者在本地仓库的 main 分支上创建新的分支进行开发。 -开发完成后,将本地分支推送到远程仓库。 -在远程仓库上创建 Pull Request,请求将更改合并到 main 分支。 -其他开发者可以审查代码,通过后合并。 - -** 以下是一般的工作方式 -- 确保本地仓库是最新的: -#+begin_src bash -git checkout main -git pull origin main -#+end_src -- 创建新的本地分支: -#+begin_src bash -git checkout -b feature/your-feature-name -#+end_src -- 在新分支上进行开发: -编写代码,提交更改 -推送新分支到远程仓库: -#+begin_src bash -git push origin feature/your-feature-name -#+end_src -- 创建 Pull Request: -在远程仓库(如 GitHub)上创建 Pull Request, 请求将你的 feature/your-feature-name 分支合并到 develop 分支 -等待代码审查: -其他开发者审查代码,提出反馈。 -- 合并分支: -代码审查通过后,合并 Pull Request 到 develop 分支。 -将 develop 分支合并到 main: -当 develop 分支稳定后,可以将其合并到 main 分支。 -- 删除旧的本地和远程分支(可选): -#+begin_src bash -git branch -d feature/your-feature-name -git push origin --delete feature/your-feature-name -#+end_src -这是一个基本的工作流程,具体可能会根据团队的需求和习惯有所不同。 - - diff --git "a/posts/20241017T202935--\346\226\207\345\274\217\347\274\226\347\250\213__notes.org" "b/posts/20241017T202935--\346\226\207\345\274\217\347\274\226\347\250\213__notes.org" deleted file mode 100644 index ef64a02..0000000 --- "a/posts/20241017T202935--\346\226\207\345\274\217\347\274\226\347\250\213__notes.org" +++ /dev/null @@ -1,549 +0,0 @@ -#+title: 文式编程 -#+date: [2024-10-17 Thu 20:29] -#+filetags: :notes: -#+identifier: 20241017T202935 -#+description: 认识并了解什么是文式编程 -* 文式编程 -** 文学编程 -文学编程 (Literate programming) 的一些概念,上个世纪 70 年代就有人提出来了。 - -文学编程的思想非常简单,就是将那些为了能被编译器/解释器正确识别而编写的代码打碎,然后用人类语言将它们编织到文档中,这种文档就是文学编程的源文件。这一概念第一次被完整的实现,是 Knuth 开发的 WEB 工具(此 WEB 并非现代漫天飞舞的那个 Web)。Knuth 的神作——TeX 系统便是借助 WEB 开发的。 - -WEB 工具由 tangle 与 weave 这两个程序构成。tangle 程序从文学编程的源文件中提取复合编译器/解释器逻辑的程序代码。weave 程序将文学编程的源文件转换为 TeX 源文件,然后由 TeX 系统排版处理,生成程序文档。这种程序文档使作者能在以后的任何时间重新找到自己的思路,也能使其他程序员更容易理解程序的建构过程。 - -在代码里写大量的注释,或者像 Doxygen 之类从具有特定注释格式的代码中产生文档的工具,这些都不算文学编程。文学编程强调的是,代码出现的顺序应该按照人的逻辑,而不是编译器的。 -** 很近的遥远 -文学编程,从未真正的被广泛应用,但是每个程序猿都曾经看到过它的影子。阅读一本讲 Linux 内核的书籍,远比阅读 Linux 内核源码容易的多。 - -每一本讲编程技术的书籍都是以类似『文学编程』的方式写成的。我们在阅读这些书籍时,总是先关心作者说了些什么,然后再看他给出的示例代码。事实上,有本很古老但是很有名而且现在也不过时的书,它的中文译本叫《C 语言接口与实现——创建可重用软件的技术》,这本书就是基于文学编程的方式实现的,作者用的文学编程工具叫 noweb——后面我会介绍它。 - -既然文学编程如此的美好,那么为何它一直没有被广泛应用呢?答案很简单,写书(不是那种烂书)要比写程序难得多。因此,写面向人类读者的文学程序要比写面向机器的代码难得多。[[https://www.cs.tufts.edu/~nr/noweb/][noweb]]的作者在 noweb 的源文件里说:One of my observations is that the cost of creating a high quality, well-documented literate program adds 1-3 times of amount of effort it took to create the program in the first place. - -大部分程序猿并不那么热爱编程,他们仅仅是将编程当作养家糊口的一种手段。采用文学编程方式干活,要比面向机器编程多付出 1 到 3 倍的努力,而薪酬却不变……太多的正常人是绝对不会这么虐待自己的。 - -这个世界从不缺乏足够好的东西,只是缺乏足够好的人。 -** 它文学么? -什么是文学?据说,文学是以语言文字为工具,形象化地反映客观现实、表现作家心灵世界的艺术。 - -虽然文学编程用的都是语言文字,但是它的目标却不是形象化的反映客观现实或表现作者心灵世界的,它的目标是写出让人类与机器都能能容易的解读的『文档』。编程的本质,是让计算机执行确定的计算。计算的本质是数学意义上的。如果数学还没有变成文学,那么编程永远也无法变成文学。 - -这一点决定了文学编程并不能用来搞文学,所以不要被它的名字所迷惑。要搞文学创作,只能热爱生活,认真思考,锤炼文字。 -** 论文式编程 -Knuth 明确提出了文学编程的概念,并付诸于实践,开发了 TeX、MetaFont 以及 MMIX 元模拟器这些大的程序,此外还出版了一本阐述文学编程的专著。文学编程对于他的重要作用,用他自己的话来说,就是:『文学编程确实是由TeX项目衍生出来的最重要的东西。它不仅让我前所未有地更快地写和维护可靠性更高的程序,而且成为我自20世纪80年代以来的最大的快乐之源——它有时实际上是不可或缺的。我做的其它一些大程序,比如 MMIX 元模拟器,用我见过的任何一种其它的方法论是无法写出来的。其复杂性让我有限的智能望而却步。没有文学编程,我的整个事业规划就会轰然倒塌。……文学编程是你更上一层楼的必要工具。』 - -如果我们打算用文学编程的方式编程,那么必须要注意,Knuth 是计算机领域的科学家!我不是在鼓吹他的个人头衔,而是强调他的身份。科学家,是这个世界上最会写论文的一群人。他们探索未知区域,在失败中前进,认真总结自己的发现,最后以严肃的论文或专著的形式将自己的发现公布于世。TeX 对于我们来说是一个程序,但它对于 Knuth 来说是一本讲述计算机排版技术的专著。也就是说,如果想用好文学编程,那么首先你得学会如何写文章以及如何写科技文献。 - -写一个程序与写一本专著,写一个程序模块与写一篇科技论文,它们之间是存在着对应关系的。写程序,首先要明确程序所解决的问题,然后将问题拆分为更小的问题,每个问题都用一个比较小的程序模块来解决,最后将各个模块组装起来得到程序。做科研的人,首先要明确自己要研究的主题,然后将主题分解为一些小的主题,对小的主题展开研究,每个研究成果都以论文的形式发表,最后将整个主题的相关论文整合起来形成专著。 - -所以,我觉得文学编程,应该叫论文式编程,至少也该叫『文式编程』。如果按后者来理解,那么本文的标题应该这么读『论(文式编程)』:) - -如果我们像写一篇论文那样来写一个程序模块,大致的过程应该是: - -引言(背景):这个程序模块要解决什么问题?要解决这个问题,有哪些现有的资源?我该如何利用这些资源来解决问题? -方法(算法与程序):准确描述利用现有资源解决问题的全部步骤,有序的组织程序代码。 -结果(单元测试):验证方法是否正确。 -讨论:使用这个模块需要注意什么,它还存在哪些不足,应该怎样弥补。 -其实,我们在写每一个程序模块时,都会经历上述的思考与实现过程,我们最终所得到的是代码,这个过程中我们的那些思考活动却很少被记录下来。很多人说,我要去读 XXXX 项目的源代码,这是学习 XXXX 的最好方法。这种观点其实并不正确,因为你阅读源代码的过程,实际上就是在猜测这些代码当初是怎样写出来的。大可以放心,无论你怎么猜测,你只能得到一个很模糊的结果,因为那些原本很确切的信息已经永远的丢失了,甚至连当初写这些代码的人可能也想不起来了,他们留给你的只是一个巨大的迷宫。 - -虽然有一些代码是自明的,但是,显然这些代码也都是非常简单的。对一个矩阵进行奇异值分解(SVD)的代码,无论怎么写,它也无法是自明的,除非你去阅读一篇阐述矩阵奇异值分解算法的论文。 -** 示例 -作为示例,我要用论文式编程的方式来写一个遗传算法的程序。这个程序的源码如下: -#+begin_src noweb -% -*- mode: Noweb; noweb-code-mode: python-mode -*- -\title{Hello!遗传算法} -\cprotect\author{garfileo\\ \verb|lyr_m2@live.cn|} -\date{\today} -\maketitle - -\tableofcontents -\newpage - -\section*{引言} - -这篇文章讲述如何利用遗传算法解决一个二元函数的最大值求解问题。由于我对遗传算法的理解处于菜鸟级别,所以本文所讲的方法以及所写的程序不一定正确。之所以写这篇文章,是因为我已经烦透了教科书或论文里对遗传算法那么刻板的叙述,所以很想写一篇稍微轻松一点的入门文档,娱乐一下。 - -\section{问题} - -这个二元函数是这样的: - -$$f(x,\,y)=0.5-\frac{\sin^2{\sqrt{x^2+y^2}-0.5}}{1+0.001(x^2+y^2))^2}$$ - -要是我能够在大脑中直接生成这个函数的图像就好了,可惜我不能够,所以用 gnuplot 画了一下。 - -\begin{figure}[htbp] -\centering -\includegraphics[width=6cm]{f.png} -\caption[目标函数]{待求最大值的目标函数} -\end{figure} - -这个函数像是平静的池塘里丢了一颗小石子激起的波纹。我们的任务是计算它在 $x\in [-10,\,10],\;y\in [-10,\,10]$ 范围之内的最大值。 - -这个函数有无限个极大值,但是仅有一个最大值,位于 $(0, 0)$ 点,值为 1。如果你的微积分学知识还没有遗忘,可以用数学方法求解一番。不幸的是,我已经忘光了,所以我只好用遗传算法进行求解。遗传算法的特点之一就是:{\bf 不需要求导或其他辅助知识,而只需要影响一些可以影响搜索方向的目标函数和相应的适应度函数}。所谓目标函数,就是要求解的函数,也就是上述的那个函数。至于适应度函数,下文再行介绍。 - -\section{创建染色体} - -我唯一接触到生物学是在我的初中时代。就读的那个初中学校是一个落后的乡村中学,不过却拥有一个很好的教生物的老师,但是悲剧的是我在那时是一个不喜欢上课的懵懂无知的少年。现在为了理解遗传算法,我只好将『染色体』理解成一根带子,上面写着一组数据。据说这组数据记录着我们应该长成什么样子,具备什么样的天赋,可能会生什么疾病等内容。如果上帝能够将『语言程序』记录在我们的染色体中,也许我们刚生下来就可以说上百种人类语言还有火星语了。 - -虽然我们不是上帝,但是我们也可以创造染色体,例如 $000110001100$ 或者 $000XXX00XXX0X$. 这是一件很容易的事情,而真正困难的是如何在染色体中记录信息。由于用二进制来表示染色体比较方便程序计算,所以本文选择了这种最简单的方式。 - -现在,尝试为 $f(x, y)$ 的最大值所对应的 $x$ 和 $y$ 的值构造染色体。也就是说,要在一组二进制位中存储 $f(x, y)$ 的定义域中的数值信息。 - -显然,函数 $f(x, y)$ 的定义域所包含的数值是无限多的,但是基于采样的办法可以得到有限集。例如,对于 $[-10,\,10]$ 这个区间,我们可以将它平均划分为 $20\times 10^6$ 个子区间,便得到精度为 8 位,小数位为 6 位的一组数值,个数为 $20\times 10^6 + 1$ 。若用一组二进制位形式的染色体来表示这个数值集合,那么我们还要考虑所用二进制位的长度。由于 $2^{24}<20\times 10^6 + 1< 2^{25}$,因此我们可以将染色体长度确定为 25 位,因为只有如此才可以让足够多的染色体表示那么多的数值,同时又不至于太浪费。虽然长度为 25 的二进制位所能表示的数值个数要多于 $20\times 10^6 + 1$,但是这并没有负面作用,相反,它可以更精确的表示区间 $[-10,\, 10]$ 中数值。 - -现在,我们已经创建了一种 25 位长度的二进制位类型的染色体,那么对于任意一个这样的染色体,我们如何将其复原为 $[-10,\,10]$ 这个区间中的数值呢?很简单,只需要使用下面的公式: - -$$f(c) = -10.0 + c\cdot\frac{10.0 - (-10.0)}{2^{25} - 1}$$ - -例 如 $0000 0000 0000 0000 0000 0000 0000 0$ 和 $1111 1111 1111 1111 1111 1111 1$ 这两个二进制数,将其化为 10 进制数,代入上式,可得 -10.0 和 10.0。这意味着长度为 25 位的二进制数总是可以通过上式转化为 $[-10,\,10]$ 区间中的数。 - -\section{个体、种群与进化} - -染色体表达了某种特征,这种特征的载体,可以称为『个体』。例如,我本人就是一个『个体』,我身上载有 23 对染色体,也许我的相貌、性别、性格等因素主要取决于它们。众多个体便构成『种群』。 - -对于本文所要解决的二元函数最大值求解问题,个体可采用上一节所构造的染色体表示,并且数量为 2 个,其含义可理解为函数 $f(x, y$) 定义域内的一个点的坐标。许多这样的个体便构成了一个种群,其含义为一个二维点集,包含于对角定点为 $(-10.0, -10.0)$ 和 $(10.0, 10.0)$ 的正方形区域。 - -也许有这样一个种群,它所包含的个体对应的函数值会比其他个体更接近于函数 $f(x, y)$ 的理论最大值,但是它一开始的时候可能并不比其他个体优秀,它之所以优秀是因为它选择了不断的进化,每一次的进化都要尽量保留种群中的优秀个体,淘汰掉不理想的个体,并且在优秀个体之间进行染色体交叉,有些个体还可能出现变异。种群的每一次进化后,必定会产生一个最优秀的个体。种群所有世代中的那个最优个体也许就是函数 $f(x, y)$ 的最大值对应的定义域中的点。如果种群不休止的进化,它总是能够找到最好的解。但是,由于我们的时间是有限的,有可能等不及种群的最优进化结果,通常是在得到了一个看上去还不错的解时,便终止了种群的进化。 - -那么,对于一个给定的种群,如何赋予它进化的能力呢? - -\begin{itemize} -\item {\bf 选择}:对于种群的每一代个体,可以用一个适应度函数(也叫评估函数)计算个体的适应度,根据适应度可以计算出个体的生存几率。适应度较大的个体被保留的可能性也较大,反之被淘汰的可能性较大。 -\item {\bf 交叉}:在一定的概率下对两个个体的染色体进行交叉重组,从而得到两个新个体。 -\item {\bf 变异}:些个体的染色体会以一定的概率发生变化。 -\end{itemize} - -达尔文的进化论也许并不正确,但是它对于我们运用这种理论来计算问题并没有什么错误的影响。我们不管人类是否是由猿猴进化来的,还是由别的什么生物。那些进化论的反对者总是想用自己的理论推翻进化论,不过他们的理论却往往无法用于计算!基督徒们相信世界末日,也许只是因为上帝的时间也很有限,等不及人类进化到最优解,于是就设定了人类进化的最大世代数。 - -\section{种群} - -如果你不熟悉 python 语言,那么请原谅我使用了它。我将种群声明为 python 的一个类: - -<<种群>>= -class Population -@ - -种群的初始化过程就是 `Population` 类的初始化函数: - -<<种群初始化>>= -def __init__ (self, size, chrom_size, cp, mp, gen_max): - self.individuals = [] # 个体集合 - self.fitness = [] # 个体适应度集合 - self.selector_probability = [] # 个体选择概率集合 - self.new_individuals = [] # 新一代个体集合 - - self.elitist = {'chromosome':[0, 0], - 'fitness':0, - 'age':0} # 最佳个体的信息 - - self.size = size # 种群所包含的个体数 - self.chromosome_size = chrom_size # 个体的染色体长度 - self.crossover_probability = cp # 个体之间的交叉概率 - self.mutation_probability = mp # 个体之间的变异概率 - - self.generation_max = gen_max # 种群进化的最大世代数 - self.age = 0 # 种群当前所处世代 - - # 随机产生初始个体集,并将新一代个体、适应度、选择概率等集合以 0 值进行初始化 - v = 2 ** self.chromosome_size - 1 - for i in range (self.size): - self.individuals.append ([random.randint (0, v), random.randint (0, v)]) - self.new_individuals.append ([0, 0]) - self.fitness.append (0) - self.selector_probability.append (0) -@ - -代码中的 [[self]] 就是种群的实例,下文中也是如此。 - -\section{选择} - -可以简单的模拟出『物竞天择』的效果:将种群的各个个体摆在一个轮盘上,然后转一下轮盘,将盘外的指针所指向的个体保留下来,然后接着转轮盘,接着选择,直至产生一组与种群原有个体数量一致的个体,这就是我们所选择的下一代。这种赌博不违法。 - -要模拟这个轮盘赌博机制,首先需要构造个体适应度评价机制: - -<<物竞天择机制>>= -def decode (self, interval, chromosome): - d = interval[1] - interval[0] - n = float (2 ** self.chromosome_size -1) - return (interval[0] + chromosome * d / n) - -def fitness_func (self, chrom1, chrom2): - interval = [-10.0, 10.0] - (x, y) = (self.decode (interval, chrom1), - self.decode (interval, chrom2)) - n = lambda x, y: math.sin (math.sqrt (x*x + y*y)) ** 2 - 0.5 - d = lambda x, y: (1 + 0.001 * (x*x + y*y)) ** 2 - func = lambda x, y: 0.5 - n (x, y)/d (x, y) - return func (x, y) - -def evaluate (self): - sp = self.selector_probability - for i in range (self.size): - self.fitness[i] = self.fitness_func (self.individuals[i][0], - self.individuals[i][1]) - ft_sum = sum (self.fitness) - for i in range (self.size): - sp[i] = self.fitness[i] / float (ft_sum) - for i in range (1, self.size): - sp[i] = sp[i] + sp[i-1] -@ - -[[decode]] 函数可以将染色体 [[chromosome]] 映射为区间 [[interval]] 之内的数值。[[fitness_func]] 是适应度函数,可以根据个体的两个染色体计算出该个体的适应度,这里直接采用了本文所要求解的目标函数 - -$$f(x,\,y)=0.5-\frac{\sin^2{\sqrt{x^2+y^2}-0.5}}{1+0.001(x^2+y^2))^2}$$ - -作为适应度函数。 - -[[evaluate]] 函数用于评估种群中的个体集合 [[self.individuals]] 中各个个体的适应度,即将各个个体的 2 个染色体代入 [[fitness_func]] 函数,并将计算结果保存在 [[self.fitness]] 列表中,然后将 [[self.fitness]] 中的各个个体适应度除以所有个体适应度之和,得到各个个体的生存概率。为了适合轮盘赌博游戏,需要将个体的生存概率进行叠加,从而计算出各个个体的选择概率。例如有 5 个个体,根据其适应度计算的生存概率与选择概率如表 \ref{table:选择概率计算示例} 所示。 - -\begin{table}[H] -\centering -\caption{选择概率的计算结果示例} -\label{table:选择概率计算示例} -\begin{tabular}{cccc} -\toprule -\bf 个体 & \bf 适应度 & \bf 生存概率 & \bf 选择概率 \\ -\midrule -1 & 0.9042845033795694 & 0.28693981857759787 & 0.28693981857759787 \\ -2 & 0.5588628304907922 & 0.17733356990137467 & 0.46427338847897254 \\ -3 & 0.6899948769706024 & 0.21894326849291637 & 0.6832166569718889 \\ -4 & 0.3114709778723004 & 0.09883330472749545 & 0.7820499616993843 \\ -5 & 0.6868647339474463 & 0.21795003830061557 & 0.9999999999999999 \\ -\bottomrule -\end{tabular} -\end{table} - -有了这些数据,便可以构造图 \ref{fig:轮盘} 所示的轮盘赌博机了。 - -\begin{figure}[h] -\centering -\includegraphics[width=4cm]{selector.png} -\caption{轮盘赌博机} -\label{fig:轮盘} -\end{figure} - -这样的轮盘赌博机,可用 python 代码表示为: - -<<物竞天择机制>>= -def select (self): - (t, i) = (random.random (), 0) - for p in self.selector_probability: - if p > t: - break - i = i + 1 - return i -@ - -\section{染色体交叉模拟} - -<<染色体交叉机制>>= -def cross (self, chrom1, chrom2): - p = random.random () - n = 2 ** self.chromosome_size -1 - if chrom1 != chrom2 and p < self.crossover_probability: - t = random.randint (1, self.chromosome_size - 1) - mask = n << t - (r1, r2) = (chrom1 & mask, chrom2 & mask) - mask = n >> (self.chromosome_size - t) - (l1, l2) = (chrom1 & mask, chrom2 & mask) - (chrom1, chrom2) = (r1 + l2, r2 + l1) - return (chrom1, chrom2) -@ - -[[cross]] 函数可以将两个染色体进行交叉配对,从而生成 2 个新染色体。 - -此处使用染色体交叉方法很简单,先生成一个随机概率 [[p]],如果两个待交叉的染色体不同并且 [[p]] 小于种群个体之间的交叉概率 [[self.crossover_probability]],那么就在 $[0, \text{self.chromosome\_size}]$ 中间随机选取一个位置,将两个染色体分别断为 2 截,然后彼此交换一下。例如: - -\begin{verbatim} -1000 1101 1100 0010 0001 0110 1 -0001 0011 1111 1001 0010 1110 0 -\end{verbatim} - -\noindent 在第 10 位处交叉,结果为: - -\begin{verbatim} -1000 1101 1100 0011 0010 1110 0 -0001 0011 1111 1000 0001 0110 1 -\end{verbatim} - -这种染色体交叉方法叫做{\bf 单点交叉}。如果不嫌麻烦,也可以使用{\bf 多点交叉}。 - -\section{染色体变异} - -<<染色体变异机制>>= -def mutate (self, chrom): - p = random.random () - if p < self.mutation_probability: - t = random.randint (1, self.chromosome_size) - mask1 = 1 << (t - 1) - mask2 = chrom & mask1 - if mask2 > 0: - chrom = chrom & (~mask2) - else: - chrom = chrom ^ mask1 - return chrom -@ - -mutate 函数可以将一个染色体按照变异概率进行单点变异。例如: - -\begin{verbatim} -1000 1101 1100 0010 0001 0110 1 -\end{verbatim} - -\noindent 在第 13 位发生变异,结果为: - -\begin{verbatim} -1000 1101 1100 1010 0001 0110 1 -\end{verbatim} - -同交叉类似,也可以进行{\bf 多点变异}。 - -\section{进化} - -<<进化机制>>= -def evolve (self): - indvs = self.individuals - new_indvs = self.new_individuals - - # 计算适应度及选择概率 - self.evaluate () - - # 进化操作 - i = 0 - while True: - # 选择两个个体,进行交叉与变异,产生新的种群 - idv1 = self.select () - idv2 = self.select () - - # 交叉 - (idv1_x, idv1_y) = (indvs[idv1][0], indvs[idv1][1]) - (idv2_x, idv2_y) = (indvs[idv2][0], indvs[idv2][1]) - (idv1_x, idv2_x) = self.cross (idv1_x, idv2_x) - (idv1_y, idv2_y) = self.cross (idv1_y, idv2_y) - - # 变异 - (idv1_x, idv1_y) = (self.mutate (idv1_x), self.mutate (idv1_y)) - (idv2_x, idv2_y) = (self.mutate (idv2_x), self.mutate (idv2_y)) - - (new_indvs[i][0], new_indvs[i][1]) = (idv1_x, idv1_y) - (new_indvs[i+1][0], new_indvs[i+1][1]) = (idv2_x, idv2_y) - - # 判断进化过程是否结束 - i = i + 2 - if i >= self.size: - break - - # 更新换代 - for i in range (self.size): - self.individuals[i][0] = self.new_individuals[i][0] - self.individuals[i][1] = self.new_individuals[i][1] - -@ - -[[evolve]] 函数可以实现种群的一代进化计算,计算过程分为三个步骤: - -\begin{itemize} -\item 使用 [[evaluate]] 函数评估当前种群的适应度,并计算各个体的选择概率。 -\item 对于数量为 [[self.size]] 的 [[self.individuals]] 集合,循环 $\text{self.size}/ 2$ 次,每次从 [[self.individuals]] 中选出 2 个个体,对其进行交叉和变异操作,并将计算结果保存于新的个体集合 [[self.new_individuals]] 中。 -\item 用种群进化生成的新个体集合 [[self.new_individuals]] 替换当前个体集合。 -\end{itemize} - -如果循环调用 [[evolve]] 函数,那么便可以产生一个种群进化的过程,如下: - -<<进化机制>>= -def run (self): - for i in range (self.generation_max): - self.evolve () - print (i, max (self.fitness), sum (self.fitness)/self.size, - min (self.fitness)) -@ - -[[run]] 函数根据种群最大进化世代数设定了一个循环。在循环过程中,调用 [[evolve]] 函数进行种群进化计算,并输出种群的每一代的个体适应度最大值、平均值和最小值。 - -\section{开启上帝模式} - -下面的代码可以启动种群进化过程: - -<<启动一个种群的进化过程>>= -if __name__ == '__main__': - # 种群的个体数量为 50,染色体长度为 25,交叉概率为 0.8,变异概率为 0.1,进化最大世代数为 150 - pop = Population (50, 24, 0.8, 0.1, 150) - pop.run () -@ - -注意,因为个体交叉的需求,种群所包含的个体数量一般设为偶数。这个程序没考虑个体数量为奇数的情况。 - -如果将以上所有出现的 python 代码依序组装在一起,假设存为 test.py 文件: - -<>= -import math, random - -<<种群>>: - <<种群初始化>> - <<物竞天择机制>> - <<染色体交叉机制>> - <<染色体变异机制>> - <<进化机制>> - -<<启动一个种群的进化过程>> -@ - -执行以下命令便可运行这个程序: - -\begin{verbatim} -$ python3 test.py -\end{verbatim} - -注意,这里我们使用的是 python 3。如果你的系统中只安装了 python 2,要让程序能够运行,需要在 test.py 的首行添加: - -\begin{verbatim} -# -*- coding: utf-8 -*- -\end{verbatim} - -然后将 [[evolve]] 函数中的 [[print]] 语句修改为: - -\begin{verbatim} -print i, max (self.fitness), sum (self.fitness)/self.size, min (self.fitness) -\end{verbatim} - -\section{结果} - -如果使用命令: - -\begin{verbatim} -$ python3 hello-ga.py > test.log -\end{verbatim} - -那么使用下面的 gnuplot 脚本 test.gnu 可以绘制出种群的每一代最大适应度、平均适应度和最小适应度的变化情况。 - -\begin{verbatim} -#!/usr/bin/gnuplot -set term pngcairo -set size ratio 0.75 -set output 'test.png' -plot "test.log" using 1:2 title "max" with lines, \ - "test.log" using 1:3 title "ave" with lines, \ - "test.log" using 1:4 title "min" with lines -\end{verbatim} - -运行这个 gnuplot 脚本,可以生成图片文件。 - -\begin{verbatim} -chmod +x ./test.gnu -./test.gnu -\end{verbatim} - -图 \ref{fig:第一次测试的结果} 中红色的折线表示种群每一代个体中适应度最大值的变化情况,显然,我们所得结果是比较接近 $f(x,\,y)$ 理论上的最大值 1.0。蓝色折线反映了种群每一代最差个体适应度的变动情况,它的波动幅度看上去比较剧烈。如果将变异概率设为 0.4,那么它看起来就会比较温顺一些,如图 \ref{fig:第二次测试的结果} 所示。变异概率如果设置的越大,那么蓝色折线的波动幅度便会越小。图 \ref{fig:第三次测试的结果} 显示了比较极端的情况,此时变异概率设为 1.0。 - -在固定变异概率的条件下,可以用类似的方法观察一下交叉概率对计算结果的影响。 - -\begin{figure}[H] -\centering -\includegraphics[width=8cm]{ga-test.png} -\caption{交叉概率为 0.8,变异概率为 0.1,种群的进化过程} -\label{fig:第一次测试的结果} -\end{figure} - -\begin{figure}[H] -\centering -\includegraphics[width=8cm]{ga-test-1.png} -\caption{交叉概率为 0.8,变异概率为 0.4,种群的进化过程} -\label{fig:第二次测试的结果} -\end{figure} - -\begin{figure}[H] -\centering -\includegraphics[width=8cm]{ga-test-2.png} -\caption{交叉概率为 0.8,变异概率为 1.0,种群的进化过程} -\label{fig:第三次测试的结果} -\end{figure} - -\section{讨论} - -研究遗传算法的人证明了几个定理。 - -\begin{theorem} -标准遗传算法不能收敛至全局最优解。 -\end{theorem} - -本程序按照标准遗传算法实现的,从上面的几幅图也可以看出来,受交叉与变异的影响,种群的每一代个体的最大适应度都有可能在不断变化。 - -\begin{theorem} -标准遗传算法,如果在选择之前保留当前最佳个体,最终能收敛到全局最优解。 -\end{theorem} - -对于本文所实现的遗传算法,只需要添加一个可以复制当前最佳个体信息的函数,即可保证全局最优解的收敛性,如下: - -\begin{verbatim} -# 将该函数插入 Population 类中 - def reproduct_elitist (self): - # 与当前种群进行适应度比较,更新最佳个体 - j = 0 - for i in range (self.size): - if self.elitist['fitness'] < self.fitness[i]: - j = i - self.elitist['fitness'] = self.fitness[i] - if (j > 0): - self.elitist['chromosome'][0] = self.individuals[j][0] - self.elitist['chromosome'][1] = self.individuals[j][1] - self.elitist['age'] = self.age -\end{verbatim} - -然后在 [[evlove]] 函数中调用 [[reporduct_elitist]] 函数: - -\begin{verbatim} -# 修改后的 evolve 函数 - def evolve (self): - indvs = self.individuals - new_indvs = self.new_individuals - - # 计算适应度及选择概率 - self.evaluate () - - # 进化操作 - i = 0 - while True: - # 选择两个个体,进行交叉与变异,产生新的种群 - idv1 = self.select () - idv2 = self.select () - - # 交叉 - (idv1_x, idv1_y) = (indvs[idv1][0], indvs[idv1][1]) - (idv2_x, idv2_y) = (indvs[idv2][0], indvs[idv2][1]) - (idv1_x, idv2_x) = self.cross (idv1_x, idv2_x) - (idv1_y, idv2_y) = self.cross (idv1_y, idv2_y) - - # 变异 - (idv1_x, idv1_y) = (self.mutate (idv1_x), self.mutate (idv1_y)) - (idv2_x, idv2_y) = (self.mutate (idv2_x), self.mutate (idv2_y)) - - (new_indvs[i][0], new_indvs[i][1]) = (idv1_x, idv1_y) - (new_indvs[i+1][0], new_indvs[i+1][1]) = (idv2_x, idv2_y) - - # 判断进化过程是否结束 - i = i + 2 - if i >= self.size: - break - - # 最佳个体保留 - self.reproduct_elitist () - - # 更新换代 - for i in range (self.size): - self.individuals[i][0] = self.new_individuals[i][0] - self.individuals[i][1] = self.new_individuals[i][1] -\end{verbatim} - -注意,我没有将最佳个体保存在种群的个体集合中,因为我觉得一个既不参与交叉也不参与变异的个体,是不能放在种群中的,它应当存放在历史课本里。所以,我在 [[Population]] 类中设置了一个 [[elitist]] 的成员,用以记录最佳个体对应的染色体、适应度及其出现的年代。当遗传算法结束后,这个最佳个体可作为目标函数的解。 - -\begin{theorem} -遗传算法所接受的参数有种群规模、适应度函数、染色体的表示、交叉概率、变异概率等,对于这些参数而言,不存在一个最佳组合,使它对于任何问题都能达到最优性能。 -\end{theorem} - -也就是说,成功的设计一个遗传算法的关键在于针对具体问题去选择恰当的参数。如果不利用所求解一些问题的特定知识,那么算法的性能于我们采用何种参数没有多大关系,情况可能会更糟糕。还要记住的是,对于单个问题,不存在最好的搜索算法。 -#+end_src -** 为什么慢 -如果你看了上文中的论文式编程代码以及所生成的 hello-ga.pdf 文档,可能会受到一些启发,甚至在业余时间里也尝试使用 noweb 来写一些论文式程序。如果你真的这么做了,很快就会发现,事情并没有那么美好。 - -因为在写『论文』的过程中,你无法及时的验证论文中的程序代码是否正确,只能等到论文式源文件中包含了程序的一个完整的子集(即提取出的代码可被编译或解释运行),然后方有机会去测试程序代码的正确性。如果你坚持一次性的将论文写完,然后再去验证其中的程序代码是否正确,那时可能已经积攒了一大堆错误了。我们不是机器,我们大脑的容错能力非常强,所以很多代码在我们看来是『正确』的,而编译器或解释器会很生气的说 no! - -我不认为真的有人能在论文里将程序写正确。实践论文式编程,最可行的办法是先写引言部分,然后全力以赴的去写程序、验证程序的正确性,最后再将自己所写的代码论文化。所以,这个过程总是要比非文学编程方式多耗费 1~3 倍的时间。其实,这个过程与严肃的编程过程并没有什么本质区别,我们先思考,然后写代码,最后再写程序文档。文学编程的真正意义在于,它强调了文档的重要性——文档一直是程序猿最不想写的东西,并统一了文档与代码——至少在形式上是这样。 diff --git "a/posts/20241018T094654--lua-\345\222\214-c-\345\246\202\344\275\225\344\272\244\344\272\222__notes.html" "b/posts/20241018T094654--lua-\345\222\214-c-\345\246\202\344\275\225\344\272\244\344\272\222__notes.html" new file mode 100644 index 0000000..3b2f872 --- /dev/null +++ "b/posts/20241018T094654--lua-\345\222\214-c-\345\246\202\344\275\225\344\272\244\344\272\222__notes.html" @@ -0,0 +1,372 @@ + + + + +Lua 和 C 如何交互 + + +

    Lua 和 C 如何交互

    堆栈

    lua与c之间交互是通过“lua堆栈”通信的。不管是lua调用c还是c调用lua,都是通过操作lua堆栈实现的。顾名思义,lua堆栈也满足后进先出的特点,入栈/出栈都围绕栈顶进行的。与通用的栈不同的是,这个虚拟栈每个位置都对应一个索引,可以通过索引操作指定位置的数据。1代表栈底,向栈顶依次递增;-1代表栈顶,向栈底依次递减,如图。 +

    + +
    +lost + +
    Figure 1: Lua堆栈结构
    +
    +

    +

    全局表

    Lua的全局表可以想象成一个map哈希表结构,比如Lua有一个变量: +name = "hello world" +全局表中存放了name和hello world的对应关系, 可以通过name在全局表中找到对应的hello world +

    +

    lua中类型在c中如何表示

    要实现c和lua之间的交互,先了解下lua中基本类型与c中类型怎么对应的。lua中有八种基本类型:nil、boolean、number、string、table、function、userdata、thread,其中,userdata分轻量用户数据(lightuserdata)和完成用户数据(userdata)两种。这些类型都可以压入栈中,在c中统一用TValue结构表示,是一个{值,类型}结构。 +

    + +
    +lost + +
    Figure 1: lua类型在c中如何表示
    +
    +

    TValue->tt表示类型,类型定义在lua.h,nil为LUA_TNIL,boolean为LUA_TBOOLEAN等 +

    +
    // lua.h
    +#define LUA_TNIL                0
    +#define LUA_TBOOLEAN            1
    +#define LUA_TLIGHTUSERDATA      2
    +#define LUA_TNUMBER             3
    +#define LUA_TSTRING             4
    +#define LUA_TTABLE              5
    +#define LUA_TFUNCTION           6
    +#define LUA_TUSERDATA           7
    +#define LUA_TTHREAD             8
    +
    +

    TValue->Value是个union: +int b:只存boolean类型,注:number类型并不存在这里,b只存boolean +lua_Number n:存放所有number类型 +void *p:存放轻量用户数据类型(lightuserdata) +gcObject *gc:存放所有需要垃圾回收的类型,是一个指向union GCObject的指针,通过GCObject可以看到其包含string、userdata、closure、table、proto、upvalue、thread +由此可知,nil、boolean、number、lightuserdata类型是把数据本身直接存在栈里,和lua的垃圾回收无关;而GCObject表示的类型是把数据的内存地址(即指针)存在栈里的,当生命周期结束需要垃圾回收释放内存。 +

    +

    对堆栈的基本操作

    luaL_newstate: 创建一个状态机 +lua_close: 关闭状态机 +

    +
    #include <stdio.h>
    +
    +#include <lua.h>
    +#include <lauxlib.h>
    +#include <lualib.h>
    +
    +int main(int argc, char *argv[]){
    +    lua_State *L = luaL_newstate(); //创建一个状态机
    +
    +    lua_pushnil(L); //nil
    +    int type = lua_type(L, -1);
    +    printf("nil type = %d\n", type);
    +    if(lua_isnil(L, -1)){
    +        printf("------nil-----\n");
    +    }
    +
    +    lua_pushboolean(L, 0); //boolean
    +    type = lua_type(L, -1);
    +    printf("boolean type = %d\n", type);
    +    if(lua_isboolean(L, -1))
    +        printf("--------boolean------\n");
    +
    +    lua_pushlightuserdata(L, NULL); //lightuserdata
    +    type = lua_type(L, -1);
    +    printf("lightuserdata type = %d\n", type);
    +    if(lua_islightuserdata(L, -1))
    +        printf("--------lightuserdata------\n");
    +
    +    lua_pushnumber(L, 10); //number
    +    type = lua_type(L, -1);
    +    printf("number type = %d\n", type);
    +    if(lua_isnumber(L, -1))
    +        printf("--------number------\n");
    +
    +    lua_pushstring(L, "string"); //string
    +    type = lua_type(L, -1);
    +    printf("string type = %d\n", type);
    +    if(lua_isstring(L, -1))
    +        printf("--------string------\n");
    +
    +    lua_newtable(L); //table, 创建空表,并压入栈
    +    type = lua_type(L, -1);
    +    printf("table type = %d\n", type);
    +    if(lua_istable(L, -1))
    +        printf("--------table------\n");
    +
    +    lua_newuserdata(L, 1024); //userdata, 分配1024大小的内存块,并把内存地址压入栈
    +    type = lua_type(L, -1);
    +    printf("userdata type = %d\n", type);
    +    if(lua_isuserdata(L, -1))
    +        printf("--------userdata------\n");
    +
    +    lua_pushthread(L); //thread, 创建一个lua新线程,并将其压入栈。lua线程不是OS线程
    +    type = lua_type(L, -1);
    +    printf("thread type = %d\n", type);
    +    if(lua_isthread(L, -1))
    +        printf("--------thread------\n");
    +
    +    lua_close(L); //关闭状态机
    +    return 0;
    +}
    +
    +

    lua_pushXXX:push*族api向栈顶压入数据,比如lua_pushnumber压入数值,lua_pushstring压入字符串,lua_pushcclosure压入c闭包。 +lua_isXXX:is*族api判断栈里指定位置的索引是否是指定类型,比如,lua_istable(L,-1)判断栈顶位置的数据是否是表,lua_isuserdata(L,-1)判断栈顶位置的数据是否是用户数据等。 +运行结果如下,对应lua.h中的类型定义。 +

    + +
    +lost + +
    Figure 1: 运行结果
    +
    +

    +

    c如何调用Lua的,即c作为宿主语言,Lua为附加语言。c和Lua之间是通过Lua堆栈交互的,基本流程是:把元素入栈——从栈中弹出元素——处理——把结果入栈。

    加载运行Lua脚本

    通过luaL_newstate()创建一个状态机L,c与Lua之间交互的api的第一个参数几乎都是L,是因为可以创建多个状态机,调用api需指定在哪个状态机上操作。lua_close(L)关闭状态机。 +

    +
    int main(int argc, char *argv[]){
    +    lua_State *L = luaL_newstate(); //创建一个状态机
    +    luaL_openlibs(L); //打开所有lua标准库
    +
    +    int ret = luaL_loadfile(L, "c2lua.lua"); //加载但不运行lua脚本
    +    if(ret != LUA_OK){
    +        const char *err = lua_tostring(L, -1); //加载失败,会把错误信息压入栈顶
    +        printf("-------loadfile error = %s\n", err);
    +        lua_close(L);
    +        return 0;
    +    }
    +
    +    ret = lua_pcall(L, 0, 0, 0); //保护模式调用栈顶函数
    +    if(ret != LUA_OK){
    +        const char *err = lua_tostring(L, -1); //发生错误,会把唯一值(错误信息)压入栈顶
    +        printf("-------pcall error = %s\n", err);
    +        lua_close(L);
    +        return 0;
    +    }
    +
    +    lua_close(L);
    +    return 0;
    +}
    +
    +

    在c中加载运行Lua脚本的流程通常是,luaL_newstate、luaL_openlibs、luaL_loadfile、lua_pcall +

    +

    操作Lua中全局变量

    lua_getglobal(L, name),获取Lua脚本中命名为name的全局变量并压栈,然后c通过栈获取 +

    +
    void test_global(lua_State *L){ //读取,重置,设置全局变量
    +    lua_getglobal(L, "var"); //获取全局变量var的值并压入栈顶
    +    int var = lua_tonumber(L, -1);
    +    printf("var = %d\n", var);
    +    lua_pushnumber(L, 10);
    +    lua_setglobal(L, "var"); //设置全局变量var为栈顶元素的值,即10
    +    lua_pushstring(L, "c str");
    +    lua_setglobal(L, "var2"); //设置全局变量var2为栈顶元素的值,即c str
    +
    +    lua_getglobal(L, "f");
    +    lua_pcall(L,0,0,0);
    +}
    +
    +
    var = 5
    +
    +function f()
    +    print("global var = ", var, var2)
    +end
    +
    +

    调用Lua中函数

    通过lua_pcall这个api在保护模式下调用一个Lua函数 +

    +
    int lua_pcall (lua_State *L, int nargs, int nresults, int msgh);
    +
    +

    nargs是函数参数的个数,nresults是函数返回值的个数。 +约定:调用前需要依次把函数,nargs个参数(从左向右)压栈(此时最后一个参数在栈顶位置),然后函数和所有参数都出栈,并调用指定的Lua函数。 +如果调用过程没有发生错误,会把nresults个结果(从左向右)依次压入栈中(此时最后一个结果在栈顶位置),并返回成功LUA_OK。 +如果发生错误,lua_pcall会捕获它,把唯一返回值(错误信息)压栈,然后返回特定的错误码。此时,如果设置msgh不为0,则会指定栈上索引msgh指向的位置为错误处理函数,然后以错误信息作为参数调用该错误处理函数,最后把返回值作为错误信息压栈。 +

    +
    void test_function(lua_State *L){ //调用lua函数
    +    lua_getglobal(L, "f1");
    +    lua_pcall(L, 0, 0, 0); //调用f1
    +    lua_getglobal(L, "f2");
    +    lua_pushnumber(L, 100);
    +    lua_pushnumber(L, 10);
    +    lua_pcall(L, 2, 2, 0); //调用f2
    +    lua_getglobal(L, "f3");
    +    char *str = "c";
    +    lua_pushstring(L, str);
    +    lua_pcall(L,1,1,0); //调用f3
    +}
    +
    +
    --c2lua.lua
    +function f1()
    +    print("hello lua, I'm c!")
    +end
    +
    +function f2(a, b)
    +    return a+b, a-b
    +end
    +
    +function f3(str)
    +    return str .. "_lua"
    +end
    +
    +

    操作Lua中的table

    对表的操作主要有查找t[k]、赋值t[k]=v以及遍历表。 +

    +
    -- c2lua.lua
    +t = {1, 2, ["a"] = 3, ["b"] = {["c"] = 'd'}}
    +
    +
    int lua_getfield (lua_State *L, int index, const char *k);
    +/* 查找,把t[k]的值压栈,t为栈上索引index指向的位置,跟Lua一样该api可能触发"index"事件对应的元方法,等价于lua_pushstring(L,const char*k)和lua_gettable(L, int index)两步,所以通常用lua_getfield在表中查找某个值。 */
    +void lua_setfield (lua_State *L, int index, const char *k);
    +/* 赋值,等价于t[k]=v,将栈顶的值(v)出栈,其中t为栈上索引index指向的位置,跟Lua一样该api可能触发“newindex”事件对应的元方法。需先调用lua_pushxxx(L,v)将v入栈,再调用lua_setfield赋值。 */
    +
    +void dump_table(lua_State *L, int index){
    +    if(lua_type(L, index)!=LUA_TTABLE)
    +        return;
    +    // 典型的遍历方法
    +    lua_pushnil(L);  //nil入栈,相当于从表的第一个位置遍历
    +    while(lua_next(L, index)!=0){ //没有更多元素,lua_next返回0
    +        //key-value入栈, key位于栈上-2处,value位于-1处
    +        printf("%s-%s\n", lua_typename(L,lua_type(L,-2)), lua_typename(L,lua_type(L,-1)));
    +        lua_pop(L,1); //弹出一个元素,即把value出栈,此时栈顶为key,供下一次遍历
    +    }
    +}
    +
    +void test_table(lua_State *L){
    +    // 读取table
    +    lua_getglobal(L, "t");
    +    lua_getfield(L, 1, "a");  //从索引为1的位置(table)获取t.a,并压栈
    +    lua_getfield(L, 1, "b");
    +    lua_getfield(L, -1, "c"); //从索引为-1的位置(栈顶)获取t.c,并压栈
    +
    +    // 修改table
    +    lua_settop(L, 1); //设置栈的位置为1,此时栈上只剩一个元素t
    +    lua_pushnumber(L, 10);
    +    lua_setfield(L, 1, "a"); //t.a=10
    +    lua_pushstring(L, "hello c");
    +    lua_setfield(L, 1, "e"); //t.e="hello c"
    +
    +    dump_table(L, 1); //遍历table number-number 1-1
    +                      //          number-number 1-2
    +                      //          string-number a-3
    +                      //          string-string e-hello c
    +                      //          string-table b-table
    +
    +    //新建一个table
    +    lua_settop(L, 0); //清空栈
    +    lua_newtable(L); //创建一个table
    +    lua_pushboolean(L, 0);
    +    lua_setfield(L, 1, "new_a");
    +    lua_pushboolean(L, 1);
    +    lua_setfield(L, 1, "new_b");
    +
    +    dump_table(L, 1); //遍历table string-boolean new_a-false
    +                      //          string-boolean new_b-true
    +}
    +
    +

    注:lua_settop(L, int index)设置栈顶为index,大于index位置的元素都被移除,特别当index为0,即清空栈;如果原来的栈小于index,多余的位置用nil填充。 +总之,c调用lua的流程通常是:c把需要的数据入栈——Lua从栈中取出数据——执行Lua脚本——Lua把结果入栈——c从栈中获取结果 +

    +

    Lua是如何调用c的,Lua是宿主语言,c是附加语言。Lua调用c有几种不同方式,这里只讲解最常用的一种:将c模块编译成so库,然后供Lua调用。

    约定:c模块需提供luaopen_xxx接口,xxx与文件名必须一致,比如"mylib";还需提供一个注册数组,该数组必须命名为luaL_Reg,每一项是{lua函数名,c函数名},最后一项是{NULL, NULL};通过luaL_newlib创建新的表入栈,然后将数组中的函数注册进去,这样Lua就可以调用到。 +

    +
    //mylib.c
    +
    +#include <lua.h>
    +#include <lauxlib.h>
    +#include <lualib.h>
    +
    +#define TYPE_BOOLEAN 1
    +#define TYPE_NUMBER 2
    +#define TYPE_STRING 3
    +
    +static int ladd(lua_State *L){
    +    double op1 = luaL_checknumber(L, -2);
    +    double op2 = luaL_checknumber(L, -1);
    +    lua_pushnumber(L, op1+op2);
    +    return 1;
    +}
    +
    +static int lsub(lua_State *L){
    +    double op1 = luaL_checknumber(L, -2);
    +    double op2 = luaL_checknumber(L, -1);
    +    lua_pushnumber(L, op1-op2);
    +    return 1;
    +}
    +
    +static int lavg(lua_State *L){
    +    double avg = 0.0;
    +    int n = lua_gettop(L);
    +    if(n==0){
    +        lua_pushnumber(L,0);
    +        return 1;
    +    }
    +    int i;
    +    for(i=1;i<=n;i++){
    +        avg += luaL_checknumber(L, i);
    +    }
    +    avg = avg/n;
    +    lua_pushnumber(L,avg);
    +    return 1;
    +}
    +
    +static int fn(lua_State *L){
    +    int type = lua_type(L, -1);
    +    printf("type = %d\n", type);
    +    if(type==LUA_TBOOLEAN){
    +        lua_pushvalue(L, lua_upvalueindex(TYPE_BOOLEAN));
    +    } else if(type==LUA_TNUMBER){
    +        lua_pushvalue(L, lua_upvalueindex(TYPE_NUMBER));
    +    } else if(type==LUA_TSTRING){
    +        lua_pushvalue(L, lua_upvalueindex(TYPE_STRING));
    +    }
    +    return 1;
    +}
    +
    +int luaopen_mylib(lua_State *L){
    +    luaL_Reg l[] = {
    +        {"add", ladd},
    +        {"sub", lsub},
    +        {"avg", lavg},
    +        {NULL, NULL},
    +    };
    +    luaL_newlib(L,l);
    +
    +    lua_pushliteral(L, "BOOLEAN");
    +    lua_pushliteral(L, "NUMBER");
    +    lua_pushliteral(L, "STRING");
    +    lua_pushcclosure(L, fn, 3);
    +
    +    lua_setfield(L, -2, "fn");
    +    return 1;
    +}
    +
    +

    Lua文件里,需将so库加入cpath路径里,通过require返回栈上的表,Lua就可以调用表中注册的接口,比如,add、sub、avg等 +Lua调用c api的过程:Lua将api需要的参数入栈——c提取到参数——处理——c将结果入栈——Lua提取出结果 +

    +
    package.cpath = "./?.so"
    +
    +local mylib = require "mylib"
    +
    +local a, b = 3.14, 1.57
    +
    +print(mylib.add(a, b), mylib.sub(a, b))   -- 4.71. 1.57
    +
    +print(mylib.avg())  -- 0.0
    +
    +print(mylib.avg(1,2,3,4,5)) -- 3.0
    +
    +print(mylib.fn(true), mylib.fn(10), mylib.fn("abc")) -- BOOLEAN NUMBER STRING
    +
    +

    例中还提供了简单的c闭包的使用方法,关于c闭包,提供了多个上值(upvalue)关联到函数上,这些upvalue可以理解成该函数内部的全局变量,即只能被该函数访问到,且在函数返回时不会消亡,该函数任何时候都可以访问到。 +

    + +

    void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n); +用来把一个新的c闭包压栈,fn是一个c api,n指定关联多少个upvalue,这些upvalue需要依次压栈,即栈顶位置是第n个upvalue的值,lua_pushcclosure会把这些upvalue出栈,这些upvalue的伪索引依次为1-n。 +

    + +

    int lua_upvalueindex (int i); +获取当前运行函数第i个upvalue的值。 +

    + +

    总之,Lua调用c的流程:编写好c模块,在堆栈上建一个表,将接口注册给这个表,然后把c模块编译成so库,在Lua里require这个so库,就可以调用注册的函数了。 +

    +
    Made with Emacs :) +

    Disclaimer +

    + diff --git "a/posts/20241018T094654--lua-\345\222\214-c-\345\246\202\344\275\225\344\272\244\344\272\222__notes.org" "b/posts/20241018T094654--lua-\345\222\214-c-\345\246\202\344\275\225\344\272\244\344\272\222__notes.org" deleted file mode 100644 index 1ec181c..0000000 --- "a/posts/20241018T094654--lua-\345\222\214-c-\345\246\202\344\275\225\344\272\244\344\272\222__notes.org" +++ /dev/null @@ -1,379 +0,0 @@ -#+title: Lua 和 C 如何交互 -#+date: [2024-10-18 Fri 09:46] -#+filetags: :notes: -#+identifier: 20241018T094654 -#+description: Lua 和 C 交互的教程 -* Lua 和 C 如何交互 -** 堆栈 -lua与c之间交互是通过“lua堆栈”通信的。不管是lua调用c还是c调用lua,都是通过操作lua堆栈实现的。顾名思义,lua堆栈也满足后进先出的特点,入栈/出栈都围绕栈顶进行的。与通用的栈不同的是,这个虚拟栈每个位置都对应一个索引,可以通过索引操作指定位置的数据。1代表栈底,向栈顶依次递增;-1代表栈顶,向栈底依次递减,如图。 -\\ - -#+begin_export html -
    -lost - -
    Figure 1: Lua堆栈结构
    -
    -#+end_export -\\ -** 全局表 -Lua的全局表可以想象成一个map哈希表结构,比如Lua有一个变量: -name = "hello world" -全局表中存放了name和hello world的对应关系, 可以通过name在全局表中找到对应的hello world -** lua中类型在c中如何表示 -要实现c和lua之间的交互,先了解下lua中基本类型与c中类型怎么对应的。lua中有八种基本类型:nil、boolean、number、string、table、function、userdata、thread,其中,userdata分轻量用户数据(lightuserdata)和完成用户数据(userdata)两种。这些类型都可以压入栈中,在c中统一用TValue结构表示,是一个{值,类型}结构。 -\\ - -#+begin_export html -
    -lost - -
    Figure 1: lua类型在c中如何表示
    -
    -#+end_export -\\ -TValue->tt表示类型,类型定义在lua.h,nil为LUA_TNIL,boolean为LUA_TBOOLEAN等 -#+begin_src c -// lua.h -#define LUA_TNIL 0 -#define LUA_TBOOLEAN 1 -#define LUA_TLIGHTUSERDATA 2 -#define LUA_TNUMBER 3 -#define LUA_TSTRING 4 -#define LUA_TTABLE 5 -#define LUA_TFUNCTION 6 -#define LUA_TUSERDATA 7 -#define LUA_TTHREAD 8 -#+end_src -TValue->Value是个union: -int b:只存boolean类型,注:number类型并不存在这里,b只存boolean -lua_Number n:存放所有number类型 -void *p:存放轻量用户数据类型(lightuserdata) -gcObject *gc:存放所有需要垃圾回收的类型,是一个指向union GCObject的指针,通过GCObject可以看到其包含string、userdata、closure、table、proto、upvalue、thread -由此可知,nil、boolean、number、lightuserdata类型是把数据本身直接存在栈里,和lua的垃圾回收无关;而GCObject表示的类型是把数据的内存地址(即指针)存在栈里的,当生命周期结束需要垃圾回收释放内存。 -** 对堆栈的基本操作 -luaL_newstate: 创建一个状态机 -lua_close: 关闭状态机 -#+begin_src c -#include - -#include -#include -#include - -int main(int argc, char *argv[]){ - lua_State *L = luaL_newstate(); //创建一个状态机 - - lua_pushnil(L); //nil - int type = lua_type(L, -1); - printf("nil type = %d\n", type); - if(lua_isnil(L, -1)){ - printf("------nil-----\n"); - } - - lua_pushboolean(L, 0); //boolean - type = lua_type(L, -1); - printf("boolean type = %d\n", type); - if(lua_isboolean(L, -1)) - printf("--------boolean------\n"); - - lua_pushlightuserdata(L, NULL); //lightuserdata - type = lua_type(L, -1); - printf("lightuserdata type = %d\n", type); - if(lua_islightuserdata(L, -1)) - printf("--------lightuserdata------\n"); - - lua_pushnumber(L, 10); //number - type = lua_type(L, -1); - printf("number type = %d\n", type); - if(lua_isnumber(L, -1)) - printf("--------number------\n"); - - lua_pushstring(L, "string"); //string - type = lua_type(L, -1); - printf("string type = %d\n", type); - if(lua_isstring(L, -1)) - printf("--------string------\n"); - - lua_newtable(L); //table, 创建空表,并压入栈 - type = lua_type(L, -1); - printf("table type = %d\n", type); - if(lua_istable(L, -1)) - printf("--------table------\n"); - - lua_newuserdata(L, 1024); //userdata, 分配1024大小的内存块,并把内存地址压入栈 - type = lua_type(L, -1); - printf("userdata type = %d\n", type); - if(lua_isuserdata(L, -1)) - printf("--------userdata------\n"); - - lua_pushthread(L); //thread, 创建一个lua新线程,并将其压入栈。lua线程不是OS线程 - type = lua_type(L, -1); - printf("thread type = %d\n", type); - if(lua_isthread(L, -1)) - printf("--------thread------\n"); - - lua_close(L); //关闭状态机 - return 0; -} -#+end_src -lua_pushXXX:push*族api向栈顶压入数据,比如lua_pushnumber压入数值,lua_pushstring压入字符串,lua_pushcclosure压入c闭包。 -lua_isXXX:is*族api判断栈里指定位置的索引是否是指定类型,比如,lua_istable(L,-1)判断栈顶位置的数据是否是表,lua_isuserdata(L,-1)判断栈顶位置的数据是否是用户数据等。 -运行结果如下,对应lua.h中的类型定义。 -\\ - -#+begin_export html -
    -lost - -
    Figure 1: 运行结果
    -
    -#+end_export -\\ -** c如何调用Lua的,即c作为宿主语言,Lua为附加语言。c和Lua之间是通过Lua堆栈交互的,基本流程是:把元素入栈——从栈中弹出元素——处理——把结果入栈。 -*** 加载运行Lua脚本 -通过luaL_newstate()创建一个状态机L,c与Lua之间交互的api的第一个参数几乎都是L,是因为可以创建多个状态机,调用api需指定在哪个状态机上操作。lua_close(L)关闭状态机。 -#+begin_src c -int main(int argc, char *argv[]){ - lua_State *L = luaL_newstate(); //创建一个状态机 - luaL_openlibs(L); //打开所有lua标准库 - - int ret = luaL_loadfile(L, "c2lua.lua"); //加载但不运行lua脚本 - if(ret != LUA_OK){ - const char *err = lua_tostring(L, -1); //加载失败,会把错误信息压入栈顶 - printf("-------loadfile error = %s\n", err); - lua_close(L); - return 0; - } - - ret = lua_pcall(L, 0, 0, 0); //保护模式调用栈顶函数 - if(ret != LUA_OK){ - const char *err = lua_tostring(L, -1); //发生错误,会把唯一值(错误信息)压入栈顶 - printf("-------pcall error = %s\n", err); - lua_close(L); - return 0; - } - - lua_close(L); - return 0; -} -#+end_src -在c中加载运行Lua脚本的流程通常是,luaL_newstate、luaL_openlibs、luaL_loadfile、lua_pcall -*** 操作Lua中全局变量 -lua_getglobal(L, name),获取Lua脚本中命名为name的全局变量并压栈,然后c通过栈获取 -#+begin_src c -void test_global(lua_State *L){ //读取,重置,设置全局变量 - lua_getglobal(L, "var"); //获取全局变量var的值并压入栈顶 - int var = lua_tonumber(L, -1); - printf("var = %d\n", var); - lua_pushnumber(L, 10); - lua_setglobal(L, "var"); //设置全局变量var为栈顶元素的值,即10 - lua_pushstring(L, "c str"); - lua_setglobal(L, "var2"); //设置全局变量var2为栈顶元素的值,即c str - - lua_getglobal(L, "f"); - lua_pcall(L,0,0,0); -} -#+end_src -#+begin_src lua -var = 5 - -function f() - print("global var = ", var, var2) -end -#+end_src -*** 调用Lua中函数 -通过lua_pcall这个api在保护模式下调用一个Lua函数 -#+begin_src c -int lua_pcall (lua_State *L, int nargs, int nresults, int msgh); -#+end_src -nargs是函数参数的个数,nresults是函数返回值的个数。 -约定:调用前需要依次把函数,nargs个参数(从左向右)压栈(此时最后一个参数在栈顶位置),然后函数和所有参数都出栈,并调用指定的Lua函数。 -如果调用过程没有发生错误,会把nresults个结果(从左向右)依次压入栈中(此时最后一个结果在栈顶位置),并返回成功LUA_OK。 -如果发生错误,lua_pcall会捕获它,把唯一返回值(错误信息)压栈,然后返回特定的错误码。此时,如果设置msgh不为0,则会指定栈上索引msgh指向的位置为错误处理函数,然后以错误信息作为参数调用该错误处理函数,最后把返回值作为错误信息压栈。 -#+begin_src c -void test_function(lua_State *L){ //调用lua函数 - lua_getglobal(L, "f1"); - lua_pcall(L, 0, 0, 0); //调用f1 - lua_getglobal(L, "f2"); - lua_pushnumber(L, 100); - lua_pushnumber(L, 10); - lua_pcall(L, 2, 2, 0); //调用f2 - lua_getglobal(L, "f3"); - char *str = "c"; - lua_pushstring(L, str); - lua_pcall(L,1,1,0); //调用f3 -} -#+end_src -#+begin_src lua ---c2lua.lua -function f1() - print("hello lua, I'm c!") -end - -function f2(a, b) - return a+b, a-b -end - -function f3(str) - return str .. "_lua" -end -#+end_src -*** 操作Lua中的table -对表的操作主要有查找t[k]、赋值t[k]=v以及遍历表。 -#+begin_src lua --- c2lua.lua -t = {1, 2, ["a"] = 3, ["b"] = {["c"] = 'd'}} -#+end_src -#+begin_src c -int lua_getfield (lua_State *L, int index, const char *k); -/* 查找,把t[k]的值压栈,t为栈上索引index指向的位置,跟Lua一样该api可能触发"index"事件对应的元方法,等价于lua_pushstring(L,const char*k)和lua_gettable(L, int index)两步,所以通常用lua_getfield在表中查找某个值。 */ -void lua_setfield (lua_State *L, int index, const char *k); -/* 赋值,等价于t[k]=v,将栈顶的值(v)出栈,其中t为栈上索引index指向的位置,跟Lua一样该api可能触发“newindex”事件对应的元方法。需先调用lua_pushxxx(L,v)将v入栈,再调用lua_setfield赋值。 */ - -void dump_table(lua_State *L, int index){ - if(lua_type(L, index)!=LUA_TTABLE) - return; - // 典型的遍历方法 - lua_pushnil(L); //nil入栈,相当于从表的第一个位置遍历 - while(lua_next(L, index)!=0){ //没有更多元素,lua_next返回0 - //key-value入栈, key位于栈上-2处,value位于-1处 - printf("%s-%s\n", lua_typename(L,lua_type(L,-2)), lua_typename(L,lua_type(L,-1))); - lua_pop(L,1); //弹出一个元素,即把value出栈,此时栈顶为key,供下一次遍历 - } -} - -void test_table(lua_State *L){ - // 读取table - lua_getglobal(L, "t"); - lua_getfield(L, 1, "a"); //从索引为1的位置(table)获取t.a,并压栈 - lua_getfield(L, 1, "b"); - lua_getfield(L, -1, "c"); //从索引为-1的位置(栈顶)获取t.c,并压栈 - - // 修改table - lua_settop(L, 1); //设置栈的位置为1,此时栈上只剩一个元素t - lua_pushnumber(L, 10); - lua_setfield(L, 1, "a"); //t.a=10 - lua_pushstring(L, "hello c"); - lua_setfield(L, 1, "e"); //t.e="hello c" - - dump_table(L, 1); //遍历table number-number 1-1 - // number-number 1-2 - // string-number a-3 - // string-string e-hello c - // string-table b-table - - //新建一个table - lua_settop(L, 0); //清空栈 - lua_newtable(L); //创建一个table - lua_pushboolean(L, 0); - lua_setfield(L, 1, "new_a"); - lua_pushboolean(L, 1); - lua_setfield(L, 1, "new_b"); - - dump_table(L, 1); //遍历table string-boolean new_a-false - // string-boolean new_b-true -} -#+end_src -注:lua_settop(L, int index)设置栈顶为index,大于index位置的元素都被移除,特别当index为0,即清空栈;如果原来的栈小于index,多余的位置用nil填充。 -总之,c调用lua的流程通常是:c把需要的数据入栈——Lua从栈中取出数据——执行Lua脚本——Lua把结果入栈——c从栈中获取结果 -** Lua是如何调用c的,Lua是宿主语言,c是附加语言。Lua调用c有几种不同方式,这里只讲解最常用的一种:将c模块编译成so库,然后供Lua调用。 -约定:c模块需提供luaopen_xxx接口,xxx与文件名必须一致,比如"mylib";还需提供一个注册数组,该数组必须命名为luaL_Reg,每一项是{lua函数名,c函数名},最后一项是{NULL, NULL};通过luaL_newlib创建新的表入栈,然后将数组中的函数注册进去,这样Lua就可以调用到。 -#+begin_src c -//mylib.c - -#include -#include -#include - -#define TYPE_BOOLEAN 1 -#define TYPE_NUMBER 2 -#define TYPE_STRING 3 - -static int ladd(lua_State *L){ - double op1 = luaL_checknumber(L, -2); - double op2 = luaL_checknumber(L, -1); - lua_pushnumber(L, op1+op2); - return 1; -} - -static int lsub(lua_State *L){ - double op1 = luaL_checknumber(L, -2); - double op2 = luaL_checknumber(L, -1); - lua_pushnumber(L, op1-op2); - return 1; -} - -static int lavg(lua_State *L){ - double avg = 0.0; - int n = lua_gettop(L); - if(n==0){ - lua_pushnumber(L,0); - return 1; - } - int i; - for(i=1;i<=n;i++){ - avg += luaL_checknumber(L, i); - } - avg = avg/n; - lua_pushnumber(L,avg); - return 1; -} - -static int fn(lua_State *L){ - int type = lua_type(L, -1); - printf("type = %d\n", type); - if(type==LUA_TBOOLEAN){ - lua_pushvalue(L, lua_upvalueindex(TYPE_BOOLEAN)); - } else if(type==LUA_TNUMBER){ - lua_pushvalue(L, lua_upvalueindex(TYPE_NUMBER)); - } else if(type==LUA_TSTRING){ - lua_pushvalue(L, lua_upvalueindex(TYPE_STRING)); - } - return 1; -} - -int luaopen_mylib(lua_State *L){ - luaL_Reg l[] = { - {"add", ladd}, - {"sub", lsub}, - {"avg", lavg}, - {NULL, NULL}, - }; - luaL_newlib(L,l); - - lua_pushliteral(L, "BOOLEAN"); - lua_pushliteral(L, "NUMBER"); - lua_pushliteral(L, "STRING"); - lua_pushcclosure(L, fn, 3); - - lua_setfield(L, -2, "fn"); - return 1; -} -#+end_src -Lua文件里,需将so库加入cpath路径里,通过require返回栈上的表,Lua就可以调用表中注册的接口,比如,add、sub、avg等 -Lua调用c api的过程:Lua将api需要的参数入栈——c提取到参数——处理——c将结果入栈——Lua提取出结果 -#+begin_src lua -package.cpath = "./?.so" - -local mylib = require "mylib" - -local a, b = 3.14, 1.57 - -print(mylib.add(a, b), mylib.sub(a, b)) -- 4.71. 1.57 - -print(mylib.avg()) -- 0.0 - -print(mylib.avg(1,2,3,4,5)) -- 3.0 - -print(mylib.fn(true), mylib.fn(10), mylib.fn("abc")) -- BOOLEAN NUMBER STRING -#+end_src -例中还提供了简单的c闭包的使用方法,关于c闭包,提供了多个上值(upvalue)关联到函数上,这些upvalue可以理解成该函数内部的全局变量,即只能被该函数访问到,且在函数返回时不会消亡,该函数任何时候都可以访问到。 - -void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n); -用来把一个新的c闭包压栈,fn是一个c api,n指定关联多少个upvalue,这些upvalue需要依次压栈,即栈顶位置是第n个upvalue的值,lua_pushcclosure会把这些upvalue出栈,这些upvalue的伪索引依次为1-n。 - -int lua_upvalueindex (int i); -获取当前运行函数第i个upvalue的值。 - -总之,Lua调用c的流程:编写好c模块,在堆栈上建一个表,将接口注册给这个表,然后把c模块编译成so库,在Lua里require这个so库,就可以调用注册的函数了。