tcp
一 tcp(计算机网络)
可靠性:
TCP 的核心目标就是确保数据从发送端准确无误、完整有序地送达接收端。
这是通过以下机制实现的:
序列号和确认应答: 发送的每个字节都被分配一个唯一的序列号。接收方收到数据后,会向发送方发送一个确认应答,指明它期望收到的下一个字节的序列号。如果发送方在一定时间(超时重传时间)内没有收到确认,它会重传未得到确认的数据。
校验和: 每个 TCP 报文段都包含一个校验和字段。接收方会计算校验和并与报文中的校验和进行比较。如果不匹配,说明数据在传输中损坏,接收方会丢弃该报文段,发送方最终会因未收到确认而重传。
重传机制: 如上所述,通过超时重传和快速重传(当接收方收到失序的报文段时,会立即发送重复确认,提示发送方可能丢失了数据包)来应对丢包问题。
面向连接:
在数据传输开始之前,TCP 要求通信双方(客户端和服务器)必须先建立一个逻辑连接。这个过程就是著名的 “三次握手”。
数据传输完成后,双方会执行 “四次挥手” 来有序地终止连接。
这个连接状态(包括序列号、窗口大小、确认号等)在通信过程中由两端的主机维护。
基于字节流:
TCP 把应用层交下来的数据(无论是一个文件、一段文本还是其他形式)视为一连串无结构的字节流。
TCP 不保留应用层消息的边界。发送方应用程序多次写入的数据,可能在接收方应用程序一次读取中全部收到;同样,发送方一次写入的大量数据,也可能被 TCP 分成多个报文段发送,接收方需要多次读取才能收齐。
应用层需要自己定义消息边界(如使用特定的分隔符、固定长度消息头或在应用层协议中定义长度字段)。
流量控制:
防止发送方发送数据过快,导致接收方来不及处理而缓冲区溢出,造成丢包。
通过 滑动窗口机制 实现。接收方在每次确认应答中会通告自己的接收窗口大小,表示当前还能接收多少字节的数据。发送方发送的数据量不能超过这个通告的窗口大小。
拥塞控制:
防止发送方发送数据过快,导致网络本身(路由器、链路)过载,造成全局性的网络性能下降和丢包。
这是 TCP 最复杂的机制之一,主要算法包括:
慢启动: 连接开始时或发生超时后,发送窗口从一个很小的值开始,然后指数增长(每收到一个确认,窗口大小增加一个 MSS)。
拥塞避免: 当窗口增长到慢启动阈值后,进入拥塞避免阶段,窗口变为线性增长(每经过一个往返时间 RTT,窗口增加一个 MSS)。
快速重传和快速恢复: 当发送方收到 3 个重复确认时(表明可能丢包,但网络仍有能力传输其他包),会立即重传丢失的数据包,并将慢启动阈值设为当前拥塞窗口的一半,然后直接进入拥塞避免阶段(快速恢复),避免了从慢启动开始的开销。
核心思想是:TCP 主动探测网络可用带宽,在网络出现拥塞迹象(丢包)时,迅速减少发送速率,以缓解拥塞。
二 tcp报文
关键字段解释
源端口号 / 目标端口号: 标识发送和接收应用程序进程(如 HTTP=80, HTTPS=443, SSH=22)。IP地址+端口号唯一标识一个连接端点(套接字)。
序列号: 本报文段所发送数据的第一个字节在整个数据流中的序号。用于排序和确认。
确认号: 期望收到对方下一个报文段的第一个数据字节的序列号。表示该序号之前的所有数据都已正确接收。仅在
ACK
标志位为 1 时有效。数据偏移: 指示 TCP 首部的长度(以 32 位字为单位),因为首部有可变长度的选项字段。最小 5 (20 字节),最大 15 (60 字节)。
控制标志: 6 个比特位:
URG
: 紧急指针有效(很少用)。
ACK
: 确认号字段有效。建立连接后,该标志位通常总是置 1。
PSH
: 提示接收方应立即将数据交付给应用层(Push 功能,通常操作系统会优化,不一定严格执行)。
RST
: 重置连接。表示发生严重错误,需要强制断开连接。
SYN
: 同步序列号。用于发起连接请求(三次握手)。
FIN
: 结束连接。用于请求终止连接(四次挥手)。窗口大小: 接收方通告的接收窗口大小(字节数),用于流量控制。表示从确认号开始,接收方还能接收多少字节的数据。
校验和: 覆盖 TCP 首部、数据和伪首部(包含源/目标 IP 地址、协议号、TCP 长度)的校验和,用于检测传输错误。
紧急指针: 当
URG=1
时有效,指示本报文段中紧急数据的末尾位置(相对于序列号的偏移)。选项: 可变长度,用于扩展功能。常见选项有:
最大报文段长度: 在三次握手时协商双方能接受的单个 TCP 报文段的最大长度。
窗口扩大因子: 用于扩展窗口大小(原窗口字段只有 16 位)。
时间戳: 用于更精确计算往返时间和处理序列号回绕问题。
选择性确认: 允许接收方告知发送方哪些不连续的数据块已经收到,提高重传效率。
数据: 应用层交付给 TCP 传输的实际数据。长度由 MSS 和网络 MTU 决定。
三 tcp通信
三 实践项目(C++面对对象版本)
版本1.
此版本实现了客户端client对服务端server的通信,(客户端对服务端)
具体代码,在文件的资源中
server.cpp
#include "server.h"
#include <thread>
#include <map>
#include <atomic>
#include <algorithm>
extern std::atomic<bool> show_client_msg;
// 构造函数,初始化端口和IP,默认IP为0.0.0.0
server::server(int port, const std::string &ip)
: port(port), ip(ip), server_fd(-1), running(false)
{
memset(&server_addr, 0, sizeof(server_addr)); // 清空地址结构体
server_addr.sin_family = AF_INET; // 地址族为IPv4
server_addr.sin_port = htons(port); // 设置端口号(网络字节序)
// 显式处理特殊IP地址
if (ip == "0.0.0.0")
{
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有接口
}
else
{
server_addr.sin_addr.s_addr = inet_addr(ip.c_str()); // 设置指定IP
}
// 验证IP转换结果
if (server_addr.sin_addr.s_addr == INADDR_NONE)
{
std::cerr << "Invalid IP address: " << ip << std::endl;
}
}
// 析构函数,关闭所有套接字
server::~server()
{
stop(); // 停止服务器,关闭所有连接
}
// 启动服务器,绑定端口并监听
bool server::start()
{
server_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
if (server_fd < 0)
{
std::cerr << "Socket creation failed." << std::endl;
return false;
}
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 允许地址重用
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
perror("Bind failed");
close(server_fd);
return false;
}
if (listen(server_fd, MAX_CONNECTIONS) < 0)
{
perror("Listen failed");
close(server_fd);
return false;
}
running = true;
std::cout << "Server started at " << ip << ":" << port << std::endl;
return true;
}
// 停止服务器,关闭所有连接
void server::stop()
{
running = false;
// 关闭所有客户端线程
for (auto &pair : client_threads)
{
if (pair.second.joinable())
pair.second.join();
}
client_threads.clear();
for (int fd : client_fds)
{
close(fd); // 关闭每个客户端套接字
}
client_fds.clear();
if (server_fd != -1)
{
close(server_fd); // 关闭服务器套接字
server_fd = -1;
}
std::cout << "Server stopped." << std::endl;
}
// 等待并接受客户端连接
void server::acceptClients()
{
while (running)
{
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); // 接受客户端连接
if (client_fd < 0)
{
if (running) // 仅在服务器运行时报告错误
std::cerr << "Accept failed." << std::endl;
continue;
}
client_fds.push_back(client_fd); // 保存客户端套接字
std::cout << "Client connected: " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl;
// 为每个客户端创建线程处理消息
client_threads[client_fd] = std::thread(&server::clientHandler, this, client_fd);
}
}
// 客户端消息处理函数
void server::clientHandler(int client_fd)
{
while (running)
{
std::string msg = receiveMsg(client_fd);
if (msg.empty())
{
if (show_client_msg)
std::cout << "Client disconnected: fd=" << client_fd << std::endl;
close(client_fd);
client_fds.erase(std::remove(client_fds.begin(), client_fds.end(), client_fd), client_fds.end());
client_threads.erase(client_fd);
break;
}
if (show_client_msg)
std::cout << "收到客户端(" << client_fd << ")消息: " << msg << std::endl;
// 可在此处添加自动回复、广播等逻辑
}
}
// 接收指定客户端的信息
std::string server::receiveMsg(int client_fd)
{
char buffer[1024] = {0}; // 接收缓冲区
ssize_t bytes = recv(client_fd, buffer, sizeof(buffer) - 1, 0); // 接收数据
if (bytes <= 0)
{
return "";
}
return std::string(buffer, bytes); // 返回接收到的消息
}
// 向指定客户端发送信息
bool server::sendMsg(int client_fd, const std::string &msg)
{
ssize_t bytes = send(client_fd, msg.c_str(), msg.size(), 0); // 发送数据
return bytes == (ssize_t)msg.size();
}
// 广播消息给所有客户端
void server::broadcastMsg(const std::string &msg)
{
for (int fd : client_fds)
{
sendMsg(fd, msg); // 逐个发送消息
}
}
// 获取当前已连接客户端数量
size_t server::getClientCount() const
{
return client_fds.size();
}
// 获取服务器运行状态
bool server::isRunning() const
{
return running;
}
版本2.
此版本实现了客户端client对c客户端client的通信,(客户端对客户端)客户端转发
版本3.
实现客户端到客户端的群聊,
四种io模型
概念
1. 阻塞模型
概念:
进程发起 I/O 操作后立即被阻塞,直到数据准备好并复制到用户空间。
全程等待,期间 CPU 无法执行其他任务。
代码:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
connect(sockfd, &addr, sizeof(addr));
char buffer[1024];
// 阻塞在此,直到数据就绪
int n = read(sockfd, buffer, sizeof(buffer));
process_data(buffer, n);
使用指南:
适用场景:简单客户端、低并发场景。
优点:编程简单,逻辑清晰。
缺点:每个连接需独立线程/进程,资源消耗大,无法支撑高并发。
2 非阻塞模型 non-blocking-io
概念:
进程发起 I/O 操作后立即返回(成功或错误)。
通过轮询检查数据是否就绪,避免阻塞等待。
代码:
fcntl(sockfd, F_SETFL, O_NONBLOCK); // 设为非阻塞
while (1) {
int n = read(sockfd, buffer, sizeof(buffer));
if (n > 0) { // 数据就绪
process_data(buffer, n);
break;
} else if (n == -1 && errno == EAGAIN) {
usleep(1000); // 数据未就绪,短暂休眠后重试
} else {
// 处理错误
}
}
使用指南:
适用场景:低延迟但连接数少的场景。
优点:单线程可管理多个连接。
缺点:轮询消耗 CPU,不适用于大规模连接。
3 io多路复用
概念:
使用
select
/poll
/epoll
监控多个文件描述符(FD)。当任一 FD 就绪时,通知进程处理,避免轮询。
3.1 select
特点:
通过
fd_set
位图管理 FD,上限FD_SETSIZE
(通常 1024)。每次调用需重置 FD 集合。
代码:
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd1, &readfds);
FD_SET(sockfd2, &readfds);
while (1) {
fd_set tmp = readfds;
int ret = select(max_fd+1, &tmp, NULL, NULL, NULL); // 阻塞等待
if (FD_ISSET(sockfd1, &tmp)) {
// 处理 sockfd1 数据
}
if (FD_ISSET(sockfd2, &tmp)) {
// 处理 sockfd2 数据
}
}
3.2 poll
特点:
使用
pollfd
结构体数组,无 FD 数量限制。避免
select
的位图重置问题。
伪代码:
struct pollfd fds[2];
fds[0].fd = sockfd1; fds[0].events = POLLIN;
fds[1].fd = sockfd2; fds[1].events = POLLIN;
while (1) {
int ret = poll(fds, 2, -1); // 阻塞等待
if (fds[0].revents & POLLIN) {
// 处理 sockfd1
}
if (fds[1].revents & POLLIN) {
// 处理 sockfd2
}
}
共同缺点:
每次调用需传递所有 FD 到内核,性能随 FD 数量下降。
需遍历所有 FD 检查就绪状态(O(n))。
4. epoll(Linux 特有)
概念:
基于事件驱动的 I/O 多路复用,高效处理海量连接。
核心 API:
epoll_create
,epoll_ctl
,epoll_wait
.
工作模式:
水平触发 (LT, Level-Triggered):
就绪事件未处理时会持续通知(默认模式)。边缘触发 (ET, Edge-Triggered):
仅在状态变化时通知一次,需一次性处理所有数据。
代码:
int epoll_fd = epoll_create1(0);
struct epoll_event ev, events[10];
// 添加 sockfd 到 epoll 监控
ev.events = EPOLLIN | EPOLLET; // ET 模式
ev.data.fd = sockfd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
int nfds = epoll_wait(epoll_fd, events, 10, -1); // 阻塞等待
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
int fd = events[i].data.fd;
while (1) { // ET 模式需读尽数据
int n = read(fd, buffer, sizeof(buffer));
if (n <= 0) break; // 无数据或错误
process_data(buffer, n);
}
}
}
}
核心优势:
高效数据结构:
使用红黑树管理 FD,哈希表存储就绪事件,时间复杂度 O(1)。无需重复传递 FD:
通过epoll_ctl
动态增删 FD。支持海量连接:
仅受系统内存限制(1GB 内存约支持 10 万 FD)。
使用指南:
适用场景:高并发服务器(如 Nginx、Redis)。
模式选择:
LT:编程简单,适合低频大包场景。
ET:减少事件通知次数,需搭配非阻塞 I/O 和循环读写。
最佳实践:
ET 模式必须设置 FD 为非阻塞(
O_NONBLOCK
)。读操作需循环调用
read
直到EAGAIN
错误。边缘触发下,一次性处理完所有数据。
总结对比
模型 性能 复杂度 适用场景 阻塞 I/O 低 简单 低并发客户端 非阻塞 I/O 中(轮询) 中 连接数少且低延迟 select/poll 中 中 跨平台,中等并发 epoll 高 高 Linux 高并发服务器
关键结论:
现代高性能服务器首选 epoll(ET 模式 + 非阻塞 I/O)。
Windows 平台使用
IOCP
,FreeBSD 使用kqueue
。