【Linux】Socket网络套接字


网络套接字

1. 网络编程概念

1.1 IP地址和端口号

在网络通信中,数据是由进程产生和接收的。IP地址唯一标识了互联网中的一台主机,而端口号则唯一标识了主机上的一个网络进程。因此,网络通信的本质是跨网络的两台主机之间的进程间通信。

  • IP地址:用于标识互联网中的主机。
  • 端口号:用于标识主机上的网络进程。

源端口号用于确定源主机上的网络进程,目的端口号用于确定目的主机上的网络进程。通过IP地址和端口号,我们可以唯一标识互联网上的一个进程。

概念作用
IPIP地址唯一的标识了互联网中的一台主机。
端口端口号唯一地标识主机上的一个网络进程。

源端口号确定源主机上的网络进程,目的端口号确定目的主机上的网络进程。

IP和端口就能标识互联网上的唯一一台机器上的唯一一个进程。

可以把整个网络看作是一个大操作系统,所有的网络行为就可看作是这个系统内的进程间通信。

进程具有独立性,进程间通信的前提是先让不同的进程看到同一份资源,而网络通信的临界资源就是网络。

1.2 理解网络字节序

当跨主机传输数据时,必须考虑且强制规定字节序。规定:网络中的数据一律都采用大端的形式

一般从低到高地将数据发出,所以接收时也是先收到低地址数据,便于存储。

所以系统提供了库函数,可将数据进行主机和网络字节序的转化,如下:

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);   // 主机 转 网络 长整型
uint16_t htons(uint16_t hostshort);  // 主机 转 网络 短整型
uint32_t ntohl(uint32_t netlong);    // 网络 转 主机 长整型
uint16_t ntohs(uint16_t netshort);   // 网络 转 主机 短整型

1.3 sockaddr结构体

通信方式有很多种,比如TCP/IP属于是AF_INET,域间套接字属于AF_UNIX。各家协议都有自己的套接字结构体。

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

接口设计者提供一个通用结构体叫sockaddr。各种结构体可以强转成sockaddr,接口内部可以通过前16位数据判断具体协议类型。

#include <netinet/in.h>
#include <apra/inet.h>

#define	__SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family // sa_family_t(unsigned short) sin_family

typedef uint16_t in_port_t;   // 端口类型
typedef uint_32_t in_addr_t;  // IP 类型
struct in_addr
  {
    in_addr_t s_addr;    
  };

/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);   /* Protocol Family.  */
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;	/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

 

2. 网络编程接口

2.1 通用接口

socket()

socket()函数用于创建一个套接字,它本质上是打开一个文件,与网络无关。

#include <sys/types.h>
#include <sys/socket.h> 
int socket(int domain, int type, int protocol);
  • domain:指定通信协议族,如AF_INET(IPv4)。
  • type:指定通信类型,如SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)。
  • protocol:指定具体的协议,通常为0。
bind()

bind()函数用于将套接字绑定到一个特定的IP地址和端口上。

int bind(int socket, const struct sockaddr *address, socklen_t address_len);
  • socket:套接字文件描述符。CP
  • address:包含IP地址和端口的套接字信息结构体。
  • address_len:结构体长度。
接口解释
socket()创建套接字本质就是打开文件,与网络无关。
bind()本质是将IP端口和套接字文件关联。

2.2 UDP接口

recvfrom()

recvfrom()函数用于接收UDP数据包,并获取发送方的地址信息。

ssize_t recvfrom(int socket, void *buffer, size_t length, int flags, struct sockaddr *address, socklen_t *address_len);
  • socket:套接字文件描述符。
  • buffer:接收数据的缓冲区。
  • length:缓冲区长度。
  • flags:接收标志,通常为0。
  • address:用于存储发送方地址的结构体。
  • address_len:地址结构体长度。
sendto()

sendto()函数用于发送UDP数据包到指定的地址。

