上一节介绍了visual studio搭建boost环境,现在我们来使用它来进行网络编程。
1、网络编程基本流程
服务端
1)socket——创建socket对象。
2)bind——绑定本机ip+port。
3)listen——监听来电,若在监听到来电,则建立起连接。
4)accept——再创建一个socket对象给其收发消息。
原因是现实中服务端都是面对多个客户端,那么为了区分各个客户端,
则每个客户端都需再分配一个socket对象进行收发消息。
5)read、write——就是收发消息了。
客户端
1)socket——创建socket对象。
2)connect——根据服务端ip+port,发起连接请求。
3)write、read——建立连接后,就可发收消息了。
在使用boost中的asio进行网络编程时,头文件为
#include <boost/asio.hpp>
2、终端节点的创建
关于终端结点的创建,可以有几种方式:
第一种:客户端可以使用这种方式,较为麻烦
// std::string raw_ip_address:字符串变量,表示IP地址
std::string raw_ip_address = "127.4.8.1";
// unsigned short port_num:无符号短整型变量,表示端口号
unsigned short port_num = 3333;
// boost::system::error_code ec:错误码对象,用于存储可能出现的错误信息
boost::system::error_code ec;
// 将字符串形式的IP地址 raw_ip_address 转换为 boost::asio::ip::address 类型的对象 ip_address
// 并将可能出现的错误信息存储在 ec 中
boost::asio::ip::address ip_address = boost::asio::ip::address::from_string(raw_ip_address, ec);
// 如果 ec.value() 不等于0
if (ec.value() != 0) {
// 输出错误码和错误信息,并返回错误码
std::cout << "ip地址解析错误,错误码:" << ec.value() << ",错误信息:" << ec.message() << std::endl;
return ec.value();
}
// 创建一个 boost::asio::ip::tcp::endpoint 对象 ep,它表示一个网络端点
// 由IP地址 ip_address 和端口号 port_num 组成
boost::asio::ip::tcp::endpoint ep(ip_address, port_num);
第二种:针对服务端的方法,不需要指定ip,获取自己的ip即可
unsigned short port_num = 3333;
// 可以绑定到所有可用的IPv6接口。这种设置通常用于服务器端,以便接收来自任何网络接口的连接
boost::asio::ip::address ip_address = boost::asio::ip::address_v6::any();
boost::asio::ip::tcp::endpoint ep(ip_address, port_num);
第三种:简化方法,这种使用起来较为方便,代码简洁
boost::asio::ip::tcp::endpoint remote_ep(boost::asio::ip::address::from_string("127.0.0.1"), 10086);
3、创建socket
创建socket分为4步,创建上下文iocontext,选择协议,绑定上下文信息,打开socket。
// boost::asio::io_context ioc:I/O 上下文对象。
// 它管理所有异步 I/O 操作的执行,所有的网络操作都依赖于它来处理事件循环
boost::asio::io_context ioc;
// boost::asio::ip::tcp 对象,指定使用 TCP 协议
// 如果希望使用 IPv6,可以使用 boost::asio::ip::tcp::v6()
boost::asio::ip::tcp protocol = boost::asio::ip::tcp::v4();
// boost::asio::ip::tcp::socket 对象,表示一个 TCP 套接字。
// 它被绑定到先前创建的 I/O 上下文 ioc 上
boost::asio::ip::tcp::socket sock(ioc);
boost::system::error_code ec;
sock.open(protocol, ec);
if (ec.value() != 0) {
std::cout
<< "错误,错误码:" << ec.value()
<< ",错误信息:" << ec.message()
<< std::endl;
return ec.value();
}
上述socket只是通信的socket,如果是服务端,我们还需要生成一个acceptor的socket,用来接收新的连接。
boost::asio::io_context ioc;
boost::asio::ip::tcp::acceptor a(ioc,
boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 3333));
4、绑定acceptor
对于acceptor类型的socket,服务器要将其绑定到指定的端点,所有连接这个端点的连接都可以被接收到。
int bind_acceptor_socket()
{
unsigned short port_num = 3333;
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address_v4::any(), port_num);
boost::asio::io_context ioc;
// 创建一个 TCP 接收器对象,指定使用 ioc 作为 I/O 上下文,并使用 ep 的协议(IPv4)
boost::asio::ip::tcp::acceptor accptor(ioc, ep.protocol());
boost::system::error_code ec;
// 将接收器绑定到指定的端点 ep,如果出现错误,将错误信息存储在 ec 中
accptor.bind(ep, ec);
if (ec.value() != 0) {
std::cout
<< "错误,错误码:" << ec.value()
<< ",错误信息:" << ec.message()
<< std::endl;
return ec.value();
}
return 0;
}
5、连接指定的端点
连接至服务器指定的端点
// 客户端连接服务器
int connect_to_end()
{
std::string raw_ip_address = "192.168.1.124";
unsigned short port_num = 3333;
try {
// 创建指定ip和端口的端点
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(raw_ip_address), port_num);
boost::asio::io_context ioc;
// 创建一个 TCP 套接字对象,指定使用 ioc 作为 I/O 上下文
boost::asio::ip::tcp::socket sock(ioc);
// 连接到指定的端点 ep。这一步是阻塞操作,直到连接成功或失败
sock.connect(ep);
}
catch (boost::system::system_error& e) {
std::cout
<< "错误,错误码:" << e.code()
<< ",错误信息:" << e.what()
<< std::endl;
return e.code().value();
}
return 0;
}
6、服务器接收连接
int accept_new_connection()
{
// 定义服务器监听队列的大小,即同时可处理的未决连接数。这里设置为30
const int BACKLOG_SIZE = 30;
unsigned short port_num = 3333;
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address_v4::any(), port_num);
boost::asio::io_context ioc;
try {
// 创建一个 TCP 接收器对象,指定使用 ioc 作为 I/O 上下文,并使用 ep 的协议(IPv4)
boost::asio::ip::tcp::acceptor acceptor(ioc, ep.protocol());
// 将接收器绑定到指定的端点 ep,即所有 IPv4 地址和端口 3333
acceptor.bind(ep);
// 开始监听传入的连接请求,设置连接等待队列的最大长度为 BACKLOG_SIZE
acceptor.listen(BACKLOG_SIZE);
// 创建一个用于通信的 TCP 套接字对象 sock
boost::asio::ip::tcp::socket sock(ioc);
// 阻塞等待并接受一个传入的连接,将新连接的套接字对象赋给 sock
acceptor.accept(sock);
}
catch (boost::system::system_error& e) {
std::cout
<< "错误,错误码:" << e.code()
<< ",错误信息:" << e.what()
<< std::endl;
return e.code().value();
}
return 0;
}
7、buffer
buffer就是接收和发送数据时缓存数据的结构,boost::asio提供的数据结构较为复杂,但是提供了相对简单的api接口。
将hello转化为该类型
void use_const_buffer()
{
std::string buf = "hello";
// boost::asio::const_buffer是一个 Boost.Asio 提供的常量缓冲区类,用于表示一块只读的内存区域
// buf.c_str():返回指向 buf 首字符数据的指针
// buf.length():返回字符串的长度,即数据的字节数
boost::asio::const_buffer asio_buf(buf.c_str(), buf.length());
// std::vector 容器可以包含多个缓冲区,用于一次性传输多个数据块
std::vector<boost::asio::const_buffer> buffers_sequence;
buffers_sequence.push_back(asio_buf);
// boost::asio.send(buffers_sequence);
}
上面的方法比较麻烦,可以直接用buffer函数转化为send需要的参数类型
void use_buffer_str()
{
// boost::asio::const_buffers_1 是一个单一的常量缓冲区类型。它用于包装一个不可修改的数据缓冲区
boost::asio::const_buffers_1 output_buf = boost::asio::buffer("hello");
}
对于数组,可以这样写
void use_buffer_array()
{
const std::size_t BUF_SIZE_BYTES = 20;
std::unique_ptr<char[]> buf(new char[BUF_SIZE_BYTES]);
auto input_buf = boost::asio::buffer(static_cast<void*>(buf.get()), BUF_SIZE_BYTES);
}
8、同步写
8.1 同步写write_some
write_some可以每次向指定的空间写入固定的字节数,如果写缓冲区满了,就只写一部分,返回写入的字节数。它的特点是非阻塞。
void write_to_socket(boost::asio::ip::tcp::socket& sock)
{
std::string buf = "hello";
std::size_t total_bytes_weitten = 0;
// 循环发送
// write_some 返回每次写入的字节数
while (total_bytes_weitten != buf.length()) {
total_bytes_weitten += sock.write_some(boost::asio::buffer(buf.c_str() + total_bytes_weitten,
buf.length() - total_bytes_weitten));
}
}
8.2 同步写send
write_some使用起来比较麻烦,需要多次调用,asio提供了send函数。send函数会一次性将buffer中的内容发送给对端,如果有部分字节因为发送缓冲区满无法发送,则阻塞等待,直到发送缓冲区可用,则继续发送完成。
int send_data_by_send() {
std::string raw_ip_address = "192.168.3.11";
unsigned short port_num = 3333;
try {
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(raw_ip_address), port_num);
boost::asio::io_context ioc;
boost::asio::ip::tcp::socket sock(ioc, ep.protocol()); // ep.protocol()传不传都可以
sock.connect(ep);
std::string buf = "hello";
// 阻塞在这,直到发完为止,比write_some用起来方便
int send_length = sock.send(boost::asio::buffer(buf.c_str(), buf.length()));
if (send_length <= 0) {
// 发送失败
return 0;
}
}
catch (boost::system::system_error& e) {
std::cout
<< "错误,错误码:" << e.code()
<< ",错误信息:" << e.what()
<< std::endl;
return e.code().value();
}
return 0;
}
8.3 同步写write
类似send方法,asio还提供了一个write函数,可以一次性将所有数据发送给对端,如果发送缓冲区满了则阻塞,直到发送缓冲区可用,将数据发送完成。与send不同的是,这是一个全局函数,调用方式不一样。
int send_data_by_write() {
std::string raw_ip_address = "192.168.3.11";
unsigned short port_num = 3333;
try {
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(raw_ip_address), port_num);
boost::asio::io_context ioc;
boost::asio::ip::tcp::socket sock(ioc, ep.protocol()); // ep.protocol()传不传都可以
sock.connect(ep);
std::string buf = "hello";
// 也是阻塞在这,直到发完为止,也比write_some用起来方便
int send_length = boost::asio::write(sock, boost::asio::buffer(buf.c_str(), buf.length()));
if (send_length <= 0) {
// 发送失败
return 0;
}
}
catch (boost::system::system_error& e) {
std::cout
<< "错误,错误码:" << e.code()
<< ",错误信息:" << e.what()
<< std::endl;
return e.code().value();
}
return 0;
}
9、同步读
和同步写类似,也提供了三种方法
9.1 同步读read_some
read_some可以指定读取的字节数
std::string read_from_socket(boost::asio::ip::tcp::socket& sock) {
const unsigned char MESSAGE_SIZE = 7;
char buf[MESSAGE_SIZE];
std::size_t total_bytes_read = 0;
while (total_bytes_read != MESSAGE_SIZE) {
total_bytes_read += sock.read_some(boost::asio::buffer(buf + total_bytes_read,
MESSAGE_SIZE - total_bytes_read));
}
return std::string(buf, total_bytes_read);
}
int read_data_by_read_some() {
std::string raw_ip_address = "127.0.0.1";
unsigned short port_num = 3333;
try {
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(raw_ip_address), port_num);
boost::asio::io_context ioc;
boost::asio::ip::tcp::socket sock(ioc, ep.protocol());
sock.connect(ep);
read_from_socket(sock);
}
catch (boost::system::system_error& e) {
std::cout
<< "错误,错误码:" << e.code()
<< ",错误信息:" << e.what()
<< std::endl;
return e.code().value();
}
return 0;
}
9.2 同步读receive
可以一次性同步读取对方发送的数据,返回接收到的字节数。
int read_data_by_receive() {
std::string raw_ip_address = "127.0.0.1";
unsigned short port_num = 3333;
try {
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(raw_ip_address), port_num);
boost::asio::io_context ioc;
boost::asio::ip::tcp::socket sock(ioc, ep.protocol());
sock.connect(ep);
const unsigned char BUFF_SIZE = 7;
char buffer_receive[BUFF_SIZE];
int receive_length = sock.receive(boost::asio::buffer(buffer_receive, BUFF_SIZE));
if (receive_length <= 0) {
std::cout << "接收失败" << std::endl;
}
}
catch (boost::system::system_error& e) {
std::cout
<< "错误,错误码:" << e.code()
<< ",错误信息:" << e.what()
<< std::endl;
return e.code().value();
}
return 0;
}
9.4 同步读read
这种方式是阻塞的,直到读到指定长度才会取消阻塞,receive虽然也是阻塞的,但是使用receive时至少读到一个字节的数据就会取消阻塞。
int read_data_by_read() {
std::string raw_ip_address = "127.0.0.1";
unsigned short port_num = 3333;
try {
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(raw_ip_address), port_num);
boost::asio::io_context ioc;
boost::asio::ip::tcp::socket sock(ioc, ep.protocol());
sock.connect(ep);
const unsigned char BUFF_SIZE = 7;
char buffer_receive[BUFF_SIZE];
int receive_length = boost::asio::read(sock, boost::asio::buffer(buffer_receive, BUFF_SIZE));
if (receive_length <= 0) {
std::cout << "接收失败" << std::endl;
}
}
catch (boost::system::system_error& e) {
std::cout
<< "错误,错误码:" << e.code()
<< ",错误信息:" << e.what()
<< std::endl;
return e.code().value();
}
return 0;
}
9.5 读取直到指定字符
这个方法不经常使用,了解即可。
std::string read_data_by_until(asio::ip::tcp::socket& sock) {
asio::streambuf buf;
// 一直读到换行符,即这一行结束
asio::read_until(sock, buf, '\n');
std::string message;
std::istream input_stream(&buf);
std::getline(input_stream, message);
return message;
}