TCP 协议简介

TCP 是互联网核心协议之一,本文介绍它的基础知识。

一、TCP 协议的作用

互联网由一整套协议构成。TCP 只是其中的一层,有着自己的分工。

(图片说明:TCP 是以太网协议和 IP 协议的上层协议,也是应用层协议的下层协议。)

最底层的以太网协议(Ethernet)规定了电子信号如何组成数据包(packet),解决了子网内部的点对点通信。

(图片说明:以太网协议解决了局域网的点对点通信。)

但是,以太网协议不能解决多个局域网如何互通,这由 IP 协议解决。

(图片说明:IP 协议可以连接多个局域网。)

IP 协议定义了一套自己的地址规则,称为 IP 地址。它实现了路由功能,允许某个局域网的 A 主机,向另一个局域网的 B 主机发送消息。

(图片说明:路由器就是基于 IP 协议。局域网之间要靠路由器连接。)

路由的原理很简单。市场上所有的路由器,背后都有很多网口,要接入多根网线。路由器内部有一张路由表,规定了 A 段 IP 地址走出口一,B 段地址走出口二,……通过这套”指路牌”,实现了数据包的转发。

(图片说明:本机的路由表注明了不同 IP 目的地的数据包,要发送到哪一个网口(interface)。)

IP 协议只是一个地址协议,并不保证数据包的完整。如果路由器丢包(比如缓存满了,新进来的数据包就会丢失),就需要发现丢了哪一个包,以及如何重新发送这个包。这就要依靠 TCP 协议。

简单说,TCP 协议的作用是,保证数据通信的完整性和可靠性,防止丢包。

二、TCP 数据包的大小

以太网数据包(packet)的大小是固定的,最初是1518字节,后来增加到1522字节(15KB )。其中, 1500 字节是负载(payload),22字节是头信息(head)。

IP 数据包在以太网数据包的负载里面,它也有自己的头信息,最少需要20字节,所以 IP 数据包的负载最多为1480字节。

(图片说明:IP 数据包在以太网数据包里面,TCP 数据包在 IP 数据包里面。)

TCP 数据包在 IP 数据包的负载里面。它的头信息最少也需要20字节,因此 TCP 数据包的最大负载是 1480 – 20 = 1460 字节。由于 IP 和 TCP 协议往往有额外的头信息,所以 TCP 负载实际为1400字节左右。

因此,一条1500字节的信息需要两个 TCP 数据包。HTTP/2 协议的一大改进, 就是压缩 HTTP 协议的头信息,使得一个 HTTP 请求可以放在一个 TCP 数据包里面,而不是分成多个,这样就提高了速度。

(图片说明:以太网数据包的负载是1500字节,TCP 数据包的负载在1400字节左右。)

你可能会问,为什么一个以太网数据包只能有1500字节负载,不能设计得更大一些吗?

这是因为这些协议是上个世纪设计的,必须考虑到那时的硬件状况。如果数据包太大,就会占用更多的缓存。如果缓存溢出,就会大量丢包,降低整个网络的通信效率。

三、TCP 数据包的编号(SEQ)

一个包1400字节,那么一次性发送大量数据,就必须分成多个包。比如,一个 10MB 的文件,需要发送7100多个包。

发送的时候,TCP 协议为每个包编号(sequence number,简称 SEQ),以便接收的一方按照顺序还原。万一发生丢包,也可以知道丢失的是哪一个包。

第一个包的编号是一个随机数。为了便于理解,这里就把它称为1号包。假定这个包的负载长度是100字节,那么可以推算出下一个包的编号应该是101。这就是说,每个数据包都可以得到两个编号:自身的编号,以及下一个包的编号。接收方由此知道,应该按照什么顺序将它们还原成原始文件。

(图片说明:当前包的编号是45943,下一个数据包的编号是46183,由此可知,这个包的负载是240字节。)

四、TCP 数据包的组装

收到 TCP 数据包以后,组装还原是操作系统完成的。应用程序不会直接处理 TCP 数据包。

对于应用程序来说,不用关心数据通信的细节。除非线路异常,收到的总是完整的数据。应用程序需要的数据放在 TCP 数据包里面,有自己的格式(比如 HTTP 协议)。

TCP 并没有提供任何机制,表示原始文件的大小,这由应用层的协议来规定。比如,HTTP 协议就有一个头信息Content-Length,表示信息体的大小。对于操作系统来说,就是持续地接收 TCP 数据包,将它们按照顺序组装好,一个包都不少。

操作系统不会去处理 TCP 数据包里面的数据。一旦组装好 TCP 数据包,就把它们转交给应用程序。TCP 数据包里面有一个端口(port)参数,就是用来指定转交给监听该端口的应用程序。

(图片说明:系统根据 TCP 数据包里面的端口,将组装好的数据转交给相应的应用程序。上图中,21端口是 FTP 服务器,25端口是 SMTP 服务,80端口是 Web 服务器。)

应用程序收到组装好的原始数据,以浏览器为例,就会根据 HTTP 协议的Content-Length字段正确读出一段段的数据。这也意味着,一次 TCP 通信可以包括多个 HTTP 通信。

五、慢启动和 ACK

服务器发送数据包,当然越快越好,最好一次性全发出去。但是,发得太快,就有可能丢包。带宽小、路由器过热、缓存溢出等许多因素都会导致丢包。线路不好的话,发得越快,丢得越多。

最理想的状态是,在线路允许的情况下,达到最高速率。但是我们怎么知道,对方线路的理想速率是多少呢?答案就是慢慢试。

