网络编程的基本流程:
服务端:
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——建立连接后,就可发收消息了。
图示如下
一、终端节点的创建:
所谓终端节点就是用来通信的端对端的节点,可以通过ip地址和端口构造,其的节点可以连接这个终端节点做通信.
客户端:
如果我们是客户端,我们可以通过对端的ip和端口构造一个endpoint,用这个endpoint和其通信。
int client_end_point() {
std::string raw_ip_address = "127.4.8.1";
unsigned short port_num = 3333;
boost::system::error_code ec;//错误码 出错误时用错误码判断
asio::ip::address ip_address = asio::ip::address::from_string(raw_ip_address, ec);
//↑这行代码的作用是将字符串形式的 IP 地址 raw_ip_address 转换为 asio::ip::address 对象 ip_address,并将可能出现的错误信息存储在 ec 中。这样就可以方便地将字符串形式的 IP 地址转换为网络编程中所需的 IP 地址对象,同时处理了可能出现的错误情况。
if (ec.value() != 0) {
std::cout
<< "Failed to parse the IP address.Error code="
<< ec.value() << ".Message is" << ec.message();
return ec.value();
}
asio::ip::tcp::endpoint ep(ip_address, port_num);
//这行代码的作用是创建一个 TCP 端点对象 ep,其中指定了要连接的目标主机的 IP 地址和端口号。这个端点对象可以用于在 Boost.Asio 中进行 TCP 网络通信,比如作为客户端连接远程服务器或者作为服务器监听特定的端口。
return 0;
}
该函数是终端结点创建的伪码,关键部分均已给出注释
使用时注意包含头文件,如果想更方便的使用Boost库中的asio相关内容,可以加上其命名空间。
#include<boost/asio.hpp>
using namespace boost;
服务端:
如果是服务端,则只需根据本地地址绑定就可以生成endpoint
//服务器端端点
int server_end_point() {
unsigned short port_num = 3333;
asio::ip::address ip_address = asio::ip::address_v6::any();
asio::ip::tcp::endpoint ep(ip_address, port_num);
//这行代码的作用是创建一个 TCP 端点对象 ep,其中指定了要连接的目标主机的 IP 地址和端口号。
//这个端点对象可以用于在 Boost.Asio 中进行 TCP 网络通信,
//比如作为客户端连接远程服务器或者作为服务器监听特定的端口。
return 0;
}
二、创建Socket
创建socket分为4步
1.创建上下文iocontext
2.选择协议
3.生成socket
4.打开socket
//创建Socket
//socket要用来通信的时候必须要有一个参数,叫做上下文,上下文是boost_asio的一个核心服务,所有的服务都是通过上下文服务来通信的
int create_tcp_socket() {
//此函数创建一个Socket并打开
asio::io_context ioc;
//asio::io_context ioc;:创建了一个 I/O 上下文对象 ioc。I/O 上下文是 Boost.Asio 中用于处理异步 I/O 事件的核心类,它提供了事件循环、回调管理等功能,用于驱动异步操作。
asio::ip::tcp protocol = asio::ip::tcp::v4();
//asio::ip::tcp protocol = asio::ip::tcp::v4();:创建了一个 IPv4 的 TCP 协议对象 protocol。这行代码使用了 asio::ip::tcp::v4() 方法来获取一个表示 IPv4 TCP 协议的对象,以便在后续的套接字创建中使用。
asio::ip::tcp::socket sock(ioc);
//asio::ip::tcp::socket sock(ioc);:创建了一个 TCP 套接字对象 sock,并将之前创建的 I/O 上下文对象 ioc 传递给了套接字构造函数。这样可以将套接字与指定的 I/O 上下文关联起来,以便在该上下文中处理套接字的异步 I/O 操作。
//综合起来,这段代码的作用是创建了一个 I/O 上下文对象,初始化了一个 IPv4 的 TCP 协议对象,并创建了一个与指定 I/O 上下文关联的 TCP 套接字对象。这些是在使用 Boost.Asio 进行网络编程时常见的初始化操作,为后续的异步操作和网络通信做好了准备。
boost::system::error_code ec;//错误码
sock.open(protocol, ec);
if (ec.value() != 0) {
std::cout
<< "Failed to parse the IP address.Error code="
<< ec.value() << ".Message is" << ec.message();
return ec.value();
}
}
上述socket只是通信的socket,如果是服务端,我们还需要生成一个acceptor的socket,用来接收新的连接。
int create_acceptor_socket() {
asio::io_context ios;
//asio::ip::tcp::acceptor acceptor(ios);
//asio::ip::tcp protocal = asio::ip::tcp::v4();
//boost::system::error_code ec;//错误码
//acceptor.open(protocal, ec);
//if (ec.value() != 0) {
// std::cout
// << "Failed to parse the IP address.Error code="
// << ec.value() << ".Message is" << ec.message();
// return ec.value();
//}
//旧的写法
//新写法
asio::ip::tcp::acceptor a(ios, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 3333));
//生成一个acceptor,为其指定是tcp v4的协议(只接受ipv4的连接)并且接受所有发往3333端口的连接
//这样写声明了ip和端口,实现了默认绑定
return 0;
}
三、绑定acceptor
将一个 acceptor 类型的 socket 绑定到指定的端点(endpoint)意味着在服务器端创建了一个监听指定端点的套接字,从而可以接收到针对该端点的传入连接请求。这在网络编程中有以下效果:
-
接受传入连接:通过将 acceptor 绑定到指定的端点,服务器可以开始监听该端点,并接受传入的连接请求。一旦有客户端尝试连接到该端点,服务器便可以接受并处理这些连接请求。
-
确定服务器监听的端点:通过绑定 acceptor 到特定的端点,服务器可以指定自己所监听的网络地址和端口号。这样,客户端就知道在哪里可以找到服务器,并且可以向特定的端点发起连接请求。
-
多端口监听:服务器可以创建多个不同端点的 acceptor,并分别绑定到不同的端口,以便同时监听多个网络端点,从而实现多端口服务或者多协议支持。
总之,将 acceptor 类型的 socket 绑定到特定的端点是启动服务器并开始接受传入连接请求的重要步骤,它为服务器端提供了监听和接受传入连接的能力。
//让服务器的accept绑定端口和ip
int bind_acceptor_socket() {
unsigned short port_num = 3333;
asio::ip::tcp::endpoint ep(asio::ip::address_v4::any(), port_num);
asio::io_context ios;
asio::ip::tcp::acceptor acceptor(ios, ep.protocol());
boost::system::error_code ec;//错误码
acceptor.bind(ep, ec);
//绑定本地任何一个地址和端口号,即可以接受来自任何地址的连接
if (ec.value() != 0) {
std::cout
<< "Failed to parse the IP address.Error code="
<< ec.value() << ".Message is" << ec.message();
return ec.value();
}
}
四、连接指定的端点
作为客户端可以连接服务器指定的端点进行连接
//建立与目标主机的连接
int connect_to_end() {
std::string raw_ip_address = "192.168.1.124";
unsigned short port_num = 3333;
try {
asio::ip::tcp::endpoint ep(asio::ip::address::from_string(raw_ip_address), port_num);
//这行代码的作用是创建了一个 TCP 端点对象 ep,其中指定了要连接的目标主机的 IP
asio::io_context ios;
asio::ip::tcp::socket sock(ios, ep.protocol());
//这行代码的作用是创建了一个 TCP 套接字对象 sock,并将其关联到指定的 I/O 上下文和 TCP 协议,通常ep.protocol()转出来的是v4协议,为后续的网络通信做好了准备。
sock.connect(ep);
//在网络编程中,调用 connect 方法是用于建立客户端与服务器之间的连接。通过这个操作,客户端可以与服务器建立通信通道,以便进行数据传输和交互。
}
catch (system::system_error& e) {
std::cout << "Error occured!Error code=" << e.code()
<< ".Message:" << e.what();
return e.code().value();
}
}
五、服务器接收连接
//服务器接受连接
//当有客户端连接时,服务器需要接受连接
int accept_new_connection() {
const int BACKLOG_SIZE = 30;
//缓冲区队列,根据tcp的算法能缓存30/2=60个来不及处理的客户端的连接
unsigned short port_num = 3333;
asio::ip::tcp::endpoint ep(asio::ip::address_v4::any(), port_num);
asio::io_context ios;
try{
//可作为标准服务器的一个流程
asio::ip::tcp::acceptor acceptor(ios, ep.protocol());
//1.服务器先生成一个接收器,第一个参数是上下文服务,第二个参数是服务器要处理的协议
acceptor.bind(ep);
//2.服务器绑定这个端点,服务器把接收器绑定在ip地址和端口上
acceptor.listen(BACKLOG_SIZE);
//3.服务器进行监听的操作,不监听就无法接受新的连接,监听的时候要传入监听大小
asio::ip::tcp::socket sock(ios);
//4.服务器再创建一个Socket,这个Socket跟acceptor是不一样的,这个Socket是用来跟客户端通信的
acceptor.accept(sock);
//5.把接收到的新链接通过这个Socket处理
}
catch (system::system_error &e){
std::cout << "Error occured!Error code=" << e.code()
<< ".Message:" << e.what();
return e.code().value();
}
}
至此,客户端连接的流程结束。
六、总结
1.对于服务器端:
服务器先生成一个端点,并且生成一个acceptor接收器,接收器绑定好端点,也就是服务器本地的地址加上服务器要指定的端口,然后客户端就可以通过这个地址和端口去连接,服务器为了接受他们的连接还要做listen操作,也就是监听,监听好了之后通过accept来返回,accept会返回新的连接,新的连接交给Socket来处理客户端的消息
2.对于客户端:
客户端首先先去连接服务器的地址,通过ip address的这个全局函数form_string()把这个地址转成我们asio要用到的地址,并且传递一个服务器提供的端口号,这样就会生成一个端点,保存了服务器对端的信息,客户端的socket去连接这个信息。创建socket要注意要指定这个socket绑定在哪个服务上,我们要定义一个服务,并且指定一个协议,这个协议就是对端支持的协议。