mysql innoDB使用聚族索引

https://dev.mysql.com/doc/refman/5.6/en/innodb-index-types.html
http://umumble.com/blogs/mysql/mysql-(innodb)-clustered-and-non_clustered-indexes-/
http://blog.csdn.net/jhgdike/article/details/60579883
http://blog.csdn.net/caomiao2006/article/details/52140953
http://www.cnblogs.com/xxmysql/p/5874803.html

使用swoole和redis做股票和区块链服务

PHP 的redis扩展是阻塞式 IO ,使用订阅/发布模式时,会导致整个进程进入阻塞。因此必须使用Swoole\Redis异步客户端来实现。

$server = new swoole_websocket_server("0.0.0.0", 9501);

$server->on('workerStart', function ($server, $workerId) {
    $client = new swoole_redis;
    $client->on('message', function (swoole_redis $client, $result) use ($server) {
        if ($result[0] == 'message') {
            foreach($server->connections as $fd) {
                $server->push($fd, $result[1]);
            }
        }
    });
    $client->connect('127.0.0.1', 6379, function (swoole_redis $client, $result) {
        $client->subscribe('kline1min');
    });
});

$server->on('open', function ($server, $request) {

});

$server->on('message', function (swoole_websocket_server $server, $frame) {
    $server->push($frame->fd, "hello");
});

$server->on('close', function ($serv, $fd) {

});

$server->start();
  • 在进程启动(onWorkerStart)时创建了Swoole\Redis客户端,连接到Redis服务器
  • 连接成功后,订阅msg_0主题的消息
  • 当有新的message时,Swoole\Redis会触发onMessage事件回调
  • 在这个回调函数中使用$server->connections遍历服务器所有的连接,发送消息

Mac OS普通用户启动80端口

由于系统限制非root用户不能启动1024以下端口,而我们平时使用Mac一般都是非root用户,所以如果想启动80端口必须用root用户,这时候用命令行就可以解决,但是拿Intellij Idea来说,可以在控制台却换到root权限用命令行来启动IDEA,但是这样的话很多信息都是和root用户相关的,比如mavan仓库地址,IDEA配置默认都在root用户目录下面了,而你登录的用户又是非root用户,查看修改和管理都相当麻烦,于是可以通过端口转发功能,把本地的80请求转发到你配置的1024以上的端口上,效果一样

sudo vim /etc/pf.conf

添加

rdr on lo0 inet proto tcp from any to 127.0.0.1 port 80 -> 127.0.0.1 port 8080

到 pf.conf文件的 rdr-anchor "com.apple/*" 这一行后面。
其中 lo0 通过 ifconfig 看自己那个设备绑定的是127.0.0.1, lo0是这个网络设备的名字。 8080是要转发的端口

sudo pfctl -f /etc/pf.conf sudo pfctl -e

邮箱小号Gmail sub-account

這篇文章介紹一個製造 Gmail “小號” 的方法,就是可以製造很多地址不同,但是實際導向同一個電郵地址的方法。

用這個方法,就可以在不註冊新的電郵地址的情況下,製造很多個 “小號”。一則在其他地方可以用不同電郵地址註冊多個帳號,二則方便管理透過不同地址發給自己的郵件。

本文所述的方法,其實網上已經很多人說過,而且都大概 5-10 年前的文章了,不過 Gmail 每年都有很多新使用者,所以現在重新介紹一下。

Gmail 跟其他常見的電郵地址最不同的地方,就是不支持底線 “_” ,相反卻支持點 “.” 和加號 “+” 。而這個製造小號的方法,就是有效利用 “.” 和 “+”。

為安全起見,以下的電郵地址,中間的 “@” 均以 “#” 代替。例如 abc123 # gmail.com。


法則一:gmail 可以改為 googlemail

以前有一些國家註冊的 Gmail 是以 googlemail.com 為地址的,作者本人都註冊過一個。不過現在兩個地址基本上完全通用了。

換句話說,對於 Gmail 而言,abc123 # gmail.com 和 abc123 # googlemail.com 是同一個地址。寄到後者的電郵,一律在前者的地址可以查看。

如此一來,由原來的一個地址,現在有兩個地址可以用了。


