一、UDP的概念
1.1 UDP
UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
数据报格式套接字SOCK_DGRAM
1.2 UDP特点
采用UDP只管发送数据而不去验证发送数据的正确性,不论传输是否被接收,数据流是否有丢失,都不再重新发送,特征如下:
- 强调快速传输而非传输顺序;
- 传输的数据可能丢失也可能损毁;
- 限制每次传输的数据大小;
- 数据的发送和接收是并发的。
二. 采用UDP实现多人聊天室原因
数据报套接字采用的是UDP(User Datagram Protocol)协议,本次聊天室采用UDP协议虽然可能会导致数据丢失,但是聊天并不去强调内容的正确性,而应该强调实时性和并发,并且数据丢失只是小概率事件。
三、多人聊天室项目功能
- 当有用户加入群聊上线时,将登录消息发给在线的所有人
- 当有用户在线发送消息时,将消息发给在线的所有人
- 当有用户退出群聊时,将用户退出消息发给在线的所有人
- 服务器可以发送系统公告消息
四、实现多人聊天室项目流程分析
4.1 前期准备
4.1.1 定义结构体
由于客户端给服务器发送的数据内容较多,所以需要定义结构体来发送:
typedef struct
{
char code; //操作码 'L' 登录 'C' 群聊 'Q' 退出
char name[32];
char txt[128];
} msg_t;
code相当于一个协议,用来确定将要进行的操作。
code为 ‘L’ 登录 ‘C’ 群聊 ‘Q’ 退出
name[32]用来保存登录用户名;
txt[128]用来保存发送的信息;
4.1.2 定义链表
服务器要给在线的所有客户端发送数据,需要将每一个客户端的信息保存,使用链表来保。
typedef struct _NODE
{
struct sockaddr_in c_addr;//数据域
struct _NODE *next;//指针域
} node_t;
数据域:客户端的网络信息结构体;
指针域:保存下一个结点的地址;
4.2 多人聊天室服务器
服务器既可以发送系统信息,又可以接收客户端信息并处理,可以使用多进程或者多线程;
本次使用多进程实现多人聊天室服务器。
pid_t pid;
pid = fork();
if (pid == -1)
{
//创建错误
}
else if (pid == 0)
{
//子进程
//接受数据并处理
}
else if (pid > 0)
{
//父进程
//发系统消息
}
4.2.1 接收客户端发来的消息并进行处理
子进程循环接收客户端发来的消息,通过switch判断code所存的协议,执行特定的功能函数。
- 登录操作函数
- 群聊操作函数
- 退出操作函数
4.2.2 聊天室群公告功能
将父进程视为一个客户端,向子进程发送消息给所有在线的用户。
4.3 多人聊天室客户端
客户端登录之后,为了实现一边发送数据一边接收数据,可以使用多进程或者多线程;
本次使用多进程实现多人聊天室客户端。代码框架同上。
4.3.1 接收服务器发来的消息并进行处理
子进程循环接受服务器发来的消息,并将其打印在终端上。
4.3.2 向服务器发送消息
父进程循环向服务器发送群聊的消息,如果当消息为 “quit” 时进行用户退出群聊操作。
五、多人聊天室流程图
5.1 服务器流程图
5.2 客户端流程图
六、根据多人聊天室流程模块化进行代码实现
宏定义打印错误信息,进行健壮性判断。
#define PRINT_ERR(msg) \
do \
{ \
printf("%s,%d,%s\n", __FILE__, __LINE__, __func__); \
perror(msg); \
exit(-1); \
} while (0)
入参合理性判断 可执行文件 ip地址 端口号
if (argc != 3)
{
printf("age:%s ip port\n", argv[0]);
return -1;
}
6.1 服务器代码实现
6.1.1 创建套接字
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
PRINT_ERR("socket error");
}
6.1.2 创建服务器网络信息结构体
struct sockaddr_in serviceaddr;
memset(&serviceaddr, 0, sizeof(serviceaddr));
serviceaddr.sin_family = AF_INET;
serviceaddr.sin_addr.s_addr = inet_addr(argv[1]);
serviceaddr.sin_port = htons(atoi(argv[2]));
socklen_t serviceaddr_len = sizeof(serviceaddr);
6.1.3 将服务器网络信息结构体与套接字绑定
if (bind(sockfd, (struct sockaddr *)&serviceaddr, serviceaddr_len) == -1)
{
PRINT_ERR("bind error");
}
6.1.4 创建客户端网络信息结构体
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t clientaddr_len = sizeof(clientaddr);
6.1.5 子进程内部实现代码
- 使用链表头节点函数,定义链表头节点
//创建链表头节点函数
void creat_link(node_t **head)
{
*head = (node_t *)malloc(sizeof(node_t));
}
//定义链表头节点
node_t *phead = NULL;
creat_link(&phead);
phead->next = NULL;
- 循环接受客户端发来的信息并通过switch进行判断执行哪个功能函数
while (1)
{
memset(&msg, 0, sizeof(msg));//清空操作
memset(&clientaddr, 0, sizeof(clientaddr));//清空操作
if ((recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&clientaddr, &clientaddr_len)) == -1)
{
PRINT_ERR("recvfrom error");
}
printf("%8s : [%s]\n", msg.name, msg.txt);
switch (msg.code)
{
case 'L':
do_register(sockfd, msg, clientaddr, phead);
break;
case 'C':
do_group_chat(sockfd, msg, clientaddr, phead);
break;
case 'Q':
quit_group_chat(sockfd, msg, clientaddr, phead);
break;
}
}
- 注册操作函数
注册时,遍历链表将用户登录信息发给所以在线的用户,并将自己的客户端网络信息结构体头插进入链表中。
int do_register(int sockfd, msg_t msg, struct sockaddr_in clientaddr, node_t *phead)
{
//遍历链表将登录信息发送给所以人
node_t *p = phead;
while (p->next != NULL)
{
p = p->next;
if (sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(p->c_addr), sizeof(p->c_addr)) == -1)
{
PRINT_ERR("recvfrom error");
}
}
//将登录的客户端信息插入保存在链表
//头插
//定义一个新的指针保存客户端信息
node_t *newp = NULL;
creat_link(&newp);
newp->c_addr = clientaddr;
newp->next = phead->next;
phead->next = newp;
return 0;
}
- 群聊操作函数
群聊时通过遍历,将txt中的信息发送给处了自己以外的所有在线用户
int do_group_chat(int sockfd, msg_t msg, struct sockaddr_in clientaddr, node_t *phead)
{
//遍历链表,将消息发给除自己之外的所有人
node_t *p = phead;
while (p->next != NULL)
{
p = p->next;
//判断链表客户端信息是否是自己
//是自己就不发送
if (memcmp(&(p->c_addr), &clientaddr, sizeof(clientaddr)))
{
if (sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(p->c_addr), sizeof(p->c_addr)) == -1)
{
PRINT_ERR("recvfrom error");
}
}
}
return 0;
}
- 退出操作函数
退出时,遍历链表将退出信息发送给除自己以外的所有在线用户,并将自己的客户端网络信息结构体在链表中删除。
int quit_group_chat(int sockfd, msg_t msg, struct sockaddr_in clientaddr, node_t *phead)
{
node_t *p = phead;
while (p->next != NULL)
{
//判断链表客户端信息是否是自己
//是自己就不发送并且将自己的客户端信息在链表内删除
if (memcmp(&(p->next->c_addr), &clientaddr, sizeof(clientaddr)))
{
p = p->next;
if (sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(p->c_addr), sizeof(p->c_addr)) == -1)
{
PRINT_ERR("recvfrom error");
}
}
else
{
node_t *pnew;
pnew = p->next;
p->next = pnew->next;
pnew->next = NULL;
free(pnew);
pnew = NULL;
}
}
return 0;
}
6.1.6 父进程内部实现代码
父进程视为一个客户端,向子进程发送消息,遍历链表给所有在线的用户。
msg.code='C';
strcpy(msg.name,"server");
while(1)
{
fgets(msg.txt,128,stdin);
msg.txt[strlen(msg.txt)-1]='\0';
if(sendto(sockfd,&msg,sizeof(msg_t),0,(struct sockaddr *)&serviceaddr,serviceaddr_len)==-1)
{
PRINT_ERR("sendto error");
}
}
代码结束前,在最后记得关闭套接字close(sockfd);
6.2 客户端代码实现
6.2.1 前两部与服务器一样
6.2.2 给服务器发送登录数据包
msg_t msg;
memset(&msg, 0, sizeof(msg_t));
msg.code = 'L';
printf("请输入用户名:");
fgets(msg.name, 32, stdin);
msg.name[strlen(msg.name) - 1] = '\0';
strcpy(msg.txt, "加入群聊");
if (sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&serviceaddr, serviceaddr_len) == -1)
{
PRINT_ERR("sendto error");
}
6.2.3 子进程内部实现代码
循环接收服务器发来的数据,并将其打印在终端上。
while (1)
{
//每次循环前将msg置零
memset(&msg, 0, sizeof(msg));
//接受服务器发过来的信息并打印到终端上
if (recvfrom(sockfd, &msg, sizeof(msg_t), 0, NULL, NULL) == -1)
{
PRINT_ERR("recvfrom error");
}
printf("%8s:[%s]\n", msg.name, msg.txt);
}
6.2.4 父进程内部实现代码
先将协议设置为群聊,将终端输入的数据发送给服务器,当终端输入"quit"时,将协议设置为退出,并将退出群聊发送给服务器。
如果退出向终端输入"quit"时,退出循环。
while (1)
{
//memset会把name清除
msg.code = 'C';
fgets(msg.txt, 128, stdin);
msg.txt[strlen(msg.txt) - 1] = '\0';
if (strcmp(msg.txt, "quit") == 0)
{
msg.code = 'Q';
strcpy(msg.txt, "退出群聊");
}
if (sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&serviceaddr, serviceaddr_len) == -1)
{
PRINT_ERR("sendto error");
}
if (strcmp(msg.txt, "退出群聊") == 0)
{
break;
}
}
6.2.5 退出程序
给子进程发送杀死信号,等待回收子进程,关闭套接字。
kill(pid,SIGKILL);
wait(NULL);
close(sockfd);
七、结果展示和总结
7.1 结果展示
用户之间聊天成功
系统发送公告成功
退出群聊成功
成功实现基于UDP的多人聊天室。
7.2 缺点与不足
没有对客户端断连采取更为健壮的处理,应该再客户端捕获一下客户端因为ctrl+c而结束的信号,在捕获之后发送数据包给服务器,让服务器对客户端退出行为,做出删除保存客户信息结构体的行为。还有就是没有添加数据库的使用。
7.3 总结
整体项目并不复杂,只需要想清楚service和client分别需要做什么事。画好流程图,就会很清晰的将思路缕顺。从而只需要模块化的完成相应代码即可。
server为了保存每次连入进来的client信息,使用了链表,对链表的操作就是简单的插入,删除操作。文章来源:https://www.toymoban.com/news/detail-479108.html
这算是最近学习中实现的一个稍微综合一点的项目,虽然不多,却也有所收获,便发布一篇帖子去记录,代码中可能有许多不规范的地方,逻辑可能并不严谨,还请各位不吝赐教。文章来源地址https://www.toymoban.com/news/detail-479108.html
八、UDP实现多人聊天室服务器源代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
//宏定义打印错误信息
#define PRINT_ERR(msg) \
do \
{ \
printf("%s,%d,%s\n", __FILE__, __LINE__, __func__); \
perror(msg); \
exit(-1); \
} while (0)
typedef struct
{
char code; //操作码 'L' 登录 'C' 群聊 'Q' 退出
char name[32];
char txt[128];
} msg_t;
//链表结构体
typedef struct _NODE
{
struct sockaddr_in c_addr;
struct _NODE *next;
} node_t;
void creat_link(node_t **head);
int do_register(int sockfd, msg_t msg, struct sockaddr_in clientaddr, node_t *phead);
int do_group_chat(int sockfd, msg_t msg, struct sockaddr_in clientaddr, node_t *phead);
int quit_group_chat(int sockfd, msg_t msg, struct sockaddr_in clientaddr, node_t *phead);
int main(int argc, const char *argv[])
{
//入参合理性判断
if (argc != 3)
{
printf("age:%s ip port\n", argv[0]);
return -1;
}
//创建套接字
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
PRINT_ERR("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in serviceaddr;
memset(&serviceaddr, 0, sizeof(serviceaddr));
serviceaddr.sin_family = AF_INET;
serviceaddr.sin_addr.s_addr = inet_addr(argv[1]);
serviceaddr.sin_port = htons(atoi(argv[2]));
socklen_t serviceaddr_len = sizeof(serviceaddr);
//将服务器网络信息结构体与套接字绑定
if (bind(sockfd, (struct sockaddr *)&serviceaddr, serviceaddr_len) == -1)
{
PRINT_ERR("bind error");
}
//创建客户端网络信息结构体
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t clientaddr_len = sizeof(clientaddr);
msg_t msg;
//创建父子进程
pid_t pid;
pid = fork();
if (pid == -1)
{
PRINT_ERR("fork error");
}
else if (pid == 0)
{
//子进程
//接受数据并处理
//定义链表头节点
node_t *phead = NULL;
creat_link(&phead);
phead->next = NULL;
while (1)
{
memset(&msg, 0, sizeof(msg));
memset(&clientaddr, 0, sizeof(clientaddr));
if ((recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&clientaddr, &clientaddr_len)) == -1)
{
PRINT_ERR("recvfrom error");
}
printf("%8s : [%s]\n", msg.name, msg.txt);
switch (msg.code)
{
case 'L':
do_register(sockfd, msg, clientaddr, phead);
break;
case 'C':
do_group_chat(sockfd, msg, clientaddr, phead);
break;
case 'Q':
quit_group_chat(sockfd, msg, clientaddr, phead);
break;
}
}
}
else if (pid > 0)
{
//父进程
//发系统消息
msg.code='C';
strcpy(msg.name,"server");
while(1)
{
fgets(msg.txt,128,stdin);
msg.txt[strlen(msg.txt)-1]='\0';
if(sendto(sockfd,&msg,sizeof(msg_t),0,(struct sockaddr *)&serviceaddr,serviceaddr_len)==-1)
{
PRINT_ERR("sendto error");
}
}
}
close(sockfd);
return 0;
}
//创建链表头节点函数
void creat_link(node_t **head)
{
*head = (node_t *)malloc(sizeof(node_t));
}
//登录操作
int do_register(int sockfd, msg_t msg, struct sockaddr_in clientaddr, node_t *phead)
{
//遍历链表将登录信息发送给所以人
node_t *p = phead;
while (p->next != NULL)
{
p = p->next;
if (sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(p->c_addr), sizeof(p->c_addr)) == -1)
{
PRINT_ERR("recvfrom error");
}
}
//将登录的客户端信息插入保存在链表
//头插
//定义一个新的指针保存客户端信息
node_t *newp = NULL;
creat_link(&newp);
newp->c_addr = clientaddr;
newp->next = phead->next;
phead->next = newp;
return 0;
}
int do_group_chat(int sockfd, msg_t msg, struct sockaddr_in clientaddr, node_t *phead)
{
//遍历链表,将消息发给除自己之外的所有人
node_t *p = phead;
while (p->next != NULL)
{
p = p->next;
//判断链表客户端信息是否是自己
//是自己就不发送
if (memcmp(&(p->c_addr), &clientaddr, sizeof(clientaddr)))
{
if (sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(p->c_addr), sizeof(p->c_addr)) == -1)
{
PRINT_ERR("recvfrom error");
}
}
}
return 0;
}
//退出群聊操作
int quit_group_chat(int sockfd, msg_t msg, struct sockaddr_in clientaddr, node_t *phead)
{
node_t *p = phead;
while (p->next != NULL)
{
//判断链表客户端信息是否是自己
//是自己就不发送并且将自己的客户端信息在链表内删除
if (memcmp(&(p->next->c_addr), &clientaddr, sizeof(clientaddr)))
{
p = p->next;
if (sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(p->c_addr), sizeof(p->c_addr)) == -1)
{
PRINT_ERR("recvfrom error");
}
}
else
{
node_t *pnew;
pnew = p->next;
p->next = pnew->next;
pnew->next = NULL;
free(pnew);
pnew = NULL;
}
}
return 0;
}
九、UDP实现多人聊天室客户端源代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
//宏定义打印错误信息
#define PRINT_ERR(msg) \
do \
{ \
printf("%s,%d,%s\n", __FILE__, __LINE__, __func__); \
perror(msg); \
exit(-1); \
} while (0)
typedef struct
{
char code; //操作码 'L' 登录 'C' 群聊 'Q' 退出
char name[32];
char txt[128];
} msg_t;
int main(int argc, const char *argv[])
{
//入参合理性判断
if (argc != 3)
{
printf("age:%s ip port\n", argv[0]);
return -1;
}
//创建套接字
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
PRINT_ERR("socket error");
}
//创建服务器网络信息结构体
struct sockaddr_in serviceaddr;
memset(&serviceaddr, 0, sizeof(serviceaddr));
serviceaddr.sin_family = AF_INET;
serviceaddr.sin_addr.s_addr = inet_addr(argv[1]);
serviceaddr.sin_port = htons(atoi(argv[2]));
socklen_t serviceaddr_len = sizeof(serviceaddr);
//给服务器发送登录数据包
msg_t msg;
memset(&msg, 0, sizeof(msg_t));
msg.code = 'L';
printf("请输入用户名:");
fgets(msg.name, 32, stdin);
msg.name[strlen(msg.name) - 1] = '\0';
strcpy(msg.txt, "加入群聊");
if (sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&serviceaddr, serviceaddr_len) == -1)
{
PRINT_ERR("sendto error");
}
//创建父子进程
pid_t pid;
pid = fork();
if (pid == -1)
{
PRINT_ERR("fork error");
}
else if (pid == 0)
{
//子进程
//接受数据并处理
while (1)
{
//每次循环前将msg置零
memset(&msg, 0, sizeof(msg));
//接受服务器发过来的信息并打印到终端上
if (recvfrom(sockfd, &msg, sizeof(msg_t), 0, NULL, NULL) == -1)
{
PRINT_ERR("recvfrom error");
}
printf("%8s:[%s]\n", msg.name, msg.txt);
}
}
else if (pid > 0)
{
//父进程
//发送消息
while (1)
{
//memset会把name清除
msg.code = 'C';
fgets(msg.txt, 128, stdin);
msg.txt[strlen(msg.txt) - 1] = '\0';
if (strcmp(msg.txt, "quit") == 0)
{
msg.code = 'Q';
strcpy(msg.txt, "退出群聊");
}
if (sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&serviceaddr, serviceaddr_len) == -1)
{
PRINT_ERR("sendto error");
}
if (strcmp(msg.txt, "退出群聊") == 0)
{
break;
}
}
kill(pid,SIGKILL);
wait(NULL);
close(sockfd);
}
return 0;
}
到了这里,关于【Linux网络编程】基于UDP实现多人聊天室的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!