ssize_t sendto(int socket, const void *message, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t dest_len);
  • socket:套接字文件描述符。
  • message:要发送的数据。
  • length:数据长度。
  • flags:发送标志,通常为0。
  • dest_addr:目标地址结构体。
  • dest_len:目标地址结构体长度。

2.3 TCP接口

listen()

listen()函数用于将套接字设置为监听状态,准备接受连接请求。

int listen(int socket, int backlog);
  • socket:套接字文件描述符。
  • backlog:等待连接队列的最大长度。

TCP是面向连接的通信协议,所以在通信前需先建立连接。

listen的本质是设置套接字为listen监听状态,允许用户进行连接。

accept()

accept()函数用于接受客户端的连接请求,并创建一个新的套接字用于通信。

int accept(int socket, struct sockaddr *address, socklen_t *address_len);
  • socket:监听套接字文件描述符。
  • address:用于存储客户端地址的结构体。
  • address_len:地址结构体长度。

**accept表示正式建立与客户端的连接。**本质阻塞式的等待三次握手完成,并将连接提取到应用层。

socket返回的sock_fd用来监听连接。accept返回的sock_fd是用来通信的fd。

connect()

connect()函数用于客户端向服务器发起连接请求。

int connect(int socket, const struct sockaddr *address, socklen_t address_len);
  • socket:套接字文件描述符。
  • address:服务器地址结构体。
  • address_len:地址结构体长度。

**connect的作用是主动向服务端发起连接。**本质是向服务器端发起TCP的三次握手,并等待握手完成。

recv()/send()

recv()send()函数用于TCP套接字的数据接收和发送。

ssize_t recv(int socket, void *buffer, size_t length, int flags); 
ssize_t send(int socket, const void *buffer, size_t length, int flags);
  • socket:套接字文件描述符。
  • buffer:接收或发送数据的缓冲区。
  • length:数据长度。
  • flags:接收或发送标志,通常为0。

网络数据收发接口,本质是读写文件。

TCP协议是流式套接字,TCP收发接口和文件读写非常像,仅多了方式参数flags

2.4 IP格式转换接口

// 字符串IP 转 网络整数IP
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
int inet_pton(int af, const char *src, void *dst);

// 网络整数IP 转 字符串IP
char* inet_ntoa(struct in_addr in); // 非线程安全
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

2.5 套接字接口封装

namespace inet {
// 定义网络编程API的结构体
struct api {
   // 定义网络协议类型枚举
   enum {
       udp = SOCK_DGRAM,  // UDP协议
       tcp = SOCK_STREAM  // TCP协议
   };

   // 创建套接字函数
   static int Socket(int proto) {
       int fd = socket(AF_INET, proto, 0);  // 创建套接字
       if (fd < 0) throw std::runtime_error("socket failed");  // 如果创建失败,抛出异常
       return fd;  // 返回套接字描述符
   }

   // 绑定套接字到本地地址函数
   static void Bind(int sock, const std::string& ip, uint16_t port) {
       struct sockaddr_in local;  // 定义本地地址结构体
       memset(&local, 0, sizeof(local));  // 清空结构体

       local.sin_family = AF_INET;  // 设置地址族为IPv4
       local.sin_addr.s_addr = inet_addr(ip.c_str());  // 设置IP地址
       local.sin_port = htons(port);  // 设置端口号

       if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
           throw std::runtime_error("bind error");  // 如果绑定失败,抛出异常
   }

   // 绑定套接字到任意本地地址函数
   static void Bind(int sock, uint16_t port) {
       struct sockaddr_in local;  // 定义本地地址结构体
       memset(&local, 0, sizeof(local));  // 清空结构体
       local.sin_family = AF_INET;  // 设置地址族为IPv4
       local.sin_addr.s_addr = INADDR_ANY;  // 设置IP地址为任意本地地址
       local.sin_port = htons(port);  // 设置端口号
       if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
           throw std::runtime_error("bind error");  // 如果绑定失败,抛出异常
   }

   // 监听套接字函数
   static void Listen(int sock, int backlog) {
       if (listen(sock, backlog) < 0)
           throw std::runtime_error("listen error");  // 如果监听失败,抛出异常
   }

   // 连接套接字到远程地址函数
   static void Connect(int sock, const std::string& ip, uint16_t port, int trytime = 1) {
       struct sockaddr_in peer;  // 定义远程地址结构体
       memset(&peer, 0, sizeof(peer));  // 清空结构体
       peer.sin_family = AF_INET;  // 设置地址族为IPv4
       peer.sin_addr.s_addr = inet_addr(ip.c_str());  // 设置IP地址
       peer.sin_port = htons(port);  // 设置端口号
       while (trytime-- > 0 && connect(sock, (struct sockaddr*)&peer, sizeof(peer)) < 0) {
           if (trytime == 0) throw std::runtime_error("connect failed");  // 如果连接失败,抛出异常
           sleep(1);  // 等待1秒后重试
       }
   }

   // 接受连接函数
   static int Accept(int sock, std::string* ip = nullptr, uint16_t* port = nullptr) {
       struct sockaddr_in peer;  // 定义远程地址结构体
       socklen_t len = sizeof(peer);  // 初始化地址长度
       memset(&peer, 0, len);  // 清空结构体
       int fd = accept(sock, (struct sockaddr*)&peer, &len);  // 接受连接
       if (ip)   *ip = inet_ntoa(peer.sin_addr);  // 如果指定了IP地址,则获取IP地址
       if (port) *port = ntohs(peer.sin_port);  // 如果指定了端口号,则获取端口号
       return fd;  // 返回新的套接字描述符
   }

   // 接收数据函数
   static int Recv(int sock, std::string* msg, size_t len) {
       msg->clear();  // 清空消息字符串
       std::unique_ptr<char[]> buf(new char[len]{0});  // 创建缓冲区
       ssize_t s = recv(sock, buf.get(), len, 0);  // 接收数据
       if (s > 0) {
           buf[s] = 0;  // 在数据末尾添加字符串结束符
           *msg = buf.get();  // 将接收到的数据复制到消息字符串
       }
       return s;  // 返回接收到的数据长度
   }

   // 发送数据函数
   static int Send(int sock, const std::string& msg) {
       return send(sock, msg.c_str(), msg.size(), 0);  // 发送数据
   }

   // 接收数据并获取发送方地址函数
   static int Recvfrom(int sock, std::string* msg, size_t len, 
                       std::string* ip = nullptr, uint16_t* port = nullptr) {
       msg->clear();  // 清空消息字符串
       std::unique_ptr<char[]> buf(new char[len]{0});  // 创建缓冲区
       struct sockaddr_in peer;  // 定义远程地址结构体
       socklen_t sklen = sizeof(peer);  // 初始化地址长度
       memset(&peer, 0, sklen);  // 清空结构体
       ssize_t s = recvfrom(sock, buf.get(), len, 0, (struct sockaddr*)&peer, &sklen);  // 接收数据
       if (s > 0) {
           buf[s] = 0;  // 在数据末尾添加字符串结束符
           *msg = buf.get();  // 将接收到的数据复制到消息字符串
       }
       if (ip)   *ip = inet_ntoa(peer.sin_addr);  // 如果指定了IP地址,则获取IP地址
       if (port) *port = ntohs(peer.sin_port);  // 如果指定了端口号,则获取端口号
       return s;  // 返回接收到的数据长度
   }

   // 发送数据到指定地址函数
   static int Sendto(int sock, const std::string& msg, 
                     const std::string& ip, uint16_t port) {
       struct sockaddr_in peer;  // 定义远程地址结构体
       memset(&peer, 0, sizeof(peer));  // 清空结构体
       peer.sin_family = AF_INET;  // 设置地址族为IPv4
       peer.sin_addr.s_addr = inet_addr(ip.c_str());  // 设置IP地址
       peer.sin_port = htons(port);  // 设置端口号
       ssize_t s = 
           sendto(sock, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer));  // 发送数据
       return s;  // 返回发送的数据长度
   }
};

