“深入浅出”系列之C++:(3)网络编程

目录

一、Socket

二、Boost

三、Asio

一、Socket

一种网络编程接口,用于在不同主机上的进程之间进行网络通信,Socket通信过程是一种计算机网络通信协议的实现方式,它基于TCP/IP协议栈的应用层,提供了一组API,允许不同的进程在不同计算机之间进行通信。套接字是网络编程中用于通信的一种抽象概念。在C++中使用<sys/socket.h>中定义的套接字函数来创建和操作套接字。目的是开发采用套接字通信的C/S网络应用程序。套接字是一个主机本地应用程序所创建的, 为操作系统所控制的接口 (“门”) ,应用进程通过这个接口,使用传输层提供的服务, 跨网络发送(接收)报文到(从)其他应用进程。是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。

IP地址是网络中设备的唯一标识。

端口号是区分同一台计算机上不同进程的标识。

流程:

通常涉及客户端和服务端,它们之间的通信过程:

1、服务器端初始化Socket:

根据地址类型(如IPv4、IPv6)、Socket类型(如流式Socket用于TCP,数据报Socket用于UDP)和协议创建Socket。

将创建的Socket与本地的IP地址和端口号进行绑定。

对该端口进行监听,等待客户端的连接请求。

2、客户端创建Socket:

根据地址类型、Socket类型和协议创建Socket。打开Socket,并通过服务器的IP地址和端口号尝试连接服务器的Socket。

3、建立连接:

当客户端发送连接请求时,服务器端的Socket会接收到这个请求,并被动打开Socket,开始接收客户端的请求。

在TCP/IP协议中,这个过程通过三次握手来建立可靠的连接:

客户端发送一个SYN包(同步序列编号)到服务器,表示请求连接。

服务器接收到SYN包后,发送一个SYN+ACK包(同步+确认)给客户端,表示同意连接并确认客户端的SYN包。

客户端接收到SYN+ACK包后,发送一个ACK包(确认)给服务器,表示连接建立成功。

4、数据传输:

连接建立后,客户端和服务器之间就可以进行数据传输了。

客户端通过Socket向服务器发送数据请求,服务器接收到请求后处理这些请求,并将回应数据发送给客户端。

数据传输过程中,TCP协议会确保数据的可靠性、完整性和顺序性。

5、断开连接:四次挥手

当数据传输完成后,某个应用进程会主动关闭连接,这时TCP会发送一个FIN报文(包含FIN标志的报文),表示已经完成数据的发送并请求关闭连接。

另一端接收到FIN报文后,会发送一个ACK报文进行确认,并可能继续发送剩余的数据(如果有的话)。

当数据发送完成后,另一端也会发送一个FIN报文给对方,表示自己也完成了数据的发送并请求关闭连接。

接收到FIN报文的源发送端会发送一个ACK报文进行确认,至此连接完全断开。

6、关闭Socket:在连接断开后,客户端和服务器的Socket都会被关闭,释放相关的资源。

在Socket通信过程中,会用到一些主要的函数来执行不同的操作。这些函数包括:

socket():用于创建一个新的Socket。
bind():用于将Socket与本地的IP地址和端口号进行绑定。
listen():用于监听指定端口上的连接请求(服务器端使用)。
connect():用于客户端尝试连接服务器的Socket。
accept():用于服务器端接受客户端的连接请求,并返回一个与客户端成功连接的Socket文件描述符。
read():用于读取Socket中的数据。
write():用于向Socket中写入数据。
close():用于关闭Socket并释放相关资源。

可以把socket想象成打电话,电话号码就是IP地址,打电话找谁就是端口号,协议就像是你说的是方言还是普通话,tcp就像是你每说一句话,对方都要说一声:"清楚明白"!udp就是,什么都不管,只管说就行了,也不管对方是否明白!tcp连接过程,就像是,打电话之后先说:"喂,你是xxx吗?",对方回答:"我是xxx!",然后你说:"好的,下面开始通话!"

用C语言实现继承,比如说你想实现了sock虚类,要支持收发,就可以

typedef struct {
    void send(void*);
    void recv(void*);
}sock_ops;
//然后所以sock对象都实现成
typedef struct {
    void *data;
    sock_ops* ops;
} Sock; 
//接着,比如你实现了TCP,就可以定义一个 tcp_ops tcp的sock实例就是
    sock *sock1 = malloc(sizeof(sock));
    sock1->data = create_tcp(...);
    sock1->ops = tcp_ops;
//然后要让它发数据就是
    sock1->ops->send(sock1->data);
