【Linux网络编程必学!】——Linux_网络编程_TCP

文章目录

  • 1. TCP 概念
  • 2. TCP编程接口
    • 2.1 listen()
    • 2.2 accept()
    • 2.3 connect()
    • 2.4 send()/recv()
  • 3. 三次握手
  • 4. 四次挥手
  • 5 多进程版本TCP
  • 6 多线程版本TCP
  • 7 TCP vs UDP

1. TCP 概念

【Linux网络编程必学!】——Linux_网络编程_TCP_第1张图片

  • 传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流传输层通信协议
  • TCP旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。

2. TCP编程接口

在前面讲解UDP时已经将一些预备知识和相关接口做了详解。接下来就TCP单独会用到的接口进行详解。
没讲的接口老铁可以去看-》【Linux网络编程必学!】——Linux_网络编程_UDP -》link

  • 使用TCP套接字编程可以实现基于TCP/IP协议的面向连接的通信,它分为服务器端和客户端两部分,其主要实现过程如图所示:
    【Linux网络编程必学!】——Linux_网络编程_TCP_第2张图片

2.1 listen()

listen()函数:listen函数仅被TCP服务器调用,它的作用是将用sock创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。

#include 
 int listen(int sockfd,int backlog);   
 成功返回:0    失败返回:-1
  • 第一个参数是socket函数返回的套接口描述字
  • 第二个参数规定了内核为此套接口排队的最大连接个数。由于listen函数第二个参数的原因,内核要维护两个队列:以完成连接队列未完成连接队列。未完成队列中存放的是TCP连接的三路握手未完成的连接,accept函数是从以连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。

2.2 accept()

accept()函数:accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。

#include          
 int accept(int listenfd, struct sockaddr *client, socklen_t * addrlen);  
 成功返回:非负描述字   失败返回:-1
  • 第一个参数是socket函数返回的套接口描述字
  • 第二个和第三个参数分别是指向连接方的套接口地址结构和该地址结构的长度;该函数返回的是一个全新的套接口描述字;如果对客户段的信息不感兴趣,可以将第二和第三个参数置为空。

2.3 connect()

  • connect()函数:当用socket建立了套接口后,可以调用connect为这个套接字指明远程端的地址;如果是字节流套接口,connect就使用三次握手建立一个连接;如果是数据报套接口,connect仅指明远程端地址,而不向它发送任何数据。
#include       
 int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);  
 成功返回:0    失败返回:-1
  • 第一个参数是socket函数返回的套接口描述字;第二和第三个参数分别是一个指向套接口地址结构的指针和该结构的大小。

2.4 send()/recv()

send()/recv():TCP套接字提供了send()和recv()函数,用来发送和接收操作。这两个函数与write()和read()函数很相似,只是多了一个附加的参数。

  • send()函数用于数据的发送
#include 
#include < sys/socket.h >         
ssize_t send(int sockfd, const void *buf, size_t len, int flags);  
成功返回:返回写出的字节数   失败返回:-13个参数与write()相同,参数flags是传输控制标志。
  • recv()函数用于数据的发送
#include 
#include < sys/socket.h >         
ssize_t recv(int sockfd, void *buf, size_t len, int flags);  
成功返回:返回读入的字节数   失败返回:-13个参数与read()相同,参数flags是传输控制标志。
  • write()和read()函数在前面博客已经讲解过-》link
  • socket()/bind()/close()在前面的博客也已讲解过-》link
  • 对于以上接口有不懂得老铁可以看一下博客链接,或者私信我。

3. 三次握手

  • 三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
    【Linux网络编程必学!】——Linux_网络编程_TCP_第3张图片

刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
进行三次握手:

  • 第一次握手客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SEND 状态。
    首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
  • 第二次握手服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
    在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
  • 第三次握手客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
    确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open),在socket编程中,客户端执行connect()时,将触发三次握手。

为什么需要三次握手,两次不行吗?

  • 第一次握手:客户端发送网络包,服务端收到了。
    这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了。
    这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  • 第三次握手:客户端发包,服务端收到了。
    这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

4. 四次挥手

  • 建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
    TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务器均可主动发起挥手动作。
    【Linux网络编程必学!】——Linux_网络编程_TCP_第4张图片
    假如是客户端先发起关闭请求。四次挥手的过程如下:
  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
    即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
    即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
    即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
    即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

挥手为什么需要四次?

  • 因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

5 多进程版本TCP

  • tcpClient.hpp
#pragma once 

#include
#include
#include
#include
#include
#include
#include
#include
#include
class tcpClient
{
     
  public:
    tcpClient(std::string _ip = "127.0.0.1",int _port = 8080)
      :svr_ip(_ip)
      ,svr_port(_port)
    {
     }
    void initClient()
    {
     
      sock = socket(AF_INET, SOCK_STREAM, 0);
      if(sock < 0)
      {
     
        std::cerr << "sock error" << std::endl;
        exit(2);
      }
       //bind不需要 客户端!!!由系统自动进行绑定
       //也不需要监听,服务器需要监听
       //也不需要accept
       //面向链接,需要链接
       struct sockaddr_in svr;
       svr.sin_family = AF_INET;
       svr.sin_port = htons(svr_port);
       svr.sin_addr.s_addr = inet_addr(svr_ip.c_str());

      if(connect(sock,(struct sockaddr*)&svr,sizeof(svr)) !=0)
      {
     
        std::cout<<"connect error! "<<std::endl;
        exit(3);
      }
      //connect success!
    }
    void start()
    {
     
      
      char msg[64];
      while(1)
      {
     
          std::cout<< "Please enter:";
          fflush(stdout);
          size_t s = read(0,msg,sizeof(msg)-1);
          if(s > 0)
          {
     
            msg[s]=0;
            send(sock,msg,strlen(msg),0);
            size_t ss =  recv(sock,msg,sizeof(msg)-1,0);
            if(ss > 0)
            {
     
              msg[ss] = 0;
              std::cout<< "server echo: "<< msg << std::endl;
            }
          }
      }
    }
    ~tcpClient()
    {
     
      close(sock);
    }
  private:
    int svr_port;
    std::string svr_ip;
    int sock;
};

  • tcpServer.hpp
#pragma once 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BACKLOG 5

class tcpServer
{
     
  public:
    tcpServer(int _port)
      :port(_port)
      ,lsock(-1)
    {
     }
    void initServer()
    {
     
      signal(SIGCHLD,SIG_IGN);
      lsock = socket(AF_INET,SOCK_STREAM,0);
      if(lsock < 0)
      {
     
        std::cerr<< " socket error " <<std::endl;
        exit(2);
      }
      struct sockaddr_in local;
      local.sin_family = AF_INET;
      local.sin_port = htons(port);
      local.sin_addr.s_addr=  INADDR_ANY;

      if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)
      {
     
          std::cerr<<"bind error "<<std::endl;
          exit(3);
      }
      if(listen(lsock,BACKLOG) < 0)
      {
     
        std::cerr<< "bind error " << std::endl;
        exit(4);
      }
    }
    void service(int sock)
    {
     
      char buffer[1024];
      while(1)
      {
     
        size_t s = recv(sock,buffer,sizeof(buffer)-1,0);
        if(s > 0)
        {
     
          buffer[s] = 0;
          std::cout << "client: "<< buffer;
          send(sock, buffer,strlen(buffer),0);
        }
        else if(s == 0)
        {
     
          std::cout<<" Client quit! " << std::endl;
          close(sock);
          break;
        }
        else 
        {
     
          std::cout<< "recv client data error !" << std::endl;
          break;
        }
      }
      close(sock);
    }
    void start()
    {
     
      sockaddr_in endpoint;
      while(1)
      {
     
        socklen_t len = sizeof(endpoint);
       int sock =  accept(lsock,(struct sockaddr*)&endpoint,&len);

       if(sock < 0)
       {
     
          std::cerr<<"accept error"<<std::endl;
          continue;
       }
        std::string cli_info = inet_ntoa(endpoint.sin_addr);//四字节Ip改为点分十进制
        cli_info += " : ";
        cli_info += std::to_string(ntohs(endpoint.sin_port));
        std::cout << cli_info<< std::endl;
        std::cout << "get a new link!"<< "sock: " << sock <<std::endl;

        pid_t id = fork();
        if(id == 0)
        {
     
          close(lsock);
          service(sock);
          exit(0);

        }
        close(sock);
        //waitpid(-1,NULL,0);
      }
    }

    ~tcpServer()
    {
     }

  private:
    int port;
    int lsock;
};

6 多线程版本TCP

  • tcpClient.hpp
#pragma once 

#include
#include
#include
#include
#include
#include
#include
#include
#include
class tcpClient
{
     
  public:
    tcpClient(std::string _ip = "127.0.0.1",int _port = 8080)
      :svr_ip(_ip)
      ,svr_port(_port)
    {
     }
    void initClient()
    {
     
      sock = socket(AF_INET, SOCK_STREAM, 0);
      if(sock < 0)
      {
     
        std::cerr << "sock error" << std::endl;
        exit(2);
      }
       //bind不需要 客户端!!!由系统自动进行绑定
       //也不需要监听,服务器需要监听
       //也不需要accept
       //面向链接,需要链接
       struct sockaddr_in svr;
       svr.sin_family = AF_INET;
       svr.sin_port = htons(svr_port);
       svr.sin_addr.s_addr = inet_addr(svr_ip.c_str());

      if(connect(sock,(struct sockaddr*)&svr,sizeof(svr)) !=0)
      {
     
        std::cout<<"connect error! "<<std::endl;
        exit(3);
      }
      //connect success!
    }
    void start()
    {
     
      
      char msg[64];
      while(1)
      {
     
          std::cout<< "Please enter:";
          fflush(stdout);
          size_t s = read(0,msg,sizeof(msg)-1);
          if(s > 0)
          {
     
            msg[s]=0;
            send(sock,msg,strlen(msg),0);
            size_t ss =  recv(sock,msg,sizeof(msg)-1,0);
            if(ss > 0)
            {
     
              msg[ss] = 0;
              std::cout<< "server echo: "<< msg << std::endl;
            }
          }
      }
    }
    ~tcpClient()
    {
     
      close(sock);
    }
  private:
    int svr_port;
    std::string svr_ip;
    int sock;
};

  • tcpServer.hpp
