1. TCP三次握手

tcp四层模型: 上  -> 下
    应用层, 传输层, 网络层, 网络接口层
TCP: 传输层协议, 安全的, 面向连接的, 流式传输协议
    - 安全: 数据校验机制, 数据丢失之后, 会重传
    - 面向连接:
        - 连接会有三次握手
            - 建立一个双向连接
                - 客户端和服务器的连接
                - 服务器和客户端的连接
        - 断开连接的时候四次挥手
            - 保证双向断开    
                - 客户端和服务器断开连接
                - 服务器和客户端断开连接

序号: 随机生成的
    - 客户端和服务器端都会生成, 在开始的时候生成, 之后就不会再变化了

确认序号: 确定对方成功接收了当前终端发送的数据的量
    - 客户端的:
        服务器的随机序号 + 已经成功接收的服务器端数据的量
    - 服务器端的:
        客户端的随机序号 + 已经成功接收的客户端数据的量

标志位: SYN -> 如果值为1, 表示请求建立连接
标志位: ACK -> 如果值为1, 表示确认
标志位: FIN -> 如果值为1, 表示请求断开连接
image-20191218134222520

// 服务器是被动接受连接的一方, 客户端是主动发起连接的一方
//   -- 因此三次握手的发起者肯定是客户端
第一次握手:
    - 客户端发起的(向服务器端发起了连接请求): 
        生成一个随机的32位序号: seq=J, 将协议中的SYN赋值为1 , 发送给服务器
    - 服务器端:
        接收数据
第二次握手:
    - 服务器:
        - 收到了连接请求
            - 并接受了请求, 回复了ACK=1
               - 回复了ack=J+1(这是确认序号)    
                - J: 客户端生成的随机序号
                - J+1: 代表客户端发送的一条数据收到了, 大小为1字节
            - 服务器端也生成了随机序号: seq=K
            - 服务器端在回复的数据中将SYN=1, 服务器请求和客户端建立连接
    - 客户端接收数据
        - 对服务器回复的数据进行校验:
            - 校验: ACK是不是 == 1
            - 校验: 确认序号: ack是不是==J+1,如果是则表明单向连接已经建立成功
第三次握手:
    - 客户端:
        - 接受服务器的连接请求: 
            - 回复: ACK=1
            - 回复: ack=K+1 (确认序号)
                - 服务器发送到客户端的请求(相当于1字节), 已经收到
     - 服务器接收数据:
            - 校验:
                 - 校验: ACK 是不是 == 1
                - 校验: 确认序号: ack 的值是不是 == K+1   

2. TCP四次挥手

// 在断开连接的过程中, 主动提出断开连接的一方可以是服务器端, 也可以是客户端
//         - 在程序中如何体现出断开连接了?
//                 close(fd);
// 假设是客户端主动断开的连接:
第一次挥手:
    - 客户端将协议中的标志位FIN赋值为1, 请求和服务器断开连接, 发送随机序号: seq=M
    - 服务器端: 接收了数据
第二次挥手:
    - 服务器端: 回复了断开连接的请求: ACK=1
    - 客户端: 接收数据
        - 校验: ACK 标志位的值是不是 == 1, 校验确认序号: M+1
        - 客户端就和服务器断开了连接, 但是服务器和客户端的连接还保持着
第三次挥手:
    - 服务器发送断开连接的请求, 将协议中的标志位FIN设置为1, 发送是随机序号N
    - 客户端: 接收数据
第四次挥手:
    - 客户端:
        - 回复服务器断开连接的请求, ACK=1, 同意断开连接
        - 将确认序号ack 设置为 N+1, 代表服务器给客户端发送的数据全部收到了(其实就是一字节)
    - 服务器:
        - 接收数据并校验
            - 检测 ACK 是不是 == 1
            - 检测确认序号是不是 == N+1
        - 校验成功之后, 服务器和客户端就断开了连接, 双向连接就断开了