//如果你有一个数组
    sock* sockets【10】;
//那就可以
    for(int i = 0; i < 10;++i) {
         sockets【i】->ops->send(sockets【i】->data);
 }

这样,sockets内部的每个元素都可以是不同的协议,而你不需要知道具体什么协议

通过网络编程实现基于网络的应用程序,实现计算机之间的通信和数据交换。

client/server模式

通过Socket API,提供传输层的2类传输服务:

不可靠的数据报传输、可靠的字节流传输

1. TCP进行套接字编程

Socket: 应用进程和传输层协议(UCP or TCP)之间的门。

TCP服务: 从1个进程到另1个进程的字节流的可靠传输。   

Socket是从UNIX的I/O命令集发展而来的。Socket为上层实体提供一种透明的访问网络的能力,本质上说是传输层的服务原语。

TCP 中的套接字系统调用

​​客户必须初始联系服务器+服务器进程必须先运行

服务器进程必须创建套接字 (门) 来迎候客户的初始联系

客户如何初始联系服务器

创建客户本地TCP socket

指定服务器进程的IP地址, 端口号

一旦客户创建套接字, 客户TCP 就发起3次握手并建立与服务器 TCP的连接

一旦客户初始联系(敲门)服务器, 服务器TCP为服务器进程创建1个新的socket 与客户进程通信

允许服务器与多个客户通信

源端口号被用来区分客户

从应用程序的角度来看TCP 为客户和服务器提供了可靠的, 顺序的,字节流的传输 (“管道”) 。

2、IP地址和端口号:每个主机在网络上都有一个唯一的IP地址,用于标识主机。端口号是为了区分一个主机上的不同应用程序而存在的。在C++中,我们可以使用<arpa/inet.h>头文件中的函数来处理IP地址和端口号。

3、客户端和服务器:网络应用程序通常分为客户端和服务器端。客户端向服务器请求服务,而服务器则提供服务。客户端和服务器之间通过套接字进行通信。

使用C++进行网络编程:

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
#include <thread>
#include <vector>

// 客户端处理线程函数
void clientHandler(int clientSocket)
{

    // 接收客户端用户名
    char username[1024];
    recv(clientSocket, username, sizeof(username), 0);
    // 向其他客户端广播新用户加入信息
    std::string joinMessage = username;
    joinMessage += "加入了聊天室\n";
    std::cout << joinMessage;
    for (auto socket : connectedClients)
    {
        if (socket != clientSocket)
        {
            send(socket, joinMessage.c_str(), joinMessage.length(), 0);
        }
    }
    // 接收和转发消息
    while (true)
    {
        char buffer[1024] = {0};
        int bytesRead = recv(clientSocket, buffer, sizeof(buffer), 0);
        if (bytesRead <= 0)
        {

            // 客户端断开连接
            close(clientSocket);
            // 向其他客户端广播用户离开信息
            std::string leaveMessage = username;
            leaveMessage += "离开了聊天室\n";
            std::cout << leaveMessage;
            for (auto socket : connectedClients)
            {

                if (socket != clientSocket)
                {
                    send(socket, leaveMessage.c_str(), leaveMessage.length(), 0);
                }
            }
            // 从已连接客户端列表中移除该客户端
            connectedClients.erase(std::find(connectedClients.begin(),connectedClients.end(), clientSocket));
            break;
        }
        // 向其他客户端广播消息
        std::string message = username;
        message += ": ";
        message += buffer;
        std::cout << message;
        for (auto socket : connectedClients)
        {
            if (socket != clientSocket)
            {
                send(socket, message.c_str(), message.length(), 0);
            }
        }
    }
}

int main()
{
    // 创建服务器套接字
    int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocket == -1)
    {
        std::cerr << "无法创建套接字" << std::endl;
        return -1;
    }
    // 定义服务器地址
    struct sockaddr_in serverAddress;
    serverAddress.sin_family = AF_INET;
    serverAddress.sin_port = htons(8080);
    serverAddress.sin_addr.s_addr = INADDR_ANY;
    // 绑定套接字到服务器地址
    if (bind(serverSocket, (struct sockaddr *)&serverAddress, sizeof(serverAddress)) == -1)
    {
        std::cerr << "绑定套接字失败" << std::endl;
        return -1;
    }
    // 监听连接请求
    if (listen(serverSocket, SOMAXCONN) == -1)
    {
        std::cerr << "监听连接请求失败" << std::endl;
        return -1;
    }
    std::cout << "聊天室服务器已启动,等待客户端连接..." << std::endl;
    // 接受客户端连接并创建处理线程
    std::vector<std::thread> clientHandlers;
    while (true)
    {

        struct sockaddr_in clientAddress;
        socklen_t clientAddressSize = sizeof(clientAddress);
        int clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddress, &clientAddressSize);
        if (clientSocket == -1)
        {
            std::cerr << "无法接受客户端连接" << std::endl;
            return -1;
        }
        // 将客户端套接字添加到已连接客户端列表中
        connectedClients.push_back(clientSocket);
        // 创建客户端处理线程
        std::thread handlerThread(clientHandler, clientSocket);
        handlerThread.detach();
    }
    // 关闭服务器套接字
    close(serverSocket);
    return 0;

}

二、Boost

Beast库是一个作为 Boost 库一部分的 C++ 库,它提供了低级网络编程接口,专为HTTP和WebSocket设计。此库旨在让开发者能够高效地构建高性能的网络服务而无需过多关注底层细节。通过利用C++的现代特性,Beast使得在TCP上实现标准Web协议变得更加直观和强大。

1. 项目目录结构及介绍

Boost.Beast 的GitHub仓库大致结构如下:

目录解析:

docs : 提供详细的API文档和教程。
example : 实际可用的小型应用程序示例,帮助理解如何使用Beast进行编程。
include : 包含所有Beast库的头文件,是开发者日常工作中频繁访问的区域。
test : 包含单元测试,对于了解库的内部逻辑和功能覆盖度非常有用。

2. 项目的启动文件介绍

Boost.Beast本身不直接提供一个“启动文件”,因为它的使用依赖于嵌入到用户自己的应用程序中。然而,在example目录下,有多个入门级别的示例,如http.server.basic, http.client.sync等,这些可以视作“启动点”去学习如何开始编写基于Beast的应用。

例如,从http.server.basic开始,主要启动逻辑通常位于一个main函数内,该函数设置服务器监听端口,接收请求并响应。这通常涉及到创建Beast的核心对象,比如http::server类实例(虽然实际的类名可能有所不同,具体取决于你的实现细节)。

3. 项目的配置文件介绍

Boost.Beast作为一个库,并没有要求特定的配置文件。其配置主要通过编码方式实现,即在代码中设定参数、端口号、日志级别等。对于应用程序来说,如果需要配置Beast的行为,通常是自定义的配置逻辑,通过环境变量、命令行参数或外部配置文件(XML、JSON、YAML等)来读取,并在应用初始化阶段处理这些配置信息。

例如,如果你的应用需要动态指定服务器地址和端口,你会在代码里添加解析这类信息的逻辑,而不是Beast库直接支持的配置文件读取机制。

三、Asio

跨平台的C++库,用于网络和底层I/O编程,可以在I/O对象(如socket)上执行同步和异步操作。该库可以让C++异步地处理数据,且平台独立。异步数据处理就是指,任务触发后不需要等待它们完成。 相反,Boost.Asio 会在任务完成时触发一个应用。 异步任务的主要优点在于,在等待任务完成时不需要阻塞应用程序,可以去执行其它任务。

异步任务的典型例子是网络应用。如果数据被发送出去了,比如发送至 Internet,通常需要知道数据是否发送成功。如果没有一个象 Boost.Asio这样的库,就必须对函数的返回值进行求值。但是,这样就要求待至所有数据发送完毕,并得到一个确认或是错误代码。而使用 Boost.Asio,这个过程被分为两个单独的步骤:第一步是作为一个异步任务开始数据传输。一旦传输完成,不论成功或是错误,应用程序都会在第二步中得到关于相应的结果通知。主要的区别在于,应用程序无需阻塞至传输完成,而可以在这段时间里执行其它操作。

I/O 服务与 I/O 对象

使用Boost.Asio进行异步数据处理的应用程序基于两个概念:I/O 服务和 I/O 对象。I/O 服务抽象了操作系统的接口,允许第一时间进行异步数据处理,而 I/O 对象则用于初始化特定的操作。鉴于Boost.Asio只提供了一个名为

boost::asio::io_service 的类作为 I/O 服务,它针对所支持的每一个操作系统都分别实现了优化的类,另外库中还包含了针对不同 I/O 对象的几个类。 其中,类 boost::asio::ip::tcp::socket 用于通过网络发送和接收数据,而类 boost::asio::deadline_timer 则提供了一个计时器,用于测量某个固定时间点到来或是一段指定的时长过去了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值