以下为服务器端非阻塞的例子:
#include <unistd.h>
#include <fcntl.h>
sockfd = socket(AF_INET,SOCK_STREAM,0);
fcntl(sockfd,F_SETFL,O_NONBLOCK);
while(1)
{
sockConn = accept(socketSer,(SOCKADDR*)&addrClient,&len);
if(INVALID_SOCKET == sockConn)
{
int err = WSAGetLastError();
if(WSAEWOULDBLOCK == err)
{
Sleep(100);
continue;
}
char buf[100];
recv(sockConn,buf,100,0);
}
}
而调用select()会有效的解决这个问题,它允许你把进程本身挂起,而通知使系统内核监听所要求的一组文件描述符的任何活动,只要确认在任务被监控的文件描述符上出现活动,select()调用将返回指示该文件描述符已经准备好的信息,从而实现了为进程选出随机的变化,而不必由进程本身对输入进行测试而浪费CPU开销(关于异步I/O今后将重点介绍)。
socket将数据传输分为 流(TCP)和数据报(UDP)。两者有啥区别呢?
TCP作为传输控制协议,发送的是流,发的包不是整包达到的,而是连续不断的到达的,而组包的工作就需要接收端去完成。而UDP传输的是数据报文,它一定是一整包到达,但是整包数据不宜过长。适用于一次性传输少量数据,对可靠性不高的应用环境。
网络中究竟一次性能传输多大的数据量呢?是不是我们一次塞给网络多大的数据,网络就能传输多大的呢?网络中究竟一次性能传输多大的数据量呢?是不是我们一次塞给网络多大的数据,网络就能传输多大的呢?
发包的长度是我们自己决定的,这需要我们考虑业务的具体需求与当前网络的状况。对于TCP而言,发送的长度可以比较大,但是socket内核默认的收发缓冲区大小为几KB (用户可以通过调用SetSockOpt来该改变这个值)。对于UDP,设置的值不宜过大,因为UDP要是其中有一个分片丢失,那么接受方网络层将把整个发送包丢弃,这就形成丢包,如果一个UDP包很大,被切分的自然片数很多,容易丢失的概率也就越大,但是包也不能太小,太小了会影响业务效率。
1. send( )函数返回了实际发送的长度,只要网络不断,函数绝对不会返回发送失败(对于阻塞的socket,无论一包数据多大,都能够发送,但一次传输不宜过大,否则容易造成socket缓冲的阻塞)。对于TCP,可以写一个消息循环发送;而UDP最好一次性一整包到达。
2. recv( )函数,对于TCP,接收方先收这个包头信息,然后再收包数据。一次收齐整个包也可以,可要对结果是否收齐进行验证。这也就完成了组包过程。UDP,那你只能整包接收了。要是你提供的接收Buffer过小,TCP将返回实际接收的长度,余下的还可以收,而UDP不同的是,余下的数据被丢弃并返回WSAEMSGSIZE错误。注意TCP,要是你提供的Buffer佷大,那么可能收到的就是多个发包,你必须分离它们,还有就是当Buffer太小,而一次收不完Socket内部的数据,那么Socket接收事件(OnReceive),可能不会再触发,使用事件方式进行接收时,密切注意这点。这些特性就是体现了流和数据包的区别。
发包的长度是我们自己决定的,这需要我们考虑业务的具体需求与当前网络的状况。对于TCP而言,发送的长度可以比较大,但是socket内核默认的收发缓冲区大小为几KB (用户可以通过调用SetSockOpt来该改变这个值)。对于UDP,设置的值不宜过大,因为UDP要是其中有一个分片丢失,那么接受方网络层将把整个发送包丢弃,这就形成丢包,如果一个UDP包很大,被切分的自然片数很多,容易丢失的概率也就越大,但是包也不能太小,太小了会影响业务效率。
三. send( )和recv( )工作原理
Send( )函数作用就是将buffer中的数据拷贝到socket的发送缓冲区,只要拷贝成功,send( )函数就返回了,但是此时这些数据并不一定马上通过网络传输。对于非阻塞socket的send函数调用,需要先比较发送数据的长度bLen和套接字的发送缓冲区的长度sLen:如果bLen长度大于sLen的长度,该函数将会返回SOCKET_ERROR。对于阻塞socket的send调用,则会一直等待直到全部拷贝到socket发送缓冲区才返回。
recv( )函数先检查socket的接收缓冲区,如果接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕(这里对于阻塞套接字而言)。recv函数然后把socket中接收缓冲区中的数据拷贝到buffer中。
对方如果中途调用close( )正常的关闭socket,这并不影响另外一端recv的正常接收数据;如果协议缓冲区内没有数据,recv返回0,指示对方关闭;如果协议缓冲区有数据,则返回对应数据(可能需要多次recv),在最后一次recv时,返回0,指示对方关闭。
1. recv( )的返回值:
无论阻塞还是非阻塞,recv的返回值都没有区别:
- 小于 0 出错
- 等于0 连接关闭
- 大于0 接收到数据大小
- EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时(本机的接收缓冲区中无数据)
- EBADF:sock不是有效的描述词
- ECONNREFUSE:(104) 远程主机阻绝网络连接,远程主机没有关闭socket就直接退出了
- EFAULT:内存空间访问出错
- EINTR:操作被信号中断
- EINVAL:参数无效
- ENOMEM:内存不足
- ENOTCONN:与面向连接关联的套接字尚未被连接上
- ENOTSOCK:sock索引的不是套接字
以下代码是服务器端调用libevent的读事件回调(关于libevent,以后将讲解),调用该回调时,已经保证了有数据到来:
void main_Read(evutil_socket_t fd, short events, void *arg)
{
TFdState *state = arg;
int iUsedIdx = 0;
int result = 0;
memset(state->read_buf, 0 ,MAX_BUF_LEN);
while (1)
{
result = recv(fd, state->read_buf+iUsedIdx, MAX_BUF_LEN, 0);
if (result <= 0) break;
printf("result :%d\n", result);
iUsedIdx = result;
}
printf("while out result: %d errno: %d\n", result, errno);
if(result == 0) //接收套接字关闭
{
free_fd_state(state);
printf("recv finished!\n");
}
if(result < 0)
{
if(errno == EAGAIN)
{
printf("errno == EAGAIN\n");
return ;
}
else
{
printf("else");
free_fd_state(state);
}
}
}
(1) 如果客户端发送完数据(数据长度为3930个字节)以后就调用close() 关闭 套接字;且服务器的监听套接字(listen socket,设置非阻塞模式 ) 则结果如下:
(3)如果客户端发送数据以后,服务器的监听套接字为阻塞套接字,则结果如下: