C语言网络编程实现广播、组播及本地通信

一、广播(broadcast)

1.概念

单播:前几篇博客介绍的数据包发送方式只有一个接受方,称为单播

广播:如果同时发给局域网中的所有主机,称为广播

组播:如果只发给局域网内的部分主机,称为组播

同一个sockfd 只能处理一种接收方式,如果即想发单播又想发广播,还想发组播,需要三个sockfd。只有用户数据报(使用UDP协议)套接字才能广播。

2.广播地址

以192.168.1.0 (255.255.255.0) 网段为例,最大的主机地址192.168.1.255代表该网段的广播地址(具体以ifconfig 命令查看到的 broadcast 后面的为准)发到该地址的数据包被所有的主机接收。

注:255.255.255.255在所有网段中都代表广播地址。

广播能发给局域网所有主机的原理

因为广播的数据包比较特殊,他的目的mac地址全是f(ff:ff:ff:ff:ff:ff) 这个数据包会发给交换机,交换机是工作在链路层的,交换机看到这样目的mac全是f的数据包,就会将该数据包发给局域网内的所有主机。到达主机后,进行拆包,看到目的mac是广播的mac,则允许通过。到达网络层一看IP地址是广播的IP地址,则可以通过。到达传输层,只要端口号匹配,则数据就能到达应用层。

广播的应用:ARP请求,通过ip地址获取对方的mac地址,使用的就是广播。

3.广播的流程

发送者:

        创建套接字 socket( )

        设置为允许发送广播权限 setsockopt( )

        填充广播信息结构体 sockaddr_in

        发送数据 sendto( )

接收者:

        创建套接字 socket( )

        填充广播信息结构体 sockaddr_in

        将套接字与广播信息结构体绑定 bind( )

        接收数据 recvfrom( )

getsockopt函数说明:

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
功能:设置允许发送广播

#include
#include
参数:
        @sockfd: socket函数返回的套接字
        @level:SOL_SOCKET 套接字级别
        @optname:SO_BROADCAST 允许发送广播
        @optval:int on = 1
        @optlen:sizeof(on)
返回值:成功返回0,失败返回-1,置位错误码

示例:
int on = 1;
if(-1 == setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on))){
        ERRLOG("setsockopt error");
}

4.代码实现

//接收端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
                perror(errmsg);\
                printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
                exit(-1);\
            }while(0)

int main(int argc, const char *argv[]){
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }
    //1.创建用户数据报式套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    //创建服务器广播信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    //端口号随便定  ip地址需要是  广播的ip地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t addrlen = sizeof(server_addr);

    //3.将套接字和广播信息结构体进行绑定
    if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
        ERRLOG("bind error");
    }


    //定义一个结构体,保存客户端的信息
    struct sockaddr_in client_addr;
    memset(&server_addr, 0, sizeof(client_addr));//清空
    socklen_t clientaddrlen = sizeof(client_addr);

    char buff[N] = {0};

    while(1){
        //接收数据,如果想要给对方回应,就必须保存对方的网络信息结构体
        //如果不回应,后两个参数写 NULL 也行
        if(-1 == recvfrom(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, &clientaddrlen)){
            ERRLOG("recvfrom error");
        }
        printf("%s(%d):%s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);
        
        #if 0
        //组装应答信息
        strcat(buff, "--sever");
        if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, clientaddrlen)){
            ERRLOG("sendto error");
        }
        #endif
        memset(buff, 0, N);
    }
    
    //关闭套接字
    close(sockfd);

    return 0;
}
//发送端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
                perror(errmsg);\
                printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
                exit(-1);\
            }while(0)

int main(int argc, const char *argv[]){
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }
    //1.创建用户数据报套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    //设置套接字允许发送广播  setsockopt
    int on = 1;
    if(-1 == setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on))){
        ERRLOG("setsockopt error");
    }

    //填充服务器广播信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    //广播的ip地址
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t addrlen = sizeof(server_addr);

    char buff[N] = {0};
    while(1){
        printf("input your msg:");
        fgets(buff, N, stdin);
        buff[strlen(buff)-1] = '\0';//清除 \n
        if(0 == strcmp(buff, "quit")){
            break;
        }
        if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&server_addr, addrlen)){
            ERRLOG("sendto error");
        }
        #if 0
        //因为客户端已经知道服务器的网络信息了 所以后两个参数可以传NULL
        if(-1 == recvfrom(sockfd, buff, N, 0, NULL, NULL)){
            ERRLOG("recvfrom error");
        }
        printf("recv:[%s]\n", buff);
        #endif
        memset(buff, 0, N);
    }
    //关闭套接字
    close(sockfd);

    return 0;
}

