2020 字节跳动前端实习生面经

今年春招战绩算是惨败吧,纷纷挂在二、三面上。字节客户端实习毙掉不久,就被 hr 捞到了前端实习岗。挺困惑,一个完全没有 React 或 Vue 项目开发经验,对 JavaScript 也不熟悉的人怎么会符合前端岗…不过远程实习听起来很赞啊,还是决定硬上了。

一面

熟悉的牛客网,熟悉的 UI 味道,熟悉地被挂掉,唉。

一面面试官给人的感觉很好,先是让自我介绍,简单列了一下自己学过的 related courses,随后就进入到提问环节。看在我基础薄弱的份上,问题很是直接:

  1. 你对 HTTP 知道多少?

这个答案的粒度不太好掌控,太细又像是背题,太粗又没啥营养。虽然很久前看过《图解 HTTP》( [日] 上野宣),但是今天没有讲这些细节,而是从性能优化角度挑了感兴趣的 HTTP/3 的 QUIC,Preload / Prefetch 以及 HTTP/2 的二进制流。值得注意的是,自 SPDY 即后来的 HTTP/2 开始,TCP 连接复用已经被标准版实现。

  1. 大致讲一讲浏览器页面渲染的流程。

这个问题挺细,可以从 DNS 查询 -> 建立 TCP 连接 -> HTTP 请求与响应 -> DOM 树构建 -> CSSOM 树构建 -> 渲染 来讲。往细的话其实可以考到绘制 DOM 树时所涉及的编译原理的词法分析(Scanning)、语法分析(Parsing)。

除此之外还问了一些 JavaScript 基础。可能由于前端不涉及底层,这次没考 TCP/IP 以及操作系统的内容。最后是两道笔试题。

  1. 对浅拷贝、深拷贝的理解,以及手写一个 JavaScript 的深拷贝。

问题看着挺陌生的,但是在过去的开发经历中也遇到过这样的问题。当处理 reference type,尤其是 Object 的复制时,深拷贝是个绕不过去的坎。对于 Shallow Copy,原本与副本的内存地址指向一处;而 Deep Copy 将重新分配内存空间,类似于 C 中的 memcpy(),将内容完整的复制一份。对于 JavaScript 的深拷贝,可以通过递归遍历 Object.keys() 的方式实现。需要注意的是,ArrayFunction 都属于 reference type。

  1. 手写快排。

距离上次手写快排已经过去了整整一年,模模糊糊只记得“荷兰国旗问题”以及 pivot 的选取,至于两个下标 i, j 的移动细节则忘却了。事后想起自己之前还写过一篇关于 3-way quicksort 的博文,很是惭愧。

整体感觉很 nice,面试官会引导我的解题思路,答不上来时会安抚被试者情绪。很希望这样的人能当自己的 mentor。

面试结束之后,我提了一些比较发散的问题。

  1. 如果看待大规模使用 NPM 包管理后所不得不重视的供应链安全?

A: 一般项目依赖的版本都是固定的,部分企业内部也会有私有的 mirror 或者内部维护的依赖,因此一般情况下不用担心此问题。

  1. 如何看待 PWA App、原生 App 以及 Flutter 和 React Native 这种前端驱动的开发模式?

A: React Native 这样的开发方式不太适合工业环境,因为这要求开发人员不仅了解移动端技术,还得掌握前端技术。目前来说,绝大部分 App 的内容都是 Webview,例如 Amazon 和 Taobao,这也是个大趋势。

  1. 如何看待很多企业用腾讯 X5 Webview?

A: 国产 Android 的原装 WebView 实现层次不齐,腾讯 X5 还是有优势的(对于低端机)。

二面

字节的效率挺高的,一面结束后立刻就安排了二面。

二面给人感觉很差,面试官上来就问我为什么投递前端岗,然后直接进入笔试环节了。

  1. 不允许使用类和全局变量,写一个 getPrime() 函数,返回下一个质数。

