当前位置: 首页 > article >正文

c++设计通信类

c++设计通信客户端类

c的客户端和服务器api

  // 客户端
  // C API
  int sckClient_init();
  /* 客户端 连接服务器 */
  int sckClient_connect(char *ip, int port, int connecttime, int *connfd);
  /* 客户端 关闭和服务端的连接 */
  int sckClient_closeconn(int connfd);
  /* 客户端 发送报文 */
  int sckClient_send(int connfd, int sendtime, unsigned char *data, int datalen);
  /* 客户端 接受报文 */
  int sckClient_rev(int connfd, int revtime, unsigned char **out, int *outlen); //1
  /* 释放内存 */
  int sck_FreeMem(void **buf);
  /* 客户端 释放 */
  int sckClient_destroy();//实际上就是close操作
  
  // 服务器端
  /* 服务器端初始化 */
  int sckServer_init(int port, int *listenfd);
  int sckServer_accept(int listenfd, int timeout, int *connfd);
  /* 服务器端发送报文 */
  int sckServer_send(int connfd, int timeout, unsigned char *data, int datalen);
  /* 服务器端端接受报文 */
  //outlen传出数据所以用的是指针 out也是传出数据 所以上面的释放内存函数是作用于这的
  int sckServer_rev(int  connfd, int timeout, unsigned char **out, int *outlen); 
  int sckServer_close(int connfd);
  /* 服务器端环境释放 */
  int sckServer_destroy();

封装服务器和客户端

  // 客户端
  // C API
  int sckClient_init();
  /* 客户端 连接服务器 */
  int sckClient_connect(char *ip, int port, int connecttime, int *connfd);
  /* 客户端 关闭和服务端的连接 */
  int sckClient_closeconn(int connfd);
  /* 客户端 发送报文 */
  int sckClient_send(int connfd, int sendtime, unsigned char *data, int datalen);
  /* 客户端 接受报文 */
  int sckClient_rev(int connfd, int revtime, unsigned char **out, int *outlen); //1
  /* 释放内存 */
  int sck_FreeMem(void **buf);
  /* 客户端 释放 */
  int sckClient_destroy();
  
  // 服务器端
  /* 服务器端初始化 */
  int sckServer_init(int port, int *listenfd);
  int sckServer_accept(int listenfd, int timeout, int *connfd);
  /* 服务器端发送报文 */
  int sckServer_send(int connfd, int timeout, unsigned char *data, int datalen);
  /* 服务器端端接受报文 */
  int sckServer_rev(int  connfd, int timeout, unsigned char **out, int *outlen); //1
  int sckServer_close(int connfd);
  /* 服务器端环境释放 */
  int sckServer_destroy();

客户端修改为c++:
step1:

class TcpClient
{
public:
	int sckClient_init();
	/* 客户端 连接服务器 */
	int sckClient_connect(char *ip, int port, int connecttime, int *connfd);
	/* 客户端 关闭和服务端的连接 */
	int sckClient_closeconn(int connfd);
	/* 客户端 发送报文 */
	int sckClient_send(int connfd, int sendtime, unsigned char *data, int datalen);
	/* 客户端 接受报文 */
	int sckClient_rev(int connfd, int revtime, unsigned char **out, int *outlen); //1
	/* 释放内存 */
	int sck_FreeMem(void **buf);
	/* 客户端 释放 */
	int sckClient_destroy();
}

step2:

class TcpClient
{
public:
 	TcpClient();
	/* 客户端 连接服务器 */
	int sckClient_connect(char *ip, int port, int connecttime, int *connfd);
	/* 客户端 关闭和服务端的连接 */
	int sckClient_closeconn(int connfd);
	/* 客户端 发送报文 */
	int sckClient_send(int connfd, int sendtime, unsigned char *data, int datalen);
	/* 客户端 接受报文 */
	int sckClient_rev(int connfd, int revtime, unsigned char **out, int *outlen); //1
	/* 释放内存 */
	int sck_FreeMem(void **buf);
	~TcpClient();
}

step3:

