Linux网络编程5——使用UDP协议实现群聊
引言
本文实现的功能类似于我之前所写的一篇博文(Linux之select系统调用_2),区别在于进程之间的通信方式有所不同。之前的文章中,我所使用的是管道,而本文我将会使用socket接口。
需求
客户端发送消息给服务器,服务器收到消息后,会转发该消息给所有客户端。
思路
1. server端维护一个链表,用于存放客户端的联系方式。结构如下:
typedef struct sockaddr_in SA ; typedef struct client_tag { SA ct_addr; struct client_tag* ct_next; }CNODE, *pcNODE;
2. 服务器创建一个socket端口,用于接收客户端发送的消息。消息类别分为:通知上线,通知下线,以及聊天信息。因为消息类别不同,我们使用结构体将客户端发送的消息进行如下封装:
#define TYPE_ON 1 #define TYPE_OFF 2 #define TYPE_CHAT 3
#define SIZE 1024
typedef struct msg_tag { int msg_type; int msg_len; /* 实际消息长度 */ char msg_buf[SIZE]; }MSG, *pMSG;
注意,服务器所创建的socket端口需要绑定自己的联系方式,以便其他客户端可以发消息(sendto函数)给服务器。
3. 服务器使用select轮询函数监听自己的socket端口。当返回值为0(轮询时间内没有客户端发消息)或者-1(收到信号,出错)时,继续轮询;当返回值为1时,说明有客户端发送消息。我们可以从recvfrom函数的传出参数中获取客户端的联系方式,此时根据收到的MSG类型,进行处理。如果MSG类型为上线,则将该客户端的联系方式加入链表;如果MSG类型为下线,则将其从链表中删除;如果MSG类型为聊天信息,则服务器将其转发给所有客户端。
代码
server端
/************************************************************************* > File Name: my_server.c > Author: KrisChou > Mail:zhoujx0219@163.com > Created Time: Fri 29 Aug 2014 04:21:51 PM CST ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #define TYPE_ON 1 #define TYPE_OFF 2 #define TYPE_CHAT 3 #define SIZE 1024 typedef struct sockaddr_in SA ; typedef struct msg_tag { int msg_type ; int msg_len; char msg_buf[SIZE] ; }MSG, *pMSG; typedef struct client_tag { SA ct_addr ; struct client_tag* ct_next ; }CNODE, *pCNODE; void msg_broadcast(int sockfd, char* msg, pCNODE phead) { int n ; while(phead) { n = sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&phead -> ct_addr, sizeof(SA) ); printf("%d: %s : %d \n", n, inet_ntoa(phead -> ct_addr.sin_addr), ntohs(phead -> ct_addr.sin_port)); phead = phead -> ct_next ; } } void list_insert(pCNODE * phead, pCNODE p) { p ->ct_next = *phead ; *phead = p ; } void list_delete(pCNODE* phead, SA* p) { pCNODE pCur, pPre ; pPre = NULL ; pCur = *phead ; while(pCur) { if(pCur -> ct_addr.sin_port == p ->sin_port && pCur ->ct_addr.sin_addr.s_addr ==p ->sin_addr.s_addr ) { break ; }else { pPre = pCur ; pCur = pCur -> ct_next ; } } if(pPre == NULL) { *phead = pCur -> ct_next ; free(pCur); pCur = NULL ; }else { pPre -> ct_next = pCur -> ct_next ; free(pCur); pCur = NULL ; } } int main(int argc, char* argv[])// EXE CONF { if(argc != 2) { printf("USAGE: EXE CONF ! \n"); exit(1); } pCNODE my_list = NULL ; /* 创建服务器socket端口 */ int fd_server ; if((fd_server = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } /* 从配置文件中读取服务器联系方式,以便绑定到socket端口 */ FILE* fp_conf ; fp_conf = fopen(argv[1], "r"); if(fp_conf == NULL) { perror("fopen"); exit(1); } char server_ip[32]=""; int server_port ; fscanf(fp_conf,"%s%d",server_ip, &server_port); fclose(fp_conf); /* 绑定服务器socket端口的联系方式 */ SA server_addr ; memset(&server_addr, 0, sizeof(SA)); server_addr.sin_family = AF_INET ; server_addr.sin_port = htons(server_port); server_addr.sin_addr.s_addr = inet_addr(server_ip); if(-1 ==bind(fd_server, (struct sockaddr*)&server_addr, sizeof(SA))) { perror("bind"); close(fd_server); exit(1); } /* 设置select参数:监听集合以及轮询时间 */ fd_set readset, readyset ; FD_ZERO(&readset); FD_ZERO(&readyset); FD_SET(fd_server, &readset); struct timeval tm ; /* 进入轮询 */ int select_ret ; while(1) { readyset = readset ; tm.tv_sec = 0 ; tm.tv_usec = 1000 ; select_ret = select(fd_server + 1, &readyset, NULL, NULL, &tm); if(select_ret == 0) { continue ; }else if(select_ret == -1) { continue ; }else if(select_ret == 1) { pCNODE pNew = (pCNODE)calloc(1, sizeof(CNODE)); int len = sizeof(SA); char info[1024]; MSG my_msg ; memset(&my_msg, 0, sizeof(MSG)); recvfrom(fd_server,&my_msg,sizeof(my_msg), 0, (struct sockaddr*)&(pNew ->ct_addr), &len); if(my_msg.msg_type == TYPE_ON)//on { list_insert(&my_list, pNew ); printf("%s:%d on! \n",inet_ntoa(pNew ->ct_addr.sin_addr), ntohs(pNew ->ct_addr.sin_port)); }else if(my_msg.msg_type == TYPE_OFF)// off { list_delete(&my_list, &(pNew -> ct_addr) ); printf("%s:%d off! \n",inet_ntoa(pNew ->ct_addr.sin_addr), ntohs(pNew ->ct_addr.sin_port)); //kris add: 当客户端通知下线时,发送一条空消息给客户端,这样可以使对方孙子的recvfrom返回值为0 //从而可以退出循环,退出进程 sendto(fd_server,"",0,0,(struct sockaddr*)&(pNew->ct_addr),sizeof(SA)); }else //send { printf("chat msg ! \n"); memset(info, 0, 1024); sprintf(info,"\tfrom %s:%5d:\n%s\n",inet_ntoa(pNew ->ct_addr.sin_addr), ntohs(pNew ->ct_addr.sin_port),my_msg.msg_buf); puts(info); msg_broadcast(fd_server, info, my_list); } } } return 0 ; }
client端
/************************************************************************* > File Name: my_client.c > Author: KrisChou > Mail:zhoujx0219@163.com > Created Time: Fri 29 Aug 2014 05:20:20 PM CST ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> typedef struct sockaddr* pSA ; typedef struct sockaddr_in SA ; #define TYPE_ON 1 #define TYPE_OFF 2 #define TYPE_CHAT 3 #define SIZE 1024 /* 将消息封装成结构体 */ typedef struct msg_tag { int msg_type ; int msg_len; char msg_buf[SIZE] ; }MSG, *pMSG; int main(int argc, char* argv[]) { if(argc != 2) { printf("USAGE: EXE CONF ! \n"); exit(1); } /* 从配置文件中读取服务器的联系方式:IP及端口号 */ FILE* fp_conf ; char server_ip[32]=""; int server_port ; fp_conf = fopen(argv[1], "r"); if(fp_conf == NULL) { perror("fopen"); exit(1); } fscanf(fp_conf,"%s%d",server_ip, &server_port); fclose(fp_conf); /* 创建客户端socket */ int fd_client ; if((fd_client = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } /* 存入服务器联系方式 */ SA server_addr ; memset(&server_addr, 0, sizeof(SA)); server_addr.sin_family = AF_INET ; server_addr.sin_port = htons(server_port); server_addr.sin_addr.s_addr = inet_addr(server_ip); /* 通知服务器上线 */ //MSG my_msg = {TYPE_ON,2,"on"} ; MSG my_msg = {TYPE_ON,0,""} ; sendto(fd_client, &my_msg, 8 + my_msg.msg_len , 0, (pSA)&server_addr, sizeof(SA)); /* 孙子进程用于接收服务器转发的消息,并显示在屏幕上 */ /* 当儿子进程fork出孙子后,立马会退出,从而被父进程(主程序)wait掉。 * 从而孙子成为孤儿进程,当其退出时资源会被init所回收。 * 实际上fork出孙子有两点原因,如下: * 一是不愿意父进程wait子进程,因为wait是阻塞函数。 * 二是如果不wait,在子进程先退的情况下父进程不能回收其资源,从而先退的子进程会成为僵尸进程。 * 现在让儿子fork出孙子后立马滚蛋,这样孙子就直接变成孤儿进程了,由init收养,并在退出时由init回收资源。 * 注意,父进程先滚蛋其实是无碍的,大不了init做儿子(孤儿进程)的爹,并由init来回收资源。但通常父进程会模拟服务器,不会退。*/ if(fork() == 0) { if(fork() == 0) { char msg_buf[1024]; // recvfrom是阻塞函数,孙子进程退出程序后,会被init回收。此处 while(memset(msg_buf, 0, 1024), recvfrom(fd_client, msg_buf, 1024, 0, NULL, NULL) > 0) { write(1,msg_buf, strlen(msg_buf)); } printf("child exit ! \n"); close(fd_client); exit(0); /* 用于说明爷爷退了之后,孙子不会被一起带走。 sleep(5); while(1) { printf("hahahahaha\n"); } */ } close(fd_client); exit(0); } wait(NULL); /* 从键盘输入消息,发送给服务器,按ctrl+D退出循环 */ while(memset(&my_msg, 0, sizeof(MSG)), fgets(my_msg.msg_buf, SIZE, stdin) != NULL) { my_msg.msg_type = TYPE_CHAT ; my_msg.msg_len = strlen(my_msg.msg_buf); sendto(fd_client, &my_msg, 8 + my_msg.msg_len , 0, (pSA)&server_addr, sizeof(SA)); } /* 向服务器发送离线消息 */ my_msg.msg_type = TYPE_OFF ; my_msg.msg_len = 0 ; sendto(fd_client, &my_msg, 8 + my_msg.msg_len , 0, (pSA)&server_addr, sizeof(SA)); close(fd_client); return 0 ; /*注意:只要主进程滚蛋了,马上会显示出shell界面。但是此时,fork出来的进程可能并没有结束。 *fork出来的进程先滚蛋,是不会显示shell界面的。只有主进程滚蛋,才会显示出shell界面。 */ }
配置文件
192.168.0.180 8888
注意
程序运行时,先启动服务器端,不然客户端没法连接。