该项目是一个可以实现多人同时在线的聊天室网络应用,用户各自在登录时自定义一个网名,并以这个网名在多人聊天室中与别人聊天,同时每个用户可以通过用户名辨别其他用户。主要的功能特点是能容纳多人在线聊天,分为服务器端和客户端两大模块。
关于socket套接字的使用方法可以参考这篇文章:https://blog.csdn.net/mhyasadj/article/details/131181974
2.1服务器端
用户能够以“ client name”的命令行程序运行,并以“name”的用户名登陆聊天室(不需要密码)。客户端能允许用户发言,并实时接收服务器的广播信息。当客户端输入“ quit”时,退出客户端。
首先是创建服务器端模型,本项目通过TCP传输协议流程,进行服务器端的socket创建、地址填充、绑定和监听设置。同时为了能够实现多人在线聊天的功能,本项目通过SO_REUSEADDR选项,以便可以及时重新使用处于 TIME_WAIT 状态的地址和端口,从而实现端口复用,达到多人在线聊天的效果。
第二步进行处理客户端的链接请求,服务器初始化时,将 cli_fd 数组中的元素初始化为 -1,即重置清空聊天位置。当收到客户端的链接请求后,会通过 sem_getvalue 函数获取当前信已连接客户端的数量值。然后找到一个没有被占用的聊天位置,并使用 accept 函数接受客户端的连接请求,再将返回的新的客户端套接字描述符赋给 cli_fd[index]。若描述符为负值,请求失败。同时此项目中使用sem_num(即人数上限)来限制连接服务器的客户端数。若客户端连接数达到峰值则拒绝连接,并发送连接失败的消息;若未达到峰值,则回复连接成功的消息,并创建一个server线程来处理客户端的发送过来的信息。
第三步是群发函数的实现,本项目于服务器端创建一个群发函数,使用 for 循环来遍历群友列表。如果群友列表中的 cli_fd 不等于 -1,则表示该群友的套接字描述符 cli_fd 是有效的。-1 表示没有群友,可能已退出或未被占用。当群友存在时,在服务器端打印群发内容和输出目标客户端的套接字描述符(由于收发多方都会受到群发消息,所以服务器会输出所有在线客户端的套接字描述符)。然后服务器端会将消息内容发送至所有的在线客户端,实现群发消息的项目功能。
最后是完整的服务器端代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE (5120)
#define SEM_SIZE (20) // 群聊上限人数
// 信号量--判断群聊人数
sem_t sem;
// 服务端文件描述符
int svr_fd;
// 存储群友,多一个是为了当群人满时,空一个出来接发信息
int cli_fd[SEM_SIZE + 1] = {};
struct client
{
/* data */
char buf[BUF_SIZE]; // message
char name[BUF_SIZE]; // name
int client_fd; // fd
};
//结构体数组
struct client clients[SEM_SIZE];
// 群发函数
void *send_all(char *buf)
{
for (int i = 0; i < SEM_SIZE; i++)
{
// 若值为-1,则没有此群友,表示已经退出或未被占有
if (-1 != cli_fd[i])
{
printf("%s\n", buf);
printf("send to %d\n", cli_fd[i]);
write(cli_fd[i], buf, strlen(buf) + 1);
}
}
}
// 服务端接收函数
void *server(void *arg)
{
int fd = *(int *)arg;
char buf[BUF_SIZE];
char name[BUF_SIZE];
char ts[BUF_SIZE] = {0};
// 获取昵称
read(fd, clients[fd].name, sizeof(name));
clients[fd].client_fd = fd;
// printf("clients[fd].name = %s\n", clients[fd].name);
sprintf(ts, "\n [system]欢迎 %s 进入群聊", clients[fd].name);
send_all(ts);
while(1)
{
// 接收信息,无信息时将阻塞
int recv_size = read(fd, clients[fd].buf, sizeof(buf));
// 收到退出信息
if (0 >= recv || NULL != strstr(clients[fd].buf, "quit"))
{
sprintf(ts, "[system]欢送 %s 离开群聊\n", clients[fd].name);
int index = 0;
// 找到要退出的那个人,并将其置为-1
for (; index < SEM_SIZE; index++)
{
if (cli_fd[index] == fd)
{
cli_fd[index] = -1;
break;
}
}
// 群发XXX退出聊天室提示消息
send_all(ts);
// 群友退出,信号量+1
int n;
sem_post(&sem);
sem_getvalue(&sem, &n);
printf("[system] %s 离开群聊,当前剩余%d人\n", clients[fd].name, SEM_SIZE - n);
strcpy(clients[fd].buf, "quit");
write(fd, clients[fd].buf, strlen(clients[fd].buf) + 1);
close(fd);
pthread_exit(NULL);
}
// 群发
send_all(clients[fd].buf);
}
}
/**
* quit
*/
void sigint(int signum)
{
close(svr_fd);
sem_destroy(&sem);
printf("\n[system]服务器关闭\n");
exit(0);
}
int main()
{
signal(SIGINT, sigint);
// 初始化信号量,群聊上限SEM_SIZE人
sem_init(&sem, 0, SEM_SIZE);
// 创建socket对象
printf("[system]服务器已启动\n");
svr_fd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > svr_fd)
{
perror("socket");
return -1;
}
//端口复用函数:解决端口号被系统占用的情况
int on = 1;
int gg = setsockopt(svr_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if(gg==-1)
{
perror("setsockopt");
return -1;
}
// 准备通信地址(自己)
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
socklen_t addrlen = sizeof(addr);
// 绑定socket对象与地址
if (bind(svr_fd, (struct sockaddr *)&addr, addrlen))
{
perror("bind");
return -1;
}
// 设置监听和排除数量
if (listen(svr_fd, 10))
{
perror("listen");
return -1;
}
printf("[system]等待客户端链接\n");
// 将初始值置全为-1,表示该聊天位置没有人占领
memset(cli_fd, -1, sizeof(cli_fd));
while(1)
{
int sem_num;
sem_getvalue(&sem, &sem_num);
// 找到没有人占领的聊天位
int index = 0;
while (-1 != cli_fd[index])
index++;
cli_fd[index] = accept(svr_fd, (struct sockaddr *)&addr, &addrlen);//接收客户端请求
if (0 > cli_fd[index])
{
perror("accept");
return -1;
}
char buf[BUF_SIZE];
if (0 >= sem_num)
{
printf("[system]人数已满,%d号客户端链接失败\n", cli_fd[index]);
sprintf(buf, "[system]人数已满,客户端链接失败");
write(cli_fd[index], buf, strlen(buf) + 1);
close(cli_fd[index]);
cli_fd[index] = -1;
}
else
{
sem_trywait(&sem);
sem_getvalue(&sem, &sem_num);
char msg[BUF_SIZE] = {};
printf("[system]%d号客户端链接成功,当前聊天人数%d人\n", cli_fd[index], SEM_SIZE - sem_num);
sprintf(msg, "[system]客户端链接成功,当前聊天人数%d人,输入quit可退出\n", SEM_SIZE - sem_num);
write(cli_fd[index], msg, strlen(msg) + 1);
// 创建线程客户端
pthread_t tid;
pthread_create(&tid, NULL, server, &cli_fd[index]);
}
}
}
2.2客户端
客户端能够同时接受多用户的登陆,并将每个用户的聊天信息广播到其他用户,广播的时候会附加上发言的用户名。
首先是创建客户端模型,按照创建服务器端类似的流程来进行创建客户端,创建完毕后,客户端向服务器发送链接请求。
第二步是昵称输入功能,客户端创建字符串并输入昵称,然后使用write函数将昵称发送给服务器。之后服务器端使用read函数从套接字fd中读取客户端发送的昵称信息,并将其存储到clients[fd].client_fd变量中,然后使用sprintf函数将欢迎信息格式化为字符串,将欢迎信息发送给所有已连接的客户端。
第三步是客户端发收数据的实现,发送数据前,客户端定义一个名为 msg 的字符数组,用于存储从键盘输入并格式化后的字符串,并使用 sprintf 函数将格式化的字符串存储到 msg 数组中,之后通过 write 函数将 msg 字符串发送给指定的客户端套接字描述符 cli_fd并将其发送到服务器端,之后服务器端再通过调用群发函数将字符串广播至每一个客户端中,实现多人群聊间的信息相互收发。
第四步是程序退出的设计实现,当其中一个客户端接受到从键盘输入的“quit”并发送给服务器端时,在判断接收数据正常且为quit的情况下,服务器端广播准备退出通信的服务器端的昵称,之后更新信号量的值,在服务器端屏幕上打印群聊的剩余在线情况。而服务器端将显示“通信结束”字样。
最后是完整的客户端代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE (5120)
/**
* 读取server转发过来的消息
*/
void* client_read(void* arg)
{
int cli_fd = *(int*)arg;
char buf[BUF_SIZE];
//接收数据
while(1)
{
int recv_size = read(cli_fd,buf,BUF_SIZE);
if(0 < recv_size && 0 == strcmp(buf,"quit"))
{
close(cli_fd);
pthread_exit(NULL);
}
else if(0 >= recv_size && 0 != strcmp(buf,"quit"))
{
printf("服务器已断开,请按任意键退出\n");
close(cli_fd);
pthread_exit(NULL);
}
printf("%s\n",buf);
}
}
int main()
{
//创建socket对象
int cli_fd = socket(AF_INET,SOCK_STREAM,0);
if(0 > cli_fd)
{
perror("socket");
return -1;
}
//准备通信地址(服务端)
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");//此处填写自己的ip地址
socklen_t addrlen = sizeof(addr);
//链接服务端
printf("链接服务器中\n");
if(connect(cli_fd,(struct sockaddr*)&addr,addrlen))
{
perror("connect");
return -1;
}
char buf[BUF_SIZE];
read(cli_fd,buf,BUF_SIZE);
if(NULL == strstr(buf,"链接成功"))
{
printf("群聊人已满,请稍后再来\n");
close(cli_fd);
return 0;
}
printf("%s\n",buf);
//链接成功,创建客户端
pthread_t tid;
pthread_create(&tid,NULL,client_read,&cli_fd);
//输入昵称
char name[BUF_SIZE] = {};
printf("请输入你的昵称:");
gets(name);
write(cli_fd,name,strlen(name)+1);
//发送数据
while(1)
{
gets(buf); // 获取键盘字符串
char msg[BUF_SIZE]; // 存储格式化后的字符串
sprintf(msg,"用户%s:%s",name,buf); // 输出字符串
//把msg发送到cli_fd
int send_size = write(cli_fd,msg,strlen(msg)+1);
// 如果字符串是quit就退出
if(0 < send_size && 0 == strcmp(buf,"quit"))
{
printf("已手动结束通讯\n");
break;
return 0;
}
else if(0 >= send_size && 0 != strcmp(buf,"quit"))
{
printf("已退出程序\n");
break;
return 0;
}
}
}
3.1服务器初始化
3.2客户端中用户取名与聊天
3.3客户端主动退出与服务器强制关闭功能