本文立足TCP协议以及LINUXS socket 编程
用户空间:用户空间是操作系统中用户程序运行的环境
内核空间:是操作系统内核运行的环境,(包含内核代码、数据结构和系统资源。是网络协议栈工作的地方)
1TCP协议的缓冲区
TCP 是一种面向连接的、可靠的传输层协议,它通过缓冲区来管理数据的发送和接收。TCP 缓冲区存储在操作系统的内核空间中。(由操作系统决定)
1.1 TCP 缓冲区的定义与作用
TCP 缓冲区分为两种:
发送缓冲区(Send Buffer):存储应用程序发送但尚未被 TCP 协议确认的数据。
接收缓冲区(Receive Buffer):存储从网络接收到但尚未被应用程序读取的数据。
1.1.1 发送缓冲区
定义:发送缓冲区是 TCP 协议在发送端为应用程序提供的一个缓冲区域,用于存储应用程序已经写入但尚未被 TCP 协议发送到网络中的数据。
作用
- 提高性能:允许应用程序快速写入数据,而不需要等待网络传输完成。
- 流量控制:通过缓冲区,TCP 可以根据网络状况逐步发送数据,避免网络拥塞。
- 解耦应用与网络:应用程序可以快速写入数据,而 TCP 协议会负责将数据逐步发送到网络
1.1.2 接收缓冲区
定义:接收缓冲区是 TCP 协议在接收端为应用程序提供的一个缓冲区域,用于存储从网络接收到但尚未被应用程序读取的数据。
作用:
- 提高性能:允许网络数据逐步到达,而应用程序可以按需读取。
- 流量控制:通过缓冲区,TCP 可以根据接收端的处理能力,通知发送端调整发送速率。
- 解耦网络与应用:网络数据可以逐步到达,而应用程序可以按需读取,避免数据丢失。
1.2 TCP 缓冲区的行为与限制
1.2.1 发送缓冲区的行为
- 写入数据:当应用程序调用 send 或 write 时,数据会被写入发送缓冲区。
- 缓冲区已满:如果发送缓冲区已满,send 或 write 会阻塞,直到缓冲区中有空间。
- 数据发送:TCP 协议会从发送缓冲区中读取数据,逐步发送到网络中。
- 确认机制:发送的数据需要等待接收方的确认(ACK),只有在收到确认后,数据才会从发送缓冲区中移除。(非三次确定 不需要验证要读取能力)
接收缓冲区的行为
- 接收数据:当网络数据到达时,TCP 协议会将数据存储到接收缓冲区。
- 缓冲区已满:如果接收缓冲区已满,TCP 会通过滑动窗口机制通知发送方减慢发送速度。
- 读取数据:当应用程序调用 read 或 recv 时,数据会从接收缓冲区中读取。
- 数据处理:只有在应用程序读取数据后,缓冲区中的空间才会被释放。
1.2.3 TCP 缓冲区的大小
- 默认大小
sysctl net.core.rmem_default # 接收缓冲区默认大小
sysctl net.core.wmem_default # 发送缓冲区默认大小
- 动态调整
int size = 1024 * 1024; // 1MB
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));//发送
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));//接受
缓冲区大小的影响
发送缓冲区:
- 较大的发送缓冲区可以提高吞吐量,但会增加内存占用。
- 较小的发送缓冲区可能导致频繁阻塞,降低性能。
接收缓冲区:- 较大的接收缓冲区可以更好地处理突发流量,但会增加内存占用。
- 较小的接收缓冲区可能导致数据丢失或发送方发送速率受限
对于高吞吐量的场景(如文件传输),可以增大缓冲区。
对于低延迟的场景(如实时通信),可以减小缓冲区。
2 Socket IO 读取的细节
2.1 Socket IO 读取的行为
- 阻塞模式
行为:当应用程序调用 read 或 recv 时,如果接收缓冲区中没有数据,系统调用会阻塞,直到数据到达或超时。
优点:代码简单,适合低并发场景。
缺点:在高并发场景下,阻塞会导致资源浪费。 - 非阻塞模式
行为:当应用程序调用 read 或 recv 时,如果接收缓冲区中没有数据,系统调用会立即返回错误(如 EWOULDBLOCK 或 EAGAIN)。
优点:适合高并发场景,可以提高资源利用率。
缺点:需要额外的机制(如 select、poll 或 epoll)来检测数据是否到达
设置非阻塞
void Socket::setnonblocking() {fcntl(fd,F_SETFL,fcntl(fd,F_GETFD)|O_NONBLOCK);
}
2.2 Socket IO的函数
read
定义:read 是一个通用的系统调用,用于从文件描述符(如文件、管道、socket 等)中读取数据。
功能:简单地从文件描述符中读取数据,不提供额外的控制选项。
函数原型:ssize_t read(int fd, void *buf, size_t count);
fd:文件描述符。
buf:存储读取数据的缓冲区。
count:要读取的最大字节数。
返回值
返回值 > 0:表示成功读取的字节数。例如,返回值为 10,表示成功读取了 10 个字节。
返回值 = 0:表示已经到达文件末尾(EOF),没有更多数据可读。这通常发生在读取文件时,文件内容已经全部读完//对于一个socket而言就是关系连接达到结束。
返回值 < 0:表示读取操作失败。此时,errno 会被设置为相应的错误码。
recv
定义:recv 是专门为 socket 设计的系统调用,用于从 socket 中读取数据。
功能:除了读取数据,还提供了额外的控制选项(如处理带外数据、查询数据长度等)。
函数原型:ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:socket 文件描述符。
buf:存储读取数据的缓冲区。
len:缓冲区的大小。
flags:控制读取行为的标志(如 MSG_PEEK、MSG_WAITALL、MSG_OOB 等)。
write
定义:write 是一个通用的系统调用,用于向文件描述符(如文件、管道、socket 等)写入数据。
功能:简单地将数据写入文件描述符,不提供额外的控制选项。
函数原型:ssize_t write(int fd, const void *buf, size_t count);
fd:文件描述符。
buf:要写入的数据缓冲区。
count:要写入的最大字节数。
返回值:成功时返回实际写入的字节数,失败时返回 -1,并设置 errno。
send
定义:send 是专门为 socket 设计的系统调用,用于向 socket 发送数据。
功能:除了发送数据,还提供了额外的控制选项(如处理带外数据、指示后续数据等)。
函数原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:socket 文件描述符。
buf:要发送的数据缓冲区。
len:缓冲区的大小。
flags:控制发送行为的标志(如 MSG_OOB、MSG_CONFIRM、MSG_MORE
2.3 错误处理
正常返回
返回值 > 0:表示成功读取的字节数。
返回值 = 0:表示对端已关闭连接(EOF)。
返回值 < 0:表示读取失败,需要检查 errno 以确定错误原因。
错误处理
EINTR:系统调用被信号中断。
EAGAIN 或 EWOULDBLOCK:非阻塞模式下,接收缓冲区中没有数据。
ECONNRESET:连接被对端重置。
EPIPE:连接已断开。
3 实战
std::vector:
适合存储二进制数据或任意字节数据。
在需要频繁插入和删除的场景下表现较好。
适合需要对数据进行复杂操作的场景。
std::string:
适合存储文本数据,特别是需要使用字符串专用操作的场景。
在需要与 C 风格字符串交互的场景下更方便。
提供了许多方便的字符串处理函数。
创建一个简单缓冲区
#include <string>class Buffer
{
private:std::string buf;
public:Buffer();~Buffer();void append(const char* _str, int _size);//加入数据ssize_t size();//buf大小const char* c_str();//转化为c风格 方便与read 交互void clear();//清理void getline();//从标准输入 得到数据
};
#include "Buffer.h"
#include <string.h>
#include <iostream>
Buffer::Buffer()
{
}Buffer::~Buffer()
{
}void Buffer::append(const char* _str, int _size){for(int i = 0; i < _size; ++i){if(_str[i] == '\0') break;buf.push_back(_str[i]);}
}ssize_t Buffer::size(){return buf.size();
}const char* Buffer::c_str(){return buf.c_str();
}void Buffer::clear(){buf.clear();
}void Buffer::getline(){buf.clear();std::getline(std::cin, buf);
}
echo
void Connection::echo(int sockfd){char buf[1024]; //这个buf大小无所谓while(true){ //由于使用非阻塞IO,读取客户端buffer,一次读取buf大小数据,直到全部读取完毕bzero(&buf, sizeof(buf));ssize_t bytes_read = read(sockfd, buf, sizeof(buf));if(bytes_read > 0){readBuffer->append(buf, bytes_read);} else if(bytes_read == -1 && errno == EINTR){ //客户端正常中断、继续读取printf("continue reading");continue;} else if(bytes_read == -1 && ((errno == EAGAIN) || (errno == EWOULDBLOCK))){//非阻塞IO,这个条件表示数据全部读取完毕printf("message from client fd %d: %s\n", sockfd, readBuffer->c_str());errif(write(sockfd, readBuffer->c_str(), readBuffer->size()) == -1, "socket write error");readBuffer->clear();break;} else if(bytes_read == 0){ //EOF,客户端断开连接printf("EOF, client fd %d disconnected\n", sockfd);deleteConnectionCallback(sock);break;}}
}