目录
实现功能
实现思想
实现代码(部分及详解)
服务端部分代码
客户端部分代码
实现效果
项目中出现的问题和解决方法
项目整体代码展示
代码优化思路
服务端代码
客户端代码
实现功能
- 服务端可以同时连接多个客户端;
- 新的客户端连接服务端时,可以在服务端显示自己的名字并提示登陆成功;
- 新客户端登陆成功时,其他客户端也会提示有新用户登陆;
- 客户端发送消息时,服务器内可以提示是哪个客户端发送了消息;
- 客户端发送消息时,其他客户端也可以接收到该客户端发送的消息内容;
- 服务端相当于主机,客户端相当于用户。
实现思想
- 客户端可以发送三种消息:登录消息(用户名),聊天消息,退出消息,所以需要定义枚举类型,包含这三种消息,当发送对应的消息内容时,将消息类型一起发送,接收消息的一端可以根据消息类型进入不同的函数进行处理。
- 服务器主要的功能就是转发消息,将某一用户发送的消息转发给其他用户,包括用户的登录,聊天和退出消息,还要可以给所有的用户主动发送通知。
- 聊天室要实现的是支持多人同时在线聊天,所以就需要使用链表的思想,将加入的用户依次链接,发送消息时依次遍历,退出时方便找到对应用户。
- 服务端和客户端都需要有发送消息和接收消息的功能,两种功能互不影响,所以需要创建子进程,由父进程完成消息的发送处理,由子进程完成消息的接收处理。
- 重点还是理解链表的思想。
实现代码(部分及详解)
服务端部分代码
1. 定义枚举的三种消息类型:
- 登录Login;
- 聊天Chat;
- 退出Quit。
//消息类型
enum type_t
{
Login = 1, //登录
Chat, //聊天
Quit, //退出
};
2. 定义描述发送的消息内容的结构体:
- 消息类型type(枚举中的三种消息);
- 姓名name(用户名);
- 消息正文text(聊天信息以及退出消息)。
将姓名和消息正文分开定义的原因是,用户发送消息时,需要体现出是哪个用户发送的消息,而且用户登录时要先取名,再发消息正文,如果使用一个定义的话,用户名会被消息正文覆盖。
//定义描述消息结构体
typedef struct msg_t
{
int type; //消息类型:登录 聊天 退出
char name[32]; //姓名
char text[128]; //消息正文
} MSG_t; //将结构体名重定义为MSG_t
3. 定义链表的节点结构体:根据链表的特性,定义用户地址作为数据域,定义结构体指针作为指针域。
//链表的节点结构体
typedef struct node_t
{
struct sockaddr_in addr; //数据域
struct node_t *next; //指针域
} link_t;
4.主函数中子进程实现消息的发送处理:
- 服务端不需要退出,所以使用while一直循环在子进程和父进程中,也不需要break跳出处理;
- 子进程中需要接收消息并判断接收到消息的类型,然后根据消息类型调用对应的处理函数。
//创建一个空的有头单向链表
link_t *p = createLink();
while (1)
{
//接收客户端消息
if(recvfrom(sockfd,&msg,sizeof(msg),0,(struct sockaddr *)&clientaddr,&len) < 0)
{
perror("recvfrom err.");
return -1;
}
//判断消息类型,调用对应函数
switch (msg.type)
{
case Login: //登录
client_login(sockfd, p, clientaddr, msg);
break;
case Chat: //聊天
client_chat(sockfd, p, clientaddr, msg);
break;
case Quit: //退出
client_quit(sockfd, p, clientaddr, msg);
break;
}
}
5.主函数中父进程实现消息的接收处理:
- 父进程发送消息时,记得处理字符串尾部的换行符,否则客户端每次接收到消息后,都会多打印一个换行。
while (1) //服务器发通知
{
msg.type = Chat;
//给结构体中的数组成员变量赋值,一般使用strcpy进行赋值
strcpy(msg.name, "server");
//获取终端输入
fgets(msg.text, sizeof(msg.text), stdin);
//解决发送信息时,会将换行符发送过去的问题
if (msg.text[strlen(msg.text) - 1] == '\n')
msg.text[strlen(msg.text) - 1] = '\0';
//将信息发送给同一局域网的其他客户端
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr,
sizeof(serveraddr));
}
6. 用户登录函数:
- 用户登录时,会将用户自己的地址和用户名一起发送给服务端,所以在服务端函数中可以循环遍历链表,来告诉已经登录的其他用户新登录的用户是谁。
- 然后将新登录的用户保存到当前的链表结尾。
- 需要传递给该函数的参数有(其他函数也是这些形参):
- 套接字描述符(因为要在函数中给其他用户发送消息);
- 链表(头)指针(因为要遍历链表和创建新节点);
- 新登录的用户地址(新用户加入链表时需要将用户地址保存到节点的数据域);
- 消息结构体对象(MSG_t,函数中发送消息时需要发送新用户的名字和消息正文)。
//登录函数
void client_login(int sockfd, link_t *p, struct sockaddr_in clientaddr, MSG_t msg)
{
//1.告诉其他用户登录的新用户是谁
strcpy(msg.name,"server"); //发送消息的人是服务端
sprintf(msg.text, "%s login!", msg.name); //服务端发送新登陆的用户名
//循环发送给之前已经登陆的用户
while (p->next != NULL)
{
p = p->next;
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr),
sizeof(p->addr));
}
//上面的代码运行结束后,此时链表指针已经指向了最后一个用户
//2.创建一个新节点保存新连接的客户端地址 ,连接到链表结尾
link_t *pnew = (link_t *)malloc(sizeof(link_t));
if (pnew == NULL)
{
perror("malloc new node err.");
}
//初始化
pnew->addr = clientaddr;
pnew->next = NULL;
//链接 p是最后一个节点的地址
p->next = pnew;
}
7. 聊天消息发送函数:
- 用户发送聊天消息时,要将消息发送给链表中的,除了自己以外的其他所有用户,当然也要在服务端上打印;
- 判断链表中是否为发送消息用户时,使用了memcmp函数。
int memcmp(const void *buf1, const void *buf2, unsigned int count);
功能:比较内存区域buf1和buf2的前count个字节;
参数:内存区buf1,内存区buf2,比较字节数count;
返回值:如果buf1 < buf2 则返回小于0的数,
如果buf1 = buf2 则返回0,
如果buf1 > buf2 则返回大于01的数;
//聊天信息发送函数
void client_chat(int sockfd, link_t *p, struct sockaddr_in clientaddr, MSG_t msg)
{
//从链表头开始遍历
while (p->next != NULL)
{
p = p->next;
if(memcmp(&(p->addr), &clientaddr, sizeof(clientaddr)) != 0)
{
//只要判断出用户地址和发送消息的用户地址不同,就将消息发送给该用户
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr),
sizeof(p->addr));
}
}
//在服务器中打印发送的消息,“谁说了什么”
printf("%s said %s\n", msg.name, msg.text);
}
8. 用户退出处理函数:
- 用户退出时,还是要在服务端遍历链表,而且是需要全部遍历;
- 如果遍历到退出的那个用户,则将该用户对应的链表节点删除;
- 如果遍历到其他用户,则依次发送用户要退出的信息。
//退出函数
void client_quit(int sockfd, link_t *p, struct sockaddr_in clientaddr, MSG_t msg)
{
link_t *pdel = NULL;
//从头开始遍历查找要删除的节点
while (p->next != NULL)
{
//如果循环到的地址是要删除的用户,则删除该节点
if (memcmp(&(p->next->addr), &clientaddr, sizeof(clientaddr)) == 0)
{
//删除指定用户
pdel = p->next;
p->next = pdel->next;
free(pdel);
pdel = NULL;
}
else
{
//如果不是要删除的用户,则向其发送指定用户要删除的消息
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->next->addr),
sizeof(p->next->addr));
p = p->next;
}
}
}
客户端部分代码
1. 客户端和服务端一样需要定义枚举消息类型,和描述消息结构体类型;
2. 在客户端中,一进入主函数,就应该进行用户的登录,向服务端发送登录信息
- 主要是发送用户的地址和名字;
- 名字只是为了提示服务端和其他用户,有新用户登录;
- 服务端会将新用户的地址连接到链表结尾。
MSG_t msg;
//UDP客户端不用bind地址,可以直接发送自己的登陆消息
msg.type = Login; //登录
printf("please input login name>>");
fgets(msg.name, sizeof(msg.name), stdin);
if (msg.name[strlen(msg.name) - 1] == '\n')
msg.name[strlen(msg.name) - 1] = '\0';
/*UDP客户端不用绑定地址,可以直接发送自己的登陆消息,
只需要在sendto的参数中填写接收方的地址*/
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr,
sizeof(serveraddr));
3. 主进程中创建子进程并实现发送消息的功能
- 用户发送的消息中,主要是聊天消息和退出消息 “quit” 。
- 如果发送的消息字符串是 “quit” ,则先将该用户要退出的信息发送给服务端,然后再杀死父进程,子进程跳出循环后,在程序运行结束时被释放。
- 如果发送的消息是正常的聊天信息,则只发送给服务端,然后子进程继续运行。
while (1) //发
{
fgets(msg.text, sizeof(msg.text), stdin);
if (msg.text[strlen(msg.text) - 1] == '\n')
msg.text[strlen(msg.text) - 1] = '\0';
//判断发送的消息是否为“quit”退出消息
if (strncmp(msg.text, "quit", 4) == 0)
{
msg.type = Quit;
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr,
sizeof(serveraddr));
//杀死父进程
kill(getppid(), SIGKILL);
//退出循环,子进程结束
break;
}else{
msg.type = Chat;
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr,
sizeof(serveraddr));
}
}
4. 父进程中实现接收消息的功能
- 用户接收到服务端转发的消息时,会在自己的界面打印,且会打印出详细信息,比如 “谁说了什么话” 。
while (1) //收
{
if (recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL) < 0)
{
perror("recvfrom err.");
return -1;
}
printf("%s said %s\n", msg.name, msg.text);
}
实现效果
1. 运行服务端后,运行客户端进行连接;
- 客户端成功连接服务端后,在服务端上显示用户名,以及新用户连接成功的消息;
- 客户端向服务端发送消息,服务端可以打印用户名以及消息内容(我在客户端发消息时,测试打印了进程号,s 是子进程号,f 是父进程号);
2. 连接第二个客户端;
- 连接成功后,服务端会打印第二个客户端的用户名并提示连接成功,并且第一个用户也会提示新用户的连接;sprintf(msg.text, "%s login", msg.name);
strcpy(msg.name, "server");
- 不管是第一个用户发送消息还是第二个用户发送消息,另一个用户都能收到该消息,并且会提示是哪个用户发送的消息;
3.连接第三个客户端
- 三个客户端的实现效果
项目中出现的问题和解决方法
问题一:a用户退出时,只会通知其他用户a用户退出,在服务端不显示a用户退出;
解决上述问题后,当登陆的用户大于2时,除了最后一个用户退出时还是有上述问题,其他用户退出时都正常;
上述两个问题可以用一句打印用户信息的代码解决,只不过需要将这句代码需要放到程序正确的位置,否则实现了问题1后很容易出现问题2,所以需要添加一句“printf("%s quit.\n", msg.name);”,而且要添加到“服务端程序中的用户退出函数的删除链表节点的代码中”或“服务端程序主函数中的switch判断消息类型是Quit的操作代码中”。
问题二:客户端程序中,在子进程中一开始写了一句杀死父进程的代码,后来发现可能会出现孤儿进程的问题;
尝试运行了一下之后,发现输入quit退出用户的时候,终端输出“killed”,表示父进程已被结束,而且使用命令查看进程时,子进程和父进程确实都已经退出,且查看后台进程时,子进程也不存在;后来分析代码发现,子进程杀死父进程之后,就跳出了while循环,然后程序向下依次运行,最后运行到“return 0”,子进程也正常结束;也可以在子进程的break上面加一句“exit(0)”保证子进程先退出,在向下运行到程序结束。
项目整体代码展示
代码优化思路
我在编写代码时,只编写了两个文件,所以两个文件中有部分重复代码,而且服务端的函数全部跟主函数声明实现在一起,所以有以下优化思路:文章来源:https://www.toymoban.com/news/detail-444314.html
- 创建一个 “.h” 文件,将枚举的定义和描述消息的结构体定义放入该文件,且将服务端中函数的声明和实现放入该文件,服务端和客户端在头文件中包含该“.h”文件;
- Makefile文件中补充“.h”文件;
这里只展示优化前的代码;文章来源地址https://www.toymoban.com/news/detail-444314.html
服务端代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <signal.h>
//消息类型
enum type_t
{
Login = 1, //登录
Chat, //聊天
Quit, //退出
};
//定义描述消息结构体
typedef struct msg_t
{
int type; //消息类型:登录 聊天 退出
char name[32]; //姓名
char text[128]; //消息正文
} MSG_t;
//链表的节点结构体
typedef struct node_t
{
struct sockaddr_in addr; //数据域
struct node_t *next; //指针域
} link_t;
link_t *createLink(void);
void client_login(int sockfd, link_t *p, struct sockaddr_in clientaddr, MSG_t msg);
void client_chat(int sockfd, link_t *p, struct sockaddr_in clientaddr, MSG_t msg);
void client_quit(int sockfd, link_t *p, struct sockaddr_in clientaddr, MSG_t msg);
int main(int argc, char const *argv[])
{
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
//填充服务器的ip和port
struct sockaddr_in serveraddr, clientaddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[1]));
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
socklen_t len = sizeof(clientaddr);
if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("bind err.");
return -1;
}
MSG_t msg;
//创建子进程,父进程接收客户端的信息并处理,子进程转发消息
pid_t pid = fork();
if (pid < 0)
{
perror("fork err.");
return -1;
}
else if (pid == 0)
{
//创建一个空的有头单向链表
link_t *p = createLink();
while (1) //收到客户端的请求,处理请求
{
if (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&clientaddr, &len) < 0)
{
perror("recvfrom err.");
return -1;
}
switch (msg.type)
{
case Login: //登录
client_login(sockfd, p, clientaddr, msg);
break;
case Chat:
client_chat(sockfd, p, clientaddr, msg);
break;
case Quit:
client_quit(sockfd, p, clientaddr, msg);
break;
}
}
}
else
{
while (1) //服务器发通知
{
msg.type = Chat;
//给结构体中的数组成员变量赋值,一般使用strcpy进行赋值
strcpy(msg.name, "server");
//获取终端输入
fgets(msg.text, sizeof(msg.text), stdin);
//解决发送信息时,会将换行符发送过去的问题
if (msg.text[strlen(msg.text) - 1] == '\n')
msg.text[strlen(msg.text) - 1] = '\0';
//将信息发送给同一局域网的其他客户端
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr,
sizeof(serveraddr));
}
}
//程序结束时,关闭套接字描述符
close(sockfd);
return 0;
}
//链表函数 -- 创建一个空的有头单向链表
link_t *createLink(void)
{
link_t *p = (link_t *)malloc(sizeof(link_t));
if (p == NULL)
{
perror("malloc head node err.");
return NULL;
}
p->next = NULL;
return p;
}
//登录函数 -- 将客户端的clientaddr保存到链表中,循环链表告诉其他用户谁登录了
void client_login(int sockfd, link_t *p, struct sockaddr_in clientaddr, MSG_t msg)
{
//1.告诉其他用户登录的新用户是谁
strcpy(msg.name,"server"); //发送消息的人是服务端
sprintf(msg.text, "%s login!", msg.name); //服务端发送新登陆的用户名
//循环发送给之前已经登陆的用户
while (p->next != NULL)
{
p = p->next;
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
}
//上面的代码运行结束后,此时链表指针已经指向了最后一个用户
//2.创建一个新节点保存新连接的客户端地址 ,连接到链表结尾
link_t *pnew = (link_t *)malloc(sizeof(link_t));
if (pnew == NULL)
{
perror("malloc new node err.");
}
//初始化
pnew->addr = clientaddr;
pnew->next = NULL;
//链接 p是最后一个节点的地址
p->next = pnew;
}
//聊天信息发送函数 -- 将消息转发给所有的用户,除去发送消息的自己
void client_chat(int sockfd, link_t *p, struct sockaddr_in clientaddr, MSG_t msg)
{
//从链表头开始遍历
while (p->next != NULL)
{
p = p->next;
//memcmp函数,比较内存区域a和b的前n个字节
//参数--区域a,区域b,比较字节数n
//返回值--a<b返回负数,a=b返回0,a<b返回正数
if(memcmp(&(p->addr), &clientaddr, sizeof(clientaddr)) != 0)
{
//只要判断出用户地址和发送消息的用户地址不同,就将消息发送给该用户
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr),
sizeof(p->addr));
}
}
//在服务器中打印发送的消息
printf("%s said %s\n", msg.name, msg.text);
}
//退出函数 -- 将客户端的clientaddr从链表中删除,循环链表告诉其他用户谁退出了
void client_quit(int sockfd, link_t *p, struct sockaddr_in clientaddr, MSG_t msg)
{
link_t *pdel = NULL;
//从头开始遍历查找要删除的节点
while (p->next != NULL)
{
//如果循环到的地址是要删除的用户,则删除
if (memcmp(&(p->next->addr), &clientaddr, sizeof(clientaddr)) == 0)
{
//删除指定用户
pdel = p->next;
p->next = pdel->next;
free(pdel);
pdel = NULL;
}
else
{
//如果不是要删除的用户,则向其发送指定用户要删除的消息
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->next->addr),
sizeof(p->next->addr));
p = p->next;
}
}
}
客户端代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <signal.h>
//消息类型
enum type_t
{
Login = 1, //登录
Chat, //聊天
Quit, //退出
};
//定义描述消息结构体
typedef struct msg_t
{
int type; //消息类型:登录 聊天 退出
char name[32]; //姓名
char text[128]; //消息正文
} MSG_t;
int main(int argc, char const *argv[])
{
int sockfd;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
//填充结构体
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
MSG_t msg;
//UDP客户端不用bind地址,可以直接发送自己的登陆消息
msg.type = Login; //登录
printf("please input login name>>");
fgets(msg.name, sizeof(msg.name), stdin);
if (msg.name[strlen(msg.name) - 1] == '\n')
msg.name[strlen(msg.name) - 1] = '\0';
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr,
sizeof(serveraddr));
//创建父子进程:父进程收消息 子进程发送消息
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';
//判断发送的消息是否为“quit”退出消息
if (strncmp(msg.text, "quit", 4) == 0)
{
msg.type = Quit;
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr,
sizeof(serveraddr));
//杀死父进程
kill(getppid(), SIGKILL);
//退出循环,子进程结束
break;
}
else
{
msg.type = Chat;
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr,
sizeof(serveraddr));
}
}
}
else
{
while (1) //收
{
if (recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL) < 0)
{
perror("recvfrom err.");
return -1;
}
printf("%s said %s\n", msg.name, msg.text);
}
}
close(sockfd);
return 0;
}
到了这里,关于C语言实现--基于UDP的多人在线聊天室的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!