image-20191218140937318

3. TCP滑动窗口

滑动窗口是 TCP 中用于实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。

窗口理解为一块缓存就可以了.

  • 这块内存中可以存储数据
    • 当数据量变多了, 内存的剩余容量变小了
    • 当数据被读走了, 内存的剩余容量变大了
  • 滑动窗口的作用:
    • 在网络通信过程中, 在某些情况下阻塞发送数据比较快的一端
    • 在通信的两端都是有滑动窗口的
      • 通信的两端都有一块内存, 缓存对方发送过来的数据
        • 当滑动窗口对应的内存被写满了, 就会阻塞对方继续发送数据
        • 当滑动窗口的数据被读走, 对方解除阻塞继续发送数据

这个图是一个单向的数据发送:

发送端:
    - 白色的格子:
        - 没有被使用的内存
    - 粉色的格子:
        - 被写入的并且没有被发送出去的数据
    - 灰色的格子:
        - 已经被发送出去的数据
接收方:
    - 白色的格子: 没有被使用的内存
    - 粉色的格子: 接收到的数据, 但是数据还没有被读走
    - 当白色的格子被写满, 都变成粉色格子之后
        - 缓冲区被写满了 == 滑动窗口对应的内存用完了
        - 这时候接收方就会阻塞发送方的数据发送, 当缓存中的数据被读走一部分之后, 发送方就解除阻塞了

fast sender -> 客户端
slow recv   -> 服务器
# mss: 最大的数据段大小 Maximum Segment Size  -> 一条数据的最大长度
# win: 滑动窗口
第1条数据: 
    - 第一次握手:
        SYN: 客户端发起连接请求
        0(0): 外边的0 -> 客户端生成的随机序号 == 0
                (0) => 里边的0代表客户端携带的数据量为0
        win4096: 客户端滑动窗口大小为 4k
        mss 1460: 客户端发送的最大数据长度 1460字节
第2条数据:
    - 第二次握手:
        - ACK 1, ACK服务器同意了客户端的连接
            1: 代表确认序号, 代表发送的请求数据收到了
        - SYN, 代表服务器向客户端发起连接请求
        - 8000(0):
            8000: 服务器端生成的随机序号
            0: 服务器携带的数据量
        - win 6144: 服务器端滑动窗口大小
        - mss1024: 服务器发送的最大数据长度
第3条数据:
    - 第三次握手:
        - ACK: 客户端同意服务器的连接请求
            8001: 确认请求, 代表服务器发送的请求对应的1字节数据收到了
        - win4096: 客户端滑动窗口大小为 4k == 最多可以接收4k数据
第4条数据: -> 通信的过程
    - 1(1024):
        - 1: 代表服务器已经收到的字节数(1-客户端随机序号[0])
        - 1024: 携带的数据量
第5-9条: 客户端不停的给服务器发送数据
第10条: 服务器给客户端回复的数据
    - ack6145: 确认序号, 代表收到的数据量(6145-客户端的随机序号)
    - win2048: 服务器的滑动窗口大小 == 服务器最大还能接受2k数据
第11条: 服务器给客户端回复的数据
    - ack6145: 确认序号, 代表收到的数据量(6145-客户端的随机序号)
    - win4096: 服务器的滑动窗口大小 == 服务器最大还能接受4k数据    
        - 在服务器端读缓冲区中的数据被读走了2k, 剩余容量增加2k
第12条: 客户端给服务器发送了1k数据
第13条:
    第1一次挥手:
        - 客户端给服务器发送了断开连接的请求: FIN=1
        - 客户端给服务器发送了1k数据
第14条: 
    第2次挥手:
        - 同意了客户端断开连接的请求: ACK=1
        - win2048, 服务器滑动窗口大小为2k
第15,16条: 服务器读缓冲区中数据被读走了, 滑动窗口变大了
第17条: 
    第3次挥手:
        - 服务器发送和客户端断开连接的请求: FIN=1