class TcpClient
{
public:
 	TcpClient();
	/* 客户端 连接服务器 */
	int sckClient_connect(char *ip, int port, int connecttime, int *connfd);
	/* 客户端 关闭和服务端的连接 */
	int sckClient_closeconn(int connfd);
	/* 客户端 发送报文 */
	int sckClient_send(int connfd, int sendtime, unsigned char *data, int datalen);
	/* 客户端 接受报文 */
	int sckClient_rev(int connfd, int revtime, unsigned char **out, int *outlen); //1
	/* 释放内存 */
	int sck_FreeMem(void **buf);
	~TcpClient();
}

step4:

class TcpClient
{
public:
 	TcpClient();
	/* 客户端 连接服务器 */
	int sckClient_connect(char *ip, int port, int connecttime);
	/* 客户端 关闭和服务端的连接 */
	int sckClient_closeconn();
	/* 客户端 发送报文 */
	int sckClient_send(int sendtime, unsigned char *data, int datalen);
	/* 客户端 接受报文 */
	int sckClient_rev(int revtime, unsigned char **out, int *outlen); //1
	~TcpClient();
private:
	int m_connfd;//用于通信的文件描述符
}

step5:

class TcpClient
{
public:
 	TcpClient();
	/* 客户端 连接服务器 */
	int connectToHost(string ip, unsigned short port, int connecttime);
	/* 客户端 关闭和服务端的连接 */
	int disConnect();
	/* 客户端 发送报文 */
	int sendMsg(string sendMsg, int sendtime=100000);
	/* 客户端 接受报文 */
	string tecvMsg(int timeout); //1;
	~TcpClient();
private:
	int m_connfd;
}

服务器端修改为c++:
step1:

class TcpServer
{
	/* 服务器端初始化 */
	int sckServer_init(int port, int *listenfd);//listenfd传出参数
	int sckServer_accept(int listenfd, int timeout, int *connfd);//传入
	/* 服务器端发送报文 */
	int sckServer_send(int connfd, int timeout, unsigned char *data, int datalen);
	/* 服务器端端接受报文 */
	int sckServer_rev(int  connfd, int timeout, unsigned char **out, int *outlen);//1
	int sckServer_close(int connfd);
	/* 服务器端环境释放 */
	int sckServer_destroy();
}

step2:

class TcpServer
{
public:
	/* 服务器端初始化 */
	TcpServer();
	int sckServer_accept(int listenfd, int timeout, int *connfd);
	/* 服务器端发送报文 */
	int sckServer_send(int connfd, int timeout, unsigned char *data, int datalen);
	/* 服务器端端接受报文 */
	int sckServer_rev(int  connfd, int timeout, unsigned char **out, int *outlen);//1
	int sckServer_close(int connfd);
	/* 服务器端环境释放 */
	~TcpServer();
}

step3:

class TcpServer
{
public:
	/* 服务器端初始化 */
	TcpServer();//做绑定等操作
	int acceptConn(int timeout, int *connfd);
	/* 服务器端发送报文 */
	int sckServer_send(int connfd, int timeout, unsigned char *data, int datalen);
	/* 服务器端端接受报文 */
	int sckServer_rev(int  connfd, int timeout, unsigned char **out, int *outlen);//1
	int sckServer_close(int connfd);
	/* 服务器端环境释放 */
	~TcpServer();
private:
	int m_lfd;//监听的文件描述符
}

step4:

class TcpServer
{
public:
	//初始化监听套接字
	TcpServer();
	int acceptConn(int timeout);//得到通信的文件描述符
	int sendMsg(string sendMsg, int sendtime=100000);
	string tecvMsg(int timeout); //1;
	~TcpServer();//断开监听的fd
	int disConnect();//和客户端断开连接
private:
	int m_lfd;//监听的文件描述符
	int m_connfd;//通信的文件描述符
}

完成了?
问题1:这个类不能用,因为只能和一个客户端进行连接
问题2:服务器端和客户端的代码冗余

处理思路:服务器端不负责通信只负责监听,如果通信使用客户端类

总体类的封装:
step1:
客户端:

class TcpSocket
{
public:
 	TcpSocket();
	/* 客户端 连接服务器 */
	int connectToHost(string ip, unsigned short port, int connecttime);
	/* 客户端 关闭和服务端的连接 */
	int disConnect();
	/* 客户端 发送报文 */
	int sendMsg(string sendMsg, int sendtime=100000);
	/* 客户端 接受报文 */
	string tecvMsg(int timeout); //1;
	~TcpSocket();
private:
	int m_connfd;
}

