TCP

TCP是面向连接的传输协议,可靠性传输,建立连接时要经过三次握手,断开连接时要经过四次挥手,中间传输数据时也要回复 ACK 包确认,多种机制保证了数据能够正确到达,不会丢失或出错。

TCP的3次握手过程

1、客户端发送TCP连接请求

客户端会随机一个初始序列号seq=x(client_isn),设置SYN=1,表示这是SYN握手报文。然后  就可以把这个 SYN 报文发送给服务端了,表示向服务端发起连接,之后客户端处于同步已发送状态。

2、服务端发送针对TCP连接请求的确认,服务端收到客户端的SYN报文后,也随机一个初始序列号(server_isn)(seq=y),设置ack=x+1, 表示收到了客户端的x之前的数据,希望客户端下次发送的数据从x+1开始。设置 SYN=1 和 ACK=1。表示这是一个SYN握手和ACK确认应答报文。

最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于同步已接收状态。       

3、客户端发送确认的确认

客户端收到服务端报文后,还要向服务端回应最后一个应答报文,将ACK置为 1 ,表示这是一个应答报文ack=y+1 ,表示收到了服务器的y之前的数据,希望服务器下次发送的数据从y+1开始。最后把报文发送给服务端,这次报文可以携带数据,之后客户端处于连接已建立 状态。服务器收到客户端的应答报文后,也进入连接已建立状态

通过这样的三次握手过程,TCP能够确保双方能够收到对方的请求和回应,并且双方都知道彼此的初始序列号和确认号。这样建立起来的连接可以提供可靠的数据传输和顺序控制。

ACK:确认序号有效。

SYN:发起一个新连接。

Linux 网络编程传输层TCP_客户端

CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)

LISTEN:等待从任何远端TCP 和端口的连接请求。

SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。syn_sent

SYN_RCVD:这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态

ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。


  为什么是三次握手,为什么不是两次或者四次?

主要原因:防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误

如果采用两次握手会出现以下情况:

客户端向服务器端发送的请求报文由于网络等原因滞留,未能发送到服务器端,此时连接请求报文失效,客户端会再次向服务器端发送请求报文,之后与服务器端建立连接,当连接释放后,由于网络通畅了,第一次客户端发送的请求报文又突然到达了服务器端,这条请求报文本该失效了,但此时服务器端误认为客户端又发送了一次连接请求,两次握手建立好连接,此时客户端忽略服务器端发来的确认,也不发送数据,造成不必要的错误和网络资源的浪费。

如果采用三次握手的话,就算那条失效的报文发送到服务器端,服务器端确认并向客户端发送报文,但此时客户端不会发出确认,由于客户端没有确认,由于服务器端没有接收到确认,就会知道客户端没有请求连接。

为什么不是四次?如果三次就能够确定正常连接,就没有必要在进行确认,来浪费资源了


TCP四次挥手过程

Linux 网络编程传输层TCP_#include_02

ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。

FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。

FIN_WAIT_2:等待远端TCP 的连接终止请求。

CLOSE_WAIT:等待本地用户的连接终止请求。

CLOSING:等待远端TCP 的连接终止请求确认。

LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)

TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。

FIN:断开一个连接标志;

第一次挥手:客户端发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态

第二次挥手 服务器端接收到连接释放报文后,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT 关闭等待状态

第三次挥手 客户端接收到服务器端的确认请求后,客户端就会进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文,服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

第四次挥手 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态,但此时TCP连接还未终止,必须要经过2MSL后(最长报文寿命),当客户端撤销相应的TCB后,客户端才会进入CLOSED关闭状态,服务器端接收到确认报文后,会立即进入CLOSED关闭状态,到这里TCP连接就断开了,四次挥手完成

总结:

长连接,保持链接通信

        类似我们手机打电话

        * 稳定的长连接通信

        * 相对来说比较稳定

        * 速度相对来说比较慢

        * 一般不容易丢失数据--有链接三次握手

        * 以及断开链接的四次挥手


步骤:

1》创建套接字:socket:

 int socket(int domain, int type, int protocol);

      参数

domain: 网域

AF_INET :   

AF_INET6 :  IPv6

type:选择传输协议

SOCK_STREAM ;tcp

SOCK_DGRAM :

       protocol:0

        返回值

       成功返回描述网络套接字sockfd,

失败返回-1



2》绑定:bind

将服务器自己的IP和port与描述网络套接字绑定

int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

参数

   sockfd:描述网络套接字sockfd

 my_addr:封装ip地址和端口号

struct sockaddr //此结构体不常用

{

unsigned short int sa_family; // AF_INET

char sa_data[14]; //IP和port

};

上面的结构体不方便:

#include<netinet/in.h>

struct sockaddr_in //常用的结构体

{

unsigned short int sin_family; //AF_INET

uint16_t sin_port; //端口port

struct in_addr sin_addr; //为 IP 地址

unsigned char sin_zero[8]; //未使用

};

struct in_addr

{

uint32_t s_addr; //=inet_addr("192.168.0.159")

};

