【TCP/IP网络编程-尹圣雨-学习笔记】2. 基于TCP/UDP的服务器端/客户端

C4 - 基于TCP/UDP的服务器端/客户端

TCP/IP四层协议栈各层的关系 —— 数据传输角度

  1. 四层: 链路层、网络层、传输层、应用层

  2. 各层在数据传输之间的联系

    • 链路层

      • 物理结构:一台路由器/交换机以及多台主机组成的内网结构

      • 数据传输

        数据在内网如何发送到目的主机

        从源主机发送的数据通过路由器传输到公网

    • 网络层

      • 物理结构:多台路由器组成的公网结构

      • 数据传输:路由选择

      • IP协议

        • 面向消息、不可靠
          • 每次传输时选择不一定相同的路径
          • 不解决数据丢失OR错误问题
        • 只关注1个数据包的传输过程
      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Uu0I2kH-1674225194101)(E:\Log_For_TcpIp尹圣雨\1网络连接结构_1.jpg)]

    • 传输层

      解决IP协议中数据传输不可靠问题

      TCP协议向不可靠的IP协议赋予可靠性

      • TCP协议

        • 数据包分片传输、确保数据可靠、丢失重传——解决IP协议的数据不可靠传输

          滑动窗口——防止发送方数据溢出接收方缓冲,导致的数据丢失

          SYN、ACK——1. 确定数据包的收发顺序 2. 及时发现数据包丢失问题并重传解决

          数据包切片——确保单个数据包不超过IP协议的最大传输单元

        • 无数据边界——一次write/read的数据可以多次read/write

      • UDP协议

    • 应用层

      在实际应用场景中,为无数据边界的TCP协议赋予数据边界,以此在应用层面上区分一个有意义的数据包

      • HTTP协议

        HTTP协议规定了TCP协议中以字节流传输的数据的数据边界,通过定义 header 和 body 以及相应的 length 来制定具体协议,让服务器端与客户端双方按照该协议读取/发送数据。

服务器端/客户端建立连接的细节

  1. listen 函数的 backlog 参数描述了服务器可接受连接队列的大小

    • 此队列满,服务器不接收新连接
  2. 客户端调用 connect 函数后,服务器端并立即调用accept接受该连接,而是先将该连接放入 backlog_queue 中

    • connect 返回情况
      1. 服务器端接收连接请求(指的是放入 backlog_queue,非accept)
      2. 发生断网等异常情况——>中断连接请求
    • 客户端套接字地址信息在哪?
      • 调用 connect 函数时由 OS 分配主机IP+随机Port
  3. 服务器端调用 accept 函数,才会从 backlog_queue 头部取出一个连接接受并放入 已连接队列 中

    即:

    客户端 connect 之后 ≠ 三次握手成功

    但:

    客户端 connect 之后 can send data , 在服务器 accept 之前, data 会被放入客户端 send_buffer 中

数据传输函数 read/write 的缓冲

套接字是全双工——双向传递数据

  • IO缓冲

    • write 调用时,将数据写入到 输出缓冲就返回

    • read 调用时,从输入缓冲中 读取数据后返回

    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LXWlH0fk-1674225194108)(E:\Log_For_TcpIp尹圣雨\2tcp套接字的io缓冲_1.jpg)]

  • TCP协议——数据传输控制

    • 滑动窗口——write 不必关心是否会写入过多的数据导致对端接收缓冲溢出—— because TCP 通过滑动窗口控制数据传输大小

    • “write 函数在数据传输完成时返回” 的理解

      实际上 write 函数将数据移到缓冲就返回了,但由于 TCP 的可靠性,会保证输出缓冲的数据在某个时刻发送到对端的输入缓冲区,等待对端 read 接收。

      即——write 函数在数据传输完成时返回——是一个因为TCP很可靠的传输而做出的预先事实结论。

  • IO缓冲特性

    • IO缓冲在每个 TCP 套接字中单独存在
    • IO缓冲在创建套接字时自动生成
    • 关闭套接字的表现
      • 输出缓冲遗留数据——>继续传递
      • 输入缓冲遗留数据——>丢失