二、组播(Multicast)

1.概念

单播方式只能发给一个接收方。广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信。组播(又称为多播)是一种折中的方式。只有加入某个多播组的主机才能收到数据。

使用SOCK_DGRAM多播方式既可以发给多个主机,又能避免像广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)

2.组播的地址

D类地址(组播地址)

不分网络地址和主机地址,第1字节的前4位固定为1110(224.0.0.1 – 239.255.255.255)

3.组播的流程

发送者:

        创建套接字 socket( )

        填充组播信息结构体 sockaddr_in

        发送数据 sendto( )

接收者:

        创建套接字 scoket( )

        填充组播信息结构体 sockaddr_in

        将套接字与组播信息结构体绑定 bind( )

        设置为加入多播组 setsockopt( )

        接收数据 recvfrom( )

setsockopt函数说明:

int setsockopt(int sockfd, int level, int optname, void *optval, socklen_t optlen);
功能:设置套接字加入多播组

#include
#include
参数:
        @sockfd: socket函数返回的套接字
        @level:IPPROTO_IP IP级别
        @optname:IP_ADD_MEMBERSHIP 选项名 设置加入多播组
        @optval:
              struct ip_mreqn {
                      struct in_addr imr_multiaddr;   /* 多播组的地址 */
                      struct in_addr imr_address;     /* 本地地址 */
                      int                   imr_ifindex;       /* 接口索引 我们基本不用 设置0即可 */
              };
              //或者使用不带n的结构体
              struct ip_mreq {
                      struct in_addr imr_multiaddr;   /* 多播组的地址 */
                      struct in_addr imr_address;     /* 本地地址 */
              };
        @optlen:optval的大小
返回值:成功返回0,失败返回-1,置位错误码

4.代码实现

//接收端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
                perror(errmsg);\
                printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
                exit(-1);\
            }while(0)

int main(int argc, const char *argv[]){
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }
    //1.创建用户数据报式套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    //创建服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    //多播组的ip地址  224.0.0.1 – 239.255.255.255
    server_addr.sin_addr.s_addr = inet_addr("224.0.0.1");
    socklen_t addrlen = sizeof(server_addr);

    //3.将套接字和广播信息结构体进行绑定
    if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
        ERRLOG("bind error");
    }

    //设置加入多播组
    struct ip_mreq my_mreq;
    memset(&my_mreq, 0, sizeof(my_mreq));
    my_mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1");
    my_mreq.imr_interface.s_addr = inet_addr(argv[1]);

    if(-1 == setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &my_mreq, sizeof(my_mreq))){
        ERRLOG("setsockopt error");
    }

    //定义一个结构体,保存客户端的信息
    struct sockaddr_in client_addr;
    memset(&server_addr, 0, sizeof(client_addr));//清空
    socklen_t clientaddrlen = sizeof(client_addr);

    char buff[N] = {0};

    while(1){
        //接收数据,如果想要给对方回应,就必须保存对方的网络信息结构体
        //如果不回应,后两个参数写 NULL 也行
        if(-1 == recvfrom(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, &clientaddrlen)){
            ERRLOG("recvfrom error");
        }
        printf("%s(%d):%s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff);
        
        #if 0
        //组装应答信息
        strcat(buff, "--sever");
        if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&client_addr, clientaddrlen)){
            ERRLOG("sendto error");
        }
        #endif
        memset(buff, 0, N);
    }
    
    //关闭套接字
    close(sockfd);

    return 0;
}
//发送端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define N 128

#define ERRLOG(errmsg) do{\
                perror(errmsg);\
                printf("%s--%s(%d)\n", __FILE__, __func__, __LINE__);\
                exit(-1);\
            }while(0)

int main(int argc, const char *argv[]){
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }
    //1.创建用户数据报套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    //填充服务器组播信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    //多播组的ip地址  224.0.0.1 – 239.255.255.255
    //这个范围内的每个ip地址 都表示一个多播组
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t addrlen = sizeof(server_addr);

    char buff[N] = {0};
    while(1){
        printf("input your msg:");
        fgets(buff, N, stdin);
        buff[strlen(buff)-1] = '\0';//清除 \n
        if(0 == strcmp(buff, "quit")){
            break;
        }
        if(-1 == sendto(sockfd, buff, N, 0, (struct sockaddr *)&server_addr, addrlen)){
            ERRLOG("sendto error");
        }
        memset(buff, 0, N);
    }
    //关闭套接字
    close(sockfd);

    return 0;
}

