为什么HTTP Upgrade的时候,需要Connection: upgrade

很久之前,在看HTTP头部的时候,发现WebSocket等协议的Upgrade请求,需要同时带上Connection和Upgrade头部。但是,如果是仅仅Upgrade的话,Connection头部不就是多余的设计了么?

比如一个典型的WebSocket升级请求如下:

1
2
3
4
5
6
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

当时在知乎提了一个问题,结果到现在没有满意了回答,只能自己来回答了。

Connection的起源

最开始,在HTTP/1.0出现没多久,人们就意识到HTTP持久连接的重要性(毕竟三次握手还是很慢的),所以各个服务器实现都采用了Keep-Alive头部来表示这个请求支持连接持久化。

HTTP/1.1中的Connection

在HTTP/1.1中,正式标准化了Connection头部:

Connection头部一般表示那些头部是属于逐跳头部的,比如Connection: Custom-Header,就表示在这个连接中,Custom-Header是一个逐跳头部,不应当被代理原样传递给upstream。

有两个例外:close表示会话不持久化,keep-alive表示会话支持持久化(虽然有一个Keep-Alive头部,但是大小写不一样)。

阅读更多

JVM如何获取当前容器的资源限制

本文是《容器中的Java》系列文章之 1/n ,欢迎关注后续连载 :) 。

最近同事说到Java的 ParallelGCThreads 参数,我翻了下jdk8的代码,发现 ParallelGCThreads 的参数默认值如下:

  • 如果cpu核心数目少于等于8,则GC线程数量和CPU数一致
  • 如果cpu核心数大于8,则前8个核,每个核心对应一个GC线;其他核,每8个核对应5个GC线程

但是被提醒,发现即使在分配4核的容器上,GC线程数也为38。然后就想到应该和容器的资源限制有关——jvm可能无法觉察到当前容器的资源限制。

翻了下代码,发现最新版本的java是能感知容器的资源限制的,就按照jdk版本再翻了下代码:

线上的jdk(jdk8u144)

写一个sleep 1000s的程序,用于查看JVM的线程数量:

1
./jdk1.8.0_144/bin/java -XX:+UseG1GC -XX:+ParallelRefProcEnabled Main 

然后查看GC线程数目:

1
2
$ jstack $pid | grep 'Parallel GC Threads' | wc -l
38
阅读更多

如何找到一个可用端口?

很多场景中,我们确实需要找一个空闲端口,比如启动一个子进程监听指定端口,然后通过这个端口与之通信

然后实现方式就有很多了:

VSCode的实现

比如VSCode,就是逐个连接,如果某个端口连接失败,并且错误不是ECONNREFUSED的话,那么就说明这个端口可用。

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
function doFindFreePort(startPort: number, giveUpAfter: number, clb: (port: number) => void): void {
if (giveUpAfter === 0) {
return clb(0);
}

const client = new net.Socket();

// If we can connect to the port it means the port is already taken so we continue searching
client.once('connect', () => {
dispose(client);

return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
});

client.once('data', () => {
// this listener is required since node.js 8.x
});

client.once('error', (err: Error & { code?: string }) => {
dispose(client);

// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
if (err.code !== 'ECONNREFUSED') {
return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
}

// Otherwise it means the port is free to use!
return clb(startPort);
});

client.connect(startPort, '127.0.0.1');
}

这种方式完全混淆了“端口可以被监听”和“端口不能连接”这两个语义,虽然现在Linux上述代码能够正常工作,但是保不准哪天新添加一个类似ECONNREFUSED这样的错误码呢。

vscode-mono-debug的实现

然后继续找,发现vscode-mono-debug的实现:通过不指定端口的方式listen,让操作系统分配端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int FindFreePort(int fallback)
{
TcpListener l = null;
try {
l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
return ((IPEndPoint)l.LocalEndpoint).Port;
}
catch (Exception) {
// ignore
}
finally {
l.Stop();
}
return fallback;
}

这种方式好了一点,但是比人调用这个函数是为了拿到端口去监听,而这个函数并不会保证这个端口不被别人占用,所以这个实现还是有一点问题。

阅读更多

StringBuffer,StringBuilder以及String

今天在网上闲逛,看见 @姚冬 的一个回答

他提到的问题也很有深度,然后思考了下,想评论来着。然而评论区太小,写不下,所以单独写在这儿。

基本上可以当作快问快答来读…