addrlen:sockaddr 的结构体长度。 通常是计算

返回值

成功则返回 0,失败返回-1

struct sockaddr_in addr;

addr.sin_family = AF_INET;

addr.sin_addr.s_addr = inet_addr("192.168.0.159");

addr.sin_port = htons(12345);

转化

   主机(h) ----->  网络(n)

          uint16_t htons(uint16_t hostshort);//将一个无符号短整型的主机数值转换为网络字节顺序

   网络   ------>  主机

          uint16_t ntohs(uint16_t netshort);//将一个16位数由网络字节顺序转换为主机字节顺序。



3》监听

int listen(int sockfd, int backlog);

参数:

sockfd 描述网络套接字sockfd.

 backlog 指定同时能处理的最大连接要求,通常为 10 或者 5。 最大值可设至

返回值:成功则返回 0,失败返回-1


4》等待客户端连接

接受远程计算机的连接请求,建立起与客户机之间的通信连接

int accept(     int sockfd,

struct sockaddr *addr,

socklen_t *addrlen);

              参数

                     sockfd 为前面 socket 的返回值,描述网络套接字sockfd.

                     addr:用于接受客户端的ip地址和端口号,提供空间;

                     addrlen:第二个参数大小

       返回值

                     返回新的套接字描述符(通信套接字),专门用于与建立的客户端通信,失败-1


5》write和read进行通信   或者 send和recv进行通信


ssize_t send(int s, const void *buf, size_t len, int flags);

       参数

              s:新的套接字,即前面 accept 的返回值,通信套接字.

              buf:要发送的数据缓冲区

              len: 数据长度

              flags:一般赋0 .阻塞

       返回值:成功返回真正发送的数据长度,失败-1


例题:

1.完成人机交互式沟通,模拟两个人进行交流

