C4 - 基于TCP/UDP的服务器端/客户端
TCP/IP四层协议栈各层的关系 —— 数据传输角度
-
四层: 链路层、网络层、传输层、应用层
-
各层在数据传输之间的联系
-
链路层
-
物理结构:一台路由器/交换机以及多台主机组成的内网结构
-
数据传输
数据在内网如何发送到目的主机
从源主机发送的数据通过路由器传输到公网
-
-
网络层
-
物理结构:多台路由器组成的公网结构
-
数据传输:路由选择
-
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 来制定具体协议,让服务器端与客户端双方按照该协议读取/发送数据。
-
-
服务器端/客户端建立连接的细节
-
listen 函数的 backlog 参数描述了服务器可接受连接队列的大小
- 此队列满,服务器不接收新连接
-
客户端调用 connect 函数后,服务器端并立即调用accept接受该连接,而是先将该连接放入 backlog_queue 中
- connect 返回情况
- 服务器端接收连接请求(指的是放入 backlog_queue,非accept)
- 发生断网等异常情况——>中断连接请求
- 客户端套接字地址信息在哪?
- 调用 connect 函数时由 OS 分配主机IP+随机Port
- connect 返回情况
-
服务器端调用 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; }
实现简易计算器——应用层协议设计
-
协议设计
-
客户端
- 数据包格式:1Byte表示操作数个数 + 4*nBytes存储n个操作数 + 1Byte字符类型表示操作类型
- 操作数个数可为 0~255
- 操作类型:+ - *
-
服务器端
-
数据包格式: 4Bytes存储计算结果
不考虑计算溢出问题
-
-
-
代码实现——数据传输
-
客户端
// 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);
-
实现简易文件传输
- 协议制定
- 客户端
- 服务器
- 代码实现——数据传输
- 客户端
- 服务器