本文介绍如何使用C++编写一个基本的客户端-服务端通信系统。通过这个例子,你将学到如何建立TCP连接、发送和接收消息,以及如何处理多个客户端连接。

客户端代码:

#include <stdio.h>       // 标准输入输出库,提供基本的输入输出功能
#include <stdlib.h>      // 标准库,包含了一些通用的函数和动态内存分配函数
#include <string.h>      // 字符串处理库,提供字符串操作的各种函数
#include <unistd.h>      // Linux系统调用接口,包含了一些常用的系统调用函数
#include <arpa/inet.h>   // 提供了一些函数,用于对IPv4和IPv6地址进行转换
#include <errno.h>       // 用于获取错误码,提供 perror 函数来输出错误信息

int main(int argc, char *argv[]) {
    if (argc != 4) {
        fprintf(stderr, "Usage: %s <server_ip> <server_port> <message>\n", argv[0]);
        return EXIT_FAILURE;
    }

    int sockfd;
    struct sockaddr_in servaddr;

    // 创建套接字
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("create socket error");
        return EXIT_FAILURE;
    }

    // 初始化服务器地址结构体
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(atoi(argv[2]));  // 将端口号从字符串转换为整数

    // 将IP地址从字符串转换为网络地址
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
        perror("inet_pton error");
        close(sockfd);
        return EXIT_FAILURE;
    }

    // 发起连接请求
    if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
        perror("connect error");
        close(sockfd);
        return EXIT_FAILURE;
    }

    printf("Connected to the server. Sending message: %s\n", argv[3]);

    // 发送消息到服务器
    if (send(sockfd, argv[3], strlen(argv[3]), 0) < 0) {
        perror("send error");
        close(sockfd);
        return EXIT_FAILURE;
    }

    // 接收服务器的响应
    char buffer[1024];
    ssize_t recv_len = recv(sockfd, buffer, sizeof(buffer), 0);

    if (recv_len < 0) {
        perror("recv error");
        close(sockfd);
        return EXIT_FAILURE;
    } else if (recv_len == 0) {
        printf("Connection closed by the server\n");
    } else {
        buffer[recv_len] = '\0';
        printf("Received from server: %s\n", buffer);
    }

    // 关闭套接字
    close(sockfd);

    return EXIT_SUCCESS;
}

服务端代码:

#include <iostream>     // 输入输出流库,提供了输入输出的各种功能
#include <cstring>      // 字符串处理库,提供了字符串操作的各种函数
#include <thread>       // 多线程支持库,用于创建和管理线程
#include <vector>       // 动态数组容器,提供了对动态数组的支持
#include <mutex>        // 互斥锁库,提供了对互斥锁的支持
#include <queue>        // 队列容器,提供了对队列的支持
#include <netinet/in.h> // 网络编程库,包含了与网络相关的数据结构和函数
#include <unistd.h>     // Linux系统调用接口,包含了一些常用的系统调用函数

const int MAXBUFF = 1024;

std::mutex g_mx; // 用于保护共享资源的互斥锁
std::queue<std::pair<int, std::string>> g_dataQue; // 存储客户端套接字和消息的队列

// 在单独的线程中处理从客户端接收到的消息
void writeThread() {
    while (true) {
        g_mx.lock(); // 上锁以确保安全访问共享资源
        if (!g_dataQue.empty()) {
            int clientFd = g_dataQue.front().first;
            std::string data = g_dataQue.front().second;
            std::cout << "从客户端接收到的消息:" << data << std::endl;

            // 假设有一个处理消息并返回响应的函数
            std::string response = "Server response: 你好,客户端!";
            send(clientFd, response.c_str(), response.size(), 0);

            g_dataQue.pop(); // 从队列中移除已处理的消息
        }
        g_mx.unlock(); // 解锁
    }
}

int main() {
    int listenFd, clientFd;
    struct sockaddr_in servaddr;

    if ((listenFd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        std::cerr << "创建套接字错误" << std::endl;
        return -1;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(1121);

    if (bind(listenFd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        std::cerr << "绑定套接字地址和端口错误" << std::endl;
        return -1;
    }

    if (listen(listenFd, 10) < 0) {
        std::cerr << "开启监听错误" << std::endl;
        return -1;
    }

    std::thread write_thread(writeThread); // 创建一个线程来处理从客户端接收到的消息
    size_t readLen = 0;

    while (true) {
        struct sockaddr_in client_addr;
        socklen_t size = sizeof(client_addr);

        if ((clientFd = accept(listenFd, (struct sockaddr *)&client_addr, &size)) < 0) {
            std::cerr << "建立连接错误" << std::endl;
            return -1;
        }

        // 假设有一个读取函数来从 clientFd 中读取数据
        char buff[MAXBUFF] = {0};
        readLen = read(clientFd, buff, MAXBUFF);

        if (readLen <= 0) {
            close(clientFd); // 在读取到数据后关闭客户端连接
            break;
        }

        std::string data(buff, readLen);
        g_mx.lock(); // 上锁以确保安全访问共享资源
        g_dataQue.push(std::make_pair(clientFd, data)); // 将接收到的消息和客户端套接字放入队列
        g_mx.unlock(); // 解锁
    }

    write_thread.join(); // 等待写线程结束
    close(listenFd);

    return 0;
}

客户端使用方法:

打开终端,进入客户端代码所在的目录。使用以下命令运行客户端程序:

gcc client.cpp -o client
./client <server_ip> <server_port> <message>

替换 <server_ip><server_port><message> 分别为服务器的IP地址、端口号和要发送的消息。

服务端使用方法:

打开终端,进入服务端代码所在的目录。使用以下命令编译并运行服务端程序:

g++ server.cpp -o server -lpthread
./server

服务端将开始监听连接。

主要功能:

客户端:

  • 接受命令行参数,包括服务器IP、端口号和要发送的消息。
  • 创建套接字,连接到服务器。
  • 发送消息到服务器,接收并打印服务器的响应。

服务端:

  • 创建套接字,绑定地址和端口,开始监听。
  • 接受客户端连接,将客户端套接字和消息放入队列。
  • 在单独的线程中处理队列中的消息,发送响应到客户端。

注意事项:

  • 通过互斥锁保护共享资源,确保线程安全的访问。
  • 在服务端中,处理客户端消息后返回一个固定的响应。

结论:

通过这个简单的例子,你学到了如何使用C++创建一个基本的客户端-服务端通信系统。这是一个基础框架,可以根据实际需求进行扩展和改进,用于构建更复杂的网络应用。

11-23 11:40