服务器端:

class TcpServer
{
public:
	TcpServer();
	TcpSocket* acceptConn(int timeout);
	~TcpServer();
private:
	int m_lfd;//监听的文件描述符
}

step2:
客户端:

class TcpSocket
{
public:
 	TcpSocket()
 	{
 		m_connfd=socket(AF_INET,SOCK_STREAM,0);//创建后需要进行connect
 	}
 	TcpSocket(int fd)
 	{
 		m_connfd=fd;//传递正在进行的fd 不需要连接
 	}
	int connectToHost(string ip, unsigned short port, int connecttime)
	{
		connect(m_connfd,&serverAddress,&len);
	}
	int disConnect();
	int sendMsg(string sendMsg, int sendtime=100000)
	{
		send(m_connfd,data,datalen,0);
	}
	string tecvMsg(int timeout)
	{
		recv(m_connfd,buf,sizeof(buf),0);
		return string(buf);
	}
	~TcpSocket();
private:
	int m_connfd;
}

服务器端:

class TcpServer
{
public:
	TcpServer();
	TcpSocket* acceptConn(int timeout=10000)
	{
		int fd=accept(m_lfd,&address,&len);
		//将通信的fd变成类  
		TcpSocket * tcp=new TcpSocket(fd);
		if(tcp!=NULL)
		{
			return tcp;
		}
		return NULL;
	}
	~TcpServer();
private:
	int m_lfd;//监听的文件描述符
}

使用封装进行通信伪代码(套接字通信的服务器端程序):

void* callback(void *arg)
{
	TcpSocket* tcp=(TcpSocket* )arg;
	//通信
	tcp->sendMsg();
	tcp->recvMsg();
	tcp->disConnect();
	delete tcp;
}
int main()
{
	//new对象后创建绑定监听都做了
	TcpServer *server=new TcpServer;
	//accept
	while(1)
	{
		TcpSocket* tcp=server->acceptConn();
		//创建子线程  ->   通信
		pthread_create(&tid,NULL,callback,tcp);
	}
	delete server;
	return 0;
}	

使用封装进行通信伪代码(套接字通信的客户端程序):

int main()
{
	//创建通信的套接字对象
	TcpSocket* tcp=new TcpSocket;
	//连接服务器
	tcp->connectToHost(ip,port,timeout);
	//通信
	tcp->sendMsg();
	tcp->recvMsg();
	tcp->disConnect();
	delete tcp;
	return 0;
}

套接字超时处理

// 套接字通信过程中默认的阻塞函数 -> 条件不满足, 一直阻塞
// 等待并接受客户端连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 通信
// 接收数据
ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 发送数据
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 连接服务器的时候
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 设置超时处理的原因:
	- 不想让线程/进程一直在对应的函数(上边的函数)的位置阻塞
	- 设置一个阻塞的时间, 当时间到了之后强制线程/进程处理别的任务
// 超时处理的思路:
	- 定时器 
		- linux中可以发信号, 中断休眠
		
	- sleep(10)
        - 不可用, 在指定时间之内如果阻塞函数满足条件, 直接接触阻塞, 进行业务处理
        - 上述两种方式, 不能在程序休眠过程中解除休眠, 进行业务处理
    - IO多路转接函数:
		- 帮助我们委托内核检测fd的状态://异常
		- 这些函数最后一个参数设置函数阻塞时长, 在阻塞过程中, 如果有fd状态发生变化, 函数直接返回

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);	// 单位: s

int poll(struct pollfd *fds, nfds_t nfds, int timeout);	// 单位: 毫秒
int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);		// 单位: 毫秒

accept超时

// 等待并接受客户端连接
// 如果没有客户端连接, 一直阻塞
// 检测accept函数对应的fd(监听的文件描述法)的读缓冲区就可以了
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// 使用select检测状态
struct timeval {
	time_t      tv_sec;         /* seconds */
	suseconds_t tv_usec;        /* microseconds */
};
struct timeval val = {3, 0};	// 3s
// 监听的sockfd放到读集合中进行检测
fd_set rdset;
FD_ZERO(&rdset);
FD_SET(sockfd, &rdset);	// sockfd监听的文件描述符
int ret = select(sockfd+1, &rdset, NULL, NULL, &val);
if(ret == 0)
{
 	// 超时了, 最后一个参数等待时长用完了   
}
else if(ret = 1)
{
    // 有新连接
    accept();	// 绝对不阻塞
}
else
{
    // 异常, select调用失败, 返回值为 -1
}
TcpSocket* TcpServer::acceptConn(int wait_seconds)
{
	int ret;
	if (wait_seconds > 0)
	{
		fd_set accept_fdset;//创建文件描述符集合
		struct timeval timeout;
		FD_ZERO(&accept_fdset);
		FD_SET(m_lfd, &accept_fdset);
		timeout.tv_sec = wait_seconds;//秒
		timeout.tv_usec = 0;//微秒
		do
		{
			// 检测读集合 select返回监听的文件描述符的个数
			ret = select(m_lfd + 1, &accept_fdset, NULL, NULL, &timeout);
		} while (ret < 0 && errno == EINTR);	// 被信号中断, 再次进入循环(linux下会发生)
		if (ret <= 0)
		{
			return NULL;
		}
	}

	// 一但检测出 有select事件发生,表示对等方完成了三次握手,客户端有新连接建立
	// 此时再调用accept将不会堵塞
	struct sockaddr_in addrCli;
	socklen_t addrlen = sizeof(struct sockaddr_in);
	int connfd = accept(m_lfd, (struct sockaddr*)&addrCli, &addrlen); //返回已连接套接字
	if (connfd == -1)
	{
		return NULL;
	}
	return new TcpSocket(connfd);
}

read超时

int TcpSocket::readTimeout(unsigned int wait_seconds)
{
	int ret = 0;
	if (wait_seconds > 0)
	{
		fd_set read_fdset;
		struct timeval timeout;

		FD_ZERO(&read_fdset);
		FD_SET(m_socket, &read_fdset);

		timeout.tv_sec = wait_seconds;
		timeout.tv_usec = 0;

		//select返回值三态
		//1 若timeout时间到(超时),没有检测到读事件 ret返回=0
		//2 若ret返回<0 &&  errno == EINTR 说明select的过程中被别的信号中断(可中断睡眠原理)
		//2-1 若返回-1,select出错
		//3 若ret返回值>0 表示有read事件发生,返回事件发生的个数
		do
		{
			ret = select(m_socket + 1, &read_fdset, NULL, NULL, &timeout);

		} while (ret < 0 && errno == EINTR);

		if (ret == 0)
		{
			ret = -1;
			errno = ETIMEDOUT;
		}
		else if (ret == 1)
		{
			ret = 0;
		}
	}
	return ret;
}

write超时

int TcpSocket::writeTimeout(unsigned int wait_seconds)
{
	int ret = 0;
	if (wait_seconds > 0)
	{
		fd_set write_fdset;
		struct timeval timeout;

		FD_ZERO(&write_fdset);
		FD_SET(m_socket, &write_fdset);
		timeout.tv_sec = wait_seconds;
		timeout.tv_usec = 0;
		do
		{
			ret = select(m_socket + 1, NULL, &write_fdset, NULL, &timeout);
		} while (ret < 0 && errno == EINTR);

		// 超时
		if (ret == 0)
		{
			ret = -1;
			errno = ETIMEDOUT;
		}
		else if (ret == 1)
		{
			ret = 0;	// 没超时
		}
	}

	return ret;
}

connect 超时

  1. Posix 定义了与 select/epoll 和 非阻塞 connect 相关的规定:

    • 连接过程中写缓冲区不可用

    • 连接建立成功时,socket 文件描述符变为可写。(连接建立时,写缓冲区空闲,所以可写)

    • 连接建立失败时,socket 文件描述符既可读又可写。 (由于有未决的错误,从而可读又可写)

  2. 连接失败, 错误判定方式:

    • 当用select检测连接时,socket既可读又可写,只能在可读的集合通过getsockopt获取错误码。