法則二:“@” 前面的部份,可以在任意位置加上任意個點 “.”

換句話說,對 Gmail 而言,有 “.” 和沒有 “.” 都是一樣的。

例如 abc123 # gmail.com,可以改為以下任何一個:
abc.123 # gmail.com
abc...123 # gmail.com
.a.b.c.1.2.3. # gmail.com
...abc...123... # gmail.com

對於 Gmail 而言,上面所有地址跟 abc123 # gmail.com 是沒有分別的。於是乎,這個方法可以為自己創造無限多個小號了。

不過需要注意,有些網站的註冊郵箱並不支持前面帶 “.” 的電郵地址,例如 .abc123 # gmail.com 不能用,但是可以用 abc.123 # gmail.com。


法則三:用戶名和 “@” 之間,可以用 “+” 插入任何字串,而且可以加插任意多個 “+”

換句話說,“+” 和 “@” 中間的任何字串,都會被 Gmail 忽略。

例如 abc123 # gmail.com,可以改為以下任何一個:
abc123+def456 # gmail.com
abc123+a+b+c+1+2+3 # gmail.com
abc123+Ax+By+C # gmail.com

真正的粉絲,還可以用這個 (誤) :
abc123+1s # gmail.com

用這個方法,可以讓自己在不同網站的註冊名稱不同,例如
abc123+XDA # gmail.com
abc123+Facebook # gmail.com

甚至可以作為臨時電郵地址,例如
abc123+temp+1 # gmail.com
abc123+temp+2 # gmail.com

這樣子,只要開始收到垃圾郵件,看看收件人就可以知道是那個網站出賣了你的個人資料了。

不過需要注意,不是所有服務也支持帶有 “+” 的電郵地址。例如酷安就不支持了。

這個方法,同樣可以為自己創造無限多個小號,而且比較多元化和比較有系統。


法則四:上面三個法則可以任意搭配

例如,abc123 # gmail.com 可以改為以下任何一個:
abc.123+Maki # googlemail.com
abc...123+Ma.ki+Ni.co # gmail.com
abc123.+.Nico.Nico.Ni. # gmail.com
abc.1.2.3+Yosoro.+.Zura # gmail.com

這樣,就可以創造多元化無限創意的小號了。

雖然本文的方法已經存在了好幾年,不過對於 Gmail 新手來說,應該還是有點用的。

原文:http://telegra.ph/Gmail-Cloning-08-20

distinct 和 group by的使用

公司同事有一个小项目的30w数据搜索用到like和排重查询比较慢,我对语句做了下优化。

mysql> desc shop;
+--------------+---------------------+------+-----+---------+----------------+
| Field        | Type                | Null | Key | Default | Extra          |
+--------------+---------------------+------+-----+---------+----------------+
| id           | int(10) unsigned    | NO   | PRI | NULL    | auto_increment |
| uid          | bigint(20) unsigned | NO   | MUL | 0       |                |
| sid          | bigint(20) unsigned | YES  | MUL | 0       |                |
| nick         | char(128)           | YES  | MUL |         |                |
| shop_type    | char(1)             | YES  |     | C       |                |
| shop_score   | int(10) unsigned    | YES  |     | 0       |                |
| shop_level   | int(10) unsigned    | YES  |     | 0       |                |
| category_id1 | int(10)             | YES  | MUL | 0       |                |
| category_id2 | int(10) unsigned    | NO   | MUL | 0       |                |
| shop_title   | varchar(128)        | YES  |     |         |                |
| shop_avatar  | varchar(255)        | YES  |     |         |                |
| sort         | int(6)              | NO   |     | 0       |                |
| locate       | varchar(45)         | NO   |     |         |                |
| disabled     | tinyint(1)          | NO   |     | 0       |                |
+--------------+---------------------+------+-----+---------+----------------+
14 rows in set (0.01 sec)

mysql> explain select * from shop,(select distinct sid from shop where disabled = 0 and shop_title like '%迪卡侬%' limit 10) shopx where shop.sid=shopx.sid group by shop.sid;
+----+-------------+------------+------+---------------+---------+---------+-----------+--------+----------------------------------------------+
| id | select_type | table      | type | possible_keys | key     | key_len | ref       | rows   | Extra                                        |
+----+-------------+------------+------+---------------+---------+---------+-----------+--------+----------------------------------------------+
|  1 | PRIMARY     | <derived2> | ALL  | NULL          | NULL    | NULL    | NULL      |     10 | Using where; Using temporary; Using filesort |
|  1 | PRIMARY     | shop       | ref  | INX_sid       | INX_sid | 9       | shopx.sid |      1 | NULL                                         |
|  2 | DERIVED     | shop       | ALL  | INX_sid       | NULL    | NULL    | NULL      | 269483 | Using where; Using temporary                 |
+----+-------------+------------+------+---------------+---------+---------+-----------+--------+----------------------------------------------+
3 rows in set (0.01 sec)

mysql> explain select  * from shop where disabled = 0 and shop_title like '%迪卡侬%' group by sid limit 10;
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | shop  | index | INX_sid       | INX_sid | 9       | NULL |   10 | Using where |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)


mysql> select SQL_NO_CACHE distinct sid from shop where disabled = 0 and shop_title like '%迪卡侬%' limit 10;
9 rows in set (0.45 sec)

mysql> select SQL_NO_CACHE * from shop,(select distinct sid from shop where disabled = 0 and shop_title like '%迪卡侬%' limit 10) shopx where shop.sid=shopx.sid group by shop.sid;
...
9 rows in set (0.46 sec)

mysql> select SQL_NO_CACHE  * from shop where disabled = 0 and shop_title like '%迪卡侬%' group by sid limit 10;
...
9 rows in set (4.96 sec)

nginx gzip压缩和gulp js压缩 的效果

-rwxr--r-- 1 forthxu forthxu 262K Jun 16 14:57 jquery-3.2.1.js //原始大小
-rw-r--r-- 1 forthxu forthxu 78K Jun 16 15:02 jquery-3.2.1.js.gz //原始文件gzip后的大小
-rwxr--r-- 1 forthxu forthxu 86K Jun 16 15:15 jquery-3.2.1.min-d6a2dcf9a6.js //javascript压缩后的大小
-rw-r--r-- 1 forthxu forthxu 30K Jun 16 15:16 jquery-3.2.1.min-d6a2dcf9a6.js.gz //javascript压缩后在使用gzip压缩的大小

gzip -c jquery-3.2.1.js > jquery-3.2.1.js.gz

gulp.task('testjs', function() {
    return gulp.src('../../public/test/jquery-3.2.1.js')
            //.pipe(concat('all.js'))// 合并
            .pipe(uglify())// 优化
            .pipe(rename(function(path) {// 重命名
                path.basename+='.min';
            }))
            .pipe(rev())//生成版本号
            .pipe(gulp.dest('../../public/test/'))
});

gzip -c jquery-3.2.1.min-d6a2dcf9a6.js > jquery-3.2.1.min-d6a2dcf9a6.js.gz

CSS实现分隔线中间带文字的方法,取代fieldset-legend

小小分隔线 单标签实现


小小分隔线 巧用色实现


小小分隔线 inline-block实现


小小分隔线 浮动来实现


———————————小小分隔线 字符来实现————————————


<div class="demo-container demo">
<style type="text/css">
.demo{
    width: 600px;
    margin:10px;
}
.line_01{
    padding: 0 20px 0;
    margin: 20px 0;
    line-height: 1px;
    border-left: 200px solid #ddd;
    border-right: 200px solid #ddd;
    text-align: center;
}
.line_02{
    height: 1px;
    border-top: 1px solid #ddd;
    text-align: center;
}
.line_02 span{
    position: relative;
    top: -12px;
    background: #fff;
    padding: 0 20px;
}
.line_03{
    width:600px;
}
.line_03 b{
    background: #ddd;
    margin-top: 4px;
    display: inline-block;
    width: 180px;
    height: 1px;
    _overflow: hidden;
    vertical-align: middle;
}
.line_03 span{
    display: inline-block;
    width: 220px;
    vertical-align: middle;
text-align: center;
}
.line_04{
    width:600px;
}
.line_04{
    overflow: hidden;
    _zoom: 1;
}
.line_04 b{
    background: #ddd;
    margin-top: 12px;
    float: left;
    width: 26%;
    height: 1px;
    _overflow: hidden;
}
.line_04 span{
    padding: 0 10px;
    width: 32%;
    float: left;
    text-align: center;
}
.line_05{
    letter-spacing: -1px;
    color: #ddd;
}
.line_05 span{
    letter-spacing: 0;
    color: #222;
    margin:0 20px;
}
</style>

