当前位置:   article > 正文

TCP——如何用类封装 socket 实现客户端和服务端通信(代码逐行详解)

TCP——如何用类封装 socket 实现客户端和服务端通信(代码逐行详解)

如何封装一个简单的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);
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128

服务器端

/*
 * 程序名: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;
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162

二、 代码解析——客户端

1. 头文件和命名空间

#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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这些头文件提供了输入输出、字符串操作、标准库函数、网络编程相关函数等功能。

后面文章会将代码中出现的函数与头文件进行对应。

2. ctcpclient 类定义

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(); }
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

我们可以先看一下类中都有什么:

  • private:放置了三个成员变量
  • public:放了成员函数,析构函数、构造函数

构造函数

ctcpclient(): 默认构造函数,将 m_clientfd 初始化为 -1,表示未连接状态。

析构函数

~ctcpclient(): 析构函数,确保对象销毁时关闭连接。

成员函数

  • bool connect(const string &in_ip, const unsigned short in_port):

    • 用于向服务端发起连接请求。
    • 接受两个参数:服务端的 IP 地址或域名 (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 客户端的基本操作,包括:

  1. 连接到服务器:通过 connect 方法连接到指定 IP 和端口的服务器。
  2. 发送数据:通过 send 方法发送数据到服务器。
  3. 接收数据:通过 recv 方法从服务器接收数据。
  4. 关闭连接:通过 close 方法断开连接。

这些功能被封装在一个类中,使得使用该类进行 TCP 通信变得简单和直观,用户无需直接操作底层 socket API,只需调用类的方法即可完成 TCP 通信的基本操作。

3. 成员函数解析

3.1 connect 函数

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
逐行解析——第一步:创建客户端的socket
  1. 检查 socket 是否已连接
if (m_clientfd != -1) return false;
  • 1
  1. 保存服务端的 IP 和端口
m_ip = in_ip;
m_port = in_port;
  • 1
  • 2
  1. 创建客户端的 socket
if ((m_clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) return false;
  • 1
  • AF_INET:IPv4协议
  • SOCK_STREAM 表示使用TCP协议
  • 0 表示默认使用TCP协议。

问题一:socket(AF_INET, SOCK_STREAM, 0) == -1 的含义:

  • 1)socket(AF_INET, SOCK_STREAM, 0) 创建一个IPv4协议的TCP套接字。
  • 2)如果创建成功,socket 函数返回一个文件描述符(非负整数),用于标识这个套接字。
  • 3)如果创建失败,socket 函数返回 -1,并设置 errno 变量以指示错误原因。

问题二:(m_clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 含义:

  • 1)调用 socket 函数并将返回值赋给 m_clientfd。
  • 2)检查返回值是否为 -1,如果是,则表示创建套接字失败。
  • 3)返回 false 表示连接失败。

使用 socket 函数创建一个 TCP socket,如果失败,返回 false。

  1. 初始化 sockaddr_in 结构体
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(m_port);
  • 1
  • 2
  • 3
  • 4
逐行解析——第2步:向服务器发起连接请求
struct sockaddr_in servaddr;
  • 1
  • 定义一个 sockaddr_in 结构体变量 servaddr。
  • sockaddr_in 是一个专门用于IPv4地址的结构体,定义在 <netinet/in.h> 头文件中。它通常用于存储IP地址和端口信息。
memset(&servaddr, 0, sizeof(servaddr));
  • 1
  • 使用 memset 函数将 servaddr 结构体的所有字节初始化为 0。
  • memset 的三个参数分别是:
    1. &servaddr: 指向需要初始化的内存区域的指针。
    1. 0: 用于填充内存区域的值,表示用0填充内存块。
    1. sizeof(servaddr): 内存区域的大小(以字节为单位),这里是 servaddr 结构体的大小。
  • 这一步确保结构体的所有成员变量都被初始化为 0,避免出现未定义的值。

补充:memset函数是一个标准的C库函数,用于将指定的值填充到一块内存区域。它通常用于初始化数组或结构体,使其所有字节都被设置为相同的值。这个函数在 <cstring>(C++)或 <string.h>(C) 头文件中定义。

// 函数原型
void *memset(void *s, int c, size_t n);
  • 1
  • 2

使用 memset函数的原因:

  • 初始化内存:
    确保结构体或数组的所有字节都被初始化为一个已知的值(通常是 0),避免出现未定义的行为。
  • 清零操作:
    清除内存中的旧数据,使得结构体或数组在使用前处于干净的状态。
  • 统一填充
    可以快速将一块内存区域的所有字节都设置为相同的值,而不需要逐个字节进行赋值。
servaddr.sin_family = AF_INET;
  • 1
  • 设置 servaddr 结构体的 sin_family 成员为 AF_INET。
  • sin_family 表示地址族,这里设置为 AF_INET,表示IPv4协议。
  • AF_INET 是IPv4协议的地址族标识符。
servaddr.sin_port = htons(m_port);
  • 1
  • 设置 servaddr 结构体的 sin_port 成员为 m_port 的网络字节序表示。
  • sin_port 表示端口号。
  • htons函数用于将主机字节序转换为网络字节序(大端序)。
    1.主机字节序是计算机内部存储数据的字节顺序,可能是小端序或大端序。
    2.网络字节序是网络协议规定的字节顺序,统一采用大端序。
    3.m_port 是本地变量,表示目标服务器的端口号。

补充:
htons 函数是一个标准的网络字节序转换函数,用于将主机字节序的16位无符号短整数(通常是端口号)转换为网络字节序。

函数原型和相关头文件:

#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
  • 1
  • 2

这段代码的作用是初始化一个 sockaddr_in 结构体,用于存储服务器的地址信息(包括协议族、端口号和IP地址),以便后续进行网络通信。

逐行解析——把域名/主机名/字符串格式的IP转换成结构体
  1. 解析服务端的 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);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 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);
  • 1
  • memcpy 函数将内存区域 h->h_addr 的内容复制到 servaddr.sin_addr 中。
  • h->h_addr 是一个指向主机IP地址的指针。
  • h->h_length 表示IP地址的长度(通常是4字节,对于IPv4地址)。
  • servaddr.sin_addr 是 sockaddr_in 结构体中的成员,用于存储网络字节序的IP地址。

使用 gethostbyname 将域名或主机名转换为 IP 地址。如果失败,关闭 socket 并返回 false。

这段代码的作用是将主机名或IP地址字符串转换为网络字节序的IP地址,并将其存储在 sockaddr_in 结构体的 sin_addr 成员中,用于后续的网络连接操作。

逐行解析——向服务端发起连接请求
  1. 发起连接请求

这段代码用于将客户端的套接字 m_clientfd 连接到指定的服务器地址 servaddr。如果连接失败,将关闭套接字并返回 false

if (::connect(m_clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)  
{
    ::close(m_clientfd); 
    m_clientfd = -1; 
    return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

首先:发起连接请求

if (::connect(m_clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
  • 1

解释:

  • 将客户端的套接字 m_clientfd 连接到服务器地址 servaddr。
  • connect 函数返回 0 表示成功,返回 -1 表示失败。
  • 如果返回值为 -1,表示连接失败。

若连接失败,连接失败处理:

{
    ::close(m_clientfd); 
    m_clientfd = -1; 
    return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 关闭套接字 m_clientfd,防止资源泄漏。
  • 将 m_clientfd 设置为 -1,表示套接字无效。
    返回 false,通知调用者连接失败。

使用 connect 函数向服务端发起连接请求。如果连接失败,关闭 socket 并返回 false。

  1. 返回连接结果
return true;

  • 1
  • 2

3.2 send 函数

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;
  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 检查是否已连接,如果未连接则返回 false。
  • 使用 send 函数发送数据。
 if ((::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
  • 1
  • 使用 send 系统调用将 buffer 中的数据发送到服务器。
  • buffer.data() 返回指向字符串数据的指针。
  • buffer.size() 返回字符串数据的大小(字节数)。
  • 第四个参数 0 表示没有特殊的发送选项。
  • 如果 send 返回值小于等于 0,表示发送失败,返回 false。

3.3 recv 函数

这个函数的功能是:如何从服务端接收数据并将其存储到 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;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 清空并调整接收缓冲区的大小。
  • 使用 recv 函数接收数据,并根据实际接收到的数据调整缓冲区大小。

接收数据:

int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0);
  • 1
  • 使用 recv 系统调用从套接字m_clientfd接收数据。
  • &buffer[0] 获取 buffer 的底层数据指针,可以直接操作 std::string 的内存。
  • buffer.size() 返回当前 buffer 的大小,即 maxlen
  • 第四个参数 0 表示没有特殊的接收选项。
  • recv 返回实际接收到的字节数

检查接受结果:

if (readn <= 0) { buffer.clear(); return false; }
  • 1
  •  如果 recv 返回值小于或等于 0,表示接收失败或连接已关闭,清空 buffer 并返回 false。
    
    • 1

3.4 close 函数

bool close()
{
    if (m_clientfd == -1) return false; // 如果socket的状态是未连接,直接返回失败。

    ::close(m_clientfd);
    m_clientfd = -1;
    return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 检查是否已连接,如果未连接则返回 false。
  • 关闭socket并重置描述符。

4. main 函数

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);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  1. 检查命令行参数:
if (argc != 3)
{
    cout << "Using:./demo7 服务端的IP 服务端的端口\nExample:./demo7 192.168.101.138 5005\n\n";
    return -1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 程序期望接收两个命令行参数:服务器的IP地址和端口号。
  • 如果参数数量不正确,输出使用说明并返回 -1。
  1. 创建 ctcpclient 对象:
ctcpclient tcpclient;
  • 1
  1. 连接到服务器:
if (tcpclient.connect(argv[1], atoi(argv[2])) == false)
{
    perror("connect()"); return -1;
}
  • 1
  • 2
  • 3
  • 4
  • 使用 connect 方法连接到指定的服务器和端口。
  • argv[1] 是服务器的IP地址,argv[2] 是端口号(转换为整数)。
  • 如果连接失败,输出错误信息并返回 -1。
  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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 循环10次,每次发送一个消息并等待接收服务器的响应。
  • 构造发送的消息字符串 buffer,内容为 “这是第n个超级女生,编号n。”。
  • 使用 send 方法将消息发送到服务器。
  • 如果发送失败,输出错误信息并跳出循环。
  • 打印发送的消息。
  • 使用 recv 方法接收服务器的响应,最大接收长度为1024字节。
  • 如果接收失败,输出错误信息并跳出循环。
  • 打印接收到的消息。
  • 等待1秒后进行下一次循环。

三、代码解析——服务器端

服务端部分代码解析以后再写,很多地方和客户端是一样的

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/638763
推荐阅读
相关标签
  

闽ICP备14008679号