功能要求非常简单,连接扫描仪,调用扫描仪扫描常见规格的试管盒。识别每个试管底部/顶部的Data Matrix。 技术选择 目标平台是Windows,理论上C#之类是最合适的,然而基于熟练度,只能Java/Go二选一,最终选择了Go。 连接/调用扫描仪 支持twain协议即可,选择twain 。 Data Matrix 识别 简单分割图片成M x N个小图片,依次进行识别,选择了dmtx。 图像处理 为了提高dmtx的识别能力,需要对扫描的图片做一定的处理(反色,锐化,对比度,亮度调整……),用哪个library都一样。 GUI框架 因为组件原因选择了fyne。 一些复杂的地方 windows下通过cgo调用libdmtx 通过DLL调用dtwain 大致就是这个套路 dTwain, e := syscall.LoadLibrary("dtwain64u.dll") //载入dll selectSourcePtr, e = syscall.GetProcAddress(dTwain, "DTWAIN_SelectSource") //获取函数地址 acquireR1, acquireR2, callErr := syscall.SyscallN( acquirePtr, //method source, //Source ptr(name), //FileNames uintptr(imageType), //FileType ,100 for BMP ,1000 for PNG <http://www.dynarithmic.com/onlinehelp5/dtwain/dtwain_file_transfer_constants.htm> 66, //FileControlFlag 1000, //PixelType <http://www.dynarithmic.com/onlinehelp5/dtwain/index.html?dtwain_pixel_type_constants.htm> 1, //NumPages 0, //bShowUI 0, //bCloseSource uintptr(unsafe....
Windows下通过Cgo调用libdmtx
Cgo的准备工作 安装msys2 在MSYS2 MINGW64中将Go的bin加入到path(比如.bashrc export PATH="/c/Program Files/Go/bin:$PATH" 确认go在msys2中是可用的即可 安装工具链 pacman -S --needed base-devel mingw-w64-x86_64-toolchain 选择GCC 可以通过以下代码确认Cgo package main /* #include <stdio.h> void Hello(){ printf("Hello world\\n"); } */ import "C" func main() { C.Hello() } Windows编译libdmtx clone仓库 git clone <https://github.com/dmtx/libdmtx.git> 安装autoconf pacman -S autoconf automake libtool 开始编译 ./autoconf ./configure make make install #这一步就会产生一个.libs文件夹,里面就是Cgo需要的文件 Cgo调用libdmtx package main /* #cgo windows LDFLAGS: -L....
客户机接入进阶操作
经过之前的折腾,正常使用了几个月,现在客户那边又有了新政策,几乎所有的服务都只能通过内网访问,甚至包括git(甚至在内网通过http拉取git也需要加proxy)。 客户开发部门的建议是直接用客户机开发,但是客户机孱弱的性能和瞎眼的屏幕,我肯定是选择铤而走险。 最合规的方案 在我折腾的同时,也给了团队其他人一个合理合规的建议: 开启日常开发机上的ssh,将项目的repo作为一个Bare Git Repository 客户机上作为git client,同时追踪bitbucket和开发机上的git repository,手动或者脚本将两个remote同步成一致。 基础选择 直接选择了最方便的VM+OpenWRT的方案。客户机器通过wifi接入互联网,给OpenWRT VM分配双网卡 OpenWRT WAN <---> NAT Network <---> VPN Network LAN <---> Bridge Ethernet <---> Mac 依然是用网线将开发机和客户机连接起来。这样就能直接使用客户机的网络了。 强力的安全软件及应对 (这一环节是推测,并无实质证据) 发现这样配置好之后,开发机和客户机的OpenWRT只能短暂通信几秒钟,就会被断开。将开发机(Mac),客户机宿主(Windows)VM(OpenWRT)设置成同一个内网之后。发现 Window OpenWRT Mac Window - ✅ ✅ OpenWRT ✅ - ❎ Mac ✅ ❎ - 推测应该是安全软件在网卡底层直接判断了Dest,直接一刀切了,不让入站。 所以没有办法使用桥接的有线网卡了,所以我选择USB网卡,直接把USB挂载到VM里去。所以网络变成了: OpenWRT WAN <---> NAT Network <---> VPN Network LAN <---> USB Network Adapter <---> Mac 选择性的使用客户网络 实际上只需要内网IP走客户网络即可。让所有流量默认走原来的网络,特定网络走OpenWRT。有两种方案,一种是临时的路由表...
通过客户机接入的一些小操作
现在在某客户项目上,一些资源只能通过客户电脑上的VPN访问。客户机是一台Windows。而且并没有管理员权限。而我平常开发用的是Mac,使用起来两台电脑切换非常不方便。 远程桌面 因为客户机防火墙拒绝所有接入连接,所以直接通过RDP连接到客户机是不现实的。 有的同事甚至通过把两台机器加入到同一个zoom meeting,然后再分享桌面,申请控制权来实现远程桌面。 而我选择了端口转发,客户机有安全审计,像frp是立马报毒的。所以能用到的只有ssh,毕竟谁能说自带的ssh是危险应用呢? ssh -R 8080:127.0.0.1:3389 user@remote.host 将远端的8080端口映射到本机的3389端口,经过尝试,发现机器直接拒绝远程登入。所以必须寻找一个远程桌面的代替品,这里直接使用了RustDesk,可以在无UAC的环境下执行,开启IP直接登录和密码访问,就能解决第一个问题,远程桌面。 SSH Server采用了在日常使用机器上的Gost gost -L forward+ssh://username:password@0.0.0.0:2222/ 断线重连 putty的衍生版本Kitty(http://www.9bis.net/kitty/) 支持记录密码 支持忽略Host Key变更 自动锁屏 客户机设置了自动锁屏,而且锁屏之后需要按Ctrl+Alt+Del才能进入输密码的界面。我的账户也没有权限修改,一旦锁屏,远程桌面(RustDesk)只能看着锁屏界面发呆。这里用到了两个小软件(因为只用一个会偶尔失效),分别是 Caffeine,模拟定时按一个不存在的F15按键。 StayAwake,鼠标不停移动1px。 代理 这就是一个更加敏感的操作了,尝试在客户机跑gost,还是因为操作审计,立马被删了,而且第二天客户IT就找上门来了。 所以,什么软件可以起到代理的作用,但大家又会觉得这是无害的呢? 答案是:抓包软件 所以我在客户机上装了一个fiddler。 连接 最后一个问题了,怎么样才能让Mac和Windows互相访问呢?公司规定客户设备是不能接入办公室网络的,只能接入访客网络。而访客网络是AP隔离的。 答案就是一根网线。都配上169.254.xxx.xxx网段,就可以互通了。 一同组合拳下来,操作体验上升了1251223%。
iptables + tproxy 实现透明代理
先看一下iptables,正经图 路由设置 ip rule add fwmark 1 table 100 #带有mark1的包都发到table 100 ip route add local 0.0.0.0/0 dev lo table 100 #所有包都走lo(本地回环) 不同场景 内网机器访问国内服务 A的请求到达路由器的PREROUTING链,根据链上规则 -m set --match-set reserved_ip dst -j RETURN 直接返回原来的PREROUTING连,走FORWARD以及后续的操作 内网机器访问国外服务 A的请求到达路由器的PREROUTING链,根据链上规则 -p tcp -j TPROXY --on-port 1081 --tproxy-mark 0x1/0x1 给TCP请求加上mark,然后重定向到1081,根据前面的路由设置,会直接到本地回环,请求会到达Transparent Proxy 判断需要走Proxy,那接下来的流程就是,包装原请求并发出,带上了0xff(即255)的mark 按照OUTPUT链上的规则 -m mark --mark 0xff -j RETURN `` 直接返回OUTPUT链,走POSTROUTING链出去 判断为直连,直接原封不动的请求真实服务器,也是带上0xff的mark,后面和情况1一样 路由器访问国内服务 路由器的请求会从OUTPUT开始,会匹配上OUTPUT链上规则 --match-set chnroute dst -j RETURN 直接返回OUTPUT链,走POSTROUTING链出去...
一个可以接受的BFF
公司几乎所有项目都会加上一个BFF,没有为什么,就是想要一个BFF,经历过几个项目,无一例外,BFF都成了一坨屎。包括不限于以下问题: request和response明明都不需要做任何修改,但就是要重新写一遍给客户端的API和调用上游的Client 需要把返回包上code/data/message,然后在API层面调用无数次的ResponseData.ok(……) 背锅侠,任何问题都会被第一时间找到,需要找各种日志自证清白 在最近的一个项目中,我在客户的限制下搭了一套不太完善的BFF架子,目的就是避免上面的部分问题,尽可能减少一些模板代码。 客户限制:必须要Java+Spring Boot,而且必须要WebMVC,不能是Webflux/Reactor 全局 最终成品如下 从上往下 Sleuth Tracing 这是一个servlet filter,用来给req加transactionId 自定义的Log Filter,用来记录所有的Request和Response Reverse Proxy Filter,会处理特定的path,直接进入转发流程。 Spring Mvc的Filter,进入Spring的处理范围。 Proxy 对于Proxy部分,会有一串Interceptor,这个interceptor chain是金字塔形状的, 如果对request做操作,order越优先,越接近原始,越往后,越接近转发给上游的状态 如果对response做操作,order越优先,越接近返回给客户端的状态,越往后,越接近从上游拿到的原始状态 所以 Logger的优先级放最低,拿到发给上游和从上游拿到的结果。 ErrorCatch放次低,在第二顺位拿到上游的错误,包装后抛出 ErrorHandler放最高,在最外层捕获到所有错误,记录后给客户端友好提示 Spring ResponseDataBodyAdvice负责对API包内的返回值进行包装,判断如果returnType是由Jackson处理,那就包一层。 @Slf4j @ControllerAdvice(basePackages = "com.example.bff.api") public class ResponseDataBodyAdvice<T> implements ResponseBodyAdvice<T> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType); } @Override @SneakyThrows public T beforeBodyWrite(T body, MethodParameter returnType, MediaType selectedContentType, Class<?...
递归到Y组合子
前面已经得到一个非递归版本的“递归”求列表长度的函数了。 ((λ (mk-length) (mk-length mk-length)) (λ (mk-length) (λ (l) (if (null? l) 0 (+ 1 ((mk-length mk-length) (cdr l))))))) 再次回想一下,一般所需要的递归形式 \[\displaylines{ f(l)=\begin{cases} 0, & \text{if $l=0$} \\ 1+f(l-1) \end{cases}\\ } \] 所以,需要在上面的函数中,将(mk-length mk-length)给想办法替换掉。得到一个如下的版本。 ((λ (mk-length) (mk-length mk-length)) (λ (mk-length) ((λ (length) (λ (l) ;最内层 (if (null? l) 0 (+ 1 (length (cdr l))))) ) (mk-length mk-length)) )) 去执行一下代码就能知道,是无法正常工作的,原因就在于: 传进去的length 是由(mk-length mk-length)计算而来。 之前的计算是放在最内层,一旦l满足要求,就不会在内层计算(mk-length mk-length) 现在被抽到外面作为值传入,无法得知函数何时该终止,只能无限计算(mk-length mk-length) 所以,还是需要想办法把这部分操作延迟到内层进行。有个很简单的延迟计算办法,就是将其用lambda裹一层。如下: ((λ (mk-length) (mk-length mk-length)) (λ (mk-length) ((λ (length) ;length不再提前计算 (λ (l) ;最内层 (if (null?...
不常见k8s设置+不合理DNS服务=大坑
现象 在K8s的集群中,有部分容器内部(其实大部分,也许是所有),可以正常nslookup域名,但ping一个域名的时候,会报错 / # nslookup baidu.com Server: 10.43.0.10 Address: 10.43.0.10:53 Non-authoritative answer: Non-authoritative answer: Name: baidu.com Address: 220.181.38.148 Name: baidu.com Address: 220.181.38.251 / # ping baidu.com ping: bad address 'baidu.com' 但是在k8s各个节点上用docker运行同样的镜像,却没有这个问题。 最后几经搜索,发现了问题。 奇怪的配置(其实不算坑 k8s会替换容器内的/etc/resolv.conf。 If a Pod’s dnsPolicy is set to default, it inherits the name resolution configuration from the node that the Pod runs on. The Pod’s DNS resolution should behave the same as the node. But see Known issues....
匿名函数的递归
一个简单的递归求列表长度: (define length (λ (l) (if (null? l) 0 (+ 1 (length (cdr l)))))) 如果不允许使用define,如何实现? 先实现一个最简单版本,当列表为空时成立。 (λ (l) (if (null? l) 0 (+ 1 (...... (cdr l))))) 先忽略......处,显然,当l为空的时候,能得到正确返回0。就叫它lenght0吧。 依托已有的这个函数,来实现当list为1的时候,也能正确返回的函数: (λ (l) (if (null? l) 0 (+ 1 (length0 (cdr l))))) 可以看出 和空列表版本几乎一致,唯一的区别是......和length0 由于不让使用define,所以无法直接使用lenght0,需要代换 最终length1如下: (λ (l) (if (null? l) 0 (+ 1 ((λ (l) (if (null? l) 0 (+ 1 (...... (cdr l))))) (cdr l))))) 再进一步,得到length2,如下: (λ (l) (if (null?...
一次失败的couchbase troubleshooting
前情提要 大约1年半前,我在某咖啡项目上做一些后端业务,数据库是用的couchbase。(谁选的,为啥选的,一概不知。 在使用过程中发现,并发高了之后,Couchbase在平均响应时间/95%时间依旧非常美观的情况下,后端API反倒会夹杂着一些请求超时,并发越高越明显。 这个问题一直没有被解决,最近机缘巧合,开始尝试解决这个问题。 先通过非Java版本(就是Go)的Couchbase SDK做同样的测试,得出问题应该是出在Client端,而非Server端。1 根据Couchbase SDK的源码梳理了一下整个查询的流程。 尝试回答之前的问题 为什么会超时? 在couchbaseNode那一块逻辑的时候,没有足够的Endpoint,会直接进入重试,重试直至超时。 怎么解决Endpoint不够的问题? 最直接的办法就是调大Endpoint的连接数,Spring Data默认1,官方推荐6-10,在实际项目中甚至都被调到30-50了。数值调大之后确实缓解了出错情况。 为什么Endpoint调这么大了,依然会出错? 因为没来得及释放Endpoint,负责释放Endpoint的IO线程被Block住了。后面的业务逻辑实际上是在IO线程上执行。 图上publish response给客户端,是在Computation线程池,为什么会出现IO线程的问题? 按照设计,业务确实应该都是跑在Computation线程池上(或后期切换到其他线程),IO线程对SDK的使用者来说,应该是不可见的。 问题出在这个代码块: //org/springframework/data/couchbase/core/RxJavaCouchbaseTemplate.java:392 @Override public <T>Observable<T> findByN1QL(N1qlQuery query, Class<T> entityClass) { return queryN1QL(query) .flatMap(asyncN1qlQueryResult -> asyncN1qlQueryResult.errors() .flatMap(error -> Observable.error(new CouchbaseQueryExecutionException("Unable to execute n1ql query due to error:" + error.toString()))) .switchIfEmpty(asyncN1qlQueryResult.rows())) .map(row -> { JsonObject json = ((AsyncN1qlQueryRow)row).value(); String id = json.getString(TemplateUtils.SELECT_ID); Long cas = json....