目录
项目要求
利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。类似于微信群聊。
问题思考
- 客户端会不会知道其它客户端地址?
UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。
- 有几种消息类型?
登录:服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。
聊天:服务器只需要把某个客户端的聊天消息转发给所有其它客户端。
- 服务器如何存储客户端的地址?
数据结构可以选择线性数据结构,可以选择链表
- 客户端如何同时处理发送和接收?
客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用gets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程来处理。
流程图
服务器端流程
创建流式套接字,绑定本机地址,监听客户端的连接请求,当有客户端请求连接时,与客户端建立连接,客户端第一次连接时,需要登录。
登录时:
当有客户端登录时,调用登录函数,将新登录的客户端地址存入链表中,并且将客户端
的登录消息提示给其他客户端
聊天时:接收客户端的消息,并且把消息转发给其他客户端,通过遍历链表中的客户端地
址,挨个发送消息。
退出时:当客户端退出时,接收客户端的退出信息,把客户端的地址从链表中删除,并且把
客户端退出的消息提示给其他客户端
客户端流程
从终端获取信息,向服务器发送消息,同时接收服务器发送的消息。
总体流程
服务器端代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <dirent.h>
#include <sys/stat.h>
#include <signal.h>
#include <pthread.h>
enum type_t // 枚举
{
Login,
Chat,
Quit,
};
typedef struct msg
{
char type; // L C Q
char name[32]; //
char text[128]; //
} msg_t;
typedef struct NODE // 链表
{
struct sockaddr_in cliaddr;
struct NODE *next;
} node_t;
node_t *create_node(void) // 建头节点
{
node_t *p = (node_t *)malloc(sizeof(node_t));
if (p == NULL)
{
perror("malloc err");
return NULL;
}
p->next = NULL;
return p;
}
// 登录的函数
// 功能:
// 1》将新登录的用户转发给所有已经登录的用户(遍历链表发送谁登录的消息)
// 2》创建新节点来保存新登录用户的信息,链接到链表尾就可以
void do_login(int serverfd, msg_t msg, node_t *p, struct sockaddr_in cliaddr)
{
sprintf(msg.text, "%s 以上线", msg.name);
// 循环链表,向其他客户端发送上线信息
while (p->next != NULL)
{
p = p->next;
sendto(serverfd, &msg, sizeof(msg), 0,
(struct sockaddr *)&(p->cliaddr), sizeof(p->cliaddr));
}
// 将新登录的客户端信息插入到链表中
node_t *new = (node_t *)malloc(sizeof(node_t));
// 初始化
new->cliaddr = cliaddr;
new->next = NULL;
// 链接到链表尾
p->next = new;
return;
}
// 群聊的函数
// 功能:将客户端发来的聊天内容转发给所有已登录的用户,除了发送聊天内容的用户以外
void do_chat(int serverfd, msg_t msg, node_t *p, struct sockaddr_in cliaddr)
{
// 遍历链表
while (p->next != NULL)
{
p = p->next;
// 比较链表中的存的客户端信息,向爱他客户端发送信息
if (strcmp("p->cliaddr.sin_addr", "cliaddr.sin_addr") != 0)
{
sendto(serverfd, &msg, sizeof(msg), 0,
(struct sockaddr *)&(p->cliaddr), sizeof(p->cliaddr));
}
}
return;
}
// 退出函数
// 功能:
// 1》将谁退出的消息转发给i所有用户
// 2》将链表中保存这个推出的用户信息的节点删除
void do_quit(int serverfd, msg_t msg, node_t *p, struct sockaddr_in cliaddr)
{
sprintf(msg.text, "%s 以下线", msg.name);
while (p->next != NULL)
{
// 比较链表中的存的客户端信息
if (strcmp("p->cliaddr.sin_addr", "cliaddr.sin_addr") == 0)
{
// 删掉退出客户端的节点
node_t *dele = NULL;
dele = p->next;
p->next = dele->next;
free(dele);
dele = NULL;
}
else
{
// 向节点发送客户端退出的信息
p = p->next;
sendto(serverfd, &msg, sizeof(msg), 0,
(struct sockaddr *)&(p->cliaddr), sizeof(p->cliaddr));
}
}
return;
}
int main(int argc, char const *argv[])
{
int serverfd;
int res;
struct sockaddr_in myaddr, cliaddr;
// 创建UDP套接字
serverfd = socket(AF_INET, SOCK_DGRAM, 0);
if (serverfd < 0)
{
perror("socket err");
exit(-1);
}
printf("socket scuess ");
printf("sserverfd: %d\n", serverfd);
// 填充服务器网络信息结构体
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(atoi(argv[1]));
myaddr.sin_addr.s_addr = INADDR_ANY;
socklen_t addrlen = sizeof(myaddr);
// 设置套接字属性,可以重用本地地址与端口
int optval = 1;
res = setsockopt(serverfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
if (res < 0)
{
perror("setcoskopt error");
return -1;
}
// 绑定套接字和服务器网络信息的结构体
res = bind(serverfd, (struct sockaddr *)&myaddr, addrlen);
if (res < 0)
{
perror("bind error");
return -1;
}
printf("bind ok!\n");
msg_t msg; // 结构体变量
node_t *p = create_node(); // 创建链表
while (1)
{
// 接收客户端的消息
if (recvfrom(serverfd, &msg, sizeof(msg), 0,
(struct sockaddr *)&cliaddr, &addrlen) < 0)
{
perror("recvfrom err");
return -1;
}
if (msg.type == Login) // 登录
{
printf("有新用户登录\n");
strcpy(msg.text, "以上线");
printf("ip:%s pord:%d name:%s ", inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port), msg.name);
printf("状态:%s\n", msg.text);
do_login(serverfd, msg, p, cliaddr);
}
else if (msg.type == Chat) // 聊天状态
{
do_chat(serverfd, msg, p, cliaddr);
}
else if (msg.type == Quit) // 退出
{
strcpy(msg.text, "以下线");
printf("ip:%s pord:%d name:%s ", inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port), msg.name);
printf("状态:%s\n", msg.text);
do_quit(serverfd, msg, p, cliaddr);
}
}
close(serverfd);
return 0;
}
客户端代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <dirent.h>
#include <sys/stat.h>
#include <signal.h>
#include <pthread.h>
enum type_t // 枚举
{
Login,
Chat,
Quit,
};
typedef struct msg
{
char type; // L C Q
char name[32]; //
char text[128]; //
} msg_t;
typedef struct NODE // 链表
{
struct sockaddr_in cliaddr;
struct NODE *next;
} node_t;
node_t *create_node(void) // 建头节点
{
node_t *p = (node_t *)malloc(sizeof(node_t));
if (p == NULL)
{
perror("malloc err");
return NULL;
}
p->next = NULL;
return p;
}
// 登录的函数
// 功能:
// 1》将新登录的用户转发给所有已经登录的用户(遍历链表发送谁登录的消息)
// 2》创建新节点来保存新登录用户的信息,链接到链表尾就可以
void do_login(int serverfd, msg_t msg, node_t *p, struct sockaddr_in cliaddr)
{
sprintf(msg.text, "%s 以上线", msg.name);
// 循环链表,向其他客户端发送上线信息
while (p->next != NULL)
{
p = p->next;
sendto(serverfd, &msg, sizeof(msg), 0,
(struct sockaddr *)&(p->cliaddr), sizeof(p->cliaddr));
}
// 将新登录的客户端信息插入到链表中
node_t *new = (node_t *)malloc(sizeof(node_t));
// 初始化
new->cliaddr = cliaddr;
new->next = NULL;
// 链接到链表尾
p->next = new;
return;
}
// 群聊的函数
// 功能:将客户端发来的聊天内容转发给所有已登录的用户,除了发送聊天内容的用户以外
void do_chat(int serverfd, msg_t msg, node_t *p, struct sockaddr_in cliaddr)
{
// 遍历链表
while (p->next != NULL)
{
p = p->next;
// 比较链表中的存的客户端信息,向爱他客户端发送信息
if (strcmp("p->cliaddr.sin_addr", "cliaddr.sin_addr") != 0)
{
sendto(serverfd, &msg, sizeof(msg), 0,
(struct sockaddr *)&(p->cliaddr), sizeof(p->cliaddr));
}
}
return;
}
// 退出函数
// 功能:
// 1》将谁退出的消息转发给i所有用户
// 2》将链表中保存这个推出的用户信息的节点删除
void do_quit(int serverfd, msg_t msg, node_t *p, struct sockaddr_in cliaddr)
{
sprintf(msg.text, "%s 以下线", msg.name);
while (p->next != NULL)
{
// 比较链表中的存的客户端信息
if (strcmp("p->cliaddr.sin_addr", "cliaddr.sin_addr") == 0)
{
// 删掉退出客户端的节点
node_t *dele = NULL;
dele = p->next;
p->next = dele->next;
free(dele);
dele = NULL;
}
else
{
// 向节点发送客户端退出的信息
p = p->next;
sendto(serverfd, &msg, sizeof(msg), 0,
(struct sockaddr *)&(p->cliaddr), sizeof(p->cliaddr));
}
}
return;
}
int main(int argc, char const *argv[])
{
int serverfd;
int res;
struct sockaddr_in myaddr, cliaddr;
// 创建UDP套接字
serverfd = socket(AF_INET, SOCK_DGRAM, 0);
if (serverfd < 0)
{
perror("socket err");
exit(-1);
}
printf("socket scuess ");
printf("sserverfd: %d\n", serverfd);
// 填充服务器网络信息结构体
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(atoi(argv[1]));
myaddr.sin_addr.s_addr = INADDR_ANY;
socklen_t addrlen = sizeof(myaddr);
// 设置套接字属性,可以重用本地地址与端口
int optval = 1;
res = setsockopt(serverfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
if (res < 0)
{
perror("setcoskopt error");
return -1;
}
// 绑定套接字和服务器网络信息的结构体
res = bind(serverfd, (struct sockaddr *)&myaddr, addrlen);
if (res < 0)
{
perror("bind error");
return -1;
}
printf("bind ok!\n");
msg_t msg; // 结构体变量
node_t *p = create_node(); // 创建链表
while (1)
{
// 接收客户端的消息
if (recvfrom(serverfd, &msg, sizeof(msg), 0,
(struct sockaddr *)&cliaddr, &addrlen) < 0)
{
perror("recvfrom err");
return -1;
}
if (msg.type == Login) // 登录
{
printf("有新用户登录\n");
strcpy(msg.text, "以上线");
printf("ip:%s pord:%d name:%s ", inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port), msg.name);
printf("状态:%s\n", msg.text);
do_login(serverfd, msg, p, cliaddr);
}
else if (msg.type == Chat) // 聊天状态
{
do_chat(serverfd, msg, p, cliaddr);
}
else if (msg.type == Quit) // 退出
{
strcpy(msg.text, "以下线");
printf("ip:%s pord:%d name:%s ", inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port), msg.name);
printf("状态:%s\n", msg.text);
do_quit(serverfd, msg, p, cliaddr);
}
}
close(serverfd);
return 0;
}