>  기사  >  活久见!TCP两次挥手,你见过吗?那四次握手呢?

活久见!TCP两次挥手,你见过吗?那四次握手呢?

Go语言进阶学习
Go语言进阶学习앞으로
2023-07-24 17:18:441319검색

우리 모두는 TCP가 연결 지향적이고 안정적인 바이트 스트림 기반 전송 계층 통신 프로토콜이라는 것을 알고 있습니다.

活久见!TCP两次挥手,你见过吗?那四次握手呢?
TCP란

여기서 말하는 "연결 지향"이란 연결을 설정하고, 연결을 사용하고, 연결을 해제해야 한다는 의미입니다.

연결 설정은 잘 알려진 TCP 3방향 핸드셰이크를 의미합니다.

그리고 연결을 사용하여 데이터 전송은 1회의 전송과 1회의 확인의 형태로 수행됩니다.

또 하나는 Release Connection인데, 이는 우리의 공통 TCP 4파동입니다.

TCP 웨이브 4번 익숙하실 텐데, wave 3번을 보신 적 있으신가요? 그리고 두 번 흔들기는 어떨까요?

다 보셨나요? 네 번의 악수는 어떻습니까?

오늘의 주제는 단순한 호기심이나 상식에 관한 것이 아닙니다.

네 가지 물결부터 시작하여 실용적인 지식을 얻으세요.


TCP Four Waves

TCP Four Waves에 대한 간략한 리뷰입니다.

活久见!TCP两次挥手,你见过吗?那四次握手呢?
일반적인 상황에서는 TCP가 네 번

파됩니다. 데이터 전송이 완료되면 클라이언트든 서버든 적극적으로 4개의 웨이브를 시작하여 연결을 해제할 수 있습니다.

그림처럼 4개의 웨이브가 클라이언트에 의해 시작된다고 가정하면 active party입니다. 서버는 클라이언트의 웨이브 요청을 수동적으로 수신하는데, 이를 passive party라고 합니다.

클라이언트와 서버는 처음에 설정됨 상태. ESTABLISHED状态。

第一次挥手:一般情况下,主动方执行close()shutdown()方法,会发个FIN报文出来,表示"我不再发送数据了"。

第二次挥手:在收到主动方的FIN报文后,被动方立马回应一个ACK,意思是"我收到你的FIN了,也知道你不再发数据了"。

上面提到的是主动方不再发送数据了。但如果这时候,被动方还有数据要发,那就继续发。注意,虽然第二次和第三次挥手之间,被动方是能发数据到主动方的,但主动方能不能正常收就不一定了,这个待会说。

第三次挥手:在被动方在感知到第二次挥手之后,会做了一系列的收尾工作,最后也调用一个 close(), 这时候就会发出第三次挥手的 FIN-ACK

🎜첫 번째 웨이브🎜: 일반적인 상황에서 활성 파티는 close() 또는 FIN 메시지가 나오며 "🎜더 이상 데이터를 보내지 않습니다🎜"라는 메시지가 나타납니다. 🎜🎜🎜두 번째 웨이브🎜: 활성 파티의 FIN 메시지 후 수동적 당사자는 즉시 ACK는 "FIN을 받았고 더 이상 데이터를 보내지 않을 것이라는 것도 알고 있습니다."를 의미합니다. 🎜🎜위에서 언급한 내용은 🎜active party🎜가 더 이상 데이터를 보내지 않는다는 것입니다. 하지만 이때 🎜passive party🎜에 보낼 데이터가 아직 남아 있다면 계속해서 보내세요. 참고로, 2차 웨이브와 3차 웨이브 사이에는 패시브 측에서 액티브 측으로 데이터를 보낼 수 있지만, 액티브 측에서 정상적으로 수신할 수 있을지는 확실하지 않습니다. 🎜🎜🎜세 번째 물결🎜: 패시브 파티가 두 번째 물결을 감지한 후 일련의 마무리 작업을 수행하고 마지막으로 닫기( ), 세 번째 웨이브가 발행됩니다. FIN-ACK. 🎜

네 번째 물결: 활성 파티는 ACK, 수신됨을 의미합니다. ACK,意思是收到了。

其中第一次挥手和第三次挥手,都是我们在应用程序中主动触发的(比如调用close()方法),也就是我们平时写代码需要关注的地方。