<div class="line_01">小小分隔线 单标签实现</div>
<br>
<br>
<div class="line_02"><span>小小分隔线 巧用色实现</span></div>
<br>
<br>
<div class="line_03"><b></b><span>小小分隔线 inline-block实现</span><b></b></div>
<br>
<br>
<div class="line_04"><b></b><span>小小分隔线 浮动来实现</span><b></b></div>
<br>
<br>
<div class="line_05">———————————<span>小小分隔线 字符来实现</span>————————————</div>
<br>
<br>
</div>

DNS学习

https://github.com/forthxu/mydns

mydns

DNS查询器,主要用来学习dns协议和c#软件开发。

每当我们在浏览器上敲入任何一个域名访问某个网站的时候,我们都要使用Dns协议进行一次”域名:IP”的查询;作为命令行使用者,与dns有关用的最多的就是Nslookup 命令吧;作为程序员,以c#程序员为例,要得到一个域名的ip大概也是这么一行“System.Net.Dns.GetHostByName(string UriHostName)”。

在这简单使用的背面,很少人会真了解其协议的规则,这也许就是高度封装给程序员带来的一点麻烦吧。下面来了解一下dns协议的内容。

DNS结构:

整个dns分为5个部分,分别为Header、Question、Answer、Authority、Additional。

dns协议头

其中头部的大小是固定的为12字节。这5个部分不是全部都是必须的,在向服务器发送查询请求的时候,只需要前2个。回复的时候也不一定包含5个(按查询的内容和返回的信息而定)。

Header 部分:

header头部分是必须的,无论发送查询或者返回结果都需要该部分,且长度一定,为12字节。结果如下图

dns协议头部

ID:长度为16位,是一个用户发送查询的时候定义的随机数,当服务器返回结果的时候,返回包的ID与用户发送的一致。

QR:长度1位,值0是请求,1是应答。

Opcode:长度4位,值0是标准查询,1是反向查询,2是服务器状态查询。

AA:长度1位,授权应答(Authoritative Answer) - 这个比特位在应答的时候才有意义,指出给出应答的服务器是查询域名的授权解析服务器。

TC:长度1位,截断(TrunCation) - 用来指出报文比允许的长度还要长,导致被截断。

RD:长度1位,期望递归(Recursion Desired) - 这个比特位被请求设置,应答的时候使用的相同的值返回。如果设置了RD,就建议域名服务器进行递归解析,递归查询的支持是可选的。

RA:长度1位,支持递归(Recursion Available) - 这个比特位在应答中设置或取消,用来代表服务器是否支持递归查询。

Z:长度3位,保留值,值为0.

RCode:长度4位,应答码,类似http的stateCode一样,值0没有错误、1格式错误、2服务器错误、3名字错误、4服务器不支持、5拒绝。

QDCount:长度16位,报文请求段中的问题记录数。

ANCount:长度16位,报文回答段中的回答记录数。

NSCOUNT :长度16位,报文授权段中的授权记录数。

ARCOUNT :长度16位,报文附加段中的附加记录数。

Question 部分:

这部分的内容是你要查询的内容。也是必须的。

dns协议question部分

QName:是你要查询的域名,属于不定长字段。他的格式是可变长度数据格式,一般为“长度(1字节)+N字节内容(N由前面的长度定义,不超过63,下面可变数据格式有说明)+~~~+长度0。以一个长度单位N为开始,然后连续的N字节为其内容,然后又是一个N2长度的一字节,然后后面又是N2个字节内容,直到遇到长度为0的长度标记。

假设QName字段的内容是 forth.xu ,则字节内容为:

05 66 6f 72 74 68 02 78 75 0

