socket之msghdr、select、setsockopt

来源:互联网 发布:局域网免费聊天软件 编辑:程序博客网 时间:2024/05/15 19:57
内容都来自网上,整理如下。

一、msghdr

msghdr其结构定义如下:
 struct msghdr {               void         *msg_name;       /* optional address */               socklen_t     msg_namelen;    /* size of address */               struct iovec *msg_iov;        /* scatter/gather array */               size_t        msg_iovlen;     /* # elements in msg_iov */               void         *msg_control;    /* ancillary data, see below */               size_t        msg_controllen; /* ancillary data buffer len */               int           msg_flags;      /* flags on received message */           }; 
结构成员可以分为四组。他们是:
套接口地址成员:msg_name与msg_namelen。
I/O向量引用:msg_iov与msg_iovlen。
附属数据缓冲区成员:msg_control与msg_controllen。
接收信息标记位:msg_flags。

在我们将这个结构分为上面的几类以后,结构看起来就不那样巨大了。

成员msg_name与msg_namelen
这些成员只有当我们的套接口是一个数据报套接口时才需要。
msg_name成员指向我们要发送或是接收信息的套接口地址。
msg_namelen指明了这个套接口地址的长度。
当调用recvmsg时,msg_name会指向一个将要接收的地址的接收区域。当调用sendmsg时,这会指向一个数据报将要发送到的目的地址。
注意,msg_name定义为一个(void *)数据类型。我们并不需要将我们的套接口地址转换为(struct sockaddr *)。

成员msg_iov与msg_iovlen
这些成员指定了我们的I/O向量数组的位置以及他包含多少项。msg_iov成员指向一个struct iovec数组。
其中struct iovec的定义
struct iovec  {    void *iov_base;     /* Pointer to data.  */   //用户sendto中的ptr就是赋值到此    size_t iov_len;     /* Length of data.  */      //用户sendto中的长度就是赋值到此  };
我们将会回忆起I/O向量指向我们的缓冲区。成员msg_iov指明了在我们的I/O向量数组中有多少元素。

成员msg_control与msg_controllen
这些成员指向了我们附属数据缓冲区并且表明了缓冲区大小。
msg_control指向附属数据缓冲区。
msg_controllen指明了缓冲区大小。

成员msg_flags
当使用recvmsg时,这个成员用于接收特定的标记位(他并不用于sendmsg)。在这个位置可以接收的标记位如下表所示:
标记位        描述
MSG_EOR        当接收到记录结尾时会设置这一位。这通常对于SOCK_SEQPACKET套接口类型十分有用。
MSG_TRUNC    这个标记位表明数据的结尾被截短,因为接收缓冲区太小不足以接收全部的数据。
MSG_CTRUNC    这个标记位表明某些控制数据(附属数据)被截短,因为缓冲区太小。
MSG_OOB        这个标记位表明接收了带外数据。
MSG_ERRQUEUE    这个标记位表明没有接收到数据,但是返回一个扩展错误。

这个属于sendmsg中struct msghdr的用法问题,用户空间的函数sendmsg是直接调用的内核中的sock_sendmsg。
int sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t size);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
这两个函数同时支持tcp和udp。

msg.msg_iovlen = 1
是指msg.msg_iov的指针指向的地方只有一个struct iovec结构体。
sendto一次只能从一个ptr发送长度为len的东西,故只用定义一个struct iovec iov[1];
msg_iovlen就是指struct iovec的个数。

例子:
如果你想用sendmsg发送ptr1 , len1 ; ptr2, len2的数据。  传统做法是将ptr1 和 ptr2的数据都拷贝到一起。
如:
char *ptr = (char*)malloc(len1 + len2);memcpy(ptr , ptr1 , len1) ;memcpy(ptr , ptr2 , len2)sendto(sockfd , ptr , len1 + len2,  0 , addr , addrlen);
现在用sendmsg就不用拷贝。如下:
struct msghdr msg;struct iovec iov[2];iov[0].iov_base = ptr1;iov[0].iov_len = len1;iov[1].iov_base = ptr2;iov[1].iov_len = len2;memset(&msg , 0 , sizeof(msg));msg.msg_name = &addr;    //tcp 时为NULLmsg.msg_namelen = sizeof(addr); //tcp时为0msg.msg_iov = iov;msg.msg_iovlen = 2 ;    //!!!!!此处为2!sendmsg(sockfd , &msg , 0);
这样就不省了拷贝数据的开锁!
理解struct msghdr中msg_iovlen的意义了吗?

二、select

int select(int maxfd + 1,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
用于确定一个或多个套接口的状态,对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息。
参数一:最大的文件描述符加1。
参数二:用于检查可读性,fd_set*readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
如果该套接口正处于监听linsten()状态,则若有连接请求到达,该套接口便被标识为可读,这样一个accept()调用保证可以无阻塞完成,对其他套接口而言,可读性意味着有排队数据供读取。
参数三:用于检查可写性,同rendset.
参数四:fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。
参数五:一个指向timeval结构的指针,用于决定select等待I/o的最长时间。如果为空将一直等待。
第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述.

如果对readfds、writefds或errorfds中任一个组类不感兴趣,可将它置为空NULL

timeval结构的定义:

struct timeval
{
long tv_sec; // seconds
long tv_usec; // microseconds
}

select返回值:
负值:select错误
正值:某些文件可读写或出错
 0:     等待超时,没有可读写或错误的文件

struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合 FD_ZERO(fd_set *),将一个给定的文件描述符加入集合之中FD_SET(int ,fd_set *),将一个给定的文件描述符从集合中删除FD_CLR(int ,fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* ),如下:
void FD_ZERO (fd_set *fdset);// clear all bits in fdset,清空集合 void FD_SET (int fd,fd_set *fdset);// turn on the bit for fd in fdset,将一个给定的文件描述符加入集合之中,即添加描述符void FD_CLR (int fd,fd_set *fdset);// turn off the bit for fd in fdset,将一个给定的文件描述符从集合中删除int    FD_ISSET(int fd,fd_set *fdset);// is the bit for fd on in fdset,检查集合中指定的文件描述符是否可以读写,若s为集合中一员,非零;否则为零。
服务器的几个主要动作如下:
     1.创建监听套接字,绑定,监听;
     2.创建工作者线程;
     3.创建一个套接字数组,用来存放当前所有活动的客户端套接字,每accept一个连接就更新一次数组;
     4.接受客户端的连接。
     这里有一点需要注意的,就是我没有重新定义FD_SETSIZE宏,所以服务器最多支持的并发连接数为64。而且,这里决不能无条件的accept,服务器应该根据当前的连接数来决定是否接受来自某个客户端的连接

工作者线程里面是一个死循环,一次循环完成的动作是:
     1.将当前所有的客户端套接字加入到读集fdread中;
     2.调用select函数;
     3.查看某个套接字是否仍然处于读集中,如果是,则接收数据。如果接收的数据长度为0,或者发生WSAECONNRESET错误,则表示客户端套接字主动关闭,这时需要将服务器中对应的套接字所绑定的资源释放掉,然后调整

我们的套接字数组(将数组中最后一个套接字挪到当前的位置上)。
     除了需要有条件接受客户端的连接外,还需要在连接数为0的情形下做特殊处理,因为如果读集中没有任何套接字,select函数会立刻返回,这将导致工作者线程成为一个毫无停顿的死循环,CPU的占用率马上达到100%。
     关系到套接字列表的操作都需要使用循环,在轮询的时候,需要遍历一次,再新的一轮开始时,将列表加入队列又需要遍历一次.也就是说,Select在工作一次时,需要至少遍历2次列表,这是它效率较低的原因之一.

3、setsockopt

int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
sockfd:标识一个套接口的描述字。
level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
optname:需设置的选项。
optval:指针,指向存放选项待设置的新值的缓冲区。
optlen:optval缓冲区长度。

1. 如果在已经处于 ESTABLISHED状态下的socket(一般由端口号和标志符区分)调用closesocket(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket:
BOOL bReuseaddr=TRUE;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof(BOOL));

2. 如果要已经处于连接状态的soket在调用closesocket后强制关闭,不经历TIME_WAIT的过程:
BOOL  bDontLinger = FALSE;
setsockopt(s,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));

3.在send(),recv()过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限:
int nNetTimeout=1000;//1秒//发送时限setsockopt(socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));//接收时限 setsockopt(socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));

4.在send()的时候,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节(异步);系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据
和接收数据量比较大,可以设置socket缓冲区,而避免了send(),recv()不断的循环收发:
// 接收缓冲区int nRecvBuf=32*1024;//设置为32Ksetsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));//发送缓冲区int nSendBuf=32*1024;//设置为32Ksetsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));

5. 如果在发送数据的时,希望不经历由系统缓冲区到socket缓冲区的拷贝而影响程序的性能:
int nZero=0;
setsockopt(socket,SOL_S0CKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));

6.同上在recv()完成上述功能(默认情况是将socket缓冲区的内容拷贝到系统缓冲区):
int nZero=0;
setsockopt(socket,SOL_S0CKET,SO_RCVBUF,(char *)&nZero,sizeof(int));

7.一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:
BOOL  bBroadcast=TRUE;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(BOOL));

8.在client连接服务器过程中,如果处于非阻塞模式下的socket在connect()的过程中可以设置connect()延时,直到accpet()被呼叫(本函数设置只有在非阻塞的过程中有显著的作用,在阻塞的函数调用中作用不大):
BOOL bConditionalAccept=TRUE;
setsockopt(s,SOL_SOCKET,SO_CONDITIONAL_ACCEPT,(const char*)&bConditionalAccept,sizeof(BOOL));

9.如果在发送数据的过程中(send()没有完成,还有数据没发送)而调用了closesocket(),以前我们一般采取的措施是"从容关闭"shutdown(s,SD_BOTH),但是数据是肯定丢失了,如何设置让程序满足具体应用的要求(即让没发完的数据发送出去后在关闭socket)?

struct linger {  u_short    l_onoff;  u_short    l_linger;};linger m_sLinger;m_sLinger.l_onoff=1;//(在closesocket()调用,但是还有数据没发送完毕的时候容许逗留)// 如果m_sLinger.l_onoff=0;则功能和2.)作用相同;m_sLinger.l_linger=5;//(容许逗留的时间为5秒)setsockopt(s,SOL_SOCKET,SO_LINGER,(const char*)&m_sLinger,sizeof(linger));
Note:1.在设置了逗留延时,用于一个非阻塞的socket是作用不大的,最好不用;
     2.如果想要程序不经历SO_LINGER需要设置SO_DONTLINGER,或者设置l_onoff=0;

10.还一个用的比较少的是在SDI或者是Dialog的程序中,可以记录socket的调试信息:
(前不久做过这个函数的测试,调式信息可以保存,包括socket建立时候的参数,采用的具体协议,以及出错的代码都可以记录下来)
BOOL bDebug=TRUE;
setsockopt(s,SOL_SOCKET,SO_DEBUG,(const char*)&bDebug,sizeof(BOOL));











0 0
原创粉丝点击