Linux应用——TCP通信
创始人
2024-09-26 08:47:54
0

一、TCP三次挥手

        TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。
        TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP 头部。

        TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手(保证传送数据安全的一种机制)建立一个连接。采用四次挥手来关闭一个连接。

        三次握手是协议本身的内容,不是程序员写程序时需要写入的内容,三次握手是保证双方互相建立了连接。

        三次握手是发生在客户端连接的时候,当调用connect(),底层会通过TCP协议会进行三次握手。 

为了保证双方都有发送和接受的能力:

第一次握手,客户端给服务端发送请求,服务端知道客户端拥有发送的能力,自己有接收的能力;

第二次握手,服务端给客户端回复请求,客户端知道自己和服务端都有拥有发送和接收的能力;

第三次握手,客户端给服务器就发送请求,服务端知道客户端有接收的能力,自己有发送的能力。

当然四次握手也是可以的,但是最少得三次握手。

序号与确认序号:

        为字节流中的每个字节设置一个序号,接收到后会回复一个接受序号,也是下一次发送字节的序号,保证字节的完整。

        三次握手关于数据时序的说明:

第一次握手:

        1、客户端将SYN标志位置1;

        2、随机生成一个序号seq=J,这个序号后边可以携带数据(数据大小);

第二次握手:

        1、服务器端接收客户端的连接:ACK=1;

        2、服务器会回发一个确认序号,确认序号在(seq=J)基础上+数据长度+SIN/FIN(按一个字节计算);

        ack=J+数据长度+1;

        3、服务器端会向客户端发起连接请求:SYN标示位置为1;

        4、随机生成一个序号seq=K,这个序号后边可以携带数据(数据大小);

第三次握手:

        1、客户端接收服务器的连接:ACK=1;

        2、客户端会回发一个确认序号,确认序号在(seq=K)基础上+数据长度+SIN/FIN(按一个字节计算);

        ack=K+数据长度+1;

二、TCP滑动窗口

        滑动窗口是一种流量控制技术,早期的网络中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸)。

        TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0
时,发送方一般不能再发送数据报。    

       窗口理解为缓冲区的大小,滑动窗口的大小会随着发送数据和接收数据而变化。通信的双方都有发送缓冲区和接收缓冲区。

        服务器:

        发送缓冲区(发送缓冲区的窗口)

        接收缓冲区(接受缓冲区的窗口)

        客户端:   

        发送缓冲区(发送缓冲区的窗口)

        接收缓冲区(接受缓冲区的窗口)

1、发送方的缓冲区:

        白色格子:空闲的空间;

        灰色格子:数据已经被发送出去了,但是还没有被接受。 

        紫色格子:还没有发送出去的数据;

2、接收方的缓冲区:

        白色格子:空闲的空间;

        紫色格子:已经接收到的数据

MMS:Maximun segment size(一条数据的最大数据量);

win:滑动窗口大小; 

1、客户端向服务器发起连接,客户端的滑动窗口是4096,一次能接收的最大数据量为1460;

2、服务器行客户端发送回复ACK=1(接收连接情况),告诉服务器的窗口大小为6144;一次接收最大数据量为1024;

3、第三次握手;’

4、第(4-9)次,客户端连续给服务器发送6K数据,每次发送1K;

5、第10次,服务器告诉服务器发送6K数据已经接收到了,存储在缓冲区中,缓冲区数据已经处理了2K,窗口大小是2k;

6、第11次,服务器告诉服务器发送6K数据已经接收到了,存储在缓冲区中,缓冲区数据已经处理了4K,窗口大小是4k;

7、第12次,客户端给服务器发送了1K的数据,

8、第13次,第一次挥手,客户端主动请求和服务断开连接,并且给服务器发送了1K的数据;

9、第14次,第二次挥手。服务器回复ACK,8194,同意断开连接的请求,告诉客户端已经接收到刚才发送的2K数据。滑动窗口为2K;

10、第15-16次,服务器通知客户端滑动窗口的大小;

11、第17次,第三次挥手。服务器端给客户端发送FIN,请求断开连接;

12、第18次,第四次挥手。客户端同意了服务器端的断开请求。

三、TCP四次挥手

        

        四次挥手发生在断开连接的时候,在程序中调用close函数会使用TCP协议进行四次挥手, 对对应的信息进行释放。

        客户端和服务端都可以主动发起断开连接,谁先调用close()谁就是发起;因为在TCP连接时,采用三次握手建立的连接时双向的,所以在断开连接的时候,也需要双向断开。

四、TCP并发通信

        要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。

  思路:

        1、一个父进程,多个子进程;

        2、父进程负责等待并接受客户端的连接;

        3、子进程:完成通信,接受一个客户端连接,就创建一个子进程通信。