TCP 协议为了做到效率与可靠性的统一,设计了一个慢启动(slow start)机制。开始的时候,发送得较慢,然后根据丢包的情况,调整速率:如果不丢包,就加快发送速度;如果丢包,就降低发送速度。

Linux 内核里面设定了(常量TCP_INIT_CWND),刚开始通信的时候,发送方一次性发送10个数据包,即”发送窗口”的大小为10。然后停下来,等待接收方的确认,再继续发送。

默认情况下,接收方每收到两个 TCP 数据包,就要发送一个确认消息。”确认”的英语是 acknowledgement,所以这个确认消息就简称 ACK。

ACK 携带两个信息。

  • 期待要收到下一个数据包的编号
  • 接收方的接收窗口的剩余容量

发送方有了这两个信息,再加上自己已经发出的数据包的最新编号,就会推测出接收方大概的接收速度,从而降低或增加发送速率。这被称为”发送窗口”,这个窗口的大小是可变的。

(图片说明:每个 ACK 都带有下一个数据包的编号,以及接收窗口的剩余容量。双方都会发送 ACK。)

注意,由于 TCP 通信是双向的,所以双方都需要发送 ACK。两方的窗口大小,很可能是不一样的。而且 ACK 只是很简单的几个字段,通常与数据合并在一个数据包里面发送。

(图片说明:上图一共4次通信。第一次通信,A 主机发给B 主机的数据包编号是1,长度是100字节,因此第二次通信 B 主机的 ACK 编号是 1 + 100 = 101,第三次通信 A 主机的数据包编号也是 101。同理,第二次通信 B 主机发给 A 主机的数据包编号是1,长度是200字节,因此第三次通信 A 主机的 ACK 是201,第四次通信 B 主机的数据包编号也是201。)

即使对于带宽很大、线路很好的连接,TCP 也总是从10个数据包开始慢慢试,过了一段时间以后,才达到最高的传输速率。这就是 TCP 的慢启动。

六、数据包的遗失处理

TCP 协议可以保证数据通信的完整性,这是怎么做到的?

前面说过,每一个数据包都带有下一个数据包的编号。如果下一个数据包没有收到,那么 ACK 的编号就不会发生变化。

举例来说,现在收到了4号包,但是没有收到5号包。ACK 就会记录,期待收到5号包。过了一段时间,5号包收到了,那么下一轮 ACK 会更新编号。如果5号包还是没收到,但是收到了6号包或7号包,那么 ACK 里面的编号不会变化,总是显示5号包。这会导致大量重复内容的 ACK。

如果发送方发现收到三个连续的重复 ACK,或者超时了还没有收到任何 ACK,就会确认丢包,即5号包遗失了,从而再次发送这个包。通过这种机制,TCP 保证了不会有数据包丢失。

(图片说明:Host B 没有收到100号数据包,会连续发出相同的 ACK,触发 Host A 重发100号数据包。)

七、参考链接

(完)

文档信息

地铁时报6.07

发信人: nzhth (n站同号), 信区: Joke
标 题: 地铁时报6.07
发信站: 水木社区 (Wed Jun 7 09:14:22 2017), 站内

好运气,也是一种实力!
?

给大家讲段悲惨往事,我高中时化学奇差,考试成绩都是单数,10分以下。有一次试卷发下来,我居然15 分,狂喜啊!然后化学老师就在讲台上说:“最后一道大题出错了,15分,都给你们了。”

某人走进一家酒馆,看到酒馆里有个人身上挂着一个牌子:世界上记忆力最好的人!于是这个人就跑过去问:“你30年前的今天早上吃了什么?”“鸡蛋!”这个人无法判断他说的是真是假,耸耸肩走了。几年后,这个人又来到这家酒馆,又看到这个人,于是走过去问:“怎么做的?”“炒着吃。”

她最喜欢吃酸菜鱼,他为了追她,走遍全国拜师学艺,习得众多酸菜鱼做法。回到家后,他凭着一手绝活开了家酸菜鱼店,生意好得不得了,数钱数到手软,再也没心思琢磨儿女之情了。

我开车经过农村的时候不小心碾到了一只鸭子,我赶快下车,看到一个小男孩在旁边玩,我忐忑地问:“小朋友,这鸭子是你家的吗?”小男孩仔细地看了会儿,说:“不是,我家的鸭子没有那么扁!”

我老丈人家总共三个女儿。老婆的大姐结婚时,男方给了1万彩礼,老丈人陪嫁了2万;二姐结婚时,男方给了2万,老丈人陪嫁了4万;轮到我俩结婚,我一咬牙给了10万,然后就没有然后了。

我和我女朋友都是医学院的。刚开始时,她学扎针就在我手上练习,后来学习刮痧也在我背上练习,学习拔罐、针灸依旧如此。昨天我看见她在淘宝上买了一套手术刀……

昨晚我和同事加班到很晚,下班后,我开车送一个美女同事回家。在一个老旧的小区门口,我把车停住:“就送你到这吧,我不进去了。”美女同事凑过来,倚在我耳边说:“你开车送我进去吧,还能上去坐坐。”我果断地拒绝了。望着她远去的背影,我心想:要是当初好好练科目二,现在也不用害怕车倒不出来了!

我接上小学的外甥放学,有个小女孩追上来问:“叔叔,你是XX的爸爸吗?”我微笑着说:“我是他舅舅。”外甥低下头使劲拽着我离开,我想这小子肯定惹祸了。结果小女孩一脸轻蔑地对外甥说:“你可真能吹,两篇日记都写照顾植物人舅舅。”

