IP 组播

先澄清一下几个概念:IP 组播, UDP组播, UDP多播。后两者从字面上就可以看出是同一个东西,但是前两者之间就比较难以区分了,首先看最前面的字母,IP, UDP,明显不是一回事,但是其实他们是一回事。到底怎么理解呢?那让我们先看看IP 组播是怎么一回事。

我们引用一段英文解释:

IP multicast is a technique for one-to-many and many-to-many real-time communication over an IP infrastructure in a network. It scales to a larger receiver population by not requiring prior knowledge of who or how many receivers there are. Multicast uses network infrastructure efficiently by requiring the source to send a packet only once, even if it needs to be delivered to a large number of receivers. The nodes in the network (typically network switches and routers) take care of replicating the packet to reach multiple receivers such that messages are sent over each link of the network only once. The most common low-level protocol to use multicast addressing is User Datagram Protocol (UDP).

上面这段文件引自:http://en.wikipedia.org/wiki/IP_multicast

 

大概的意思是: IP 组播是基于网络IP架构,可以进行一对多,多对多的实时通信的一种技术。可以在事先不清楚到底有哪些参与者,或者参与者的数量的情况下,调整接受者规模。IP 组播可以只发送一次,即使需要接受数据的客户端数量很大,通过这种方式非常有效地使用了网络。网络节点(典型的如网络交换机,路由器)负责复制那些需要发送给多个接受者的网络包。通常用于组播的底层网络协议是数据报协议(UDP).

 

我想通过上面这段文字,你已经知道前两个也是指同一个东西了。

 

我们接着看看组播能给我们带来什么好处。可能通常看到的和自己写的代码里面用比较比较多是unicast(单播)也就是一对一,如果比如说我们平时浏览网页是涉及到的http协议(tcp unicast),文件传输ftp协议(tcp unicast)。考虑这样一种情况:如果某个服务器要传输一段视频或者音频给100个客户端,也许你可以用udp unicast(为什么不用tcp unicast, 你可以自己考虑),最后程序写出来了,发现能工作,100个客户端都接收到了数据。但是你发现第一收到数据的人最后一个收到的数据机器的时间间隔实在太大了,可能超过5秒了,并且发现服务器上面的带宽被吃掉了很多。你无法忍受,你的老板和客户也无法接受这样的结果。 那么这时候你就需要组播来做了,这两个问题都解决了(其实,这两个问题解决的同时,可能还会有另外一个问题,丢包率上升了,如果是有线的网络,可能影响不大,但是如果是无线网络可能就会很惨,怎么样解决这个问题超出本篇的范围,感兴趣的可以自己试试)。

 

接下来说说组播的一个缺点: 因为我们一般都是基于udp来实现IP mulicast,因此这种实现就会有udp的一些特性,乱序和丢包。

当然IP multicast也可以变成可靠的,目前可供参看的有 Pragmatic General Multicast (PGM)。

 

接下来从代码的层面上看看IP multicast.

既然上面说了,一般的实现是基于udp,所以先看看udp的单播的客户端和服务端是怎么实现的。

服务端:

// init

 WSADATA wsaData;
 WORD wVersion = MAKEWORD( VERSIONHIGH, VERSIONLOW );
::WSAStartup( wVersion, &wsaData );

 

// create UDP socket

  m_socketServer = ::socket( AF_INET, SOCK_DGRAM, 0 );
  if ( INVALID_SOCKET == pCom->m_socketServer )
  {
   return INTONE;
  }
  
  // bind socket to port
  sockaddr_in sin;
  sin.sin_family = AF_INET;
  sin.sin_port = htons( pCom->m_uPort );
  sin.sin_addr.s_addr = INADDR_ANY;
  if ( ::bind( pCom->m_socketServer, ( sockaddr* )&sin, sizeof( sin )))
  {

   // error handler
  }

 

while ( true )

{

     // receive data

     int nRet = ::recvfrom( m_socketServer,
         m_pRecvBuf,
      RECVFUFFERSIZE,
      0,
      (struct sockaddr*)pClient,
      &nClientSize );

      …

      …

}

WSACleanup();

 

客户端

 

// init

 WSADATA wsaData;
 WORD wVersion = MAKEWORD( VERSIONHIGH, VERSIONLOW );
::WSAStartup( wVersion, &wsaData );

 

// create UDP socket

  m_socketServer = ::socket( AF_INET, SOCK_DGRAM, 0 );
  if ( INVALID_SOCKET == pCom->m_socketServer )
  {
   return false;
  }