第18条:
    第4次挥手

4. TCP通信并发

1558162823677

  • 多进程 -> 服务器

    阻塞程序的情况?
            - accept
            - read
            - write
    // 进程分2类: 父进程, 子进程
    /*
        父进程干什么?
            接受客户端连接请求, 创建子进程和当前客户端通信
        子进程干什么?
              通信
    */
    
    while(1)
    {
        // 接受了客户端连接
        accept();
        // 创建子进程 -> 和这个客户端通信
    }
    // 思路:
    void* recyle(void* arg);
    int main()
    {
        // 1. 创建监听的套接字
        socket();
        // 2. 绑定
        bind();
        // 3. 监听
        listen();
    
        // 注册信号捕捉
        struct sigactioin act;
        act.sa_handler = recyle;
        sigaction(sigchld, &act, NULL);
        // 4. 接收多客户端连接 -> 循环
        while(1)
        {
            int cfd = accept();
            if(cfd == -1)
            {
                // error
                exit(0);
            }
            // 通信 -> 子进程
            pid_t pid = fork();
            if(pid == 0)
            {
                // 子进程
                while(1)
                {
                    int ret = working();
                    if(ret == -1)
                    {
                        break;
                    }
                }
                // 退出子进程
                exit(0);
            }
        }
    }
    
    // 子进程处理动作
    int working()
    {
        // 接收数据
        int len = read();
        if(len == 0)
        {
            return -1;
        }
        // 发送数据
        write();
    }
    
    void* recyle(void* arg)
    {
        while(1)
        {
            pid_t pid = waitpid(-1, NULL, wnohang);
            if(pid > 0)
            {
                // 回收成功了
            }
            else
            {
                // 子进程还没死, 子进程都回收完毕, 回收失败了
                break;
            }
        }
    }
    // 服务器代码
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <arpa/inet.h>
    #include <signal.h>
    #include <sys/wait.h>
    #include <errno.h>
    
    // 子进程的工作函数
    int working(int cfd, struct sockaddr_in *addr);
    // 信号的处理函数
    void recycle(int num);
    
    int main()
    {
        // 1. 创建监听的套接字
        int lfd = socket(AF_INET, SOCK_STREAM, 0);
        if(lfd == -1)
        {
            perror("socket");
            exit(0);
        }
        // 2. lfd绑定本地的IP和端口
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(9999);   // 使用大端的端口
        addr.sin_addr.s_addr = INADDR_ANY;  // 这个宏的值就是0
        int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
        if(ret == -1)
        {
            perror("bind");
            exit(0);
        }
        // 3. 设置监听
        ret = listen(lfd, 128);
        if(ret == -1)
        {
            perror("listen");
            exit(0);
        }
    
        // 注册信号捕捉
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = recycle;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);
    
        // 4. 等待并接受连接
        struct sockaddr_in cliaddr;
        int clilen = sizeof(cliaddr);
        // 接收多客户端连接
        while(1)
        {
            // 父进程两个任务:
            // 1. 等待并接受客户端的连接
            // 2. 回收子进程 -> 信号 -> 优先级高 -> 终端程序的执行流程
            printf("等待客户端的连接...\n");
            // 当进程阻塞在accept上的时候, 突然别信号终端, 进程终止当前的动作
            // 当前进程去处理对应的信号的动作, 当处理完毕之后, 回到被信号终端的位置
            // 如果原来是阻塞的状态, 再次回到这个位置的时候就不阻塞了, 直接返回-1
            // 打印的信息: accept: Interrupted system call
            int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
            if(cfd == -1)
            {
                if(errno == EINTR)
                {
                    continue;
                }
                perror("accept");
                exit(0);
            }
            printf("和客户端的连接成功建立了...\n");
            // 和建立连接的客户端通信
            // 创建子进程 -> 通信
            pid_t pid = fork();
            if(pid > 0)
            {
                // father进程
                // 关闭通信的文件描述符
                close(cfd);
            }
            else if(pid == 0)
            {
                // 子进程 -> 通信和对应的客户端, 通过得到的cfd
                // 关闭监听的fd
                close(lfd);
                while(1)
                {
                    int ret = working(cfd, &cliaddr);
                    if(ret == -1)
                    {
                        break;
                    }
                }
                close(cfd);
                exit(0);
            }
    
        }
        return 0;
    }
    
    int working(int cfd, struct sockaddr_in *addr)
    {
        // 4.1 打印客户端的地址信息
        // 将cliaddr中的大端IP -> 点分十进制IP字符串
        // cliaddr中的端口也是大端的
        char ip[64];
        printf("client IP: %s, client port: %d\n",
               inet_ntop(AF_INET, &addr->sin_addr.s_addr, ip, sizeof(ip)),
               ntohs(addr->sin_port));
    
        // 接收数据
        char buf[1024];
        int len = recv(cfd, buf, sizeof(buf), 0);
        printf("client say: %s\n", buf);
        if(len == 0)
        {
            printf("client disconnect....\n");
            return -1;
        }
        // 发送数据
        send(cfd, buf, len, 0);
        return 0;
    
    }
    
    void recycle(int num)
    {
        while(1)
        {
            pid_t pid = waitpid(-1, NULL, WNOHANG);
            if(pid > 0)
            {
                printf("child die, pid = %d\n", pid);
            }
            else 
            {
                // 没有可以回收的子进程, 退出循环
                break;
            }
        }
    }