笑出马甲线

@光消失的地方
早晨我坐地铁去学校,在地铁上捧着书临时补作业。一个年轻的妈妈指着我教育孩子:“你看这个哥哥求知欲多强。”我很想转过头对她说:“拉倒吧,这哪是求知欲,这是求生欲。”

@夏日的帆

Go语言的修饰器编程

之前写过一篇《Python修饰器的函数式编程》,这种模式很容易的可以把一些函数装配到另外一些函数上,可以让你的代码更为的简单,也可以让一些“小功能型”的代码复用性更高,让代码中的函数可以像乐高玩具那样自由地拼装。所以,一直以来,我对修饰器decoration这种编程模式情有独钟,这里写一篇Go语言相关的文章。

看过Python修饰器那篇文章的同学,一定知道这是一种函数式编程的玩法——用一个高阶函数来包装一下。多唠叨一句,关于函数式编程,可以参看我之前写过一篇文章《函数式编程》,这篇文章主要是,想通过从过程式编程的思维方式过渡到函数式编程的思维方式,从而带动更多的人玩函数式编程,所以,如果你想了解一下函数式编程,那么可以移步先阅读一下。所以,Go语言的修饰器编程模式,其实也就是函数式编程的模式。

不过,要提醒注意的是,Go 语言的“糖”不多,而且又是强类型的静态无虚拟机的语言,所以,无法做到像 Java 和 Python 那样的优雅的修饰器的代码。当然,也许是我才才疏学浅,如果你知道有更多的写法,请你一定告诉我。先谢过了。

简单示例

我们先来看一个示例:

package main

import "fmt"

func decorator(f func(s string)) func(s string) {

        return func(s string) {
                fmt.Println("Started")
                f(s)
                fmt.Println("Done")
        }
}

func Hello(s string) {
        fmt.Println(s)
}

func main() {
        decorator(Hello)("Hello, World!")
}

我们可以看到,我们动用了一个高阶函数 decorator(),在调用的时候,先把 Hello() 函数传进去,然后其返回一个匿名函数,这个匿名函数中除了运行了自己的代码,也调用了被传入的 Hello() 函数。

这个玩法和 Python 的异曲同工,只不过,有些遗憾的是,Go 并不支持像 Python 那样的 @decorator 语法糖。所以,在调用上有些难看。当然,如果你要想让代码容易读一些,你可以这样:

hello := decorator(Hello)
hello("Hello")

我们再来看一个和计算运行时间的例子:

package main

import (
  "fmt"
  "reflect"
  "runtime"
  "time"
)

type SumFunc func(int64, int64) int64

func getFunctionName(i interface{}) string {
  return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}

func timedSumFunc(f SumFunc) SumFunc {
  return func(start, end int64) int64 {

    defer func(t time.Time) {
      fmt.Printf("--- Time Elapsed (%s): %v ---\n", 
          getFunctionName(f), time.Since(t))
    }(time.Now())

    return f(start, end)
  }
}

func Sum1(start, end int64) int64 {
  var sum int64
  sum = 0
  if start > end {
    start, end = end, start
  }
  for i := start; i <= end; i++ {
    sum += i
  }
  return sum
}

func Sum2(start, end int64) int64 {
  if start > end {
    start, end = end, start
  }
  return (end - start + 1) * (end + start) / 2
}

func main() {

  sum1 := timedSumFunc(Sum1)
  sum2 := timedSumFunc(Sum2)

  fmt.Printf("%d, %d\n", sum1(-10000, 10000000), sum2(-10000, 10000000))
}

关于上面的代码,有几个事说明一下:

1)有两个 Sum 函数,Sum1() 函数就是简单的做个循环,Sum2() 函数动用了数据公式。(注意:start 和 end 有可能有负数的情况)

2)代码中使用了 Go 语言的反射机器来获取函数名。

3)修饰器函数是 timedSumFunc()

运行后输出:

$ go run time.sum.go
--- Time Elapsed (main.Sum1): 3.557469ms ---
--- Time Elapsed (main.Sum2): 291ns ---
49999954995000, 49999954995000

HTTP 相关的一个示例

我们再来看一个处理 HTTP 请求的相关的例子。

先看一个简单的 HTTP Server 的代码。

package main

import (
        "fmt"
        "log"
        "net/http"
        "strings"
)

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                log.Println("--->WithServerHeader()")
                w.Header().Set("Server", "HelloServer v0.0.1")
                h(w, r)
        }
}

func hello(w http.ResponseWriter, r *http.Request) {
        log.Printf("Recieved Request %s from %s\n", r.URL.Path, r.RemoteAddr)
        fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
        http.HandleFunc("/v1/hello", WithServerHeader(hello))
        err := http.ListenAndServe(":8080", nil)
        if err != nil {
                log.Fatal("ListenAndServe: ", err)
        }
}

上面代码中使用到了修饰模式,WithServerHeader() 函数就是一个 Decorator,其传入一个 http.HandlerFunc,然后返回一个改写的版本。上面的例子还是比较简单,用 WithServerHeader() 就可以加入一个 Response 的 Header。

于是,这样的函数我们可以写出好些个。如下所示,有写 HTTP 响应头的,有写认证 Cookie 的,有检查认证Cookie的,有打日志的……

package main

import (
        "fmt"
        "log"
        "net/http"
        "strings"
)

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                log.Println("--->WithServerHeader()")
                w.Header().Set("Server", "HelloServer v0.0.1")
                h(w, r)
        }
}

