协议
再谈 "协议" 协议是一种 "约定". socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们 要传输一些 "结构化的数据" 怎么办呢?
协议就是双方约定好的结构化的数据
重新理解tcp为什么支持全双 工
write,read里面到底再做什么吗??

我们之前在 C 语言中学习过 read、write 等系统调用,它们本质上是对文件描述符的读写操作。为了提高数据传输效率,系统在发送和接收数据时都会引入相应的缓冲区机制。因此,当进程通过 write 发送数据时,数据并不是直接立刻发送到另一台主机,而是先写入发送缓冲区;当进程通过 read 接收数据时,也不是直接从网络中读取数据,而是从接收缓冲区中读取数据。
在主机之间进行数据读写的过程中,也存在一些类似 "管道" 的问题。例如,如果一方发送数据的速度较快,而另一方接收数据的速度较慢,接收方的缓冲区可能会被填满,此时发送方继续发送数据就会受到阻塞。反之,如果接收方读取数据的速度较快,而发送方发送数据的速度较慢,接收方也可能因为缓冲区中没有数据而进入等待状态。
这种现象类似于现实生活中的生产者 --- 消费者模型:发送方相当于生产者,接收方相当于消费者,而缓冲区则相当于一个临时存放数据的队列。当生产者生产速度过快时,队列会被填满;当消费者消费速度过快时,队列又可能为空。网络通信中的缓冲区机制,本质上就是在协调发送方和接收方之间的数据速度差异。
- 对于任意一台主机上的 TCP 连接,内核会同时维护一套发送缓冲区和接收缓冲区。依靠这两套独立缓冲区,TCP 能够做到一边发送数据、一边接收数据,该特性称为全双工。
- 正是因为存在两套独立缓冲区,同一个 TCP
sockfd文件描述符,既能调用read/recv读取对端发来的数据,也能调用write/send向外发送数据。- 应用层只需要把数据交给内核缓冲区即可,至于数据包什么时候发、一次发多大、丢包之后如何重传,全部由 TCP 协议负责控制,这就是 "传输控制协议" 名字的含义。
对于之前实现的tcp,有没有考虑过通过网络去进行传输的时候,如果没有一次性将我们的字符串穿过去要怎么办呢???,发生粘包现象该如何处理
TCP 粘包问题:一次 send 不一定一次 read 读完
TCP 是字节流协议 ,没有消息边界。你调用一次
send("hello world"),对端recv()不一定一次就把完整字符串读出来;也可能多次 send 的数据粘在一起被一次读到,这就是粘包。
send/write:只是把数据拷贝到内核发送缓冲区,不代表数据立刻全部发出去;recv/read:只是从内核接收缓冲区拿字节,不对应发送方的一次 send 调用;- 应用层没有消息边界,内核不知道你的一条消息到底多长。
我们要设计报文和解包的逻辑进行解决
代码
要求
网络版计算器 例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器 进行计算, 最后再把结果返回给客户端
约定方案一:直接约定文本格式
• 客户端发送一个形如"1+2"的字符串;
• 这个字符串中有两个操作数, 都是整形;
• 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
• 数字和运算符之间没有空格;
约定方案二:
• 定义结构体来表示我们需要交互的信息;
• 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字 符串转化回结构体;
• 这个过程叫做 "序列化" 和 "反序列化"
用方案一,不用处理大小端对齐,但字符串解析繁琐,数据量大时效率低。
用方案二的化,传输结构体,会有**内存对齐的问题,**不同编译器、编译选项,结构体中间填充的占位字节不一样,两边结构体内存布局不一样。
我们采用两者相互结合的方式
| 文件 | 作用说明 |
|---|---|
| Socket.hpp | 对原生 socket 系统调用做 C++ 面向对象封装。封装socket/bind/listen/accept/connect,封装文件描述符管理、地址结构体,对外提供类接口,不用到处写原生 C 系统调用。 |
| TcpServer.hpp | TCP 服务端高层封装。内部持有 listen 监听 socket,提供Init()、Start()启动服务;循环 accept 获取新连接,对接业务逻辑。 |
| Calculate.hpp | 计算器业务逻辑。接收运算请求,完成加减等计算,返回运算结果,只做数学计算,不碰网络。 |
| Protocol.hpp | 自定义通信协议(核心!序列化 / 反序列化在这里) 。1. 定义消息结构体;2. 实现序列化:结构体转字节流;3. 实现反序列化:字节流还原结构体;4. 实现ReadN/WriteN循环读写,处理长度头,解决 TCP 粘包拆包;网络两端共用这一份协议头,保证客户端、服务端报文格式完全一致。 |
socket
整体设计:
Socket:抽象基类,把 socket 所有行为定义成虚函数。把监听、连接的通用逻辑写在基类普通成员函数。TcpSocket:继承Socket,重写全部虚函数,完成 TCP 真正系统调用。

在这里我们就只实现TCP的部分
socket 能干什么?
- 创建 socket 文件描述符
socket() - bind 绑定端口
- listen 监听(服务端用)
- accept 接收新连接(服务端用)
- connect 连接服务器(客户端用)
- 获取 / 设置 fd,关闭 fd
- recv 接收数据、send 发送数据
先一步一步来
基类

Tpc派生类实现
cpp
void CreateSocketOrDie() override
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket false";
exit(10);
}
else
{
LOG(LogLevel::INFO) << "socket success !!! fd:" << _sockfd;
}
}
void BindSocketOrDie(uint16_t port) override
{
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind false";
cerr << "bind error->" << strerror(errno) << " (errno: " << errno << ")" << endl;
exit(10);
}
else
{
LOG(LogLevel::INFO) << "bind success ";
}
}
void ListenSocketOrDie(int backlog=defaultval) override
{
int n=listen(_sockfd, backlog);
if(n<0)
{
LOG(LogLevel::FATAL) << "listen false";
exit(10);
}
else
{
LOG(LogLevel::INFO) << "listen success";
}
}