开启辅助访问 切换到窄版

打印 上一主题 下一主题

关于linux网络编程的一些实用技巧和细节总结

[复制链接]
作者:6101111992 
版块:
嵌入式操作系统 linux 发布时间:2020-10-6 05:08:08
22280
楼主
跳转到指定楼层
| 只看该作者 回帖奖励 |倒序浏览 |阅读模式
由于网络编程涉及很多细节和技巧,一直想写篇文章来总结下这方面的心得与经验,希望对来者有一点帮助,那就善莫大焉了。
我们知道用connect函数默认是阻塞的,直到三次握手建立之后,或者实在连不上超时返回,期间程序执行流一直阻塞在那里。那么如何利用connect函数编写非阻塞的连接代码呢?
无论在windows还是linux平台都可以采取以下思路来实现:
1. 创建socket时,将socket设置成非阻塞模式;
2. 接着调用connect进行连接,如果connect能立即连接成功,则返回0;如果此刻不能立即连接成功,则返回-1(windows上返回SOCKET_ERROR也等于-1),这个时候错误码是WSAEWOULDBLOCK(windows平台),或者是EINPROGRESS(linux平台),表明立即暂时不能完成。
3. 接着调用select函数在指定的时间内检测socket是否可写,如果可写表明connect连接成功。
需要注意的是:linux平台上connect暂时不能完成返回-1,错误码可能是EINPROGRESS,也可能是由于被信号给中断了,这个时候错误码是:EINTR。这种情况也要考虑到;而在windows平台上除了用select函数去检测socket是否可写,也可以使用windows平台自带的函数WSAAsyncSelect或WSAEventSelect来检测。
下面是代码:
/** *@param timeout 连接超时时间,单位为秒*@return 连接成功返回true,反之返回false**/bool CSocket::Connect(int timeout){//windows将socket设置成非阻塞的方式unsigned long on = 1;if (::ioctlsocket(m_hSocket, FIONBIO, &on) < 0)return false;//linux将socket设置成非阻塞的方式//将新socket设置为non-blocking/*int oldflag = ::fcntl(newfd, F_GETFL, 0);int newflag = oldflag | O_NONBLOCK;if (::fcntl(m_hSocket, F_SETFL, newflag) == -1) return false;*/struct sockaddr_in addrSrv = { 0 };addrSrv.sin_family = AF_INET;addrSrv.sin_addr = htonl(addr);addrSrv.sin_port = htons((u_short)m_nPort);int ret = ::connect(m_hSocket, (struct sockaddr*)&addrSrv, sizeof(addrSrv));if (ret == 0)return true;//windows下检测WSAEWOULDBLOCKif (ret < 0 && WSAGetLastError != WSAEWOULDBLOCK)return false;//linux下需要检测EINPROGRESS和EINTR/*if (ret < 0 && (errno != EINPROGRESS || errno != EINTR))return false;*/fd_set writeset;FD_ZERO(&writeset);FD_SET(m_hSocket, &writeset);struct timeval tv;tv.tv_sec = timeout;//可以利用tv_usec做更小精度的超时设置tv.tv_usec = 0;if (::select(m_hSocket + 1, NULL, &writeset, NULL, &tv) != 1)return false;return true;} 这里不讨论阻塞模式下,阻塞模式下send和recv函数如果tcp窗口太小或没有数据的话都是阻塞在send和recv调用处的。对于收数据,一般的流程是先用select(windows和linux平台皆可)、WSAAsyncSelect或WSAEventSelect(windows平台)、poll或epoll_wait(linux平台)检测socket有数据可读,然后进行收取。对于发数据,;linux平台下epoll模型存在水平模式和边缘模式两种情形,如果是边缘模式一定要一次性把socket上的数据收取干净才行,也就是一定要循环到recv函数出错,错误码是EWOULDBLOCK。而linux下的水平模式或者windows平台上可以根据业务一次性收取固定的字节数,或者收完为止。还有个区别上文也说过,就是windows下发数据的代码稍微有点不同的就是不需要检测错误码是EINTR,只需要检测是否是WSAEWOULDBLOCK。代码如下:
用于windows或linux水平模式下收取数据,这种情况下收取的数据可以小于指定大小,总之一次能收到多少是多少:
bool TcpSession::Recv{//每次只收取256个字节char buff;//memset(buff, 0, sizeof(buff));int nRecv = ::recv(clientfd_, buff, 256, 0);if (nRecv == 0)return false;inputBuffer_.add(buff, (size_t)nRecv);return true;}如果是linux epoll边缘模式(ET),则一定要一次性收完:
bool TcpSession::RecvEtMode{//每次只收取256个字节char buff;while (true){//memset(buff, 0, sizeof(buff));int nRecv = ::recv(clientfd_, buff, 256, 0);if (nRecv == -1){if (errno == EWOULDBLOCK || errno == EINTR)return true;return false;}//对端关闭了socketelse if (nRecv == 0)return false;inputBuffer_.add(buff, (size_t)nRecv);}return true;} 用于linux平台发送数据:
bool TcpSession::Send{while (true){int n = ::send(clientfd_, buffer_, buffer_.length, 0);if (n == -1){//tcp窗口容量不够, 暂且发不出去,下次再发if (errno == EWOULDBLOCK)break;//被信号中断,继续发送else if (errno == EINTR)continue;return false;}//对端关闭了连接else if (n == 0)return false;buffer_.erase(n);//全部发送完毕if (buffer_.length == 0)break;}return true;} 另外,收发数据还有个技巧是设置超时时间,除了用setsocketopt函数设置send和recv的超时时间以外,还可以自定义整个收发数据过程中的超时时间,思路是开始收数据前记录下时间,收取完毕后记录下时间,如果这个时间差大于超时时间,则认为超时,代码分别是:
long tmSend = 3*1000L;long tmRecv = 3*1000L;setsockopt(m_hSocket, IPPROTO_TCP, TCP_NODELAY,(LPSTR)&noDelay, sizeof(long));setsockopt(m_hSocket, SOL_SOCKET, SO_SNDTIMEO,(LPSTR)&tmSend, sizeof(long));int httpclientsocket::RecvData(string& outbuf,int& pkglen){ if(m_fd == -1) return -1; pkglen = 0; char buf; time_t tstart = time(NULL); while(true) { int ret = ::recv(m_fd,buf,4096,0); if(ret == 0) { Close; return 0;//对方关闭socket了 } else if(ret < 0) { if(errno == EAGAIN || errno ==EWOULDBLOCK || errno == EINTR) { if(time(NULL) - tstart > m_timeout) { Close; return 0; } else continue; } else { Close; return ret;//接收出错 } } outbuf.append(buf,buf+ret); pkglen = GetBufLen(outbuf.data,outbuf.length); if(pkglen
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表