注:如果接收不到组播消息:

        先执行一下命令: sudo ifconfig 网卡名 allmulti

        网卡名可以使用 ifconfig命令查出来

三、本地通信

1.概念

socket最早起也是用于本地通信的,后来有了TCP/IP协议族的的加入,才可以实现不同主机的进程间通信。本地通信和其他进程间通信方式相比使用方便、效率更高,多用于前后台进程通信。

创建套接字时使用本地协议AF_UNIX(或AF_LOCAL)。分为流式套接字和用户数据报套接字 ----区别还是有无连接。

2.本地通信使用的结构体

struct sockaddr_un {
        sa_family_t sun_family;   /* AF_UNIX */
        char sun_path[108];         /* 带路径的文件名 */
};

3.TCP本地通信流程及代码实现

服务器:

        创建套接字 socket( )

        填充服务器本地信息结构体 sockaddr_un

        将套接字与服务器本地信息结构体绑定 bind( )

        将套接字设置为被动监听状态 listen( )

        阻塞等待客户端的连接请求 accept( )

        进行通信 recv( )/send( ) 或 read( )/write( )

客户端:

        创建套接字 socket( )

        填充服务器本地信息结构体 sockaddr_un

        发送客户端的连接请求 connect( )

        进行通信 send( )/recv( )

代码实现:

//服务器端
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ERRLOG(errmsg) do{\
			perror(errmsg);\
			printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
			exit(-1);\
		}while(0)

int main(int argc, char *argv[]){
	if(2!=argc){
		printf("Usage : %s \n", argv[0]);
		exit(-1);
	}
	//1.创建套接字
	int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
	if(-1 == sockfd){
		ERRLOG("socket error");
	}

	//2.填充服务器本地信息结构体
	struct sockaddr_un server_addr;
	memset(&server_addr, 0, sizeof(server_addr));//清空
	server_addr.sun_family = AF_UNIX;
	//注意:sun_path是一个数组  不能使用 等号赋值
	strcpy(server_addr.sun_path, argv[1]);
	socklen_t addrlen = sizeof(server_addr);

	//3.将套接字和网络信息结构体进行绑定
	if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
		ERRLOG("bind error");
	}

	//4.将服务器的套接字设置成被动监听状态
	if(-1 == listen(sockfd, 5)){
		ERRLOG("listen error");
	}

	//定义一个结构体,保存客户端的信息
	struct sockaddr_un client_addr;
	memset(&client_addr, 0, sizeof(client_addr));//清空
	socklen_t clientaddrlen = sizeof(client_addr);

	char buff[128] = {0};
	int acceptfd = 0;
	int bytes = 0;
	while(1){
		//5.阻塞等待客户端连接
		acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen);
		if(-1 == acceptfd){
			ERRLOG("accept error");
		}
		printf("客户端 [%s] 连接到服务器了\n", client_addr.sun_path);
		
		while(1){
			//6.与客户端通信
			if(0 > (bytes = recv(acceptfd, buff, 128, 0))){
				ERRLOG("recv error");
			}else if(bytes == 0){
				printf("客户端 [%s] 断开了连接\n", client_addr.sun_path);
				break;
			}else{
				if(0 == strcmp(buff, "quit")){
					printf("客户端 [%s] 退出了\n", client_addr.sun_path);
					break;
				}
				printf("客户端 [%s] 发来数据:[%s]\n", client_addr.sun_path, buff);
				//组装应答
				strcat(buff, "--sever");
				if(-1 == send(acceptfd, buff, 128, 0)){
					ERRLOG("send error");
				}
			}
		}
		//7.关闭套接字
		close(acceptfd);
	}
	close(sockfd);

	return 0;
}
//客户端
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ERRLOG(errmsg) do{\
			perror(errmsg);\
			printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
			exit(-1);\
		}while(0)

