1.需求
1.如果有用户登录,其他用户可以收到这个人的登录信息
2.如果有人发送信息,其他用户可以收到这个人的群聊信息
’3.如果有人下线,其他用户可以收到这个人的下线信息
4.服务器可以发送系统信息
2.流程图
3.服务器的搭建
1)宏函数、链表结构体及全局变量
//打印错误信息的宏函数
#define ERR_MSG(msg) do{\
fprintf(stderr, " __%d__ ", __LINE__);\
perror(msg);\
}while(0)
//链表所用的结构体
typedef struct Node
{
union{
struct sockaddr_in add;
int len;
};
char name[30];
struct Node *next;
}Linklist;
//将套接字变量和链表头设置成全局变量便于在子线程使用
int sfd;
Linklist* L;
2)功能函数
2.1申请节点
Linklist * node_buy(struct sockaddr_in e,char name[])
{
Linklist *p = (Linklist *)malloc(sizeof(Linklist));
if(NULL == p)
{
printf("节点申请失败\n");
return NULL;
}
p->add = e;
strcpy(p->name,name);
p->next = NULL;
return p;
}
2.2判空
int list_empty(Linklist *L)
{
//1表示空 0表示非空
return NULL == L->next ? 1:0;
}
2.3尾插
int list_intsrt_tail(Linklist *L,struct sockaddr_in e,char name[])
{
if(NULL==L)
{
printf("所给的链表不合法\n");
return -1;
}
Linklist *p = node_buy(e,name);
Linklist *q = L;
while(q->next != NULL)
{
q = q->next;
}
q->next = p;
L->len++;
return 0;
}
2.4头删
int list_delete_head(Linklist *L)
{
//判断逻辑
if(NULL==L || list_empty(L))
{
printf("删除失败\n");
return -1;
}
//头删
Linklist *p = L->next; //标记
L->next = p->next; //孤立
free(p); //踢开
p=NULL;
//表的变化
L->len--;
printf("头删成功\n");
return 0;
}
2.5销毁表
void list_delete_all(Linklist *L)
{
if(NULL==L)
{
return;
}
//不断调用头删,将节点进行删除
while(L->next != NULL)
{
list_delete_head(L);
}
//将头节点释放
free(L);
L=NULL;
return;
}
3)子线程用于发送系统信息
void* callBack(void* arg)
{
char str[128] = "";
char buf[140] = "";
while(1)
{
Linklist *q = L; //遍历指针
//从终端读取要发送的信息
bzero(str, sizeof(str));
fgets(str, sizeof(str), stdin);
buf[strlen(str)-1] = 0;
//转换格式
bzero(buf, sizeof(buf));
int res = sprintf(buf,"**system**%s",str);
if(res < 0)
{
ERR_MSG("sprintf");
return NULL;
}
while(q->next!=NULL)
{
q=q->next;
//发送系统信息
if(sendto(sfd,buf,sizeof(buf),0,(struct sockaddr*)&(q->add),sizeof(q->add))<0)
{
ERR_MSG("sendto");
return NULL;
}
}
}
}
4)主线程实现其余功能(登录请求、群聊请求和下线请求)
char buf[128] = "";
char str[255] ="";
int flag=0;
int i=0;
while(1)
{
flag=0; //用于判断用户是否已经在线
i=0; //定位用户的位置
bzero(buf, sizeof(buf));
//接收
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, &addrlen) < 0)
{
ERR_MSG("recvfrom");
return -1;
}
//遍历链表查看发送消息的用户是否已经在线
Linklist *q = L;
while(q != NULL)
{
if(q->add.sin_addr.s_addr==cin.sin_addr.s_addr && q->add.sin_port==cin.sin_port)
{
flag=1; //查找到有该用户时将flag的值进行改变
break;
}
q = q->next;
i++;
}
if(flag==0) //flag的值未改变表示该次信息是用户的登录
{
bzero(str,sizeof(str));
//将新用户的信息打印到服务器终端
printf("%s [%s : %d]登录\n",buf,inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
//调用尾插函数
list_intsrt_tail(L,cin,buf);
//使用sprintf函数将上线提醒消息打包
int res=sprintf(str,"%s上线了",buf);
if (res<0)
{
ERR_MSG("sprintf");
return -1;
}
//因为链表结点的增加是使用尾插实现
//所以发送上线提醒只需遍历到最后第二个结点
Linklist *q2 = L->next;
while(q2->next != NULL)
{
//发送上线提醒
if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(q2->add),sizeof(q2->add))<0)
{
ERR_MSG("sendto");
return -1;
}
q2=q2->next;
}
}
else
{
if(strcasecmp(buf,"quit") == 0)
{
//遍历链表找到存放申请下线的用户的节点和前驱节点
Linklist *k = L->next; //用户信息节点
Linklist *kq = L; //前驱节点
bzero(str,sizeof(str));
for(int j=0; j<i-1; j++)
{
k=k->next;
kq=kq->next;
}
int res=sprintf(str,"%s下线了",k->name);
if (res<0)
{
ERR_MSG("sprintf");
return -1;
}
printf("%s [%s : %d]下线\n",k->name,inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
//利用前驱节点删除该用户的信息节点
kq->next = k->next;
free(k);
k=NULL;
L->len--;
//删除该用户节点后将下线信息发送给剩余用户
Linklist *kp = L;
while(kp->next != NULL)
{
kp=kp->next;
if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(kp->add),sizeof(kp->add))<0)
{
ERR_MSG("sendto");
return -1;
}
}
}
else
{
//将用户传输来的信息的格式做转换
Linklist *k = L;
bzero(str,sizeof(str));
for(int j=0; j<i; j++)
{
k=k->next;
}
int res=sprintf(str,"%s说:%s",k->name,buf);
if (res<0)
{
ERR_MSG("sprintf");
return -1;
}
//遍历链表
Linklist *k2 = L;
while(k2->next!=NULL)
{
k2=k2->next;
//跳过发送信息的用户本身
if(k2->add.sin_addr.s_addr==cin.sin_addr.s_addr && k2->add.sin_port==cin.sin_port)
{
continue;
}
//给其他用户各发一份转换后的信息
if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(k2->add),sizeof(k2->add))<0)
{
ERR_MSG("sendto");
return -1;
}
}
}
}
}
5)服务器完整代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <net/if.h>
#include <pthread.h>
//打印错误信息的宏函数
#define ERR_MSG(msg) do{\
fprintf(stderr, " __%d__ ", __LINE__);\
perror(msg);\
}while(0)
//链表所用的结构体
typedef struct Node
{
union{
struct sockaddr_in add;
int len;
};
char name[30];
struct Node *next;
}Linklist;
//将套接字变量和链表头设置成全局变量便于在子线程使用
int sfd;
Linklist* L;
//申请节点
Linklist * node_buy(struct sockaddr_in e,char name[])
{
Linklist *p = (Linklist *)malloc(sizeof(Linklist));
if(NULL == p)
{
printf("节点申请失败\n");
return NULL;
}
p->add = e;
strcpy(p->name,name);
p->next = NULL;
return p;
}
//判空
int list_empty(Linklist *L)
{
//1表示空 0表示非空
return NULL == L->next ? 1:0;
}
//尾插
int list_intsrt_tail(Linklist *L,struct sockaddr_in e,char name[])
{
if(NULL==L)
{
printf("所给的链表不合法\n");
return -1;
}
Linklist *p = node_buy(e,name);
Linklist *q = L;
while(q->next != NULL)
{
q = q->next;
}
q->next = p;
L->len++;
return 0;
}
//头删
int list_delete_head(Linklist *L)
{
//判断逻辑
if(NULL==L || list_empty(L))
{
printf("删除失败\n");
return -1;
}
//头删
Linklist *p = L->next; //标记
L->next = p->next; //孤立
free(p); //踢开
p=NULL;
//表的变化
L->len--;
printf("头删成功\n");
return 0;
}
//销毁表
void list_delete_all(Linklist *L)
{
if(NULL==L)
{
return;
}
//不断调用头删,将节点进行删除
while(L->next != NULL)
{
list_delete_head(L);
}
//将头节点释放
free(L);
L=NULL;
return;
}
//子线程用于发送系统信息
void* callBack(void* arg)
{
char str[128] = "";
char buf[140] = "";
while(1)
{
Linklist *q = L; //遍历指针
//从终端读取要发送的信息
bzero(str, sizeof(str));
fgets(str, sizeof(str), stdin);
buf[strlen(str)-1] = 0;
//转换格式
bzero(buf, sizeof(buf));
int res = sprintf(buf,"**system**%s",str);
if(res < 0)
{
ERR_MSG("sprintf");
return NULL;
}
while(q->next!=NULL)
{
q=q->next;
//发送系统信息
if(sendto(sfd,buf,sizeof(buf),0,(struct sockaddr*)&(q->add),sizeof(q->add))<0)
{
ERR_MSG("sendto");
return NULL;
}
}
}
}
int main(int argc, const char *argv[])
{
//创建链表头
L = (Linklist*)malloc(sizeof(Linklist));
if(NULL == L)
{
printf("创建失败\n");
return -1;
}
//初始化
L->len=0;
L->next=NULL;
printf("聊天室服务器创建成功\n");
//判断传参个数的合法性
if(argc < 3)
{
printf("请输入IP和端口号");
}
//创建报式套接字
sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd < 0)
{
ERR_MSG("socket");
return -1;
}
//验证端口号的合法性
int port = atoi(argv[2]);
if(port < 1024 || port > 49151)
{
printf("端口号不正确\n");
return -1;
}
//填充服务器的IP地址以及端口号
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
sin.sin_addr.s_addr = inet_addr(argv[1]);
//绑定服务器的地址信息结构体
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("bind");
return -1;
}
struct sockaddr_in cin; //存储接收到的数据包来自哪里
socklen_t addrlen = sizeof(cin);
//创建一个线程
pthread_t tid;
if(pthread_create(&tid,NULL,callBack,NULL)!=0)
{
perror("pthread_create");
return -1;
}
char buf[128] = "";
char str[255] ="";
int flag=0;
int i=0;
while(1)
{
flag=0; //用于判断用户是否已经在线
i=0; //定位用户的位置
bzero(buf, sizeof(buf));
//接收
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, &addrlen) < 0)
{
ERR_MSG("recvfrom");
return -1;
}
//遍历链表查看发送消息的用户是否已经在线
Linklist *q = L;
while(q != NULL)
{
if(q->add.sin_addr.s_addr==cin.sin_addr.s_addr && q->add.sin_port==cin.sin_port)
{
flag=1; //查找到有该用户时将flag的值进行改变
break;
}
q = q->next;
i++;
}
if(flag==0) //flag的值未改变表示该次信息是用户的登录
{
bzero(str,sizeof(str));
//将新用户的信息打印到服务器终端
printf("%s [%s : %d]登录\n",buf,inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
//调用尾插函数
list_intsrt_tail(L,cin,buf);
//使用sprintf函数将上线提醒消息打包
int res=sprintf(str,"%s上线了",buf);
if (res<0)
{
ERR_MSG("sprintf");
return -1;
}
//因为链表结点的增加是使用尾插实现
//所以发送上线提醒只需遍历到最后第二个结点
Linklist *q2 = L->next;
while(q2->next != NULL)
{
//发送上线提醒
if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(q2->add),sizeof(q2->add))<0)
{
ERR_MSG("sendto");
return -1;
}
q2=q2->next;
}
}
else
{
if(strcasecmp(buf,"quit") == 0)
{
//遍历链表找到存放申请下线的用户的节点和前驱节点
Linklist *k = L->next; //用户信息节点
Linklist *kq = L; //前驱节点
bzero(str,sizeof(str));
for(int j=0; j<i-1; j++)
{
k=k->next;
kq=kq->next;
}
int res=sprintf(str,"%s下线了",k->name);
if (res<0)
{
ERR_MSG("sprintf");
return -1;
}
printf("%s [%s : %d]下线\n",k->name,inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
//利用前驱节点删除该用户的信息节点
kq->next = k->next;
free(k);
k=NULL;
L->len--;
//删除该用户节点后将下线信息发送给剩余用户
Linklist *kp = L;
while(kp->next != NULL)
{
kp=kp->next;
if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(kp->add),sizeof(kp->add))<0)
{
ERR_MSG("sendto");
return -1;
}
}
}
else
{
//将用户传输来的信息的格式做转换
Linklist *k = L;
bzero(str,sizeof(str));
for(int j=0; j<i; j++)
{
k=k->next;
}
int res=sprintf(str,"%s说:%s",k->name,buf);
if (res<0)
{
ERR_MSG("sprintf");
return -1;
}
//遍历链表
Linklist *k2 = L;
while(k2->next!=NULL)
{
k2=k2->next;
//跳过发送信息的用户本身
if(k2->add.sin_addr.s_addr==cin.sin_addr.s_addr && k2->add.sin_port==cin.sin_port)
{
continue;
}
//给其他用户各发一份转换后的信息
if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(k2->add),sizeof(k2->add))<0)
{
ERR_MSG("sendto");
return -1;
}
}
}
}
}
//关闭套接字
close(sfd);
//调用函数销毁链表
list_delete_all(L);
return 0;
}
4.客户端的搭建
1)宏函数及全局变量
#define ERR_MSG(msg) do{\
fprintf(stderr, " __%d__ ", __LINE__);\
perror(msg);\
}while(0)
int sfd;
2)子线程用于接收服务器发送的信息并将其打印到终端上
void* callBack(void* arg)
{
char buf[128]="";
struct sockaddr_in* sin=(struct sockaddr_in*)arg;
socklen_t addrlen = sizeof(*sin);
while(1)
{
bzero(buf, sizeof(buf));
//接收信息
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)sin,&addrlen) < 0)
{
ERR_MSG("recvfrom");
return NULL;
}
fprintf(stderr,"%s\n",buf);
}
}
3)发送上线申请
char buf[128] = "";
printf("请输入姓名>>>");
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = 0;
//将数据包发送给服务器,所以地址信息结构体需要填服务器的信息
if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("sendto");
return -1;
}
printf("欢迎进入聊天室\n");
4)主线程发送聊天内容
while(1)
{
//从终端读取要发送的信息
bzero(buf, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = 0;
//将数据包发送给服务器,所以地址信息结构体需要填服务器的信息
if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("sendto");
return -1;
}
//判断输入的是否为quit,是则退出循环
if(strcasecmp(buf,"quit") == 0)
{
printf("准备退出聊天室\n");
break;
}
printf("信息已发送\n");
}
5)客户端完整代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
//打印错误信息的宏函数
#define ERR_MSG(msg) do{\
fprintf(stderr, " __%d__ ", __LINE__);\
perror(msg);\
}while(0)
int sfd;
//子线程用于接收服务器发送的信息并将其打印到终端上
void* callBack(void* arg)
{
char buf[128]="";
struct sockaddr_in* sin=(struct sockaddr_in*)arg;
socklen_t addrlen = sizeof(*sin);
while(1)
{
bzero(buf, sizeof(buf));
//接收信息
if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)sin,&addrlen) < 0)
{
ERR_MSG("recvfrom");
return NULL;
}
fprintf(stderr,"%s\n",buf);
}
}
int main(int argc, const char *argv[])
{
//判断传参个数的合法性
if(argc < 3)
{
printf("请输入IP和端口号");
}
//创建报式套接字
sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd < 0)
{
ERR_MSG("socket");
return -1;
}
//验证端口号的合法性
int port = atoi(argv[2]);
if(port < 1024 || port > 49151)
{
printf("端口号不正确\n");
return -1;
}
//填充接收端的IP地址以及端口号
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
sin.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(sin);
char buf[128] = "";
printf("请输入姓名>>>");
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = 0;
//将数据包发送给服务器,所以地址信息结构体需要填服务器的信息
if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("sendto");
return -1;
}
printf("欢迎进入聊天室\n");
//创建一个线程
pthread_t tid;
if(pthread_create(&tid,NULL,callBack,(void*)&sin)!=0)
{
perror("pthread_create");
return -1;
}
while(1)
{
//从终端读取要发送的信息
bzero(buf, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = 0;
//将数据包发送给服务器,所以地址信息结构体需要填服务器的信息
if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("sendto");
return -1;
}
//判断输入的是否为quit,是则退出循环
if(strcasecmp(buf,"quit") == 0)
{
printf("准备退出聊天室\n");
break;
}
printf("信息已发送\n");
}
//关闭套接字
close(sfd);
return 0;
}