udp_server_recv.h:
/***********************************************************************
* File name :
* Module Name : (omit)
* Author : George
* Create Date :
* Abstract Description: (description omit)
*
* ------------------------- Revision History -------------------------
* No Version Date Revise By Item Deacription
* 1 V1.0 0000.00.00 George (omit) (omit)
*
***********************************************************************/
#ifndef CHAT_ROOM
#define CHAT_ROOM
/***********************************************************************
* (1)Debug switch Section
***********************************************************************/
/***********************************************************************
* (2)Include File Section
***********************************************************************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
/***********************************************************************
* (3)Macro Define Section
***********************************************************************/
/// 主线程返回值
#define OK 1
#define ERROR 0
/// 本机IP和端口
#if 1
#define IP "192.168.8.45"
#else
#define IP "192.168.71.50"
#endif
#define PORT 6666
/// 登录登出信息
#define LOGIN "usr enter"
#define LOGOUT "usr quit"
/***********************************************************************
* (4)Struct(Data Types) Define Section
***********************************************************************/
/// 规定的传送数据包
typedef struct info
{
char usrname[32];// 用户名 // 32
char usrmsg[128];// 数据信息 // 128
}Info; // 160
/// 在线用户信息结点
typedef struct node
{
struct sockaddr_in cin; // 客户端地址结构体 // ?
struct info inform; // 用户登录上传的数据包 // 160
struct node *next; // 下一个在线用户信息结点 // 8
}UsrNode; // 168+?
/// 在线信息列表表头
typedef struct head
{
int len; // 用户信息表的长度[初始为0] // 4
UsrNode *next; // 第一个用户信息结点[初始为NULL] // 8
}UsrList; // 12
/***********************************************************************
* (5)Prototype Declare Section
***********************************************************************/
/// 服务端
void *thread_login(void *arg);
void *thread_logout(void *arg);
void *thread_output(void *arg);
void *thread_admin(void *arg);
/// 客户端
void *thread_send(void *arg);
void *thread_recv(void *arg);
#endif
udp_server_recv.c:
// 项目需求:
// 1. 如果有用户登录,其他用户可以收到这个人的登录信息
// 2. 如果有人下线,其他用户可以收到这个人的下线信息
// 3. 如果有人发送信息,其他用户可以收到这个人的群聊信息
// 4. 服务器可以发送系统信息
#include "udp_server_recv.h"
UsrList *usrListHead = NULL;// 用户信息链表表头[全局:方便线程获取]
int sfd = 0; // 套接字返回的文件描述符[全局:方便线程获取]
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int main(int argc, char const *argv[])
{
// 初始化
int ret = 0;// 用于函数返回int型的运行判断
ssize_t res = 0;// 用于函数返回ssize_t型的运行判断
// 第一步:创建UDP套接字
sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd < 0){perror("socket"); return ERROR;}
else{printf("创建UDP套接字[成功]\n");}
// 第二步:快速启用端口
int reuse = 1;
ret = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
if(ret < 0){perror("setsockopt"); return ERROR;}
else{printf("快速启用端口[成功]\n");}
// 第三步:填充sin服务器结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT);
sin.sin_addr.s_addr = inet_addr(IP);
// 第四步:绑定sin服务器结构体与UDP套接字
ret = bind(sfd, (struct sockaddr *)&sin, sizeof(sin));
if(ret < 0){perror("bind"); return ERROR;}
else{printf("绑定sin服务器结构体与UDP套接字[成功]\n");}
// 第五步:会话循环使用信息的初始化
struct sockaddr_in cin;// 用于接收recv返回的客户端地址结构体
memset(&cin, 0, sizeof(cin));
cin.sin_family = AF_INET;
socklen_t addrlen = sizeof(cin);
pthread_t tid = 0;// 用于接收创建线程返回的线程id
// 第六步:会话循环维护单链表
/// 创建用户列表[让用户信息链表表头指向堆区空间]
usrListHead = (UsrList *)malloc(sizeof(UsrList));
usrListHead->len = 0;
usrListHead->next = NULL;
/// 创建传送数据包
Info info; // 用于接收recv返回的数据包
memset(&info, 0, sizeof(info));
/// 测试信息
printf("服务端准备就绪...\n");
///
ret = pthread_create(&tid, NULL, thread_admin, NULL);
if(ret < 0){perror("pthread_create"); printf("管理员线程未上线...\n");}
else{printf("管理员线程上线...\n");}
while(1)
{
memset(&info, 0, sizeof(info));
// 第一步:阻塞等待客户端的信息
res = recvfrom(sfd, (void *)&info, sizeof(info), 0, (struct sockaddr *)&cin, &addrlen);
// 第二步:判断接收函数正确运行
if(res < 0){perror("recvfrom"); continue;}
else if(res < sizeof(info)){printf("客户端传输数据包格式有误...丢弃\n"); continue;}
// 封装用户信息到本地[注意]指针操作
UsrNode *usr = (UsrNode *)malloc(sizeof(UsrNode));
usr->cin = cin; // 将cin内容复制到堆空间
usr->inform = info;// 将inform内容复制到堆空间
usr->next = NULL;// 让用户结点指向挂载到空指针
// 用于测试[注意]字节序
printf("接收到来自{%s[%d] %s}新的信息...\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), info.usrname);
// 第三步:判断客户端的信息{1. 登录;2. 下线;3. 发言}
if(0 == strcmp(info.usrmsg, LOGIN))
{
// 通知在线用户有新用户登录[项目需求:1.] >>> 封装函数 thread_login
ret = pthread_create(&tid, NULL, thread_login, (void *)usr);
if(ret < 0){perror("pthread_create"); continue;}
else
{
pthread_detach(tid);
// 用于测试[注意]字节序
printf("新用户登录 {%s[%d] %s}\n", \
inet_ntoa(usr->cin.sin_addr), ntohs(usr->cin.sin_port), usr->inform.usrname);
}
}// 1. 登录[结束]
else if(0 == strcmp(info.usrmsg, LOGOUT))
{
// 通知在线用户有用户登出[项目需求:2.] >>> 封装函数 thread_logout
ret = pthread_create(&tid, NULL, thread_logout, (void *)usr);
if(ret < 0){perror("pthread_create"); continue;}
else
{
pthread_detach(tid);
// 用于测试[注意]字节序
printf("{该用户登出 %s[%d] %s}\n", \
inet_ntoa(usr->cin.sin_addr), ntohs(usr->cin.sin_port), usr->inform.usrname);
}
}// 2. 下线[结束]
else
{
// 向在线用户发送刚接收的群聊发言[项目需求:3.] >>> 封装函数 pthread_create
ret = pthread_create(&tid, NULL, thread_output, (void *)usr);
if(ret < 0){perror("pthread_create"); continue;}
else
{
pthread_detach(tid);
// 用于测试[注意]字节序
printf("{该用户发言 %s[%d] %s}\n", \
inet_ntoa(usr->cin.sin_addr), ntohs(usr->cin.sin_port), usr->inform.usrname);
}
}// 2. 群发[结束]
// 第三步[结束]
}// 第六步:会话循环维护单链表[结束]
// 最后:服务器关闭
/// 下线管理员线程
/// 循环释放堆区用户信息单链表
close(sfd);
return 0;
}
// 若用户上线,先遍历单链表发送新用户上线信息,再将新用户插入单链表[实现]
void *thread_login(void *arg)
{
// 第一步:初始化
ssize_t res = 0; // 用于函数返回ssize_t型的运行判断
UsrNode *usr = (UsrNode *)arg; // 接收传入的用户信息结点堆区地址
UsrNode *cur = usrListHead->next; // 移动cur指针遍历用户链表,指向头结点后继结点
Info info; // 用于发送send的数据包
strcpy(info.usrname, usr->inform.usrname); // 数据包[usrname]
strcpy(info.usrmsg, "用户登录"); // 数据包[usrmsg]
pthread_mutex_lock(&lock); printf("thread_login正在操作用户信息列表...\n");
// 第二步:判断是否为新用户
for(int i = 0; i < usrListHead->len; i++, cur=cur->next)
{
// 若结构体完全一样则该用户存在[注意]memcmp函数需要结构体被赋值前先清零,不然比对出错
if(0 == memcmp(&(usr->cin), &(cur->cin), sizeof(struct sockaddr_in)))
{
printf("用户%s已存在\n", usr->inform.usrname);
printf("thread_login结束任务...\n");
pthread_exit(NULL);
}
}// 第二步[结束]
// 第三步:遍历单链表发送新用户上线信息
cur = usrListHead->next; // 移动cur指针遍历用户链表,指向头结点后继结点
for(int i = 0; i < usrListHead->len; i++, cur=cur->next)
{
// 发送通知信息
res = sendto(sfd, (void *)&info, sizeof(info), 0, (struct sockaddr *)&(cur->cin), sizeof(cur->cin));
if(res < 0){perror("sendto"); continue;}
else{printf("已通知%d人\n", i+1);}
}
// 第四步:将新用户插入单链表[头插]
usr->next = usrListHead->next;
usrListHead->next = usr;
usrListHead->len++;
pthread_mutex_unlock(&lock); printf("thread_login操作完毕用户信息列表...\n");
// 第五步:退出线程任务
printf("thread_login结束任务...\n");
pthread_exit(NULL);
}
// 若用户下线,先将用户从单链表删除,再遍历单链表发送该用户下线信息
void *thread_logout(void *arg)
{
// if(NULL == arg)
if(0 == usrListHead->len){printf("目前无用户\n"); return NULL;}
// 第一步:初始化
ssize_t res = 0; // 用于函数返回ssize_t型的运行判断
UsrNode *usr = (UsrNode *)arg; // 接收传入的用户信息结点堆区地址
UsrNode *cur = usrListHead->next; // 移动cur指针遍历用户链表,指向要删除的结点前驱
UsrNode *del = usrListHead->next; // 移动del指针遍历用户链表,指向要删除的结点
Info info; // 用于发送send的数据包
strcpy(info.usrname, usr->inform.usrname); // 数据包[usrname]
strcpy(info.usrmsg, "用户下线"); // 数据包[usrmsg]
int count = 1;
pthread_mutex_lock(&lock); printf("thread_logout正在操作用户信息列表...\n");
int len = usrListHead->len;
// 第二步:判断是否为存在用户
/// 若为第一个结点
if(0 == memcmp(&(usr->cin), &(cur->cin), sizeof(struct sockaddr_in)))
{
printf("__%d__\n", __LINE__);
printf("用户%s将被删除\n", usr->inform.usrname);
// 第三步:将用户从单链表删除
usrListHead->next = del->next;
usrListHead->len--;
free(del);
cur = usrListHead->next;
for(int i = 0; i < usrListHead->len; i++, cur=cur->next)
{
res = sendto(sfd, (void *)&info, sizeof(info), 0, (struct sockaddr *)&(cur->cin), sizeof(cur->cin));
if(res < 0){perror("sendto"); continue;}
else{printf("已通知第%d人{%s}\n", count++, cur->inform.usrname);}
}
}
else/// 若为其它结点
{
cur = usrListHead->next;
// 通知第一个结点
printf("__%d__\n", __LINE__);
res = sendto(sfd, (void *)&info, sizeof(info), 0, (struct sockaddr *)&(cur->cin), sizeof(cur->cin));
if(res < 0){perror("sendto");}
else{printf("已通知第%d人{%s}\n", count++, cur->inform.usrname);}
// 遍历后面的结点
del = cur->next;
for(int i = 1; i < len; i++)
{
if((0 == memcmp(&(usr->cin), &(del->cin), sizeof(struct sockaddr_in))))// 头结点未删除时运行
{
printf("__%d__\n", __LINE__);
printf("用户%s将被删除\n", usr->inform.usrname);
// 第三步:将用户从单链表删除
cur->next = del->next;
usrListHead->len--;
free(del);
del = cur->next;
continue;
}
else
{
// 第四步:发送通知信息
printf("__%d__\n", __LINE__);
res = sendto(sfd, (void *)&info, sizeof(info), 0, (struct sockaddr *)&(cur->next->cin), sizeof(cur->next->cin));
if(res < 0){perror("sendto"); continue;}
else{printf("已通知第%d人{%s}\n", count++, cur->next->inform.usrname);}
cur = cur->next;
del = cur->next;
}
}
}
pthread_mutex_unlock(&lock);
// 第四步:退出线程任务
printf("thread_logout结束任务...\n"); printf("thread_logout操作完毕用户信息列表...\n");
pthread_exit(NULL);
}
//若用户发送其它信息,则遍历单链表并向除发言用户结构体的结点用户发送该用户发言
void *thread_output(void *arg)
{
if(0 == usrListHead->len){printf("目前无用户\n"); return NULL;}
// 第一步:初始化
ssize_t res = 0; // 用于函数返回ssize_t型的运行判断
UsrNode *usr = (UsrNode *)arg; // 接收传入的用户信息结点堆区地址
UsrNode *cur = usrListHead->next; // 移动cur指针遍历用户链表,指向头结点后继结点
pthread_mutex_lock(&lock); printf("thread_output正在操作用户信息列表...\n");
// 第二步:发送信息给其它用户
for(int i = 0, count = 1; i < usrListHead->len; i++, cur=cur->next)
{
if(0 == memcmp(&(usr->cin), &(cur->cin), sizeof(struct sockaddr_in)))
{
continue;
}
// 发送通知信息
res = sendto(sfd, (void *)&(usr->inform), sizeof(usr->inform), 0, (struct sockaddr *)&(cur->cin), sizeof(cur->cin));
if(res < 0){perror("sendto"); continue;}
else{printf("已通知第%d人\n", count++);}
}
pthread_mutex_unlock(&lock); printf("thread_output操作完毕用户信息列表...\n");
// 第三步:退出线程任务
printf("thread_output结束任务...\n");
pthread_exit(NULL);
}
// 服务器可以发送系统信息
void *thread_admin(void *arg)
{
ssize_t res = 0;
Info info;
memset(&info, 0, sizeof(info));
strcpy(info.usrname, "SYSTEM$admin");
while(1)
{
bzero(info.usrmsg, sizeof(info.usrmsg));
fgets(info.usrmsg, sizeof(info.usrmsg), stdin);
if(0 == strlen(info.usrmsg)){continue;}// 丢弃空数据包
if(0 == usrListHead->len){printf("目前无用户\n"); return NULL;}
pthread_mutex_lock(&lock);
UsrNode *cur = usrListHead->next;
for(int i = 0, count = 1; i < usrListHead->len; i++, cur=cur->next)
{
res = sendto(sfd, (void *)&info, sizeof(info), 0, (struct sockaddr *)&(cur->cin), sizeof(cur->cin));
if(res < 0){perror("sendto"); continue;}
else{printf("已通知第%d人\n", count++);}
}
pthread_mutex_unlock(&lock);
}
pthread_exit(NULL);
}
udp_client_send.c:
// 项目需求:
// 1. 如果有用户登录,其他用户可以收到这个人的登录信息
// 2. 如果有人下线,其他用户可以收到这个人的下线信息
// 3. 如果有人发送信息,其他用户可以收到这个人的群聊信息
// 4. 服务器可以发送系统信息
#include "udp_server_recv.h"
int cfd = 0; // 套接字返回的文件描述符[全局:方便线程获取]
char usrname[32] = "";
int main(int argc, char const *argv[])
{
// 初始化
int ret = 0;// 用于函数返回int型的运行判断
ssize_t res = 0;// 用于函数返回ssize_t型的运行判断
pthread_t tid_snd = 0;// 用于接收创建线程返回的线程id
pthread_t tid_rcv = 0;// 用于接收创建线程返回的线程id
Info info; // 用于发送send的数据包
memset(&info, 0, sizeof(info));
// 第一步:创建UDP套接字
cfd = socket(AF_INET, SOCK_DGRAM, 0);
if(cfd < 0){perror("socket"); return ERROR;}
else{printf("创建UDP套接字[成功]\n");}
// 第二步:快速启用端口
int reuse = 1;
ret = setsockopt(cfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
if(ret < 0){perror("setsockopt"); return ERROR;}
else{printf("快速启用端口[成功]\n");}
// 第三步:填充cin服务器结构体[omit]
// 第四步:绑定cin服务器结构体与UDP套接字[omit]
// 第五步:填充sin服务器结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(PORT);
sin.sin_addr.s_addr = inet_addr(IP);
// 第六步:会话循环[thread_send线程发送/thread_recv线程接收]
/// 连接聊天室服务器
printf("请输入用户名: ");
scanf("%s", info.usrname); strcpy(usrname, info.usrname);
strcpy(info.usrmsg, LOGIN);
res = sendto(cfd, (void *)&info, sizeof(info), 0, (struct sockaddr *)&sin, sizeof(sin));
/// 接收连接聊天室成功通知[omit]
printf("客户端准备就绪...\n");
/// 开始会话
ret = pthread_create(&tid_snd, NULL, thread_send, (void *)&sin);
if(ret < 0){perror("pthread_create");}
else{printf("thread_sned投入运行...\n");}
ret = pthread_create(&tid_rcv, NULL, thread_recv, (void *)&sin);
if(ret < 0){perror("pthread_create");}
else{printf("thread_recv投入运行...\n");}
// 最后:客户端关闭
pthread_join(tid_snd, NULL);
pthread_cancel(tid_rcv);
pthread_join(tid_rcv, NULL);
// 5. close
close(cfd);
return 0;
}
void *thread_recv(void *arg)
{
// 初始化
struct sockaddr_in sin = *((struct sockaddr_in *)arg);
ssize_t res = 0;// 用于函数返回ssize_t型的运行判断
Info info;
memset(&info, 0, sizeof(info));
while(1)
{
// 第一步:阻塞等待服务端的信息
res = recvfrom(cfd, (void *)&info, sizeof(info), 0, NULL, NULL);
// 第二步:判断接收函数正确运行
if(res < 0){perror("recvfrom"); continue;}
else if(res < sizeof(info)){printf("服务端传输数据包格式有误...丢弃\n"); continue;}
else{if(0 != strlen(info.usrmsg))printf("%s >>> %s\n", info.usrname, info.usrmsg);}
}
pthread_exit(NULL);
}
void *thread_send(void *arg)
{
// 初始化
struct sockaddr_in sin = *((struct sockaddr_in *)arg);
ssize_t res = 0;// 用于函数返回ssize_t型的运行判断
Info info;
memset(&info, 0, sizeof(info));
strcpy(info.usrname, usrname);
while(1)
{
bzero(info.usrmsg, sizeof(info.usrmsg));
// 第一步:阻塞等待终端输入
// scanf("%s", info.usrmsg);
fgets(info.usrmsg, sizeof(info.usrmsg), stdin);
info.usrmsg[strlen(info.usrmsg)-1] = '\0'; // \n
if(0 == strlen(info.usrmsg)){continue;}
// 出口
if(0 == strcasecmp(info.usrmsg, "quit"))
{
bzero(info.usrmsg, sizeof(info.usrmsg));
strcpy(info.usrmsg, LOGOUT);
}
// 第二步:发送信息给聊天室服务器
res = sendto(cfd, (void *)&info, sizeof(info), 0, (struct sockaddr *)&sin, sizeof(sin));
if(res < 0){perror("sendto"); continue;}
else{printf("已发送\n");}
// 出口
if(0 == strcasecmp(info.usrmsg, LOGOUT))
{
printf("发送线程退出...\n");
break;
}
}
pthread_exit(NULL);
}