网络编程DAY 5(UDP聊天室)

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);
}

 

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值