在前一篇中,我们提到在对端主机上没有创建指定的UDP套接字时,我们向其发送一个UDP包,会得到一个目的端口不可达的ICMP出错报文。但内核在处理完该报文后,给应用程序仅仅返回一个ECONNREFUSED错误号,所以应用程序能知道的全部信息就是连接被拒绝,至于为什么被拒绝,没有办法知道。我们可以通过套接字选项的设置,让内核返回更为详细的出错信息,以利于调试程序,发现问题。下面是通过套接字选项传递扩展出错信息的一个示例程序。关于内核原理的分析,在下一篇给出。
#include <sys/socket.h>
#include <linux/types.h>
#include <linux/errqueue.h>
#include <sys/ioctl.h>
#include "my_inet.h"
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
int ip_control_msg( struct cmsghdr *msg )
{
int ret = 0;
switch( msg->cmsg_type ){
case IP_RECVERR:
{
struct sock_extended_err *exterr;
exterr = (struct sock_extended_err *)(CMSG_DATA(msg));
printf("ee_errno: %u\n", exterr->ee_errno );
printf("ee_origin: %u\n", exterr->ee_origin );
printf("ee_type: %u\n", exterr->ee_type );
printf("ee_code: %u\n", exterr->ee_code );
printf("ee_pad: %u\n", exterr->ee_pad );
printf("ee_info: %u\n", exterr->ee_info );
printf("ee_data: %u\n", exterr->ee_data );
}
ret = -1;
break;
default:
break;
}
return ret;
}
int control_msg( struct msghdr *msg )
{
int ret = 0;
struct cmsghdr *control_msg = CMSG_FIRSTHDR( msg );
while( control_msg != NULL ){
switch( control_msg->cmsg_level ){
case SOL_IP:
ret = ip_control_msg( control_msg );
break;
default:
break;
}
control_msg = CMSG_NXTHDR( msg, control_msg );
}
return ret;
}
int main()
{
int i;
struct sockaddr_in dest;
dest.sin_family = MY_PF_INET;
dest.sin_port = htons(16000);
dest.sin_addr.s_addr = 0x013010AC;
int fd = socket( MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP );
if( fd < 0 ){
perror("socket: ");
return -1;
}
if( connect( fd, (struct sockaddr*)&dest, sizeof(dest) ) < 0 ){
perror("connect: ");
return -1;
}
int val = 1;
if( setsockopt( fd, SOL_IP, IP_RECVERR, &val, sizeof(val) ) == -1 ){
perror("setsockopt: ");
return -1;
}
int bwrite = send( fd, "abcdefg", 7, 0 );
if( bwrite == -1 ){
perror("send: ");
return -1;
}
char buf[1024];
char control_buf[1024];
struct msghdr msg;
struct iovec iov = { buf, 1024 };
memset( &msg, 0, sizeof(msg) );
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = &control_buf;
msg.msg_controllen = 1024;
int bread = recvmsg( fd, &msg, MSG_ERRQUEUE );
if( bread == -1 ){
perror("recv: ");
return -1;
}
if( control_msg( &msg ) >= 0 )
printf("successed!\n");
else
printf("failed!\n");
close( fd );
return 0;
}
执行结果:
ee_errno: 111 //ECONNREFUSED
ee_origin: 2 //SO_EE_ORIGIN_ICMP
ee_type: 3 //目的不可达
ee_code: 3 //端口不可达
ee_pad: 0
ee_info: 0
ee_data: 0
failed!
我们来看这个应用程序背后,内核真正做了一些什么事情。
代表MY_INET域套接字的结构体struct inet_sock有一个成员recverr,它占1bit长度,可能的取值是1或0,当为0时表示socket上出错时,只通过系统调用向应用程序返回错误号,不提供进一步的详细信息。当取值为1时,则表示socket上出错时,则向struct inet_sock的成员sk_error_queue(一个sk_buff的队列)存入一个特殊的struct sk_buff,在sk_buff的成员cb中放入详细的错误信息,应用程序通过特定的系统调用可以取得详细的出错信息。
recverr的值可以通过套接字选项操作进行设置,它是一个IP层的选项,对应的选项名是IP_RECVERR。下面的代码就是将它的值设为1(打开选项):
int val = 1;
if( setsockopt( fd, SOL_IP, IP_RECVERR, &val, sizeof(val) ) == -1 )
;//deal with error
当打开了这个选项后,我们在该socket上发送UDP数据报,按照前面文章提及的测试环境运行,172.16.48.2继续会收到ICMP目的不可达报文,在差错数据报处理时,会达到函数myudp_err,该函数会设置socket的成员sk_err,同时,它也会检查recverr成员,如果为1,则要在sk_error_queue队列中放入一个特殊的出错信息sk_buff。该sk_buff保留了出错的那个源UDP数据报,同时在它的cb成员中保存了一个结构体struct sock_exterr_skb,该结构体记录了详细的出错信息,下面是其定义:
struct sock_exterr_skb
{
union {
struct inet_skb_parm h4;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct inet6_skb_parm h6;
#endif
} header;
struct sock_extended_err ee;
u16 addr_offset;
u16 port;
};
addr_offset和port是出错UDP数据报的地址和端口号,ee的定义如下:
struct sock_extended_err
{
__u32 ee_errno; //错误号。
__u8 ee_origin; //产生错误的源,我们的环境下,产生错误的源为一个ICMP包。
__u8 ee_type; //ICMP类型。
__u8 ee_code; //ICMP代码。
__u8 ee_pad;
__u32 ee_info; //用于EMSGSIZE时找到的MTU。
__u32 ee_data;
};
我们保存了出错信息,应用程序要取得这个出错信息,必须使用特定的系统调用,recvmsg可以获得详细的出错信息,同时,调用接口上必须使用标志MSG_ERRQUEUE表示取错误队列,下面是recvmsg的定义:
ssize_t recvmsg(int s, struct msghdr *msg, int flags);
flags置MSG_ERRQUEUE,msg结构控制信息成员msg_control和msg_controllen需要分配一个缓存,用于辅助信息的传递。关于接收,可以查看前面一篇的源代码和man recvmsg,这里不再重复。
#include <sys/socket.h>
#include <linux/types.h>
#include <linux/errqueue.h>
#include <sys/ioctl.h>
#include "my_inet.h"
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
int ip_control_msg( struct cmsghdr *msg )
{
int ret = 0;
switch( msg->cmsg_type ){
case IP_RECVERR:
{
struct sock_extended_err *exterr;
exterr = (struct sock_extended_err *)(CMSG_DATA(msg));
printf("ee_errno: %u\n", exterr->ee_errno );
printf("ee_origin: %u\n", exterr->ee_origin );
printf("ee_type: %u\n", exterr->ee_type );
printf("ee_code: %u\n", exterr->ee_code );
printf("ee_pad: %u\n", exterr->ee_pad );
printf("ee_info: %u\n", exterr->ee_info );
printf("ee_data: %u\n", exterr->ee_data );
}
ret = -1;
break;
default:
break;
}
return ret;
}
int control_msg( struct msghdr *msg )
{
int ret = 0;
struct cmsghdr *control_msg = CMSG_FIRSTHDR( msg );
while( control_msg != NULL ){
switch( control_msg->cmsg_level ){
case SOL_IP:
ret = ip_control_msg( control_msg );
break;
default:
break;
}
control_msg = CMSG_NXTHDR( msg, control_msg );
}
return ret;
}
int main()
{
int i;
struct sockaddr_in dest;
dest.sin_family = MY_PF_INET;
dest.sin_port = htons(16000);
dest.sin_addr.s_addr = 0x013010AC;
int fd = socket( MY_PF_INET, SOCK_DGRAM, MY_IPPROTO_UDP );
if( fd < 0 ){
perror("socket: ");
return -1;
}
if( connect( fd, (struct sockaddr*)&dest, sizeof(dest) ) < 0 ){
perror("connect: ");
return -1;
}
int val = 1;
if( setsockopt( fd, SOL_IP, IP_RECVERR, &val, sizeof(val) ) == -1 ){
perror("setsockopt: ");
return -1;
}
int bwrite = send( fd, "abcdefg", 7, 0 );
if( bwrite == -1 ){
perror("send: ");
return -1;
}
char buf[1024];
char control_buf[1024];
struct msghdr msg;
struct iovec iov = { buf, 1024 };
memset( &msg, 0, sizeof(msg) );
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = &control_buf;
msg.msg_controllen = 1024;
int bread = recvmsg( fd, &msg, MSG_ERRQUEUE );
if( bread == -1 ){
perror("recv: ");
return -1;
}
if( control_msg( &msg ) >= 0 )
printf("successed!\n");
else
printf("failed!\n");
close( fd );
return 0;
}
执行结果:
ee_errno: 111 //ECONNREFUSED
ee_origin: 2 //SO_EE_ORIGIN_ICMP
ee_type: 3 //目的不可达
ee_code: 3 //端口不可达
ee_pad: 0
ee_info: 0
ee_data: 0
failed!
我们来看这个应用程序背后,内核真正做了一些什么事情。
代表MY_INET域套接字的结构体struct inet_sock有一个成员recverr,它占1bit长度,可能的取值是1或0,当为0时表示socket上出错时,只通过系统调用向应用程序返回错误号,不提供进一步的详细信息。当取值为1时,则表示socket上出错时,则向struct inet_sock的成员sk_error_queue(一个sk_buff的队列)存入一个特殊的struct sk_buff,在sk_buff的成员cb中放入详细的错误信息,应用程序通过特定的系统调用可以取得详细的出错信息。
recverr的值可以通过套接字选项操作进行设置,它是一个IP层的选项,对应的选项名是IP_RECVERR。下面的代码就是将它的值设为1(打开选项):
int val = 1;
if( setsockopt( fd, SOL_IP, IP_RECVERR, &val, sizeof(val) ) == -1 )
;//deal with error
当打开了这个选项后,我们在该socket上发送UDP数据报,按照前面文章提及的测试环境运行,172.16.48.2继续会收到ICMP目的不可达报文,在差错数据报处理时,会达到函数myudp_err,该函数会设置socket的成员sk_err,同时,它也会检查recverr成员,如果为1,则要在sk_error_queue队列中放入一个特殊的出错信息sk_buff。该sk_buff保留了出错的那个源UDP数据报,同时在它的cb成员中保存了一个结构体struct sock_exterr_skb,该结构体记录了详细的出错信息,下面是其定义:
struct sock_exterr_skb
{
union {
struct inet_skb_parm h4;
#if defined(CONFIG_IPV6) || defined (CONFIG_IPV6_MODULE)
struct inet6_skb_parm h6;
#endif
} header;
struct sock_extended_err ee;
u16 addr_offset;
u16 port;
};
addr_offset和port是出错UDP数据报的地址和端口号,ee的定义如下:
struct sock_extended_err
{
__u32 ee_errno; //错误号。
__u8 ee_origin; //产生错误的源,我们的环境下,产生错误的源为一个ICMP包。
__u8 ee_type; //ICMP类型。
__u8 ee_code; //ICMP代码。
__u8 ee_pad;
__u32 ee_info; //用于EMSGSIZE时找到的MTU。
__u32 ee_data;
};
我们保存了出错信息,应用程序要取得这个出错信息,必须使用特定的系统调用,recvmsg可以获得详细的出错信息,同时,调用接口上必须使用标志MSG_ERRQUEUE表示取错误队列,下面是recvmsg的定义:
ssize_t recvmsg(int s, struct msghdr *msg, int flags);
flags置MSG_ERRQUEUE,msg结构控制信息成员msg_control和msg_controllen需要分配一个缓存,用于辅助信息的传递。关于接收,可以查看前面一篇的源代码和man recvmsg,这里不再重复。