TCP

  1. 字节序
    • 两种字节序
      • 主机字节序 -> 小端
        • 数据在pc机内存中默认的存储顺序
        • 数据的低位字节, 存储在内存的低地址位, 高位字节, 内存的高地址位
      • 网络字节序 -> 大端
        • 通过套接字通信, 在网络中传输的数据默认是大端的
        • 低位字节存储在内存的高地址位, 高位字节存储在内存的低地址位
  2. tcp通信的流程
    • 服务器端
      • 创建监听的套接字
        • int lfd = socket(af_inet, sock_stream, 0);
      • 将监听的套接字和本地的IP and 端口绑定
        • 初始化sockaddr指定的IP和端口应该是大端的
        • 服务器端绑定的IP地址可以是 0 地址
          • 自动识别机的IP地址
          • 可以在绑定的时候, 绑定本机所有的IP地址
        • bind(lfd, sockaddr, sizeof(sockaddr));
      • 设置监听
        • listen(lfd, 128);
      • 等待并接受客户端连接, 返回通信的文件描述符
        • struct sockaddr_in clieaddr;
        • int clilen = sizeof(clieaddr);
        • int connfd = accept(lfd, &clieaddr, &clilen);
      • 通信: 这个过程是阻塞的
        • 发送数据: send/write
        • 接收数据: recv/read
          • 没有断开连接:
            • 没有数据, 函数阻塞
            • 有数据, 读数据, 不阻塞, 读完之后阻塞
          • 断开连接之后:
            • 如果有数据读完数据, 返回0
            • 如何没有数据, 解除阻塞, 返回0
      • 关闭连接
        • close(fd);
    • 客户端通信流程:
      • 创建通信的套接字
        • int fd = socket(af_inet, sock_stream, 0);
      • 连接服务器, 通过服务器端绑定的IP和端口
        • struct sockaddr_in serveraddr;
        • connect(fd, serveraddr, sizeof(serveraddr));
      • 通信
        • 发送数据: send/write
        • 接收数据: recv/read
          • 没有断开连接:
            • 没有数据, 函数阻塞
            • 有数据, 读数据, 不阻塞, 读完之后阻塞
          • 断开连接之后:
            • 如果有数据读完数据, 返回0
            • 如何没有数据, 解除阻塞, 返回0
      • 断开连接
        • close(fd);