欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

结合socket详解TCP三次握手四次挥手

程序员文章站 2022-06-14 11:26:57
...

结合socket详解TCP三次握手四次挥手


  TCP协议中的三次握手和四次挥手大家应该都至少听说过了,本人一直觉得理论学习要结合代码才能学习的更深刻,当知道东西是这样,然后再知道为什么是这样的时候,领悟往往更加深刻,今天本人就结合socket编程中的API来解析一下TCP协议的三次握手和四次挥手过程。
  那么TCP协议中的三次握手和四次挥手实际在网络编程中是怎么对应的呢?先贴一个简单的echo服务器端和客户端的代码:
  
Server: 

#include <iostream>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<unistd.h>
#define DEFAULT_PORT 8000
using namespace std;
const int buffer_size = 2048;

int main()
{
    int listenfd,connfd,n;
    struct sockaddr_in servaddr;
    char rec_buff[buffer_size];
    //char send_buff[buffer_size];
    if((listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
    {
        cout<< " create socket error";
        return -1;
    }
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(DEFAULT_PORT);

    if(bind(listenfd,(sockaddr*)&servaddr,sizeof(servaddr)) == -1)
    {
        cout<<"bind error";
        return -1;
    }
    if(listen(listenfd,5) == -1)
    {
        cout<< "listen error";
        return -1;
    }
    while(true)
    {
        if((connfd = accept(listenfd,(sockaddr*)NULL,NULL))== -1)
        {
            cout<<"accept error";
            continue;
        }
        if(send(connfd,"connect successful",19,0) == -1)
        {
            cout<< "send error"<<endl;
        }
    while(true)
    {
        if((n = recv(connfd,rec_buff,buffer_size,0)) > 0)
        {
            cout << "receive message is "<<rec_buff<<endl;
            if(send(connfd,"receive successful",19,0) == -1)
            {
             cout<< "send error"<<endl;
            }
        }
        else
        {
            cout << "recv error"<<endl;
        }

        rec_buff[n] = '\0';

        if(strcmp(rec_buff,"quit") == 0)
        {
            break;
        }
    }       

        close(connfd);
    }   
    close(listenfd);
    return 0;
}

Clinet

#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
using namespace std;

int main(int argc,char** argv)
{
    int sockfd,n,recv_len;
    char rec_buffer[2048],send_buffer[2048];
    struct sockaddr_in servaddr;

    if(argc != 2)
    {
        cout<<"input IP address"<<endl;
        return -1;
    }

    if((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
    {
        cout<<"socket error"<<endl;
        return -1;
    }

    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8000);
    if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr) <= 0)
    {
        cout<<"inet_pton error"<<endl;
        return -1;
    }

    if(connect(sockfd,(sockaddr*)&servaddr,sizeof(servaddr)) < 0)
    {
        cout<< "connect error"<<endl;
        return -1;
    }

    while(true)
    {
        cout<<"send message to server:";
        cin >> send_buffer;

        if(strcmp(send_buffer,"exit") == 0)
        {
            break;
        }

        if(send(sockfd,send_buffer,strlen(send_buffer),0) < 0)
        {
            cout<<"send error"<<endl;
            return -1;
        }

        if((recv_len = recv(sockfd,rec_buffer,2048,0)) < 0)
        {
            cout<<"receive error"<<endl;
            return -1;
        }
        cout << rec_buffer <<endl;
    }
    close(sockfd);
    return 0;
}

  功能很简单,就是客户端发送字符串,然后服务器端收到打印出来,结合这个简单的模型来看看通信过程中的三次握手和四次挥手
  
client端主要做了以下几件事:
  socket() ->connect() -> send() ->recv() ->close()
sever端主要做了以下几件事:

  首先,socket(),bind()这两个函数并不算实际参与到了三次握手和四次挥手的过程中,它们算是通信开始前的预备动作,socket()负责产生一个套接字的描述符,bind()负责将一个本地协议地址赋予一个套接字,同时也将通信端口进行绑定,如果bind绑定的是INADDR_ANY,即表示所有发送到服务器的这个端口,不管是哪个网卡/哪个IP地址接收到的数据,都由这个服务端进程进行处理。一般情况下,如果你要建立网络服务器应用程序,则你要通知服务器操作系统:请在某地址 xxx.xxx.xxx.xxx上的某端口 yyyy上进行侦听,并且把侦听到的数据包发送给我。这个过程,你是通过bind()系统调用完成的。——也就是说,你的程序要绑定服务器的某地址,或者说:把服务器的某地址上的某端口占为已用。服务器操作系统可以给你这个指定的地址,也可以不给你。如果你的服务器有多个网卡(每个网卡上有不同的IP地址),而你的服务(不管是在udp端口上侦听,还是在tcp端口上侦听),出于某种原因:可能是你的服务器操作系统可能随时增减IP地址,也有可能是为了省去确定服务器上有什么网络端口(网卡)的麻烦 —— 可以要在调用bind()的时候,告诉操作系统:“我需要在 yyyy 端口上侦听,所有发送到服务器的这个端口,不管是哪个网卡/哪个IP地址接收到的数据,都是我处理的。”这时候,服务器程序则在0.0.0.0这个地址上进行侦听。
  listen()函数则完成主要的三次握手的过程,listen负责将主动连接套接字变为被动连接套接字(当一个socket()创建套接字后,它被假设为一个主动套接字),然后指定内核为相应套接字排队的最大连接个数。然后维护一个半连接队列和一个全连接队列,什么是半连接队列呢?就是只进行了两次握手,还未收到client确认ACK的连接,全连接队列就是3次握手成功的连接的队列,这个连接的总长度是由listen(int sockfd,int backlog)中的backlog来决定,但是这个backlog不好确定,Berkeley的实现为这个backlog增设了一个模糊因子(将其乘以1.5得到队列最大长度),现在的http的服务器一般都将backlog设定一个大值来保证连接都可以得到处理,在《Unix网络编程》中给出了一个利用环境变量LISTENQ来设定backlog值得做法,方式如下
  结合socket详解TCP三次握手四次挥手
  然后就是accept(),accept()也不算参与3次握手的过程中了,accept()的任务就是从listen()的全连接队列里取出第一个就绪的连接,然后返回一个新的connection fd。
  结合socket详解TCP三次握手四次挥手
  如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回client的TCP连接,要注意区分监听套接字(listenfd)和已连接套接字(connfd),一个服务器通常仅创建一个listenfd,它在服务器的生命期内一直存在,内核为每个连接成功的client创建一个connfd,当server完成对某个client的服务时,就会关闭这个connfd。
  为什么要3次握手而不是两次呢?在谢希仁的《计算机网络》中是这样说的:为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。比如:
  “已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”
  这就很明白了,防止了服务器端的一直等待而浪费资源。
  参与到四次挥手过程主要就是close()函数了:
  结合socket详解TCP三次握手四次挥手
  那为什么要4次挥手呢?TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。