【C++那些事】关于linux中recvfrom为何获取地址为空的深层解析
问题:
在进行linux编程中,必定存在socket的相关编程操作,在获取客户端地址时由于传入地址的长度初始化异常导致无法获取到recvfrom中client的地址
具体代码如下:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
void handle_error(const char* msg)
{
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc,char* argv[])
{
int ser_sock = -1;
char message[512] = "";
struct sockaddr_in servaddr,clientaddr;
if(argc!=2)
{
printf("usage:% <port>\n", argv[0]);
handle_error("argement is error!:\n");
}
int clientlen = 0;
ser_sock = socket(PF_INET,SOCK_DGRAM,0);
if (ser_sock==-1)
{
handle_error("create socket failed:");
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons((short)atoi(argv[1]));
if (bind(ser_sock,(struct sockaddr*)&servaddr,sizeof(servaddr)))
{
handle_error("bind failed:");
}
for(int i = 0;i<10;i++)
{
clientlen = 0; ///////////////////////see here
ssize_t len=recvfrom(ser_sock, message, sizeof(message), 0, (struct sockaddr*)&clientaddr, &clientlen);
printf("recvfrom msg:%s,from :%s :%d,addrlen:%d\n",message,inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), clientlen);
sendto(ser_sock, message, len, 0, (struct sockaddr*)&clientaddr, clientlen);
}
close(ser_sock);
return 0;
}
输出结果如下:
为何输出的client地址一直为0呢,因为recvfrom函数的原因,下面分析下recvfrom函数,linux内核中关于recvfrom代码定义如下:
SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size, unsigned int, flags, struct sockaddr __user *, addr, int __user *, addr_len) { return __sys_recvfrom(fd, ubuf, size, flags, addr, addr_len); }
/*
* Receive a frame from the socket and optionally record the address of the
* sender. We verify the buffers are writable and if needed move the
* sender address from kernel to user space.
*/
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)
{
struct socket *sock;
struct iovec iov;
struct msghdr msg;
struct sockaddr_storage address;
int err, err2;
int fput_needed;
err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;
msg.msg_control = NULL;
msg.msg_controllen = 0;
/* Save some cycles and don't copy the address if not needed */
msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
/* We assume all kernel code knows the size of sockaddr_storage */
msg.msg_namelen = 0;
msg.msg_iocb = NULL;
msg.msg_flags = 0;
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
err = sock_recvmsg(sock, &msg, flags);
if (err >= 0 && addr != NULL) {
err2 = move_addr_to_user(&address,
msg.msg_namelen, addr, addr_len);
if (err2 < 0)
err = err2;
}
fput_light(sock->file, fput_needed);
out:
return err;
}
/** * move_addr_to_user - copy an address to user space * @kaddr: kernel space address * @klen: length of address in kernel * @uaddr: user space address * @ulen: pointer to user length field * * The value pointed to by ulen on entry is the buffer length available. * This is overwritten with the buffer space used. -EINVAL is returned * if an overlong buffer is specified or a negative buffer size. -EFAULT * is returned if either the buffer or the length field are not * accessible. * After copying the data up to the limit the user specifies, the true * length of the data is written over the length limit the user * specified. Zero is returned for a success. */ static int move_addr_to_user(struct sockaddr_storage *kaddr, int klen, void __user *uaddr, int __user *ulen) { int err; int len; BUG_ON(klen > sizeof(struct sockaddr_storage)); err = get_user(len, ulen); if (err) return err; if (len > klen) len = klen; if (len < 0) return -EINVAL;
///////////// see here if (len) { if (audit_sockaddr(klen, kaddr)) return -ENOMEM; if (copy_to_user(uaddr, kaddr, len)) return -EFAULT; } /* * "fromlen shall refer to the value before truncation.." * 1003.1g */ return __put_user(klen, ulen); }
根据代码分析,在用户空间和内核空间拷贝地址时,需要获取用户空间传下来的地址的长度,并根据长度决定是否需要拷贝地址的值
那么根据原始代码中clientlen是0,所以导致地址一直没有向用户空间拷贝,但是clientlen会被底层修改,所以是正确的值:16,但是由于循环后再次设置为0,所以一直无法拷贝struct sockaddr结构体,导致无法获取到用户空间的值
所以需要吧clientlen赋值为clientaddr的结构体的长度,这样就能正常从内核空间拷贝地址到用户空间了。
修改如下:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <stdlib.h> void handle_error(const char* msg) { fputs(msg, stderr); fputc('\n', stderr); exit(1); } int main(int argc,char* argv[]) { int ser_sock = -1; char message[512] = ""; struct sockaddr_in servaddr,clientaddr; if(argc!=2) { printf("usage:% <port>\n", argv[0]); handle_error("argement is error!:\n"); } int clientlen = sizeof(clientaddr); ///////////modify init with sizeof(clientaddr) ser_sock = socket(PF_INET,SOCK_DGRAM,0); if (ser_sock==-1) { handle_error("create socket failed:"); } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons((short)atoi(argv[1])); if (bind(ser_sock,(struct sockaddr*)&servaddr,sizeof(servaddr))) { handle_error("bind failed:"); } for(int i = 0;i<10;i++) { //clientlen = 0; //////modify delete ssize_t len=recvfrom(ser_sock, message, sizeof(message), 0, (struct sockaddr*)&clientaddr, &clientlen); printf("recvfrom msg:%s,from :%s :%d,addrlen:%d\n",message,inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), clientlen); sendto(ser_sock, message, len, 0, (struct sockaddr*)&clientaddr, clientlen); } close(ser_sock); return 0; }
那么就可以在第一次就能获取到客户端的地址了,运行结果如下: