利用WinSock的组播(多播)实现局域网一对多聊天程序

五一本来是个玩耍的好日子,结果撂下一大堆事情没完成,果然程序员就是命短。一周三更新的计划没完成,好吧这周就得六篇更新了,这里记一下WinSock下编程的一个局域网聊天和发文件的程序,本来以为代码没保存呢,结果同学U盘里有一份,那就把以前做的一些实验拿来充充数咯。

导航目录

  1. 基本程序结构和思路
  2. 局域网网络发现,使用组播
  3. 文件传输

程序实现思路

实验室用的是Win7+visualstudio2008,记得网络实验要求是使用WinSock编写一个使用TCP及UDP的局域网工具,其具有网络发现,单人聊天,群聊和一对一文件发送功能。

tcp和udp连接只要有对方ip就好弄,但是这个网络发现怎么实现。首先想到的就是局域网的广播(目标IP为255.255.255.255即是向局域所有主机发送数据报),一个客户端打开时向局域网指定端口发送广播新用户加入的消息,其他客户端收到该端口广播消息就回应一个消息,这样就实现了局域网的网络发现,群聊也可以这样做,这个可以有!因为需要广播,所以网络发现和消息传输就得用UDP实现了。

第一次写WinSock,开始网上查各种广播实现的资料,实现很简单,将套接字绑定广播IPSO_BROADCAST,将setsockopt第三个参数设为SO_BROADCAST

setsockopt(socketMC, IPPROTO_IP, SO_BROADCAST, (char*)&mreq, sizeof(struct ip_mreq));

搜索中又发现有的类似程序是用组播(多播)实现的,组播这个概念理论课时还只是听懂了一半,果断复习一下:不考虑底层协议实现,简单了说就是一台主机加入一个组播IP后,之后所有向该组播IP发送的额数据报都会发送到该IP,也就是说你向一个组播IP发送一个数据包,加入了该组播IP的所有主机都能收到该数据包。
IP组播协议中,可用的组播地址是一组D类地址,范围从224.0.0.0 到 239.255.255.255。224.0.0.0到224.0.0.255的地址大多是为了特殊的目的保持的,比如IGMP协议,所以最好不要用,其他随便选择吧。是不是感觉组播用来做聊天室特别合适,下面是一些使用方法。

setsockopt()函数可以设置套接字选项,通过传入以下参数实现组播一对多的数据报传输:
IP_ADD_MEMBERSHIP //加入指定的多点广播组。

IP_DROP_MEMBERSHIP //离开指定的多点广播组。

IP_MULTICAST_IF //设置通过其发送出局多点广播数据报的接口。

IP_MULTICAST_TTL //在 IP 头中设置出局多点广播数据报的“有效时间”(TTL)。

IP_MULTICAST_LOOP //指定当发送主机是多点广播组的成员时,是否将出局多点广播数据报的副本传送至发送主机。 

组播(多播)实现一对多传输及网络发现

代码里需要做的,就是把socket和本地的一个端口绑定,通过setsockopt函数 IP_ADD_MEMBERSHIP加入一个组播组,然后就能通过sendto / recvfrom进行数据的收发,话说记得当时把组播地址弄错了一位数,结果调式了大半天功夫。。。

初始化socket并加入IP为234.0.12.34的组播网络:

UDPMulticast(){
    if(socketMC == INVALID_SOCKET){
        return 0;
    }
    int ret;
    sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(MCPORT);
    local.sin_addr.S_un.S_addr = INADDR_ANY;
    memset(local.sin_zero, 0, 8);
    ret = bind(socketMC, (struct sockaddr *)&local, sizeof(local)); //绑定地址
    if(ret==SOCKET_ERROR) return FALSE;

    ret = ::WSAAsyncSelect(socketMC,m_hWnd,WM_SOCKET_MC,FD_READ|FD_CLOSE);  //设置异步模式
    if(ret==SOCKET_ERROR) return FALSE;

/**加入组播**/
    ip_mreq mreq;
    memset(&mreq, 0, sizeof(struct ip_mreq));
    mreq.imr_multiaddr.S_un.S_addr = inet_addr("234.0.12.34");    //组播源地址
    mreq.imr_interface.S_un.S_addr = inet_addr(inet_ntoa(*(struct in_addr*)pHost->h_addr_list[0]));       //本地地址
    int m = setsockopt(socketMC, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mreq, sizeof(struct ip_mreq));
    if (m == SOCKET_ERROR) return FALSE;

/**!在 IP 头中设置出局多点广播数据报的“有效时间”(TTL)**/
    int optval = 8;
    setsockopt(socketMC, IPPROTO_IP, IP_MULTICAST_TTL, (char*)&optval, sizeof(int));

/**!指定当发送主机是多点广播组的成员时,是否将出局多点广播数据报的副本传送至发送主机**/
    int loop = 0;
    setsockopt(socketMC, IPPROTO_IP, IP_MULTICAST_LOOP, (char*)&loop, sizeof(int));

    remote.sin_addr.S_un.S_addr = inet_addr("234.0.12.34");  //组播地址
    remote.sin_family = AF_INET;  
    remote.sin_port = htons(MCPORT);  
    memset(remote.sin_zero,0,8);
    //!!!关闭时调用,离开组播
/**setsockopt(sock,IPPROTO_IP,IP_DROP_MEMBERSHIP,(char*)&mcast,sizeof(mcast));  **/
/**closesocket(sock);**/
    return 1;
}

代码里是程序启动时发送一个J字符到组播网络中,其他客户端收到为J的消息时返回一个R字符给新加入的主机,并将该主机IP添加到列表中,所以写一个发送单个字符向指定组播地址的函数:

//发送组播消息
BOOL MyDlg::SendMC(char c){
    char buff[2] = { c, '\0' };     //组播网络发现标识
    return sendto(socketMC, buff, 2, 0, (struct sockaddr*)&remote, sizeof(remote))==2?TRUE:FALSE;
}

其他的MFC界面和聊天消息处理等等就不说了,详细代码可以看源文件,一些Win32开发细节可以查看《Windows程序设计》.pdf

实现文件发送

这个模块是交给同组同学做的咯,所以wo也没什么好说的,原来在想发送文件时新建一个线程的,后来新建一个新线程窗口类出现各种问题,结果实验课提交截止前没弄出来,还是留给以后有时间弄了。

新建一个TCP服务器端的socket函数:

 CreateFServer(char * file_name){
    sockaddr_in server_addr; 
    server_addr.sin_family = AF_INET; 
    server_addr.sin_addr.S_un.S_addr = INADDR_ANY; 
    server_addr.sin_port = htons(FILEPORT);

    // 创建socket 
    SOCKET m_Socket = socket(AF_INET, SOCK_STREAM, 0); 
    if (SOCKET_ERROR == m_Socket) 
    { 
        ::AfxMessageBox("Create Socket Error!"); //closesocket(c_Socket); 
                return 0;
    } 

    //绑定socket和服务端(本地)地址 
    if (SOCKET_ERROR == bind(m_Socket, (LPSOCKADDR)&server_addr, sizeof(server_addr))) 
    { 
        ::AfxMessageBox("Server Bind Failed: "+ WSAGetLastError()); 
        return 0;
    } 

    //监听 
    if (SOCKET_ERROR == listen(m_Socket, 10)) 
    { 
        ::AfxMessageBox("Server Listen Failed: "+ WSAGetLastError());
                closesocket(m_Socket); 
                return 0;
    } 

        ::AfxMessageBox("点确定开始传送..\n"); 

        sockaddr_in client_addr; 
        int client_addr_len = sizeof(client_addr); 

        m_Socket = accept(m_Socket, (sockaddr *)&client_addr, &client_addr_len); 
        if (SOCKET_ERROR == m_Socket) 
        { 
            ::AfxMessageBox("Server Accept Failed: "+ WSAGetLastError()); closesocket(m_Socket); 
                return 0;
        } 

        char buffer[BUFFER_SIZE];

        memset(buffer, 0, BUFFER_SIZE); 


        FILE * fp = fopen(file_name, "rb"); //windows下是"rb",表示打开一个只读的二进制文件 
        if (NULL == fp) 
        { 
            ::AfxMessageBox("File:  Not Found\n"); 
        } 
        else
        { 
            memset(buffer, 0, BUFFER_SIZE); 
            int length = 0; 

            while ((length = fread(buffer, sizeof(char), BUFFER_SIZE, fp)) > 0) 
            { 
                if (send(m_Socket, buffer, length, 0) < 0) 
                { 
                    ::AfxMessageBox("Send File:  Failed\n");
                    closesocket(m_Socket); 
                return 0;
                } 
                memset(buffer, 0, BUFFER_SIZE); 
            } 

            fclose(fp); 
            closesocket(m_Socket); 
        }

        ::AfxMessageBox("File:  Transfer Successful!\n"); 
    return 1;
}

WeTalk.png

总的来说程序实现过程还是很简单的,想想当时才刚学Windows编程没几天,MFC结构都不清楚,摸索了好几天才把程序弄出来,这里推荐下当时看的Windows编程的书《Windows程序设计》,下面是程序界面和代码。

详细可以参考程序源代码:Wetalk

标签: WinSock, win32

添加新评论