01. 先导

1.网络编程的基本流程

socket(套接字)
protocol(协议)

对于服务端:

  1. socket--创建socket对象。
  2. bind--绑定本机ip和port。即调用bind函数分配IP地址和端口号。
  3. listen--监听来电,若监听到来电,则建立连接。
  4. accept--调用此函数受理连接请求。即再创建一个socket对象给其收发消息。因为实际情况中,服务端连接多个客户端,每个客户端都得要分配一个socket进行通信。

对于客户端:

  1. 创建套接字。但此时套接字不会马上分为服务器端和客户端。如果紧接着调用bind、listen函数,将成为服务器端套接字。如果调用connect函数,则成为客户端套接字。
  2. 调用connect函数,向服务器发送连接请求。

那些由两个或多个部分组成的应用,每一部分都在一个独立的计算设备上运行,并且通过计算机网络和其他部分进行交流。这种应用叫做分布式应用。分布式应用相比于传统的运行在单个计算机上的应用具备更大的优势。

2. TCP和UDP简介

这两个协议都是传输层协议。

TCP协议具备以下特点:

  1. 可靠。意思是这个协议保证信息以正确的顺序传输,或者会在信息没有传输时发出通知。此协议包含错误处理机制,可以让开发者无需再在应用中加入错误处理机制。
  2. 假定逻辑连接的建立。在一个应用可以通过TCP协议与其他应用进行通信前,应用必须通过依照标准交换设备信息的方式建立逻辑连接。
  3. 假定点对点通信模型。即,只有两个应用通过单个连接互相通信。不支持多播信息传输。
  4. TCP是面向字节流协议。意味着一个应用发往另一个应用的数据会根据协议转换为字节流。由于缓冲区的存在,TCP程序的读和写不需要一一匹配。例如: 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节; 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次 read一个字节, 重复100次。

UDP协议具备以下特点:

  1. 不可靠。如果发端通过UDP协议发送信息,并不会保证信息一定传输。此协议也不会检测或修复错误。
  2. 不需要连接,即在应用之间进行通信之前不需要建立连接。
  3. 支持点对点和点对多通信模型。支持多播。
  4. 面向数据报: 应用层交给UDP多长的报文, UDP原样发送既不会拆分,也不会合并。如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个 字节,而不能循环调用10次recvfrom, 每次接收10个字节。所以UDP不能够灵活的控制读写数据的次数和数量。

由于TCP协议的可靠性,在需要通过网络进行通信时,此协议常常是最佳选择

3. 创建终端节点(endpoint)

一个典型的客户端应用程序,在与服务器应用程序进行通信以使用其服务之前,必须获取运行服务器应用程序的主机的IP地址以及与之关联的协议端口号。由IP地址和协议端口号组成的一对值,在计算机网络中唯一标识在特定主机上运行的特定应用程序,这对值称为一个端点。

客户端应用程序通常会通过以下方式获取标识服务器应用程序的IP地址和端口号:直接通过应用程序的用户界面从用户那里获取,或作为命令行参数输入,或者从应用程序的配置文件中读取。

此外,服务器的IP地址还可以以间接的形式提供给客户端应用程序,例如包含DNS名称的字符串(例如localhost或www.google.com)。另一种表示IP地址的方式是使用整数值。IPv4地址表示为32位整数,IPv6地址表示为64位整数。然而,由于这种表示方式的可读性和记忆性较差,因此极少使用。

如果客户端应用程序被提供了一个DNS名称,在与服务器应用程序进行通信之前,它必须解析该DNS名称以获取运行服务器应用程序的主机的实际IP地址。有时,DNS名称可能会映射到多个IP地址,在这种情况下,客户端可能需要逐个尝试这些地址,直到找到一个可用的地址。

服务器应用程序也需要处理端点。它使用端点向操作系统指定希望监听来自客户端的传入消息的IP地址和协议端口。如果运行服务器应用程序的主机只有一个网络接口并且分配了一个IP地址,那么服务器应用程序在监听地址上只有一个选择。然而,有时主机可能具有多个网络接口,并相应地拥有多个IP地址。在这种情况下,服务器应用程序会面临选择一个合适的IP地址来监听传入消息的难题。问题在于,应用程序并不了解诸如底层IP协议设置、数据包路由规则、映射到相应IP地址的DNS名称等细节。因此,对于服务器应用程序来说,预测客户端发送的消息将通过哪个IP地址传递到主机是一个相当复杂的任务(有时甚至无法解决)。

如果服务器应用程序只选择一个IP地址来监听传入的消息,它可能会错过路由到主机其他IP地址的消息。因此,服务器应用程序通常希望监听主机上所有可用的IP地址。这确保了服务器应用程序能够接收到到达任何IP地址和特定协议端口的所有消息。

  1. 客户端应用程序使用端点来指定其希望与之通信的特定服务器应用程序。
  2. 服务器应用程序使用端点来指定一个本地IP地址和端口号,以接收来自客户端的传入消息。如果主机上有多个IP地址,服务器应用程序将希望创建一个特殊的端点,代表所有IP地址。

3.1 开始

在创建端点之前,客户端应用程序必须获取指定它将与之通信的服务器的原始IP地址和协议端口号。另一方面,服务器应用程序通常监听所有IP地址上的传入消息,因此它只需要获取一个用于监听的端口号。

3.1.1客户端端点的创建步骤:
  1. 获得服务器应用端的IP地址和端口号。IP地址应该指明是以何种方式表示的。
  2. 将原始的IP地址表示为asio::ip::address类的对象。
  3. 用第二步的对象和端口号初始化asio::ip::tcp::endpoint类的对象。
  4. 终端节点就可以使用Boost.Asio通信相关的方法指定服务器应用端的应用。

代码示例如下:

#include "endpoint.h"
#include <boost/asio.hpp>
#include <iostream>
using namespace boost;
int client_end_point() {
std::string raw_ip_address = "127.0.0.1";//
unsigned short port_num = 7777;
boost::system::error_code ec;//错误关键字,转换时出现错误用来判断。
asio::ip::address ip_address = asio::ip::address::from_string(raw_ip_address, ec);
if (ec.value() != 0) {
//ec的值不为零,表示转换失败,提供的IP地址无效。终止执行。
std::cout << "Failed, error code = " << ec.value() << ".Message" << ec.message();
return ec.value();
}
asio::ip::tcp::endpoint ep(ip_address, port_num);
return 0;
}
3.1.2 服务器端端点的创建步骤:
  1. 获取服务器将监听传入请求的协议端口号。
  2. 创建一个代表所有ip的asio::ip::address对象的具体实例代表服务器的主机上可用的地址。
  3. 从步骤2中创建的地址对象实例化asio::ip::tcp::endpoint类的对象和端口号。
  4. 端点已经准备好用于向操作系统指定服务器想要侦听所有IP地址和特定协议端口号上的传入消息。

代码示例如下:

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);
return 0;
}

注意,上述代码使用的是TCP协议

4. socket(套接字)

4.1 创建主动socket

用于向远程应用程序发送和接收数据或启动连接建立过程的套接字称为主动套接字,而被动套接字用于被动等待来自远程应用程序的传入连接请求。被动套接字不参与用户数据传输。

创建主动socket的步骤:

  1. 创建asio::io_context类的实例或者使用先前创建的实例。
  2. 创建一个类的对象,该对象表示传输层协议(TCP或UDP)和套接字打算在其上进行通信的底层IP协议(IPv4或IPv6)的版本。
  3. 创建一个对象,表示与所需协议类型对应的套接字。将asio::io_service类的对象传递给套接字的构造函数。
  4. 调用socket的method()方法,将步骤2中代表协议的对象作为参数传递给它。
int create_tcp_socket()
{
asio::io_context ios;
asio::ip::tcp protocol = asio::ip::tcp::v4();
asio::ip::tcp::socket sock(ios);
boost::system::error_code ec;
sock.open(protocol, ec);
if (ec.value() != 0) {
std::cout << "Failed to open the socket! Error code = " << ec.value() << ".Message" << ec.message();
return ec.value();
}
return 0;
}

在新版本的asio中,生成socket更加简便,不再需要上述代码中的open操作。

4.2 创建被动socket

被动套接字仅在服务器应用程序或混合应用程序中使用同时扮演客户端和服务器的角色。
被动套接字仅为TCP协议定义。由于UDP协议并不意味着建立连接,因此在UDP上进行通信时不需要被动套接字。

Boost.Asio中,一个被动socket通过asio::ip::tcp::acceptor类表示。

创建被动socket的步骤如下:

  1. 创建asio::io_context类的实例或者使用先前创建的实例。
  2. 创建一个asio::ip::tcp类的对象,该对象表示TCP协议和底层IP协议(IPv4 或 IPv6)的所需版本。
  3. 创建一个asio::ip::tcp::acceptor类的对象,表示一个acceptor套接字,并将asio::io_context类的对象传递给其构造函数。
  4. 调用接受器套接字的open()方法,将表示步骤2中创建的协议的对象作为参数传递。

示例代码如下:

int create_tcp_acceptor_socket()
{
asio::io_context ios;//创建socket,前提是有上下文服务。
asio::ip::tcp protocol = asio::ip::tcp::v6();
asio::ip::tcp::acceptor acceptor(ios);
boost::system::error_code ec;
if (ec.value() != 0) {
std::cout << "Failed to open" << "error code = " << ec.value() << ".Message:" << ec.message();
return ec.value();
}
return 0;
}

新的写法如下所示:

int create_tcp_acceptor_socket()
{
asio::io_context ios;//创建socket,前提是有上下文服务。
asio::ip::tcp::acceptor a(ios, asio::ip::tcp::endpoint(asio::ip::tcp:v4(),3333));
return 0;
}

5.解析DNS名称

原始IP地址非常不方便人类感知和记住,尤其是当它们是IPv6地址时。为了能够使用用户友好的名称标记网络中的设备,引入了域名系统(DNS)。简而言之,DNS是一种分布式命名系统,允许将人类友好名称与计算机网络中的设备相关联。DNS名称或域名是表示计算机网络中设备名称的字符串。

准确地说,DNS 名称是一个或多个 IP 地址的别名,而不是设备的别名。它不命名特定的物理设备,而是命名可以分配给设备的 IP 地址。因此,DNS 在寻址网络中的特定服务器应用程序时引入了一定程度的间接性。DNS 充当分布式数据库,存储 DNS 名称到相应 IP 地址的映射,并提供一个接口,允许查询特定 DNS 名称映射到的 IP 地址。将 DNS 名称转换为相应 IP 地址的过程称为 DNS 名称解析(DNS name resolution)。现代网络操作系统包含可以查询 DNS 以解析 DNS 名称的功能,并提供应用程序可用于执行 DNS 名称解析的接口。

以下算法描述了在客户端应用程序中需要执行的步骤,以便解析 DNS 名称以获得运行客户端应用程序想要与之通信的服务器应用程序的主机(零个或多个)的 IP 地址(零个或多个):

  1. 获取指定服务器应用程序的 DNS 名称和协议端口号,并将它们表示为字符串。
  2. 创建一个 asio::io_service 类的实例,或者使用之前已创建的实例。
  3. 创建一个表示 DNS 名称解析查询的 resolver::query 类对象。
  4. 创建一个适用于所需协议的 DNS 名称解析器类实例。
  5. 调用解析器的 resolve() 方法,并将步骤3中创建的查询对象作为参数传递给它。

代码示例如下:

int dns_connect_to_end(){
std::string host = "samplehost.com";
std::string port_num = "3333";
asio::io_context ios;
asio::ip::tcp::resolver::query resolver_query(host, port_num,
asio::ip::tcp::resolver::query::numeric_service);
asio::ip::tcp::resolver resolver(ios);
try {
asio::ip::tcp::resolver::iterator it = resolver.resolve(resolver_query);
asio::ip::tcp::socket sock(ios);
asio::connect(sock, it);
}catch (system::system_error& e) {
std::cout << "Failed to resolve a DNS name."
<< "Error code = " << e.code()
<< ". Message = " << e.what();
return e.code().value();
}
return 0;
}

6.将socket绑定到终端节点

将socket和特定的节点进行关联的过程叫做绑定(binding)。
在主动套接字可以与远程应用程序通信或被动套接字可以接受传入连接请求之前,它们必须与特定的本地 IP 地址(或多个地址)和协议端口号(即端点)相关联。当套接字绑定到端点时,从网络进入主机的所有网络数据包(以该端点作为其目标地址)将作系统重定向到该特定套接字。同样,从绑定到特定端点的套接字传出的所有数据都将被输出,通过与该终端节点中指定的相应 IP 地址关联的网络接口从主机到网络。
有些操作隐式绑定未绑定的套接字。例如,将未绑定的主动套接字连接到远程应用程序的操作将隐式地将其绑定到底层操作系统选择的IP地址和协议端口号。通常,客户端应用程序不需要显式地将活动套接字绑定到特定的端点,因为它不需要该特定端点与服务器通信;它只需要用于此目的的任何端点。

以下描述了创建一个被动套接字并将其绑定到指定主机上可用的所有IP地址和IPv4 TCP服务器应用程序中的特定协议端口号的端点所需的步骤:

  1. 获取协议端口号,服务器应该在该端口号上侦听传入的连接请求。
  2. 创建一个端点,该端点表示主机上所有可用的IP地址和步骤1中获得的协议端口号。
  3. 创建并打开一个被动socket。
  4. 调用被动套接字的bind()方法,将端点对象作为参数传递给它。

代码示例如下:

int bind_acceptor_socket()
{
unsigned port_num = 3333;
asio::ip::tcp::endpoint ep(asio::ip::address_v4::any(),port_num);
asio::io_service 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 bind" << ec.value() << ".Message" << ec.message();
return ec.value();
}
return 0;
}

7.connect函数(连接socket)

在使用TCP套接字与远程应用程序通信之前,它必须与远程应用程序建立逻辑连接。根据TCP协议,连接的建立过程是两个应用程序之间交换服务消息,如果交换成功,则两个应用程序在逻辑上连接并准备相互通信。

假设两个连接的套接字之间采用点对点通信模型。这意味着如果套接字A连接到套接字B,则两者只能通信。不能与任何其他socket c通信,在socket A可以之前与套接字C通信时,必须关闭与套接字B的连接并建立与套接字C的新连接。

下面的算法描述了在TCP客户端应用程序中将活动套接字连接到服务器应用程序所需执行的步骤:

  1. 获取目标服务器应用程序的IP地址和协议端口号。
  2. 根据步骤1中获取的ip地址和协议端口号,创建asio::ip::tcp::endpoint类的对象。
  3. 创建并打开一个主动套接字。
  4. 调用套接字的connect()方法,指定在步骤2中创建的端点对象作为参数。
  5. 如果该方法成功,则认为套接字已连接,并可用于向服务器发送和接收数据。
int connect_to_acceptor()
{
std::string raw_ip_address = "192.168.1.10";//假设事先知道IP地址
unsigned short port_num = 3333;
try {
//创建端点,作为客户端用来连接,作为服务器端用来绑定(bind)
asio::ip::tcp::endpoint ep(asio::ip::address::from_string(raw_ip_address), port_num);
asio::io_context ios;
asio::ip::tcp::socket sock(ios, ep.protocol());
sock.connect(ep);//connect
}
catch (system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
return 0;
}

8. (接收连接)Accepting connections

当客户端应用程序希望通过TCP协议与服务器应用程序通信时,它首先需要与服务器建立逻辑连接。因此,客户端分配一个主动套接字并对其发出连接命令(例如,通过调用套接字对象上的connect()方法),这将导致将连接建立请求消息发送到服务器。在服务端,在服务端应用能够接受和处理来自客户机的连接请求之前,必须执行一些操作。在此之前,所有针对此服务器应用的连接请求都将被操作系统拒绝。

请注意,接收套接字(acceptor socket)仅用于与客户机应用程序建立连接,在进一步的通信过程中不使用。处理挂起的连接请求时,接收方套接字分配一个新的主动套接字,将其绑定到操作系统选择的端点,并将其连接到发出该连接请求的相应客户端应用程序。然后,这个新的活动套接字就可以用于与客户机通信了。接收方套接字可用于处理下一个挂起的连接请求。

接收连接的步骤如下:

  1. 获取服务器接收传入连接请求的端口号。
  2. 创建服务器端点。
  3. 实例化并打开一个接收(acceptor) socket。
  4. 将acceptor socket绑定到步骤2中创建的服务器端点。
  5. 调用acceptor socket的listen()方法,使其开始侦听端点上传入的连接请求。
  6. 实例化一个主动socket对象。
  7. 当准备好处理连接请求时,调用acceptor socket的accept()方法,并传递在步骤6中创建的主动套接字对象作为参数。、
  8. 如果调用成功,主动套接字将连接到客户端应用程序,并准备用于与之通信。

该算法假设只有一个传入连接将以同步模式处理。示例代码如下所示:

int accept_new_connection(){
const int BACKLOG_SIZE = 30;//实际缓冲大于30
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());
acceptor.bind(ep);
acceptor.listen(BACKLOG_SIZE);
asio::ip::tcp::socket sock(ios);
acceptor.accept(sock);
}
catch (system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
}
posted @   yyyyyllll  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示