第一个字节是长度:5,那么接下来的5个字节都是内容66 6f 72 74 68 ,ascii码转过来是“forth”。然后又是长度2,后面2个字节的内容78 75 字母为xu,然后是长度0,表示结束了。最后还要把两段文字组合起来中间加点号成forth.xu。

QType:长度16位,表示查询类型。取值大概如下:

enum QueryType //查询的资源记录类型。

{

A=0x01, //指定计算机 IP 地址。

NS=0x02, //指定用于命名区域的 DNS 名称服务器。

MD=0x03, //指定邮件接收站(此类型已经过时了,使用MX代替)

MF=0x04, //指定邮件中转站(此类型已经过时了,使用MX代替)

CNAME=0x05, //指定用于别名的规范名称。

SOA=0x06, //指定用于 DNS 区域的“起始授权机构”。

MB=0x07, //指定邮箱域名。

MG=0x08, //指定邮件组成员。

MR=0x09, //指定邮件重命名域名。

NULL=0x0A, //指定空的资源记。

WKS=0x0B, //描述已知服务。

PTR=0x0C, //如果查询是 IP 地址,则指定计算机名;否则指定指向其它信息的指针。

HINFO=0x0D, //指定计算机 CPU 以及操作系统类型。

MINFO=0x0E, //指定邮箱或邮件列表信息。

MX=0x0F, //指定邮件交换器。

TXT=0x10, //指定文本信息。

UINFO=0x64, //指定用户信息。

UID=0x65, //指定用户标识符。

GID=0x66, //指定组名的组标识符。

ANY=0xFF //指定所有数据类型。

};

QClass:长度为16位,表示分类。

enum QueryClass //指定信息的协议组。

{

IN=0x01, //指定 Internet 类别。

CSNET=0x02, //指定 CSNET 类别。(已过时)

CHAOS=0x03, //指定 Chaos 类别。

HESIOD=0x04,//指定 MIT Athena Hesiod 类别。

ANY=0xFF //指定任何以前列出的通配符。

};

Answer、Authority、Additional:

接下来的3个结构,格式可以说相同。都是如下图的结构和字段。

dns协议资源结构

Name:回复查询的域名,不定长。 这里的名字和Question结构的名字是一样的,但是为了节省资源,在question结构是这样,在之后的结构中,如果name字段的内容前面有出现了,那么他就不会再浪费空间去重复记录,而是指向某个前面出现了name的位置。如:

在question结构中的name字段的内容为forth.xu,即“05 66 6f 72 74 68 02 78 75 0”。然后在第3个结构中的answer中,第一个字段name的内容也是forth.xu,那么他会指向question中的name地址,让我们去那个地址读name内容。所以此时answer结构的name字段的内容为:

C0 0C

C0:这时不是表示接下来的内容有多长,而是接下来的内容在偏移量中,

0C:十进制是12的意思,就是偏移12个字节。从头开始12位,因为Header结构是固定的12字节,所以偏移0C就是到了Question的Name字段,即上面的“05 66 6f 72 74 68 02 78 75 0”。

Type:同上QType。

Class:同上QClass。

TTL:生存时间。4字节,指示RDATA中的资源记录在缓存的生存时间。

RDLength:资源的长度。

RDdata:资源的内容。

可变长度数据格式 说明:

可变长度有两种内容格式:

长度方式:

1字节长度N + N字节内容 [+ 1字节长度N + N字节内容] + 0x00

N最多不超过63也就是2^6,因为最前面两位用来表示地址方式的偏移量。

地址方式:

第一字节大于等于0xc0开头,表示指针偏移量,所以偏移量的计算其实并不是指c0后面跟着的一个字节,不然一个字节的偏移量最多只有256个位置。

比如偏移量超过255,是300 ,他的小端格式为0x012c,那么他在内存中的表示应该是这样的:0xc12c。

这个300偏移量应该是这样计算的:

300 = 0x012c(小头) = 00000001 00101100(小头) = 00101100 00000001(大头) = 0x2c01(大头)

0xc0 & 0x2c01(大头) = 11000000 & 00101100 00000001 = 00101100 11000001(大头) = 11000001 00101100(小头) = 0xC12C

也就是:

0xc0 & (((0x012c << 8) & 0xff) & (0x012c >> 8)) = 0xC12C

同理反向计算:

((0xC12C & ~0xc0) & 0xff) << 8 & (0xC12C & ~0xc0) >> 8 = 0x012c = 300

需要注意是可变长度的格式有3中结尾方式:

  1. 长度+内容+~+长度0
  2. 偏移标识+偏移量
  3. 长度+内容+~+偏移标识+偏移量

现在来说说这个程序了

我按dns协议的结构把项目分成 MyDnsHeader.cs、MyDnsQuestion.cs、MyDnsRecord.cs 这样的3个大结构。

发送dns请求时只需要构造MyDnsHeader和MyDnsQuestion结构,然后通过GetBytes()函数得到构造好的字节数组,然后通过udp发送出去。然后接受来自服务器的响应,将接收到的字节数组通过Parse(byte[] recvData)方法让3个结构去解析,最后通过这些结构的属性字段获取相应的查询信息。

其中的资源记录,目前能分析 A记录、SOA记录、TXT记录、CNAME记录、MX记录、NS记录。

界面截图:

dns界面截图:

设计和实施 DNS 服务器和客户端服务时可能用到的RFC相关规范
RFC 标题
RFC1034 域名 - 概念和工具
RFC1035 域名 - 实现和规范
RFC1123 Internet 主机 - 应用和支持的要求
RFC1886 支持 IP 版本 6 的 DNS 扩展名
RFC1995 DNS 中的增量区域传输
RFC1996 提示通知区域更改的机制 (DNS NOTIFY)
RFC2136 域名系统中的动态更新 (DNS UPDATE)
RFC2181 对 DNS 规范的说明
RFC2308 DNS 查询的负缓存 (DNS NCACHE)
RFC2535 域名系统安全扩展 (DNSSEC)
RFC2671 DNS 的扩展机制 (EDNS0)
RFC2782 指定服务位置的 DNS RR (DNS SRV)
RFC2930 DNS 的密钥建立 (TKEY RR)
RFC3645 DNS (GSS-TSIG) 密钥事务身分验证的通用安全服务算法
RFC3646 IPv6 (DHCPv6) 动态主机配置协议的 DNS 配置选项

MySQL数据库名、表名、列名、别名、字段值大小写规则

MySQL在Window下数据库名、表名、列名、别名大小写规则不敏感。

MySQL在Linux下数据库名、表名、列名、别名大小写规则是这样的:
  1、数据库名与表名是严格区分大小写的;
  2、表的别名是严格区分大小写的;
  3、列名与列的别名在所有的情况下均是忽略大小写的;
  4、字段值默认字符集情况下是大小写不敏感的。

同时MySQL中数据库名和表名的大小写敏感受参数lower_case_table_names影响,为0时不做处理敏感的地方区分大小写,为1时,表示将转化为小写后存储,查询会做转化,因此不区分大小写,此配置的操作对Window系统的MySQL同样有效。

Linux之所以会有架构敏感,全因为Linux的文件存储系统。数据库名和表名在系统的存储形式分别是文件夹和文件,因此会敏感。

字段值的大小写由mysql的校对规则来控制。提到校对规则,就不得不说字符集。字符集是一套符号和编码,校对规则是在字符集内用于比较字符的一套规则。
一般而言,校对规则以其相关的字符集名开始,通常包括一个语言名,并且以_ci(大小写不敏感)、_cs(大小写敏感)或_bin(二元)结束 。比如 utf8字符集,utf8_general_ci,表示不区分大小写,这个是utf8字符集默认的校对规则;utf8_general_cs表示区分大小写,utf8_bin表示二进制比较,同样也区分大小写 。

关于TCP可靠性的一点思考,借此浅谈应用层协议设计

本文主要讨论如何设计一个可靠的RPC协议。TCP是可靠的传输协议,不会丢包,不会乱序,这是课本上讲述了无数遍的道理。基于TCP的传输理论上来说都是可靠的,但是实际这也得看场景。当我做网络游戏的时候也是一直把它当一个可靠的传输协议来用,从没考虑过TCP丢包的问题。直到当我面临像网络存储、机器学习这样领域时,我发现TCP变得“不可靠”了。

