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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

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. 代码实现——数据传输
    • 客户端
    • 服务器
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: 服务器