首页 > 其他分享 >玩转TCP

玩转TCP

时间:2022-10-11 20:25:40浏览次数:33  
标签:return err nil 报文 TCP 玩转 服务端

玩转TCP

目前已经有了Netty基础,正在学习Go的net包,以此出发进行TCP的学习。

沾包和半包

当不存在任何的处理方式的时候

  • 一份数据可能会超出MSS,那这样可能就会超出我们的一个包的范围,那就会导致半包的出现。
  • 一份数据太小了,所以TCP没想着直接给它传出去,性价比太低,所以和其他的一起传过去了,如果没有区分的办法,这样就会导致沾包
  • 说道沾包就不得不提到Nagle算法

Nagle算法

由于TCP觉得说单独发一份很小的数据,太亏了,所以他想着堆积着很多数据的时候才发送出去。
具体策略为:

  • 如果现在没有已发送但没有接受到的数据,直接发送
  • 如果现在有已发送但没有接受到的数据,就等待收到这个数据,或者TCP包达到了MTU大小,一起发出去
  • 在接受的时候,又有一种东西叫延迟确认

延迟确认

延迟确认的意思就是,TCP也觉得单独发一个ACK太亏了,想把数据一起发送出去,所以它要等待一下,看看有没有附带的数据一起发送出去,所以它有一个延迟确认时间。

  • 最大延迟确认时间:超过了这个时间还没有数据就发送
  • 最小延迟确认时间:如果到了这个时间,有数据就发送,没有就等着最大延迟确认

如何解决沾包和半包

在Netty中解决

在Netty中,加入了几个编码解码器,就约定俗成的对这些数据进行了拆分,知道了哦,这一块有这个结束符号,我可以知道这是属于这份数据的。
image

Go中进行TCP连接

Go的TCP服务端

package main

import (
	"bufio"
	"fmt"
	"io"
	"learngo/tcp/proto"
	"net"
)

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	for {
		msg, err := proto.Decode(reader)
		if err == io.EOF {
			return
		}
		if err != nil {
			fmt.Println("decode message, err: ", err)
			return
		}
		fmt.Println("收到消息: ", msg)
	}
}

func main() {
	fmt.Println("服务器监听开始")
	listen, err := net.Listen("tcp", "127.0.0.1:30000")
	if err != nil {
		fmt.Println("listen failed : ", err)
		return
	}
	defer listen.Close()
	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		go process(conn)
	}
}

Go的TCP客户端

package main

import (
	"fmt"
	"learngo/tcp/proto"
	"net"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:30000")
	if err != nil {
		fmt.Println("connected failed: ", err)
		return
	}
	defer conn.Close()
	for i := 0; i < 20; i++ {
		msg := "Hello, hi, im your daddy do you know"
		data, err := proto.Encode(msg)
		if err != nil {
			fmt.Println("encode failed :", err)
			return
		}
		conn.Write(data)
	}
}

编码,消息头带上消息长度

package proto

import (
	"bufio"
	"bytes"
	"encoding/binary"
)

func Encode(message string) ([]byte, error) {
	// 读取消息的长度
	var length = int32(len(message)) //32位的int为4B
	var pkg = new(bytes.Buffer)
	// 把长度先写到消息头里面
	err := binary.Write(pkg, binary.LittleEndian, length)
	if err != nil {
		return nil, err
	}
	// 写入消息
	err = binary.Write(pkg, binary.LittleEndian, []byte(message))
	if err != nil {
		return nil, err
	}
	return pkg.Bytes(), nil
}

func Decode(reader *bufio.Reader) (string, error) {
	// 读一下消息的长度
	lengthByte, _ := reader.Peek(4) // 32位int是4字节
	lengthBuff := bytes.NewBuffer(lengthByte)
	var length int32
	err := binary.Read(lengthBuff, binary.LittleEndian, &length)
	if err != nil {
		return "", err
	}
	// 出错了,没有这么多内容
	if int32(reader.Buffered()) < length+4 {
		return "", err
	}
	pkg := make([]byte, int(4+length))
	_, err = reader.Read(pkg)
	if err != nil {
		return "", err
	}
	return string(pkg[4:]), nil
}

TCP连接-三次握手

三次握手真的也是特别经典的面试题了。
三次握手过程:
image

三次握手就是一个双重确认!
问题:

  • 为什么两次挥手不行
  • 从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题,第三次就不再是SYN包

为什么一定是三次握手

接下来,以三个方面分析三次握手的原因:

三次握手才可以阻止重复历史连接的初始化(主要原因)
三次握手才可以同步双方的初始序列号
三次握手才可以避免资源浪费

阻止重复历史连接

假设客户端先发了一个SYN给服务端,这时候这个客户端挂了,并且这个SYN也由于网络环境没有发到服务端
然后客户端重启了,这时候它有发SYN过去,此时两个SYN的seq_num是不同的。如果只有两次连接,服务端遇到了之前的SYN,发送了一个ACK过去,这时候客户端发现了不对劲,但是连接已经建立起来了,没办法了,所以需要第三次去判断,这个服务端返回的连接对不对。是不是历史重复连接。

同步序列号

关键就在于这个ack值,每次tcp连接都是靠ack值告诉对面我期望接受到的seq_num是什么,这样可以确保连接同步。

资源消耗

由于第一次握手可能会导致重发,而也许之前的报文没有丢,这样每次没丢的报文,就可能会导致新建一次连接,新建连接是要耗费资源的,没必要

TCP断开连接-四次挥手

image

什么时候是三次挥手/CloseWait的必要的吗

  • 当服务端没有东西要发送的时候,或者是close暴力处理,直接让客户端没有接受和发送能力了,shutdown就不会。然后开启了TCP延迟发送,这时候他可能就会导致只有三次挥手

TimeWait状态

  • 让所有的报文都死掉,保证这次连接的报文不会影响到下一次
  • 确保ack报文让服务端能够接受到

TCP某一方异常断开会发生什么

回想到锁

如果一个线程获取到了锁,但是它由于某种卡了,这时候设置好了心跳检测,觉得它故障了,所以把锁释放了。然后B去获得了锁,但是这时候A其实没坏,它回过来把B好不容易获得的锁给释放了。
所以当“异常断开”的时候,怎么处理也是一门学问。

TCP的处理

客户端崩溃

TCP的保活机制:
如果开启了 TCP 保活,需要考虑以下几种情况:

第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。

第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。

第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

这就关乎于心跳检测了,服务器会去检测客户端是不是还活着,设置了一个keepalive_timeout,超过了这个时间还不反应,那就说明它挂了,断开连接

服务端崩溃

服务端崩溃就比较有意思了。
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP四次挥手的过程。

我自己做了个实验,使用 kill -9 来模拟进程崩溃的情况,发现在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手。

标签:return,err,nil,报文,TCP,玩转,服务端
From: https://www.cnblogs.com/azxx/p/16782231.html

相关文章