linux网络编程(三) TCP通信时序与多进程/线程并发服务器的编写

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
简介: linux网络编程(三) TCP通信时序与多进程/线程并发服务器的编写

1.TCP通信时序


下图是一次TCP通讯的时序图。TCP连接建立断开。包含大家熟知的三次握手和四次握手。

1670952005472.jpg

在这个例子中,首先客户端主动发起连接、发送请求,然后服务器端响应请求,然后客户端主动关闭连接。两条竖线表示通讯的两端,从上到下表示时间的先后顺序,注意,数据从一端传到网络的另一端也需要时间,所以图中的箭头都是斜的。双方发送的段按时间顺序编号为1-10,各段中的主要信息在箭头上标出,例如段2的箭头上标着SYN,8000(0), ACK1001, ,表示该段中的SYN位置1,32位序号是8000,该段不携带有效载荷(数据字节数为0),ACK位置1,32位确认序号是1001,带有一个mss(Maximum Segment Size,最大报文长度)选项值为1024。


建立连接(三次握手)的过程:


1.客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的段1。 客户端发出段1,SYN位表示连接请求。序号是1000,这个序号在网络通讯中用作临时的地址,每发一个数据字节,这个序号要加1,这样在接收端可以根据序号排出数据包的正确顺序,也可以发现丢包的情况,另外,规定SYN位和FIN位也要占一个序号,这次虽然没发数据,但是由于发了SYN位,因此下次再发送应该用序号1001。mss表示最大段尺寸,如果一个段太大,封装成帧后超过了链路层的最大帧长度,就必须在IP层分片,为了避免这种情况,客户端声明自己的最大段尺寸,建议服务器端发来的段不要超过这个长度。


2.服务器端回应客户端,是三次握手中的第2个报文段,同时带ACK标志和SYN标志。它表示对刚才客户端SYN的回应;同时又发送SYN给客户端,询问客户端是否准备好进行数据通讯。

服务器发出段2,也带有SYN位,同时置ACK位表示确认,确认序号是1001,表示“我接收到序号1000及其以前所有的段,请你下次发送序号为1001的段”,也就是应答了客户端的连接请求,同时也给客户端发出一个连接请求,同时声明最大尺寸为1024。


3.客户必须再次回应服务器端一个ACK报文,这是报文段3。 客户端发出段3,对服务器的连接请求进行应答,确认序号是8001。在这个过程中,客户端和服务器分别给对方发了连接请求,也应答了对方的连接请求,其中服务器的请求和应答在一个段中发出,因此一共有三个段用于建立连接,称为“三方握手(three-way-handshake)”。在建立连接的同时,双方协商了一些信息,例如双方发送序号的初始值、最大段尺寸等。


在TCP通讯中,如果一方收到另一方发来的段,读出其中的目的端口号,发现本机并没有任何进程使用这个端口,就会应答一个包含RST位的段给另一方。例如,服务器并没有任何进程使用8080端口,我们却用telnet客户端去连接它,服务器收到客户端发来的SYN段就会应答一个RST段,客户端的telnet程序收到RST段后报告错误Connection

refused:
$ telnet 192.168.0.200 8080
Trying 192.168.0.200...
telnet: Unable to connect to remote host: Connection refused


数据传输的过程:


1.客户端发出段4,包含从序号1001开始的20个字节数据。


2.服务器发出段5,确认序号为1021,对序号为1001-1020的数据表示确认收到,同时请求发送序号1021开始的数据,服务器在应答的同时也向客户端发送从序号8001开始的10个字节数据,这称为piggyback。


3.客户端发出段6,对服务器发来的序号为8001-8010的数据表示确认收到,请求发送序号8011开始的数据。 在数据传输过程中,ACK和确认序号是非常重要的,应用程序交给TCP协议发送的数据会暂存在TCP层的发送缓冲区中,发出数据包给对方之后,只有收到对方应答的ACK段才知道该数据包确实发到了对方,可以从发送缓冲区中释放掉了,如果因为网络故障丢失了数据包或者丢失了对方发回的ACK段,经过等待超时后TCP协议自动将发送缓冲区中的数据包重发。


关闭连接(四次握手)的过程:


由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。


1.客户端发出段7,FIN位表示关闭连接的请求。

2.服务器发出段8,应答客户端的关闭连接请求。

3.服务器发出段9,其中也包含FIN位,向客户端发送关闭连接请求。

4.客户端发出段10,应答服务器的关闭连接请求。 建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中,因为有连接半关闭的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。


2.滑动窗口 (TCP流量控制)


介绍UDP时我们描述了这样的问题:如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP协议通过“滑动窗口(Sliding Window)”机制解决这一问题。看下图的通讯过程:

1670952036941.jpg

滑动窗口


1.发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。


2.发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。


3.接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。


4.接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。


5.发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。


6.接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。


7.接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。


8.接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。


9.接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。


上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序提走数据,虚线框是向右滑动的,因此称为滑动窗口。


从这个例子还可以看出,发送端是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),在底层通讯中这些数据可能被拆成很多数据包来发送,但是一个数据包有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。


3.出错处理封装函数


上面的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。


为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块wrap.c:

wrap.c
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s)
{
  perror(s);
  exit(1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
  int n;
  again:
  if ( (n = accept(fd, sa, salenptr)) < 0) {
  if ((errno == ECONNABORTED) || (errno == EINTR))
    goto again;
  else
    perr_exit("accept error");
  }
  return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
  int n;
  if ((n = bind(fd, sa, salen)) < 0)
  perr_exit("bind error");
  return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
  int n;
  if ((n = connect(fd, sa, salen)) < 0)
  perr_exit("connect error");
  return n;
}
int Listen(int fd, int backlog)
{
  int n;
  if ((n = listen(fd, backlog)) < 0)
  perr_exit("listen error");
  return n;
}
int Socket(int family, int type, int protocol)
{
  int n;
  if ( (n = socket(family, type, protocol)) < 0)
  perr_exit("socket error");
  return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
  ssize_t n;
again:
  if ( (n = read(fd, ptr, nbytes)) == -1) {
  if (errno == EINTR)
    goto again;
  else
    return -1;
  }
  return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
  ssize_t n;
again:
  if ( (n = write(fd, ptr, nbytes)) == -1) {
  if (errno == EINTR)
    goto again;
  else
    return -1;
  }
  return n;
}
int Close(int fd)
{
  int n;
  if ((n = close(fd)) == -1)
  perr_exit("close error");
  return n;
}
ssize_t Readn(int fd, void *vptr, size_t n)
{
  size_t nleft;
  ssize_t nread;
  char *ptr;
  ptr = vptr;
  nleft = n;
  while (nleft > 0) {
  if ( (nread = read(fd, ptr, nleft)) < 0) {
    if (errno == EINTR)
    nread = 0;
    else
    return -1;
  } else if (nread == 0)
    break;
  nleft -= nread;
  ptr += nread;
  }
  return n - nleft;
}
ssize_t Writen(int fd, const void *vptr, size_t n)
{
  size_t nleft;
  ssize_t nwritten;
  const char *ptr;
  ptr = vptr;
  nleft = n;
  while (nleft > 0) {
  if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
    if (nwritten < 0 && errno == EINTR)
    nwritten = 0;
    else
    return -1;
  }
  nleft -= nwritten;
  ptr += nwritten;
  }
  return n;
}
static ssize_t my_read(int fd, char *ptr)
{
  static int read_cnt;
  static char *read_ptr;
  static char read_buf[100];
  if (read_cnt <= 0) {
again:
  if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
    if (errno == EINTR)
    goto again;
    return -1;  
  } else if (read_cnt == 0)
    return 0;
  read_ptr = read_buf;
  }
  read_cnt--;
  *ptr = *read_ptr++;
  return 1;
}
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
  ssize_t n, rc;
  char c, *ptr;
  ptr = vptr;
  for (n = 1; n < maxlen; n++) {
  if ( (rc = my_read(fd, &c)) == 1) {
    *ptr++ = c;
    if (c == '\n')
    break;
  } else if (rc == 0) {
    *ptr = 0;
    return n - 1;
  } else
    return -1;
  }
  *ptr = 0;
  return n;
}


wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif


4.多进程并发服务器编写


使用多进程并发服务器时要考虑以下几点:

1.父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)

2.系统内创建进程个数(与内存大小相关)

3.进程创建过多是否降低整体服务性能(进程调度)