#pragma once 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define BACKLOG 5

class tcpServer
{
     
  public:
    tcpServer(int _port)
      :port(_port)
      ,lsock(-1)
    {
     }
    void initServer()
    {
     
      signal(SIGCHLD,SIG_IGN);
      lsock = socket(AF_INET,SOCK_STREAM,0);
      if(lsock < 0)
      {
     
        std::cerr<< " socket error " <<std::endl;
        exit(2);
      }
      struct sockaddr_in local;
      local.sin_family = AF_INET;
      local.sin_port = htons(port);
      local.sin_addr.s_addr=  INADDR_ANY;

      if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)
      {
     
          std::cerr<<"bind error "<<std::endl;
          exit(3);
      }
      if(listen(lsock,BACKLOG) < 0)
      {
     
        std::cerr<< "bind error " << std::endl;
        exit(4);
      }
    }

    static void service(int sock)
    {
     
      char buffer[1024];
      while(1)
      {
     
        size_t s = recv(sock,buffer,sizeof(buffer)-1,0);
        if(s > 0)
        {
     
          buffer[s] = 0;
          std::cout << "client: "<< buffer;
          send(sock, buffer,strlen(buffer),0);
        }
        else if(s == 0)
        {
     
        std::cout<<" Client quit! " << std::endl;
          close(sock);
          break;
        }
        else 
        {
     
          std::cout<< "recv client data error !" << std::endl;
          break;
        }
      }
      close(sock);
    }

    static void *serviceRoutine(void *arg)
    {
     
        pthread_detach(pthread_self());
        std::cout<<"create a new thread for IO" << std::endl;
        int tsock = *(int *)arg;
        service(tsock);
    }

    void start()
    {
     
      sockaddr_in endpoint;
      while(1)
      {
     
        socklen_t len = sizeof(endpoint);
       int sock =  accept(lsock,(struct sockaddr*)&endpoint,&len);

       if(sock < 0)
       {
     
          std::cerr<<"accept error"<<std::endl;
          continue;
       }
        std::string cli_info = inet_ntoa(endpoint.sin_addr);//四字节Ip改为点分十进制
        cli_info += " : ";
        cli_info += std::to_string(ntohs(endpoint.sin_port));
        std::cout << cli_info<< std::endl;
        std::cout << "get a new link!"<< "sock: " << sock <<std::endl;

        pthread_t tid;
        pthread_create(&tid, nullptr, serviceRoutine, (void*)&sock);//bug
       // pid_t id = fork();
       // if(id == 0)
       // {
     
       //   close(lsock);
       //   service(sock);
       //   exit(0);

       // }
       // close(sock);
        //waitpid(-1,NULL,0);
      }
    }

    ~tcpServer()
    {
     }

  private:
    int port;
    int lsock;
};

【Linux网络编程必学!】——Linux_网络编程_TCP_第5张图片
【Linux网络编程必学!】——Linux_网络编程_TCP_第6张图片

7 TCP vs UDP

区别

  1. TCP 是面向连接的、可靠的、基于字节流的传输层协议;UDP 是无连接的,不可靠的,面向报文的传输层协议
  2. TCP 是面向连接的通信,需要有三次握手等连接过程,会有延时,实时性较差,同时过程复杂,也使其易于攻击;UDP 没有建立连接的过程,因而实时性较强,也稍安全
  3. 在传输相同大小数据时,TCP 首部开销 20 字节;UDP 首部开销 8 字节,TCP 首部比 UDP 复杂,故实际包含的用户数据较少。TCP 在 IP 协议的基础上添加了 序号机制、确认机制、超时重传机制等,保证了传输的可靠性,不会出现丢包、重复、乱序,而 UDP 有丢包,故 TCP 开销大,UDP 开销小
  4. 每条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一,多对多的交互通信

应用场景

  • 对实时性要求高和高速传输的场合下使用 UDP;在可靠性要求低,追求效率的情况下使用 UDP
  • 需要传输大量数据且对可靠性要求高的情况下使用 TCP

什么是面向连接,什么是面向无连接

  • 在互通之前,面向连接的协议会先建立连接,如 TCP 有三次握手,而 UDP 不会

TCP 为什么是可靠连接

  1. 通过 TCP 连接传输的数据无差错,不丢失,不重复,且按顺序到达。
  2. TCP 报文头里面的序号能使 TCP 的数据按序到达
  3. 报文头里面的确认序号能保证不丢包,累计确认及超时重传机制
  4. TCP 拥有流量控制及拥塞控制的机制

你可能感兴趣的:(linux,笔记,网络,linux,网络协议,新星计划,网络通信)