第二和第四次挥手,都是内核协议栈自动帮我们完成的,我们写代码的时候碰不到这地方,因此也不需要太关心。

另外不管是主动还是被动,每方发出了一个 FIN 和一个ACK 。也收到了一个 FIN 和一个ACK这一点大家关注下,待会还会提到。

FIN一定要程序执行close()或shutdown()才能发出吗?

不一定。一般情况下,通过对socket执行 close()shutdown() 方法会发出FIN。但实际上,只要应用程序退出,不管是主动退出,还是被动退出(因为一些莫名其妙的原因被kill了), 都会发出 FIN

첫 번째 물결과 세 번째 물결은 모두 애플리케이션에서 적극적으로 트리거됩니다(예: close()method), 이것이 우리가 하는 것입니다. 코드를 작성할 때 주의할 점이 있습니다. 🎜🎜두 번째와 네 번째 웨이브는 커널 프로토콜 스택에 의해 자동으로 수행됩니다. 코드를 작성할 때 이 부분은 건드릴 수 없으므로 크게 신경 쓸 필요가 없습니다. 🎜🎜또한, 활성 또는 수동 여부에 관계없이 각 당사자는 FIN확인. 또한 FINACK . 🎜 이 점 꼭 주의하시고 나중에 언급하겠습니다. 🎜🎜

프로그램이 close() 또는 shutdown()을 실행한 후에 FIN을 발행해야 합니까?

🎜🎜 반드시🎜는 아닙니다. 일반적으로 소켓실행close() 또는 shutdown() 메소드는 FIN. 그러나 실제로 애플리케이션이 종료되는 한 🎜active🎜exit 또는 🎜passive🎜exit인지 여부는 알 수 없는 이유로 인해 kill ), 🎜 🎜 82, 82호);배경: rgb(248, 248, 248);">FIN. 🎜

FIN은 "더 이상 데이터를 보내지 않습니다"를 의미하므로 shutdown() 关闭读不会给对方发FIN, 关闭写才会发FIN。


如果机器上FIN-WAIT-2状态特别多,是为什么

根据上面的四次挥手图,可以看出,FIN-WAIT-2主动方那边的状态。

处于这个状态的程序,一直在等第三次挥手FIN。而第三次挥手需要由被动方在代码里执行close() 发出。

因此当机器上FIN-WAIT-2状态特别多,那一般来说,另外一台机器上会有大量的 CLOSE_WAIT。需要检查有大量的 CLOSE_WAIT的那台机器,为什么迟迟不愿调用close()关闭连接。

所以,如果机器上FIN-WAIT-2状态特别多,一般是因为对端一直不执行close() 메서드는 세 번째 웨이브를 발행합니다.

活久见!TCP两次挥手,你见过吗?那四次握手呢?
FIN-WAIT-2가 많은 이유


닫은 후 활성 당사자가 수신한 데이터는 어떻게 처리됩니까?

"코드가 성공적으로 전송되면 데이터가 전송됩니까?" 전에 작성된 기사. 》, 소스 코드의 관점에서 일반적인 상황에서 프로그램은 close(); close()的时候;

  • 如果当前连接对应的socket接收缓冲区有数据,会发RST

  • 如果发送缓冲区有数据,那会等待发送完,再发第一次挥手的FIN

大家知道,TCP是全双工通信,意思是发送数据的同时,还可以接收数据。

Close()的含义是,此时要同时关闭发送和接收消息的功能。

也就是说,虽然理论上,第二次和第三次挥手之间,被动方是可以传数据给主动方的。

但如果 主动方的四次挥手是通过 close() 触发的,那主动方是不会去收这个消息的。而且还会回一个 RST

  • 현재 연결이 소켓

    수신 버퍼
    에 데이터가 있습니다. RST. 活久见!TCP两次挥手,你见过吗?那四次握手呢?
  • 전송 버퍼
    에 데이터가 있으면 대기합니다. 전송 후 첫 번째 웨이브 FIN.

우리 모두 알다시피 TCP는
전이중 통신

입니다. 즉, 데이터를 보내는 동시에 데이터를 받을 수도 있습니다. 🎜🎜Close()는 메시지 보내기 및 받기 기능을 동시에 꺼야 함을 의미합니다. 🎜🎜즉, 🎜이론적으로는🎜 2차와 3차 웨이브 사이에도 패시브 당사자가 액티브 당사자에게 데이터를 전송할 수 있습니다. 🎜🎜그러나 활성 파티의 4파동이 통과되면close()가 트리거되면 활성 파티는 이 메시지를 받지 않습니다. 그리고 RST. 이 연결을 직접 종료하세요. 🎜🎜🎜🎜close()는 TCP 4파동을 트리거합니다🎜🎜🎜🎜🎜

