System.out.println 的线程安全问题

有锁的 System.out.println

今天,我在实验“多线程循环输出 helloworld”的时候,发现“Hello,world!”在 Terminal 中的输出不会被乱序插入。而是这样:

可以看到,下面的“Hello,world!”排列整齐。于是,通过查阅源码,我发现 println 方法是有锁的:

System.out.println 带来的伪原子性

锁粗化

对于如下代码,我们都知道,volatile 只能防止指令重排序和保证内存可见,不能确保变量的原子性:

public class Test {
    private static final int THREADS_COUNT = 20;
    public static volatile int count = 0;
    public static void increase() {
        count++;
    }
    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(count);
    }
}

所以,count 变量最后不会达到 200000.

但是如果这样写:

public class MultiThreadIncrease {
    static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {}
    static void increase() {
        count++;
        System.out.println(0);
    }
}

最后的输出将会是 200000。解释:对于处在循环内的 sychronized,Java 的锁粗化机制会把它优化为:

/* 优化前 */
for(i) {
  sychonized (obj) {}
}
/* 优化后 */
sychonized (obj) {
  for(i);
}

内存同步

对于这个:

public class MultiThreadBoolean implements Runnable {
    public volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        MultiThreadBoolean runnable = new MultiThreadBoolean();
        new Thread(runnable).start();
        Thread.sleep(3000);
        runnable.flag = false;
        Thread.sleep(3000);
        System.out.println(runnable.flag);
    }
    @Override
    public void run() {
        while (flag) {
        }
    }
}

大家都知道因为 flag 的缓存没能和内存实时同步,所以造成 System.out.println(runnable.flag); 之后工作线程仍在运行的问题。但是如果这样:

while (flag) {
  System.out.println("1");
}

工作线程就会如期退出。解释:println 在每次输出后都会清楚工作线程的缓存,造成工作线程的内存同步。

引入多个链接选项

一个大型项目在编译的过程中,有很多的库需要链接。但是在 Mac 上,由于复杂的系统结构和非官方的包管理器,导致由 brew 安装的众多第三方的开发库不能被系统正确的识别和发现。所以我们需要手动指定链接选项,比如这样:

ssl-brew-info

说实话,这是非常不靠谱的,因为即使配置了 export,在许多的编译环境下,由于脚本执行顺序的问题,变量还是没能成功引入。但是我只是临时用用,所以无所谓了。

对于多个库的链接要求,我们这样配置:

multi-export

我们在配置 PATH 的时候,也不能“竭泽而渔”,使用 export 一定要注意,不要覆盖之前已经定义的变量。

记一次上线

在完成了业务串讲之后,我的第一个任务是封堵线上的一个漏洞。

详情

漏洞的详细内容不能透露,但大致是线上用户查询操作没有鉴权导致遍历错误。出现问题的不是核心服务,而且不会导致用户信息泄漏,所以不算高危。

漏洞的堵法很简单,修改底层的接口,添加一个可以为空校验字段,就可以完成过渡期的逻辑。作为一名新人,这个光荣的任务就交给我了。按理说这么小的一个漏洞,总体市场也不过一天。但是因为流程不够熟悉,再加上这两天会议有点多,我还是拖到了一天半才正式完成。

撸码

写代码的过程非常简单,主要的难点在于单元测试。因为近期组内业务变动较大,所以 dev 环境不稳定,导致依赖 dev 环境的本地单元测试跑不同。所以这次单测主要是采用了 Mock 的方式,屏蔽了 dev 环境的 RPC 访问。说实话,单元测试是我花费时间最长流程。

问题

除了测试上的难点,这次开发翻了许多小错误。比如各种文档参照模板来写但是没有改日期,Long 装箱类型的比较使用 == 没有用 .equal()。因为 Mock 的时候数字太小所以没有测试到这些问题。各个方面其实都还有一些不足之处需要改进。

反思

“苦练基本功”。确实,相较于组内的诸位大佬,我的基本功还不够熟练。加下来,我会在数据库,代码细节上面加码,在后面的开发中避免这类基本错误。

WireGuard 配置 IPv6 隧道

之前配置网络的时候看了这篇 GRE、Wireguard、网桥与 IPv6 收到了触动,又因为之后要去美团,不知道有没有好用的 IPv6 网络,所以决定搭建 IPv6 隧道。经过一番 Google,我发现网上其它文章都是通过 NAT 方式分配的 IPv6 地址,正好之前我有折腾过 IPv6 网关,这里就来自己实现以下公网 IPv6 的分配。

WireGuard 的基础配置

首先是配置一个最基本的 WireGuard。WireGuard 配置文件分为两部分,一部分是本机配置,另一部分是 peer 配置。贴一份服务器配置:

[Interface]
PrivateKey = <server-secret>
Address = 192.168.10.1/24
PostUp   = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
ListenPort = 10101
DNS = 1.1.1.1
MTU = 1420
[Peer]
PublicKey = <client-public>
AllowedIPs = 192.168.10.2/24

可以看到,这份配置文件里有局域网 IP,认证,路由和 DNS 等配置,灵活度还是很高的。而且格式简单,很快就能上手。这里我们仅仅配置了 IPv4,IPv6 的部分等会再说。

然后是 Client 的配置文件:

[Interface]
PrivateKey = <client-secret>
Address = 192.168.10.2/24
DNS = 1.1.1.1
[Peer]
PublicKey = <server-public>
Endpoint =  <server-address>:10101
AllowedIPs = 0.0.0.0/0, ::0/0
PersistentKeepalive = 25

可以看到,客户端的配置就非常简单了。

IPv6 地址分配

其实截止上一步,我们完成了 IPv4 NAT 隧道的搭建,现在本机可以使用服务器 IPv4 地址进行网络访问。但美中不足的是,WireGuard 生成的 wg0 网卡不支持 bridge。所以我们要再多加一层 gre 隧道来实现公网 IPv6 地址分配。

在服务器上,我们运行:

SERVER_IP=<server_ipv4_addr>
SERVER_IP6=<server_ipv6_addr>
SERVER_LOCAL_IP=172.0.0.1
INTERFACE=eth0
iptables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
ip6tables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
ip link add taps_bridge type bridge
ip link set taps_bridge up
ip address add $SERVER_IP6/64 dev taps_bridge
ip address add $SERVER_LOCAL_IP/16 dev taps_bridge
ip link add name gretap_office type gretap \
    remote 192.168.10.2 local 192.168.10.1 ttl 225
ip link set gretap_office up
ip link set gretap_office master taps_bridge

在客户端上,我们运行:

ip link add name gretap_private address 12:ab:34:cd:56:ef type gretap \
    remote 192.168.10.1 local 192.168.10.2 ttl 225
ip link set gretap_private up
ethtool --offload gretap_private tx off

上面两端脚本的具体含义是,在服务器上建立一个名为 taps_bridge 的网桥,用于桥接所有的接入客户端。同时建立 gretap,经由 WireGuard 建立一个 ip 隧道,并在此隧道上分配 IPv6 地址。客户端建立 gretap 然后通过 WireGuard 于服务器相连。

为了分配 IPv6 地址,我们还需要完成两件事:

通过 RADVD 分配地址:

interface taps_bridge {
    AdvSendAdvert on;
    prefix <server_ipv6_subnet>/64 {
        AdvOnLink on;
        AdvAutonomous on;
        AdvRouterAddr on;
    };
};

通过 NDPPD 启用邻居发现协议:

proxy eth0 {
    autowire yes
    rule <server_ipv6_subnet>/64 {
        auto
    }
}

这样,客户端得到的 IPv6 地址就会是公网地址,可以被其他设备正常访问而不需要 NAT66.

附两张效果图:

服务器

客户端

ping 测试

存在的问题

由于我们的公网地址是通过 gretap 分配的,所以在 Windows 上大家很难完成这一操作。可以通过在虚拟机 Linux 内建立隧道,然后通过路由 NAT 给本机比较合适。诸位如果有更好的办法也欢迎在下方留言。

2019 下半年技术规划

收到了美团实习 Offer 之后,就是马上要参加工作的人了,在这里详细规划一下后半年的技术目标。

工作

根据 Offer,预期的实习时间是从 6 月 21 开始,今年内应该都会在美团。每周大约工作五天,算是 995 吧。算上通勤时间,每天早晨 8 点就要出门,一直到晚上 10 点才能回来,工作压力还是挺大的。

美团的技术栈是 Java,数据库猜测是 MySQL。所以后半年的技术栈应该会放在 Java 上面。据说美团会发一台 Macbook Pro,编程体验应该还是不错的。就是不知道公司网络如何。

个人技术

因为美团技术栈偏向 Java,所以后半年的技术应该会围绕 Java 展开。根据最近的趋势,后端这块 Java 依然是爸爸,其它的 JVM 语言根本不能打。像 Golang、Rust直流也不是面试的主要问题。又考虑到还有海量的 Android 历史代码用 Java 写就,Java 势头依然强劲。

为了和工作相互补充,我会在工作之外掌握 Java 常用的几个框架,并维护自己的开源代码。首先是 Java Web 部分,一个经典的 Blog 需求是必不可少的。然后是搁置许久的 Hybrid Cloud 开发计划,考虑目前的精力不足以再让我精通 C#,所以只好用 Java 了。

考虑到美团还有一些历史代码是 PHP 写就,掌握基本的 PHP 知识也是必不可少的。以目前的趋势看,PHP 会逐渐被 Python 之流取代,所以水平只需要能读懂 Typecho 代码就 OK。

开源

首先就是 Hybrid Cloud 了,这个不能放下。然后是移植我的 Blog,预期是 Spring Boot。其它的计划暂定。有了 Macbook Pro 之后或许会有心情学学 Swift 写几个 APP。

写在最后

后半年的重心是深入自己的 Java 技术栈。对于其它的所谓“趋势语言”,并没有急切地需求。在完全熟悉 Java 细节之后再考虑也不迟。

撸起袖子加油干,走好我的长征路!

《下一代网络技术》四

# 《下一代网络技术》课程作业四

请说明 GPRS网络设备 GGSN、SGSN 的功能

GGSN

GGSN(Gateway GPRS Support Node) 网关GPRS支持节点,主要是起网关作用,它可以和多种不同的数据网络连接,如 LAN 等。GGSN 可以把 GSM 网中的 GPRS 分组数据包进行协议转换,从而可以把这些分组数据包传送到远端的 TCP/IP 网络或 X.25 网络。具体包括:

  • 网络接入控制功能。包括:网络控制的信息屏蔽、计费等;
  • 维护路由表,实现路由选择和分组的转发功能。包括:PDP上下文激活、PDP上下文修改、DNS、隧道传输等;
  • 用户数据管理,实现对分组数据的过滤
  • DHCP

SGSN

GPRS 服务支持节点。主要包括:

  • 用户数据管理:进行用户数据库的访问及用户接入控制。例如 PDP 信息和路由信息等。
  • 移动性管理和会话管理:SGSN网元的移动性管理包含终端的附着、寻呼以及位置相关的功能;会话管理指SGSN可以完成激活、修改或者删除PDP上下文等功能,此外SGSN还与MSCS相连,以支持数据业务和电路业务的协同工作和短信收发等网络应用。
  • 分组路由选择和转发:SGSN 可提供路由查询功能,查找 GGSN 的 IP 地址,为上下行的数据提供转发和传输功能。
  • 计费功能:SGSN 具备话单采集功能并可以传送用户的计费信息;这在早期GPRS业务尚未普及时应用较多。

SGSN 是 GPRS 网络功能的主要承载部分。

请说明 GPRS 附着、PDP 上下文激活的含义

用户接入 GPRS 网络时,必须进行 GPRS 附着和 PDP 上下文激活。通过 GGSN 和 SGSN 配合,用户进行 TMSI 认证,并由 SGSN 在 GSM 数据库中查询 IMEI,最终使设备顺利接入当前网关。其中,PDP 上下文激活还包括 DNS 查询,返回 GGSN IP 地址等功能。

请说明 IMS 系统中的 P-CSCF、I-CSCF、S-CSCF、HSS 的作用

CSCF 呼叫会话控制功能(Call Session Control Function)是 IP 多媒体子系统(IMS:IP Multimedia Subsystem)内部的功能实体,是整个 IMS 网络的核心。

P-CSCF

P-CSCF 是 IMS 网络的统一入口点。所有发起于 IMS 终端和终止于 IMS 终端的会话消息都要通过 P-CSCF。P-CSCF 是一个 SIP Proxy。

I-CSCF

I-CSCF 是 IMS 归属网络的入口点。在注册过程中,I-CSCF通过查询 HSS,为用户选择一个 S-CSCF。在呼叫过程中,去往 IMS 网络的呼叫首先路由到 I-CSCF,由 I-CSCF 从HSS 获取用户所注册的 S-CSCF 地址,将消息路由到 S-CSCF。

S-CSCF

S-CSCF 在 IMS 网络会话控制中处于核心地位,它接受来自拜访网络通过 P-CSCF 转发来的注册请求,与 HSS 配合进行用户鉴权。进行 SIP 业务触发,实现丰富的 IMS 业务功能。

阅读 ims_to_ims_call.pdf 文件,分析 IMS 用户 A 从其漫游地向位于归属地的 IMS 用户 B 发起 IMS 呼叫并建立 IMS 会话的过程,说明 IMS 会话建立过程

用户 A 首先从漫游地的 P-CSCF 获取当前 IMS 网络信息,并由漫游地的 P-CSCF 查询到归属地 S-CSCF。归属地 S-CSCF 将消息转发到用户 B 归属地的 I-CSCF(同 IMS 节点内),此时 I-CSCF 查询 HSS 得知用户 B 的归属地,并通过 S-CSCF,从 P-CSCF 邀请用户 B。并依次返回成功消息。至此,用户 A 和用户 B 成功建立连接。

请说明 IMS 初始过滤条件(IMS Initial Filter Criteria)的作用

IMS 核心系统通过初始过滤进行业务触发。通过 SIP 包的内容,决定 SIP 包去向哪一业务。由 CSCF 分析并触发到规则指定的应用服务器,由应用服务器完成业务逻辑处理。

IMS 公共用户标识(Public User Identity)有哪两种

IP多媒体公共标识(IMPU – IP Multimedia PUblic Identity)和通配公共享户标识(Wildcarded PUblic User Identity)。IMPI 和 IMPU 可以是数字(Tel URI,如 tel: +86-182-0364-4567),也可以是字符标识符(SIP URI,如sip:john.doe@example.com)。

请说明对称型 NAT(symmetric NAT)进行私有网络(Private Network)和公共网络(Public Network)间的 IP 地址及端口号映射的特点

  • 每一个来自相同内部IP与端口,到一个特定目的地地址和端口的请求,都映射到一个独特的外部IP地址和端口。同一内部IP与端口发到不同的目的地和端口的信息包,都使用不同的映射
  • 只有曾经收到过内部主机数据的外部主机,才能够把数据包发回

请说明当 IMS 终端均位于私有网络中,为了在 IMS 终端之间建立 IMS 会话,需要采用 NAT 穿越(NAT Traversal)技术解决 NAT 对 IMS 会话建立造成的哪些问题

NAT穿越中的 STUN、TURN 方案的基本工作原理

STUN

客户端找出自己的公网地址,查出自己位于哪种类型的 NAT 之后以及 NAT 为某一个本地端口所绑定的公网端口。这些信息被用来在两个同时处于 NAT 路由器之后的主机之间创建 UDP 通信。

TURN

通过获取应用层中的公有地址达到NAT穿透。TURN 终端必须在通信开始前与 TURN 服务器交互,并要求 TURN 服务器产生 relayed-transport-address。这时 TURN 服务器会创建 peer,开始进行中继。TURN 终端利用 relay port 将数据发送至 peer,再由 peer 转发到另一方的 TURN 终端。

OpenIMSCore 配置

众所周知,在 Linux 下安装和配置软件环境,一个详细的文档是必不可少的。而且,软件包的二进制兼容问题也是难以手动解决的。最近我就遇到了一个名为 OpenIMSCore 的 SIPS 服务框架,配置它费劲千辛万苦。下面和大家分享一下。

OpenIMSCore 的官网已经挂掉,现在访问是一个指向 wix 的过期页面。目前最有公信力的“官网”是在 SourceForge 的项目文档,写的还算可以。

环境

因为是上古软件了,所以选了当前手头版本最古老的虚拟机 Ubuntu 14.04.6。考虑到之后还需要运行同样古老的 myMonster 软件,所以安装了 Desktop 版本。虚拟机是运行在 Manjaro 上的 Virtualbox。

安装过程

更新与换源

apt update && apt upgrade -y && apt install vim

安装 JDK 和 MySQL

首先是换源,这里因为是教育网,所以选择了清华源。之后安装 MySQL 和 OpenJDK。

apt install mysql-server openjdk-7-jdk ant

编译

然后克隆官方仓库并编译,在此之前需要安装 subversion:

apt install subversion
mkdir -r /opt/OpenIMSCore
cd /opt/OpenIMSCore
svn checkout https://svn.code.sf.net/p/openimscore/code/FHoSS/trunk FHoSS
svn checkout https://svn.code.sf.net/p/openimscore/code/ser_ims/trunk ser_ims

如果网速不佳,可以自行配置 proxychains,这里不再赘述。

然后是编译过程:

cd FHoSS
ant compile deploy
cd ..
cd ser_ims
make install-libs all
cd ..

这两部分可以同时进行以加快速度。其中 make 的过程也可以通过加参数 -j 以并行编译。如果等不及可以先进行下一步。对于依赖文件,大家可以根据报错信息来手动安装,这里提供我遇到的依赖错误:

apt install bison flex libmysqlclient-dev ipsec-tools libcurl4-gnutls-dev debhelper cdbs lintian build-essential fakeroot devscripts pbuilder dh-make debootstrap dpatch libxml2-dev