// TCP协议相关的命名空间
namespace tcp {

// TCP服务器类
class server {
public:
   // 构造函数,指定端口和监听队列长度
   server(uint16_t port, int backlog = 12) : _sock(0), _port(port), _backlog(backlog) {
       init();  // 初始化服务器
   }

   // 构造函数,指定IP地址、端口和监听队列长度
   server(const std::string& ip, uint16_t port, int backlog = 12)
       : _sock(0), _ip(ip), _port(port), _backlog(backlog) {
       init();  // 初始化服务器
   }

   // 接受客户端连接函数
   int accept(std::string* cip = nullptr, uint16_t* cport = nullptr) {
       return inet::api::Accept(_sock, cip, cport);  // 接受连接
   }

   // 接收数据函数
   int recv(int sock, std::string* msg, size_t len) { 
       return inet::api::Recv(sock, msg, len);  // 接收数据
   }

   // 发送数据函数
   int send(int sock, const std::string& msg) { 
       return inet::api::Send(sock, msg);  // 发送数据
   }

   // 析构函数,关闭套接字
   ~server() { close(_sock); }

private:
   // 初始化服务器函数
   void init() {
       _sock = inet::api::Socket(inet::api::tcp);  // 创建TCP套接字
       if (_ip.empty()) inet::api::Bind(_sock, _port);  // 如果未指定IP地址,则绑定到任意本地地址
       else inet::api::Bind(_sock, _ip, _port);  // 否则绑定到指定IP地址和端口
       inet::api::Listen(_sock, _backlog);  // 开始监听
   }

protected:
   int _sock;  // 套接字描述符
   std::string _ip;  // IP地址
   uint16_t _port;  // 端口号
   int _backlog;  // 监听队列长度
};

// TCP客户端类
class client
{
public:
   // 构造函数,指定服务器IP地址、端口和重试次数
   client(const std::string& svr_ip, uint16_t svr_port, int trytime = 1)
       : _sock(0), _sip(svr_ip), _sport(svr_port), _trytime(trytime) {
       _sock = inet::api::Socket(inet::api::tcp);  // 创建TCP套接字
       inet::api::Connect(_sock, _sip, _sport, _trytime);  // 连接服务器
   }

   // 发送数据函数
   int send(const std::string& msg) { return inet::api::Send(_sock, msg); }
   int send(int sock, const std::string& msg) { return inet::api::Send(sock, msg); }

   // 接收数据函数
   int recv(std::string* msg, size_t len) { return inet::api::Recv(_sock, msg, len); }
   int recv(int sock, std::string* msg, size_t len) 
   { return inet::api::Recv(sock, msg, len); }

   // 析构函数,关闭套接字
   ~client() { close(_sock); }

protected:
   int _sock;  // 套接字描述符
   std::string _sip;  // 服务器IP地址
   uint16_t _sport;  // 服务器端口号
   int _trytime;  // 重试次数
};
}

// UDP协议相关的命名空间
namespace udp {

// UDP服务器类
class server {
public:
   // 构造函数,指定端口
   server(uint16_t port) : _sock(0), _port(port) {
       init();  // 初始化服务器
   }

   // 构造函数,指定IP地址和端口
   server(const std::string& ip, uint16_t port) : _sock(0), _ip(ip), _port(port) {
       init();  // 初始化服务器
   }

   // 发送数据到指定地址函数
   int sendto(const std::string& msg, const std::string& cip, uint16_t cport) {
       return inet::api::Sendto(_sock, msg, cip, cport);  // 发送数据
   }

   // 接收数据并获取发送方地址函数
   int recvfrom(std::string* msg, size_t len, 
                std::string* cip = nullptr, uint16_t* cport = nullptr) {
       return inet::api::Recvfrom(_sock, msg, len, cip, cport);  // 接收数据
   }

   // 析构函数,关闭套接字
   ~server() { close(_sock); }

private:
   // 初始化服务器函数
   void init() {
       _sock = inet::api::Socket(inet::api::udp);  // 创建UDP套接字
       if (_ip.empty()) inet::api::Bind(_sock, _port);  // 如果未指定IP地址,则绑定到任意本地地址
       else inet::api::Bind(_sock, _ip, _port);  // 否则绑定到指定IP地址和端口
   }

protected:
   int _sock;  // 套接字描述符
   std::string _ip;  // IP地址
   uint16_t _port;  // 端口号
};

// UDP客户端类
class client {
public:
   // 构造函数,指定服务器IP地址和端口
   client(const std::string& svr_ip, uint16_t svr_port)
       : _sock(0), _sip(svr_ip), _sport(svr_port) {
       _sock = inet::api::Socket(inet::api::udp);  // 创建UDP套接字
   }

   // 发送数据到服务器函数
   int sendto(const std::string& msg) 
   { return inet::api::Sendto(_sock, msg, _sip, _sport); }

   // 接收数据函数
   int recvfrom(std::string* msg, size_t len) 
   { return inet::api::Recvfrom(_sock, msg, len); }

   // 析构函数,关闭套接字
   ~client() { close(_sock); }

protected:
   int _sock;  // 套接字描述符
   std::string _sip;  // 服务器IP地址
   uint16_t _sport;  // 服务器端口号
};
}
}

 

3. 网络通信设计

3.1 UDP通信

UDP客户端

UDP客户端的设计采用了多线程的方式,一个线程负责发送消息,另一个线程负责接收消息。这种设计允许客户端在发送消息的同时,能够实时接收服务器的响应,从而实现双向通信。

class udp_client
{
public:
   // 构造函数,接收服务器IP地址和端口号
   udp_client(std::string sip, uint16_t sport) : _sock(0), _sip(sip), _sport(sport)
   {}

   // 析构函数,关闭套接字并等待发送和接收线程结束
   ~udp_client() {
       close(_sock);
       _sender.join();
       _recver.join();
   }

   // 初始化函数,创建UDP套接字,并启动发送和接收线程
   void init() {
       _sock = socket(AF_INET, SOCK_DGRAM, 0);
       if (_sock < 0) exit(SOCKET_ERR);
       _sender = std::thread(&udp_client::send, this);
       _recver = std::thread(&udp_client::recv, this);
   }

   // 发送消息函数,运行在独立的线程中
   void send()
   {
       struct sockaddr_in peer; // 定义服务器地址结构体
       peer.sin_family = AF_INET; // 设置地址族为IPv4
       peer.sin_addr.s_addr = inet_addr(_sip.c_str()); // 设置服务器IP地址
       peer.sin_port = htons(_sport); // 设置服务器端口号

       // 无限循环,等待用户输入消息并发送到服务器
       while (true)
       {
           std::string msg;
           std::cout << "please input:> ";
           getline(std::cin, msg);
           sendto(_sock, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
       }
   }

   // 接收消息函数,运行在独立的线程中
   void recv()
   {
       while (true)
       {
           char buf[1024] = {0}; // 接收缓冲区
           struct sockaddr_in tmp; // 临时地址结构体,用于接收发送方的地址信息
           socklen_t len = sizeof(tmp); // 地址结构体长度
           ssize_t s = recvfrom(_sock, buf, sizeof(buf), 0, (struct sockaddr*)&tmp, &len);
           if (s > 0) buf[s] = 0; // 如果接收到数据,在数据末尾添加字符串结束符
           else continue; // 如果接收失败,继续下一次接收
           std::cout << "server return# " << buf << std::endl; // 打印接收到的消息
       }
   }

private:
   int _sock; // UDP套接字描述符
   std::string _sip; // 服务器IP地址
   uint16_t _sport; // 服务器端口号

   std::thread _sender; // 发送消息的线程
   std::thread _recver; // 接收消息的线程
};
UDP服务端

UDP服务端的设计相对简单,它只需要创建一个套接字,绑定到指定的端口,然后无限循环接收客户端的消息并回复。

class udp_server
{
public:
   // 构造函数,接收端口号
   udp_server(uint16_t port) : _sock(0), _port(port)
   {}

   // 析构函数,关闭套接字
   ~udp_server()
   {
       close(_sock);
   }

   // 初始化函数,创建UDP套接字并绑定到指定端口
   void init()
   {
       _sock = socket(AF_INET, SOCK_DGRAM, 0);
       if (_sock < 0) exit(SOCKET_ERR);

       struct sockaddr_in local; // 定义本地地址结构体
       local.sin_family = AF_INET; // 设置地址族为IPv4
       local.sin_addr.s_addr = INADDR_ANY; // 设置IP地址为任意本地地址
       local.sin_port = htons(_port); // 设置端口号

       if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
           exit(BIND_ERR); // 如果绑定失败,退出程序
   }

   // 启动服务器函数,用于接收客户端消息并回复
   void start()
   {
       char buf[1024] = {0}; // 接收缓冲区
       while (true)
       {
           struct sockaddr_in peer; // 定义客户端地址结构体
           socklen_t len = sizeof(peer); // 地址结构体长度

           ssize_t s = recvfrom(_sock, buf, sizeof(buf), 0, (struct sockaddr*)&peer, &len);
           if (s > 0) buf[s] = 0; // 如果接收到数据,在数据末尾添加字符串结束符
           else continue; // 如果接收失败,继续下一次接收

           std::string cip = inet_ntoa(peer.sin_addr); // 获取客户端IP地址
           uint16_t cport = ntohs(peer.sin_port); // 获取客户端端口号
           std::cout << '[' << cip << ':' << cport << "] " << buf << std::endl; // 打印接收到的消息

           std::string rsp = buf; // 回复消息与接收到的消息相同
           sendto(_sock, rsp.c_str(), rsp.size(), 0, (struct sockaddr*)&peer, sizeof(peer)); // 发送回复消息
       }
   }

private:
   int _sock; // UDP套接字描述符
   uint16_t _port; // 端口号
};

3.2 TCP通信

TCP客户端

TCP客户端的设计包括创建套接字、连接服务器、发送和接收消息。

class tcp_client
{
public:
   // 构造函数,接收服务器IP地址和端口号
   tcp_client(std::string sip, uint16_t sport) : _sock(0), _sip(sip), _sport(sport)
   {}

   // 析构函数,关闭套接字
   ~tcp_client()
   {
       close(_sock);
   }

   // 初始化函数,创建TCP套接字并尝试连接到服务器
   void init()
   {
       // 1. 创建套接字
       _sock = socket(AF_INET, SOCK_STREAM, 0);
       if (_sock < 0)
       {
           std::cerr << "socket error" << std::endl;
           exit(SOCKET_ERR);
       }
       std::cout << "socket success " << _sock << std::endl;

       // 2. 连接服务器
       struct sockaddr_in peer; // 定义服务器地址结构体
       memset(&peer, 0, sizeof(peer)); // 清空结构体
       peer.sin_family = AF_INET; // 设置地址族为IPv4
       peer.sin_port = htons(_sport); // 设置服务器端口号
       inet_aton(_sip.c_str(), &peer.sin_addr); // 设置服务器IP地址

       int cnt = 5; // 设置重试次数
       while (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) < 0)
       {
           std::cerr << "connect failed, remaining try " << cnt-- << std::endl;
           if (cnt == 0)
           {
               std::cerr << "connect error" << std::endl;
               exit(CONNECT_ERR);
           }
           sleep(1); // 等待1秒后重试
       }
       std::cout << "connect success" << std::endl;
   }