2차 웨이브와 3차 웨이브 간에 데이터를 전송할 수 없나요?

둘 다 아닙니다. 앞에서 언급했듯이닫기()는 동시에메시지 보내기 및 받기 기능을 끄세요. Close()的含义是,要同时关闭发送和接收消息的功能。

那如果能做到只关闭发送消息不关闭接收消息的功能,那就能继续收消息了。这种 half-close 的功能,通过调用shutdown() 方法就能做到。

int shutdown(int sock, int howto);

其中 howto 为断开方式。有以下取值:

  • SHUT_RD:关闭读。这时应用层不应该再尝试接收数据,内核协议栈中就算接收缓冲区收到数据也会被丢弃。

  • SHUT_WR:关闭写。如果发送缓冲区中还有数据没发,会将将数据传递到目标主机。

  • SHUT_RDWR:关闭读和写。相当于close()

    그런 다음 메시지 보내기만 끄면, 메시지 수신 기능을 끄지 않으면 메시지를 계속 받을 수 있습니다. 이반 닫기 함수, shutdown() 메소드 완료됩니다.
  • int send( SOCKET s,const char* buf,int len,int flags);

    여기서 방법은 연결 해제 방법입니다. 다음 값을 사용할 수 있습니다:

  • SHUT_RD: 읽기를 닫습니다. 이때 애플리케이션 계층은 더 이상 데이터 수신을 시도해서는 안 됩니다. 수신 버퍼가 커널 프로토콜 스택에서 데이터를 수신하더라도 폐기됩니다.