看题面可知用闭包来处理,可是我说过没有系统性学过前端欸,为什么上来就考我闭包(逃。想到一个骚操作:在计算完当前一轮质数后,将本函数序列化,将变量的初始化语句用正则表达式替换,再反序列化回函数,不过面试不是日常开发,不能查 JavaScript API,所以没能写出来。面试结束之后经过一番讨论,得到了一个比较 tricky 的答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getPrime() {
let num = 2;
function isPrime(n) {
let res = true;
for (let i = 2; i <= Math.sqrt(n) ; i++) {
if (n % i == 0) res = false;
}
return res;
}

// 这个函数太巧妙了
return function() {
while (!isPrime(num)) {
num++;
}
return num++;
};
}

let t = getPrime();
console.log(t());
console.log(t());

注意最后这个部分,需要先通过一个变量接下 getPrime() 所返回的函数体,再执行,而不能直接 getPrime()(),否则会得到两个独立的函数实例,均返回第一个结果。

面试小插曲。

1
2
3
4
我:(有思路后开始敲测试样例)
面试官:你页面切换出去我是看得到的啊。
我:?我在写测试样例,没有切出去
面试官:哦,我就是提醒你一下。
  1. 手动实现一个 forEach(),使能原本不支持 forEach 的浏览器。

不会写。事后发现可以用 Callback 来实现,非常巧妙。首先找到 ECMAScript 2015 - Array.prototype.forEachforEach 函数将对于每一个数组中的元素执行一次传入的 callback 函数,然后返回一个三元组 $<f_j, j, f>$。

1
2
3
4
5
6
7
8
9
10
forEach = function (arr, callback) {
if (!Array.isArray(arr)) {
throw new Error("Not an array!");
}

const len = arr.length;
for (let i = 0; i < len; ++i) {
callback(arr[i], i, arr);
}
};

从二面面试官的角度看来,国内互联网企业关于实习生选择,更多的还是倾向于招“准-校招应届生”的入职即用的劳动力。

后续

跟一位在外企实习的同学交流了一番,很是羡慕:

每周聊天老大就要提醒我一次,来这里不是让我当工具人的。
我反而有些为难…我知道他是想早点让我悟出软件设计的精髓。

前后端分离的 Web App 踩坑笔记

最近在做一个整整 10 学分的团队课设。

Environment

  • 前端:React + Antd
  • 后端:PHP + MySQL
  1. 刚开始技术选型时准备采用 Flask 作为后端架构,毕竟半年前大家刚做了 Flask 的单人项目,对于其他组员来说 Python 比 PHP 更容易上手一些。但是在 Python 中缩进对代码逻辑起到影响,如果编码过程或者冲突处理时没做好,可能会产生比较棘手的麻烦。最终还是选了 PHP,代价是增加了约一个月的学习成本,不过整体代码可读性和维护性都高了不少,依赖注入也很香。
  2. Jetbrains 全家桶自带 deployment 功能,可以快捷部署当前文件至 remote 以便测试。但是全量部署更适合 Github Actions。配置好 appleboy/[email protected],就能将 Node.js 项目的 dist 目录打包后再上传至生产环境,节约了不少时间。值得注意的是,当 source path 填写为 dist/ 时,文件将被部署到远程目录的 path/dist/ 下。在 scp 中可以使用 dist/* 来解决这个问题,在此插件中可以指定 strip_components: 1 来去除前导路径元素(推荐),或者在 nginx 中修改根目录至 wwwroot/dist。不建议修改 nginx 的 wwwroot 配置,这将导致 lnmp 无法更新 Let’s Encrypt 证书(也可以在 nginx 中为 .well-known 路径单独指定根目录)。
  3. 前后端跨域是非常棘手的一个问题,我们在此花了数十个小时 debug。CORS 的出现保护了用户免遭跨站攻击,但是也为前后端对接产生了巨大的麻烦。
    • 当用到跨域 API 调用,通常做法是在后端 header 加上 Access-Control-Allow-Origin: *,但 Fetch 请求不允许这种掩耳盗铃的操作。解决办法是动态指定参数或者将前端域名写死。
    • CORS 预检请求方法是 OPTIONS,因此需要设置 Access-Control-Allow-Methods: POST, GET, OPTIONS。根据 Issue #251 · whatwg/fetch 的讨论中,当 Fetch 的 withCredentials = true 时(跨域请求携带目标域的 Cookies),此处不允许通配符,因此应当避免使用 Access-Control-Allow-Methods: *。此规则同样也适用于 Access-Control-Allow-Methods,因此应当手动声明后端允许的 request header,例如 form-data
    • 必须设定 Access-Control-Allow-Credentials: true,才能使后端接受前端跨域发送的 Cookies。
    • 我们使用 PHPSESSION 来鉴权,但 Fetch 无法响应 response header 中的 set-cookie(并非 http-only),导致浏览器无法存储用户凭据。前端同学试了 axios 和 Webpack Proxy 也没有什么头绪。对此有两个临时解决方案:
      1. sessionId 直接塞在登录接口的 data 字段里回传给前端,然后手动设置 cookie(或者存储至 SessionStorage 中)。
      2. 让前端通过 XMLHttpRequest 发送原生的 POST 的请求,浏览器自己会响应 set-cookie 的字段。
  4. 自 MySQL 5.7 开始,正式引入了 JSON 作为字段类型。这意味着我们再也不用为同质化的数据表手写烦人的 SQL 文档了,只需要将他们抽象成一种数据,然后前后端再根据 JSON 中的某个字段来路由逻辑即可。

2020 字节跳动校园招聘后端和客户端方向第二场考试 Writeup

Q1

给定等长数组 $a_n, b_n$,判断是否能给 $a_n$ 的一个连续区间的元素加上非负整数 $k$,使得 $a_n$ 变成 $b_n$。

分析:

  1. 因为 $k \ge 0$, 那么显然有 $a_i \leq b_i$。
  2. 作用范围是连续的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include "bits/stdc++.h"

int a[100000];
int b[100000];

inline int getInt(){
int t = 0;
bool neg = false;

char c = getchar();

while (c == ' ' || c == '\n')
c = getchar();

if (c == '-') {
neg = true;
c = getchar();
}

while (c != ' ')
{
if (c == '\n')
break;
// printf("get %c", c);
t = t * 10 + (c - '0');

c = getchar();
}

return neg ? -t : t;
}

int main()
{
int T;
scanf("%d", &T);
int n;
for (int i = 0; i < T; ++i) {
scanf("%d", &n);
getchar();
for (int j = 0; j < n; ++j)
a[j] = getInt();
for (int j = 0; j < n; ++j)
b[j] = getInt();
int d = 0;
bool bad = false;
bool had = false;
for (int j = 0; j < n; ++j){
if (a[j] != b[j]) {
if (a[j] - b[j] > 0) {
bad = true;
break;
}
if (had) {
if (a[j-1] == b[j-1])
{
bad = true;
break;
}
} else {
had = true;
}
if (d == 0)
d = a[j] - b[j];
else {
if (d != a[j] - b[j]) {
bad = true;
break;
}
}
}
}

if (bad)
printf("NO\n");
else
printf("YES\n");
}
return 0;
}

Q2

给定 $n$ 根木棍,你需要依次掰断他们,使他们构成一个单调不递减的序列。

分析:

  1. 木棍的相对顺序不变。
  2. 木棍只能折断为整数。
  3. 单调不递减,即 $a_i \leq a_{i+1}$

WA 了。

Q3

这题是个背包题,给定 $n$ 张可以重复使用的优惠券,$m$ 个商品,问你最少要花多少钱。

不会写。

Q4

给定 $n$ 个数字的序列,求对于每个元素,从它自己开始,统计向两侧方向的连续范围内小于等于它的元素个数。

分析:这题逻辑很清晰,暴力可以过 71% 的测试点。但是剩下的测试点无论是从输入、输出,或是循环边界上优化都过不去,显然测试数据中肯定掺入了大量的连续重复数据段。我最后选择开一个数组统计当前元素的副本数,但是交上去 WA 了不少测试点。交卷后的那一刻才突然意识到还需要对每个副本都单独输出一次结果。

原版暴力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#include "bits/stdc++.h"

inline int getInt()
{
int t = 0;
bool neg = false;

char c = getchar();

while (c == ' ' || c == '\n')
c = getchar();

while (c != ' ')
{
if (c == '\n')
break;
// printf("get %c", c);
t = t * 10 + (c - '0');

c = getchar();
}

return t;
}
int crr[6];
inline void putInt(int t)
{
// t == 0
if (t == 0)
{
putchar('0');
putchar(' ');
return;
}

// t != 0

int j = 0;
while (t != 0)
{
crr[j] = '0' + t % 10;
++j;
t /= 10;
}

--j;
while (j != -1)
{
putchar(crr[j]);
--j;
}
putchar(' ');
}

int arr[100010];
int brr[100010];
int main()
{
int T;
scanf("%d", &T);

int n;
int j;
for (int c = 0; c < T; ++c)
{
scanf("%d", &n);
for (int i = 0; i < n; ++i)
arr[i] = getInt();
memset(brr, 0, n * 4);
for (int i = 0; i < n; ++i)
{
j = i - 1;
while (j != -1)
{
if (arr[j] == arr[j + 1] || arr[j] <= arr[i])
{
++brr[i];
--j;
}
else
{
break;
}
}
j = i + 1;
while (j != n)
{
if (arr[j] == arr[j - 1] || arr[j] <= arr[i])
{
++brr[i];
++j;
}
else
{
break;
}
}
}
for (int i = 0; i < n - 1; ++i)
{
// printf("%d ", brr[i]);
putInt(brr[i]);
}
printf("%d\n", brr[n - 1]);
}
return 0;
}
}

应该是正解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include "bits/stdc++.h"

inline int getInt()
{
int t = 0;
bool neg = false;

char c = getchar();

while (c == ' ' || c == '\n')
c = getchar();

// if (c == '-')
// {
// neg = true;
// c = getchar();
// }

while (c != ' ')
{
if (c == '\n')
break;
// printf("get %c", c);
t = t * 10 + (c - '0');

c = getchar();
}

return t;
}
int crr[6];
inline void putInt(int t)
{
// t == 0
if (t == 0)
{
putchar('0');
putchar(' ');
return;
}

// t != 0

int j = 0;
while (t != 0)
{
crr[j] = '0' + t % 10;
++j;
t /= 10;
}

--j;
while (j != -1)
{
putchar(crr[j]);
--j;
}
putchar(' ');
}

int arr[100010];
int brr[100010];
int drr[100010];
int main()
{
int T;
scanf("%d", &T);

int n;
int j;
for (int c = 0; c < T; ++c)
{
scanf("%d", &n);
memset(drr, 0, sizeof(drr));
for (int i = 0; i < n; ++i)
{
arr[i] = getInt();
if (i != 0 && arr[i] == arr[i - 1])
{
++drr[i - 1];
--n;
--i;
}
else
{
drr[i] = 1;
}
}
memset(brr, 0, n * 4);
for (int i = 0; i < n; ++i)
{
j = i - 1;
while (j != -1)
{
if (arr[j] == arr[j + 1] || arr[j] <= arr[i])
{
brr[i] += drr[j];
--j;
}
else
{
break;
}
}
j = i + 1;
while (j != n)
{
if (arr[j] == arr[j - 1] || arr[j] <= arr[i])
{
brr[i] += drr[j];
++j;
}
else
{
break;
}
}
brr[i] += drr[i] - 1;
}
for (int i = 0; i < n - 1; ++i)
{
// printf("%d ", brr[i]);
for (int j = 0; j < drr[i]; ++j)
{
putInt(brr[i]);
}
}

for (int j = 0; j < drr[n - 1] - 1; ++j)
putInt(brr[n - 1]);
printf("%d\n", brr[n - 1]);
}
return 0;
}

2020 腾讯 TEG 技术运营暑期实习面经

一面

一面给人的感觉挺好,提前几天发了邮件和短信通知,准时来电。先让进行了一个并不简短的自我介绍(实习经历、学生工作、竞赛经验),然后就开始问问题。

  1. TCP 与 UDP 数据包 Header 区别:大概是 sequence number,ACK number、TCP Flags 和 Window。
  2. 一些不常见的 HTTP 状态码
  3. 常见协议,例如 ping 基于什么协议(ICMP),telnet 基于什么协议(TCP)
  4. 在 Linux 下查看端口状态:lsofnetstat 以及具体参数的意义
  5. 从实习经历中挑了 Prometheus 工作原理(基于时序数据库 RRD 和 Exporter)
  6. 开放性试题:对 100 万个 IP 地址进行排序(答案似乎是 bitmap)

除了 tcp header 与开放性试题没答完整,其他感觉还行。

二面

没有事先通知(没有接收到短信,邮件或微信提醒,腾讯招聘官网也查询不到面试时间),下午三点突然接到面试电话(归属地北京)。面试官花了一分钟时间介绍了业务情况和岗位说明,又一次让我说明实习经历和项目经历,然后挑了一些相关问题。

  1. 当消息队列因为网络问题发生重写,如何解决?(可以通过 TTL、时间戳或者根据数据的重要程度来忽略或修正)
  2. 当发生了各种意外时,如何保障数据库的 consistency 一致性。

这次面试整体上给人的感觉还不错,虽然很紧张但是面试官语速比较平缓,营造出正常的技术交流氛围。持续约 20 分钟结束。

三面

又是一次没有任何通知(短信,邮件或微信提醒,官网显示暂未约定时间)的突击面试,晚上 7 点多接到来自深圳的面试电话(当时正在上直播课),先是进行了简单提问:

  1. 有点不屑的语气问了学历(几级学院?虽然我们天天自嘲野鸡 211,但是听到别人这样问还是很气的。)和成绩(GPA 成绩?英语成绩?没听清他在问什么)
  2. 是否有考研的打算?(可能想招实习完就 return 的。现在对硕博意向的学生歧视这么严重么
  3. 是否有技术博客?(我寻思简历 header 上就印了博客地址呀)写了多少篇?(写了快六年了,看上去十几篇,其实删了几十篇…)
  4. 又双叒叕一次让我介绍实习经历(在后来的面试过程中说出了公司名,所以应该是粗略瞟了一眼,而不是完全没看)
  5. 你对 Java 脚本熟吗?(楞了一会儿才知道他指 JavaScript)。

接着就进入非常紧张的问答。

  1. 对 JQuery 使用有什么建议?(没有上下文语境突然抛出这样一个奇怪的问题,我没回答也不知道回答,另外也并非前端程序员就会接触 JQuery)
  2. Java 脚本(他其实想说 JavaScript)里面有哪些基本数据类型?(number/string/boolean/underfined/null 和 Object,注意区分 reference types 和 primitive types。)
  3. Stack 和 Heap 的区别(可以从内存分布以及内存分配的角度答)
  4. JavaScript 的 this (this 指代的对象取决于具体的 context)
  5. JavaScript 的 document 与 window 对象(document 是网页内容,window 则包含浏览器窗口信息,例如状态栏)
  6. JavaScript 中 nullundefined 的区别(none 是对象不存在,即是没有对象,underfined 是值未定义,例如在赋值前调用一个发生了变量提升的数据)
  7. Linux 下如何查看进程信息?(ps, top, htop
  8. th 是啥?(口音很重,没听清,直接过了)
  9. ppph 是啥?(口音很重,没听清,直接过了)
  10. Linux 下查看内存占用?(topfree
  11. /home/etc 目录的使用场景。
  12. “句柄泄露”是什么(菜是原罪,我以后用 Fedora)
  13. 磁盘 IO 状态?(答案应该是 iostat,《鸟哥私房菜》得精读哇)
  14. 你了解云技术么(我说自己搭建过 ESXi,平常使用 ECS(大意了,ECS 是阿里云的产品名,CVM 才是腾讯云的虚机),部署过 OpenCDN 算么,也许不算,我也不知道他想问什么)

有意思的是,我明明面试的技术运维方向,却问起了前端内容。回过头来看,这次面试也暴露出了蛮多问题:项目经验与算法题只是面试的敲门砖,之后还有可能死在非常基础的技术细节上。想起之前 V2EX 上有人评价 @Livid 不懂 CSS 选择器的 descendants combinator 与交集的区别:

感觉中国很多公司的招聘流程有很多可以优化的地方
很多人会因为不知道这些只需要 just in time 的知识而被刷掉

总的来说,作为被试者,总是被动的接到通知,或者没有通知,感受不到相互的尊重。三面面试官较重的中英文口音,以及不给面试者思考时间的风格带来了极差的面试体验。

区分 Cookie, Web Storage 和 IndexedDB

由于 HTTP 是一个无状态(Stateless)协议,每一个请求相互独立,因此需要通过一些客户端存储(Client-side Storage)来保存用户的使用偏好或者登录状态等信息。当下(2019年)客户端的存储技术大概有 Cookie、Web Storage(包含 sessionStorage 和 localStorage)以及 IndexedDB 等四种。目前使用 Cookie 的实现比较常见。

  • 是服务器发送到用户浏览器并保存在本地的一小块数据
  • 每一次 HTTP 请求都会携带全部的 Cookies(会带来性能开销)

类别

  • 会话期 Cookie:浏览器关闭后会被自动清除
  • 持久性 Cookie:可以指定一个过期时间 Expires 或者有效期 Max-Age

标记

  • Secure:标记此 Cookie 只可以被 HTTPS 方式进行传输
  • HttpOnly:禁止被浏览器 JavaScript 访问,防止 XSS 跨站脚本攻击
  • SameSite:标记此 Cookie 在跨站时不被发送,实验性功能

作用域

  • 域名:包含其子域名
  • URI 路径(例如属于 https://example.com/path1/ 目录的 Cookie 将不被发送目的地址为 https://example.com/path2/ 的请求所携带)

用途

  • 行为跟踪(广告联盟)
  • 个性化设置(网站显示语言、主题配色等)
  • 会话状态(游戏分数,登录状态等一些并非很敏感的信息)

限制

  • 每个条目最大 4KB(包含其 key、value 以及 expires 等字段)

Web Storage

sessionStorage

  • 为给定的源提供一个独立的 key-value 存储区域,比 Cookies 更加直观
  • 无法跨域,子域名无法继承
  • 不需要在每次请求时携带(不会被主动发送给服务器)
  • 有效期为浏览器的生命周期,当浏览器关闭后被清除
  • 限制:每个条目大小 5MB

localStorage

  • 可以理解为持久性的 sessionStorage,在浏览器关闭后数据仍然存在

IndexedDB

  • 是一个客户端非关系型数据库
  • 也是 key-value 键值对存储
  • 一般用于在客户端存储持久性结构化数据(二进制对象)
  • 所有操作通过事务(transaction)完成
  • 无限容量

Edgerouter Lite 3 镜像烧录

两百来块收来的洋垃圾 Ubnt Edgerouter Lite 3 使用一根 USB 2.0 U 盘作为启动盘,终于在无数次 I/O Error 后宕机了。一番搜索之后发现了这个:EdgeMax Rescue Kit (EMRK) 救援工具包。不过这种方式比较繁琐,需要通过 Console 线连接路由,然后使用 TFTP 传输镜像进行刷机。参考此评论此主题后得到两个解决方案。

总的来说,ERLite-3 洋垃圾相比 ER-X 有着非常高的性价比(见下表,数据来源),内置 DPI 深度包审计便于流控,且具有更高的大包吞吐性能。

Packets ERLite-3 ER-X
1518 bytes 3 Gbps / 240,000 pps 1 Gbps / 80,000 pps
64 bytes 490 Mbps / 730,000 pps 957 Mbps / 1,400,000 pps

Plan A:需要 Linux 执行环境

  1. ERLite-3 | Ubiquiti - Downloads 下载最新的固件。
  2. sowbug/mkeosimg | Github 下载 EdgeRouter 启动盘制作工具。
  3. 在 Linux 执行环境下运行 sudo ./mkeosimg ER-e100.v1.xxxxxxx.tar 将下载的固件包转换至 .img 格式的磁盘镜像。
  4. 使用 ImageUSB 等工具将镜像烧录至 U 盘。
  5. 替换损坏的 U 盘然后开机,即可获得一台全新的 Edgerouter Lite 3。

Plan B:需要准备好用于 DD 的磁盘镜像

  1. ERL_1.4.0.img | Google Drive 下载由 landers7222 制作好的磁盘镜像,或者自己提前制作好之前的 U 盘镜像。
  2. 使用 dd 命令或 ImageUSB 等工具将镜像烧录至 U 盘。
  3. 替换损坏的 U 盘然后开机,即可获得一台全新的 Edgerouter Lite 3。

Windows 使用 802.11k、802.11v、802.11r 进行快速漫游

现在,改进的 WLAN 漫游体验已经可用于运行 Windows 10 的设备,一些可以减少移动终端在 AP 间漫游的时间开销的行业标准实现也得到了应用。

802.1k 邻区报告

支持 802.11k 协议的无线接入点(AP)可以为运行 Windows 10 的设备提供邻区报告。邻区报告涵盖了邻区接入点信息,能够让客户端对其周围无线电环境有更好的认识。 Windows 10 利用此功能来缩短移动终端在漫游前扫描周边待漫游的目的接入点列表的时间。

802.11v BSS 漫游管理帧

支持 802.11v 协议的接入点现在可以引导 Windows 10 设备漫游至被认为能提供更好的 WLAN 链路质量的接入点。Windows 10 设备现在可以接受并响应 BSS 漫游管理帧,可以在支持 802.11v 的 WLAN 网络下改进链路质量。

802.11r 快速 BSS 漫游

快速 BSS 漫游减少了 Windows 10 设备在支持 802.11r 的网络下所需要的漫游时间。这里的时间减少得益于漫游时与先前的 AP 只需交换更少的数据帧。通过减少终端在接入点间漫游过程后进行数据传输前的等待,一些延迟敏感型的业务质量得到了提升,例如 Skype 通话。Windows 10 的快速 BSS 漫游当前仅支持位于 802.1X 身份认证的 WLAN 网络,而不支持预共享密钥(PSK)和开放型(Open)无线网络。

得益于 802.11k、802.11v 以及 802.11r 技术的结合,Windows 10 利用已有的行业标准来改善用户的 WLAN 漫游体验。VoIP 应用程序现在可以为移动中的用户提供更好的通话质量。

注意事项

并非所有的 Windows 10 设备都支持 802.11k、802.11v 以及 802.11r。系统的硬件驱动必须支持这些特性能正常工作于 Windows 10。请与你的设备制造商联系以确认你的设备是否支持这些 WLAN 漫游特性。除了客户端一侧的支持,所处的 WLAN 网络(包括 AC 和 AP)必须也支持这些 WLAN 漫游特性。请与你的网络管理员一起确认这些功能是否被无线网络设备支持并已经被启用。

当 802.11r 在当前设备或 WLAN 上不被支持时,Windows 10 仍然提供粘滞密钥缓存(OKC)。

这三项特性都依赖于接入点侧的支持,否则将不可用。

本文翻译自 Fast Roaming with 802.11k, 802.11v, and 802.11r - Windows Drivers | Microsoft Docs

实习月报(下)

在大数字的实习工作逐步进入尾声了,写写最近一个月的心得感悟。

工作流

团队协作是软件工程中最重要的部分,人们为提高协作效率开发了各种协同工具以及工作流(Workflow)。工作流是一系列工作过程的标准化描述,核心是人;协同工具则是对工作流的辅助,在特定的工作流下提高协作效率,作用于事物。实际过程中,很多开发者局限于对协作工具的使用,而忽视了编码流程的规范,反而导致协作工具成为了开发过程中的阻力,使用效果适得其反。例如,不及时更新 Issue 状态,或者将正在开发的 Axure RP 原型设计稿导出为文件而不是提供在线访问。

团队沟通

有些公司没有协同工作的概念,使用微信、QQ 等公共即时聊天工具作为沟通方式。这不仅为工作环境引入了外部的干扰(非工作内容),也带来了安全隐患(信息泄露、不可控因素)。电子邮件也曾是主流的内部沟通方式,因为其低时效性的特点,比较适合异步通讯(例如缺陷跟踪、事务通知),而不再适合节奏较快的工作环境。现在,企业微信、钉钉、Slack 等平台被广泛使用,解决了传统 IM 的一些缺陷,也引入了电子邮件的群发通知功能。尤其是其可作为 SaaS 以订阅方式提供,也可以进行私有化部署,比较灵活。

对于进行同一项工作且存在异地分支的团队,定期的电话会议是很有必要的。成员之间可以互相提供开发建议,统一工作进度和节奏。

代码质量

代码审查(Code Review)是敏捷开发中很重要的一个部分,每位开发者所提交的代码都将被至少一位开发者进行审计,有效改善了代码质量以及潜在的安全隐患。除此之外,新手程序员能通过 Code Review 的讨论过程学习到一些开发技巧和项目结构,能够更快的融入团队。

在我看来,开发需求较紧张时,开发人员可能会放弃单元测试以及模糊测试,而试图将测试任务甩给 QA 团队甚至永久地搁置。尤其是当项目管理不接触具体开发工作,无法对开发周期做出准确估计时,为开发者下发“加班加点加新功能”的任务会造成非常严重的后果(较差的代码质量)。

知识库

为项目团队维护一份知识库还是挺重要的,不仅便于开发人员理解项目的系统架构,还有助于提高接口测试与沟通效率。

对于编写 API 开发文档,可以使用 SwaggerApiDocJS 或是由阿里妈妈 MUX 团队出品的 RAP2,嫌麻烦也可以选择 Markdown 或者 Github Wiki(如果项目 Repo 位于 Github)。Swagger 和 RAP2 是非内联形式的独立文档系统,文档数据独立于项目代码,不限制 API 语言类型。ApiDocJS 则是一个内联文档系统(inline documentation),提供从代码注释自动生成静态文档的能力,受编程语言类型的限制。由于文档编写于代码注释,会对项目代码有影响。其所生成的静态页面可以单独部署至 Web 服务器或者进行本地访问。

知识库可以视项目大小选择 Github Wiki 或者 DokuWiki。Github Wiki 可以直接使用 Markdown 进行编写并通过 Git 管理,使用起来较方便,适合项目内小范围使用,但有局限性:只能为有此 Github repo 访问权限的用户提供访问。而 DokuWiki 作为一个轻型的 Wiki 系统,在权限管理上更加自由,允许匿名编辑和访问,也可以通过插件来对接 LDAP 进行集中认证,比较适合公司内部使用。诸如 Confluence 这样的文档协作解决方案也很不错,但是相对的,成本也很高。当然,相比于研发人员的工资,这些生产力工具还是值得企业去选购的。

预定义规范

“所见即所得”的开发模式只适合于 WordPress 这样抽象后的 CMS 表层设计。对于一个从零开发的具体产品来说,还是得有一套预定义的设计规范。包括但不限于:色彩搭配、字体样式、界面布局以及中英文混合排版规范。除此之外,编码规范也很重要,例如接口命名风格、RESTful API 样式以及代码缩进定义。在规范下协作,能很大程度上降低开发人员间的沟通成本,提高团队协作效率。

Confluence 在线协同文档系统搭建及激活教程

Introduction

Confluence 是由澳大利亚软件公司 Atlassian 出品的一款在线文档协作平台,提供跨平台的移动客户端,可以方便地与 Jira、Google Drive 等平台进行对接,形成一个完整的敏捷开发工作流,被广泛应用于公司内部知识库的构建。作为商用软件,其授权价格不菲,但以 10 美元的价格提供 10 用户的 Starter Package,支持本地部署或云服务,且提供持续一年的维护,对于小团队来说是个很不错的选择。

同类产品有 DokuWiki。作为一款开源免费的文档解决方案,相比于 Confluence 来说界面比较简陋,扩展性也没有那么强,适合仅做内部文档用途的场景。

Installation

Environment

Confluence 版本:6.15.8

1
2
3
4
5
6
7
8
9
10
11
CPU Model      : Intel(R) Xeon(R) E-2176M  CPU @ 2.70GHz
CPU Cores : 4 Cores @ 2712.009 MHz x86_64
CPU Cache : 12288 KB
OS : Ubuntu 18.04.1 LTS (64 Bit) Hyper-V
Kernel : 4.15.0-45-generic
Total Space : 8.8 GB / 125.2 GB
Total RAM : 2740 MB / 4905 MB (1289 MB Buff)
Total SWAP : 26 MB / 2047 MB
Uptime : 0 days 1 hour 49 min
Load Average : 0.00, 0.00, 0.07
TCP CC : cubic

Step 0 Preparation

作为开发者,请支持正版软件!

Step 1 Run Installer

1
2
3
4
5
6
7
8
9
10
# 为二进制包添加可执行权限
[[email protected] ~]# chmod +x atlassian-confluence-6.15.8-x64.bin

# 执行安装
[[email protected] ~]# ./atlassian-confluence-6.15.8-x64.bin
Unpacking JRE ...
Starting Installer ...

This will install Confluence 6.15.8 on your computer.
OK [o, Enter], Cancel [c]

输入字母 o 然后回车,继续安装。

1
2
3
4
5
6
...
Choose the appropriate installation or upgrade option.
Please choose one of the following:
Express Install (uses default settings) [1],
Custom Install (recommended for advanced users) [2, Enter],
Upgrade an existing Confluence installation [3]

输入 1 并回车进行快速安装,或者输入 2 进行自定义安装以手动指定程序安装路径及数据存放目录。

1
2
3
4
5
6
7
...
Configure which ports Confluence will use.
Confluence requires two TCP ports that are not being used by any other
applications on this machine. The HTTP port is where you will access
Confluence through your browser. The Control port is used to Startup and
Shutdown Confluence.
Use default ports (HTTP: 8090, Control: 8000) - Recommended [1, Enter], Set custom value for HTTP and Control ports [2]

默认 HTTP 端口为 8090,Tomcat 管理端口为 8000。关于 Atlassian 产品的端口使用说明可以参考 Ports used by Atlassian

1
2
3
4
5
6
...
Confluence can be run in the background.
You may choose to run Confluence as a service, which means it will start
automatically whenever the computer restarts.
Install Confluence as Service?
Yes [y, Enter], No [n]

回车,继续安装。

1
2
3
4
5
6
7
...
Extracting files ...
Please wait a few moments while we configure Confluence.

Installation of Confluence 6.15.8 is complete
Start Confluence now?
Yes [y, Enter], No [n]

输入 n,暂时不运行 Confluence 服务。

1
2
Installation of Confluence 6.15.8 is complete
Finishing installation ...

当看到上述文字时,Confluence 服务已经安装完成了,接下来需要进行进一步配置与产品激活。

Step 2 Patch

将下载得到的 atlassian-agent.jar 补丁文件上传至服务器的一个固定位置,演示时我将其放在了 /opt/atlassian 目录下。

1
scp atlassian-agent.jar [USER]@[SERVER_HOSTNAME]:/opt/atlassian/

然后修改 Confluence 的启动环境变量文件(位于 Confluence 安装目录下的 bin/setenv.sh),添加一行 CATALINA_OPTS="-javaagent:/opt/atlassian/atlassian-agent.jar ${CATALINA_OPTS}"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
# http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
CATALINA_OPTS="-javaagent:/opt/atlassian/atlassian-agent.jar ${CATALINA_OPTS}" # 这是新增的

CATALINA_OPTS="-XX:-PrintGCDetails -XX:+PrintGCDateStamps -XX:-PrintTenuringDistribution ${CATALINA_OPTS}"
CATALINA_OPTS="-Xloggc:$LOGBASEABS/logs/gc-`date +%F_%H-%M-%S`.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=2M ${CATALINA_OPTS}"
CATALINA_OPTS="-XX:G1ReservePercent=20 ${CATALINA_OPTS}"
CATALINA_OPTS="-Djava.awt.headless=true ${CATALINA_OPTS}"
CATALINA_OPTS="-Datlassian.plugins.enable.wait=300 ${CATALINA_OPTS}"
CATALINA_OPTS="-Xms1024m -Xmx1024m -XX:+UseG1GC ${CATALINA_OPTS}"
CATALINA_OPTS="-Dsynchrony.enable.xhr.fallback=true ${CATALINA_OPTS}"
CATALINA_OPTS="-Dorg.apache.tomcat.websocket.DEFAULT_BUFFER_SIZE=32768 ${CATALINA_OPTS}"
CATALINA_OPTS="${START_CONFLUENCE_JAVA_OPTS} ${CATALINA_OPTS}"
CATALINA_OPTS="-Dconfluence.context.path=${CONFLUENCE_CONTEXT_PATH} ${CATALINA_OPTS}"
CATALINA_OPTS="-XX:ReservedCodeCacheSize=256m -XX:+UseCodeCacheFlushing ${CATALINA_OPTS}"

export CATALINA_OPTS

随后将 MySQL JDBC 驱动上传至 Confluence 安装目录的 confluence/WEB-INF/lib/ 路径下。

Step 3 Check

接下来,我们运行 Confluence 服务,并查看补丁是否安装成功。

1
2
3
4
# 启动 Confluence 服务
service confluence start
# 查看 Confluence 日志
tail -f /opt/atlassian/confluence/logs/catalina.out

当在日志中发现 agent working 字段时,即意味着补丁已经正确安装并运行。

1
2
3
29-Aug-2019 13:52:37.568 INFO [Catalina-utility-2] org.springframework.context.support.DefaultLifecycleProcessor.start Starting beans in phase 2147483647
29-Aug-2019 13:52:37.583 INFO [Catalina-utility-2] org.springframework.web.servlet.DispatcherServlet.initServletBean FrameworkServlet 'dispatcher': initialization completed in 860 ms
============================== agent working ==============================

Step 4 Configuration

接下来我们将对 Confluence 进行初始化配置。访问 http://[SERVER_IP]:8090/ 进入安装向导,如果页面语言为英文,可以通过页面右上角下拉框进行切换。当进行至输入 License 步骤时,将页面上方格式为 XXXX-XXXX-XXXX-XXXX 的服务器 ID 复制,随后手动执行补丁以获取序列号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 查看帮助信息
$ java -jar atlassian-agent.jar

====================================================
======= Atlassian Crack Agent =======
======= https://zhile.io =======
======= QQ Group: 30347511 =======
====================================================

KeyGen usage: java -jar /opt/atlassian/atlassian-agent.jar [-d] [-h] -m <arg> [-n <arg>] -o <arg> -p <arg> -s <arg>

-d,--datacenter Data center license[default: false]
-h,--help Print help message
-m,--mail <arg> License email
-n,--name <arg> License name[default: <license email>]
-o,--organisation <arg> License organisation
-p,--product <arg> License product, support:
[crowd: Crowd]
[questions: Questions plugin for Confluence]
[crucible: Crucible]
[capture: Capture plugin for JIRA]
[conf: Confluence]
[training: Training plugin for JIRA]
[*: Third party plugin key, looks like:
com.foo.bar]
[bitbucket: Bitbucket]
[tc: Team Calendars plugin for Confluence]
[bamboo: Bamboo]
[fisheye: FishEye]
[portfolio: Portfolio plugin for JIRA]
[jc: JIRA Core]
[jsd: JIRA Service Desk]
[jira: JIRA Software(common jira)]
-s,--serverid <arg> License server ID

================================================================================

# Crack agent usage: append -javaagent arg to system environment: JAVA_OPTS.
# Example(execute this command or append it to setenv.sh/setenv.bat file):

export JAVA_OPTS="-javaagent:/opt/atlassian/atlassian-agent.jar ${JAVA_OPTS}"

# Then start your confluence/jira server.
1
2
# 生成 Confluence License
java -jar /home/confluence/atlassian/atlassian-agent.jar -p conf -m [EMAIL] -n [NAME] -o [ORGANIZATION] -s [SERVER_ID]

将补丁生成的 License 拷贝,粘贴至页面,即可进行后续的数据库配置步骤。

FAQ

  1. The database collation utf8_general_ci is not supported by Confluence. You need to use utf8_bin.

    Confluence 必须使用 utf8_bin 字符集的 MySQL 数据库,而 MySQL 默认数据库字符集为 utf8_general_ci,可以通过以下命令解决。

    1
    2
    DROP DATABASE [DB_NAME] # 删除之前错误创建的数据库
    CREATE DATABASE [DB_NAME] CHARACTER SET utf8 COLLATE utf8_bin # 创建字符集为 utf8_bin
  2. 新建页面模板的中文字符串显示乱码。

    这是因为 MySQL 默认的字符集为 Latin1,需要将其修改为 utf8。修改 MySQL 配置文件如下,并重启 MySQL 服务即可解决。

    1
    2
    [mysqld]
    character-set-server = utf8 # 指定默认 Server 级字符集为 utf8
  3. Your database must use READ-COMMITTED as the default isolation level.

    Confluence 要求使用 READ-COMMITTED 作为 MySQL 默认的事务隔离级别,修改 MySQL 配置文件如下并重启 MySQL 服务即可。

    1
    2
    [mysqld]
    transaction_isolation = READ-COMMITTED

  1. 1.Confluence 安装及破解 - qinjj 的博客 https://www.qinjj.tech/2019/01/04/confluence%20install/
  2. 2.Confluence 中文乱码解决思路和方法 http://moheqionglin.com/site/blogs/26/detail.html

实习月报(上)

大暑已至,实习的日子也过去了一半。在短短的四周中,接触了不少新技术,也总结出了许多经验。

  1. 思想理论对于编码实践有着极其重要的指导作用。

    月初逛论坛时看到一条回复:

    标题:前端的技术更新换代速度是不是有点快? - V2EX
    @xuanbg:MVC 模式还是 76 年提出来的呢,前端的同学用上才几年。。。

    大概十多年前,互联网还没有前端开发者这个概念。由于当时网站基本使用表格布局或 Flash,而不是如今的 DIV + CSS 的布局,前端开发工作基本由后端完成。许多站点通过将前端代码硬编码于后端代码,配合 iframe 和后端语言的 include 语句,构造标准的表格式单页。

    Web 2.0 技术之后,UGC 内容逐渐成为主流,前端技术也不再局限于简单的表格布局,各种技术栈百花齐放,诞生了 jQuery、Dojo 等一系列开源 JavaScript 框架。CSS3 与 HTML5 标准的出现也极大地推动了前端技术栈的发展,人们开发出各种炫酷的交互动画,甚至可以通过 WebGL 实时渲染 3D 模型。Bootstrap 等前端 UI Kit 的出现则将前段开发者们从手写代码中拯救了出来,模块化的响应式布局成为主流,Web 前端页面的基本构建逐渐变得简易上手,由此也催生了一大批前端培训班。

    在我看来,一个优秀的前端工程师除了掌握各种 Web 技术与开发框架之外,还需要具有一定的审美能力与设计理念。对于 to B 的产品可能不需要考虑太多用户体验上的需求,但是如果是面向普通用户,用户体验很大程度上影响了用户留存与活跃时间。

  2. 慢下来,提高代码质量。

    没经验没能力的软件开发团队一开始就冲刺/加班加点地加新功能,几个月后就慢下来了,所有时间都在修 bug,没精力去加新功能。
    —— 软件开发过程中,先慢下来,才能将来跑得更快

    前期开发过程中缺少 Code Review 及自测等评估环节,加上一些不合理的产品设计,我们耗费了大量的时间在变更需求和寻找代码缺陷上。和一位曾在某一线手机厂商工作的朋友聊起工作流,其公司生产环境代码小版本一月一更,大版本甚至一年才发布一次。当然,处于开发期的项目迭代本就应该比维护期的项目快许多,但是如果不使用 CI/CD 以及自动化测试等解决方案,后期推进将变得非常困难。

  3. 预留数据审查中间件,做好安全措施。

    如果发现一个产品在安全(包括但不限于权限管理)方便不讲究了,说明他们内部在为 KPI 赶进度了。因为安全领域是花费最大却看不到成果的地方。
    —— [email protected]老毒师

    2018 年是区块链的风口,但不知有多少交易所和私链因为代码缺陷所致的安全问题而崩盘,无论是运营者还是用户都损失惨重。但就目前看来,各个项目(除了互联网金融方向)的开发过程中仍然是业务线优先,安全、风控最后。在我们的项目初期,前端未考虑对用户做防呆设计以及数据预过滤,后端也基本未对接口数据做鉴权或合法性校验。作为一个暴露在公网的在线服务,是非常危险的。

  4. 产品需求一定要明确,做好任务进度可视化。

    在前段时间的课设中,我们尝试应用了 Github Kanban 作为敏捷开发管理工具,对项目进度进行把控。作为一个辅助工具,它并不具有什么魔幻能力,但却能直观地掌控项目的功能粒度上的开发进度,优化需求下发流程。使用 IM(如微信、蓝信等即时聊天工具)及电子邮件作为需求下发方式的形式是一个没有缓冲区的 [生产者-消费者] 问题。通过引入看板作为缓冲区,开发者完成手头任务后即可查看后续需求,产品经理也无须为此在群里 at 全体成员,打断所有人正在处理的事情。这不仅可以避免开发人员工作时间不饱和,还有助于提升开发效率。