Tursom Blog - tursom
type: channel
link: https://blog.tursom.cn/author/1/
description: tursom
items: Windows Server 2022 SMB 客户端无法连接问题修复
items: 使用 wireshark + rpcapd 对 linux 服务器远程抓包
items: gitea 支持 .rst 格式化
items: 行式链表 RowLinkedList 源码
items: 基于数组的链表实现
items: 双杯取水问题解析
items: 关于 cglib 实现的研究
items: 关于 Kotlin 可见性修饰的一些研究
items: ts-socket 是如何用同步语法实现异步读写的
items: 对go atomic泛型化的尝试

Windows Server 2022 SMB 客户端无法连接问题修复
type: item
date: 2024-01-06T20:45:00+08:00
description: 添加 DWORD 注册表项，值为 1\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters\AllowInsecureGuestAuth

使用 wireshark + rpcapd 对 linux 服务器远程抓包
type: item
date: 2023-07-17T15:27:00+08:00
description: 在调试数据库驱动时，我们偶尔需要对数据的时序进行分析，这就需要使用 wireshark 进行抓包。同时，我们还要防止本地开发环境的复杂性影响测试结果，我们需要在更简单的服务器上运行测试。为了实现这些测试，我们就需要找到对服务端抓包的方法。目前最主流的方案是使用 rpcapd 将服务端的抓包结果传输到本地，大部分文章对如何使用 rpcapd 抓包有较为详细的描述，本篇则不再重复，而是将重点放在其过时或错误的部分上——即 rpcapd 的安装。在 github 上有 rpcapd 项目。根据其描述，libpcap 自 v1.9.0 开始自带 rpcapd，但是需要在 configure 过程中添加 --enable-remote参数。经测试，在 Ubuntu 22.04 源中自带的 libpcap-dev 和 libpcap0.8 都没有 rpcapd 命令，而 Ubuntu 18.04 源的 libpcap 版本则直接是 v1.8.0。很明显，我们在 Ubuntu 上需要自己编译 rpcapd。编译 rpcapd 的步骤为：克隆 libpcap 项目执行autogen.sh脚本来生成 configure 脚本运行 configure 脚本，./configure --enable-remotemake在 configure 过程中，可能会提示缺少依赖。根据需求安装对应依赖即可。如果 make 的过程不出意外，那么 rpcapd 的可执行程序应该就放在 rpcapd 目录下了。我们只需要执行sudo rpcapd/rpcapd -4 -n就可以启动 rpcapd 服务了。默认监听的端口为 2002。

gitea 支持 .rst 格式化
type: item
date: 2023-01-28T20:25:00+08:00
description: https://github.com/go-gitea/gitea/issues/374#issuecomment-423688581pip 安装 docutils修改app.ini[markup.restructuredtext] ENABLED = true FILE_EXTENSIONS = .rst RENDER_COMMAND = rst2html.py IS_INPUT_FILE = false修改模板文件。rst2html.py --help 中 --template 参数有写默认位置，或者用此参数自定义 template 位置。模板文件内容为：%(body_pre_docinfo)s %(docinfo)s %(body)s