四、TCP实现多进程通信

server_process.c 服务器代码:

#define _XOPEN_SOURCE #include  #include  #include  #include  #include  #include  #include  #include  void recyleChild(int arg) {     while(1)     {         int ret=waitpid(-1,NULL,WNOHANG);         if(ret==-1)         {             //所有的子进程都被回收了;             break;         }         else if(ret==0)         {             //还有子进程活着;             break;         }         else if(ret>0)         {             //被回收了             printf("子进程%d被回收了\n",ret);         }     }  }  int main() {     //注册信号捕捉     struct sigaction act;     act.sa_flags=0;     sigemptyset(&act.sa_mask);     act.sa_handler=recyleChild;     sigaction(SIGCHLD,&act,NULL);      //创建socket     int lfd =socket(AF_INET,SOCK_STREAM,0);     if(lfd==-1)     {         perror("socket");         exit(-1);     }     //绑定     struct sockaddr_in saddr;     saddr.sin_addr.s_addr=0;     saddr.sin_port=htons(9999);     saddr.sin_family=AF_INET;     int ret = bind(lfd,(const struct sockaddr*)&saddr,sizeof(saddr));      if(ret==-1)     {         perror("bind");         exit(-1);     }     //监听     ret=listen(lfd,8);     if(ret==-1)     {         perror("listen");         exit(-1);     }      //不断循环,接收客户端连接;     while(1)     {         struct sockaddr_in cliaddr;         int len =sizeof(cliaddr);          //接收连接         int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);         if(cfd==-1)         {             if(errno==EINTR)             {                 continue;             }             perror("accept");             exit(-1);         }          //每一个连接进来就创建一个子进程进程客户端通信;             pid_t pid=fork();             if(pid==0)             {                 //子进程                 //获取客户端信息                     char cliIP[16];                     inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,cliIP,sizeof(cliIP));                     unsigned short cliPort=ntohs(cliaddr.sin_port);                     printf("cliIp is :%s,port is %d\n",cliIP,cliPort);                      //接收客户端发来的数据                     char recvBuff[1024]={0};                     while(1)                     {                         int len1=read(cfd,&(recvBuff),sizeof(recvBuff)+1);                         if(len1==-1)                         {                             perror("read");                             exit(-1);                         }                         else if(len1>0)                         {                             printf("recive client data:%s\n",recvBuff);                         }                         else if(len1==0)                         {                             printf("client close\n");                         }                     write(cfd,recvBuff,strlen(recvBuff)+1);                      }                   close(cfd);                  exit(0);             }     }     close(lfd);     return 0; }

client.c客户端代码 

//TCP通信客户端  #include  #include  #include  #include  #include   int main() {     //1、创建套接字   int fd = socket(AF_INET,SOCK_STREAM,0);   if(fd==-1)   {     perror("socket");     exit(-1);   }     //2、连接服务器端     struct sockaddr_in serve_addr;     serve_addr.sin_family=AF_INET;    inet_pton(AF_INET,"192.168.254.171",&serve_addr.sin_addr.s_addr);    serve_addr.sin_port=htons(9999);    int ret= connect(fd,(const struct sockaddr *)&serve_addr,sizeof(serve_addr));     if(ret==-1)     {         perror("connect");         exit(-1);     }     //3、进行通信     char readbuff[1024]={0};    // char writebuff[1024]={0};     int i=0;     while(1)     {      // memset(writebuff,0,sizeof(writebuff));       //printf("请输入内容:\n");       //scanf("%s",writebuff);        //char * data="I am client";        sprintf(readbuff,"data:%d\n",i++);        //给服务端发送数据        write(fd, &readbuff,strlen(readbuff));             sleep(1);      int len =read(fd,readbuff,sizeof(readbuff));       if(len==-1)     {         perror("read");        exit(-1);     }else if(len>0)     {      printf("recive client data:%s\n",readbuff);     }   else if(len==0)   {     //表示服务器端断开连接。     printf("serve closed...\n");     break;   }     }   //关闭文件描述符;   close(fd);     return 0;  }

 运行结果:

服务器结果:

客户端1运行结果:

客户端2运行结果:

结果分析:

1、服务器端

        首先创建socket函数,在进行绑定,封装服务器的IP、端口号等相关信息;然后进行监听,等待客户端的连接,客户端的连接是在while循环中进行的,因为需要不断接收客户端的连接。连接成功一个客户端后,创建一个进程,在子进程完成服务器与客户端的通信。关于子进程的回收是利用信号进行回收,因为如果利用wait函数进行回收,会造成程序在对应位置进行阻塞。

2、客户端

        首先创还能socket函数,然后连接服务器,链接成功后进行通信,进行数据的收发,最后回收文件描述符。一个客户端在一个子进程中通信。这也是TCP实现多进程通信的方法原理。

五、TCP实现多线程通信 

        利用TCP实现多线程通信与多进程通信的区别在于,当服务端连接到客户端后,与每一个客户端进行通信时,是创建进程进行通信函数还是线程进行通信函数,也就是说其客户端的代码是相同的,区别在于服务器端的代码;

#include  #include  #include  #include  #include  #include   struct sockinfo//该结构体用于存放服务端发送给子进程关于客户端的信息,因为进程只能传递一个参数。 {     int fd;//通信文件描述符;     struct sockaddr_in addr;     pthread_t tid;//线程号; };  struct sockinfo sockinfos[128];//定义全局变量,用于存放客户端的信息;  void * working (void *arg) { //获取客户端信息     struct sockinfo *pinfo=(struct sockinfo*)arg;          char cliIP[16];         inet_ntop(AF_INET,&pinfo->addr.sin_addr.s_addr,cliIP,sizeof(cliIP));         unsigned short cliPort=ntohs(pinfo->addr.sin_port);         printf("cliIp is :%s,port is %d\n",cliIP,cliPort);          //接收客户端发来的数据         char recvBuff[1024]={0};         while(1)         {             int len1=read(pinfo->fd,&(recvBuff),sizeof(recvBuff)+1);             if(len1==-1)             {                 perror("read");                 exit(-1);             }             else if(len1>0)             {                 printf("recive client data:%s\n",recvBuff);             }             else if(len1==0)             {                 printf("client close\n");             }         write(pinfo->fd,recvBuff,strlen(recvBuff)+1);          }     close(pinfo->fd);     exit(0); //子线程与客户端进行通信; cfd; 客户端的信息;线程号;     return NULL; }    int main() {       //创建socket     int lfd =socket(AF_INET,SOCK_STREAM,0);     if(lfd==-1)     {         perror("socket");         exit(-1);     }     //绑定     struct sockaddr_in saddr;     saddr.sin_addr.s_addr=0;     saddr.sin_port=htons(9999);     saddr.sin_family=AF_INET;     int ret = bind(lfd,(const struct sockaddr*)&saddr,sizeof(saddr));      if(ret==-1)     {         perror("bind");         exit(-1);     }     //监听     ret=listen(lfd,8);     if(ret==-1)     {         perror("listen");         exit(-1);     }     //  初始化数据     int max = sizeof(sockinfos)/sizeof( sockinfos[0]);     for(int i=0;ifd=cfd;        memcpy(&pinfo->addr,&cliaddr,len);                 pthread_t tid;     //  每一个连接进来,创建一个子线程与客户端进行通信         pthread_create(&pinfo->tid,NULL,working,pinfo);          pthread_detach(pinfo->tid);      }      close(lfd);     return 0; }

其运行结果同多进程是相同的,其主要的改变就是线程的创建以及客户端信息的传递。

六、TCP状态转变

        状态转变发生在三次握手与四次挥手之间,在中间的数据传输时,TCP的状态时不会发生转变的。通信双方都存在状态转变。

        三次握手,客户端先发起,客户端首先是CLOSE状态,当进行connect连接第一次握手后,客户端状态会转变为SYN_SENT,服务器刚开始为监听状态LISTEN,后来当第一次握手后成为SYN_RCVD状态。然后第二次握手后,客户端转变为ESTABLISHED状态。第三次握手后,服务端转变为ESTABLISHED状态。当通信双方都是ESTABLISHED状态时,才能进正常通信数据的传输。

        四次挥手,谁发起都可以,假设是客户端发起,客户端发送FIN调用close函数,执行第一次挥手后,客户端状态变为FIN_WAIT_1,服务器端接受到FIN与ACK后,其状态改为CLOSE_WAIT,然后执行第二次挥手,客户端接收到后,其状态转变为,FIN_WAIT2。等到第三次挥手后服务端调用close,服务器的状态转为LAST_AVK,客户端转变为TIME_WAIT,四次挥手后客户端与服务端都为CLOSE关闭状态。

 红色实线代表客户端,绿色虚线代表服务端。

 起点均为两个客户端与服务端的CLOSE状态;

客户端(CLOSE)主动打开,发送SYN,其状态转变为SYN_SENT;

服务端(CLOSE)--(LISTEN),收到SYN,发送SYN,ACK,其状态转变为SYN_RCVD;

客户端(SYN_SENT)收到SYN,ACK发送ACK,其状态状态转变为ESTABLISTHED;

服务端(SYN_RCVD)收到ACK,其状态转变为ESTABLISTHED;

通信(状态不发生变化)

