Linux应用——TCP通信

一、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 <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
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 <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

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 <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>

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;i<max;i++)
    {
        bzero(&sockinfos[i],sizeof(sockinfos[i]));
        sockinfos[i].fd=-1;
        sockinfos[i].tid=-1;
    }

    while(1)
    {
        struct sockaddr_in cliaddr;
        int len =sizeof(cliaddr);

        //接收连接
        int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
       
        struct sockinfo *pinfo;
        for(int i=0;i<max;i++)
        {
            //从这个数组中可以找到一个可以用的sockInfo元素;
            if (sockinfos[i].fd==-1)
            {
                pinfo=&sockinfos[i];
                break;
            }
            if(i==max-1)
            {
                sleep(1);
                i--;
            }

        }
       pinfo->fd=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;

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值