server
/* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 800
void do_sigchild(int num)
{
  while (waitpid(0, NULL, WNOHANG) > 0);
}
int main(void)
{
  struct sockaddr_in servaddr, cliaddr;
  socklen_t cliaddr_len;
  int listenfd, connfd;
  char buf[MAXLINE];
  char str[INET_ADDRSTRLEN];
  int i, n;
  pid_t pid;
  struct sigaction newact;
  newact.sa_handler = do_sigchild;
  sigemptyset(&newact.sa_mask);
  newact.sa_flags = 0;
  sigaction(SIGCHLD, &newact, NULL);
  listenfd = Socket(AF_INET, SOCK_STREAM, 0);
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(SERV_PORT);
  Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
  Listen(listenfd, 20);
  printf("Accepting connections ...\n");
  while (1) {
  cliaddr_len = sizeof(cliaddr);
  connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
  pid = fork();
  if (pid == 0) {
    Close(listenfd);
    while (1) {
    n = Read(connfd, buf, MAXLINE);
    if (n == 0) {
      printf("the other side has been closed.\n");
      break;
    }
    printf("received from %s at PORT %d\n",
      inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
      ntohs(cliaddr.sin_port));
    for (i = 0; i < n; i++)
      buf[i] = toupper(buf[i]);
    Write(connfd, buf, n);
    }
    Close(connfd);
    return 0;
  } else if (pid > 0) {
    Close(connfd);
  } else
    perr_exit("fork");
  }
  Close(listenfd);
  return 0;
}
client
/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
  struct sockaddr_in servaddr;
  char buf[MAXLINE];
  int sockfd, n;
  sockfd = Socket(AF_INET, SOCK_STREAM, 0);
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
  servaddr.sin_port = htons(SERV_PORT);
  Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
  while (fgets(buf, MAXLINE, stdin) != NULL) {
  Write(sockfd, buf, strlen(buf));
  n = Read(sockfd, buf, MAXLINE);
  if (n == 0) {
    printf("the other side has been closed.\n");
    break;
  } else
    Write(STDOUT_FILENO, buf, n);
  }
  Close(sockfd);
  return 0;
}


5.多进程并发服务器编写


在使用线程模型开发服务器时需考虑以下问题:

1.调整进程内最大文件描述符上限

2.线程如有共享数据,考虑线程同步

3.服务于客户端线程退出时,退出处理。(退出值,分离态)

4.系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU

server
/* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
struct s_info {
  struct sockaddr_in cliaddr;
  int connfd;
};
void *do_work(void *arg)
{
  int n,i;
  struct s_info *ts = (struct s_info*)arg;
  char buf[MAXLINE];
  char str[INET_ADDRSTRLEN];
  /* 可以在创建线程前设置线程创建属性,设为分离态,哪种效率高呢? */
  pthread_detach(pthread_self());
  while (1) {
  n = Read(ts->connfd, buf, MAXLINE);
  if (n == 0) {
    printf("the other side has been closed.\n");
    break;
  }
  printf("received from %s at PORT %d\n",
    inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
    ntohs((*ts).cliaddr.sin_port));
  for (i = 0; i < n; i++)
    buf[i] = toupper(buf[i]);
  Write(ts->connfd, buf, n);
  }
  Close(ts->connfd);
}
int main(void)
{
  struct sockaddr_in servaddr, cliaddr;
  socklen_t cliaddr_len;
  int listenfd, connfd;
  int i = 0;
  pthread_t tid;
  struct s_info ts[256];
  listenfd = Socket(AF_INET, SOCK_STREAM, 0);
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(SERV_PORT);
  Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
  Listen(listenfd, 20);
  printf("Accepting connections ...\n");
  while (1) {
  cliaddr_len = sizeof(cliaddr);
  connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
  ts[i].cliaddr = cliaddr;
  ts[i].connfd = connfd;
  /* 达到线程最大数时,pthread_create出错处理, 增加服务器稳定性 */
  pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
  i++;
  }
  return 0;
}


client
/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
  struct sockaddr_in servaddr;
  char buf[MAXLINE];
  int sockfd, n;
  sockfd = Socket(AF_INET, SOCK_STREAM, 0);
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
  servaddr.sin_port = htons(SERV_PORT);
  Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
  while (fgets(buf, MAXLINE, stdin) != NULL) {
  Write(sockfd, buf, strlen(buf));
  n = Read(sockfd, buf, MAXLINE);
  if (n == 0)
    printf("the other side has been closed.\n");
  else
    Write(STDOUT_FILENO, buf, n);
  }
  Close(sockfd);
  return 0;
}


6.TCP状态转换


这个图N多人都知道,它排除和定位网络或系统故障时大有帮助,但是怎样牢牢地将这张图刻在脑中呢?那么你就一定要对这张图的每一个状态,及转换的过程有深刻的认识,不能只停留在一知半解之中。下面对这张图的11种状态详细解析一下,以便加强记忆!不过在这之前,先回顾一下TCP建立连接的三次握手过程,以及关闭连接的四次握手过程。

1670952174681.jpg

CLOSED:表示初始状态。

LISTEN:该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。

SYN_SENT:这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。

SYN_RCVD:该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。

ESTABLISHED:表示连接已经建立。

FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是: FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。

FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。

FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。该状态下的socket只能接收数据,不能发。

TIME_WAIT:表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

CLOSING:这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

CLOSE_WAIT:此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。

LAST_ACK:该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态。


7.半关闭


当TCP链接中A发送FIN请求关闭,B端回应ACK后(A端进入FIN_WAIT_2状态),B没有立即发送FIN给A时,A方处在半链接状态,此时A可以接收B发送的数据,但是A已不能再向B发送数据。

从程序的角度,可以使用API来控制实现半连接状态。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how:    允许为shutdown操作选择以下几种方式:
    SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
        该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
    SHUT_WR(1):  关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
    SHUT_RDWR(2):   关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。

使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。


shutdown不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。


注意:

1.如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。

2.在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但,如果一个进程close(sfd)将不会影响到其它进程。


8. 2MSL


2MSL (Maximum Segment Lifetime) TIME_WAIT状态的存在有两个理由:


(1)让4次握手关闭流程更加可靠;4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。


(2)防止lost duplicate对后续新建正常链接的传输造成破坏。lost uplicate在实际的网络中非常常见,经常是由于路由器产生故障,路径无法收敛,导致一个packet在路由器A,B,C之间做类似死循环的跳转。IP头部有个TTL,限制了一个包在网络中的最大跳数,因此这个包有两种命运,要么最后TTL变为0,在网络中消失;要么TTL在变为0之前路由器路径收敛,它凭借剩余的TTL跳数终于到达目的地。但非常可惜的是TCP通过超时重传机制在早些时候发送了一个跟它一模一样的包,并先于它达到了目的地,因此它的命运也就注定被TCP协议栈抛弃。


另外一个概念叫做incarnation connection,指跟上次的socket pair一摸一样的新连接,叫做incarnation of previous connection。lost uplicate加上incarnation connection,则会对我们的传输造成致命的错误。


TCP是流式的,所有包到达的顺序是不一致的,依靠序列号由TCP协议栈做顺序的拼接;假设一个incarnation connection这时收到的seq=1000, 来了一个lost duplicate为seq=1000,len=1000,则TCP认为这个lost duplicate合法,并存放入了receive buffer,导致传输出现错误。通过一个2MSL TIME_WAIT状态,确保所有的lost duplicate都会消失掉,避免对新连接造成错误。


该状态为什么设计在主动关闭这一方:

(1)发最后ACK的是主动关闭一方。

(2)只要有一方保持TIME_WAIT状态,就能起到避免incarnation connection在2MSL内的重新建立,不需要两方都有。


如何正确对待2MSL TIME_WAIT? RFC要求socket pair在处于TIME_WAIT时,不能再起一个incarnation connection。但绝大部分TCP实现,强加了更为严格的限制。在2MSL等待期间,socket中使用的本地端口在默认情况下不能再被使用。

若A 10.234.5.5 : 1234和B 10.55.55.60 :

6666建立了连接,A主动关闭,那么在A端只要port为1234,无论对方的port和ip是什么,都不允许再起服务。这甚至比RFC限制更为严格,RFC仅仅是要求socket pair不一致,而实现当中只要这个port处于TIME_WAIT,就不允许起连接。这个限制对主动打开方来说是无所谓的,因为一般用的是临时端口;但对于被动打开方,一般是server,就悲剧了,因为server一般是熟知端口。比如http,一般端口是80,不可能允许这个服务在2MSL内不能起来。


解决方案是给服务器的socket设置SO_REUSEADDR选项,这样的话就算熟知端口处于TIME_WAIT状态,在这个端口上依旧可以将服务启动。当然,虽然有了SO_REUSEADDR选项,但sockt

pair这个限制依旧存在。比如上面的例子,A通过SO_REUSEADDR选项依旧在1234端口上起了监听,但这时我们若是从B通过6666端口去连它,TCP协议会告诉我们连接失败,原因为Address

already in use.


RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。 RFC (Request For

Comments),是一系列以编号排定的文件。收集了有关因特网相关资讯,以及UNIX和因特网社群的软件文件。


8.1 2MSL


做一个测试,首先启动server,然后启动client,用Ctrl-C终止server,马上再运行server,运行结果:
itcast$ ./server
bind error: Address already in use
这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。
我们用netstat命令查看一下:
itcast$ netstat -apn |grep 6666
tcp        1      0 192.168.1.11:38103      192.168.1.11:6666       CLOSE_WAIT  3525/client     
tcp        0      0 192.168.1.11:6666       192.168.1.11:38103      FIN_WAIT2   -
server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,
但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。
现在用Ctrl-C把client也终止掉,再观察现象:
itcast$ netstat -apn |grep 6666
tcp        0      0 192.168.1.11:6666       192.168.1.11:38104      TIME_WAIT   -
itcast$ ./server
bind error: Address already in use
client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。
TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,
等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,
因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。
MSL在RFC 1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。
至于为什么要规定TIME_WAIT的时间,可参考UNP 2.7节。

8.2 端口复用


在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为,TCP连接没有完全断开指connfd(127.0.0.1:6666)没有完全断开,而我们重新监听的是lis-tenfd(0.0.0.0:6666),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。


在server代码的socket()和bind()调用之间插入如下代码:
int opt = 1;
  setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));


有关setsockopt可以设置的其它选项请参考UNP第7章。

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
1月前
|
NoSQL 网络协议 Linux
Redis的实现一:c、c++的网络通信编程技术,先实现server和client的通信
本文介绍了使用C/C++进行网络通信编程的基础知识,包括创建socket、设置套接字选项、绑定地址、监听连接以及循环接受和处理客户端请求的基本步骤。
46 6
|
24天前
|
网络协议 安全 5G
网络与通信原理
【10月更文挑战第14天】网络与通信原理涉及众多方面的知识,从信号处理到网络协议,从有线通信到无线通信,从差错控制到通信安全等。深入理解这些原理对于设计、构建和维护各种通信系统至关重要。随着技术的不断发展,网络与通信原理也在不断演进和完善,为我们的生活和工作带来了更多的便利和创新。
61 3
|
1月前
|
安全 物联网 物联网安全
量子通信网络:安全信息交换的新平台
【10月更文挑战第6天】量子通信网络作为一种全新的安全信息交换平台,正逐步展现出其独特的优势和巨大的潜力。通过深入研究和不断探索,我们有理由相信,量子通信网络将成为未来信息安全领域的重要支柱,为构建更加安全、高效、可靠的信息社会贡献力量。让我们共同期待量子通信网络在未来的广泛应用和美好前景!
|
2月前
|
安全 量子技术 数据安全/隐私保护
量子通信:构建安全通信网络的未来
【9月更文挑战第21天】量子通信作为信息时代的一次伟大飞跃,正引领我们迈向一个全新的安全通信纪元。其独特的绝对安全性、高效率和大容量特点,使得量子通信在构建未来安全通信网络中具有不可替代的作用。随着技术的不断发展和应用的不断拓展,我们有理由期待量子通信将在未来发挥更加重要的作用,为人类社会的信息安全保驾护航。
109 13
|
5天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
20 5
|
8天前
|
Linux 数据库
Linux内核中的锁机制:保障并发操作的数据一致性####
【10月更文挑战第29天】 在多线程编程中,确保数据一致性和防止竞争条件是至关重要的。本文将深入探讨Linux操作系统中实现的几种关键锁机制,包括自旋锁、互斥锁和读写锁等。通过分析这些锁的设计原理和使用场景,帮助读者理解如何在实际应用中选择合适的锁机制以优化系统性能和稳定性。 ####
24 6
|
7天前
|
传感器 自动驾驶 物联网
探秘 5G 核心网络之 5G RAN:开启高速通信新时代
探秘 5G 核心网络之 5G RAN:开启高速通信新时代
28 4
|
1月前
|
机器学习/深度学习 人工智能 算法
|
18天前
|
物联网 5G 数据中心
|
29天前
|
网络协议 安全 数据安全/隐私保护
网络协议:互联网通信的基石
【10月更文挑战第12天】
70 1