PPC的C/C++和人工智能学习笔记
每一篇学习笔记,都只是为了更好地掌握和理解

linux开发(8)_网络编程(3)socket同步与异步

socket网络通信中:同步(阻塞)和异步(非阻塞)的学习。

 

阻塞和非阻塞,主要针对的是accept、connect、send/sendto/write、recv/recvfrom/read等函数。阻塞是指这些函数必须执行完成才返回,比如accept,假如没有客户端连入,那么会一直在这里等待客户端的连入,比如recv,假如没有收到数据,那么会一直等到有数据接收到(当然,设置了阻塞模式下的timeout,时间到还没有数据读到,也会返回错误)。而非阻塞则不同,不管是否有客户端连入或者有数据接收,有则连入或接收返回,没有则立即返回,而不会在那里等待。

 

设置某个 套接字/文件 阻塞或非阻塞的函数(fcntl):

#include <unistd.h>

#include <fcntl.h>

int fcntl(int fd, int cmd, … /* arg */ ); //失败返回-1

 

设置为非阻塞模式:

先用fcntl的F_GETFL获取flags,用F_SETFL设置flags|O_NONBLOCK;

int flags = fcntl(sockfd, F_GETFL, 0);    //获取文件的flags值。

if(flags==-1) {

perror(“Get fcntl flags Error”);

exit(EXIT_FAILURE/*#include stdlib.h*/);

}

if( fcntl(sockfd, F_SETFL, flags | O_NONBLOCK)==-1){  //设置成非阻塞模式;

perror(“Set fcntl flags Error”);

exit(EXIT_FAILURE/*#include stdlib.h*/);

}

同时在接收和发送数据时,需要使用MSG_DONTWAIT标志:

在recv、recvfrom、send、sendto数据时,将flag(第4个参数)设置为MSG_DONTWAIT。

 

设置成阻塞模式:

先用fcntl的F_GETFL获取flags,用F_SETFL设置flags&~O_NONBLOCK;

int flags  = fcntl(sockfd,F_GETFL,0);        //获取文件的flags值。

fcntl(sockfd,F_SETFL,flags&~O_NONBLOCK);    //设置成阻塞模式,没加错误判断

同时在接收和发送数据时,需要使用阻塞标志0:

在recv,recvfrom和send,sendto数据时,将flag(第4个参数)设置为0,默认是阻塞。

 

判断当前是同步还是异步(阻塞还是非阻塞):

int flags = fcntl(fd, F_GETFL);

if (flags == -1) {

perror(“get fd flags error”);

exit(EXIT_FAILURE);

}

return (flags & O_NONBLOCK) ? 1 : 0; //返回1表示非阻塞,返回0表示阻塞

 

阻塞模式下的超时设置:

在阻塞模式下,recv/recvfrom/read等函数,是可以设置超时选项的,就是说在设定的时间内没有数据读取的话,函数也会返回,有点类似模拟异步的套路。

需要注意的是:超时设置,只能在同步模式(阻塞模式)下设置才有效,异步模式是无效的(已经异步了,还要你这个超时干啥!)

struct timeval tv_out, tv_in;

tv_in.tv_sec = 3; //秒数

tv_in.tv_usec = 0; //微妙数

//设置接收超时

if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv_in, sizeof(tv_in)) < 0){

perror(“set rcv timeout error”);

exit(EXIT_FAILURE);

}

tv_out.tv_sec = 3;

tv_out.tv_usec = 0;

//设置发送超时

if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv_out, sizeof(tv_out)) < 0){

perror(“set rcv timeout error”);

exit(EXIT_FAILURE);

}

 

recv/recvfrom/read在同步和异步模式下的返回值以及处理:

==0:当返回值为0时,表示对端已经关闭了这个连接,此时应该自己关闭这个链接,即close(sockfd)。

另外假如使用了select或epoll做事件触发,则:1、如果使用select,应该使用FD_CLR(sockfd,fd_set)将sockfd清除掉,不再监听。2、如果使用epoll,系统会自己将sockfd清除掉,不再进行监听。

 

>0:当返回值大于0,返回值是读取到的字节数(长度),该值小于sizeof(buffer)缓冲区大小时,表示数据肯定读完。如果等于sizeof(buffer),可能有数据还没读,就需要继续读。可以通过一些方法首先知道需要读取数据的长度(比如先发送一个小包告诉对方字节数,或者在该包的头部某个位置写上字节数),然后通过对比长度,来得知是否已经读完该次所有数据。(tcp是基于流传输的,所以每次发生的数据之间没有边界的说法,需要我们在应用程序中自己解决需要读取多少数据的问题,也就是所谓“粘包问题”,我们需要自己封包和拆包。)

 

<0:当返回值小于0,即等于-1时,有几种情况:

1、如果errno == EAGAINE或EWOULDBLOCK:表示暂时无数据可读,可以继续读,或者等待epoll或select的后续通知。(EAGAINE,EWOULDBLOCK产生的原因:可能是多进程读同一个sockfd,可能一个进程读到数据,其他进程就读取不到数据(类似惊群效应),当然单个进程也可能出现这种情况。对于这种错误,不需用close(sockfd)。可以等待select或epoll的下一次触发,继续读。),但是在简单socket中的异步模式,也就是没有使用select、poll、epoll的时候,可以通过usleep延时几微妙读几次,假如还是没有读到数据,就认为读失败或者读错误。

