读《《图解TCP/IP》》有感
TCP/IP
近期几天读完《《图解TCP/IP》》,收获蛮多,记得上学时读stevens的《《TCP/IP具体解释》》时那是一个囫囵吞枣,没认真看也看不下去。等有时间再拜读下《《TCP/IP具体解释》》吧,预计能有不少共鸣。
如今认为。要想比較透彻理解TCP/IP,还得须要有server编程经验,学校应该同一时候开设《socket编程》》相关课程,最好同一个老师教,能够串讲,不然光理论不实战就是花架子,并且也不easy真正理解TCP/IP协议。
TCP/UDP一览
仅仅有较为透彻的理解了TCP/UDP才知道为啥TCP叫传输控制协议。UDP叫用户数据包协议。TCP在背后帮你做了非常多事,连接管理,保存时序,超时重发,拥塞控制。流控。通过mss自己主动帮你做分片,以降低传输途中路由器分片消耗(IPV6路由器是不会分片的),这叫传输控制协议,突出“控制”二字。而UDP什么都不帮你做,不保证时序。不做超时重传,是须要上层协议来保证,也就是用户来写规则,这也是第一个U(user)的含义。同一时候UDP没有mss,所以一次性发送数据最好不要太多。以免分片导致性能损耗。
那UDP为什么叫用户数据包协议呢。说到数据包就得说说TCP和UDP的面向连接和无连接了。
我们知道TCP是面向连接的,而UDP是无连接的。这就直接导致了TCP会通过三次握手连接server,假设serverport不正确TCP是知道,但UDP不一样。压根不知道server段的情况。即使server不在线,client照样发送数据。
In [22]: import socket
In [23]: s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
In [24]: s.sendto("hello world",("127.0.0.1",9999))
Out[24]: 11
没有服务端。我们照样发送数据,sendto()照样返回。假设你之前不知道。这会让你大吃一惊,这也正说明了UDP不做数据包确认。
TCP传输的是流式数据,也就是说中间是没有分段的,这也就导致了我们常有的粘包问题,所以我们须要封包和解包,常见的都是字节长度+数据的方式,如websocket。也就是说我们接受一次,可能是对方多次发送后的数据集合,也可能是发送一次的部分数据,这都有可能,不能假设是哪种情况。这不经让我想起曾经上学时写的简单socketclient,server程序。都是一发一收。我们没封包解包不也正常吗?这就要说说Linux recv系统调用了。
int recv( SOCKET s, char FAR *buf, int len, int flags);
recv运行流程(来自网上)
1.recv等待s发送缓冲区数据发送完成,假设中途出现网络错误,recv函数返回SOCKET_ERROR2.recv检查s的接受缓冲区。假设接受缓冲区没有数据或者
正在接受数据。那么recv一直等待,直到把数据接受完成。当接受完成后,recv会把s接受缓冲区的数据copy到buf中(缓冲区数据可能大于buf长度。所以须要多次调用recv。recv仅仅是copy数据,真正接受数据是底层协议来完成的)
那这就好理解了,因为我们client一般是先send再recv,再发送完成之后recv会等待,并且我们数据量比較小,每次间隔比較大。所以有数据了,recv就返回了。当然这也是存在隐患的,发送数据大。一次recv可能接受不了。那就须要多次recv。这时就须要封包了,不然你怎么知道什么时候该返回。
就拿Http来说,服务端通过Content-Length和chunked编码来封包,让浏览器解析数据。
当然http1.0事实上Content-Length是非必须的,因为每次发送完数据后。服务端都会close,clientrecv会返回0,这时候也就知道数据结束了,也就是close作为了EOF标志。一帮下载都是通过这样的方式,所以也不会有粘包。
而UDP是不会出现粘包的,接收肯定能完整接收到发送的数据,世界瞬间清净了,这也是UDP**数据包**的概念,一个一个的数据包,接收的都是完整的数据包。
校验和
TCP/IP中校验和校验算法都几乎相同,也就是所谓的
反码求和
步骤例如以下
1.将校验和置为0
2.将数据转换为16字节整形,不足补零,求和(採用32位加法,将高16位和低16位相加,再将可能产生的进位与低16位相加)
3.和求反
最后发送者将校验和替换为求反的结果,将数据发给接受者,接受者忽略第一步採用相同算法。假设最后结果不为0,将丢弃该数据包。
手痒了一下。于是用golang模拟了下
//校验和算法
//对每16位(2字节)进行二进制反码求和,反码求和的意思是先对每16位求和。再将得到的和转为反码
func CheckSum(data []byte) uint16 {
//padding 16
all := len(data) / 2
if all*2 < len(data) {
all++
data = append(data, 0)
}
u16 := (*[0xffff]uint16)(unsafe.Pointer(&data[0]))[:all]
var sum uint32
for _, u := range u16 {
sum += uint32(u)
}
sum = (sum >> 16) + (sum & 0xffff) //把高位的进位,加到低八位
sum += sum >> 16 //前一步可能有进位
return (uint16)(^sum)
}
func main() {
check := make([]byte, 2)
data := []byte("tcp/ip checksum test1")
//首位作为校验和字段
all := []byte(string(check) + string(data))
sum := CheckSum(all)
fmt.Println(all, sum)
//校验和写入首位
copy(all, (*[2]byte)(unsafe.Pointer(&sum))[:])
fmt.Print(all, CheckSum(all))
}
结果:
[0 0 116 99 112 47 105 112 32 99 104 101 99 107 115 117 109 32 116 101 115 116 49] 22732
[204 88 116 99 112 47 105 112 32 99 104 101 99 107 115 117 109 32 116 101 115 116 49] 0
我们发现最后的确是0。说明我们算法没问题,至于为什么应该是0,小伙伴们想想就知道了。
TCP序列号与滑动窗体
以下说说TCP的seq,ack,在两方通信时seq開始是随机生成的,
假设A開始seq100,ack 1,发送数据size 100
通信 | seq | ack | size |
---|---|---|---|
A->B | 100 | 1 | 10 |
B->A | 200 | 111 | 0 |
A->B | 111 | 201 | 1000 |
B->A | 201 | 1112 | 0 |
1.A->B 初始随机seq 100,ack 1,发送数据10字节
2.B->A 初始睡觉seq200,告诉A下一个发送111。前面的数据都收到了,因为B仅仅是回应包 size 0
3.A->B A收到B的回应。看到ack为111。知道前面的10个自己都已经成功发送了。所以seq为111,发送1000个字节,并回应B下一个開始发送201
4.B->A ack为1000+111+1。告诉A下次从1112開始发送
仅仅要对方回应了ack。那么就代表ack之前的数据都已经成功接收了。这是对方就能够把自己缓冲区的相应数据给清空了,不然须要保存以待重传。因为每次都单项传输。吞吐量有点低,所以TCP引用了滑动窗体。也就是在没有明确收到ack之前能够连续发送w个包。w就是滑动窗体的大小。因为引用了滑动窗体。有些ack丢失也没有关系。仅仅要收到了兴许的ack确认就代表之前的数据都已经成功接收了。
CSMA/CD
通信专业的对CSMA/CD肯定都不陌生。我发现老师特别喜欢说这玩意,但我曾经始终不明确这玩意有啥用?事实上CSMA/CD是用于共享网络的,也就是通过hub或同轴电缆等连接的总线型或星型拓扑结构。因为这东西我们基本都没见过(过时了),因为须要冲突检查所以是半双工通信。当交换机出来时,CSMA/CD就被淘汰了。通过交换机的port转发实现了全双工通信。不知道是当初老师的问题还是我太弱,如今才明确应用场景。
如今的无线也是半双工的。是CSMA/CD的改良版,叫CSMA/CA。