func WithAuthCookie(h http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                log.Println("--->WithAuthCookie()")
                cookie := &http.Cookie{Name: "Auth", Value: "Pass", Path: "/"}
                http.SetCookie(w, cookie)
                h(w, r)
        }
}

func WithBasicAuth(h http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                log.Println("--->WithBasicAuth()")
                cookie, err := r.Cookie("Auth")
                if err != nil || cookie.Value != "Pass" {
                        w.WriteHeader(http.StatusForbidden)
                        return
                }
                h(w, r)
        }
}

func WithDebugLog(h http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                log.Println("--->WithDebugLog")
                r.ParseForm()
                log.Println(r.Form)
                log.Println("path", r.URL.Path)
                log.Println("scheme", r.URL.Scheme)
                log.Println(r.Form["url_long"])
                for k, v := range r.Form {
                        log.Println("key:", k)
                        log.Println("val:", strings.Join(v, ""))
                }
                h(w, r)
        }
}
func hello(w http.ResponseWriter, r *http.Request) {
        log.Printf("Recieved Request %s from %s\n", r.URL.Path, r.RemoteAddr)
        fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
        http.HandleFunc("/v1/hello", WithServerHeader(WithAuthCookie(hello)))
        http.HandleFunc("/v2/hello", WithServerHeader(WithBasicAuth(hello)))
        http.HandleFunc("/v3/hello", WithServerHeader(WithBasicAuth(WithDebugLog(hello))))
        err := http.ListenAndServe(":8080", nil)
        if err != nil {
                log.Fatal("ListenAndServe: ", err)
        }
}

多个修饰器的 Pipeline

在使用上,需要对函数一层层的套起来,看上去好像不是很好看,如果需要 decorator 比较多的话,代码会比较难看了。嗯,我们可以重构一下。

重构时,我们需要先写一个工具函数——用来遍历并调用各个 decorator:

type HttpHandlerDecorator func(http.HandlerFunc) http.HandlerFunc

func Handler(h http.HandlerFunc, decors ...HttpHandlerDecorator) http.HandlerFunc {
        for i := range decors {
                d := decors[len(decors)-1-i] // iterate in reverse
                h = d(h)
        }
        return h
}

然后,我们就可以像下面这样使用了。

http.HandleFunc("/v4/hello", Handler(hello,
                WithServerHeader, WithBasicAuth, WithDebugLog))

这样的代码是不是更易读了一些?pipeline 的功能也就出来了。

泛型的修饰器

不过,对于 Go 的修饰器模式,还有一个小问题 —— 好像无法做到泛型,就像上面那个计算时间的函数一样,其代码耦合了需要被修饰的函数的接口类型,无法做到非常通用,如果这个事解决不了,那么,这个修饰器模式还是有点不好用的。

因为 Go 语言不像 Python 和 Java,Python是动态语言,而 Java 有语言虚拟机,所以他们可以干好些比较变态的事,然而 Go 语言是一个静态的语言,这意味着其类型需要在编译时就要搞定,否则无法编译。不过,Go 语言支持的最大的泛型是 interface{} 还有比较简单的 reflection 机制,在上面做做文章,应该还是可以搞定的。

废话不说,下面是我用 reflection 机制写的一个比较通用的修饰器(为了便于阅读,我删除了出错判断代码)

func Decorator(decoPtr, fn interface{}) (err error) {
        var decoratedFunc, targetFunc reflect.Value

        decoratedFunc = reflect.ValueOf(decoPtr).Elem()
        targetFunc = reflect.ValueOf(fn)

        v := reflect.MakeFunc(targetFunc.Type(),
                func(in []reflect.Value) (out []reflect.Value) {
                        fmt.Println("before")
                        out = targetFunc.Call(in)
                        fmt.Println("after")
                        return
                })

        decoratedFunc.Set(v)
        return
}

上面的代码动用了 reflect.MakeFunc() 函数制出了一个新的函数其中的 targetFunc.Call(in) 调用了被修饰的函数。关于 Go 语言的反射机制,推荐官方文章 —— 《The Laws of Reflection》,在这里我不多说了。

上面这个 Decorator() 需要两个参数,

  • 第一个是出参 decoPtr ,就是完成修饰后的函数
  • 第二个是入参 fn ,就是需要修饰的函数

这样写是不是有些二?的确是的。不过,这是我个人在 Go 语言里所能写出来的最好的的代码了。如果你知道更多优雅的,请你一定告诉我!

好的,让我们来看一下使用效果。首先假设我们有两个需要修饰的函数:

func foo(a, b, c int) int {
        fmt.Printf("%d, %d, %d \n", a, b, c)
        return a + b + c
}

func bar(a, b string) string {
        fmt.Printf("%s, %s \n", a, b)
        return a + b
}

然后,我们可以这样做:

type MyFoo func(int, int, int) int
var myfoo MyFoo
Decorator(&myfoo, foo)
myfoo(1, 2, 3)

你会发现,使用 Decorator() 时,还需要先声明一个函数签名,感觉好傻啊。一点都不泛型,不是吗?

嗯。如果你不想声明函数签名,那么你也可以这样

mybar := bar
Decorator(&mybar, bar)
mybar("hello,", "world!")

好吧,看上去不是那么的漂亮,但是 it works。看样子 Go 语言目前本身的特性无法做成像 Java 或 Python 那样,对此,我们只能多求 Go 语言多放糖了!

Again, 如果你有更好的写法,请你一定要告诉我。

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——

XOR 加密简介

本文介绍一种简单高效、非常安全的加密方法:XOR 加密。

一、 XOR 运算

逻辑运算之中,除了 ANDOR,还有一种 XOR 运算,中文称为”异或运算”。

它的定义是:两个值相同时,返回false,否则返回true。也就是说,XOR可以用来判断两个值是否不同。


true XOR true // false
false XOR false // false
true XOR false // true
true XOR false // true

JavaScript 语言的二进制运算,有一个专门的 XOR 运算符,写作^


1 ^ 1 // 0
0 ^ 0 // 0
1 ^ 0 // 1
0 ^ 1 // 1

上面代码中,如果两个二进制位相同,就返回0,表示false;否则返回1,表示true

二、 XOR 的应用

XOR 运算有一个很奇妙的特点:如果对一个值连续做两次 XOR,会返回这个值本身。


// 第一次 XOR
1010 ^ 1111 // 0101

// 第二次 XOR
0101 ^ 1111 // 1010

上面代码中,原始值是1010,再任意选择一个值(上例是1111),做两次 XOR,最后总是会得到原始值1010。这在数学上是很容易证明的。

三、加密应用

XOR 的这个特点,使得它可以用于信息的加密。


message XOR key // cipherText
cipherText XOR key // message

上面代码中,原始信息是message,密钥是key,第一次 XOR 会得到加密文本cipherText。对方拿到以后,再用key做一次 XOR 运算,就会还原得到message

四、完美保密性

二战期间,各国为了电报加密,对密码学进行了大量的研究和实践,其中就包括 XOR 加密。

战后,美国数学家香农(Claude Shannon)将他的研究成果公开发表,证明了只要满足两个条件,XOR 加密是无法破解的。

  • key的长度大于等于message
  • key必须是一次性的,且每次都要随机产生

理由很简单,如果每次的key都是随机的,那么产生的CipherText具有所有可能的值,而且是均匀分布,无法从CipherText看出message的任何特征。也就是说,它具有最大的”信息熵”,因此完全不可能破解。这被称为 XOR 的“完美保密性”(perfect secrecy)。

满足上面两个条件的key,叫做 one-time pad(缩写为OTP),意思是”一次性密码本”,因为以前这样的key都是印刷成密码本,每次使用的时候,必须从其中挑选key

五、实例:哈希加密

下面的例子使用 XOR,对用户的登陆密码进行加密。实际运行效果看这里

第一步,用户设置登陆密码的时候,算出这个密码的哈希,这里使用的是 MD5 算法,也可以采用其他哈希算法。


const message = md5(password);

第二步,生成一个随机的 key。


// 生成一个随机整数,范围是 [min, max]
function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// 生成一个随机的十六进制的值,在 0 ~ f 之间 
function getHex() {
  let n = 0;
  for (let i = 4; i > 0; i--) {
    n = (getRandomInt(0, 1) 

上面代码中,生成的key是32位的十六进制值,对应 MD5 产生的128位的二进制哈希。

第三步,进行 XOR 运算,求出加密后的message


function getXOR(message, key) {
  const arr = [];
  for (let i = 0; i 

使用这种方法保存用户的登陆密码,即使加密文本泄露,只要一次性的密钥(key)没有泄露,对方也无法破解。

(完)

文档信息

Server-Sent Events 教程

服务器向浏览器推送信息,除了 WebSocket,还有一种方法:Server-Sent Events(以下简称 SSE)。本文介绍它的用法。

一、SSE 的本质

严格地说,HTTP 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。

也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。

SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。

二、SSE 的特点

SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。

总体来说,WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。

但是,SSE 也有自己的优点。

  • SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
  • SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
  • SSE 默认支持断线重连,WebSocket 需要自己实现。
  • SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
  • SSE 支持自定义发送的消息类型。

因此,两者各有特点,适合不同的场合。

三、客户端 API

3.1 EventSource 对象

SSE 的客户端 API 部署在EventSource对象上。下面的代码可以检测浏览器是否支持 SSE。


if ('EventSource' in window) {
  // ...
}

使用 SSE 时,浏览器首先生成一个EventSource实例,向服务器发起连接。


var source = new EventSource(url);

上面的url可以与当前网址同域,也可以跨域。跨域时,可以指定第二个参数,打开withCredentials属性,表示是否一起发送 Cookie。


var source = new EventSource(url, { withCredentials: true });

EventSource实例的readyState属性,表明连接的当前状态。该属性只读,可以取以下值。

  • 0:相当于常量EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。
  • 1:相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。
  • 2:相当于常量EventSource.CLOSED,表示连接已断,且不会重连。

3.2 基本用法

连接一旦建立,就会触发open事件,可以在onopen属性定义回调函数。


source.onopen = function (event) {
  // ...
};

// 另一种写法
source.addEventListener('open', function (event) {
  // ...
}, false);

客户端收到服务器发来的数据,就会触发message事件,可以在onmessage属性的回调函数。


source.onmessage = function (event) {
  var data = event.data;
  // handle message
};

// 另一种写法
source.addEventListener('message', function (event) {
  var data = event.data;
  // handle message
}, false);

上面代码中,事件对象的data属性就是服务器端传回的数据(文本格式)。

如果发生通信错误(比如连接中断),就会触发error事件,可以在onerror属性定义回调函数。


source.onerror = function (event) {
  // handle error event
};

// 另一种写法
source.addEventListener('error', function (event) {
  // handle error event
}, false);

close方法用于关闭 SSE 连接。


source.close();

3.3 自定义事件

默认情况下,服务器发来的数据,总是触发浏览器EventSource实例的message事件。开发者还可以自定义 SSE 事件,这种情况下,发送回来的数据不会触发message事件。


source.addEventListener('foo', function (event) {
  var data = event.data;
  // handle message
}, false);

上面代码中,浏览器对 SSE 的foo事件进行监听。如何实现服务器发送foo事件,请看下文。

四、服务器实现

4.1 数据格式

服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。


Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam

每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。每个message内部由若干行组成,每一行都是如下格式。


[field]: value\n

上面的field可以取四个值。

  • data
  • event
  • id
  • retry

此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。


: This is a comment

下面是一个例子。


: this is a test stream\n\n

data: some text\n\n

data: another message\n
data: with two lines \n\n

4.2 data 字段

数据内容用data字段表示。


data:  message\n\n

如果数据很长,可以分成多行,最后一行用\n\n结尾,前面行都用\n结尾。


data: begin message\n
data: continue message\n\n

下面是一个发送 JSON 数据的例子。


data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

4.3 id 字段

数据标识符用id字段表示,相当于每一条数据的编号。


id: msg1\n
data: message\n\n

浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。

4.4 event 字段

event字段表示自定义的事件类型,默认是message事件。浏览器可以用addEventListener()监听该事件。


event: foo\n
data: a foo event\n\n

data: an unnamed event\n\n

event: bar\n
data: a bar event\n\n

上面的代码创造了三条信息。第一条的名字是foo,触发浏览器的foo事件;第二条未取名,表示默认类型,触发浏览器的message事件;第三条是bar,触发浏览器的bar事件。

下面是另一个例子。


event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}

event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}

event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}

4.5 retry 字段

服务器可以用retry字段,指定浏览器重新发起连接的时间间隔。


retry: 10000\n

两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。

五、Node 服务器实例

SSE 要求服务器与浏览器保持连接。对于不同的服务器软件来说,所消耗的资源是不一样的。Apache 服务器,每个连接就是一个线程,如果要维持大量连接,势必要消耗大量资源。Node 则是所有连接都使用同一个线程,因此消耗的资源会小得多,但是这要求每个连接不能包含很耗时的操作,比如磁盘的 IO 读写。

下面是 Node 的 SSE 服务器实例


var http = require("http");

http.createServer(function (req, res) {
  var fileName = "." + req.url;

  if (fileName === "./stream") {
    res.writeHead(200, {
      "Content-Type":"text/event-stream",
      "Cache-Control":"no-cache",
      "Connection":"keep-alive",
      "Access-Control-Allow-Origin": '*',
    });
    res.write("retry: 10000\n");
    res.write("event: connecttime\n");
    res.write("data: " + (new Date()) + "\n\n");
    res.write("data: " + (new Date()) + "\n\n");

    interval = setInterval(function () {
      res.write("data: " + (new Date()) + "\n\n");
    }, 1000);

    req.connection.addListener("close", function () {
      clearInterval(interval);
    }, false);
  }
}).listen(8844, "127.0.0.1");

请将上面的代码保存为server.js,然后执行下面的命令。


$ node server.js

上面的命令会在本机的8844端口,打开一个 HTTP 服务。

然后,打开这个网页,查看客户端代码并运行。

六、参考链接

(完)

文档信息

WebSocket 教程

WebSocket 是一种网络通信协议,很多高级功能都需要它。

本文介绍 WebSocket 协议的使用方法。

一、为什么需要 WebSocket?

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用“轮询”:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

二、简介

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。


ws://example.com:80/some/path

三、客户端的简单示例

WebSocket 的用法相当简单。

下面是一个网页脚本的例子(点击这里看运行结果),基本上一眼就能明白。


var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};      

四、客户端的 API

WebSocket 客户端的 API 如下。

4.1 WebSocket 构造函数

WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。


var ws = new WebSocket('ws://localhost:8080');

执行上面语句之后,客户端就会与服务器进行连接。

实例对象的所有属性和方法清单,参见这里

4.2 webSocket.readyState

readyState属性返回实例对象的当前状态,共有四种。

  • CONNECTING:值为0,表示正在连接。
  • OPEN:值为1,表示连接成功,可以通信了。
  • CLOSING:值为2,表示连接正在关闭。
  • CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

下面是一个示例。


switch (ws.readyState) {
  case WebSocket.CONNECTING:
    // do something
    break;
  case WebSocket.OPEN:
    // do something
    break;
  case WebSocket.CLOSING:
    // do something
    break;
  case WebSocket.CLOSED:
    // do something
    break;
  default:
    // this never happens
    break;
}

4.3 webSocket.onopen

实例对象的onopen属性,用于指定连接成功后的回调函数。


ws.onopen = function () {
  ws.send('Hello Server!');
}

如果要指定多个回调函数,可以使用addEventListener方法。


ws.addEventListener('open', function (event) {
  ws.send('Hello Server!');
});

4.4 webSocket.onclose

实例对象的onclose属性,用于指定连接关闭后的回调函数。


ws.onclose = function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
};

ws.addEventListener("close", function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
});

4.5 webSocket.onmessage

实例对象的onmessage属性,用于指定收到服务器数据后的回调函数。


ws.onmessage = function(event) {
  var data = event.data;
  // 处理数据
};

ws.addEventListener("message", function(event) {
  var data = event.data;
  // 处理数据
});

注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)。


ws.onmessage = function(event){
  if(typeof event.data === String) {
    console.log("Received data string");
  }

  if(event.data instanceof ArrayBuffer){
    var buffer = event.data;
    console.log("Received arraybuffer");
  }
}

除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型。


// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
  console.log(e.data.size);
};

// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
  console.log(e.data.byteLength);
};

4.6 webSocket.send()

实例对象的send()方法用于向服务器发送数据。

发送文本的例子。


ws.send('your message');

发送 Blob 对象的例子。


var file = document
  .querySelector('input[type="file"]')
  .files[0];
ws.send(file);

发送 ArrayBuffer 对象的例子。


// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i 

4.7 webSocket.bufferedAmount

实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。


var data = new ArrayBuffer(10000000);
socket.send(data);

if (socket.bufferedAmount === 0) {
  // 发送完毕
} else {
  // 发送还没结束
}

4.8 webSocket.onerror

实例对象的onerror属性,用于指定报错时的回调函数。


socket.onerror = function(event) {
  // handle error event
};

socket.addEventListener("error", function(event) {
  // handle error event
});

五、服务端的实现

WebSocket 服务器的实现,可以查看维基百科的列表

常用的 Node 实现有以下三种。

具体的用法请查看它们的文档,这里不详细介绍了。

六、WebSocketd

下面,我要推荐一款非常特别的 WebSocket 服务器:Websocketd

它的最大特点,就是后台脚本不限语言,标准输入(stdin)就是 WebSocket 的输入,标准输出(stdout)就是 WebSocket 的输出。

举例来说,下面是一个 Bash 脚本counter.sh


#!/bin/bash

echo 1
sleep 1

echo 2
sleep 1

echo 3

命令行下运行这个脚本,会输出1、2、3,每个值之间间隔1秒。


$ bash ./counter.sh
1
2
3

现在,启动websocketd,指定这个脚本作为服务。


$ websocketd --port=8080 bash ./counter.sh

上面的命令会启动一个 WebSocket 服务器,端口是8080。每当客户端连接这个服务器,就会执行counter.sh脚本,并将它的输出推送给客户端。


var ws = new WebSocket('ws://localhost:8080/');

ws.onmessage = function(event) {
  console.log(event.data);
};

上面是客户端的 JavaScript 代码,运行之后会在控制台依次输出1、2、3。

有了它,就可以很方便地将命令行的输出,发给浏览器。


$ websocketd --port=8080 ls

上面的命令会执行ls命令,从而将当前目录的内容,发给浏览器。使用这种方式实时监控服务器,简直是轻而易举(代码)。

更多的用法可以参考官方示例

websocketd 的实质,就是命令行的 WebSocket 代理。只要命令行可以执行的程序,都可以通过它与浏览器进行 WebSocket 通信。下面是一个 Node 实现的回声服务greeter.js


process.stdin.setEncoding('utf8');

process.stdin.on('readable', function() {
  var chunk = process.stdin.read();
  if (chunk !== null) {
    process.stdout.write('data: ' + chunk);
  }
});

启动这个脚本的命令如下。


$ websocketd --port=8080 node ./greeter.js

官方仓库还有其他各种语言的例子。

七、参考链接

(完)

文档信息

素食主义者

发信人: GGGGDDDDK (我叫囫囵,枣是我吞的), 信区: Joke
标 题: 素食主义者
发信站: 水木社区 (Wed May 10 09:22:09 2017), 站内

素素和菜菜都是素食主义者。有一天,素素和另两个人在荒岛上快饿死了,于是这三个人只好商量由两个人吃另一个人。素素因为是素食主义者,就成为了被吃的那个。菜菜得到这一情况后,意识到了做素食主义者的害处,开始经常吃肉,成为了一个大胖子。后来,菜菜和另两个人乘坐热气球,途中热气球的气不多了,需要扔下去一个人,菜菜因为最重,被扔下去了。

※ 来源:·水木社区 newsmth.net·[FROM: 122.139.1.109]

以鹅为单位,各种动物的战斗力排名。 (转载)

发信人: nighthawk (83。75% 「跑马溜溜的山上」), 信区: Joke
标 题: 以鹅为单位,各种动物的战斗力排名。
发信站: 水木社区 (Fri May 5 23:24:29 2017), 站内

【 以下文字转载自 NewExpress 讨论区 】
发信人: riveryy (riveryy), 信区: NewExpress
标 题: 以鹅为单位,各种动物的战斗力排名。
发信站: 水木社区 (Fri May 5 23:08:15 2017), 站内

设成年公鹅战斗力为1鹅。
如果2只鹅能和一只田园犬旗鼓相当或同归于尽,那么田园犬是2鹅。

以下就是楼主不负责任的随意排名:

蚂蚁:0.0001鹅
蚊子:0.003鹅
毛毛虫:0.003鹅
蜗牛:0.005鹅
中型蜘蛛:0.008鹅
蚂蟥:0.01鹅
蜜蜂:0.015鹅
蜈蚣:0.02鹅
麻雀:0.03鹅
蝎子:0.05鹅
老鼠:0.05鹅
龙虾:0.1鹅
牛蛙:0.12鹅
大鲤鱼:0.15鹅
较大的螃蟹:0.18鹅
3岁以下儿童:0.2鹅
小型无毒蛇:0.2鹅
非洲牛蛙:0.22鹅
鸭子:0.25鹅
普通公鸡:0.3鹅
皮皮虾:0.35鹅
食人鱼:0.4鹅
猴子:0.4鹅
猫:0.45鹅
斗鸡:0.7鹅
10岁左右儿童:0.8鹅
猫头鹰:1.0鹅
羊:1.5鹅
田园犬:2鹅
家猪:3鹅
成年男性:4鹅
老鹰:6鹅
藏獒:10鹅
猎豹:10鹅
马蜂群:15鹅
鬣狗:20鹅
狼:25鹅
黄牛:30鹅
鳄鱼:40鹅
金钱豹:60鹅
黑熊:80鹅
熊猫:90鹅
雌狮:70鹅
雄狮:120鹅
雄东北虎:150鹅
棕熊:150鹅
非洲野牛:180鹅
河马:200鹅
大白鲨:250鹅
犀牛:300鹅
亚洲雄象:400鹅
非洲雄象:500鹅
虎鲸:600鹅
霸王龙:800鹅
狮群(10只):1200鹅
雷龙:1500鹅
蓝鲸:4000鹅
虎鲸群(10只):10000鹅

警察(持手枪):30鹅
普通陆军士兵:200鹅
坦克:5000鹅
阿帕奇:12000鹅
F16:50000鹅
F22:30万鹅
弹道导弹:500万鹅
航母:1200万鹅
核潜艇:1500万鹅
航母战斗群:6000万鹅

.
※ 修改:·riveryy 于 May 5 23:23:45 2017 修改本文·[FROM: 116.224.240.*]
※ 来源:·水木社区 http://m.newsmth.net·[FROM: 116.224.240.*]

How to make flashcards online?

In this article I’ll tell you in detail how to use Flashcard.online editor to create your own flashcards.

Have a look at this video to see how the app works:

STEP 1. Prepare images

If you’re going to make flashcards without images, just go to the next step. If you’re planning to do cards with pictures, you have to do some preparation work.

Search for free images in Google or download royalty-free stock photos and clipart images from under Creative Commons (CC0) license at websites like Pixabay.com, Pexels.com, Stocksnap.io, Unsplash.com.

Ideally, the image size for 2 cards per list is 300*400 px, for 4 per list – 250*300 px, for 8 flashcards per list – 180*220 px. Still if you don’t stick to these sizes, it’s OK that an image is just vertical. I use IrfanView Editor for fast photo cutting and rotation.

prepare images for flashcards

STEP 2. Set flashcard parameters

Go to Flashcard.online editor and set the following parameters:

  • title of your flashcard set
  • number of cards per list (2, 4, or 8)
  • type of flashcards (only text or image+text).

flashcard parameters

STEP 3. Add your content

Upload your images and add text to the flashcards. If necessary, add and remove new lists to your set.

add content to flashcards

You can create up to 5 lists in a single flashcard set.

Step 4. Save the PDF file

Once you have added everyhting, click “Save as PDF”, then “Free Download” to get the file. If you work in Chrome browser, the PDF file will be opened in a new tab. Use right-button menu to save the file to your computer. Enjoy learning with flashcards!

亲爱的,我们再也回不去了,对不对?

发信人: wangsir (joker), 信区: Joke
标 题: 亲爱的,我们再也回不去了,对不对?
发信站: 水木社区 (Tue Apr 25 17:16:05 2017), 站内

下班时接女朋友回家,我们两人一路无语,整整三十分钟竟然找不出一个话题,走到门口时,她哭了,“亲爱的,我们再也回不去了,对不对?”听完,我心都凉了,一巴掌扇了过去:“你TM又把钥匙锁屋里了?”

刚拿到驾照,一次雨后出门,车速很快把积水浪到一小伙子身上,想道歉就倒车回去,结果又溅了他一身,这下不好意思下车道歉了,踩油门速速离开,结果又溅了他一身,还记得他那句传遍整条街的怒吼,我c你大爷

讲一过去真实的事,以前和几个哥们去偷鸡吃,学电视上做 叫花鸡,直接把鸡埋地下敷上土,在上面点火半个小时,扒开后,只见鸡浑身冒着热气跑了……

再来,

小时候比较淘,挨打无数。有次老妈让我去买一斤糯米粉包汤圆,我贪吃买了半斤,其他的钱买唐僧肉和芝麻糊了。走到半路袋子又被我划破了,糯米粉都撒了,走投无路之际看到人家堆放的石灰粉,装了一些回家交差,现在我还记得我爸体贴我妈的那句话:你先歇着,我来打会…

表弟刚刚打电话来批了我一顿,原因是我帮姑姑弄了个QQ号,加了表弟好友,表弟追了姑姑一个多星期,发现这个关心他的女人是他妈

那天,走在街上和闺蜜谈论前面两位男的谁攻谁受,“蓝外套的一定是受。”那个“受”字说的太响,蓝外套直接转过头说道:“你才是受,你全家都是受!”他旁边那位对他说:“媳妇,别激动,人家说的是胖瘦的瘦!”

今天街边买烧烤的来的晚了,我给他打电话,大哥在那边说:劳资卖了这么多年烧烤从来都是自由自在,自从特么认识了你,居然让我有了上班的感觉。

逛街见一男一女吵架女:为什么要这样对我!为什么!(撕心裂肺状)男:你别这样好不好?至于吗?女(蹲在地上痛哭):你不爱我了!男:祖宗!我给你买小布丁行不行!买两根!大姨妈肚子疼别赖我!女(擦擦鼻涕眼泪):我绝对不赖你!男:你都赖了多少次了…于是两人欢乐的牵手走了,留下我纠结的站着。

Category

Archives