目录
项目功能介绍
利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。
问题思考
● 客户端会不会知道其它客户端地址?
不知道,服务器完成,服务器存储连接的客户端信息---》链表
● 有几种消息类型?
登录
聊天
退出
● 客户端如何同时处理发送和接收?
客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用fgets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。
补充参考函数
链表节点结构体:struct node{
struct sockaddr_in addr;//data memcmp
struct node *next;
};
消息对应的结构体(同一个协议)
typedef struct msg_t
{
int type;//'L' C Q enum un{login,chat,quit};
char name[32];//用户名
char text[128];//消息正文
}MSG_t;
int memcmp(void *s1,void *s2,int size);//对比 相等时返回值为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_t
{
int type;
char name[32];
char text[128];
} MSG_t;
// 链表
typedef struct node
{
struct sockaddr_in addr;
struct node *next;
} node_t, *node_p;
// 登录
void Login(int sockfd, MSG_t msg, node_p p, struct sockaddr_in caddr)
{
sprintf(msg.text, "%s %s", msg.name, "已登录!!!");
while (p->next != NULL)
{
p = p->next;
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
}
node_p pnew = (node_p)malloc(sizeof(node_t));
if (NULL == pnew)
{
perror("p malloc err");
return;
}
pnew->addr = caddr;
pnew->next = NULL;
p->next = pnew;
}
// 聊天
void Chat(int sockfd, MSG_t msg, node_p p, struct sockaddr_in caddr)
{
while (p->next != NULL)
{
p = p->next;
if (memcmp(&(p->addr), &caddr, sizeof(caddr)))
{
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
}
}
}
// 退出
void Quit(int sockfd, MSG_t msg, node_p p, struct sockaddr_in caddr)
{
sprintf(msg.text, "%s %s", msg.name, "已退出!!!");
while (p->next != NULL)
{
if (memcmp(&(p->next->addr), &caddr, sizeof(caddr)))
{
p = p->next;
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
}
else
{
node_p ptail = p->next;
p->next = ptail->next;
free(ptail);
ptail = NULL;
}
}
}
node_p create()
{
node_p p = (node_p)malloc(sizeof(node_t));
if (NULL == p)
{
perror("p malloc err");
return NULL;
}
p->next = NULL;
return p;
}
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("%s <port>\n", argv[0]);
return -1;
}
int ret;
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
saddr.sin_addr.s_addr = INADDR_ANY;
int len = sizeof(caddr);
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err");
return -1;
}
printf("bind success\n");
MSG_t msg;
node_p p = create();
// 服务器可以给所有用户发送“公告”
pid_t pid = fork();
if (pid < 0)
{
perror("fork err");
return -1;
}
else if (pid == 0)
{
while (1)
{
fgets(msg.text, sizeof(msg.text), stdin);
if (msg.text[strlen(msg.text) - 1] == '\n')
msg.text[strlen(msg.text) - 1] = '\0';
msg.type = chat;
strcpy(msg.name, "server say ");
sendto(sockfd, &msg, sizeof(MSG_t), 0, (struct sockaddr *)&saddr, sizeof(saddr));
}
}
else
{
while (1)
{
memset(msg.text, 0, sizeof(msg.text));
ret = recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&caddr, &len);
if (ret < 0)
{
perror("recv err\n");
return -1;
}
switch (msg.type)
{
case 0:
printf("%s 上线\n", msg.name);
Login(sockfd, msg, p, caddr);
break;
case 1:
printf("%s %s\n", msg.name, msg.text);
Chat(sockfd, msg, p, caddr);
break;
case 2:
printf("%s 下线\n", msg.name);
Quit(sockfd, msg, p, caddr);
break;
}
}
close(sockfd);
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 node
{
struct sockaddr_in addr;
struct node *next;
} node_t, *node_p;
// 消息对应的结构体
typedef struct msg_t
{
int type;
char name[32];
char text[128];
} MSG_t;
int main(int argc, char const *argv[])
{
MSG_t msg;
int ret;
char buf[128];
if (argc != 3)
{
printf("%s <ip> <port>\n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
printf("%d\n", sockfd);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr(argv[1]);
int len = sizeof(saddr);
msg.type = login;
printf("登录账号:\n");
fgets(msg.name, sizeof(msg.name), stdin);
if (msg.name[strlen(msg.name) - 1] == '\n')
msg.name[strlen(msg.name) - 1] = '\0';
if (sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("send1 err");
return -1;
}
pid_t pid = fork();
if (pid < 0)
{
perror("fork err");
return -1;
}
else if (pid == 0)
{
while (1)
{
memset(msg.text, 0, sizeof(msg.text));
printf("请输入消息:\n");
fgets(msg.text, sizeof(msg.text), stdin);
if (msg.text[strlen(msg.text) - 1] == '\n')
msg.text[strlen(msg.text) - 1] = '\0';
if (strcmp(msg.text, "quit") == 0)
{
printf("退出\n");
msg.type = quit;
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, sizeof(saddr));
break;
}
else
{
msg.type = chat;
if (sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, sizeof(saddr))<0)
{
perror("send2 err");
return -1;
}
}
}
}
else
{
while (1)
{
if ((recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL)) < 0)
{
perror("recvfrom err.");
return -1;
}
printf("%s:%s\n", msg.name, msg.text);
}
}
close(sockfd);
return 0;
}
总结
一、整体架构和功能
整体架构
- 基于 UDP 协议实现了一个简单的多人聊天系统。服务器和客户端通过发送和接收特定结构体(
MSG_t
)封装的消息来进行通信。- 服务器端维护一个链表(
node_t
)来存储已登录客户端的网络地址信息。主要功能
- 登录功能:客户端发送登录消息(
login
类型),服务器接收后将客户端信息添加到链表中,并通知其他已登录客户端该用户已登录。- 聊天功能:客户端发送聊天消息(
chat
类型),服务器将消息转发给除发送方之外的其他已登录客户端。- 退出功能:客户端发送退出消息(
quit
类型),服务器从链表中删除该客户端信息,并通知其他已登录客户端该用户已退出。- 服务器公告功能:服务器可以发送公告消息给所有已登录客户端。
二、服务器端分析
数据结构和枚举定义
- 枚举类型
type_t
:定义了消息的类型,包括login
(登录)、chat
(聊天)和quit
(退出),使消息处理逻辑更加清晰。- 结构体
MSG_t
:用于封装消息,包含消息类型(type
)、发送者姓名(name
)和消息内容(text
)。- 结构体
node_t
和node_p
:定义了链表节点,用于存储客户端的网络地址信息。初始化部分
- 命令行参数检查:检查启动服务器时是否提供了正确的端口号作为命令行参数。
- 套接字创建和绑定:创建 UDP 套接字(
socket
函数),并将其与指定的 IP 地址和端口号绑定(bind
函数)。消息处理函数
Login
函数
- 当接收到登录消息时,构建登录通知消息(将用户名和登录状态组合)。
- 遍历链表,将登录通知消息发送给除发送方之外的所有已登录客户端。
- 创建新的链表节点,将登录客户端的地址信息添加到链表中。
Chat
函数
- 接收到聊天消息后,遍历链表,将消息发送给除发送方之外的所有已登录客户端。
Quit
函数
- 构建退出通知消息(将用户名和退出状态组合)。
- 遍历链表,删除退出客户端的节点,并将退出通知消息发送给其他已登录客户端。
主循环部分
- 服务器在一个无限循环中接收客户端发送的消息(
recvfrom
函数)。- 根据消息的类型(通过
type
字段判断)调用相应的处理函数(Login
、Chat
或Quit
)。服务器公告实现
- 通过
fork
创建一个子进程,父进程负责处理客户端的消息,子进程用于发送服务器公告。- 子进程从标准输入读取公告内容,构建聊天类型的消息(
type
设置为chat
,name
设置为server say
),并发送给所有已登录客户端。三、客户端分析
数据结构定义
- 与服务器端类似,定义了
type_t
枚举类型、MSG_t
结构体和node_t
、node_p
结构体。初始化部分
- 命令行参数检查:检查启动客户端时是否提供了正确的服务器 IP 地址和端口号作为命令行参数。
- 套接字创建:创建 UDP 套接字。
登录和消息发送流程
- 客户端首先发送登录消息(
type
设置为login
),包含用户输入的登录账号。- 通过
fork
创建一个子进程,子进程负责从标准输入读取消息内容,并根据消息内容判断是普通聊天消息还是退出消息。
- 普通聊天消息(
type
设置为chat
)直接发送给服务器。- 退出消息(
type
设置为quit
)发送给服务器后,子进程退出。- 父进程在一个无限循环中接收服务器转发的消息(
recvfrom
函数),并将消息内容打印输出。四、数据传输和处理
消息封装和解析
- 服务器和客户端都使用
MSG_t
结构体来封装消息,通过网络传输该结构体的二进制数据。- 在接收端(服务器或客户端),直接将接收到的数据解析为
MSG_t
结构体进行处理。网络地址处理
- 在服务器端,使用
struct sockaddr_in
来存储客户端的网络地址信息。在Login
、Chat
和Quit
函数中,需要根据客户端的网络地址来发送消息。- 客户端在发送消息时,需要指定服务器的网络地址(通过命令行参数获取并填充到
struct sockaddr_in
结构体中)。五、注意事项和潜在问题
错误处理
- 在代码中,对于系统调用(如
socket
、bind
、recvfrom
、sendto
等)的返回值进行了检查,如果返回值小于 0,则表示调用失败,通过perror
函数输出错误信息。缓冲区溢出
- 代码中使用固定大小的缓冲区(如
MSG_t
结构体中的text
数组),在接收和处理消息时,如果消息内容长度超过缓冲区大小,可能会导致缓冲区溢出。多线程 / 进程安全性
- 服务器端使用了
fork
创建子进程来实现服务器公告功能,在多进程环境下,需要注意进程间资源共享和同步的问题。- 虽然代码中没有明显的线程相关操作,但如果要对程序进行扩展(如添加图形界面等),可能会引入线程,需要考虑线程安全性。
链表操作的健壮性
- 服务器端使用链表来存储客户端信息,在
Login
、Quit
等函数中涉及链表的插入和删除操作。在并发环境下(多个客户端同时登录或退出),可能需要对链表操作进行加锁保护,以确保链表的一致性。