TCP协议补充实验(一)

简介: TCP协议补充实验

一、理解CLOSE_WAIT状态

当客户端和服务器在进行TCP通信时,若客户端调用close函数关闭对应的文件描述符,此时客户端底层操作系统就会向服务器发起FIN请求,服务器收到该请求后会对其进行ACK响应。


但若当服务器收到客户端的FIN请求后,服务器端不调用close函数关闭对应的文件描述符,那么服务器就不会给客户端发送FIN请求,相当于只完成了四次挥手中的前两次挥手,此时客户端和服务器的连接状态分别会变为FIN_WAIT_2和CLOSE_WAIT


86e32a1ef5f74b78a5bc7f30e42952c9.png


可以编写一个简单的TCP服务器端来模拟出该现象,采用一些网络工具来充当客户端向服务器发起连接请求


服务器的初始化需要进行套接字的创建、绑定以及监听,然后主线程就可以通过调用accept函数从底层获取建立好的连接了。获取到连接后主线程创建新线程为该连接提供服务,而新线程只需执行一个死循环逻辑即可

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
const int port = 8082;
const int num = 5;
void* Routine(void* arg)
{
  pthread_detach(pthread_self());
  int fd = *(int*)arg;
  delete (int*)arg;
  while (1){
    std::cout << "socket " << fd << " is serving the client" << std::endl;
    sleep(1);
  }
  return nullptr;
}
int main()
{
  //创建监听套接字
  int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
  if (listen_sock < 0) {
    std::cerr << "socket error" << std::endl;
    return 1;
  }
  //绑定
  struct sockaddr_in local;
  memset(&local, 0, sizeof(local));
  local.sin_port = htons(port);
  local.sin_family = AF_INET;
  local.sin_addr.s_addr = INADDR_ANY;
  if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
    std::cerr << "bind error" << std::endl;
    return 2;
  }
  //监听
  if (listen(listen_sock, num) < 0){
    std::cerr << "listen error" << std::endl;
    return 3;
  }
  //启动服务器
  struct sockaddr_in peer;
  memset(&peer, 0, sizeof(peer));
  socklen_t len = sizeof(peer);
  for (;;){
    int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
    if (sock < 0){
      std::cerr << "accept error" << std::endl;
      continue;
    }
    std::cout << "get a new link: " << sock << std::endl;
    int* p = new int(sock);
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)p);
  }
  return 0;
}


代码编写完成后编译并运行服务器,并用telnet工具连接服务器,最后运行下面监控脚本


[cl@VM-0-15-centos DisconnectStatus]$ while :; do sudo netstat -ntp|head -2&&sudo netstat -ntp | grep 8082; sleep 1; echo "##################"; done

令telnet退出,即客户端向服务器发起了连接断开请求,但此时服务器端并没有调用close函数关闭对应的文件描述符,所以当telnet退出后,客户端维护的连接的状态会变为FIN_WAIT_2,而服务器维护的连接的状态会变为CLOSE_WAIT


903d63be452d49c49f903567e9280e48.png


因此若不及时关闭不用的文件描述符,除了会造成文件描述符泄漏,可能导致连接资源没有完全释放,即内存泄漏问题



二、理解TIME_WAIT状态

当客户端和服务器在进行TCP通信时,客户端调用close函数关闭对应的文件描述符,若服务器收到后也调用close函数进行了关闭,那么此时双方将正常完成四次挥手。但主动发起四次挥手的一方在四次挥手后,不会立即进入CLOSED状态,而是进入短暂的TIME_WAIT状态等待若干时间,最终才会进入CLOSED状态


92bdf8a7b2444e73a3c607bfa3fdda41.png


继续刚才的实验,由于telnet退出后服务器端没有调用close关闭对应的文件描述符,因此客户端维护的客户端维护连接的状态停留在了FIN_WAIT_2状态,而服务器维护连接的状态停留在了CLOSE_WAIT状态。


要让客户端和服务器继续完成后两次挥手,就需要服务器端调用close函数关闭对应的文件描述符。虽然服务器代码中没有调用close函数,但因为文件描述符的生命周期是随进程的,当进程退出的时候,该进程所对应的文件描述符都会自动关闭。因此只需要在telnet退出后让服务器进程退出就行了,此时服务器进程所对应的文件描述符会自动关闭,此时服务器底层TCP就会向客户端发送FIN请求,完成剩下的两次挥手。


