从零开始学习Linux网络编程C++ Day2

在掌握了基本的 Linux 网络编程知识后,为了让代码更易于管理和重用,今天我们将对客户端和服务端的代码进行封装和重构。通过设计一个通用的 Socket 类,我们可以简化代码结构、提高可读性,并方便未来项目的使用。

构建简单的客户端和服务端可以查看上一篇文章从零开始学习Linux网络编程C++ Day1.

一、Socket 类的设计与实现

1. 类的概述与成员变量

class Socket {
public:
    Socket();                         // 创建 Socket
    Socket(int sockfd);               // 使用现有 Socket 文件描述符
    ~Socket();                        // 析构函数,关闭 Socket

    bool bind(const string &ip, int port);  // 绑定 Socket
    bool listen(int backlog);          // 监听请求
    int accept();                     // 接受客户端连接
    int recv(char *buf, int len);     // 接收数据
    int send(const char *buf, int len); // 发送数据
    bool connect(const string &ip, int port); // 连接服务端
    void close();                     // 关闭 Socket

protected:
    string m_ip;                   // IP 地址
    int m_port;                    // 端口号
    int m_sockfd;                  // 套接字文件描述符
};
  • 构造函数Socket()Socket(int sockfd) 提供了灵活的创建方式,支持直接创建新的 Socket 或使用已有的文件描述符(sockfd)。

  • 析构函数 :在对象被销毁时自动关闭 Socket,释放资源。

  • 成员变量m_ipm_port 存储了 IP 地址和端口号,m_sockfd 是 Socket 的文件描述符,用于后续操作。

2. Socket 的初始化 

在默认的 Socket 构造函数中,我们使用 socket 函数创建一个新的 Socket:

Socket::Socket() : m_ip(""), m_port(0), m_sockfd(0) {
    m_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (m_sockfd < 0) {
        // 获取错误信息并记录到日志中
        const char* error_message = strerror(errno);
        ofstream log_file("socket_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "Socket creation failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
    }
}

如果创建失败,我们会获取错误信息并将其记录到日志文件(socket_error.log)中,方便后续调试。

3. 绑定

bool Socket::bind(const string &ip, int port) {
    struct sockaddr_in sockaddr;
    memset(&sockaddr, 0, sizeof(sockaddr));
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_port = htons(port);

    if (ip.empty()) {
        sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有可用的 IP 地址
    } else {
        sockaddr.sin_addr.s_addr = inet_addr(ip.c_str()); // 绑定到指定 IP 地址
    }

    if (::bind(m_sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0) {
        // 记录绑定失败的错误信息
        // ...
        return false;
    }
    return true;
}
  • bind 方法通过调用系统的 bind 函数,将 Socket 绑定到指定的 IP 地址和端口号。如果 IP 地址为空,将绑定到服务器的所有可用 IP 地址。

  • 如果绑定失败,将错误信息记录到 bind_error.log 文件中。

 4.监听

listen 方法则用于将绑定的 Socket 转换为监听状态,等待客户端连接:

bool Socket::listen(int backlog) {
    if (::listen(m_sockfd, backlog) < 0) {
        // 记录监听失败的错误信息
        // ...
        return false;
    }
    return true;
}

5.接受连接

int Socket::accept() {
    int connfd = ::accept(m_sockfd, nullptr, nullptr);
    if (connfd < 0) {
        const char* error_message = strerror(errno);
        ofstream log_file("accept_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "accept failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return -1;
    }
    return connfd;
}
  • 功能 :从监听队列中取出一个客户端连接请求,并返回一个新的套接字描述符。

  • 返回值 :成功返回新的套接字描述符,失败返回 -1

  • 日志记录 :如果接受连接失败,将错误信息记录到 accept_error.log 文件中。

6.接收数据 /发送数据

int Socket::recv(char *buf, int len) {
    return ::recv(m_sockfd, buf, len, 0);
}
  • 功能 :从套接字中接收数据。

  • 参数buf 为存储接收数据的缓冲区,len 为缓冲区长度。

  • 返回值 :成功返回接收到的字节数,失败返回 -1

int Socket::send(const char *buf, int len) {
    return ::send(m_sockfd, buf, len, 0);
}
  • 功能 :向套接字发送数据。

  • 参数buf 为要发送的数据缓冲区,len 为数据长度。

  • 返回值 :成功返回发送的字节数,失败返回 -1

7.连接 

bool Socket::connect(const string &ip, int port) {
    struct sockaddr_in sockaddr;
    memset(&sockaddr, 0, sizeof(sockaddr));
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_addr.s_addr = inet_addr(ip.c_str());
    sockaddr.sin_port = htons(port);

    if (::connect(m_sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0) {
        const char* error_message = strerror(errno);
        ofstream log_file("connect_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "connect failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
    m_ip = ip;
    m_port = port;
    return true;
}
  • 功能 :客户端连接到指定的服务端。

  • 参数ip 为服务端 IP 地址,port 为服务端端口号。

  • 返回值 :连接成功返回 true,失败返回 false

  • 日志记录 :如果连接失败,将错误信息记录到 connect_error.log 文件中。

8.关闭 

void Socket::close() {
    if (m_sockfd > 0) {
        ::close(m_sockfd);
        m_sockfd = 0;
    }
}

二、完整代码

socket.h:

#pragma once
#include <string>
using namespace std;
class Socket{
    public:
    Socket();//创建socket
    Socket(int sockfd);
    ~Socket();

    bool bind(const string & ip,int port);//绑定socket
    bool listen(int backlog);//监听,backlog是最大接受长度
    int accept();//接受请求
    int recv(char * buf,int len);//接收消息
    int send(const char * buf,int len);//发送消息
    bool connect(const string & ip,int port);//连接
    void close();

    protected:
    string m_ip;//IP
    int m_port;//端口
    int m_sockfd;//套接字
};

socket.cpp

#include "socket.h"
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>//有IPPROTO_TCP
#include <unistd.h>//close
#include <iostream>
#include <fstream>
#include <cstring>  // 用于 strerror
using namespace std;


Socket::Socket():m_ip(""),m_port(0),m_sockfd(0)
{
    m_sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    if(m_sockfd<0){
        // 获取错误信息
        const char* error_message = strerror(errno);
        
        // 打开日志文件并写入错误信息
        ofstream log_file("socket_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "Socket creation failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
    }
}

Socket::Socket(int sockfd):m_ip(""),m_port(0),m_sockfd(sockfd){}

Socket::~Socket(){
    close();
}

bool Socket::bind(const string & ip,int port){
    struct sockaddr_in sockaddr;
    memset(&sockaddr,0,sizeof(sockaddr));
    sockaddr.sin_family=AF_INET;
    if(ip.empty()){
        //如果没有上传ip地址,就随便选一个网卡的ip地址
        sockaddr.sin_addr.s_addr=htonl(INADDR_ANY);
    }else{
        sockaddr.sin_addr.s_addr=inet_addr(ip.c_str());
    }
    sockaddr.sin_port=htons(port);
    if(::bind(m_sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr))<0){//调用的是全局命名空间中的 bind 函数,而不俗socket类中的,如果不加::就需要将socket类中的bind函数名修改一下
        // 获取错误信息
        const char* error_message = strerror(errno);
        
        // 打开日志文件并写入错误信息
        ofstream log_file("bind_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "bind failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
    return true;
}

bool Socket::listen(int backlog){
    if(::listen(m_sockfd,backlog)<0){//::同上bind
        // 获取错误信息
        const char* error_message = strerror(errno);
        
        // 打开日志文件并写入错误信息
        ofstream log_file("listen_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "listen failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
    return true;
}

int Socket::accept(){
    int connfd=::accept(m_sockfd,nullptr,nullptr);
    //connfd 是专门为该客户端创建的套接字描述符,包含与客户端通信所需的所有必要信息。
    if(connfd<0){
        // 获取错误信息
        const char* error_message = strerror(errno);
        
        // 打开日志文件并写入错误信息
        ofstream log_file("accept_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "accept failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
    return connfd;
}

int Socket::recv(char * buf,int len){
    return ::recv(m_sockfd,buf,len,0);
}

int Socket::send(const char * buf,int len){
    return ::send(m_sockfd,buf,len,0);
}

bool Socket::connect(const string & ip,int port){
    struct sockaddr_in sockaddr;
    memset(&sockaddr,0,sizeof(sockaddr));
    sockaddr.sin_family=AF_INET;
    sockaddr.sin_addr.s_addr=inet_addr(ip.c_str());//inet_addr 的作用:将点分十进制的 IPv4 地址字符串转换为网络字节序的 32 位无符号整数
    sockaddr.sin_port=htons(port);//htons(port) 是一个用于将主机字节序(Host Byte Order)的短整型数值转换为网络字节序(Network Byte Order)

    if(::connect(m_sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr))<0){
        // 获取错误信息
        const char* error_message = strerror(errno);
        
        // 打开日志文件并写入错误信息
        ofstream log_file("connect_error.log", ios::app);
        if (log_file.is_open()) {
            log_file << "connect failed. Error: " << error_message << endl;
            log_file.close();
        } else {
            cerr << "Failed to open log file" << endl;
        }
        return false;
    }
    m_ip=ip;
    m_port=port;
    return true;
}

void Socket::close(){
    if(m_sockfd>0){
        ::close(m_sockfd);
        m_sockfd=0;
    }
}

三、使用方式

客户端

#include "socket/socket.h"
#include <iostream>
#include <cstring>

using namespace std;

int main(){
    //第一步,创建socket
    Socket client;

    string ip="192.168.66.132";//服务端的ip
    int port=50000;//服务端的端口
    string data;
    int c;
    char buf[1024]={0};
    //第二步,连接服务端
    client.connect(ip,port);

    while(true){
        cout<<"请选择:1.... 2.... 3.... 0.退出"<<endl;
        cin>>c;
        cin.ignore(); // 清除缓冲区中的换行符
        cout<<"请输入要传送的内容"<<endl;
        getline(cin, data);
        string result=to_string(c)+data;
        data.clear();
        //第三步,向服务端发送数据
        client.send(result.c_str(),result.size());
        
        if(c==0){
            break;
        }
        
        //第四步,接受服务端消息
        memset(buf, 0, sizeof(buf)); // 清空缓冲区
        client.recv(buf,sizeof(buf));
        cout<<buf<<endl;
    }
    
    //第五步,关闭socket
    client.close();
}

服务端

#include <iostream>
#include <cstring>
#include "socket/socket.h"

using namespace std;

int main(){
    //第一步,创建服务端socket
    Socket serve;

    //第二步,绑定socket
    string ip="192.168.66.132";//自己服务端的ip
    int port=50000;//自己服务端的端口号
    serve.bind(ip,port);

    //第三步,监听socket
    serve.listen(10240);

    char buf[1024]={0};//用于接受客户端发送过来的信息
    while(true){
        //第四步,接受客户端连接
        int connfd=serve.accept();

        Socket client(connfd);
        
        while(true){
            memset(buf, 0, sizeof(buf));
            int bytes_received = client.recv(buf, sizeof(buf)-1); // 保留一个字节给\0
            
            if(bytes_received <= 0){ // 处理连接关闭
                cout << "Connection closed" << endl;
                client.close();
                break;
            }
            
            buf[bytes_received] = '\0'; // 确保字符串终止
            cout << "Received: " << buf << endl;
        
            // 构造响应
            string response;
            switch(buf[0]){
                case '1': response = "Response for option 1"; break;
                case '2': response = "Response for option 2"; break;
                case '3': response = "Response for option 3"; break;
                case '0': 
                    response = "Goodbye";
                    client.send(response.c_str(), response.length());
                    client.close();
                    goto break_label;
                    break;
                default: response = "Invalid option";
            }
            
            // 发送响应
            client.send(response.c_str(), response.length());
            break_label:
            break;
        }

    //第七步,关闭socket
    serve.close();
    return 0;
    }
}

四、vscode自定义头文件导入失败解决方式

报错找不到头文件。原因是在默认的tasks.json文件中,编译参数不对。

将需要编译的文件路径添加进去就好了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值