行式链表 RowLinkedList 源码
type: item
date: 2022-12-10T19:19:00+08:00
description: import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.*; /** * a linked list which based on array but not node object * * @param <E> contains element */ public class RowLinkedList<E> extends AbstractList<E> implements Deque<E>, Cloneable, java.io.Serializable { @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) private @interface UnsafeMethod { String reason() default ""; } @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) private @interface SafeMethod { } private interface NodeSetter { void set(@NotNull RowLinkedList<?> list, int node); } private static final int NULL_NODE = -1; private static final NodeSetter headSetter = (list, node) -> list.head = node; private static final NodeSetter tailSetter = (list, node) -> list.tail = node; private int[] nextArray; private int[] prevArray; private Object[] elements; private int head = NULL_NODE; private int tail = NULL_NODE; private int empty = NULL_NODE; private int used = 0; private int size = 0; @Override public void addFirst(@Nullable E element) { resize(); int node = insertNode(element, head, nextArray, prevArray); if (head == NULL_NODE) { tail = node; } head = node; } @Override public void addLast(@Nullable E element) { resize(); int node = insertNode(element, tail, prevArray, nextArray); if (tail == NULL_NODE) { head = node; } tail = node; } @Override public boolean offerFirst(E e) { addFirst(e); return true; } @Override public boolean offerLast(E e) { addLast(e); return true; } @Override public E removeFirst() { int node = removeFirstNode(); if (node == NULL_NODE) { throw new NoSuchElementException(); } else { return popElement(node); } } @UnsafeMethod private int insertNode(@Nullable E element, int tail, @NotNull int[] prevArray, @NotNull int[] nextArray) { int node = alloc(); elemen…

基于数组的链表实现
type: item
date: 2022-12-09T00:27:00+08:00
description: 前言链表是一种由“节点”组成的数据结构，每个节点都储存着指向下一个节点的指针。通常来讲，指针都是存储的物理地址，因此在无法获取物理地址的语言里无法实现（比如BASIC）；不过，其实也有别的方法在这些语言里实现指针的功能——即使用数组，以对象在数组内的索引作为指针的值。传统的链表实现是定义节点对象的结构，用一个个独立的节点构建链表，不过这种做法在Java中空间效率并不是最高的。在64位Hotspot中，每个对象都有128位的对象头，里面包含了对象类型、对象锁、GC分代信息等。一个单向链表的节点对象最小需要两个引用，即16字节，加上对象头一共32字节，对象头的占比高达50%，这对内存而言是非常大的浪费。传统的关系型数据库是按行存储的，每条记录都存储在“行”中；后来发展出了列数据库，数据被存在“列”中。这种列式存储的思维可以帮助我们实现另一种形式的“列式”链表。基本思路以数组表达的列的形式储存节点的所有属性，我们实现的是一个单向链表，需要两个数组，分别是next数组和value数组。我们使用一个变量head表示列表头，head指向列表头，而被head指向的节点则指向它的下一个节点，以此类推。为了支持从队列尾添加数据，我们还需要一个tail变量用来缓存链表尾的索引。同时，我们还有一个缓存链表大小的size我们这个链表如果每次增删对象都要拷贝或者复制数组的话，运行效率会变得非常低（时间复杂度O(n)），因此我们在一个数组中实现了两个平行的链表，我将其命名为values链表（简称V）和empty链表（简称E），V用来存储数据，而E则用来暂存被删除的节点以待复用。从V中删除的节点都会同步加入E中，而当插入节点的时候也会优先取用E的节点。参考 ArrayList 的实现，为了防止频繁复制数组，我们在empty链表用尽，需要扩容的时候还会额外扩容一部分空间。由于前文定义的E链表与V链表的性质，我们可以推导出E与V的组合体储存在一片连续的空间中，我们用变量used来表示这一段内存的结尾。当我们需要新的节点时，我们直接取used所指向的节点即可。实现完备性证明push 函数push 函数把元素添加到链表头。数组容量校验过程略。push对象需要先获取一个节点，根据前面的定义，我们会优先从E链表中取节点，否则从used取节点。我们将这两种情况分开讨论。当E链表为空时当E链表不为空时未完待续源码import java.util.Arrays; public class RowLinkedStack<V> { private int[] nextArray; private Object[] values; private int head = -1; private int tail = -1; private int empty = -1; private int used = 0; private int size = 0; public void append(V obj) { if (nextArray == null || used >= nextArray.length) { resize(); } if (empty < 0) { if (tail < 0) { head = used; } else { nextArray[tail] = used; } nextArray[used] = -1; values[used] = obj; tail = used++; } else { int nextEmpty = nextArray[empty]; if (tail < 0) { head = empty; } else { nextArray[tail] = empty; } nextArray[empty] = -1; values[empty] = obj; tail = empty; empty = nextEmpty; } size++; } public void push(V obj) { if (nextArray == null || used >= nextArray.length) { resize(); } if (empty < 0) { nextArray[used] = head; values[used] = obj; head = used++; } else { int nextEmpty = nextArray[empty]; nextArray[empty] = head; values[empty] = obj; head = empty; empty = nextEmpty; } if (tail < 0) { tail = head; } size++; } public V pop() {…

双杯取水问题解析
type: item
date: 2022-12-07T21:11:00+08:00
description: 引言今天面试，面试官问了一道经典的问题：一个5L的杯子和一个3L的杯子，水无限，如何得到4L水？ 这个问题非常经典，经典到我下意识就能回忆起解法：将5L杯子装满水，然后倒满3L的杯子，此时5L杯子里有2L水将3L杯子清空将5L被子里剩余的2L水导倒入3L杯子中5L杯子取水，倒入3L杯，此时5L杯中就有4L水了我们可以用表格来表达每一步的状态：步骤操作3L杯内水的体积5L杯内水的体积15L杯取水0L5L25L杯倒入3L杯3L2L33L杯清空0L2L45L杯倒入3L杯2L0L55L杯取水2L5L65L杯倒入3L杯3L4L问题很简单，很好解决，接下来面试官问出了一个变种：杯子容量变成 7L 和 11L，取出 2L 水。解法其实也不变，大杯接水倒小杯。接下来的表格中我们会用（容量）L 代表杯子，-> 代表水的流动：步骤操作7L杯11L杯1-> 11L0L11L211L -> 7L7L4L37L ->0L4L411L -> 7L4L0L5-> 11L4L11L611L -> 7L7L9L77L ->0L9L811L -> 7L7L2L随后，面试官问出了终极难题：什么情况下这个问题无解？证明这个问题我在网上没有找到它的名字，我就将其命名为“双杯取水问题”好了。我们先只考虑我们目前的解法——即将大杯中的水倒入小杯，最后取得指定容量的水——这种情况下，会有那些情形是无解的。我们假设大杯容量为M，小杯容量为N，每次我们能取出的特定容量为：M - NM - (N - (M - N)) = 2(M - N)M - (N - 2(M - N)) = 3(M - N)M - (N - 3(M - N)) = 4(M - N)不难看出，每一步我们能取出的特定容量都为 n(M - N)，考虑每次我们还会将 N 的整数倍倒掉，最后的结果是 n(M-N) mod N。我的直觉告诉我，这个问题的解和 M - N 与 N 的最大公因数有关，如果最大公因数不为1，则会有无解的情况。要证明也不是很困难，我们画一个数轴就可以得到。假设最大公约数为a，M - N 为 b，则 nb 在数轴上一定会落到 a 的倍数上，而nN也一定会落到 a 的倍数上，所以 nb 减去 N 的整数倍后还是会落到 a 的整数倍上。在 a != 1 的情况下，a 的整数倍之间的空隙就是我们要找的无解的解。换个角度把 N 和 M 的整数倍都画在数轴上，也能很容易得出，用小杯往大杯倒水也是一样的效果，他们在数学上是等价的。这个问题的数学本质是在问 nN 与 mM 之间的差值。当然，我们也很容易看出，当 M 和 N 都是整数的时候，我们是取不出小数升的水的。

关于 cglib 实现的研究
type: item
date: 2022-12-05T00:01:00+08:00
description: cglib 是 Spring 框架默认使用的动态代理库，其主要工作原理是使用 ASM 技术在运行现场制造出被代理对象的子类，并以子类的对象替换被代理对象。其与 Java 的动态的主要不同是其不受代理接口的限制，但是无法代理 final 类。研究方法与工具cglib 的代理类是由net.sf.cglib.proxy.Enhancer创建的，而 Enhancer 提供public generateClass(ClassVisitor v)方法将生成的类写入任意org.objectweb.asm.ClassVisitor的实现中；而 ClassVisitor 的实现org.objectweb.asm.ClassWriter的方法public byte[] toByteArray()则支持将写入其中的类转换为字节数组，这样我们就获得了代理类的字节码了，只需要将其写入文件就可以深入研究了。研究对象为了简化研究，我定义的类比较简单，他的源码如下：class Example { open class TestClass protected constructor() { open var a: Int = 0 } }获取字节码的源码为：@Test fun getClass() { val writer = ClassWriter(0) val enhancer = Proxy.newEnhancer(TestClass::class.java) val clazz = enhancer.createClass() CglibUtil.setStaticCallbacks(clazz, arrayOf(ProxyInterceptor())) enhancer.generateClass(writer) File("TestClass.class").writeBytes(writer.toByteArray()) }未完待续

关于 Kotlin 可见性修饰的一些研究
type: item
date: 2022-12-04T23:32:00+08:00
description: bennyhuo 在新一集视频中谈到了 Kotlin 与 Java 的可见性修饰问题，这个问题不仅影响程序设计，有些细节也会影响反射最终落地的实现。我曾经参与过一个 Minecraft 模组 LightLand 的开发工作，其中用到了一个使用 ASM 技术实现高性能反射的库 reflectasm。这个库会用 ASM 技术在运行现场制造出一个与被反射类同包（java包除外）的类，并提供相应接口以反射形式访问被反射对象。该实现导致的限制是其对可见性敏感，无法访问被反射类的私有成员，这个特性对我印象深刻，因此我在设计反射与可见性的问题上总会想到这一点，同样的，我也可以用这个特性对类成员的可见性进行初步窥探。我一开始设计了一个涵盖 Kotlin 所有四个可见性修饰符（private、protected、internal 和 public）的类 FieldTestClass 并尝试用 reflectasm 访问他的成员变量：internal class FieldTestClass { val i1: Int = 0 protected val i2: Int = 0 private val i3: Int = 0 internal val i4: Int = 0 } // Class.methodAccessList 是我写的一个针对 reflectasm 的扩展函数 // 详见 https://github.com/tursom/TursomServer/tree/master/ts-core/ts-reflectasm FieldTestClass::class.java.fieldAccessList.forEach { println(it.fieldNames.asList()) }不出意外的出意外了，我忘记了 Kotlin 在 JVM 平台的所有成员变量都是 private 的，真实的可见性是 getter 和 setter 实现的。将对应的方法改成对成员方法的获取，得到了以下输出：[getI4$ts_reflectasm, getI1, getI2]i1 是 public 的，我们可以轻松获取到他的 getter，很正常i2 是 protected 的，同包可见i3 是 private 的，同包也不可见（查看字节码甚至可以看到没生成 getter）i4 比较有趣，他是原本 getter 的名字后面加了当前 module 的名字（因为这段测试我是在 ts-reflectasm 中运行的），目前原理未知以下是 FieldTestClass 字节码的反编译：@Metadata( mv = {1, 8, 0}, k = 1, d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\b\b\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002R\u0014\u0010\u0003\u001a\u00020\u0004X\u0086D¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006R\u0014\u0010\u0007\u001a\u00020\u0004X\u0084D¢\u0006\b\n\u0000\u001a\u0004\b\b\u0010\u0006R\u000e\u0010\t\u001a\u00020\u0004X\u0082D¢\u0006\u0002\n\u0000R\u0014\u0010\n\u001a\u00020\u0004X\u0080D¢\u0006\b\n\u0000\u001a\u0004\b\u000b\u0010\u0006¨\u0006\f"}, d2 = {"Lcn/tursom/reflect/asm/ReflectAsmFieldTest$FieldTestClass;", "", "()V", "i1", "", "getI1", "()I", "i2", "getI2", "i3", "i4", "getI4$ts_reflectasm", "ts-reflectasm"} ) public static final class FieldTestClass { private final int i1; private final int i2; private final int i3; private final int i4; public final int getI1() { return this.i1…

ts-socket 是如何用同步语法实现异步读写的
type: item
date: 2022-12-04T02:07:00+08:00
description: ts-socket 是我在大三时期做的一个研究向的产品，它的主要功能是利用Kotlin的协程技术与Java的NIO技术，为用户提供写法类似传统 Socket 的同步语法的异步读写。从核心流程上讲，首先创建对应的ServerChannel，注册到 NioThread 上进行监听。等到有新连接进入的时候，由 ts-socket 接收连接，启动协程，并调用操作者提供的回调函数。读写操作则是注册 NioThread 对应的读写事件，然后挂起操作者协程直到事件触发。由 ts-socket 进行读写操作并将操作结果返回给用户。在这整个过程中，用户的语法与传统的 BIO 高度相似，以下是使用 ts-socket 实现的 echo 服务器的例子：val handler: suspend BufferedAsyncSocket.() -> Unit = { while (open) { val read = read(timeout = 60 * 1000) write(read) } } fun main() { val server = BufferedNioServer(port = 12345, handler = handler) server.run() }NioThread接口 NioThread 所在的包cn.tursom.niothread包含了在独立线程上运行的，与业务逻辑无关的基础代码与接口。这个包的设计由需求驱动，因较复杂的需求而变得比较复杂。NioThread 的逻辑架构分成三层，每层代表了不同的抽象需求。按照调用关系为：NioThread->workLoop: (thread: NioThread, key: SelectionKey) -> Unit->NioProtocol。NioThread 代表执行 NIO 循环的线程，是最底层的部门。其负责调用selector.select，并将 select 出的每个 SelectionKey交给 workLoop 处理。NioThread 由实现原理可分为通过线程池实现的ThreadPoolNioThread与普通线程+while循环实现的WorkerLoopNioThread。workLoop 是 NioThread 在每个循环时都会调用的回调函数，负责解析处理每个被 select 出的 SelectionKey。主要实现有BossLoopHandler、WorkerLoopHandler与MultithreadingBossLoopHandler。NioProtocol 由 workLoop 调用，由用户提供，表示对应事件发生时的回调。

对go atomic泛型化的尝试
type: item
date: 2022-12-03T23:40:00+08:00
description: 本文源码可查看 https://github.com/tursom/GoCollections/tree/master/lang/atomicGO 在1.18终于添加了泛型。有了泛型，我们对一些泛用接口就有了更好的约束手段，合理使用泛型可以更好的复用代码。而本文则是在泛型技术的基础上对GO的原子操作简化的尝试。我在atmoic包里定义了 Atmoic[T any] 接口，这个接口定义了针对泛型 T 的原子化实现，包括 Load、Store、Swap以及最重要的CompareAndSwap。我还定义了 Atomizer[T any]接口以提供操作不同类型的方法工厂，这个接口提供四个无参函数，分别返回操作其指定类型的四个原子化方法。默认提供的实现是atomizerImpl[T]，提供 (u)int32/64 以及引用的共五种 Atomizer。考虑到开发中几乎不会需要自定义 Atomizer，类型 atomizerImpl 并没有被导出。type ( Atomic[T any] interface { Swap(new T) (old T) CompareAndSwap(old, new T) (swapped bool) Load() (val T) Store(val T) } Atomizer[T any] interface { Swap() func(addr *T, new T) (old T) CompareAndSwap() func(addr *T, old, new T) (swapped bool) Load() func(addr *T) (val T) Store() func(addr *T, val T) } atomizerImpl[T any] struct { swap func(addr *T, new T) (old T) compareAndSwap func(addr *T, old, new T) (swapped bool) load func(addr *T) (val T) store func(addr *T, val T) } )全部五种 Atomizer 实现都提供了对应的获取方法，值得一提的是由于unsafe.Pointer与引用有完全相同的内存结构，所以你获取到的针对特定类型引用的 Atomizer 其实就是实现 unsafe.Pointer 原子化的pointerAtomizer。var ( int32Atomizer *atomizerImpl[int32] int64Atomizer *atomizerImpl[int64] uint32Atomizer *atomizerImpl[uint32] uint64Atomizer *atomizerImpl[uint64] pointerAtomizer *atomizerImpl[unsafe.Pointer] ) func GetAtomizer[T any]() Atomizer[*T] { // atomizerImpl[*T] 与 atomizerImpl[unsafe.Pointer] 有完全一致的内存结构 // 因此可以通过 unsafe 强制转换 return (*atomizerImpl[*T])(unsafe.Pointer(pointerAtomizer)) }对 GO 泛型的探索同样体现在函数CompareAndSwapBit上，其泛型类型被限定在了 int32/64 与 uint32/64 中。这个函数还要求用户提供对应类型的 cas 实现函数。在没有泛型的时代，这个函数至少需要为32位和64位类型分别提供两个函数。func CompareAndSwapBit[T int32 | int64 | uint32 | uint64](cas func(p *T, old, new T) bool, p *T, bit int, old, new bool) bool { location := T(1) << bit oldValue := *p if old { oldValue = oldValue | location } else { oldValue = oldValue & ^location } var newValue T if new { newValue = oldValue | location } else { newValue = oldValue & ^location } return cas(p, oldValue, newValue) }我还尽可能多的为基础类型定义了对应的原子类，比如 int8 的原子化类type Int8 int8。这样定义类可以方便对象在不…