文章目录
- 网络套接字
- 1. 网络编程概念
- 1.1 IP地址和端口号
- 1.2 理解网络字节序
- 1.3 sockaddr结构体
- 2. 网络编程接口
- 2.1 通用接口
- socket()
- bind()
- 2.2 UDP接口
- recvfrom()
- sendto()
- 2.3 TCP接口
- listen()
- accept()
- connect()
- recv()/send()
- 2.4 IP格式转换接口
- 2.5 套接字接口封装
- 3. 网络通信设计
- 3.1 UDP通信
- UDP客户端
- UDP服务端
- 3.2 TCP通信
- TCP客户端
- TCP服务端
网络套接字
1. 网络编程概念
1.1 IP地址和端口号
在网络通信中,数据是由进程产生和接收的。IP地址唯一标识了互联网中的一台主机,而端口号则唯一标识了主机上的一个网络进程。因此,网络通信的本质是跨网络的两台主机之间的进程间通信。
- IP地址:用于标识互联网中的主机。
- 端口号:用于标识主机上的网络进程。
源端口号用于确定源主机上的网络进程,目的端口号用于确定目的主机上的网络进程。通过IP地址和端口号,我们可以唯一标识互联网上的一个进程。
概念 | 作用 |
---|---|
IP | IP地址唯一的标识了互联网中的一台主机。 |
端口 | 端口号唯一地标识主机上的一个网络进程。 |
源端口号确定源主机上的网络进程,目的端口号确定目的主机上的网络进程。
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; // 服务器端口号
};
网络套接字编程的本质是利用系统调用从零编写应用层,不是使用应用层。