   // 启动客户端函数,用于发送消息给服务器并接收回复
   void start()
   {
       while (true)
       {
           std::string msg;
           std::cout << "please input:> ";
           getline(std::cin, msg); // 读取用户输入

           send(_sock, msg.c_str(), msg.size(), 0); // 发送消息给服务器

           char buf[1024] = {0}; // 接收缓冲区
           ssize_t s = recv(_sock, buf, sizeof(buf), 0); // 接收服务器回复
           if (s > 0)
           {
               buf[s] = 0; // 在数据末尾添加字符串结束符
               std::cout << "server return " << buf << std::endl; // 打印服务器回复
           }
           else if (s == 0)
           {
               close(_sock); // 如果服务器关闭连接,关闭套接字
               std::cout << "server quit" << std::endl;
               break; // 退出循环
           }
           else
           {
               close(_sock); // 如果接收失败,关闭套接字
               std::cerr << "recv error " << strerror(errno) << std::endl; // 打印错误信息
               break; // 退出循环
           }
       }
   }

private:
   int _sock; // TCP套接字描述符
   std::string _sip; // 服务器IP地址
   uint16_t _sport; // 服务器端口号
};
TCP服务端

TCP服务端的设计需要处理监听、接受连接、发送和接收消息。

class tcp_client
{
public:
    // 构造函数,接收服务器的IP地址和端口号作为参数
    tcp_client(std::string sip, uint16_t sport) : _sock(0), _sip(sip), _sport(sport)
    {}

    // 析构函数,在对象销毁时关闭套接字
    ~tcp_client()
    {
        close(_sock);
    }

    // 初始化函数,用于创建TCP套接字并尝试连接到服务器
    void init()
    {
        // 1. 创建TCP套接字
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            // 如果套接字创建失败,输出错误信息并退出程序
            std::cerr << "socket error" << std::endl;
            exit(SOCKET_ERR);
        }
        std::cout << "socket success " << _sock << std::endl;

        // 2. 连接服务器
        struct sockaddr_in peer; // 定义服务器地址结构体
        memset(&peer, 0, sizeof(peer)); // 清空结构体
        peer.sin_family = AF_INET; // 设置地址族为IPv4
        peer.sin_port = htons(_sport); // 设置服务器端口号
        inet_aton(_sip.c_str(), &peer.sin_addr); // 设置服务器IP地址

        int cnt = 5; // 设置重试次数
        while (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) < 0)
        {
            // 如果连接失败,输出剩余尝试次数并重试
            std::cerr << "connect failed, remaining try " << cnt-- << std::endl;
            if (cnt == 0)
            {
                // 如果尝试次数用完,输出错误信息并退出程序
                std::cerr << "connect error" << std::endl;
                exit(CONNECT_ERR);
            }
            sleep(1); // 等待1秒后重试
        }
        std::cout << "connect success" << std::endl;
    }

    // 启动客户端函数,用于发送消息给服务器并接收回复
    void start()
    {
        while (true)
        {
            std::string msg;
            std::cout << "please input:> ";
            getline(std::cin, msg); // 读取用户输入

            send(_sock, msg.c_str(), msg.size(), 0); // 发送消息给服务器

            char buf[1024] = {0}; // 接收缓冲区
            ssize_t s = recv(_sock, buf, sizeof(buf), 0); // 接收服务器回复
            if (s > 0)
            {
                buf[s] = 0; // 在数据末尾添加字符串结束符
                std::cout << "server return " << buf << std::endl; // 打印服务器回复
            }
            else if (s == 0)
            {
                close(_sock); // 如果服务器关闭连接,关闭套接字
                std::cout << "server quit" << std::endl;
                break; // 退出循环
            }
            else
            {
                close(_sock); // 如果接收失败,关闭套接字
                std::cerr << "recv error " << strerror(errno) << std::endl; // 打印错误信息
                break; // 退出循环
            }
        }
    }

private:
    int _sock; // TCP套接字描述符
    std::string _sip; // 服务器IP地址
    uint16_t _sport; // 服务器端口号
};

网络套接字编程的本质是利用系统调用从零编写应用层,不是使用应用层。

  • 11
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SuhyOvO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值