为什么java中的string不以\0结尾?

  • \0结尾在很大程度上要求程序员写规范的代码,如果写出了不规范的代码,那么很容易就内存越界了。
  • 另外,string的内部存储是char[],而为了内存安全,java数组本来就有一个length属性,这时以\0结尾就是一个多余的设计了。
  • String的内部存储也只能是char[]了,如果是其他的方式,比如通过native内部放一个c风格的数组,那么java代码中的char[]和string的转换就要很多内存拷贝操作了。
  • 而C语言设计成\0结尾,是为了减少抽象层,让C语言更加贴近硬件

(在语言设计中,)字符串的长度放哪里,放到起始指针的位置,还是起始指针的前面 ?

  • Java中,String的length也就是数组的length,JLS也只是说明了arraylength字节码,没有规定如何实现
  • 不过Hot Spot的实现是,先元数据,再长度,再具体的内容(比如char[])

如果放前面,那么字符串起始指针和内存块起始不一致怎么解决

Java不存在这个问题,我觉得。元数据和length字段都在实际数组之前呢。Java中,访问任何对象之前都要再多一次跳转,跳过元数据(和length)。

字符串拼接的时候把源串复制到目标串结尾,那么目标串剩余内存不够怎么办,重新分配要多一次赋值,频繁拼接性能有问题怎么办

阅读更多

我们为什么使用Linux内核的TCP栈

本文是 Why we use the Linux kernel’s TCP stack 的翻译。

最近,有一篇文章提出了一个非常有趣的问题,我们为什么使用Linux内核的TCP栈? 这在Hacker News上引发了非常有趣的讨论。

在CloudFlare工作的时候,我也一直在想这个问题。我的经验主要来自于和数千台生产机器打交道,我也会从这个角度来尝试回答这个问题。

CC BY 2.0 图片 来自 John Vetterli

让我们从一个更加宽泛的问题开始——跑起来一个操作系统是为了啥?如果你仅仅打算运行一个应用程序,那么运行数百万行代码的内核听起来绝对是一个负担。

但,我们通常都决定跑一个操作系统,有两个原因。第一,操作系统层提供了硬件独立性,以及很容易使用的API。这样,我们就可以为任何机器写代码了——不仅仅是当前运行代码的这种机器。第二,操作系统提供了时分复用层。这让我们能同时运行多个程序。不管它是另一个http服务,还是仅仅是一个bash会话,这种不同进程共享资源的能力是非常重要的。所有由内核暴露出来的资源都是能够被多个进程共享的!

用户态网络

对于网络栈,这也没什么不同。运行通用操作系统的网络栈,我们能够运行多个网络程序。如果为了运行用户态网络栈,而让单个应用程序独享网卡硬件,那就会丢失这种能力。将一个网卡分配给另一个程序,那么很可能就没法同时与服务器进行ssh会话了。

这听起来很疯狂,但这正是很多用户态网络栈所建议的。通用术语叫“全内核旁路”(full kernel bypass)。即绕过内核,用户态进程直接使用网络硬件 。

阅读更多

macOS下使用ZMODEM协议上传/下载文件

有时候,我们ssh登录服务器操作(甚至经过跳板机),然后这个时候,我们想下载、上传一个文件,就必须重启启动一个终端,运行scp命令。这个非常的繁琐,而且要上传、下载的目录也需要自己复制粘贴,有没有办法能够在ssh会话中上传、下载文件呢?

查了下,还真有这么一个协议,叫ZMODEM

原理

下载文件

在服务器上执行sz(Send by ZMODEM),先在终端上输出**B00000000000000,然后客户端在终端发送指令,表示拒绝,还是接收(接收的话,就在客户端运行rz指令与服务端交互)

上传文件

在服务器上执行rz(Receive by ZMODEM),先在终端上输出rz waiting to receive.**B0100000023be50,然后客户端发送指令,表示取消,还是上传(上传的话,在客户端运行sz命令与服务端交互)。

可以看到在上述流程中,对Terminal的要求就是,遇到特殊指令,触发对应的操作(执行本地命令)。

遗憾的是,我一直使用的、macOS自带的Terminal.app不支持这个,所以我只能放弃Terminal.app,使用iTerm2(v3.3.0beta5)了。

如何配置

阅读更多

vscjava.vscode-java-debug 0.18.0的新特性!

微软为VSCode开发了一个Java调试器 Debugger for Java。之前用这个很不爽,还和微软的人吐槽过VSCode在debug java的时候,只能看到HashMap等java自带数据结构的物理视图,比如一个HashMap,在 0.17.0 版本下debug时,是这样的:

HashMap里面有很多实现的细节,但是一般在debug的时候,我们比较关注的是这个HashMap里面存储了哪些东西等,而不是这种具体实现的细节。

然后0.18.0就实现了HashMap的逻辑视图,就是只查看数据,而不查看实现的视图:

在调试的时候,可以很方便的查看容器数据类型内的数据。

然后再仔细看了下Debugger for Java 的Changelog,发现还有一个比较有用的更新:

Add the source hyperlinks for the stack traces in the Debug Console output.

比如异常打印的StackTrace,在0.17.0是这样的:

阅读更多

Python 2/3下如何处理cjk编码的zip文件

今天项目中遇到了中文编码的zip文件,处理了蛮长时间,所以记录下,以免下次踩坑。

Python2下

Python2中读取zip文件,zipfile.ZipInfo的filename类型是str,基本上类似于python3中的bytes,即可以被decode为unicode。

所以,要处理中文,只需要将文件名按照编码decode成unicode就好。

1
2
3
4
5
6
7
8
import zipfile

fpath = '/path/to/zip.zip'
zfile = zipfile.ZipFile(fpath, 'r')
for fileinfo in zfile.filelist:
print fileinfo.filename.decode('gb18030')
# 如果要更加详细的区分bytes/str/unicode的语义
print bytes(fileinfo.filename).decode('gb18030')

Python3下

Python3中,Language encoding flag (EFS)如果是1,则按照utf8来处理文件编码,EFS如果为0,则直接按照cp437解码文件名。这是标准直接规定的。

但是,很多软件在制作zip压缩包的时候,直接使用gb18030或者其他非标准编码格式来编码文件名,所以我们还得将文件名反转为bytes,然后再使用对应的编码方式解码:

1
2
3
4
fpath = '/path/to/zip.zip'
zfile = zipfile.ZipFile(fpath, 'r')
for fileinfo in zfile.filelist:
print(fileinfo.filename.encode('cp437').decode('gb18030'))

阅读更多

LeetCode 41. First Missing Positive

这个题目虽然是Hard,但是只要想到解法之后,就很简单了。

题目要求:找到最小的、没有出现在数组中的整数,数组未排序。

初看起来,这个至少得排个序才能搞定。但是题目说了,时间复杂度O(n),空间复杂度O(1)。

如果能把数字n填写到第n-1个,那不就能在O(n)时间内看出来缺失数字了吗?把数字n填写到第n-1个,完全就是遍历一遍所有的数字就可以了啊。

思路就这样出来了:

  1. 遍历所有数字,将数字n放到第n-1个位
  2. 遍历数组,第一个不满足nums[i]!=i+1的i+1即为缺失的数字

代码如下:

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
class Solution {
public int firstMissingPositive(int[] nums) {
for (int i = 0; i < nums.length;) {
if (nums[i] > 0 && nums[i] <= nums.length) {
if (nums[nums[i] - 1] == nums[i]) {
// 两者相等就没有必要交换了,next
i++;
continue;
}
int tmp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = tmp;
// 交换完成之后,我们并不能确保当前的nums[i]在该在的位置上了
// 所以不能贸然处理下一个
// 这是一个坑
}
if (nums[i] <= 0 || nums[i] > nums.length || nums[i] == i + 1) {
// 要么这个数字不在0-n内,要么这个数字已经在该在的位置上了
// 总而言之,next
i++;
}
}
for (int i = 0; i < nums.length; i++) {
if (nums[i] != i + 1) {
// 所有在范围内的数字都已经到对应位置了
// 但这个位置没有对应的数字,所以这个数字是缺失了的
return i + 1;
}
}
// 从0-n都有,所以缺失了n+1
return nums.length + 1;
}
}

第一个for循环,虽然i并不一定在每次循环的时候递增,但是总的时间复杂度还是O(n):

对于所有的数字,要不交换到对应位置,要么不交换:即O(n)。

阅读更多

Firefox无法播放mp4格式的视频(Fedora)

最近又开始用起来Fedora了,昨天发现Firefox没法看视频:

console提示如下:

HTTP “Content-Type” of “video/mp4” is not supported. Load of media resource https://example.com/incorrect\_feedback.mp4 failed.
VIDEOJS: ERROR: (CODE:4 MEDIA_ERR_SRC_NOT_SUPPORTED) No compatible source was found for this media.

尝试了下fedora-cisco-openh264.repo提供的mozilla-openh264,还是无法播放。

看了下网上的说明,mozilla-openh264这个plugin,是为了在WebRTC会话中解码H.264的。

而网上所说的,安装gstreamer系列的,那是因为Firefox旧版本是使用gstreamer来解码的;新版的Firefox使用了ffmpeg来解码视频。

所以只需要安装compat-ffmpeg28就行了:

sudo dnf install compat-ffmpeg28

阅读更多