活久见!TCP两次挥手,你见过吗?那四次握手呢?
  • SHUT_WR: 쓰기를 끕니다. 전송 버퍼에 전송되지 않은 데이터가 남아 있으면 해당 데이터가 대상 호스트로 전달됩니다.

  • SHUT_RDWR: 읽기 및 쓰기를 끕니다. 닫기().


    🎜🎜🎜🎜종료로 인해 TCP 4파동🎜🎜🎜🎜🎜

    怎么知道对端socket执行了close还是shutdown

    不管主动关闭方调用的是close()还是shutdown(),对于被动方来说,收到的就只有一个FIN

    被动关闭方就懵了,"我怎么知道对方让不让我继续发数据?"

    活久见!TCP两次挥手,你见过吗?那四次握手呢?

    其实,大可不必纠结,该发就发。

    第二次挥手和第三次挥手之间,如果被动关闭方想发数据,那么在代码层面上,就是执行了 send() 方法。

    int send( SOCKET s,const char* buf,int len,int flags);

    send() 会把数据拷贝到本机的发送缓冲区。如果发送缓冲区没出问题,都能拷贝进去,所以正常情况下,send()一般都会返回成功。

    ![tcp_sendmsg 逻辑](https://cdn.jsdelivr.net/gh/xiaobaiTech/image/tcp_sendmsg 逻辑.png)

    然后被动方内核协议栈会把数据发给主动关闭方。

    • 如果上一次主动关闭方调用的是shutdown(socket_fd, SHUT_WR)。那此时,主动关闭方不再发送消息,但能接收被动方的消息,一切如常,皆大欢喜。

    • 마지막 activecloser 호출이 닫기(). 그런 다음 close()。那主动方在收到被动方的数据后会直接丢弃,然后回一个RST

    针对第二种情况。

    被动方内核协议栈收到了RST,会把连接关闭。但内核连接关闭了,应用层也不知道(除非被通知)。

    此时被动方应用层接下来的操作,无非就是读或写

    • 如果是读,则会返回RST的报错,也就是我们常见的Connection reset by peer

    • 如果是写,那么程序会产生SIGPIPE信号,应用层代码可以捕获并处理信号,如果不处理,则默认情况下进程会终止,异常退出。


    总结一下,当被动关闭方 recv() 返回EOF时,说明主动方通过 close()shutdown(fd, SHUT_WR)활성 측

    은 🎜수동 측🎜의 데이터를 직접 🎜discard🎜하고 RST. 🎜🎜🎜두 번째 상황입니다. 🎜🎜패시브 측면🎜커널 스택🎜수신 RST는 연결을 종료합니다. 그러나 커널 연결은 닫혀 있으며 애플리케이션 계층은 (알려지지 않는 한) 알지 못합니다. 🎜🎜이때 패시브 🎜애플리케이션 계층🎜의 다음 작업은 🎜읽기 또는 쓰기🎜에 지나지 않습니다. 🎜
      🎜🎜읽으면 RST 오류입니다. 이는 일반적인 피어에 의한 연결 재설정. 🎜🎜🎜🎜작성하면 프로그램은 SIGPIPE 신호, 애플리케이션 계층 코드는 처리되지 않은 경우 신호를 캡처하고 처리할 수 있습니다. 기본값 프로세스가 종료되고 비정상적으로 종료됩니다. 🎜🎜
    🎜
    🎜🎜🎜요약하자면🎜, 수동적 닫기 방법recv() 반환EOF, 활성 파티가 close() 또는 shutdown(fd, SHUT_WR)은 첫 번째 웨이브를 시작합니다. 🎜

    如果此时被动方执行两次 send()send()

    • 第一次send(), 一般会成功返回。

    • 第二次send()时。如果主动方是通过 shutdown(fd, SHUT_WR) 发起的第一次挥手,那此时send()还是会成功。如果主动方通过 close()发起的第一次挥手,那此时会产生SIGPIPE信号,进程默认会终止,异常退出。不想异常退出的话,记得捕获处理这个信号。


    如果被动方一直不发第三次挥手,会怎么样

    第三次挥手,是由被动方主动触发的,比如调用close()

    如果由于代码错误或者其他一些原因,被动方就是不执行第三次挥手。

    这时候,主动方会根据自身第一次挥手的时候用的是 close() 还是 shutdown(fd, SHUT_WR)

    • 第一次send(), 一般会成功返回。🎜
    • 🎜第二次send() 时。如果主动方是通过종료(fd, SHUT_WR) 发起적第一次挥手,那此时보내기( )还是会成功。如果主动方通过 close()发起的第一次挥手,那此时会产生SIGPIPE信号,进程默认会终止,异常退ude. 🎜
      🎜

      如果被动方一直不发第三次挥手,会怎么样

      🎜第三次挥手,是由🎜被动方🎜主动触发的,比如调用close()。🎜🎜如果由于代码错误或者其他一些原因,被动方就是不执行第三次挥手。🎜🎜这时候,主动方会根据自身第一次挥手的时候用是 close() 还是 종료(fd, SHUT_WR ) ,다른 이동 방식은 없습니다.🎜
      • 如果是 shutdown(fd, SHUT_WR) ,说明主动方其实只关闭了写,但还可以读,此时会一直处于 FIN-WAIT-2, 死等被动方的第三次挥手。

      • 如果是 close(), 说明主动方读写都关闭了,这时候会处于 FIN-WAIT-2一段时间,这个时间由 net.ipv4.tcp_fin_timeout 控制,一般是 60s,这个值正好跟2MSL一样 。超过这段时间之后,状态不会变成 `TIME-WAIT`,而是直接变成`CLOSED`。

      # cat /proc/sys/net/ipv4/tcp_fin_timeout
      60

      活久见!TCP两次挥手,你见过吗?那四次握手呢?
      一直不发第三次挥手的情况


      TCP三次挥手

      四次挥手聊完了,那有没有可能出现三次挥手?

      是可能的。

      我们知道,TCP四次挥手里,第二次和第三次挥手之间,是有可能有数据传输的。第三次挥手的目的是为了告诉主动方,"被动方没有数据要发了"。

      所以,在第一次挥手之后,如果被动方没有数据要发给主动方。第二和第三次挥手是有可能合并传输的。这样就出现了三次挥手。

      活久见!TCP两次挥手,你见过吗?那四次握手呢?
      TCP三次挥手


      전송할 데이터가 있으면 세번 흔들면 안되나요?

      위에서 언급한 상황은 전송할 데이터가 없으면 2번째와 3번째 사이에 전송할 데이터가 있는 경우입니다. 손을 흔들면 세 번 흔들면 안 되는 걸까요?

      이 아닙니다. TCP에는 지연 확인이라는 기능도 있습니다. 이는 간단히 다음과 같이 이해될 수 있습니다. 수신자는 데이터를 수신한 후 즉시 ACK 확인 패키지에 응답할 필요가 없습니다. 이를 기준으로

      데이터 패킷이 전송될 때마다 해당 ACK 승인 패킷은 수신자가 승인을 병합할 수 있기 때문입니다.

      ACK 确认包,因为接收方可以合并确认。

      而这个合并确认,放在四次挥手里,可以把第二次挥手、第三次挥手,以及他们之间的数据传输都合并在一起发送。因此也就出现了三次挥手。

      活久见!TCP两次挥手,你见过吗?那四次握手呢?
      TCP三次挥手延迟确认


      TCP两次挥手

      前面在四次挥手中提到,关闭的时候双方都发出了一个FIN和收到了一个ACK

      正常情况下TCP连接的两端,是不同IP+端口的进程。

      但如果TCP连接的两端,IP+端口是一样的情况下,那么在关闭连接的时候,也同样做到了一端发出了一个FIN,也收到了一个 ACK,只不过正好这两端其实是同一个socket그리고 이 병합 확인은 4개의 웨이브에 배치되며 두 번째 웨이브, 세 번째 웨이브와 이들 사이의 데이터 전송을 병합하여 함께 보낼 수 있습니다. 그래서 세 개의 파도가 있었습니다.

      活久见!TCP两次挥手,你见过吗?那四次握手呢?活久见!TCP两次挥手,你见过吗?那四次握手呢?TCP 3파 지연 확인

      TCP 파도 두 번front It 마감 시 양측 모두 FIN을 보내고 ACK를 받았다는 4개의 웨이브에서 언급되었습니다.

      일반적인 상황에서 TCP 연결의 두 끝은 서로 다른 🎜IP+포트🎜를 사용하여 프로세스됩니다. 🎜🎜하지만 TCP 연결의 양쪽 끝에서 🎜IP + 포트🎜가 동일하면 연결을 닫을 때에도 마찬가지입니다. 🎜한 쪽은 FIN을 보내고 ACK🎜도 받는데 이 둘은 끝 실제로는 동일한 소켓. 🎜🎜🎜🎜TCP가 두 번 파도🎜🎜🎜그리고 이렇게 양쪽 끝이 동일한 🎜IP+포트🎜로 연결되는 것을 🎜TCP 자체 연결🎜이라고 합니다. 🎜

      是的,你没看错,我也没打错别字。同一个socket确实可以自己连自己,形成一个连接。


      一个socket能建立连接?

      上面提到了,同一个客户端socket,自己对自己发起连接请求。是可以成功建立连接的。这样的连接,叫TCP自连接

      下面我们尝试下复现。

      注意我是在以下系统进行的实验。在mac上多半无法复现。

      #  cat /etc/os-release
      NAME="CentOS Linux"
      VERSION="7 (Core)"
      ID="centos"
      ID_LIKE="rhel fedora"
      VERSION_ID="7"
      PRETTY_NAME="CentOS Linux 7 (Core)"

      通过nc命令可以很简单的创建一个TCP自连接

      # nc -p 6666 127.0.0.1 6666

      上面的 -p 可以指定源端口号。也就是指定了一个端口号为6666的客户端去连接 127.0.0.1:6666

      # netstat -nt | grep 6666
      tcp        0      0 127.0.0.1:6666          127.0.0.1:6666          ESTABLISHED

      整个过程中,都没有服务端参与。可以抓个包看下。

      活久见!TCP两次挥手,你见过吗?那四次握手呢?
      image-20210810093309117

      可以看到,相同的socket,自己连自己的时候,握手是三次的。挥手是两次的。

      活久见!TCP两次挥手,你见过吗?那四次握手呢?
      TCP自连接

      上面这张图里,左右都是同一个客户端,把它画成两个是为了方便大家理解状态的迁移。

      我们可以拿自连接的握手状态对比下正常情况下的TCP三次握手。

      活久见!TCP两次挥手,你见过吗?那四次握手呢?
      正常情况下的TCP三次握手

      看了自连接的状态图,再看看下面几个问题。


      一端发出第一次握手后,如果又收到了第一次握手的SYN包,TCP连接状态会怎么变化?

      第一次握手过后,连接状态就变成了SYN_SENT状态。如果此时又收到了第一次握手的SYN包,那么连接状态就会从SYN_SENT状态变成SYN_RCVD

      // net/ipv4/tcp_input.c
      static int tcp_rcv_synsent_state_process()
      {
          // SYN_SENT状态下,收到SYN包
          if (th->syn) {
              // 状态置为 SYN_RCVD
              tcp_set_state(sk, TCP_SYN_RECV);
          }
      }


      一端发出第二次握手后,如果又收到第二次握手的SYN+ACK包,TCP连接状态会怎么变化?

      第二握手过后,连接状态就变为SYN_RCVD了,此时如果再收到第二次握手的SYN+ACK包。连接状态会变为ESTABLISHED

      // net/ipv4/tcp_input.c
      int tcp_rcv_state_process()
      {
          // 前面省略很多逻辑,能走到这就认为肯定有ACK
          if (true) {
              // 判断下这个ack是否合法
              int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) > 0;
              switch (sk->sk_state) {
              case TCP_SYN_RECV:
                  if (acceptable) {
              // 状态从 SYN_RCVD 转为 ESTABLISHED
                      tcp_set_state(sk, TCP_ESTABLISHED);
                  } 
              }
          }
      }


      一端第一次挥手后,又收到第一次挥手的包,TCP连接状态会怎么变化?

      第一次挥手过后,一端状态就会变成 FIN-WAIT-1。正常情况下,是要等待第二次挥手的ACK。但实际上却等来了 一个第一次挥手的 FIN包, 这时候连接状态就会变为CLOSING

      // net/
      static void tcp_fin(struct sock *sk)
      {
          switch (sk->sk_state) {
          case TCP_FIN_WAIT1:
              tcp_send_ack(sk);
          // FIN-WAIT-1状态下,收到了FIN,转为 CLOSING
              tcp_set_state(sk, TCP_CLOSING);
              break;
          }
      }

      这可以说是隐藏剧情了。

      CLOSING 很少见,除了出现在自连接关闭外,一般还会出现在TCP两端同时关闭连接的情况下。

      处于CLOSING状态下时,只要再收到一个ACK,就能进入 TIME-WAIT 状态,然后等个2MSL,连接就彻底断开了。这跟正常的四次挥手还是有些差别的。大家可以滑到文章开头的TCP四次挥手再对比下。


      代码复现自连接

      可能大家会产生怀疑,这是不是nc这个软件本身的bug

      那我们可以尝试下用strace看看它内部都做了啥。

      # strace nc -p 6666 127.0.0.1 6666
      // ...
      socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
      fcntl(3, F_GETFL)                       = 0x2 (flags O_RDWR)
      fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
      setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
      bind(3, {sa_family=AF_INET, sin_port=htons(6666), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
      connect(3, {sa_family=AF_INET, sin_port=htons(6666), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress)
      // ...

      无非就是以创建了一个客户端socket句柄,然后对这个句柄执行 bind, 绑定它的端口号是6666,然后再向 127.0.0.1:6666发起connect方法。

      我们可以尝试用C语言去复现一遍。

      下面的代码,只用于复现问题。直接跳过也完全不影响阅读。

      #include <stdio.h>
      #include <unistd.h>
      #include <sys/socket.h>
      #include <stdlib.h>
      #include <arpa/inet.h>
      #include <ctype.h>
      #include <string.h>
      #include <strings.h>
      
      
      int main()
      {
          int lfd, cfd;
          struct sockaddr_in serv_addr, clie_addr;
          socklen_t clie_addr_len;
          char buf[BUFSIZ];
          int n = 0, i = 0, ret = 0 ;
          printf("This is a client \n");
      
          /*Step 1: 创建客户端端socket描述符cfd*/    
          cfd = socket(AF_INET, SOCK_STREAM, 0);
          if(cfd == -1)
          {
              perror("socket error");
              exit(1);
          }
      
          int flag=1,len=sizeof(int);
          if( setsockopt(cfd, SOL_SOCKET, SO_REUSEADDR, &flag, len) == -1)
          {
              perror("setsockopt");
              exit(1);
          } 
      
      
          bzero(&clie_addr, sizeof(clie_addr));
          clie_addr.sin_family = AF_INET;
          clie_addr.sin_port = htons(6666);
          inet_pton(AF_INET,"127.0.0.1", &clie_addr.sin_addr.s_addr);
      
          /*Step 2: 客户端使用bind绑定客户端的IP和端口*/  
          ret = bind(cfd, (struct sockaddr* )&clie_addr, sizeof(clie_addr));
          if(ret != 0)
          {
              perror("bind error");
              exit(2);
          }
      
          /*Step 3: connect链接服务器端的IP和端口号*/    
          bzero(&serv_addr, sizeof(serv_addr));
          serv_addr.sin_family = AF_INET;
          serv_addr.sin_port = htons(6666);
          inet_pton(AF_INET,"127.0.0.1", &serv_addr.sin_addr.s_addr);
          ret = connect(cfd,(struct sockaddr *)&serv_addr, sizeof(serv_addr));
          if(ret != 0)
          {
              perror("connect error");
              exit(3);
          }
      
          /*Step 4: 向服务器端写数据*/
          while(1)
          {
              fgets(buf, sizeof(buf), stdin);
              write(cfd, buf, strlen(buf));
              n = read(cfd, buf, sizeof(buf));
              write(STDOUT_FILENO, buf, n);//写到屏幕上
          }
          /*Step 5: 关闭socket描述符*/
          close(cfd);
          return 0;
      }

      保存为 client.c 文件,然后执行下面命令,会发现连接成功。

      # gcc client.c -o client && ./client
      This is a client
      # netstat -nt | grep 6666
      tcp        0      0 127.0.0.1:6666          127.0.0.1:6666          ESTABLISHED

      说明,这不是nc的bug。事实上,这也是内核允许的一种情况。


      自连接的解决方案

      自连接一般不太常见,但遇到了也不难解决。

      解决方案比较简单,只要能保证客户端和服务端的端口不一致就行。

      事实上,我们写代码的时候一般不会去指定客户端的端口,系统会随机给客户端分配某个范围内的端口。而这个范围,可以通过下面的命令进行查询

      # cat /proc/sys/net/ipv4/ip_local_port_range
      32768   60999

      也就是只要我们的服务器端口不在32768-60999这个范围内,比如设置为8888。就可以规避掉这个问题。

      另外一个解决方案,可以参考golang标准网络库的实现,在连接建立完成之后判断下IP和端口是否一致,如果遇到自连接,则断开重试。

      func dialTCP(net string, laddr, raddr *TCPAddr, deadline time.Time) (*TCPConn, error) {
          // 如果是自连接,这里会重试
          for i := 0; i < 2 && (laddr == nil || laddr.Port == 0) && (selfConnect(fd, err) || spuriousENOTAVAIL(err)); i++ {
              if err == nil {
                  fd.Close()
              }
              fd, err = internetSocket(net, laddr, raddr, deadline, syscall.SOCK_STREAM, 0, "dial", sockaddrToTCP)
          }
          // ...
      }
      
      func selfConnect(fd *netFD, err error) bool {
          // 判断是否端口、IP一致
          return l.Port == r.Port && l.IP.Equal(r.IP)
      }


      四次握手

      前面提到的TCP自连接是一个客户端自己连自己的场景。那不同客户端之间是否可以互联?

      答案是可以的,有一种情况叫TCP同时打开

      活久见!TCP两次挥手,你见过吗?那四次握手呢?
      TCP同时打开

      大家可以对比下,TCP同时打开在握手时的状态变化,跟TCP自连接是非常的像。

      比如SYN_SENT状态下,又收到了一个SYN,其实就相当于自连接里,在发出了第一次握手后,又收到了第一次握手的请求。结果都是变成 SYN_RCVD

      SYN_RCVD 状态下收到了 SYN+ACK,就相当于自连接里,在发出第二次握手后,又收到第二次握手的请求,结果都是变成 ESTABLISHED他们的源码其实都是同一块逻辑。


      复现TCP同时打开

      分别在两个控制台下,分别执行下面两行命令。

      while true; do nc -p 2224 127.0.0.1 2223 -v;done
      
      while true; do nc -p 2223 127.0.0.1 2224 -v;done

      上面两个命令的含义也比较简单,两个客户端互相请求连接对方的端口号,如果失败了则不停重试。

      执行后看到的现象是,一开始会疯狂失败,重试。一段时间后,连接建立完成。

      # netstat -an | grep  2223
      Proto Recv-Q Send-Q Local Address           Foreign Address         State 
      tcp        0      0 127.0.0.1:2224          127.0.0.1:2223          ESTABLISHED
      tcp        0      0 127.0.0.1:2223          127.0.0.1:2224          ESTABLISHED

      期间抓包获得下面的结果。

      活久见!TCP两次挥手,你见过吗?那四次握手呢?

      可以看到,这里面建立连接用了四次交互。因此可以说这是通过"四次握手"建立的连接。

      而且更重要的是,这里面只涉及两个客户端,没有服务端

      看到这里,不知道大家有没有跟我一样,被刷新了一波认知,对socket有了重新的认识。

      在以前的观念里,建立连接,必须要有一个客户端和一个服务端,并且服务端还要执行一个listen()和一个accept()。而实际上,这些都不是必须的。

      그럼 다음에는 면접관이 물어보시죠"아니요listen(), TCP가 연결을 설정할 수 있습니까?"listen(), TCP能建立连接吗?", 我想大家应该知道该怎么回答了。

      但问题又来了,只有两个客户端,没有listen() ,为什么能建立TCP连接?

      如果大家感兴趣,我们以后有机会再填上这个坑。


      总结

      • 四次挥手中,不管是程序主动执行close(),还是进程被杀,都有可能发出第一次挥手FIN包。如果机器上FIN-WAIT-2状态特别多,一般是因为对端一直不执行close()方法发出第三次挥手。

      • Close()同时关闭发送和接收消息的功能。shutdown(), 모두가 어떻게 대답했는지 알아야 한다고 생각합니다. . 하지만 문제가 다시 발생합니다. 클라이언트는 두 개뿐이고 listen(), 왜 TCP 연결?

      • 관심이 있으시면 나중에 이 구멍을 다시 메울 수 있는 기회를 갖도록 하겠습니다.

      • 요약 span>

        • 4번 웨이브
        • 프로그램이 close() 또는 프로세스가 종료되면 첫 번째 웨이브를 발행할 수 있습니다FIN 패키지. 컴퓨터에 있는 경우 82); background: rgb(248, 248, 248);">FIN-WAIT-2여러 상태가 있습니다. 일반적으로 피어가 close() 메소드는 세 번째 웨이브를 보냅니다.

          Close()는 메시지 보내기와 받기를 끄고

          . shutdown()은 개별적으로 메시지를 🎜닫기🎜 보내거나 받을 수 있습니다. 🎜🎜🎜🎜2차, 3차 웨이브가 합쳐질 수도 있습니다. 그래서 네 개의 파도가 🎜세 개의 파도🎜로 바뀌었습니다. 🎜🎜🎜🎜동일한 소켓을 자신에게 연결하면 🎜TCP 자체 연결🎜이 발생합니다. 자체 연결의 물결은 🎜두 배의 물결🎜입니다. 🎜
        • 아니요듣기, 두 클라이언트 간에 연결을 설정할 수도 있습니다. 이러한 상황을 TCP 동시 열기라고 하며, 이는 4방향 핸드셰이크가 생성됩니다. listen,两个客户端之间也能建立连接。这种情况叫TCP同时打开,它由四次握手产生。


        最后

        今天提到的,不管是两次挥手,还是自连接,或是TCP同时打开什么的。

        咋一看,可能对日常搬砖没什么用,实际上也确实没什么用。

        并且在面试上大概率也不会被问到。

        毕竟一般面试官也不在意茴字有几种写法。

        这篇文章的目的,主要是想从另外一个角度让大家重新认识下socket。原来TCP


      드디어

    • 오늘 말씀드린 내용은 두 웨이브 두 번 또는 자체 연결 또는 TCP가 동시에 열립니다.
      🎜얼핏 보면 일상적인 벽돌 옮기기에는 별 도움이 안 될 수도 있고, 실제로는 정말 쓸모가 없습니다. 🎜🎜그리고 면접에서는 안 물어볼 확률이 높습니다. 🎜🎜결국 대부분의 면접관은 fennel이라는 단어를 몇 가지 방법으로 쓸 수 있는지 신경 쓰지 않습니다. . 🎜🎜이 글의 목적은 주로 모든 사람들이 다른 각도에서 다시 이해할 수 있도록 하는 것입니다.소켓. 원본TCP는 자체적으로 연결할 수 있으며, 두 클라이언트도 서버 없이 연결할 수 있습니다. 🎜🎜이건 정말 정말 예상치 못한 일이에요. 🎜🎜🎜

      위 내용은 活久见!TCP两次挥手,你见过吗?那四次握手呢?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

      성명:
      이 기사는 Go语言进阶学习에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제