已经工作了接近一年的时间,工作之余也只能看看书,了解一下相关的技术细节,在网络设备公司不可避免的要和socket打交道,但通常都是调用公司封装好的接口,没有去考虑这些封装背后的工作,回过头来看真的觉得进步很小,我只能逼自己看看书,看看一些好的代码。
sendmsg和recvmsg这两个接口是高级套接口,这两个接口支持一般数据的发送和接收,还支持多缓冲区的报文发送和接收(readv和sendv支持多缓冲区发送和接收),还可以在报文中带辅助数据。这些功能是常用的send、recv等接口无法完成的。
接口的声明如下:
#include < sys / socket . h >
ssize_t recvmsg ( int sockfd , struct msghdr * msg , int flags ) ;
ssize_t sendmsg ( int sockfd , struct msghdr * msg , int flags ) ;
上述接口的参数分别是套接字描述符,消息的头部,已经对应的标识,这些标识主要用于对该套接口进行设置,如:MSG_DONTWAI(将本操作设置为非阻塞模式),MSG_OOB(发送或接受带外数据)等。
struct msghdr {
void * msg_name ; / * 消息的协议地址 * /
socklen_t msg_namelen ; / * 地址的长度 * /
struct iovec * msg_iov ; / * 多io缓冲区的地址 * /
int msg_iovlen ; / * 缓冲区的个数 * /
void * msg_control ; / * 辅助数据的地址 * /
socklen_t msg_controllen ; / * 辅助数据的长度 * /
int msg_flags ; / * 接收消息的标识 * /
} ;
接下来的两个成员是关于接受和发送数据的的。其中的strcut iovec是io向量,如下所示:
struct iovec {
void * io_base ; / * buffer空间的基地址 * /
size_t iov_len ; / * 该buffer空间的长度 * /
} ;
多缓冲区的发送和接收处理就是一个struct iovec的数组,每个成员的io_base都指向了不同的buffer的地址。io_len是指该buffer中的数据长度。而在struct msghdr中的msg_iovlen是指buffer缓冲区的个数,即iovec数组的长度。
msg_control字段的也是指向一段内存,msg_controllen是指该内存的总大小长度,通常该内存被用来存储辅助数据,辅助数据可用于一些特殊的处理。msg_control通常指向一个控制消息头部,其结构体如下所示:
struct cmsghdr {
socklen_t cmsg_len ; / * 包含该头部的数据长度 * /
int cmsg_level ; / * 具体的协议标识 * /
int cmsg_type ; / * 协议中的类型 * /
} ;
其中的cmsg_level主要包含IPPROTO_IP ( ipv ) , IPPROTO_IPV6 ( ipv6 ) , SOL_SOCKET ( unix domain ) .
其中的cmsg_type是根据上述的类型有分别有不同的内容,比如SOL_SOCKET中主要包含:SCM_RIGHTS(发送接收描述字), SCM_CREDS(发送接收用户凭证)
关于辅助数据,在recvmsg的msg_control中可能携带多个cmsghdr, 可以采用对应的宏协助处理: CMSG_FIRSTHDR(), CMSG_NXTHDR(), CMSG_DATA(), CMSG_LEN(), CMSG_SPACE().
最后的字段是msg_flags,该字段主要用于在接收消息的过程中用来获取消息的相关属性。
接下来采用进程间传递描述字的例子来说明上述函数的使用。
描述字的传递,就是将一个进程中的描述字传递到另一个进程中,使得该描述字依然有效,也就是使得在一个进程中的描述字传递到另一个描述字依然有效。
在多进程的网络CS模式下,服务器fork产生的子进程在fork调用返回后,子进程共享父进程的所有打开的描述字。即使在子进程中调用exec函数,所有描述字通常还是保持打开的状态,也就是描述字是跨exec函数的。这也是为什么在exec只有的子进程仍然可以调用父进程共享的套接字的原因。
但是这种实现并不能解决子进程的描述字传递给父进程的需求,对于无亲缘关系的进程之间传递描述字就更加不可能,在unix中的实现是:在两个进程之间创建一个unix domain socket套接口,然后调用sendmsg跨这个套接口发送一个特殊的消息,该消息由内核进行特殊的处理,从而把打开的描述字从发送进程传递到接收进程(采用recvmsg接收)。分析主要包含如下几个过程:
(1) 创建一个字节流或者数据报的unix domain socket套接口,
1、在父子进程之间可采用socketpair实现流管道,类似于pipe的实现,两个进程中分别有一个socket套接口
2、在无亲缘关系的进程之间采用基本的domain socket过程(服务器Bind, listen, accpet返回的套接口, 客户端connect返回的套接口),将两个进程关联起来。
(2) 发送进程通过调用返回描述字的任一unix函数打开一个描述字(也就是返回fd类型的函数打开一个描述字)。
(3) 发送进程创建一个msghdr结构,其中将待传递的描述字作为辅助数据发送,调用sendmsg跨越(1)中获得的套接口发送描述字。在发送完成以后,在发送进程即使关闭该描述字也不会影响接收进程的描述符,发送一个描述字导致该描述字的引用计数加1。
(4) 接收进程调用recvmsg在(1)中获取的套接口上接收该描述字,该描述字在接收进程中的描述字号不同于在发送进程中的描述字号是正常的,也就是说如果在发送进程中描述字号是20,而在接收进程中对应的描述字号可能被使用,该进程会分配一个不一样的描述字号(如open对同一个文件进行多次打开,每一次的fd返回值都不一样是一个道理),此时就是说两个进程同时指向一个描述字。
注意:在发送过程中由于没有报文,在接收的过程中会分不清是文件已经结束还是只是发送了辅助数据,因此通常在发送辅助数据的时候会传输至少一个字节的数据,该数据在接收过程中不做任何的处理。
服务器端接收客户端发送过来的描述字:
服务器端接收客户端发送过来的描述字:
#include "unp.h"
int main ( int argc , char * argv [ ] )
{
int clifd , listenfd ;
struct sockaddr_un servaddr , cliaddr ;
int ret ;
socklen_t clilen ;
struct msghdr msg ;
struct iovec iov [ 1 ] ;
char buf [ 100 ] ;
char * testmsg = "test msg.\n" ;
union {
struct cmsghdr cm ;
char control [ CMSG_SPACE ( sizeof ( int ) ) ] ;
} control_un ;
struct cmsghdr * pcmsg ;
int recvfd ;
listenfd = socket ( AF_UNIX , SOCK_STREAM , 0 ) ;
if ( listenfd < 0 ) {
printf ( "socket failed.\n" ) ;
return - 1 ;
}
unlink ( UNIXSTR_PATH ) ;
bzero ( & servaddr , sizeof ( servaddr ) ) ;
servaddr . sun_family = AF_UNIX ;
strcpy ( servaddr . sun_path , UNIXSTR_PATH ) ;
ret = bind ( listenfd , ( SA * ) & servaddr , sizeof ( servaddr ) ) ;
if ( ret < 0 ) {
printf ( "bind failed. errno = %d.\n" , errno ) ;
close ( listenfd ) ;
return - 1 ;
}
listen ( listenfd , 5 ) ;
while ( 1 ) {
clilen = sizeof ( cliaddr ) ;
clifd = accept ( listenfd , ( SA * ) & cliaddr , & clilen ) ;
if ( clifd < 0 ) {
printf ( "accept failed.\n" ) ;
continue ;
}
msg . msg_name = NULL ;
msg . msg_namelen = 0 ;
iov [ 0 ] . iov_base = buf ;
iov [ 0 ] . iov_len = 100 ;
msg . msg_iov = iov ;
msg . msg_iovlen = 1 ;
msg . msg_control = control_un . control ;
msg . msg_controllen = sizeof ( control_un . control ) ;
ret = recvmsg ( clifd , & msg , 0 ) ;
if ( ret < = 0 ) {
return ret ;
}
if ( ( pcmsg = CMSG_FIRSTHDR ( & msg ) ) ! = NULL & & ( pcmsg - > cmsg_len = = CMSG_LEN ( sizeof ( int ) ) ) ) {
if ( pcmsg - > cmsg_level ! = SOL_SOCKET ) {
printf ( "cmsg_leval is not SOL_SOCKET\n" ) ;
continue ;
}
if ( pcmsg - > cmsg_type ! = SCM_RIGHTS ) {
printf ( "cmsg_type is not SCM_RIGHTS" ) ;
continue ;
}
recvfd = * ( ( int * ) CMSG_DATA ( pcmsg ) ) ;
printf ( "recv fd = %d\n" , recvfd ) ;
write ( recvfd , testmsg , strlen ( testmsg ) + 1 ) ;
}
}
return 0 ;
}
#include "unp.h"
#define OPEN_FILE "test"
int main ( int argc , char * argv [ ] )
{
int clifd ;
struct sockaddr_un servaddr ;
int ret ;
struct msghdr msg ;
struct iovec iov [ 1 ] ;
char buf [ 100 ] ;
union {
struct cmsghdr cm ;
char control [ CMSG_SPACE ( sizeof ( int ) ) ] ;
} control_un ;
struct cmsghdr * pcmsg ;
int fd ;
clifd = socket ( AF_UNIX , SOCK_STREAM , 0 ) ;
if ( clifd < 0 ) {
printf ( "socket failed.\n" ) ;
return - 1 ;
}
fd = open ( OPEN_FILE , O_CREAT | O_RDWR , 0777 ) ;
if ( fd < 0 ) {
printf ( "open test failed.\n" ) ;
return - 1 ;
}
bzero ( & servaddr , sizeof ( servaddr ) ) ;
servaddr . sun_family = AF_UNIX ;
strcpy ( servaddr . sun_path , UNIXSTR_PATH ) ;
ret = connect ( clifd , ( SA * ) & servaddr , sizeof ( servaddr ) ) ;
if ( ret < 0 ) {
printf ( "connect failed.\n" ) ;
return 0 ;
}
msg . msg_name = NULL ;
msg . msg_namelen = 0 ;
iov [ 0 ] . iov_base = buf ;
iov [ 0 ] . iov_len = 100 ;
msg . msg_iov = iov ;
msg . msg_iovlen = 1 ;
msg . msg_control = control_un . control ;
msg . msg_controllen = sizeof ( control_un . control ) ;
pcmsg = CMSG_FIRSTHDR ( & msg ) ;
pcmsg - > cmsg_len = CMSG_LEN ( sizeof ( int ) ) ;
pcmsg - > cmsg_level = SOL_SOCKET ;
pcmsg - > cmsg_type = SCM_RIGHTS ;
* ( ( int * ) CMSG_DATA ( pcmsg ) ) = = fd ;
ret = sendmsg ( clifd , & msg , 0 ) ;
printf ( "ret = %d.\n" , ret ) ;
return 0 ;
}
上面的程序主要实现了两个进程(无亲缘关系)之间传递描述符的实现,主要使用了sendmsg和recvmsg的强大功能,利用了辅助数据来传递描述字(内核经过特殊处理的消息)。
关于这两个函数的另一个应用是获取认证相关的处理,也是通过辅助数据的方式将用户的认证数据发送给对应的客户端。需要将cmsg->cmsg_type = SCM_CREDS。其余的处理都差不多。