具体来说:

发送方能不能知道已发送的数据对方是不是都收到了?或者,收到了多少?答:不能
如果怀疑对方没收到,有没有办法可以确认对方没有收到? 答:不能
我想发送的是“123”,对方收到的会不会是“1223”? 答:是的,会这样,而且无法避免。
​第一个问题看起来很傻,众所周知TCP有ACK啊,ACK就是用来对方通知接收到了多少个字节的。可是,实际情况是,ACK是操作系统的事儿,它收到ACK后并不会通知用户态的程序。发送的流程是这样的:

应用程序把待发送的数据交给操作系统
操作系统把数据接收到自己的buffer里,接收完成后通知应用程序发送完成
操作系统进行实际的发送操作
操作系统收到对方的ACK
问题来了,假如在执行完第二步之后,网络出现了暂时性故障,TCP连接断了,你该怎么办?如果是网络游戏,这很简单,把用户踢下线,让他重新登录去,活该他网不好。但是如果比较严肃的场合,你当然希望能支持TCP重连。那么问题就来了,应用程序并不知道哪些数据发丢了。

以Windows I/O completion ports举个例子。一般的网络库实现是这样的:在调用WSASend之前,malloc一个WSABuffer,把待发送数据填进去。等到收到操作系统的发送成功的通知后,把buffer释放掉(或者转给下一个Send用)。在这样的设计下,就意味着一旦遇上网络故障,丢失的数据就再也找不回来了。你可以reconnect,但是你没办法resend,因为buffer已经被释放掉了。所以这种管理buffer的方式是一个很失败的设计,释放buffer应当是在收到response之后。

Solution:不要依赖于操作系统的发送成功通知,也不要依赖于TCP的ACK,如果你希望保证对方能收到,那就在应用层设计一个答复消息。再或者说,one-way RPC都是不可靠的,无论传输层是TCP还是UDP,都有可能会丢。​

第二个问题,是设计应用层协议的人很需要考虑的,简单来说,“成功一定是成功但失败不一定是失败”。我想举个例子。假如你现在正在通过网银给房东转账交房租,然后网银客户端说:“网络超时,转账操作可能失败”。你敢重新再转一次吗?我打赌你不敢。

再举个例子,假设你设计了一个分布式文件存储服务。这个服务只有一条“Append”协议:

客户端向服务器发送文件名和二进制data。
服务器把文件打开(不存在则创建),写入数据,然后返回“OK”。中途遇到任何错误则返回“FAIL”
假设你现在有一个20TB的文件,你按照1 GB、1 GB的方式往上传。每发送1 GB,收到OK后,继续发送下1 GB。然后不幸的是中途遇到一个FAIL,你该怎么办?能断点续传吗?NO。因为服务器有可能在写入成功的情况下也返回FAIL(或者网络超时,没有任何回复)。所以你不能重发送未完成的请求。如果你选择从头传,而文件又特别大,那么你可能永远都不会成功。

Solution:采用positioned write。即在客户端发给服务器的请求里加上文件偏移量(offset)。缺点是:若你想要多个客户端同时追加写入同一个文件,那几乎是不可能的。​

​第三个问题:我想发送的是“123”,对方收到的会不会是“1223”?你想要支持重连、重试,那么你得容忍这种情况发生。

Solution:在应用层给每个message标记一个id,让接收者去重即可

接下来讨论下如何关闭连接。简单来说:谁是收到最后一条消息的人,谁来主动关闭tcp 连接。另一方在recv返回0字节之后close,千万不要主动的close。

在协议设计上,分两种情况:

协议是一问一答(类似于HTTP),且发“问”(request)的总是同一方。一方只问,另一方只答
有显式的EOF消息通知对方shutdown。
如果不满足以上两点的任何一点,那么就没有任何一方能判断它收到的消息是不是最后一条,那协议设计有问题,要改!

(p.s. Windows上还有一种方法,就是用半关连接shutdown(SD_SEND)来标志结束,但是操作起来比较复杂,还不如改协议来的快,容易debug)

原文:http://weibo.com/ttarticle/p/show?id=2309404060342857671095
扩展:http://www.ideawu.net/blog/archives/782.html