四次挥手后客户端维护的连接就会进入到TIME_WAIT状态,而服务器维护的连接则会进入到CLOSED状态


90677806aa1a465a9202c91e1f7b4362.png


解决TIME_WAIT状态引起的bind失败的方法

主动发起四次挥手的一方在四次挥手后,会进入TIME_WAIT状态。若在有客户端连接服务器的情况下服务器进程退出了,就相当于服务器主动发起了四次挥手,此时服务器维护的连接在四次挥手后就会进入TIME_WAIT状态


18ebe0a2ce864e43bf0f8415eacf6ff7.png


在该连接处于TIME_WAIT期间,若服务器想重新启动,则会出现绑定失败问题


ce3fe736429c4f83997480bc5333d13e.png


在TIME_WAIT期间,连接并没有被完全释放,即服务器绑定的端口号仍然被占用,此时服务器若想继续绑定该端口号,则只能等待TIME_WAIT结束。但当服务器崩溃后最重要的是让服务器立马重新启动,若想要让服务器崩溃后在TIME_WAIT期间也能立刻重新启动,需要让服务器在调用socket函数创建套接字后,继续调用setsockopt函数设置端口复用,这也是编写服务器代码时的推荐做法


setsockopt函数


int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

sockfd:需要设置的套接字对应的文件描述符

level:被设置选项的层次。比如在套接字层设置选项对应就是SOL_SOCKET

optname:需要设置的选项。该选项的可取值与设置的level参数有关

optval:指向存放选项待设置的新值的指针

optlen:待设置的新值的长度

返回值:设置成功返回0,设置失败返回-1,同时错误码会被设置


设置监听套接字,将监听套接字在套接字层设置端口复用选项SO_REUSEADDR,该选项设置为非零值表示开启端口复用


int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

此时当服务器崩溃后即可立马重新启动,而不用等待TIME_WAIT结束


实验


先启动服务器,再启动客户端连接服务器,此时出现两个ESTABLISHED连接

881d303a8e474fd48dda3feabfa16c38.png

此时向服务器发送信号使得服务器退出,客户端检测到服务器关闭也关闭了文件描述符,由于服务器主动发起四次挥手,其状态变为TIME_WAIT

989a486a3b984e6dbacf436da0c543b3.png


由于代码中调用了setsockopt函数,设置监听套接字端口复用,此时可用重新启动服务器。接下来重新启动服务器和客户端,此时出现了三个连接,其中8082端口绑定了两个连接(端口复用)

7e2682c6386845b7acfb75d51d02f613.png

从上述实验中可以看出,即便通信双方对应的进程都退出,但服务器端依然存在一个处于TIME_WAIT状态的连接,这也说明了进程管理和连接管理是两个相对独立的单元。连接由TCP自行管理,连接不一定会随进程的退出而关闭

目录
相关文章
|
9月前
|
网络协议 数据安全/隐私保护 网络架构
TCP协议下的三大协议的验证实验
通过本章的探险,你将学会如何在验证三大协议的存在于TCP协议下
|
10月前
|
网络协议
TCP协议补充实验(二)
TCP协议补充实验
46 0
|
6月前
|
存储 网络协议 Linux
|
6月前
|
网络协议 算法 安全
|
6月前
|
网络协议 Unix 关系型数据库
|
6月前
|
存储 网络协议 安全
|
7月前
|
网络协议 Java API
网络基础编程:TCP协议
网络基础编程:TCP协议
28 0
|
9月前
|
域名解析 缓存 网络协议
传输方式的分类【图解TCP/IP(笔记五)】
传输方式的分类【图解TCP/IP(笔记五)】
166 0
|
存储 网络协议 定位技术
《网络是怎么样连接的》读书笔记 - Tcp/IP连接(二)(上)
《网络是怎么样连接的》读书笔记 - Tcp/IP连接(二)(上)
124 0
|
缓存 网络协议 安全
《网络是怎么样连接的》读书笔记 - Tcp/IP连接(二)(下)
《网络是怎么样连接的》读书笔记 - Tcp/IP连接(二)(下)
131 0