SOCKET实战
Writen()、Readn()函数
send()
函数存在需要发送的字符数len小于函数返回的已发送的字符数(ssize_t)的问题
recv()
函数存在需要接收的字符数len小于函数返回的已接收的字符数(ssize_t)的问题
解决上述问题通过Writen()
、Readn()
函数实现
//buff是数据存储地址,n是待读取长度
bool Readn(const int sockfd, char *buffer, const size_t n)
{
int nLeft, nread, idx;
nLeft = n;
idx = 0;
while (nLeft > 0)
{
if ((nread = recv(sockfd, buffer + idx, nLeft, 0)) <= 0)
return false;
idx += nread;
nLeft -= nread;
}
return true;
}
bool Writen(const int sockfd, const char *buffer, const size_t n)
{
int nLeft, idx, nwritten;
nLeft = n;
idx = 0;
while (nLeft > 0)
{
if ((nwritten = send(sockfd, buffer + idx, nLeft, 0)) <= 0)
return false;
nLeft -= nwritten;
idx += nwritten;
}
return true;
}
TcpWrite()、TcpRead()函数
💡 解决TCP报文分包和粘包的问题
bool TcpRead(const int sockfd, char *buffer, int *ibuflen, const int itimeout)
{
if (sockfd == -1)
return false;
//连接超时
if (itimeout > 0)
{
fd_set tmpfd;
FD_ZERO(&tmpfd);
FD_SET(sockfd, &tmpfd);
struct timeval timeout;
timeout.tv_sec = itimeout;
timeout.tv_usec = 0;
int i;
if ((i = select(sockfd + 1, &tmpfd, 0, 0, &timeout)) <= 0)
return false;
}
(*ibuflen) = 0; //既然重新赋值为0,那么是否有参数传入的必要
if (Readn(sockfd, (char *)ibuflen, 4) == false)
return false;
(*ibuflen) = ntohl(*ibuflen); //网络字节序转换为主机字节序
if (Readn(sockfd, buffer, (*ibuflen)) == false)
return false;
return true;
}
bool TcpWrite(const int sockfd, const char *buffer, const int ibuflen)
{
if (sockfd == -1)
return false;
fd_set tmpfd;
FD_ZERO(&tmpfd);
FD_SET(sockfd, &tmpfd);
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
if (select(sockfd + 1, 0, &tmpfd, 0, &timeout) <= 0)
return false;
int ilen = 0;
// ibuf==0默认为字符数组,否则要给定大小
if (ibuflen == 0)
ilen = strlen(buffer);
else
ilen = ibuflen;
int ilenn = htonl(ilen); //主机字节序转换为网络字节序
char strTBuffer[ilen + 4];
memset(strTBuffer, 0, sizeof(strTBuffer));
memcpy(strTBuffer, &ilenn, 4); //用memcpy能够完成任何数据类型的拷贝
memcpy(strTBuffer + 4, buffer, ilen);
if (Writen(sockfd, strTBuffer, ilen + 4) == false)
return false;
return true;
}
多进程网络服务端框架
socket服务端封装为CTcpServer类
// socket通信的服务端类
class CTcpServer
{
private:
int m_socklen; // 结构体struct sockaddr_in的大小。
struct sockaddr_in m_clientaddr; // 客户端的地址信息。
struct sockaddr_in m_servaddr; // 服务端的地址信息。
public:
int m_listenfd; // 服务端用于监听的socket。
int m_connfd; // 客户端连接上来的socket。
bool m_btimeout; // 调用Read和Write方法时,失败的原因是否是超时:true-超时,false-未超时。
int m_buflen; // 调用Read方法后,接收到的报文的大小,单位:字节。
CTcpServer(); // 构造函数。
// 服务端初始化。
// port:指定服务端用于监听的端口。
// 返回值:true-成功;false-失败,一般情况下,只要port设置正确,没有被占用,初始化都会成功。
bool InitServer(const unsigned int port);
// 阻塞等待客户端的连接请求。
// 返回值:true-有新的客户端已连接上来,false-失败,Accept被中断,如果Accept失败,可以重新Accept。
bool Accept();
// 获取客户端的ip地址。
// 返回值:客户端的ip地址,如"192.168.1.100"。
char *GetIP();
// 接收客户端发送过来的数据。
// buffer:接收数据缓冲区的地址,数据的长度存放在m_buflen成员变量中。
// itimeout:等待数据的超时时间,单位:秒,缺省值是0-无限等待。
// 返回值:true-成功;false-失败,失败有两种情况:1)等待超时,成员变量m_btimeout的值被设置为true;2)socket连接已不可用。
bool Read(char *buffer,const int itimeout=0);
// 向客户端发送数据。
// buffer:待发送数据缓冲区的地址。
// ibuflen:待发送数据的大小,单位:字节,缺省值为0,如果发送的是ascii字符串,ibuflen取0,如果是二进制流数据,ibuflen为二进制数据块的大小。
// 返回值:true-成功;false-失败,如果失败,表示socket连接已不可用。
bool Write(const char *buffer,const int ibuflen=0);
// 关闭监听的socket,即m_listenfd,常用于多进程服务程序的子进程代码中。
void CloseListen();
// 关闭客户端的socket,即m_connfd,常用于多进程服务程序的父进程代码中。
void CloseClient();
~CTcpServer(); // 析构函数自动关闭socket,释放资源。
};
socket客户端封装为CTcpClient类
// socket通信的客户端类
class CTcpClient
{
public:
int m_sockfd; // 客户端的socket.
char m_ip[21]; // 服务端的ip地址。
int m_port; // 与服务端通信的端口。
bool m_btimeout; // 调用Read和Write方法时,失败的原因是否是超时:true-超时,false-未超时。
int m_buflen; // 调用Read方法后,接收到的报文的大小,单位:字节。
CTcpClient(); // 构造函数。
// 向服务端发起连接请求。
// ip:服务端的ip地址。
// port:服务端监听的端口。
// 返回值:true-成功;false-失败。
bool ConnectToServer(const char *ip,const int port);
// 接收服务端发送过来的数据。
// buffer:接收数据缓冲区的地址,数据的长度存放在m_buflen成员变量中。
// itimeout:等待数据的超时时间,单位:秒,缺省值是0-无限等待。
// 返回值:true-成功;false-失败,失败有两种情况:1)等待超时,成员变量m_btimeout的值被设置为true;2)socket连接已不可用。
bool Read(char *buffer,const int itimeout=0);
// 向服务端发送数据。
// buffer:待发送数据缓冲区的地址。
// ibuflen:待发送数据的大小,单位:字节,缺省值为0,如果发送的是ascii字符串,ibuflen取0,如果是二进制流数据,ibuflen为二进制数据块的大小。
// 返回值:true-成功;false-失败,如果失败,表示socket连接已不可用。
bool Write(const char *buffer,const int ibuflen=0);
// 断开与服务端的连接
void Close();
~CTcpClient(); // 析构函数自动关闭socket,释放资源。
};
mpserver.cpp
-
mpserver.cpp
/* * 程序名:mpserver.cpp,此程序演示采用freecplus框架的CTcpServer类实现socket通信多进程的服务端。 * 作者:C语言技术网(www.freecplus.net) 日期:20190525 */ #include "_freecplus.h" CLogFile logfile; // 服务程序的运行日志。 CTcpServer TcpServer; // 创建服务端对象。 // 程序退出时调用的函数 void FathEXIT(int sig); // 父进程退出函数。 void ChldEXIT(int sig); // 子进程退出函数。 int main(int argc,char *argv[]) { if (argc!=3) { printf("Using:./mpserver port logfile\nExample:./mpserver 5005 /tmp/mpserver.log\n\n"); return -1; } // 关闭全部的信号 for (int ii=0;ii<100;ii++) signal(ii,SIG_IGN); // 打开日志文件。 if (logfile.Open(argv[2],"a+")==false) { printf("logfile.Open(%s) failed.\n",argv[2]); return -1;} // 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程 // 但请不要用 "kill -9 +进程号" 强行终止 signal(SIGINT,FathEXIT); signal(SIGTERM,FathEXIT); if (TcpServer.InitServer(atoi(argv[1]))==false) // 初始化TcpServer的通信端口。 { logfile.Write("TcpServer.InitServer(%s) failed.\n",argv[1]); FathEXIT(-1); } while (true) { if (TcpServer.Accept()==false) // 等待客户端连接。 { logfile.Write("TcpServer.Accept() failed.\n"); continue; } if (fork()>0) { TcpServer.CloseClient(); continue; } // 父进程返回到循环首部。 // 子进程重新设置退出信号。 signal(SIGINT,ChldEXIT); signal(SIGTERM,ChldEXIT); TcpServer.CloseListen(); // 以下是子进程,负责与客户端通信。 logfile.Write("客户端(%s)已连接。\n",TcpServer.GetIP()); char strbuffer[1024]; // 存放数据的缓冲区。 while (true) { memset(strbuffer,0,sizeof(strbuffer)); if (TcpServer.Read(strbuffer,50)==false) break; // 接收客户端发过来的请求报文。 logfile.Write("接收:%s\n",strbuffer); strcat(strbuffer,"ok"); // 在客户端的报文后加上"ok"。 logfile.Write("发送:%s\n",strbuffer); if (TcpServer.Write(strbuffer)==false) break; // 向客户端回应报文。 } logfile.Write("客户端已断开。\n"); // 程序直接退出,析构函数会释放资源。 ChldEXIT(-1); // 通信完成后,子进程退出。 } } // 父进程退出时调用的函数 void FathEXIT(int sig) { if (sig > 0) { signal(sig,SIG_IGN); signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN); logfile.Write("catching the signal(%d).\n",sig); } kill(0,15); // 通知其它的子进程退出。 logfile.Write("父进程退出。\n"); // 编写善后代码(释放资源、提交或回滚事务) TcpServer.CloseClient(); exit(0); } // 子进程退出时调用的函数 void ChldEXIT(int sig) { if (sig > 0) { signal(sig,SIG_IGN); signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN); } logfile.Write("子进程退出。\n"); // 编写善后代码(释放资源、提交或回滚事务) TcpServer.CloseClient(); exit(0); }
-
利用
fork()
函数创建子进程,父进程负责监听工作,子进程负责与客户端的连接 -
父进程关闭
connectfd
,子进程关闭listenfd
,避免占用系统资源
if (TcpServer.Accept() == false) // 等待客户端连接。
{
logfile.Write("TcpServer.Accept() failed.\n");
continue;
}
if (fork() > 0)
{
TcpServer.CloseClient();
continue;
} // 父进程返回到循环首部。
// 子进程重新设置退出信号。
signal(SIGINT, ChldEXIT);
signal(SIGTERM, ChldEXIT);
TcpServer.CloseListen();
💡 考虑用sigaction()函数代替signal()函数进行优化
避免僵尸进程
- 忽略子进程的退出信号
wait()
函数阻塞式等待,影响主进程工作,可利用waitpid()
函数实现无阻塞等待
// 关闭全部的信号
for (int ii = 0; ii < 100; ii++)
{
signal(ii, SIG_IGN);
}
💡 关闭全部信号意味着同时忽略子进程的退出信号
多进程服务程序的退出和资源释放
- 实际开发中在终端按
Ctrl+C
退出是不专业的 - 用
信号
来通知和实现进程的退出 - 一般而言父进程退出则子进程也退出,而子进程单独退出则不受影响
// 父进程退出时调用的函数
void FathEXIT(int sig)
{
if (sig > 0)
{
signal(sig,SIG_IGN); signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
logfile.Write("catching the signal(%d).\n",sig);
}
kill(0,15); // 通知其它的子进程退出。
logfile.Write("父进程退出。\n");
// 编写善后代码(释放资源、提交或回滚事务)
TcpServer.CloseClient();
exit(0);
}
// 子进程退出时调用的函数
void ChldEXIT(int sig)
{
if (sig > 0)
{
signal(sig,SIG_IGN); signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
}
logfile.Write("子进程退出。\n");
// 编写善后代码(释放资源、提交或回滚事务)
TcpServer.CloseClient();
exit(0);
}
TCP短连接与长连接
client与server建立连接进行通信,通信完成后释放连接,建立连接时需要3次握手,释放连接需要4次挥手,连接的建立和释放都需要时间,server还有创建新进程或线程的开销
短连接:
client/server间只进行一次或连续多次通信
,通信完成后马上断开了。管理起来比较简单,不需要额外的控制手段
长连接:
client/serverl间需要多次通信,通信的频率和次数不确定
,所以client和server需要保持这个连接
增加心跳机制
心跳机制是针对长连接
而言的
如果client与server采用长连接,在连接空闲时,client每若干秒向server发送一个心跳报文,server也回复一个心跳报文,确认连接继续生效中
如果server在约定的时间内没有收到client的任何报文,则认为客户端已掉线,就主动断开连接,释放资源
💡 心跳报文建议在60秒以内,不要超过120秒
可以将心跳报文的发送放在子线程
中
多线程网络服务端框架
mtserver_biz.cpp
-
mtserver_biz.cpp
/* * 程序名:mtserver_biz.cpp,此程序演示采用freecplus框架的CTcpServer类实现socket通信多线程的服务端。 * 作者:C语言技术网(www.freecplus.net) 日期:20190525 */ #include "_freecplus.h" void *pthmain(void *arg); // 线程主函数。 vector<long> vpthid; // 存放线程id的容器。 void mainexit(int sig); // 信号2和15的处理函数。 void pthmainexit(void *arg); // 线程清理函数。 CLogFile logfile; // 服务程序的运行日志。 CTcpServer TcpServer; // 创建服务端对象。 // 处理业务的主函数。 bool _main(const char *strrecvbuffer,char *strsendbuffer); // 心跳报文。 bool biz000(const char *strrecvbuffer,char *strsendbuffer); // 身份验证业务处理函数。 bool biz001(const char *strrecvbuffer,char *strsendbuffer); // 查询余客业务处理函数。 bool biz002(const char *strrecvbuffer,char *strsendbuffer); int main(int argc,char *argv[]) { if (argc!=3) { printf("Using:./mtserver_biz port logfile\nExample:./mtserver_biz 5005 /tmp/mtserver_biz.log\n\n"); return -1; } // 关闭全部的信号 for (int ii=0;ii<100;ii++) signal(ii,SIG_IGN); // 打开日志文件。 if (logfile.Open(argv[2],"a+")==false) { printf("logfile.Open(%s) failed.\n",argv[2]); return -1;} // 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程 // 但请不要用 "kill -9 +进程号" 强行终止 signal(SIGINT,mainexit); signal(SIGTERM,mainexit); if (TcpServer.InitServer(atoi(argv[1]))==false) // 初始化TcpServer的通信端口。 { logfile.Write("TcpServer.InitServer(%s) failed.\n",argv[1]); return -1; } while (true) { if (TcpServer.Accept()==false) // 等待客户端连接。 { logfile.Write("TcpServer.Accept() failed.\n"); continue; } logfile.Write("客户端(%s)已连接。\n",TcpServer.GetIP()); pthread_t pthid; if (pthread_create(&pthid,NULL,pthmain,(void *)(long)TcpServer.m_connfd)!=0) { logfile.Write("pthread_create failed.\n"); return -1; } vpthid.push_back(pthid); // 把线程id保存到vpthid容器中。 } return 0; } void *pthmain(void *arg) { pthread_cleanup_push(pthmainexit,arg); // 设置线程清理函数。 pthread_detach(pthread_self()); // 分离线程。 pthread_setcanceltype(PTHREAD_CANCEL_DISABLE,NULL); // 设置取消方式为立即取消。 int sockfd=(int)(long)arg; // 与客户端的socket连接。 int ibuflen=0; char strrecvbuffer[1024],strsendbuffer[1024]; // 存放数据的缓冲区。 while (true) { memset(strrecvbuffer,0,sizeof(strrecvbuffer)); memset(strsendbuffer,0,sizeof(strsendbuffer)); if (TcpRead(sockfd,strrecvbuffer,&ibuflen,50)==false) break; // 接收客户端发过来的请求报文。 logfile.Write("接收:%s\n",strrecvbuffer); // 处理业务的主函数。 if (_main(strrecvbuffer,strsendbuffer)==false) break; logfile.Write("发送:%s\n",strsendbuffer); if (TcpWrite(sockfd,strsendbuffer)==false) break; // 向客户端回应报文。 } pthread_cleanup_pop(1); pthread_exit(0); } // 信号2和15的处理函数。 void mainexit(int sig) { logfile.Write("mainexit begin.\n"); // 关闭监听的socket。 TcpServer.CloseListen(); // 取消全部的线程。 for (int ii=0;ii<vpthid.size();ii++) { logfile.Write("cancel %ld\n",vpthid[ii]); pthread_cancel(vpthid[ii]); } logfile.Write("mainexit end.\n"); exit(0); } // 线程清理函数。 void pthmainexit(void *arg) { logfile.Write("pthmainexit begin.\n"); // 关闭与客户端的socket。 close((int)(long)arg); // 从vpthid中删除本线程的id。 for (int ii=0;ii<vpthid.size();ii++) { if (vpthid[ii]==pthread_self()) { vpthid.erase(vpthid.begin()+ii); } } logfile.Write("pthmainexit end.\n"); } bool _main(const char *strrecvbuffer,char *strsendbuffer) // 处理业务的主函数。 { int ibizcode=-1; GetXMLBuffer(strrecvbuffer,"bizcode",&ibizcode); switch (ibizcode) { case 0: // 心跳 biz000(strrecvbuffer,strsendbuffer); break; case 1: // 身份验证。 biz001(strrecvbuffer,strsendbuffer); break; case 2: // 查询余额。 biz002(strrecvbuffer,strsendbuffer); break; default: logfile.Write("非法报文:%s\n",strrecvbuffer); return false; } return true; } // 身份验证业务处理函数。 bool biz001(const char *strrecvbuffer,char *strsendbuffer) { char username[51],password[51]; memset(username,0,sizeof(username)); memset(password,0,sizeof(password)); GetXMLBuffer(strrecvbuffer,"username",username,50); GetXMLBuffer(strrecvbuffer,"password",password,50); if ( (strcmp(username,"wucz")==0) && (strcmp(password,"p@ssw0rd")==0) ) sprintf(strsendbuffer,"<retcode>0</retcode><message>成功。</message>"); else sprintf(strsendbuffer,"<retcode>-1</retcode><message>用户名或密码不正确。</message>"); return true; } // 查询余额业务处理函数。 bool biz002(const char *strrecvbuffer,char *strsendbuffer) { char cardid[51]; memset(cardid,0,sizeof(cardid)); GetXMLBuffer(strrecvbuffer,"cardid",cardid,50); if (strcmp(cardid,"62620000000001")==0) sprintf(strsendbuffer,"<retcode>0</retcode><message>成功。</message><ye>100.50</ye>"); else sprintf(strsendbuffer,"<retcode>-1</retcode><message>卡号不存在。</message>"); return true; } // 心跳报文 bool biz000(const char *strrecvbuffer,char *strsendbuffer) { sprintf(strsendbuffer,"<retcode>0</retcode><message>成功。</message>"); return true; }
💡 考虑用thread代替pthread实现多线程