赞
踩
如何封装一个简单的TCP客户端与服务端类,并使其进行通信。
代码来自b站 码农论坛——C++中高级工程师(数据开放平台)
完整代码如下:
/* * 程序名:demo7.cpp,此程序用于演示封装socket通讯的客户端 */ #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std; class ctcpclient // TCP通讯的客户端类。 { private: int m_clientfd; // 客户端的socket,-1表示未连接或连接已断开;>=0表示有效的socket。 string m_ip; // 服务端的IP/域名。 unsigned short m_port; // 通讯端口。 public: ctcpclient():m_clientfd(-1) {} // 向服务端发起连接请求,成功返回true,失败返回false。 bool connect(const string &in_ip,const unsigned short in_port) { if (m_clientfd!=-1) return false; // 如果socket已连接,直接返回失败。 m_ip=in_ip; m_port=in_port; // 把服务端的IP和端口保存到成员变量中。 // 第1步:创建客户端的socket。 if ( (m_clientfd = socket(AF_INET,SOCK_STREAM,0))==-1) return false; // 第2步:向服务器发起连接请求。 struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。 memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET。 servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口。 struct hostent* h; // 用于存放服务端IP地址(大端序)的结构体的指针。 if ((h=gethostbyname(m_ip.c_str()))==nullptr ) // 把域名/主机名/字符串格式的IP转换成结构体。 { ::close(m_clientfd); m_clientfd=-1; return false; } memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // ③指定服务端的IP(大端序)。 // 向服务端发起连接清求。 if (::connect(m_clientfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1) { ::close(m_clientfd); m_clientfd=-1; return false; } return true; } // 向服务端发送报文,成功返回true,失败返回false。 bool send(const string &buffer) // buffer不要用const char * { if (m_clientfd==-1) return false; // 如果socket的状态是未连接,直接返回失败。 if ((::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false; return true; } // 接收服务端的报文,成功返回true,失败返回false。 // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。 bool recv(string &buffer,const size_t maxlen) { // 如果直接操作string对象的内存,必须保证:1)不能越界;2)操作后手动设置数据的大小。 buffer.clear(); // 清空容器。 buffer.resize(maxlen); // 设置容器的大小为maxlen。 int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作buffer的内存。 if (readn<=0) { buffer.clear(); return false; } buffer.resize(readn); // 重置buffer的实际大小。 return true; } // 断开与服务端的连接。 bool close() { if (m_clientfd==-1) return false; // 如果socket的状态是未连接,直接返回失败。 ::close(m_clientfd); m_clientfd=-1; return true; } ~ctcpclient(){ close(); } }; int main(int argc,char *argv[]) { if (argc!=3) { cout << "Using:./demo7 服务端的IP 服务端的端口\nExample:./demo7 192.168.101.138 5005\n\n"; return -1; } ctcpclient tcpclient; if (tcpclient.connect(argv[1],atoi(argv[2]))==false) // 向服务端发起连接请求。 { perror("connect()"); return -1; } // 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。 string buffer; for (int ii=0;ii<10;ii++) // 循环3次,将与服务端进行三次通讯。 { buffer="这是第"+to_string(ii+1)+"个超级女生,编号"+to_string(ii+1)+"。"; // 向服务端发送请求报文。 if (tcpclient.send(buffer)==false) { perror("send"); break; } cout << "发送:" << buffer << endl; // 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。 if (tcpclient.recv(buffer,1024)==false) { perror("recv()"); break; } cout << "接收:" << buffer << endl; sleep(1); } }
/* * 程序名:demo8.cpp,此程序用于演示封装socket通讯的服务端 */ #include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> using namespace std; class ctcpserver // TCP通讯的服务端类。 { private: int m_listenfd; // 监听的socket,-1表示未初始化。 int m_clientfd; // 客户端连上来的socket,-1表示客户端未连接。 string m_clientip; // 客户端字符串格式的IP。 unsigned short m_port; // 服务端用于通讯的端口。 public: ctcpserver():m_listenfd(-1),m_clientfd(-1) {} // 初始化服务端用于监听的socket。 bool initserver(const unsigned short in_port) { // 第1步:创建服务端的socket。 if ( (m_listenfd=socket(AF_INET,SOCK_STREAM,0))==-1) return false; m_port=in_port; // 第2步:把服务端用于通信的IP和端口绑定到socket上。 struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。 memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family=AF_INET; // ①协议族,固定填AF_INET。 servaddr.sin_port=htons(m_port); // ②指定服务端的通信端口。 servaddr.sin_addr.s_addr=htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯。 // 绑定服务端的IP和端口(为socket分配IP和端口)。 if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1) { close(m_listenfd); m_listenfd=-1; return false; } // 第3步:把socket设置为可连接(监听)的状态。 if (listen(m_listenfd,5) == -1 ) { close(m_listenfd); m_listenfd=-1; return false; } return true; } // 受理客户端的连接(从已连接的客户端中取出一个客户端), // 如果没有已连接的客户端,accept()函数将阻塞等待。 bool accept() { struct sockaddr_in caddr; // 客户端的地址信息。 socklen_t addrlen=sizeof(caddr); // struct sockaddr_in的大小。 if ((m_clientfd=::accept(m_listenfd,(struct sockaddr *)&caddr,&addrlen))==-1) return false; m_clientip=inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串。 return true; } // 获取客户端的IP(字符串格式)。 const string & clientip() const { return m_clientip; } // 向对端发送报文,成功返回true,失败返回false。 bool send(const string &buffer) { if (m_clientfd==-1) return false; if ( (::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false; return true; } // 接收对端的报文,成功返回true,失败返回false。 // buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。 bool recv(string &buffer,const size_t maxlen) { buffer.clear(); // 清空容器。 buffer.resize(maxlen); // 设置容器的大小为maxlen。 int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作buffer的内存。 if (readn<=0) { buffer.clear(); return false; } buffer.resize(readn); // 重置buffer的实际大小。 return true; } // 关闭监听的socket。 bool closelisten() { if (m_listenfd==-1) return false; ::close(m_listenfd); m_listenfd=-1; return true; } // 关闭客户端连上来的socket。 bool closeclient() { if (m_clientfd==-1) return false; ::close(m_clientfd); m_clientfd=-1; return true; } ~ctcpserver() { closelisten(); closeclient(); } }; int main(int argc,char *argv[]) { if (argc!=2) { cout << "Using:./demo8 通讯端口\nExample:./demo8 5005\n\n"; // 端口大于1024,不与其它的重复。 cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n"; cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n"; return -1; } ctcpserver tcpserver; if (tcpserver.initserver(atoi(argv[1]))==false) // 初始化服务端用于监听的socket。 { perror("initserver()"); return -1; } // 受理客户端的连接(从已连接的客户端中取出一个客户端), // 如果没有已连接的客户端,accept()函数将阻塞等待。 if (tcpserver.accept()==false) { perror("accept()"); return -1; } cout << "客户端已连接(" << tcpserver.clientip() << ")。\n"; string buffer; while (true) { // 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待。 if (tcpserver.recv(buffer,1024)==false) { perror("recv()"); break; } cout << "接收:" << buffer << endl; buffer="ok"; if (tcpserver.send(buffer)==false) // 向对端发送报文。 { perror("send"); break; } cout << "发送:" << buffer << endl; } }
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
这些头文件提供了输入输出、字符串操作、标准库函数、网络编程相关函数等功能。
后面文章会将代码中出现的函数与头文件进行对应。
class ctcpclient { private: int m_clientfd; // 客户端的socket,-1表示未连接或连接已断开;>=0表示有效的socket。 string m_ip; // 服务端的IP/域名。 unsigned short m_port; // 通讯端口。 public: ctcpclient(): m_clientfd(-1) {} bool connect(const string &in_ip, const unsigned short in_port); bool send(const string &buffer); bool recv(string &buffer, const size_t maxlen); bool close(); ~ctcpclient() { close(); } };
我们可以先看一下类中都有什么:
ctcpclient()
: 默认构造函数,将 m_clientfd 初始化为 -1,表示未连接状态。
~ctcpclient()
: 析构函数,确保对象销毁时关闭连接。
bool connect(const string &in_ip, const unsigned short in_port)
:
in_ip
) 和通讯端口号 (in_port
)。true
,失败时返回 false
。bool send(const string &buffer)
:
buffer
)。true
,失败时返回 false
。bool recv(string &buffer, const size_t maxlen)
:
buffer
) 和本次接收数据的最大长度 (maxlen
)。true
,失败时返回 false
。bool close()
:(放在析构函数里)
true
,失败时返回 false
。ctcpclient
类封装了一个 TCP 客户端的基本操作,包括:
connect
方法连接到指定 IP 和端口的服务器。send
方法发送数据到服务器。recv
方法从服务器接收数据。close
方法断开连接。这些功能被封装在一个类中,使得使用该类进行 TCP 通信变得简单和直观,用户无需直接操作底层 socket API,只需调用类的方法即可完成 TCP 通信的基本操作。
connect 函数向服务端发起连接请求,成功返回true,失败返回false。
bool connect(const string &in_ip, const unsigned short in_port) { // 如果socket已连接,直接返回失败。 if (m_clientfd != -1) return false; // 把服务端的IP和端口保存到成员变量中。 m_ip = in_ip; m_port = in_port; // 第1步:创建客户端的socket。 if ((m_clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false; // 第2步:向服务器发起连接请求。 struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET。 servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口。 // 把域名/主机名/字符串格式的IP转换成结构体。 struct hostent* h; if ((h = gethostbyname(m_ip.c_str())) == nullptr) { // 如果获取服务端IP地址失败,关闭socket并返回false。 ::close(m_clientfd); m_clientfd = -1; return false; } // ③指定服务端的IP(大端序)。 memcpy(&servaddr.sin_addr, h->h_addr, h->h_length); // 向服务端发起连接请求。 if (::connect(m_clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { // 如果连接失败,关闭socket并返回false。 ::close(m_clientfd); m_clientfd = -1; return false; } // 连接成功,返回true。 return true; }
if (m_clientfd != -1) return false;
m_ip = in_ip;
m_port = in_port;
if ((m_clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false;
问题一:socket(AF_INET, SOCK_STREAM, 0) == -1
的含义:
问题二:(m_clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1
含义:
使用 socket 函数创建一个 TCP socket,如果失败,返回 false。
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(m_port);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
&servaddr
: 指向需要初始化的内存区域的指针。0
: 用于填充内存区域的值,表示用0填充内存块。sizeof(servaddr)
: 内存区域的大小(以字节为单位),这里是 servaddr 结构体的大小。补充:memset
函数是一个标准的C库函数,用于将指定的值填充到一块内存区域。它通常用于初始化数组或结构体,使其所有字节都被设置为相同的值。这个函数在 <cstring>
(C++)或 <string.h>
(C) 头文件中定义。
// 函数原型
void *memset(void *s, int c, size_t n);
使用 memset
函数的原因:
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(m_port);
htons
函数用于将主机字节序转换为网络字节序(大端序)。m_port
是本地变量,表示目标服务器的端口号。补充:
htons
函数是一个标准的网络字节序转换函数,用于将主机字节序的16位无符号短整数(通常是端口号)转换为网络字节序。
函数原型和相关头文件:
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
这段代码的作用是初始化一个 sockaddr_in 结构体,用于存储服务器的地址信息(包括协议族、端口号和IP地址),以便后续进行网络通信。
struct hostent* h;
if ((h = gethostbyname(m_ip.c_str())) == nullptr)
{
::close(m_clientfd);
m_clientfd = -1;
return false;
}
memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
gethostbyname
函数用于将主机名或IP地址字符串转换为 hostent
结构体。m_ip.c_str()
将 m_ip
(一个 std::string 类型)转换为C风格字符串。gethostbyname
返回 nullptr,表示解析失败,此时关闭 m_clientfd
套接字,并返回 false关于memcpy
函数
memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
使用 gethostbyname 将域名或主机名转换为 IP 地址。如果失败,关闭 socket 并返回 false。
这段代码的作用是将主机名或IP地址字符串转换为网络字节序的IP地址,并将其存储在 sockaddr_in 结构体的 sin_addr 成员中,用于后续的网络连接操作。
这段代码用于将客户端的套接字 m_clientfd 连接到指定的服务器地址 servaddr。如果连接失败,将关闭套接字并返回 false
if (::connect(m_clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
::close(m_clientfd);
m_clientfd = -1;
return false;
}
首先:发起连接请求
if (::connect(m_clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
解释:
若连接失败,连接失败处理:
{
::close(m_clientfd);
m_clientfd = -1;
return false;
}
使用 connect 函数向服务端发起连接请求。如果连接失败,关闭 socket 并返回 false。
return true;
bool send(const string &buffer) // buffer不要用const char *
{
if (m_clientfd==-1) return false; // 检查连接状态:若socket的状态是未连接,返回失败
if ((::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
return true;
}
if ((::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
这个函数的功能是:如何从服务端接收数据并将其存储到 std::string 类型的变量 buffer 中。它封装在 recv 方法中,并对接收的数据进行处理。
bool recv(string &buffer, const size_t maxlen)
{
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为maxlen。
int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0); // 直接操作buffer的内存。
if (readn <= 0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置buffer的实际大小。
return true;
}
接收数据:
int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0);
m_clientfd
接收数据。&buffer[0]
获取 buffer
的底层数据指针,可以直接操作 std::string
的内存。buffer.size()
返回当前 buffer
的大小,即 maxlen
。检查接受结果:
if (readn <= 0) { buffer.clear(); return false; }
如果 recv 返回值小于或等于 0,表示接收失败或连接已关闭,清空 buffer 并返回 false。
bool close()
{
if (m_clientfd == -1) return false; // 如果socket的状态是未连接,直接返回失败。
::close(m_clientfd);
m_clientfd = -1;
return true;
}
int main(int argc, char *argv[]) { //检查命令行的参数是否正确 if (argc != 3) { cout << "Using:./demo7 服务端的IP 服务端的端口\nExample:./demo7 192.168.101.138 5005\n\n"; return -1; } ctcpclient tcpclient; if (tcpclient.connect(argv[1], atoi(argv[2])) == false) // 向服务端发起连接请求。 { perror("connect()"); return -1; } string buffer; for (int ii = 0; ii < 10; ii++) // 循环10次,与服务端进行通信。 { buffer = "这是第" + to_string(ii + 1) + "个超级女生,编号" + to_string(ii + 1) + "。"; if (tcpclient.send(buffer) == false) { perror("send"); break; } cout << "发送:" << buffer << endl; if (tcpclient.recv(buffer, 1024) == false) { perror("recv()"); break; } cout << "接收:" << buffer << endl; sleep(1); } }
if (argc != 3)
{
cout << "Using:./demo7 服务端的IP 服务端的端口\nExample:./demo7 192.168.101.138 5005\n\n";
return -1;
}
ctcpclient tcpclient;
if (tcpclient.connect(argv[1], atoi(argv[2])) == false)
{
perror("connect()"); return -1;
}
for (int ii = 0; ii < 10; ii++) { buffer = "这是第" + to_string(ii + 1) + "个超级女生,编号" + to_string(ii + 1) + "。"; if (tcpclient.send(buffer) == false) { perror("send"); break; } cout << "发送:" << buffer << endl; if (tcpclient.recv(buffer, 1024) == false) { perror("recv()"); break; } cout << "接收:" << buffer << endl; sleep(1); }
服务端部分代码解析以后再写,很多地方和客户端是一样的
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。