// socket通讯的服务端类
class ctcpserver
{
private:
int m_socklen; // 结构体struct sockaddr_in的大小。
struct sockaddr_in m_clientaddr; // 客户端的地址信息。
struct sockaddr_in m_servaddr; // 服务端的地址信息。
int m_listenfd; // 服务端用于监听的socket。
int m_connfd; // 客户端连接上来的socket。
public:
ctcpserver():m_listenfd(-1),m_connfd(-1) {} // 构造函数。
// 服务端初始化。
// port:指定服务端用于监听的端口。
// 返回值:true-成功;false-失败,一般情况下,只要port设置正确,没有被占用,初始化都会成功。
bool initserver(const unsigned int port,const int backlog=5);
// 从已连接队列中获取一个客户端连接,如果已连接队列为空,将阻塞等待。
// 返回值:true-成功的获取了一个客户端连接,false-失败,如果accept失败,可以重新accept。
bool accept();
// 获取客户端的ip地址。
// 返回值:客户端的ip地址,如"192.168.1.100"。
char *getip();
// 接收对端发送过来的数据。
// buffer:存放接收数据的缓冲区。
// ibuflen: 打算接收数据的大小。
// itimeout:等待数据的超时时间(秒):-1-不等待;0-无限等待;>0-等待的秒数。
// 返回值:true-成功;false-失败,失败有两种情况:1)等待超时;2)socket连接已不可用。
bool read(string &buffer,const int itimeout=0); // 接收文本数据。
bool read(void *buffer,const int ibuflen,const int itimeout=0); // 接收二进制数据。
// 向对端发送数据。
// buffer:待发送数据缓冲区。
// ibuflen:待发送数据的大小。
// 返回值:true-成功;false-失败,如果失败,表示socket连接已不可用。
bool write(const string &buffer); // 发送文本数据。
bool write(const void *buffer,const int ibuflen); // 发送二进制数据。
// 关闭监听的socket,即m_listenfd,常用于多进程服务程序的子进程代码中。
void closelisten();
// 关闭客户端的socket,即m_connfd,常用于多进程服务程序的父进程代码中。
void closeclient();
~ctcpserver(); // 析构函数自动关闭socket,释放资源。
};
关于read函数的细节
bool ctcpserver::read(string &buffer,const int itimeout) // 接收文本数据。
{
if (m_connfd==-1) return false;
return(tcpread(m_connfd,buffer,itimeout));
}
tcpread函数
bool tcpread(const int sockfd,string &buffer,const int itimeout) // 接收文本数据。
{
if (sockfd==-1) return false;
// 如果itimeout>0,表示等待itimeout秒,如果itimeout秒后接收缓冲区中还没有数据,返回false。
if (itimeout>0)
{
struct pollfd fds;
fds.fd=sockfd;
fds.events=POLLIN;
if ( poll(&fds,1,itimeout*1000) <= 0 ) return false;
}
// 如果itimeout==-1,表示不等待,立即判断socket的接收缓冲区中是否有数据,如果没有,返回false。
if (itimeout==-1)
{
struct pollfd fds;
fds.fd=sockfd;
fds.events=POLLIN;
if ( poll(&fds,1,0) <= 0 ) return false;
}
int buflen=0;
// 先读取报文长度,4个字节。
if (readn(sockfd,(char*)&buflen,4) == false) return false;
buffer.resize(buflen); // 设置buffer的大小。
// 再读取报文内容。
if (readn(sockfd,&buffer[0],buflen) == false) return false;
return true;
}
为什么这里先读取报文内容:
主要为了解决粘包和分包问题
TCP是面向流的协议,发送的数据在接收端并没有明确的边界。多个报文可能会被合并成一个大的数据块(粘包),或者一个报文可能被拆分成多个数据块。读取报文长度后,可以根据长度信息来判断何时一个完整的报文已经接收完毕。
将 &buflen
强制类型转换为 char*
类型,可以解决类型不匹配的问题。这样,recv
函数就会将接收到的字节正确地写入到 buflen
变量所指向的内存位置。
报文长度告诉接收方整个报文的实际大小,这样接收方可以准确地分配合适大小的内存空间来存储数据,避免数据溢出或者不完整的情况
明确报文长度可以帮助接收方更高效地处理数据。它可以避免不必要的等待和重试,因为接收方知道什么时候应该开始和结束读取数据。
poll函数
poll
函数是一个系统调用,用于在多个文件描述符上等待事件的发生。它允许程序在等待时不阻塞整个进程,而是允许程序继续执行其他任务或进行轮询,直到所关心的文件描述符上有指定的事件发生为止。
函数原型
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明
-
fds:
- 类型:
struct pollfd *
- 描述:指向一个
pollfd
结构数组的指针。每个pollfd
结构描述了一个待检查的文件描述符及其关心的事件。
- 类型:
-
nfds:
- 类型:
nfds_t
- 描述:表示
fds
数组中元素的数量。
- 类型:
-
timeout:
- 类型:
int
- 描述:表示超时时间,单位是毫秒。指定为
-1
表示poll
调用将无限期阻塞,直到有事件发生;指定为0
表示poll
调用立即返回,不阻塞;指定大于0
的值表示在超时时间内等待事件发生。
- 类型:
返回值
- 如果
poll
调用成功,返回值为发生事件的文件描述符数量。 - 如果超时时间到达而没有任何文件描述符上的事件发生,返回值为
0
。 - 如果发生错误,返回值为
-1
,并设置errno
指示具体错误。
struct pollfd
结构体
pollfd
结构体用于描述一个文件描述符及其关心的事件:
struct pollfd {
int fd; // 文件描述符
short events; // 等待的事件(如 POLLIN、POLLOUT 等)
short revents; // 实际发生的事件(由系统填充)
};
readn函数
bool readn(const int sockfd,char *buffer,const size_t n)
{
int nleft=n; // 剩余需要读取的字节数。
int idx=0; // 已成功读取的字节数。
int nread; // 每次调用recv()函数读到的字节数。
while(nleft > 0)
{
if ( (nread=recv(sockfd,buffer+idx,nleft,0)) <= 0) return false;
idx=idx+nread;
nleft=nleft-nread;
}
return true;
}
recv函数:
recv(int sockfd, void *buf, size_t len, int flags)
参数解释:
-
sockfd:这是一个已经通过
socket
函数创建的套接字描述符(socket descriptor)。recv
函数通过这个描述符来识别和操作特定的网络连接。 -
buf:
buf
是一个指向存放接收数据的缓冲区的指针。接收到的数据将会被写入到这个缓冲区中。它的类型是void*
,即可以指向任意类型的数据。通常在使用时会进行类型转换,比如(char*)buf
。 -
len:
len
是buf
所指向的缓冲区的长度,即能够接收的最大字节数。这个参数告诉recv
函数缓冲区的大小,避免写入超出缓冲区界限的数据。 -
flags:
flags
是一个用于控制接收行为的参数,可以设置为0或者其他特定的标志。常见的选项有:- 可能还有其他平台特定的标志,用于控制接收的行为。
MSG_DONTWAIT
:非阻塞操作,立即返回,如果没有数据可接收则返回错误。MSG_WAITALL
:阻塞操作,直到接收到指定长度的数据。
recv 函数返回值:
- 如果成功接收数据,
recv
函数返回接收到的字节数(可以小于请求的字节数)。 - 如果连接已关闭(对端关闭),
recv
返回 0 表示连接结束。 - 如果发生错误,
recv
返回一个负数,表示特定的错误类型(如 -1 表示错误,可以通过errno
获取具体错误码)
write中有writen函数
使用write你函数取代send函数因为:
send函数共能是把待发送数据拷贝到发送缓冲区,如果缓冲区内存不足,返回本次已拷贝的字节数;为保证发送成功,应该循环调用send函数;在wirten中循环调用了send函数