int nSend = sendto( socketClient, szClientReq, 64, 0,
  ( sockaddr* )&addrServer, sizeof( sockaddr_in ));
 if ( SOCKET_ERROR == nSend )
 {
  cout<<“failed to send ACK package to server”<<endl;
 }

 …

 …

WSACleanup();

 

可以看出来服务端大致流程:

初始化socket库

创建数据报套接字

绑定端口

接受数据

清理socket库。

 

客户端大致流程:

初始化socket库

创建数据报套接字

发送数据

清理socket库。

我们留意一下客户端的发送,就会发现发送数据前,一定要知道对方(这里就是我们的服务器)的地址:IP 地址,端口号。这样我们就确定的唯一的一个发送对象,但是如果我们把这个地址换成一个组播地址呢,那会怎样呢?结果就是将数据发送给已经在这个组内注册的所有成员。好现在我们改一下sendto:

 m_saUdpServ.sin_family = AF_INET;
 
 m_saUdpServ.sin_port = htons(6666);
 
 m_saUdpServ.sin_addr.s_addr = inet_addr( (“233.1.1.1”);

 if ( SOCKET_ERROR == ::sendto( m_socketServer,
  ( const char* )&packLogin,
  sizeof( LoginHeader_t ),
  INTZERO,
  (SOCKADDR  *)&m_saUdpServ,
  sizeof( SOCKADDR_IN )))

{}

先不要细究233.1.1.1是怎么一回事,后面我们会提到。

这样我们就把数据发送给了一个组。那我们的服务端就能收到发送的数据吗?答案是不可以,因为服务端还是不是我们发送组里的成员。那接下来让服务器加入这个组:

 m_saUdpServ.sin_family = AF_INET;
 
 m_saUdpServ.sin_port = htons(6666);
 
 m_saUdpServ.sin_addr.s_addr = inet_addr(“233.1.1.1”);
 
 if(( m_socketMulti = WSAJoinLeaf( m_socketServer, (SOCKADDR*)&m_saUdpServ,
  
  sizeof(m_saUdpServ),NULL,NULL,NULL,NULL,
  
  JL_BOTH)) == INVALID_SOCKET)
  
 {
  
  printf(“WSAJoinLeaf() failed:%d/n”,WSAGetLastError());
  
  closesocket( m_socketServer );
  
  WSACleanup();
  
  return FALSE;
  

注意这是windows下面加入组的方式(因为WSA开头的socket函数只有windows下面有),linux加入组的方式不一样的:

 

/* use setsockopt() to request that the kernel join a multicast group */
     mreq.imr_multiaddr.s_addr=inet_addr(HELLO_GROUP);
     mreq.imr_interface.s_addr=htonl(INADDR_ANY);
     if (setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq)) < 0) {
  perror(“setsockopt”);
  exit(1);
     }

 }

 

这时候组播的实现已完成,服务端和客户端都可以正常的接受数据了。

 

回过头来看看233.1.1.1这个字符串,其实它就是组播中很重要的一个东西,组播地址,或者说组播的IP地址,这个IP地址全球没有任何一个机构和个人可以使用它,所以它不可能和任何几个IP地址冲突。组播地址是一个范围: 224.0.0.0 - 224.0.0.255

 

还有一点需要注意: bind时用的端口一定要和WSAJoinLeaf时使用的端口一致,否则,是接受不到发给组的数据报的,我曾经有一次就犯了这个错误,导致死活都接受到对方发给组的数据。

 

我们理一下流程。

服务端:

初始化socket库

创建数据报套接字

绑定端口

加入组播组

接受数据

清理socket库。

 

客户端大致流程:

初始化socket库

创建数据报套接字

发送数据

清理socket库。

 

可以看出虽然客户端没有加入组播组,但是仍然可以给服务端发数据,只要我们在sendto的参数中指定目标地址为服务端加入的组播组地址就可以。因此我们得出结论: 给某个组播组发数据不需要加入这个组。当然如果你除了给这个组发数据外,还想从这个组接受数据就必须加入这个组。

 

参看文献:

 

http://ntrg.cs.tcd.ie/undergrad/4ba2/multicast/antony/

 

http://ntrg.cs.tcd.ie/undergrad/4ba2/multicast/antony/example.html#sender

 

http://en.wikipedia.org/wiki/IP_multicast

 

http://baike.baidu.com/view/534094.htm

 

http://www.ietf.org/rfc/rfc3170.txt

 

版权所有,禁止转载. 如需转载,请先征得博主的同意,并且表明文章出处,否则按侵权处理.

    分享到:

Leave a Reply

Your email address will not be published. Required fields are marked *