在掌握了基本的 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_ip和m_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文件中,编译参数不对。

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

被折叠的 条评论
为什么被折叠?