//-----------------cli.c 用户
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
int fd=0;
void *R_FUN(void *p);
void *W_FUN(void *p);
int main()
{
    //1.创建网络套接字    
    fd=socket(AF_INET,SOCK_STREAM,0);
    //2.连接
    struct sockaddr_in ser_addr;
    ser_addr.sin_family=AF_INET;
    ser_addr.sin_port=htons(11115);
    ser_addr.sin_addr.s_addr=inet_addr("172.20.10.4");
    int con=connect(fd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
    if(con==-1)
    {
       perror("connect"); 
       return -1;
    }
    printf("连接成功,开始沟通\n");

    pthread_t r_id,w_id;//创建两个线程分别用来读写

    pthread_create(&r_id,NULL,R_FUN,NULL);
    pthread_create(&w_id,NULL,W_FUN,NULL);

    pthread_join(r_id,NULL);
    pthread_join(w_id,NULL);
    return 0;
}

void *W_FUN(void *p)
{
    //写入
    char buf[128]={0};
    while(1)
    {
        memset(buf,0,sizeof(buf));//先把数组清零
        printf("请输入信息\n");
        scanf("%s",buf);
        getchar();
        write(fd,buf,sizeof(buf));//通过buf往fd内写
        if(strcmp(buf,"bye")==0)
        {
            close(fd);
            exit(0);
        }
    }
}


void *R_FUN(void *p)
{
    //读取
    char buf[128]={0};
    int a=0;
    while(1)
    {
        memset(buf,0,sizeof(buf));//先把数组清零
        a=read(fd,buf,sizeof(buf));//通过buf往fd内写
        printf("buf:%s\n",buf);

        if(a==0)
        {
            close(fd);
            printf("通信已退出\n");
            exit(0);
        }
    }
}


//--------------ser.c  服务器
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
int acc=0;//用于接收accept返回值,创建全局是为了线程用得到
void *R_FUN(void *p);
void *W_FUN(void *p);
int main()
{
    //1.创建网络套接字    
    int sfd=socket(AF_INET,SOCK_STREAM,0);
    //2.绑定
    struct sockaddr_in my_addr;
    my_addr.sin_family=AF_INET;
    my_addr.sin_port=htons(11115);
    my_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
    int res = bind(sfd,(struct sockaddr *)&my_addr,sizeof(my_addr));
    if(res == -1)
    {
        perror("bind");
        return -1;
    }
    //3.监听
    res = listen(sfd,10);
    if(res == -1)
    {
        perror("listen");
        return -1;
    }
    //4.等待客户端连接
    struct sockaddr_in o_addr;
    socklen_t size = sizeof(o_addr);
   
     pthread_t r_id,w_id;//创建两个线程分别用来读写
     int acc[5]={0};
    for(int i=0;i<1000;i++)
    {
        acc[i]=accept(sfd,(struct sockaddr *)&o_addr,&size);
        if(acc[i] == -1)
    {
    
        perror("accept");
        printf("连接成功,开始沟通\n");
        return -1;
    }
        pthread_create(&w_id,NULL,W_FUN,NULL);
    }
   
    
    pthread_join(w_id,NULL);
    return 0;
}

void *W_FUN(void *p)
{
    //写入
    char buf[128]={0};
    while(1)
    {
        memset(buf,0,sizeof(buf));//先把数组清零
        printf("请输入信息\n");
        scanf("%s",buf);
        getchar();
        write(acc[i],buf,sizeof(buf));//通过buf往cfd内写
        if(strcmp(buf,"bye")==0)
        {
            close(acc[i]);
            exit(0);
        }
    }
     //读取
    char buf[128]={0};
    int a=0;
    while(1)
    {
        memset(buf,0,sizeof(buf));//先把数组清零
        a=read(acc[i],buf,sizeof(buf));//通过buf往cfd内写
        printf("buf:%s\n",buf);
        if(a==0)
        {
            close(acc[i]);
            printf("通信已退出\n");
            exit(0);
        }
    }
}

2.实现若干人的群聊

服务器ser.c   客户端cli.c

//-------------------------ser.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
void *Fun(void *p);
#define IP "172.20.10.4"
#define PORT 11223
long per[10];//10个人

int main()
{
    //1.创建套接字
    int sfd=socket(AF_INET,SOCK_STREAM,0);//IPV4 tcp协议
    if(sfd==-1)//创建套接字失败
    {
        perror("socket");
        return -1;
    }
    int val = 1;
    setsockopt(sfd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof(val));
    setsockopt(sfd,SOL_SOCKET,SO_REUSEPORT,&val,sizeof(val));
    //2.绑定
    struct sockaddr_in s_addr;
    s_addr.sin_port=htons(PORT);//端口
    s_addr.sin_family=AF_INET;
    s_addr.sin_addr.s_addr=inet_addr(IP);//IP地址
    int res=bind(sfd,(struct sockaddr *)&s_addr,sizeof(s_addr));
    if(res==-1)
    {
        perror("bind");
        return -1;
    }
    //3.监听
    res=listen(sfd,10);
    if(res==-1)
    {
        perror("listen");
        return -1;
    }
    //4.等待客户端连接
    struct sockaddr_in o_addr;
    socklen_t len=sizeof(o_addr);
    
    pthread_t id[10]={0};
    for(int i=0;i<10;i++)//10个人通信
    {   
        per[i]=accept(sfd,(struct sockaddr *)&o_addr,&len);
        printf("靓仔%d号已上线\n",sfd);
        pthread_create(&id[i],NULL,Fun,(void *)per[i]);//创建线程
    }
    for(int i=0;i<10;i++)
    {
        pthread_join(id[i],NULL);
    }
}

    void *Fun(void *p)
    {
        //读写
        long fd=(long)p;//其他客户
        char buf[128]={0};
        while(1)
        {
            memset(buf,0,sizeof(buf));
            int a=read(fd,buf,sizeof(buf));
            //read阻塞就说明有人下线或者发消息
            if(a==0)//下线情况
            {
                sprintf(buf,"靓仔%ld号已下线\n",fd);
            }
            //将某某某下线消息发给其他客户端
            for(int k=0;k<10;k++)
            {
                if(per[k]!=fd && per[k]>0)//排除没下线的客户端
                {
                    write(per[k],buf,sizeof(buf));
                }
            }
            //下线客户清除
            if(a==0)
            {
                    for(int k=0;k<10;k++)
                    {
                        if(per[k]==fd)
                        {
                            per[k]=-1;
                        }
                    }
                //结束当前子线程
                pthread_exit(NULL);
            }

    }
}
//---------------------cli.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#define IP "172.20.10.4"
#define PORT 11223
int fd=0;
void *W_FUN(void *p);
void *R_FUN(void *p);
int main()
{
    //1.创建套接字
    fd=socket(AF_INET,SOCK_STREAM,0);
    printf("fd = %d\n",fd);
    if(fd==-1)
    {
        perror("socket");
        return -1;
    }
    //2.连接
    struct sockaddr_in ser_addr;
    ser_addr.sin_family=AF_INET;
    ser_addr.sin_addr.s_addr=inet_addr(IP);
    ser_addr.sin_port=htons(PORT);
    int res=connect(fd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
    if(res==-1)
    {
        perror("connect");
        return -1;
    }
    printf("连接成功\n");
    //创建读写进程
    pthread_t r_id,w_id;
    pthread_create(&r_id,NULL,R_FUN,NULL);
    pthread_create(&w_id,NULL,W_FUN,NULL);

    pthread_join(r_id,NULL);
    pthread_join(w_id,NULL);
    return 0;
}
void *R_FUN(void *p)
{
    char buf[128]={0};
    int a=0;
    while(1)
    {
        memset(buf,0,sizeof(buf));
        a=read(fd,buf,sizeof(buf));
        printf("%s\n",buf);
        if(a==0)
        {
            close(fd);
            exit(0);
        }
    }
}

void *W_FUN(void *p)
{
    char buf[128]={0};
    while(1)
    {
        memset(buf,0,sizeof(buf));
        scanf("%s",buf);
        getchar();
        write(fd,buf,sizeof(buf));
        if(strcmp(buf,"bye")==0)
        {
            close(fd);
            exit(0);
        }
    }
}