功能
有新用户登录,其他在线的用户可以收到登录信息
有用户群聊,其他在线的用户可以收到群聊信息
有用户退出,其他在线的用户可以收到退出信息
服务器可以发送系统信息
流程图如下:
提示:
客户端登录之后,为了实现一边发送数据一边接收数据,可以使用多进程或者多线程
服务器既可以发送系统信息,又可以接收客户端信息并处理,可以使用多进程或者多线程
服务器需要给多个用户发送数据,所以需要保存每一个用户的信息,使用链表来保存数据传输的时候要定义结构体,结构体中包含操作码、用户名以及数据。
代码如下:
服务器代码
//服务器代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>#define ERRLOG(msg) \do { \printf("%s %s %d:", __FILE__, __func__, __LINE__); \perror(msg); \exit(-1); \} while (0)typedef struct _MSG {char code; //操作码 ('L' 登录),('C' 群聊),('Q' 退出)char name[32];char txt[128];
} msg_t;
typedef struct _NODE {struct sockaddr_in clientaddr; //客户端网络信息结构体struct _NODE* next; //指针域
} node_t;int create_node(node_t **p);
int do_login(struct sockaddr_in clientaddr, node_t* phead, int sockfd, msg_t msg);
int do_chat(struct sockaddr_in clientaddr, node_t* phead, int sockfd, msg_t msg);
int do_quit(struct sockaddr_in clientaddr, node_t* phead, int sockfd, msg_t msg);
int main(int argc, char const* argv[])
{//入参合理性检查if (3 != argc) {printf("Usage : %s <IP> <PORT>\n", argv[0]);return -1;}// 1.创建套接字int sockfd = 0;if (-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0))) {ERRLOG("socket error");}// 2.填充服务器网络信息结构体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");}//用来保存客户端网络信息结构体struct sockaddr_in clientaddr;socklen_t clientaddr_len = sizeof(clientaddr);msg_t msg;pid_t pid = 0;if (-1 == (pid = fork())) {ERRLOG("fork error");} else if (0 == pid) {//子进程,用来接收数据并处理//创建用来保存客户端网络信息结构体的链表node_t *phead = NULL;create_node(&phead);phead->next =NULL;printf("聊天室已创建,等待用户加入群聊!!!\n");while(1) {memset(&msg, 0, sizeof(msg));memset(&clientaddr, 0, sizeof(clientaddr));if (-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&clientaddr, &clientaddr_len)) {ERRLOG("recvfrom error");}printf("[%s]: [%s]\n", msg.name, msg.txt);switch (msg.code) {case 'L':do_login(clientaddr, phead, sockfd,msg);break;case 'C':do_chat(clientaddr, phead, sockfd, msg);break;case 'Q':do_quit(clientaddr, phead, sockfd, msg);break;}}} else if (0 < pid) {//父进程,用来发送系统消息//把父进程当作一个客户端,以群聊的方式发送消息给所有人(系统消息)msg.code = 'C';strcpy(msg.name, "系统");while (1){fgets(msg.txt, 128, stdin);msg.txt[strlen(msg.txt) - 1] = '\0';if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, serveraddr_len)) {ERRLOG("sendto error");}}}close(sockfd);return 0;
}
//创建链表节点的函数
int create_node(node_t** p)
{*p = (node_t*)malloc(sizeof(node_t));if (NULL == *p || NULL == p) {ERRLOG("malloc error");}
}
//登录的函数
int do_login(struct sockaddr_in clientaddr, node_t* phead, int sockfd, msg_t msg)
{//先遍历链表,将xxxx登录的消息 转发给所有人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;return 0;
}//群聊消息的函数
int do_chat(struct sockaddr_in clientaddr, node_t* phead, int sockfd, msg_t msg)
{//遍历链表,将群聊的数据发送给除自己外的所有人node_t* ptemp = phead;while (ptemp->next != NULL) {ptemp = ptemp->next;if (0 != memcmp(&clientaddr, &(ptemp->clientaddr), sizeof(clientaddr))) {if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, ((struct sockaddr*)&(ptemp->clientaddr)), sizeof(ptemp->clientaddr))) {ERRLOG("sendto error");}}}
}int do_quit(struct sockaddr_in clientaddr, node_t* phead, int sockfd, msg_t msg)
{//将xxxx退出群聊的消息发送给除自己外所有的所有客户端,并且将自己在链表中删除node_t* ptemp = phead;while (ptemp->next != NULL) {if (0 != memcmp(&clientaddr, &(ptemp->clientaddr), sizeof(clientaddr))) {//判断是不是自己,不是自己就发送数据ptemp = ptemp->next;if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, ((struct sockaddr*)&(ptemp->clientaddr)), sizeof(ptemp->clientaddr))) {ERRLOG("sendto error");}} else {//是自己,就将自己在链表中删除node_t* pdel = ptemp->next;ptemp->next = pdel->next;free(pdel);pdel == NULL;}}
}
客户端代码
//客户端代码
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define ERRLOG(msg) \do { \printf("%s %s %d:", __FILE__, __func__, __LINE__); \perror(msg); \exit(-1); \} while (0)typedef struct _MSG {char code; //操作码 ('L' 登录),('C' 群聊),('Q' 退出)char name[32];char txt[128];
} msg_t;
int main(int argc, const char* argv[])
{//入参合理性检查if (3 != argc) {printf("Usage : %s <IP> <PORT>\n", argv[0]);return -1;}// 1.创建套接字int sockfd = 0;if (-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0))) {ERRLOG("socket error");}// 2.填充服务器网络信息结构体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));printf("请输入用户名: ");fgets(msg.name, 32, stdin);msg.name[strlen(msg.name) - 1] = '\0';//给服务器发送登录的数据包msg.code = 'L';strcpy(msg.txt, "加入群聊");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.txt);}} else if (0 < pid) {//父进程while (1) {msg.code = 'C';fgets(msg.txt, 128, stdin);printf("我: ");msg.txt[strlen(msg.txt) - 1] = '\0';//判断是不是要退出if (!strncmp(msg.txt, "quit", 5)) {msg.code = 'Q';strcpy(msg.txt, "退出群聊");}if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, serveraddr_len)) {ERRLOG("sendto error");}if (!strcmp(msg.txt, "退出群聊")) {break;}}//先让子进程自杀kill(pid, SIGKILL);wait(NULL);close(sockfd);}return 0;
}