配置

导入 sql 配置文件:

mysql -u root -p < FHoSS/scripts/hss_db.sql
mysql -u root -p < FHoSS/scripts/userdata.sql
mysql -u root -p < ser_ims/cfg/icscf.sql

配置 DNS

首先要安装,这里选择官方推荐的 BIND9:

apt install bind

然后将默认的 DNS 配置文件添加到 BIND9 里:

cp ser_ims/cfg/open-ims.dnszone /etc/bind/
sed -i '3azone "open-ims.test" {\n\ttype master;\n\tfile "\/etc\/bind\/open-ims.dnszone";\n};' /etc/bind/named.conf.local
sed -i '2a127.0.0.1\topen-ims.test mobicents.open-ims.test ue.open-ims.test presence.open-ims.test icscf.open-ims.test scscf.open-ims.test pcscf.open-ims.test hss.open-ims.test' /etc/hosts

这里为了防止本机不生效,同步把配置添加到了 hosts 文件。然后重启 BIND9:

service bind restart

运行服务器

OpenIMSCore 一共分为 pcscf、icscf、scscf 三个 cscf 服务器,分别运行它们:

cp ser_ims/cfg/*.cfg ./
cp ser_ims/cfg/*.xml ./
cp ser_ims/cfg/*.sh ./
./pcscf.sh
./icscf.sh
./scscf.sh

然后我们运行 HSS 数据库:

cd FHoSS/deploy
./startup.sh

这一步如果报错的话可能是 JAVA_HOME 没有配置,这里我们直接写进文件内:

sed -i 's/JAVA_HOME\/bin\/java/JAVA_HOME\/usr\/bin\/java/g' /opt/OpenIMSCore/FHoSS/deploy/startup.sh

然后再次运行 HSS 服务。这里 HSS 服务会以 Tomcat 的形式供我们访问。地址:http://localhost:8080

OpenIMSCore 服务器的配置到这里就结束了。

客户端

因为我们配置的是本地服务器,所以客户端也要运行在本地。Linux 下好用的 IMS 客户端没多少,这里雷老师提供了 myMonster。这同样是一个已经挂掉官网的软件,请点击前面的链接下载。

OpenIMSCore 默认提供了 Alice 和 Bob 两个账户,这里我们填入账户信息。

myMonster 账户配置

输入对方的 SIP 地址就可以通话啦!

添加阿里云 OSS 作为图床

说到图床,有 BLOG 的朋友肯定都不陌生。之前几个好用的图床,要么性能不佳,要么需要备案域名,总之是各种麻烦。经过 @Faultiness 安利,我选择了[阿里云 OSS] 作为自己的图床。总结下来,有这么几个好处:

优势

  • 便宜,一年 40GB 空间才不到 ¥10。
  • 性能好。阿里云的性能不需多说,20MB 左右的高清大图加载不到 1s。
  • 方便。阿里云 OSS 有自己的 Windows 客户端,还提供了 API 方便的上传本地文件到 OSS。

下面来讲讲我是怎么创建 OSS 作为图床的。

过程

首先在阿里云启用 oss 服务,并选择付费方式。我建议流量小的站点寻用按量付费而不是按带宽付费。开启 oss 之后,转到控制台,新建一个 bucket:

新建 bucket

这一步建议购买流量包,会比直接按量付费便宜。记得一定要选择公共读。
创建完毕后转到文件管理,根据具体情况上传文件:

oss 文件列表

然后复制图片链接,就可以在 Markdown 里引入了。

复制图片链接

《下一代网络技术》三

# 《下一代网络技术》作业三

请说明 IPv6 头的结构特点?IPv6 头中定义了哪些可选头

包头长为 64bit,顺序依次是:版本 4bit,通信类别 8bit,流标记 20bit,分组长度 16bit,下一协议头 8bit,跳数限制 8bit。源地址 128bit,目的地址 128bit。

如图:

IPv6 Header

IPv6 地址有哪几种

单播(unicast)

单播地址标示一个网络接口。协议会把送往地址的数据包送往给其接口。单播地址包括可聚类的全球单播地址、链路本地地址等。

任播(anycast)

任播发送给距离最近的其中一个接收地址,当该接收地址收到数据包并进行回应,该接收列表的其他节点会知道某个节点地址已经回应了,就不会继续传输。

任播地址主要分配给服务端,且不能作为发送端地址。

多播(multicast)

多播地址也称组播地址。送到多播地址的数据包会被发送到所有的地址。它们的前置为 FF00::/8。

移动 IP 需要解决的问题是什么?移动 IP 中的主要功能实体有哪些

主要解决的问题是设备漫游时的网络通路问题。例如:对于无线终端,从一个局域网漫游到另一个局域网,使用移动 IP 保证链路联通。

主要功能实体有:移动节点、归属代理和外部代理,即:客户端,源代理和现代理。其中,源代理通过隧道等方式跨过现代理向终端传输数据包。

移动 IP 的基本原理是怎样的?移动 IPv4 与移动 IPv6 有什么区别

终端在漫游时含有两个 IP 地址,一个是 home address,另一个是 CoA。归属代理保存终端的相关信息,当服务节点与终端通信时,将数据包发往源地址,并由归属代理负责转发数据包。也可以通过 redirect 的方式告知服务节点终端的新 IP。

这一技术在 IPv6 的区别是:

  • 移动IPv6不需要外地代理的支持
  • 移动IPv6支持路由优化
  • 移动IPv6在移动节点不在本地网络时通过IPv6路由头部而不是隧道来路由
  • 可以通过邻居发现的方式来改变路由

如何使得一个应用程序既能使用 IPv4 对外通信,也能使用 IPv6 对外通信

首先,程序想通过 IPv6 对外通信,本机必须有 IPv6 地址。其次,程序必须监听 IPv6 端口。从编码上讲,创建 socket 时,必须指定是 IPv6 类型。

这里附一段 C++ 代码,使用 Boost.Asio 创建了可以在双栈网络运行的 Echo Server:

void session(tcp::socket sock)
{
  try
  {
    while (true)
    {
      std::vector<char> buffer(1024);
      boost::system::error_code error;
      size_t length = sock.read_some(boost::asio::buffer(buffer), error);
      if (error == boost::asio::error::eof)
        break;
      else if (error)
        throw boost::system::system_error(error);
      boost::asio::write(sock, boost::asio::buffer(buffer));
    }
  }
  catch (std::exception& e)
  {
    std::cerr << "Exception in thread: " << e.what() << std::endl;
  }
}
void server(boost::asio::io_context& io_context, unsigned short port)
{
  tcp::acceptor a(io_context, tcp::endpoint(tcp::v6(), port));
  while (true)
  {
    std::thread(session, a.accept()).detach();
  }
}

在 Docker 中部署 Jenkins

规范的项目一定会有好的持续集成过程。在本次课程的开发过程中,我部署了 Jenkins 作为小组的持续集成服务器。下面分享我的部署过程。

在 Docker 中部署 Jenkins

首先安装 Docker。我的母鸡是 Debian Sid,按照官网教程安装 Docker 和 Docker-Compose 之后,编写如下 docker-compose.yml 文件:

version: '3.3'
services:
  jenkins:
    image: jenkins/jenkins
    network_mode: host
    container_name: jenkins
    volumes:
      - /docker/jenkins:/var/jenkins_home
    restart: always
    ports:
      - 8080:8080
      - 50000:50000

然后运行 docker-compose up -d 创建并运行 Jenkins Docker。

更新 Jenkins

Jenkins 官方的更新服务器太烂,洛杉矶和香港的 VPS 实测都无法完成更新,所以我们换个速度快的源。这里我使用 mirrors.xtom.com.hk 内网源。

在“系统设置 > 插件管理 > 高级”里更换地址为 https://mirrors.xtom.com.hk/jenkins/updates/current/update-center.json

愉快的进行 DevOps 吧!

未完待续

在 Vue.JS 中使用百度地铁图 JS API

面向领域的实践这门课要求我们做一个地铁热力图的 Web 项目,我,@NIRVANALAN@0123goodvegetable 负责前端部分。

项目结构

前端我们选用了 Vue.JS,使用 vue-cli 创建前端项目,并加入了 element-ui依赖。以下是我们的项目结构:

.
│  .gitignore
│  babel.config.js
│  package-lock.json
│  package.json
│  README.md
│  vue.config.js
│
├─public
│      index.html
│      map.html
│
└─src
    │  App.vue
    │  main.js
    │
    ├─assets
    │      logo.png
    │
    ├─components
    │      Card.vue
    │      Chart.vue
    │      Sidebar.vue
    │      SubwayMap.vue
    │      UserHeader.vue
    │
    ├─plugins
    │      element.js
    │
    └─views

百度地铁图 JS API

在注册了百度地铁图 JS API 开发者密钥之后,我们就可以通过 CDN 的方式来引入百度地铁图 JS API。按照官方示例编写 HTML 之后,通过 CDN 引入的 JS API 会有全局的 BMapSub 类型用于创建地铁图。这里我们可以直接 new BMapSub.Subway('container', subwaycity.citycode); 来创建新的地铁图并绑定到 container 容器。

相关内容

newcoderlife/vue-bmap-demo

百度地铁图开发者指南

在 Vue.JS 中使用高德地铁图 JS API

面向领域的实践这门课要求我们做一个地铁热力图的 Web 项目,我,@NIRVANALAN@0123goodvegetable 负责前端部分。

项目结构

前端我们选用了 Vue.JS,使用 vue-cli 创建前端项目,并加入了 element-ui依赖。以下是我们的项目结构:

    Directory: C:\Users\newco\Develop\vue-amap-demo
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----          2019/5/7    19:12                node_modules
d-----          2019/5/7    19:41                public
d-----          2019/5/7    17:20                src
-a----          2019/5/7    17:20            214 .gitignore
-a----          2019/5/8    14:21              0 a.txt
-a----          2019/5/7    17:20             53 babel.config.js
-a----          2019/5/7    18:44           1060 package.json
-a----          2019/5/7    19:36         443106 package-lock.json
-a----          2019/5/7    19:44            286 README.md
    Directory: C:\Users\newco\Develop\vue-amap-demo\public
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----          2019/5/7    17:52            543 index.html
-a----          2019/5/7    18:10            637 map.html
    Directory: C:\Users\newco\Develop\vue-amap-demo\src
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----          2019/5/6    10:02                assets
d-----          2019/5/7    19:36                components
d-----          2019/5/7    18:44                plugins
-a----          2019/5/7    19:08           3549 App.vue
-a----          2019/5/7    18:11            331 element-variables.scss
-a----          2019/5/7    18:45            206 main.js
    Directory: C:\Users\newco\Develop\vue-amap-demo\src\components
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----          2019/5/7    19:36           2769 Chart.vue
    Directory: C:\Users\newco\Develop\vue-amap-demo\src\plugins
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----          2019/5/7    18:45             72 echarts.js
-a----          2019/5/7    17:20            108 element.js

高德地铁图 JS API

在注册了高德地铁图 JS API 开发者密钥之后,我们就可以通过 CDN 的方式来引入高德地铁图 JS API。按照官方示例编写 HTML 之后,通过 CDN 引入的 JS API 会创建 window.subway(container, opt) 函数用于创建地铁图。高德地铁图 JS API 并不像他家的地图 API 一样,会有 AMap 类提供使用,这里直接绑定在 window 上了。

相关内容

newcoderlife/vue-amap-demo

高德地铁图开发者指南

强行停止卡死的 Hyper-V 虚拟机

在 Windows 平台下,很多人使用 Hyper-V 作为虚拟化解决方案。我在配置一台用于实验的虚拟机时,遇到了虚拟机卡死,无法正常或强制关机的 bug(甚至重启也不行,万恶的巨硬!)。一番 gogl 之后,就有了这篇文章。

打开一个管理员权限的 PowerShell,键入如下命令:

$VMGUID = (Get-VM "VM-NAME").ID # VM-NAME需要换成你的虚拟机名称
$VMWMProc = (Get-WmiObject Win32_Process | ? {$_.Name -match 'VMWP' -and $_.CommandLine -match $VMGUID}) # 通过 VM-NAME 获取相关的进程
Stop-Process ($VMWMProc.ProcessId) -Force # 把它干掉

具体原理是找到 HOST 机对应的虚拟机进程然后结束掉。

leetcode 标签和索引

# leetcode 列表

为了秋招,肯定要下功夫刷一刷 leetcode 了。主要集中在常见的面试题型,leetcode 对于找工作还是有意义的。这里记录一下我的刷题经历,并对我刷过的题打标签和索引。

索引

编号 类型 标签 难度
1 数据结构 map 简单
2 数据结构 bigint 简单
3 算法 回文串 中等
4 算法 二分查找 中等
5 算法 回文串 中等
6 算法 模拟 中等
7 算法 模拟 简单
8 算法 模拟 简单
9 算法 模拟 简单
11 算法 线性算法 中等
14 算法 最长公共前缀、二分、Trie 树 中等
15 算法 二分查找、线性算法、剪枝、双指针 中等
21 数据结构 合并链表、插入排序 简单
23 算法 排序 中等
25 数据结构 List、Stack、列表逆序 中等
33 算法 二分、位运算 中等
42 算法 线性算法、双指针 中等
45 算法 动态规划、线性算法、贪心 中等
53 算法 二分、线性算法 简单
56 算法 线性算法、模拟 简单
54 算法 模拟 中等
70 算法 模拟、数学、搜索、记忆化搜索、斐波那契、快速幂 简单
78 算法 模拟 简单
88 算法 排序 简单
93 算法 模拟、剪枝 简单
98 数据结构 简单
102 数据结构 树、BFS 简单
120 算法 动态规划 简单
121 算法 线性算法 简单
135 算法 线性算法、动态规划 简单
146 算法、数据结构 LRU、Hash、List 简单
148 算法 归并排序 简单
175 数据库 合并两表、outer join 简单
176 数据库 查询第 k 大、排序 简单
177 数据库 查询第 n 大、定义变量 简单
178 数据库 数据排序、编号 简单
199 数据结构 遍历二叉树 简单
200 算法 DFS、BFS、并查集 简单
206 数据结构 模拟、反转链表 简单
215 算法 二分、排序 简单
695 算法 并查集、连通图 简单
1114 多线程 线程同步、atomic、条件变量 简单
1115 多线程 线程同步、锁 简单
1116 多线程 线程同步、锁 简单
1117 多线程 线程同步、锁 中等

相关链接

Github

《下一代网络技术》二

## SIP 协议中的 Session(会话)、Dialog(对话)、Transaction(事务)有什么不同?在分析 SIP 协议消息格式及字段含义的基础上,说明在 SIP 协议中如何标识一个会话、一个 Dialog、一个事务?

Session

会话是指在终端之间的媒体流,包括音视频和文件。在 SIP 协议中主要是指在链路上的话音数据。

Dialog

对话是指用户代理之间的联系,由 SIP 协议建立并持续一段时间。

Transaction

事物是指从客户端到服务器的一组请求以及对应的答复。

区别与标识

区别主要是三者的层次不同,在逻辑上存在大小之分。对话和事务处于信令层,而会话处于媒体传输层。Session 主要是在链路建立之后的通信,Dialog 可以包含多个 Transaction,可以通过 Call-ID、local tag、和 remote tag 进行标识。

SIP 协议中的用户代理(UA:User Agent)、用户代理客户端(User Agent Client)、用户代理服务器端(User Agent Server)、代理服务器(Proxy Server)这些实体有什么不同?

用户代理客户端

一个对象,可以创建新请求,并发往用户代理服务器。

用户代理服务器

相应 SIP 请求。

用户代理

包含用户代理客户端和用户代理服务器

代理服务器

服务器和终端的中介,可以代表终端发出请求。

有状态的代理(Stateful Proxy)和无状态的代理(Stateless Proxy)有什么不同?

有状态代理

保存请求的相关信息,每个请求可以默认地使用以前的请求信息。服务端容易对客户状态进行管理,服务端并不要求每次客户请求都携带额外的状态数据。

无状态代理

客户信息必须全部来自于请求所携带的信息以及其他服务器自身所保存的、并且可以被所有请求所使用的公共信息。并不保存客户请求的数据,客户在请求时需要携带额外的状态数据,无状态服务器更加健壮,重启服务器不会丢失状态信息,这使得维护和扩容更加简单。

阅读课程实验一中使用到的服务器软件 opensips 的文档以及参考资料“SER – Getting Started”第六章“HelloWorldser.cfg”及第10章“Call Forwarding ser.cfg”,说明呼叫转移(Call Forwarding)业务(/功能)的实现原理。

呼叫转移业务主要是由一个 SIP 服务器作为中转,通过配置呼叫转移策略,将客户端 A 发往客户端 B 的数据包转往客户端 C。这一配置可以通过 OpenSIPS 的配置文件完成,也可以由用户在数据库中添加相关的记录来实现。

分析opensips软件的实现原理,分析opensips软件在不需要重新编译的情况下,仅通过opensips.cfg文件就可重新配置opensips软件以实现新的业务功能的基本原理。

通过 OpenSIPS 源码进行分析,发现它的脚本文件解析是通过 lex 和 yacc 实现的,在 main 函数里有 yyparse() 函数可以解析 opensips.cfg,解析完成后,执行速度可以达到 C 原生代码级别。在其它的高级语言中,例如在 Java 中,这一特性是通过反射实现的,可以通过重载类的加载器来实现相关内容。

IPv6 邻居发现协议

在配置我的内网 NAS 的时候,遇到了一些问题(详情请看 Docker Container 分配独立 IPv6 地址)。主要是校园网分配给我的是一个 /64 前缀的地址,我不能继续往下分。为了使位于路由后的几台机子(容器)获得 IPv6 地址,我进行了如下配置。

适用范围

如果你的 ISP 通过 SLAAC 方式分配了唯一的 /64 地址给你,你又不想使用 NAT66,不妨看下去。如果不想看废话,请直接跳到最后。

前置科技

不想看的可以跳过。

IPv6 NDP

The Neighbor Discovery Protocol (NDP, ND) is a protocol in the Internet protocol suite used with Internet Protocol Version 6 (IPv6). It operates at the link layer of the Internet model (RFC 1122), and is responsible for gathering various information required for internet communication, including the configuration of local connections and the domain name servers and gateways used to communicate with more distant systems.

简单来讲就是 Host 机可以告知网络上的其它机器,某个 IPv6 公网地址在我这/在我旁边。举个栗子:我的本地地址是 2001:da8:215:d573:3055:2165:A:B,拥有地址 2001:da8:215:d573:3055:2165:A:C 的主机在物理拓扑上“躲”在我后面。通过 NDP,我们就可以告诉互联网上的其他主机,2001:da8:215:d573:3055:2165:A:C 在我这里,来找我,我帮你们转发数据包。在 Linux 上,配合 IPv6 Forwarding,就可以达到“IPv6 穿透”的目的。

NAT

Network address translation (NAT) is a method of remapping one IP address space into another by modifying network address information in the IP header of packets while they are in transit across a traffic routing device. The technique was originally used as a shortcut to avoid the need to readdress every host when a network was moved. It has become a popular and essential tool in conserving global address space in the face of IPv4 address exhaustion. One Internet-routable IP address of a NAT gateway can be used for an entire private network.

简单来说就是在 IPv4 时代构建局域网的技术。比如你把网线插在无线路由器的 WAN 口,你就可以上网了。这一技术在 IPv6 时代也是存在的,但是然并卵。IPv6 地址足够多,据称可以给地球上的每个沙子都分一个公网地址。所以在 IPv6 时代,通过组件内网来节约地址就没有必要。在任何时候 NAT66 都不应该作为你的选项。NAT66 的搭建方法在上一篇文章提到过,这里就不再赘述。

问题分析

我需要在母鸡只有一个物理网卡的情况下,给每个 Docker 容器分配公网 IPv6 地址。这就要用到上文的 NDP 协议了。每个地址都应该在原本的 /64 网络内,通过 NDP 协议“暴露” 给其它机器。Host 机打开 IPv6 转发,实现路由功能。

解决方案

我本地是 Debian Buster。首先安装 NDPPD 来自动管理“邻居”。当然如果你不想,通过 ip -6 neigh add 来手动管理也没问题。

apt install ndppd

然后,通过 Docker 自带的 IPv6 功能给每个容器分配一个合适的地址。其实使用 radvd 也不是不可以,但很显然 Docker 更快一点。Docker 配置文件 daemon.json 如下:

{
  "dns": ["101.6.6.6", "2001:da8::666"],
  "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn/"],
  "ipv6": true,
  "fixed-cidr-v6": "2001:da8:215:A:B::/80"
}

然后是配置 NDP 协议。相应的 ndppd 配置如下:

proxy enp1s0 {
    autowire yes
    rule 2001:da8:215:A:B::/80 {
        auto
    }
}

重启相关服务之后,位于 Host 机之后的机器(容器)就能取得 IPv6 地址了。

写在最后

相比于简单的 IPv4 协议,IPv6 的配置无疑更加困难。但是 IPv6 的便利性也是 IPv4 无法媲美的。在这里真诚的建议大家尽快将自己的网络环境升级到 IPv6 以获取更方便快捷的网络体验。

Docker Container 分配独立 IPv6 地址

** Deprecated!这样实现有一些小问题无法解决,请看这篇. **

因为 我国的 IPv6 建设逐渐提上了日程,改造自己的网络环境,全面拥抱 IPv6 刻不容缓 IPv6 不计流量,所以我要配置我的 Docker 支持 IPv6 网络。

踩过的坑

按照官网文档,只需要在 /etc/docker/daemon.json 里打开 IPv6 并配置 IPv6 前缀,Docker 就会按照前缀自动给每个容器配置 v6 地址。但是情况在北邮校园网不太一样。北邮校园网的 v6 分配方式是 SLAAC,前缀是 /64,最终拿到的地址是一个 /128 的地址,不像其它学校是一个 /64 的网段。这就导致了 Docker 不能获取足够的地址空间。

我按照网上的 NAT 方法(比如这个:Docker containers with IPv6 behind NAT),配置了 IPv6 NAT来解决问题。但是这又带来了新问题:BT 下载的时候,无法有效的获取 peer,外网不能直接对 Docker Container 发起通讯。这不能满足我的要求。并且,IPv6 地址那么多,NAT66 在任何情况下都不应该存在。

解决方案

不想看的可以直接跳到最后。

首先建立一个 Docker Network:

docker network create --ipv6 --subnet=fd00::/64 ipv6_bridge

然后运行 ifconfig 找到新创建的 Docker Network:

可以看到,我们新创建的名为 ipv6_bridge 的网络在 Host 机名为 br-f495e6f8d3fd

然后通过 brctl 把新创建的 network 和母鸡网卡桥接器来。我们运行:

apt install bridge-utls -y
brctl addif br-f495e6f8d3fd enp1s0
sysctl -w net.ipv6.conf.br-f495e6f8d3fd.accept_ra=2
sysctl -p --system

最后我们创建一个新的 Container 来测试一下,得到如下回显:

docker run -itd --name alpine --net=ipv6_bridge alpine sh
docker exec -it alpine sh
ifconfig

一句话概括:新建一个 Docker Network,然后把这个 bridge 和 母鸡网卡桥接,直接暴漏 Container 到外网。

Linux 使用 cat 添加多行文本

在配置 Docker Container 的时候,新建的 ubuntu container 默认没有安装 vinano 等编辑工具,这时候就需要使用 cat 命令直接向文件写入内容了。

举个栗子:你需要在新建的 ubuntu container 内更换软件源,这时就可以先备份 sources.list:

cd /etc/apt
cp sources.list sources.list.orig

然后使用 cat 写入多行文本:

cat > sources.list << EOL

键入多行文本以后输入 EOL 结束编辑。

OpenSIPS 配置 VOIP

OpenSIPS是一个“上了年纪的”VOIP 软件,在配置它的时候费了一番周折。下面我就来说一说我踩过的坑。

OpenSIPS

我的 OpenSIPS 服务运行在一台位于香港的 VPS 上,服务器系统是 Ubuntu Bionic。我本来想将 OpenSIPS 部署在 Docker 上以隔离本地的其它服务,但是失败了。一些组件在穿透 NAT 的使用需要运行 modprobe 来加载内核模块,而 Docker 不支持这一操作(貌似有骚操作可以强上)。不得已,我跑在了 Host 机。

首先安装 MySQL,然后给 OpenSIPS 配置一个账户(opensips-opensipsrw)。这里我们选择配置默认账号密码来避免后期修改麻烦,反正也只是临时用用。然后 apt 安装 OpenSIPS 和它的 MySQL 模块。

PS:这一步网上的教程都是下载源码然后手动编译,但是实测并不需要。考虑到我的 VPS 是 2 die 1G,我没有选用这种方法。

安装完成后,修改 /etc/opensips/opensipsctlrc 文件,取消前面几项的注释。这一步的目的是配置 MySQL 后端,同学们根据自己的情况配置。如果是默认账号密码的话不需要修改。

配置完成后执行 opensipsdbctl create 这一步会在 MySQL 中创建 opensips 数据库并写入相关数据,VOIP 的账户信息也会在这里。

写下来执行 osipsconfig 命令。这是 OpenSIPS 的一个生成配置文件的脚本,选择 ---> Generate OpenSIPS Script —> Residential Script —> Configure Residential Script 并选中如下几项:

[*] ENABLE_TCP
[*] USE_ALIASES
[*] USE_AUTH
[*] USE_DBACC
[*] USE_DBUSRLOC
[*] USE_DIALOG
[*] USE_NAT

保存并生成配置文件,然后在 /etc/opensips 目录下覆盖原本的 opensips.cfg 文件。随后编辑 opensips.cfg 文件,将 0.0.0.0 改成你自己的公网IP。值得注意的是,如果你修改了默认的账号密码,请在 opensips.cfg 文件中搜索默认账户,并改称你的自定义账户。

对于运行在局域网内的用户,加下来的 RTPProxy 就可以不用配置了。这里我们执行 opensipsctl add user pwd 添加 VOIP 账号(有两个才能通话喔)。执行 opensipsctl start 启动 OpenSIPS 服务。这里要注意,我本机使用 systemd 启动OpenSIPS 服务失败,如果不想修改 service 配置文件的话还是用 opensipsctl 吧。

打开 LinPhone 客户端(同样是 apt 安装)并输入 sip 账户,就可以通话了。

RTPProxy 配置

对于处在 NAT 后的用户,使用 RTPProxy 就很有必要了。值得一提的是,跨越 NAT 通信必须要求服务器具有公网地址,没有的童鞋请考虑局域网部署。安装 RTPProxy 可以直接 apt install rtpproxy

安装完成后,编辑 /etc/default/rtpproxy,在这里打开 #CONTROL_SOCK="unix:/var/run/rtpproxy/rtpproxy.sock" 的注释,然后修改 /etc/opensips/opensips.cfg 文件,找到 rtpproxy 相关配置,并修改原本地址为上述 unix sock。

然后 service rtpproxy restart。重启 OpenSIPS 服务就可以了。

关于网络环境

这一配置在我本地是可以运行的,但是考虑到国内的网络环境极其复杂,被 UDP 承载的 sip 协议可能会被运行商限速(这里批评某校校园网),所以大家可以考虑 TCP。在上面的 OpenSIPS 配置中我们已经打开了 TCP 服务,端口 5060,大家可以自行尝试。

关于 sip 通信

实测基于 MediaProxy 的通信是更稳定可靠的,RTPProxy 会有莫名其妙的卡顿。但是 MediaProxy 的配置在我本地出现了一些问题,不能保证每次都成功运行。同时MediaProxy 官方的编译版本也存在依赖问题。望有志之士自行探索相关环节。

关于蜗牛 J1900 NAS 方案

# 关于蜗牛 J1900 NAS 方案

蜗牛灵车已经在余 gay 的宿舍“稳定”运行超过半个学期了,这期间没有出现过数据丢失的情况。考虑到它的价格,这个表现还是值得满意的。

但是最近我在运行一些小服务的时候,出现了服务器性能不足的情况。通过日志,我发现用来挂 BT 的 transmission 服务占用了大量的 IO 资源。同时,频繁的上传和下载也挤占了原本就不宽裕的 CPU 性能。为此,我重新规划了 NAS 上应该运行的一些服务。

关于 BT

买 NAS 的一个重要的目的就是刷 BYRBT 的上传下载量。目前受限于硬盘大小和个人精力有限,刷分享率已经不再是主要目的。所以准备把原本的我和宇晨的独立 transmission 合并成一个,同一份种子通过不同的 key 挂两份上传量。这将是一个 docker 容器。

关于 SMB

SMB 服务目前是通过目录来区分权限的,通过限制父目录的访问来限制用户访问私有区域。接下来,将会把粒度放在文件上,通过不同用户的权限区别来限制访问。

关于权限

因为后期可能会加入的 HTTP 访问服务,这次的权限限制要比之前更好。初步考虑通过不同 linux 用户来管理权限,包括 share、private、limite 三种权限层来控制:share 提供公共空间的读写;limite 提供受限制空间读写,并做一些额度限制;private 提供额外的高速存储空间。

这三层属于用户层,禁止 SSH 登陆。管理则完全通过 root 进行。考虑到是内网,所以这样做也差强人意。值得一提的是,share 账户禁止公开,访客通过 HTTP 访问公开资源,www-data 用户组将会在 share 权限层。

关于底层

考虑到捉襟见肘的性能问题,这次我准备使用 Clearlinux 来做母鸡系统,尚未决定是否采用 Kata Container。