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 超时
Posix 定义了与 select/epoll 和
非阻塞 connect
相关的规定:
连接过程中写缓冲区不可用
连接建立
成功时
,socket 文件描述符变为可写
。(连接建立时,写缓冲区空闲,所以可写)连接建立
失败时
,socket 文件描述符既可读又可写
。 (由于有未决的错误,从而可读又可写)连接失败, 错误判定方式:
- 当用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
- …