int main(int argc, char *argv[]){
	if(2!=argc){
		printf("Usage : %s \n", argv[0]);
		exit(-1);
	}
    //1.创建套接字
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    struct sockaddr_un server_addr;
	memset(&server_addr, 0, sizeof(server_addr));//清空
	server_addr.sun_family = AF_UNIX;
	//注意:sun_path是一个数组  不能使用 等号赋值
	strcpy(server_addr.sun_path, argv[1]);
	socklen_t addrlen = sizeof(server_addr);
   
   	//定义一个结构体,保存客户端的信息
	struct sockaddr_un client_addr;
	memset(&client_addr, 0, sizeof(client_addr));//清空
    client_addr.sun_family = AF_UNIX;
    strcpy(client_addr.sun_path,"tcp_client_sock_file");
	socklen_t clientaddrlen = sizeof(client_addr);

    if(-1 == bind(sockfd, (struct sockaddr *)&client_addr, clientaddrlen)){
		ERRLOG("bind error");
	}
    
    //3.与服务器建立连接
    if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){
        ERRLOG("connect error");
    }

    //4.与服务器通信
    char buff[128] = {0};
	while(1){
        fgets(buff, 128, stdin);
        buff[strlen(buff)-1] = '\0';//清除 \n
        if(-1 == send(sockfd, buff, 128, 0)){
            ERRLOG("send error");
        }
        if(0 == strcmp(buff, "quit")){
            break;
        }
        if(-1 == recv(sockfd, buff, 128, 0)){
            ERRLOG("recv error");
        }
        printf("收到回复:[%s]\n", buff);
	}

    //5.关闭套接字
    close(sockfd);

    return 0;
}

4.UDP本地通信流程及代码实现

服务器:

        创建套接字 socket( ) SOCK_DGRAM

        填充服务器本地信息结构体 sockaddr_un

        将套接字与服务器本地信息结构体绑定 bind( )

        进行通信 recvfrom( )/sendto( )

客户端:

        创建套接字 socket( )

        填充客户端本地信息结构体 sockaddr_un

        将套接字与客户端本地信息结构体绑定 bind( ) //要绑定 否则服务器无法给客户端回信

        填充服务器本地信息结构体 sockaddr_un

        进行通信 sendto( ) / recvfrom( )

代码实现:

//服务器端
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ERRLOG(errmsg) do{\
                        perror(errmsg);\
                        printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
                        exit(-1);\
                }while(0)

int main(int argc, char *argv[]){
        if(2!=argc){
                printf("Usage : %s \n", argv[0]);
                exit(-1);
        }
        //1.创建套接字
        int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
        if(-1 == sockfd){
                ERRLOG("socket error");
        }

        //2.填充服务器本地信息结构体
        struct sockaddr_un server_addr;
        memset(&server_addr, 0, sizeof(server_addr));//清空
        server_addr.sun_family = AF_UNIX;
        //注意:sun_path是一个数组  不能使用 等号赋值
        strcpy(server_addr.sun_path, argv[1]);
        socklen_t addrlen = sizeof(server_addr);

        //3.将套接字和网络信息结构体进行绑定
        if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){
                ERRLOG("bind error");
        }

        //定义一个结构体,保存客户端的信息
        //给发送者回信 需要用到
        struct sockaddr_un client_addr;
        memset(&client_addr, 0, sizeof(client_addr));//清空
        socklen_t clientaddrlen = sizeof(client_addr);

        char buff[128] = {0};
        int bytes = 0;
        while(1){
                //6.与客户端通信
                if(0 > (bytes = recvfrom(sockfd, buff, 128, 0, (struct sockaddr *)&client_addr, &clientaddrlen))){
                        ERRLOG("recvfrom error");
                }else{
                        printf("客户端 [%s] 发来数据:[%s]\n", client_addr.sun_path, buff);
                        //组装应答
                        strcat(buff, "--sever");
                        if(-1 == sendto(sockfd, buff, 128, 0, (struct sockaddr *)&client_addr, clientaddrlen)){
                                ERRLOG("sendto error");
                        }
                }
        }
        close(sockfd);

        return 0;
}
//客户端
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ERRLOG(errmsg) do{\
                        perror(errmsg);\
                        printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
                        exit(-1);\
                }while(0)