实现 echo

  • 服务器端——数据传输代码

    char buf[BUFSIZE] = {0};
    int read_len = 0;
    // 循环读取客户端数据 until 客户端关闭连接
    while ((read_len = read(clnt_sock, buf, BUFSIZE-1)) != 0)
    {
        write(clnt_sock, buf, read_len);
    }
    
  • 客户端——数据传输代码

    char buf[BUFSIZE] = {0};
    while (1)
    {
        std::cout << "Please enter a string(Q to quit):" << std::endl;
        fgets(buf, BUFSIZE, stdin);
        if (!strcmp(buf, "Q\n") || !strcmp(buf, "q\n"))
        {
            break;
        }
        int str_len = write(sock, buf, strlen(buf));
        // 读取服务器数据
        int recved_len = 0;
        char read_buf[BUFSIZE] = {0};
        while (recved_len < str_len) // 循环读取单次数据
        {
            int recv_cnt = read(sock, &read_buf[recved_len], BUFSIZE-recved_len-1);	// 注意这里的 &read_buf[recved_len]
            if (recv_cnt == -1)
            {
                std::cout << "read() error!" << std::endl;
                return -1;
            }
            recved_len += recv_cnt;
        }
        std::cout << "Message from server: " << read_buf << std::endl;
    }
    

实现简易计算器——应用层协议设计

  1. 协议设计

    1. 客户端

      • 数据包格式:1Byte表示操作数个数 + 4*nBytes存储n个操作数 + 1Byte字符类型表示操作类型
      • 操作数个数可为 0~255
      • 操作类型:+ - *
    2. 服务器端

      • 数据包格式: 4Bytes存储计算结果

        不考虑计算溢出问题

  2. 代码实现——数据传输

    • 客户端

      // 3. 读取数据 —— 组装请求包
      char buf[BUFSIZE] = {0};
      // 3-1. 操作数个数
      std::cout << "Please enter numbers count:" << std::endl;
      int cnt = 0;
      scanf("%d", &cnt);
      buf[0] = (char)cnt;
      // 3-2. 操作数
      std::cout << "Please enter every number:" << std::endl;
      for (int i = 0; i < cnt; ++i)
      {
          scanf("%d", (int*)&buf[i*4+1]);
      }
      // 3-3. 操作符
      fgetc(stdin); // 删除缓冲中的字符!!!
      std::cout << "Please enter oprander: " << std::endl;
      scanf("%c", &buf[cnt*4+1]);
      // 写入数据
      write(sock, buf, cnt*4+2);
      std::cout << ".." << std::endl;
      // 读取结果
      int result = 0;
      read(sock, &result, 4);
      std::cout << "Result is: " << result << std::endl;
      
    • 服务器端

      // 5. 读写数据 - 读取客户端请求包 - 返回响应包
      char buf[BUFSIZE] = {0};
      // 5-1. 读取操作数个数
      int cnt = 0;
      read(clnt_sock, &cnt, 1);
      // 5-2. 循环读取操作数
      int oprands[256] = {0};
      for (int i = 0; i < cnt; ++i)
      {
          read(clnt_sock, &oprands[i], 4);
      }
      /**
           * @brief 直接以 字符形式读取够需要的字节数
           * 
           */
      // int recv_len = 0;
      // while (recv_len < cnt*4)
      // {
      //     recv_len += read(clnt_sock, buf+recv_len, BUFSIZE-1);
      // }
      
      // 5-3. 读取操作符
      char op;
      read(clnt_sock, &op, 1);
      // 计算结果
      int result = oprands[0];
      switch (op)
      {
          case '+':
              for (int i = 1; i < cnt; ++i)
              {
                  result += oprands[i];
              }
              break;
          case '-':
              for (int i = 1; i < cnt; ++i)
              {
                  result -= oprands[i];
              }
              break;
          case '*':
              for (int i = 1; i < cnt; ++i)
              {
                  result *= oprands[i];
              }
              break;
          default:
              break;
      }
      // 发送响应包
      write(clnt_sock, (char*)&result, 4);
      

实现简易文件传输

  1. 协议制定
    • 客户端
    • 服务器
  2. 代码实现——数据传输
    • 客户端
    • 服务器

你可能感兴趣的:(网络,tcp/ip,udp,网络协议,后端)