有新用户登录,其他在线的用户可以收到登录信息
有用户发送群聊消息,其他在线的用户可以收到群聊信息
有用户退出,其他在线的用户可以收到退出信息
服务器可以发送系统信息
1.已经加入群聊的用户可以看到新加入群聊的用户
2.用户退出或者断线,其他用户也可以看到
3.server端可以发送系统消息给所有在聊天室的用户
客户端登录之后,为了实现一边发送数据一边接收数据,可以使用多进程或者多线程。
服务器既可以发送系统信息,又可以接收客户端信息并处理,可以使用多进程或者多线程。
服务器需要给多个用户发送数据,所以需要保存每一个用户的信息,使用链表来保存。
数据传输的时候要定义结构体,结构体中包含操作码、用户名以及数据。
对登录聊天室的用户,需要保存用户信息,本文用链式存储来存放用户信息,因此需要用到链队列,动态分配空间。链式队列的便利之处就在于往队列中插入用户信息的时候,不用想数组那样可能需要大量移动数据。
代码如下:
#ifndef __LINKLIST_H__
#define __LINKLIST_H__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(msg) \
do { \
printf("%s:%s:%d\n", __FILE__, __func__, __LINE__); \
perror(msg); \
exit(-1); \
} while (0)
#define N 128
#define LEN 128
#define NUM_USR 64
#define datatype int
//自定义结构体,用来保存所有连接的客户的IP地址、端口号以及acceptfd文件描述符这三个参数
typedef struct info {
struct sockaddr_in clientaddr;
int acceptfd;
char name[16];
char named;
struct info* next;
char sayHi;
} usr_info_t;
usr_info_t* info_head;
usr_info_t* LinkListNodeCreate(void);
int LinkListInsertHead(usr_info_t* head, usr_info_t* node);
usr_info_t* LinkListSearchUsrByAcceptfd(usr_info_t* h, int acceptfd);
#endif
代码如下:
#include "LinkList.h"
#include
/*
*功能:创建单链表
*参数:
* @:无
*返回值:成功返回单链表节点的首地址,失败返回NULL
*注:创建链表节点,将指针域指向NULL,并将节点内容清零
*/
usr_info_t* LinkListNodeCreate(void)
{
usr_info_t* h;
h = (usr_info_t*)malloc(sizeof(*h));
if (h == NULL) {
printf("alloc memory error\n");
return NULL;
}
memset(h, 0, sizeof(*h));
h->next = NULL;
return h;
}
/*
*功能:单链表按照头插法插入数据
*参数:
* @head: 用户信息链表头节点的首地址
* @node: 要插入的节点的地址
*返回值:成功返回0
*/
int LinkListInsertHead(usr_info_t* head, usr_info_t* node)
{
// 1.将node插入到head中即可
node->next = head->next;
head->next = node;
// 2.成功返回0
return 0;
}
/*
*功能:通过Acceptfd查询用户数据
*参数:
* @h:用户信息链表头指针
* @acceptfd:accept函数产生的文件描述符
*返回值:成功返回0,失败返回-1
*/
usr_info_t* LinkListSearchUsrByAcceptfd(usr_info_t* h, int acceptfd)
{
while (h->next != NULL) {
if (h->next->acceptfd == acceptfd) {
return h->next;
}
h = h->next;
}
printf("用户不存在,查询失败\n");
}
代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ERRLOG(msg) \
do { \
printf("%s:%s:%d\n", __FILE__, __func__, __LINE__); \
perror(msg); \
exit(-1); \
} while (0)
void recycle()
{
wait(NULL);
}
void tuichu()
{
exit(0);
}
int main(int argc, const char* argv[])
{
//参数合理性检查
if (3 != argc) {
printf("Usage : %s \n" , argv[0]);
exit(-1);
}
//创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd) {
ERRLOG("socket error");
}
//填充服务器网络信息结构体
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]);
socklen_t serveraddr_len = sizeof(serveraddr);
//与服务器建立连接
if (-1 == connect(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len)) {
ERRLOG("connect error");
}
printf("与服务器连接成功..\n");
char buff[128] = { 0 };
int num = 0;
//等待子进程退出信号SIGCHLD,回收子进程资源
signal(SIGCHLD, recycle);
//需要分出来一个子进程,专门用于接收消息,父进程用于发送消息
pid_t pid = fork();
int named = 0;
while (1) {
if (pid == 0) { //子进程,接收消息
signal(SIGINT, tuichu); //子进程捕获到SIGINT信号就退出
//接收应答消息
memset(buff, 0, sizeof(buff));
if (-1 == (num = recv(sockfd, buff, 128, 0))) {
ERRLOG("recv ersockfdror");
}
printf("%s\n", buff);
//如果收到自己的退出消息,就结束子进程
} else { //父进程,发送消息
if (named == 0) {
printf("请输入群聊名称:\n");
named = 1;
} else if (named == 1) {
printf("请再次输入群聊名称:\n");
named = 2;
}
fgets(buff, 128, stdin);
if (strlen(buff) != 0) {
buff[strlen(buff) - 1] = '\0';
}
//如果用户输入quit就退出
if (!strcmp(buff, "quit")) {
//如果退出了,最后也要发送数据"quit"
if (-1 == send(sockfd, buff, 128, 0)) {
ERRLOG("send error");
}
char buf[22] = { 0 };
sprintf(buf, "kill %d", pid);
system(buf);
wait(NULL);
break;
}
//发送数据
if (-1 == send(sockfd, buff, 128, 0)) {
ERRLOG("send error");
}
}
}
//关闭套接字
close(sockfd);
return 0;
}
代码如下:
#include "LinkList.h"
int max_fd = 0;
int acceptfd = 0;
int ret = 0;
int i = 0;
int nbytes = 0;
char buff[N] = { 0 };
int loop = 0;
char send_buf[128] = { 0 };
//创建要监视的文件描述集合
fd_set readfds; //母本
fd_set readfds_temp; //给select擦除用的
void* sendThread(void* arg)
{
char sys_send_buf[256] = { 0 };
while (1) {
memset(send_buf, 0, sizeof(send_buf));
scanf("%s", send_buf);
/*for (loop = 4; loop < max_fd + 1 && ret != 0; loop++)
这样写是不对的,因为,和主线程共享ret,主线程中的ret每次结束都是会减到0的,
所以如果这么写,一次循环也不会进入!!!!
*/
memset(sys_send_buf, 0, sizeof(sys_send_buf));
sprintf(sys_send_buf, "[系统消息]:%s\n", send_buf);
printf("%s\n", sys_send_buf);
for (loop = 4; loop < max_fd + 1; loop++) {
if (FD_ISSET(loop, &readfds)) {
if (-1 == send(loop, sys_send_buf, N, 0)) {
ERRLOG("send error");
}
}
}
}
}
int main(int argc, const char* argv[])
{
//参数合理性检查
if (3 != argc) {
printf("Usage : %s \n" , argv[0]);
exit(-1);
}
//创建套接字 流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd) {
ERRLOG("socket error");
}
//填充网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
socklen_t serveraddr_len = sizeof(serveraddr);
//绑定
if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len)) {
ERRLOG("bind error");
}
//将套接字设置成被动监听状态
if (-1 == listen(sockfd, 5)) {
ERRLOG("listen error");
}
//清空集合
FD_ZERO(&readfds);
FD_ZERO(&readfds_temp);
//将sockfd添加到集合中
FD_SET(sockfd, &readfds);
//更新最大文件描述符
max_fd = max_fd > sockfd ? max_fd : sockfd;
socklen_t clientaddr_len = sizeof(struct sockaddr_in);
usr_info_t* usr_info_head = LinkListNodeCreate();
usr_info_t* usr_info_node;
usr_info_t* usr = NULL;
pthread_t tid;
int err_code;
if (0 != (err_code = pthread_create(&tid, NULL, sendThread, NULL))) {
printf("pthread_create error %s\n", strerror(err_code));
exit(-1);
}
while (1) {
readfds_temp = readfds;
if (-1 == (ret = select(max_fd + 1, &readfds_temp, NULL, NULL, NULL))) {
ERRLOG("select error");
}
//遍历集合 看哪个文件描述符就绪了
for (i = 3; i < max_fd + 1 && ret != 0; i++) {
if (FD_ISSET(i, &readfds_temp)) {
if (sockfd == i) {
//说明有新客户端连接了,创建一个新节点保存客户信息
usr_info_node = LinkListNodeCreate();
if (-1 == (acceptfd = accept(sockfd, (struct sockaddr*)&usr_info_node->clientaddr, &clientaddr_len))) {
ERRLOG("accept error");
}
usr_info_node->acceptfd = acceptfd;
LinkListInsertHead(usr_info_head, usr_info_node); //用户信息入队
printf("客户端[%d]号连接到服务器..\n", usr_info_node->acceptfd);
printf("客户端[%d]已连接\n", ntohs(usr_info_node->clientaddr.sin_port));
memset(send_buf, 0, sizeof(send_buf));
//将新客户端的acceptfd加入到集合
FD_SET(acceptfd, &readfds);
//更新最大文件描述符
max_fd = max_fd > acceptfd ? max_fd : acceptfd;
} else {
//说明有客户端发来数据了
usr = LinkListSearchUsrByAcceptfd(usr_info_head, i); //先根据acceptfd找一下用户信息
if (-1 == (nbytes = recv(i, buff, N, 0))) {
ERRLOG("recv error");
} else if (0 == nbytes) {
printf("客户端[%d]断开连接..\n", i);
memset(send_buf, 0, sizeof(send_buf));
sprintf(send_buf, "用户[%s]断开连接..", usr->name);
for (loop = 4; loop < max_fd + 1 && ret != 0; loop++) {
if (FD_ISSET(loop, &readfds) && loop != i) {
if (-1 == send(loop, send_buf, N, 0)) {
ERRLOG("send error");
}
}
}
//将该客户端的文件描述符在集合中删除
FD_CLR(i, &readfds);
close(i);
continue;
}
if (!strncmp(buff, "quit", 4)) {
printf("客户端[%d]退出了..\n", i);
memset(send_buf, 0, sizeof(send_buf));
sprintf(send_buf, "用户[%s]退出了..", usr->name);
for (loop = 4; loop < max_fd + 1 && ret != 0; loop++) {
if (FD_ISSET(loop, &readfds) && loop != i) {
if (-1 == send(loop, send_buf, N, 0)) {
ERRLOG("send error");
}
}
}
//将该客户端的文件描述符在集合中删除
FD_CLR(i, &readfds);
close(i);
continue;
}
//第一次发过来的是用户的名字,所以,应该保存一下
if (usr->named != 9) {
strcpy(usr->name, buff);
usr->named = 9;
continue;
}
printf("客户端[%d]发来数据[%s]..\n", i, buff);
//组装应答
if (usr->sayHi == 7) {
memset(send_buf, 0, sizeof(send_buf));
sprintf(send_buf, "用户[%s]:%s\n", usr->name, buff);
} else { //只有第一次才会对所有已经加入群聊的用户显示**加入群聊
memset(send_buf, 0, sizeof(send_buf));
sprintf(send_buf, "用户[%s]加入群聊..\n", usr->name);
usr->sayHi = 7;
}
//如果用户的acceptfd还在readfds里,就接收本次用户发送的消息.注意不能从3开始,3是sockfd,给sockfd发消息,会出错!!!!
for (loop = 4; loop < max_fd + 1 && ret != 0; loop++) {
if (FD_ISSET(loop, &readfds) && loop != i) {
if (-1 == send(loop, send_buf, N, 0)) {
ERRLOG("send error");
}
}
}
ret--;
}
}
}
}
close(sockfd);
return 0;
}