int main(int argc, char *argv[]){
        if(3!=argc){
                printf("Usage : %s  \n", argv[0]);
                exit(-1);
        }
    //1.创建套接字
    int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if(-1 == sockfd){
        ERRLOG("socket error");
    }

    //服务器的本地信息结构体
    struct sockaddr_un server_addr;
        memset(&server_addr, 0, sizeof(server_addr));//清空
        server_addr.sun_family = AF_UNIX;
        //注意:sun_path是一个数组  不能使用 等号赋值
        strcpy(server_addr.sun_path, argv[1]);
        socklen_t addrlen = sizeof(server_addr);
   
        //定义一个结构体,保存自己的信息
        struct sockaddr_un client_addr;
        memset(&client_addr, 0, sizeof(client_addr));//清空
    client_addr.sun_family = AF_UNIX;
    strcpy(client_addr.sun_path, argv[2]);
        socklen_t clientaddrlen = sizeof(client_addr);

    if(-1 == bind(sockfd, (struct sockaddr *)&client_addr, clientaddrlen)){
                ERRLOG("bind error");
        }

    //4.与服务器通信
    char buff[128] = {0};
        while(1){
        fgets(buff, 128, stdin);
        buff[strlen(buff)-1] = '\0';//清除 \n
        if(-1 == sendto(sockfd, buff, 128, 0, (struct sockaddr *)&server_addr, addrlen)){
            ERRLOG("sendto error");
        }
        //无需再保存服务器的 本地信息结构体了 因为server_addr我们没修改过
        if(-1 == recvfrom(sockfd, buff, 128, 0, NULL, NULL)){
            ERRLOG("recvfrom error");
        }
        printf("收到回复:[%s]\n", buff);
        }

    //5.关闭套接字
    close(sockfd);

    return 0;
}

至此,“网络编程”  专栏的全部知识讲完了,下面的小项目检测一下学习成果,建议先试着自己写,不会的部分再看下面的代码。

注:接下来会更新Python专栏,喜欢的我的博客可以点赞关注,持续更新中。

四、练习:UDP群聊聊天室

1.功能

①有新用户登录,其他在线的用户可以收到登录信息

②有用户群聊,其他在线的用户可以收到群聊信息

③有用户退出,其他在线的用户可以收到退出信息

④服务器可以发送系统信息

提示:

①客户端登录之后,为了实现一边发送数据一边接收数据,可以使用多进程或者多线程

②服务器既可以发送系统信息,又可以接收客户端信息并处理,可以使用多进程或者多线程

③服务器需要给多个用户发送数据,所以需要保存每一个用户的信息,使用链表来保存

④数据传输的时候要定义结构体,结构体中包含操作码、用户名以及数据

2.写项目的流程

①画流程图

②根据流程图写框架

③一个功能一个功能实现

流程图:

C语言网络编程实现广播、组播及本地通信_第1张图片

3.功能实现

//服务器端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ERRLOG(errmsg) do{\
                        perror(errmsg);\
                        printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
                        exit(-1);\
                }while(0)

typedef struct __MSG{
    char code;
    char name[32];
    char text[128];
}msg_t;

typedef struct __NODE{
    struct sockaddr_in clientaddr;
    struct __NODE *next;
}node_t;

void do_login(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr);
void do_chat(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr);
void do_quit(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr);
void create_node(node_t **pnew);

int main(int argc, char *argv[]){
    //对入参合理性进行检查
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }
    //创建数据报式套接字
    int sockfd = 0;
    if(-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0))){
        ERRLOG("socket error");
    }

    //填充服务器的网络信息结构体
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t serveraddr_len = sizeof(serveraddr);

    //绑定套接字与服务器网络信息结构体
    if(-1 == bind(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)){
        ERRLOG("bind error");
    }

    //创建子进程
    pid_t pid = 0;
    if(-1 == (pid = fork())){
        ERRLOG("fork error");
    }else if(0 == pid){//子进程逻辑
        //创建保存客户端网络信息结构体的链表的头结点
        node_t *phead = NULL;
        create_node(&phead);

        //定义结构体保存客户端的信息
        struct sockaddr_in clientaddr;
        memset(&clientaddr, 0, sizeof(clientaddr));
        socklen_t clientaddr_len = sizeof(clientaddr);

        msg_t msg;
        while(1){
            memset(&msg, 0, sizeof(msg));
            if(-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&clientaddr, &clientaddr_len)){
                ERRLOG("recvfrom error");
            }
            //根据操作码不同,做不同的操作
            switch(msg.code){
                case 'L':
                    do_login(sockfd, phead, msg, clientaddr);
                    break;
                case 'C':
                    do_chat(sockfd, phead, msg, clientaddr);
                    break;
                case 'Q':
                    do_quit(sockfd, phead, msg, clientaddr);
                    break;
            }
        }
        close(sockfd);
    }else if(0 < pid){
        //父进程逻辑
        //发送系统消息:把父进程当做客户端给子进程发送群聊消息
        msg_t msg;
        while(1){
            memset(&msg, 0, sizeof(msg));
            msg.code = 'C';
            strcpy(msg.name, "server");
            fgets(msg.text, 128, stdin);
            msg.text[strlen(msg.text)-1] = '\0';
            if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, serveraddr_len)){
                ERRLOG("sendto error");
            }
        }
        close(sockfd);
    }
    return 0;
}