// 连接服务器 -> 如果连接过程中, 函数不返回-> 程序阻塞在这个函数上, 通过返回值判断函数是不是调用成功了
// 返回值: 0 -> 连接成功, -1: 连接失败
// 默认该函数有一个超时处理: 75s, 175s
// 如果上述不能满足, 需要自己设置超时处理
// 设置超时连接处理过程:
	- 设置connect函数操作的文件描述符为非阻塞
	- 调用connect
	- 使用select检测
		- 需要getsockopt进行判断
	- 设置connect函数操作的文件描述符为阻塞 -> 状态还原
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 获取文件描述符的状态是否有错误
int getsockopt(int sockfd, int level, int optname,
                      void *optval, socklen_t *optlen);
// 判断错误
sockfd: 文件描述符
level: SOL_SOCKET
optname: SO_ERROR
optval: int 类型, 存储错误状态
	- 没有问题: 0
    - 有问题: 保存了错误码(错误编号 > 0)
optlen: optval大小对一个的以地址
// connect超时处理
// 设置非阻塞
int flag = fcntl(connfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
// 连接服务器 -> 不阻塞了  因为前面的文件描述符设置为非阻塞
connect(connfd, &serveraddress, &addlen);
// 通过select检测
struct timeval val = {3, 0};	// 3s
// 通信的connfd放到写集合中进行检测
fd_set wrset;
FD_ZERO(&wrset);
FD_SET(connfd, &wrset);	// fd通信的文件描述符
// 函数返回了, connect有结果了, 成功/失败 -> 过程走完了, 得到了结果
int ret = select(fd+1, NULL, &wrset, NULL, &val);
if(ret == 0)
{
 	// 超时了, connect还在连接过程中
}
else if(ret == 1)
{
    // 写缓冲区可写
    // 连接过程完成了, 得到了结果
    int opt;
    getsockopt(connfd, SOL_SOCKET, SO_ERROR, &opt, sizeof(opt));
    if(opt > 0)
    {
        // connect失败了
    }
    else if(opt == 0)
    {
        // connect连接成功了
    }
}
else
{
    // 异常, select调用失败, 返回值为 -1
}
// 将connfd状态还原 -> 阻塞

tcp通信粘包问题

客户端每隔1s给服务器发送一条数据, 每条数据长度 100字节 , 服务器每隔2s接收一次数据

  • 服务器接收一个数据得到200字节 -> 粘包

怎么造成的?

  • 发送的时候, 内核进行了优化, 数据到达一定量发送一次
  • 网络环境不好, 有延时
  • 接收方接收数据频率低, 一次性读到了多条客户端发送的数据

解决方案:

  • 发送的时候, 强制缓冲区数据被发送出去 - > flush
  • 在发送数据的时候每个数据包添加包头
    • 包头: 一块内存, 存储了当前这条消息的属性信息
      • 属于谁 -> char[12]
      • 有多大 -> int

http://www.kler.cn/news/368795.html

相关文章:

  • MySQLDBA修炼之道-开发篇(一)
  • Kafka如何控制消费的位置?
  • pdf压缩如何操作?教你8招,轻松搞定文件压缩!
  • 【C++打怪之路Lv12】-- 模板进阶
  • (前瞻篇)机器学习与深度学习对比
  • asp.net Core日志 ILoggerFactory、ILogger、ILoggerProvider
  • react 总结+复习+应用加深
  • P11227 [CSP-J 2024] 扑克牌(民间数据)
  • 环 境 配 置
  • 【递归、回溯及搜索】No.4---综合练习
  • 【Spring MVC】请求参数的传递
  • 人工智能岗位英语面试 - 如何确保模型的可靠性和性能
  • wordpress伪静态规则
  • mongodb:增删改查和特殊查询符号手册
  • 安全边际篇
  • 【React】React 18:新特性与重大更新解析
  • Jenkins部署springboot项目 记录一下过程
  • LeetCode 107.二叉树的层次遍历 II
  • Flutter按钮控件(六)
  • 冒泡排序和二分查找--go
  • 报错解决:opene3d draw_geometries(): incompatible function arguments.
  • 智能工厂的设计软件 意识能力(被动综合/主动把握/折衷解决):意识形态及认知计算机科学的架构、系统和运用
  • Linux学习_7
  • C++学习路线(二十六)
  • Mysql 数据库架构
  • 每天五分钟深度学习框架pytorch:从底层搭建多项式线性回归模型