客户端(ESTABLISTHED)关闭,发送FIN,ACK其状态转变为FIN_WAIT_1;

服务端(ESTABLISTHED)接收FIN,ACK,发送ACK,其状态转变为CLOSE_WAIT;

客户端(FIN_WAIT_1)接受ACK,其状态转变为FIN_WAIT_2;

服务端(ESTABLISTHED)关闭,发送FIN,其状态转变为LAST_ACK;

客户端(FIN_WAIT_2)接受FIN,并发送ACK,其状态转变为TIME_WAIT;

客户端与服务端都成为CLOSE状态;

TIME_WAIT:定时经过两倍报文段寿命后,2MSL, 为了保证安全性。

        当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK。

        以客户端为主动关闭方为例,客户端接收到服务端的FIN后,并发送ACK,其不能保证服务端是否接收到,如果没有接收到,且客户端状态为CLOSE,这样服务端就永远不会回到CLOSE状态,相反,客户端状态为TIME_WAIT,TIME_WAIT状态, 这个状态会持续: 2msl。

在此期间,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK,ACK丢失,服务端再次发送FIN,客户端就有时间发送ACK。

七、半关闭与端口复用

什么是半关闭状态?

例如:在前面讲的四次挥手中,客户端发送FIN和ACK,服务端发送了ACK,但是并没有向客户端发送FIN,则处于一个半关闭状态。

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

半关闭状态有什么作用?

实现数据的单方向传输;

利用API实现半关闭状态;

        如果利用close进行半关闭的话,其fd直接关闭,既不能读也不能写,无法实现数据的单方向传递,所以,我们一般利用API实现半关闭状态。

        使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。 

注意:
1. 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用进程都调用了 close,套接字将被释放。
2. 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。但如果一个进程 close(sfd) 将不会影响到其它进程。

端口复用

端口复用最常用的用途是:
防止服务器重启时之前绑定的端口还未释放;
程序突然退出而系统没有释放端口;

网络信息相关命令:

        netsata

        参数:

                -a:所有的socket;

                -p:显示正在使用socket的程序的名称;

                -n:直接显示使用IP地址,而不通过域名服务器;

        客户端与服务端进行连接,在经过服务端主动断开后,客户端还没有进行断开,如果要是再执行服务端,就会出现报错。并且在主动断开连接一方存在TIME_WAIT状态,2MSL时间,在此期间端口号一直被占用,不能在执行程序。 此时就需要进行端口复用,系统调用API:

        不仅仅设置端口复用,还可以设置套接字;

相关参数:

         sockfd:文件描述符,只能是套接字的文件描述符;

         level:级别;SOL_SOCKET(端口复用的级别)

        optname:选项名;SO_REUSEADDR,SOREUSEPORT;

        optval:端口复用的值,整型:1可以复用。0不可以复用;

        optlen:optval参数的大小;

        返回值:成功返回0,错误返回-1;

端口复用的时间是在,服务器绑定端口之前。

相关内容

热门资讯

三分钟了解!aapoker一直... 三分钟了解!aapoker一直输(透视)原来是有挂(软件透明挂)-哔哩哔哩是一款可以让一直输的玩家,...
技术分享!德扑之星不发牌,wp... 技术分享!德扑之星不发牌,wpk稳赢其实确实是真的有挂(今日头条)(2025已更新)-哔哩哔哩;一、...
一分钟了解!wpk辅助器小程序... 一分钟了解!wpk辅助器小程序(透视)原来真的是有挂(软件透明挂)-哔哩哔哩;wpk是一种具有地方特...
一分钟了解!Wepoke机制原... 一分钟了解!Wepoke机制原来真的是有挂(WepOke)原来是真的有挂(2023已更新)(哔哩哔哩...
攻略教程!来玩德州app有挂的... 攻略教程!来玩德州app有挂的,Wepokeapp果真真实是有挂的(有挂分享)(2021已更新)-哔...
6分钟了解!Wepoke长期原... 6分钟了解!Wepoke长期原来真的是有挂(Wepoke)原来一直都是有挂(2022已更新)(哔哩哔...
有nat怎么做bgp(网络管理... 什么是BGP?BGP(Border Gateway Protocol)是一种广泛使用的Interne...
有了云主机怎么用(如何使用云主... 什么是云主机?云主机是基于云计算技术的虚拟主机,它可以在云服务提供商的数据中心中创建,并通过远程的方...
买主机送什么(购买主机有哪些赠... 买主机送什么?——购买主机有哪些赠品?对于许多人来说,购买新电脑主机是一项昂贵的投资,而且往往需要花...
8分钟了解!Wepoke存在原... 8分钟了解!Wepoke存在原来是真的有挂(wePoKe)原来确实是有挂(2025已更新)(哔哩哔哩...