void create_node(node_t **pnew){
    *pnew = (node_t *)malloc(sizeof(node_t));
    memset((*pnew), 0, sizeof(node_t));
}

//登录操作
void do_login(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr){
    //将xx加入群聊的消息发给链表的所有节点
    node_t *ptemp = phead;
    while(ptemp->next != NULL){
        ptemp = ptemp->next;
        if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->clientaddr), sizeof(ptemp->clientaddr))){
            ERRLOG("sendto error");
        }
    }
    //将新客户端的网络信息结构体插入链表--头插法
    node_t *pnew = NULL;
    create_node(&pnew);
    pnew->clientaddr = clientaddr;
    pnew->next = phead->next;
    phead->next = pnew;
}

//群聊操作
void do_chat(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr){
    node_t *ptemp = phead;
    while(ptemp->next != NULL){
        ptemp = ptemp->next;
        if(memcmp(&(ptemp->clientaddr), &clientaddr, sizeof(clientaddr))){
            //进到这里说明 不是自己
            if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->clientaddr), sizeof(ptemp->clientaddr))){
                ERRLOG("sendto error");
            }
        }    
    }
}

//退出操作
void do_quit(int sockfd, node_t * phead, msg_t msg, struct sockaddr_in clientaddr){
    //先将要突出的客户端网络信息结构体在链表中删除
    node_t *ptemp = phead;
    node_t *pdel = NULL;
    while(ptemp->next != NULL){
        if(!memcmp(&(ptemp->next->clientaddr), &clientaddr, sizeof(clientaddr))){
            //说明是自己
            pdel = ptemp->next;
            ptemp->next = pdel->next;
            free(pdel);
            pdel = NULL;
        }else{
            if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->next->clientaddr), sizeof(ptemp->next->clientaddr))){
                ERRLOG("sendto error");
            }
            ptemp = ptemp->next;
        }
    }
}
//客户端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ERRLOG(errmsg) do{\
                        perror(errmsg);\
                        printf("%s-%s(%d)\n", __FILE__, __func__, __LINE__);\
                        exit(-1);\
                }while(0)

typedef struct __MSG{
    char code;
    char name[32];
    char text[128];
}msg_t;

int main(int argc, char *argv[]){
    //对入参合理性进行检查
    if(3 != argc){
        printf("Usage : %s  \n", argv[0]);
        exit(-1);
    }
    //创建数据报式套接字
    int sockfd = 0;
    if(-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0))){
        ERRLOG("socket error");
    }

    //填充服务器的网络信息结构体
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t serveraddr_len = sizeof(serveraddr);

    //先发送登录消息
    msg_t msg;
    memset(&msg, 0, sizeof(msg));
    msg.code = 'L';
    printf("请输入用户名:");
    fgets(msg.name, 32, stdin);
    msg.name[strlen(msg.name)-1] = '\0';
    strcpy(msg.text, "加入了群聊");

    if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, serveraddr_len)){
        ERRLOG("sendto error");
    }

    //创建子进程
    pid_t pid = 0;
    if(-1 == (pid = fork())){
        ERRLOG("fork error");
    }else if(0 == pid){//子进程
        while(1){
            memset(&msg, 0, sizeof(msg));
            if(-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL)){
                ERRLOG("recvfrom error");
            }
            printf("%s : %s\n", msg.name, msg.text);
        }
    }else if(0 < pid){//父进程
        while(1){
            memset(msg.text, 0, sizeof(msg.text));
            fgets(msg.text, 128, stdin);
            msg.text[strlen(msg.text)-1] = '\0';
            if(!strcmp(msg.text, "quit")){
                msg.code = 'Q';
                strcpy(msg.text,"退出群聊");
            }else{
                msg.code = 'C';
            }
            if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, serveraddr_len)){
                ERRLOG("sendto error");
            }
            if(!strcmp(msg.text, "退出群聊")){
                break;
            }
        }
        //让子进程退出
        kill(pid, SIGKILL);
        wait(NULL);
        //关闭套接字
        close(sockfd);
    }

    return 0;
}

你可能感兴趣的:(网络编程,网络,udp,tcp,c语言,linux)