最近在团队分享中,关于长链接的服务中,提到了一个TIMEWAIT的解决方案,具体关于TIMEWAIT具体产生的原因,原理,对系统产生的影响,会在其他的文章中给出具体的说明和解释,这个简单介绍一下概括性的内容。

 

简介

  1. 只有断开的发起端才会进入到TIME_WAIT的状态。

  2. TIME_WAIT状态的socket会占用系统的内存和CPU资源,占用的资源相对比较有限,比如内存每个也就是4K(没有具体测试,Net/3实现中,这三个的和位264字节,TCP/IP详解卷三中描述),整体在1万的情况下,也才40M的内存浪费,CPU是会有定期的轮询检查以及端口分配时的消耗,基本可以忽略。

综上所述,TIME_WAIT在一定的量级上时基本可以不用管的,尤其时笔者所在的长连接的场景,正常情况下就更难出现。如果想要做更好的优化,下面有两个思路,

  1. 通过加速TIME_WAIT的回收

  2. 通过RST的方式替代之前的FIN报文进行四次握手,实现一次就能断开释放掉资源。

本篇文章主要介绍通过RST的方式。

如何使用RST来解决TCP断开问题

Linger的原理介绍

  1. SO_LINGER controls the action taken when unsent messages are queued on socket and a close(2) is performed. If the socket promises reliable delivery of data and SO_LINGER is set, the system will block the process on the close(2) attempt until it is able to transmit the data or until it decides it is unable to deliver the information (a timeout period, termed the linger interval, is specified in seconds in the setsockopt() system call when SO_LINGER is requested).

Linger选项的定义了当socket中还有未发送的数据的情况下,执行了close的动作之后,后续会由什么样的表现。如果开启的Linger选项的情况,close会阻塞直到系统传输万所有的数据,或者到达指定超时时间,执行了close之后,再收到对端的消息的时候,会直接回复RST报文。

实现RST的方式就是通过开启Linger选项,并且超时时间设置为0,这样的话,就能直接丢弃掉没有发送的数据,发出RST报文。

golang版本的代码验证

客户端代码

  1. package main

  2.  

  3. import (

  4. "flag"

  5. "fmt"

  6. "net"

  7. "os"

  8. )

  9.  

  10. var host = flag.String("host", "localhost", "host")

  11. var port = flag.String("port", "3333", "port")

  12.  

  13. func main() {

  14. flag.Parse()

  15. conn, err := net.Dial("tcp", *host+":"+*port)

  16. if err != nil {

  17. fmt.Println("Error connecting:", err)

  18. os.Exit(1)

  19.  

  20. }

  21. defer conn.Close()

  22. fmt.Println("Connecting to " + *host + ":" + *port)

  23. done := make(chan string)

  24. go handleWrite(conn, done)

  25. go handleRead(conn, done)

  26. fmt.Println(<-done)

  27. fmt.Println(<-done)

  28. }

  29.  

  30. func handleWrite(conn net.Conn, done chan string) {

  31. _, e := conn.Write([]byte("hello \r\n"))

  32. if e != nil {

  33. fmt.Println("Error to send message because of ", e.Error())

  34. }

  35. done <- "Sent"

  36.  

  37. }

  38. func handleRead(conn net.Conn, done chan string) {

  39. buf := make([]byte, 1024)

  40. reqLen, err := conn.Read(buf)

  41. if err != nil {

  42. fmt.Println("Error to read message because of ", err)

  43. done <- "Read Error"

  44. return

  45. }

  46. fmt.Println(string(buf[:reqLen-1]))

  47. done <- "Read"

  48.  

  49. }

服务端代码

  1. package main

  2.  

  3. import (

  4. "flag"

  5. "fmt"

  6. "net"

  7. "os"

  8. )

  9.  

  10. var host = flag.String("host", "", "host")

  11. var port = flag.String("port", "3333", "port")

  12.  

  13. func main() {

  14. flag.Parse()

  15. var l net.Listener

  16. var err error

  17. l, err = net.Listen("tcp", *host+":"+*port)

  18. if err != nil {

  19. fmt.Println("Error listening:", err)

  20. os.Exit(1)

  21.  

  22. }

  23. // defer l.Close()

  24. fmt.Println("Listening on " + *host + ":" + *port)

  25. for {

  26. conn, err := l.Accept()

  27. if err != nil {

  28. fmt.Println("Error accepting: ", err)

  29. os.Exit(1)

  30.  

  31. }

  32. //logs an incoming message

  33. fmt.Printf("Received message %s -> %s \n", conn.RemoteAddr(), conn.LocalAddr())

  34. // Handle connections in a new goroutine.

  35. go handleRequest(conn)

  36.  

  37. }

  38.  

  39. }

  40. func handleRequest(conn net.Conn) {

  41. defer conn.Close()

  42. buf := make([]byte, 1024)

  43. reqLen, err := conn.Read(buf)

  44. if err != nil {

  45. fmt.Println("Error to read message because of ", err)

  46. return

  47. }

  48. conn.Write(buf[:reqLen-1])

  49. fmt.Println(string(buf[:reqLen-1]))

  50.  

  51. tcpConn, ok := conn.(*net.TCPConn)

  52. if !ok {

  53. fmt.Println("this is error", ok)

  54. }

  55.  

  56. tcpConn.SetLinger(0)

  57. }

通过客户端的代码中添加了如下代码

  1. tcpConn.SetLinger(0)

完成设置Linger选项的功能,关于需要转换相应的类型,是因为对应的方法是TCPConn而不是net.Conn的原因。

正常TCP关闭过程

执行结果的抓包数据如下所示

如何使用RST来解决TCP断开问题_服务端

通过报文可以看到,服务端在读取写入数据之后执行了正常的TCP四次挥手的关闭过程。

RST方式关闭过程

执行结果的抓包数据如下所示

如何使用RST来解决TCP断开问题_服务端_02

通过报文可以看到,服务端在读取写入数据之后直接发送RST的报文终止整个的连接过程。

总结

通过以上的代码实现,以及抓包对报文的分析,可以看到通过Linger选项的设置,能够实现预期的通过RST的方式替代原有的FIN的关闭方式。对于Linger选项在各种实现方式下的不同表现,以及相应的限制,后续会持续的跟进。

RESET TCP and Linger

如何使用RST来解决TCP断开问题_服务端_03