另外:在同步模式下,设置了timeout的话,那么在timeout时间到而没有读到数据的时候,也会返回错误,测试下来是(EWOULDBLOCK),所以,在同步模式下,读操作只要返回错误,设置了timeout(比如3秒)就是在该时间段内没有读到数据,也一样认为读失败或者读错误。

2、如果errno == EINTR :表示被中断了,可以继续读,或者等待epoll或select后续的通知。

3、errno == 其他值,读取数据失败。(此时应该close(sockfd))

同步异步模式,其实都差不多,只是同步模式下,函数会一直等待,直到有数据读到(或者超时返回错误),异步模式下,没有读到数据立即返回。

 

sned/sendto/write在同步和异步模式下的返回值以及处理:

返回值是实际发送的字符数,因为我们知道要发送的总长度,所以,如果没有发送完,我们可以继续发送。

<0: 当返回值为-1时,需要判断errno:

1、如果errno == EAGAINE或 EWOULDBLOCK,表示当前缓冲区写满,可以继续写,                      或者等待epoll或select的后续通知,一旦有缓冲区,就会触发写操作。

2、如果errno == EINTR,表示被中断了,可以继续写,或者等待epoll或select的后续通知。

3、errno == 其他值,出错了,即errno不为EAGAINE或EWOULDBLOCK或EINTR,此时应该close(sockfd)。

 

>=0: >=0且不等于要求发送的长度,应该继续send,如果等于要求发送的长度,发送完毕。

 

connect在同步和异步模式下的返回值以及处理:

同步模式下,返回0表示成功,<0错误。

在异步模式下:

=0:当返回0时,表示立即创建了socket链接,和同步模式一样。

<0:当返回-1时,需要判断errno是否是EINPROGRESS(表示当前进程正在处理),否则失败。

 

例:关于使用select或epoll监听fd是否建立链接成功,需要注意getsockopt验证,因为三次握手的第三个ACK有可能会丢失,但是客户端认为连接已经建立:

 

int ret = connect(sockfd, (struct sockaddr)&addr, len);

if(ret == 0){

//建立链接成功

}

else if(ret < 0 && errno == EINPROGRESS){ // EINPROGRESS表示正在建立链接

fd_set set;

FD_ZERO(&set);

FD_SET(sockfd,&set);  //相反的是FD_CLR(sockfd,&set)

timeout = 10;          //(超时时间设置为10毫秒)

struct timeval timeo;

timeo.tv_sec = timeout / 1000;

timeo.tv_usec = (timeout % 1000) * 1000;

 

int retval = select(sockfd + 1, NULL, &set, NULL, &timeo); //事件监听

if(retval < 0) {

//建立连接错误close(sockfd)

}

else if(retval == 0){ // 超时

//超时链接没有建立close(sockfd)

}

 

//将检测到sockfd读事件或写事件,并不能说明connect成功

if(FD_ISSET(sockfd,&set))  {

int error = 0;

socklen_t len = sizeof(error);

if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) <0)  {

//建立连接失败close(sockfd)

}

if(error != 0){ // 失败

//建立连接失败close(sockfd)

}

else{

//建立链接成功

}

}

}

else{

//出现错误 close(sockfd)

}

 

注意:这里主要是想强调当epoll或select监听到sockfd上有EPOLL_IN或EPOLL_OUT时,即读写事件时,并不能说明链接已经建立,如上面的代码。

 

 

一些函数:

 

//tcp的一些设置函数与收发函数

//设置同步异步模式(阻塞、非阻塞模式)
//参数fd:需要设置的句柄,is_nonblock: 1-设置为nonblock(非阻塞) 0-设置为block(阻塞)
void set_nonblock(int fd,int is_nonblock) {
 int flags = fcntl(fd, F_GETFL, 0); //读fd的一些参数,用F_GETFL
 if (flags == -1) {
 perror("fcntl: get fd flags error");
 exit(EXIT_FAILURE);
 }
 if(is_nonblock)
 flags |= O_NONBLOCK;
 else
 flags &= ~O_NONBLOCK;

 flags = fcntl(fd, F_SETFL, flags); //设置fd的一些参数,用F_SETFL
 if (flags == -1) {
 perror("fcntl: set fd flags error");
 exit(EXIT_FAILURE);
 }
}

//判断是否nonblock非阻塞模式
//返回值:1--nonblock非阻塞模式 0--block阻塞模式
int is_nonblock(int fd){
 int flags = fcntl(fd, F_GETFL, 0);
 if(flags == -1){
 perror("fcntl: get fd flags error");
 exit(EXIT_FAILURE);
 }
 return (flags & O_NONBLOCK)? 1 : 0;
}

//设置同步模式下的超时timeout
//参数:fd-需要设置的socket句柄,ms-毫秒数
void set_block_timeout(int fd,unsigned int ms){
 struct timeval tv_timeout;
 tv_timeout.tv_sec = ms / 1000;
 tv_timeout.tv_usec = (ms % 1000) * 1000;
 if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv_timeout, sizeof(tv_timeout)) < 0) {
 perror("setsockopt: set rcv timeout error");
 exit(EXIT_FAILURE);
 }
 if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv_timeout, sizeof(tv_timeout)) < 0) {
 perror("setsockopt: set rcv timeout error");
 exit(EXIT_FAILURE);
 }
}

(2017-11-22 www.vsppc.com)

学习笔记未经允许不得转载:PPC的C/C++和人工智能学习笔记 » linux开发(8)_网络编程(3)socket同步与异步

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址