读《《图解TCP-IP》》有感

读《《图解TCP/IP》》有感

TCP/IP

最近几天读完《《图解TCP/IP》》,收获蛮多,记得上学时读stevens的《《TCP/IP详解》》时那是一个囫囵吞枣,没认真看也看不下去。等有时间再拜读下《《TCP/IP详解》》吧,估计能有不少共鸣。

现在觉得,要想比较透彻理解TCP/IP,还得需要有服务器编程经验,学校应该同时开设《socket编程》》相关课程,最好同一个老师教,可以串讲,不然光理论不实战就是花架子,而且也不容易真正理解TCP/IP协议。

TCP/UDP一览

只有较为透彻的理解了TCP/UDP才知道为啥TCP叫传输控制协议,UDP叫用户数据包协议。TCP在背后帮你做了很多事,连接管理,保存时序,超时重发,拥塞控制,流控;通过mss自动帮你做分片,以减少传输途中路由器分片消耗(IPV6路由器是不会分片的),这叫传输控制协议,突出“控制”二字。而UDP什么都不帮你做,不保证时序,不做超时重传,是需要上层协议来保证,也就是用户来写规则,这也是第一个U(user)的含义;同时UDP没有mss,所以一次性发送数据最好不要太多,以免分片导致性能损耗。那UDP为什么叫用户数据包协议呢,说到数据包就得说说TCP和UDP的面向连接和无连接了。

我们知道TCP是面向连接的,而UDP是无连接的,这就直接导致了TCP会通过三次握手连接服务器,如果服务器端口不对TCP是知道,但UDP不一样,压根不知道服务器段的情况,即使服务器不在线,客户端照样发送数据。

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。也就是说我们接受一次,可能是对方多次发送后的数据集合,也可能是发送一次的部分数据,这都有可能,不能假设是哪种情况。这不经让我想起以前上学时写的简单socket客户端,服务器程序,都是一发一收,我们没封包解包不也正常吗?这就要说说Linux recv系统调用了。

int recv( SOCKET s, char FAR *buf, int len, int flags);
recv执行流程(来自网上)
1.recv等待s发送缓冲区数据发送完毕,如果中途出现网络错误,recv函数返回SOCKET_ERROR

2.recv检查s的接受缓冲区,如果接受缓冲区没有数据或者
正在接受数据,那么recv一直等待,直到把数据接受完毕。当接受完毕后,recv会把s接受缓冲区的数据copy到buf中(缓冲区数据可能大于buf长度,所以需要多次调用recv。recv仅仅是copy数据,真正接受数据是底层协议来完成的)

那这就好理解了,由于我们客户端通常是先send再recv,再发送完成之后recv会等待,而且我们数据量比较小,每次间隔比较大,所以有数据了,recv就返回了。当然这也是存在隐患的,发送数据大,一次recv可能接受不了,那就需要多次recv,这时就需要封包了,不然你怎么知道什么时候该返回。

就拿Http来说,服务端通过Content-Length和chunked编码来封包,让浏览器解析数据。当然http1.0其实Content-Length是非必须的,因为每次发送完数据后,服务端都会close,客户端recv会返回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就被淘汰了,通过交换机的端口转发实现了全双工通信。不知道是当初老师的问题还是我太弱,现在才明白应用场景。现在的无线也是半双工的,是CSMA/CD的改良版,叫CSMA/CA。

你可能感兴趣的:(socket,TCP-IP)