1. TCP三次握手
tcp四层模型: 上 -> 下 应用层, 传输层, 网络层, 网络接口层 TCP: 传输层协议, 安全的, 面向连接的, 流式传输协议 - 安全: 数据校验机制, 数据丢失之后, 会重传 - 面向连接: - 连接会有三次握手 - 建立一个双向连接 - 客户端和服务器的连接 - 服务器和客户端的连接 - 断开连接的时候四次挥手 - 保证双向断开 - 客户端和服务器断开连接 - 服务器和客户端断开连接
序号: 随机生成的 - 客户端和服务器端都会生成, 在开始的时候生成, 之后就不会再变化了 确认序号: 确定对方成功接收了当前终端发送的数据的量 - 客户端的: 服务器的随机序号 + 已经成功接收的服务器端数据的量 - 服务器端的: 客户端的随机序号 + 已经成功接收的客户端数据的量 标志位: SYN -> 如果值为1, 表示请求建立连接 标志位: ACK -> 如果值为1, 表示确认 标志位: FIN -> 如果值为1, 表示请求断开连接
// 服务器是被动接受连接的一方, 客户端是主动发起连接的一方 // -- 因此三次握手的发起者肯定是客户端 第一次握手: - 客户端发起的(向服务器端发起了连接请求): 生成一个随机的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 - 校验成功之后, 服务器和客户端就断开了连接, 双向连接就断开了
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通信并发
多进程 -> 服务器
阻塞程序的情况? - 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
- 字节序
- 两种字节序
- 主机字节序 -> 小端
- 数据在pc机内存中默认的存储顺序
- 数据的低位字节, 存储在内存的低地址位, 高位字节, 内存的高地址位
- 网络字节序 -> 大端
- 通过套接字通信, 在网络中传输的数据默认是大端的
- 低位字节存储在内存的高地址位, 高位字节存储在内存的低地址位
- 主机字节序 -> 小端
- 两种字节序
- 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);
- 创建通信的套接字
- 服务器端