C++ 网络编程(9)字节序处理和消息队列的控制
文章目录
- 前言
- 字节序的问题
- 一、目前服务器单线程操作流程
- 二、大小端模式
- 三、如何分辨本机字节序是大端序还是小端序
- 代码
- 细节详解
- 四、服务器使用网络字节序
- 五、消息队列控制
- 总结
前言
提示:这里可以添加本文要记录的大概内容:
字节序的问题
在计算机网络中,由于不同的计算机使用的 CPU 架构和字节顺序可能不同,
因此在传输数据时需要对数据的字节序进行统一,以保证数据能够正常传输和解析。
这就是网络字节序的作用。
具体来说,计算机内部存储数据的方式有两种:大端序(Big-Endian)和小端序(Little-Endian)。在
大端序中,高位字节存储在低地址处,而低位字节存储在高地址处;在
小端序中,高位字节存储在高地址处,而低位字节存储在低地址处。
在网络通信过程中,通常使用的是大端序。这是因为早期的网络硬件大多采用了 Motorola 处理器,而 Motorola 处理器使用的是大端序。此外,大多数网络协议规定了网络字节序必须为大端序。
因此,在进行网络编程时,需要将主机字节序转换为网络字节序,也就是将数据从本地字节序转换为大端序。可以使用诸如(本地转网络) htonl、htons、(网络转本地)ntohl 和 ntohs 等函数来实现字节序转换操作。
综上所述,网络字节序的主要作用是统一不同计算机间的数据表示方式,以保证数据在网络中的正确传输和解析
提示:以下是本篇文章正文内容,下面案例可供参考
一、目前服务器单线程操作流程
当应用层调用一个读操作或者写操作,这个时候会将读写操作和读写操作的回调函数注册进入io_context中然后注册给对应的模型
在linux环境下会使用epoll模型,windows环境下会使用iocp模型,
它们是高效的操作系统底层事件通知机制
● 用来监控大量文件描述符(socket)上的事件(比如有没有数据可读、可写、连接是否断开等)。
● 解决了“多路复用”的问题——也就是让一个线程高效地管理成百上千的网络连接
epoll 和 IOCP 本身不会执行你的回调,而是当某些事件确定触发以后,其回调函数就会一个个的放入就绪事件队列中按顺序触发
二、大小端模式
当我们想存 0x12345678
大端模式
小端模式
三、如何分辨本机字节序是大端序还是小端序
如何区分本机字节序,可以通过判断低地址存储的数据是否为低字节数据,如果是则为小端,否则为大端,下面写一段代码讲述这个逻辑
代码
#include <iostream>
using namespace std;// 判断当前系统的字节序是大端序还是小端序
bool is_big_endian() {int num = 1;if (*(char*)&num == 1) {// 当前系统为小端序return false;}else {// 当前系统为大端序return true;}
}int main() {int num = 0x12345678;char* p = (char*)#cout << "原始数据:" << hex << num << endl;if (is_big_endian()) {cout << "当前系统为大端序" << endl;cout << "字节序为:";for (int i = 0; i < sizeof(num); i++) {cout << hex << (int)*(p + i) << " ";}cout << endl;}else {cout << "当前系统为小端序" << endl;cout << "字节序为:";// 小端序时,从低地址到高地址输出for (int i = 0; i < sizeof(num); i++) {cout << hex << (int)*(p + i) << " ";}cout << endl;}return 0;
}
细节详解
if ((char)&num == 1)
这里我们在前面已经定义了num==1
在内存中,int 类型通常占 4 个字节
数值 1 在十六进制表示中是 0x00000001
然后我们判断最低位 是否为1就行
如果最低位为1 说明就是小端序 因为低位字节序存储在低地址
hex 是一个 格式控制符,它的作用是让 cout 以 十六进制 的格式输出后面的数值。
例如,如果当前系统为大端序,则输出结果为:
原始数据:12345678
当前系统为大端序
字节序为:12 34 56 78
如果当前系统为小端序,则输出结果为:
原始数据:12345678
当前系统为小端序
字节序为:78 56 34 12
四、服务器使用网络字节序
为保证字节序一致性,网络传输使用网络字节序,也就是大端模式。
在 boost::asio 库中,可以使用 boost::asio::detail::socket_ops::host_to_network_long() 和 boost::asio::detail::socket_ops::host_to_network_short() 函数
将主机字节序转换为网络字节序。具体方法如下
#include <boost/asio.hpp>
#include <iostream>
int main()
{uint32_t host_long_value = 0x12345678;uint16_t host_short_value = 0x5678;uint32_t network_long_value = boost::asio::detail::socket_ops::host_to_network_long(host_long_value);uint16_t network_short_value = boost::asio::detail::socket_ops::host_to_network_short(host_short_value);std::cout << "Host long value: 0x" << std::hex << host_long_value << std::endl;std::cout << "Network long value: 0x" << std::hex << network_long_value << std::endl;std::cout << "Host short value: 0x" << std::hex << host_short_value << std::endl;std::cout << "Network short value: 0x" << std::hex << network_short_value << std::endl;return 0;
}
上述代码中,使用了 boost::asio::detail::socket_ops::host_to_network_long() 和 boost::asio::detail::socket_ops::host_to_network_short() 函数
将主机字节序转换为网络字节序。
host_to_network_long() 函数将一个 32 位无符号整数从主机字节序转换为网络字节序,返回转换后的结果。host_to_network_short() 函数将一个 16 位无符号整数从主机字节序转换为网络字节序,返回转换后的结果。
在上述代码中,分别将 32 位和 16 位的主机字节序数值转换为网络字节序,并输出转换结果。需要注意的是,在使用这些函数时,应该确保输入参数和返回结果都是无符号整数类型,否则可能会出现错误
同样的道理,我们只需要在服务器发送数据时,将数据长度转化为网络字节序,在接收数据时,将长度转为本机字节序
//获取头部数据进行判断
short data_len = 0;
memcpy(&data_len, _recv_head_node->_data, HEAD_LENGTH);
//将网络字节序转为本地字节序
data_len = boost::asio::detail::socket_ops::network_to_host_short(data_len);
cout << "data_len is " << data_len << endl;
在服务器的发送数据时会构造消息节点,构造消息节点时,将发送长度由本地字节序转化为网络字节序
MsgNode(char* msg,short max_len):_cur_len(0),_total_len(HEAD_LENGTH+max_len)
{_data = new char[_total_len+1]();//转为网络字节序int max_len_host = boost::asio::detail::socket_ops::host_to_network_short(max_len);memcpy(_data, &max_len_host, HEAD_LENGTH);memcpy(_data+ HEAD_LENGTH, msg, max_len);_data[_total_len] = '\0';
}
五、消息队列控制
发送时我们会将发送的消息放入队列里以保证发送的时序性,每个session都有一个发送队列,因为有的时候发送的频率过高会导致队列增大,所以要对队列的大小做限制,当队列大于指定数量的长度时,就丢弃要发送的数据包,以保证消息的快速收发
void CSession::Send(char* msg, int max_length)
{std::lock_guard<std::mutex>lock (_send_lock);if (_send_que.size() > 1000) {cout << "session: " << _uuid << " send que fulled, size is 1000"<< endl;return;}_send_que.push(make_shared<MsgNode>(msg, max_length));if (_send_que.size() > 0)// 保证同一时刻只有一个 async_write 在进行,防止多次并发写{return;}auto& msgnode = _send_que.front();boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_total_len),std::bind(&CSession::HandleWrite, this, std::placeholders::_1, std::placeholders::_2, shared_from_this()));
}
加锁(std::lock_guardstd::mutex lock(_send_lock))
作用:保证同一时刻只有一个线程能进入这段 Send 代码,防止多线程下队列(_send_que)被并发操作而发生数据竞争或破坏。
if (_send_que.size() > 0) return;
作用:保证同一时刻只有一个 async_write 正在进行。
如果队列原来就有消息,说明之前已经有 async_write 在发送了,现在只需要把新消息放进队列即可,不需要再发起新的 async_write。
只有队列原来为空时,才会发起一次 async_write。
总结(更口语化):
锁:只让一个线程能“进屋”操作队列,避免线程打架。
if 判断:只让“屋里”同一时刻只挂着一个 async_write,保证异步写不会重复发起。
总结
本文介绍了如何使用网络字节序以及为什么使用网络字节序作为网络传输,且为发送队列设置了阈值保证发送数据的高效性
协议头部里的字段(比如长度、类型、端口号等)是数值型,需要进行主机字节序到网络字节序的转换,确保不同平台间通信时数值被一致解释。
协议内容/数据部分通常被当作原始字节流(无论是字符串还是二进制),直接按字节传输,不做字节序转换。