UDP网络聊天室

UDP网络聊天室程序中问题记录:

并发如何实现?
可以通过fork来实现,但并发不仅仅局限于fork。在引用中提到了利用fork来实现简单的并发服务器,这是因为fork可以创建一个与父进程一样的子进程,从而实现并发处理客户端请求。但是并发还可以通过其他方式实现,比如使用线程、进程池、协程等。
使用线程可以实现并发,线程是轻量级的执行单元,可以在同一个进程中并发执行多个任务。线程之间共享进程的资源,可以通过共享内存来进行通信。
使用进程池也可以实现并发,进程池是一组预先创建好的进程,可以重复使用来处理多个任务。进程池中的进程可以并发执行任务,从而实现并发处理。
使用协程也可以实现并发,协程是一种轻量级的线程,可以在同一个线程中实现并发执行多个任务。协程之间可以通过yield和send来进行通信。
所以,并发不仅仅是通过fork来实现,还可以使用线程、进程池、协程等方式来实现。具体选择哪种方式取决于具体的需求和场景。

  1. man socket
    我们使用IPV4 查看手册 man 7 ip 即可查看
    在这里插入图片描述

  2. recvfrom函数之“transport endpoint is not connected”创建父子进程 pid=fork()

  3. 客户端client.c 不能写bind (绑定到套接字)
    //bind写上会报错:bind error: Address already in use
    在这里插入图片描述

UDP网络聊天室的实现

值得思考:server.c的do_quit()函数的写法

链表删除的时候,链表要定位在 删除结点 的前一个位置,
所以我们判断的是ptemp->next->clientaddr
这样写的话,
删除后不能执行ptemp = ptemp->next;这条语句,因为会跳过被删除结点的后一个结点。

//假设删除D
//         t        当t走到d的时候,再用链表删除D,就来不及了,因为删除结点时,要找到被删除结点 的前一个结点.
//   A B C D E F G
// t                所以我们要先判断t的next的位置是否是D,不是的话就先发送,再到next
//   A B C D E F G
ptemp = ptemp->next; //不能写在if语句的后面,因为当把D删除之后,t会走到E的位置,而判断的时候会判断t->next也就是F 这样就会丢下E没判断
//     t        
//   ABCEFG

代码:

server.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>

// UDP聊天室 服务器
#define ERRLOG(msg)                                         \
    do                                                      \
    {                                                       \
        printf("%s %s %d\n", __FILE__, __func__, __LINE__); \
        perror(msg);                                        \
        exit(-1);                                           \
    } while (0)
// 定义用户信息结构体
typedef struct MSG
{
    char type; // 类型:L登录 C群聊 Q退出
    char name[32];
    char txt[128]; // 发送的内容
} msg_t;

// 定义存储客户数据的链表
typedef struct NODE
{
    struct sockaddr_in clientaddr; // 数据域--客户信息
    struct NODE *next;             // 指针域
} node_t;

void do_login(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr);
void do_chat(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr);
void do_quit(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr);
void create_node(node_t **p);

int main(int argc, char const *argv[])
{
    // 入参合理性检查
    if (3 != argc)
    {
        printf("Usage : %s <ip> <port>\n", argv[0]);
        return 0;
    }
    // 1.创建用户数据报套接字
    int sockfd = 0;
    if (-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0)))
        ERRLOG("socket error");

    // 2.填充服务器的网络信息结构体
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(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);

    // 3.将套接字与服务器的网络信息结构体绑定
    if (-1 == (bind(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)))
        ERRLOG("bind error");

    // 定义结构体保存客户端信息
    struct sockaddr_in clientaddr;
    memset(&clientaddr, 0, sizeof(clientaddr));
    socklen_t clientaddr_len = sizeof(clientaddr);

    msg_t msg;
    // 定义链表类型
    node_t *phead = NULL;
    create_node(&phead);

    // 4.收发数据  recvfrom sendto
    pid_t pid = 0;
    if (-1 == (pid = fork()))
        ERRLOG("fork error");
    else if (0 == pid)
    {
        // 子进程 循环接收数据 并处理(登录操作L 群聊操作C 退出操作Q)
        // 服务器用链表来保存用户数据
        
        while (1){
            memset(&msg, 0, sizeof(msg));
            if (-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&clientaddr, &clientaddr_len))
                ERRLOG("recvfrom error");   //需要保存客户端信息
            printf("客户端[%s:%d]发来数据[%s:%s]\n",
                   inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), msg.name, msg.txt);

            switch (msg.type){
                case 'L':
                    do_login(phead, msg, sockfd, clientaddr);
                    break;
                case 'C':
                    do_chat(phead, msg, sockfd, clientaddr);
                    break;
                case 'Q':
                    do_quit(phead, msg, sockfd, clientaddr);
                    break;
            }
        }
    }
    else if (0 < pid)
    {
        //负责在终端获取系统消息 并发送给所有客户端
        //这时候要注意 不能直接发送 因为在子进程接收客户端消息之前
        //父子进程就完成fork()了 此时父进程是什么都没有的 父子进程独立
        //这里比较好的处理方式是:
        // 把父进程当做一个客户端 以群聊的方式 把系统消息发给子进程(也就是服务器)
    
        msg.type ='C';
        strcpy(msg.name,"server");
        while(1){
            memset(msg.txt,0,128);//在终端获取字符之前要memset先清理一下
            fgets(msg.txt,sizeof(msg.txt),stdin);
            msg.txt[strlen(msg.txt)-1]='\0';
            if(-1 == sendto(sockfd,&msg.txt,sizeof(msg.txt),0,(struct sockaddr*)&serveraddr,serveraddr_len))
                ERRLOG("sendto error");
        }
    }
    // 5.关闭套接字
    close(sockfd);
    return 0;
}
// 创建链表结点的函数
void create_node(node_t **p)
{
    *p = (node_t *)malloc(sizeof(node_t));
    memset(*p, 0, sizeof(node_t));
}

void do_login(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr)
{
    //先将 xxx 加入群聊 的消息发给在线的所有人
    node_t *ptemp = phead;
    while(ptemp->next != NULL){
        ptemp = ptemp->next;
        if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->clientaddr), sizeof(ptemp->clientaddr)))
            ERRLOG("sendto error");
    }
    //再将新登录的客户端加入到链表--头插
    node_t *pnew = NULL;
    create_node(&pnew);
    pnew->clientaddr = clientaddr;
    pnew->next = phead->next;
    phead->next = pnew;
    return ;
}
void do_chat(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr)
{
    //遍历链表 将群聊的消息发送给除了自己之外的所有人
    node_t *ptemp = phead;
    while(ptemp->next != NULL){
        ptemp = ptemp->next;
        //需要判断不是自己的时候在sendto 
        if(memcmp(&clientaddr,&(ptemp->clientaddr),sizeof(clientaddr))){
            if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->clientaddr), sizeof(ptemp->clientaddr)))
                ERRLOG("sendto error");
        }
    }
    return;
}
void do_quit(node_t *phead, msg_t msg, int sockfd, struct sockaddr_in clientaddr)
{
    //将 xxx 退出群聊的消息发给其他人  将自己在链表中删除
    //遍历链表,如果不是自己,就转发消息,如果是自己 就删除节点
    node_t *ptemp = phead;
    while(ptemp->next != NULL){
        if(memcmp(&clientaddr,&(ptemp->next->clientaddr),sizeof(clientaddr))){
            //不是自己 发送消息
            //假设删除D
            //         t        当t走到d的时候,再用链表删除D,就来不及了,因为删除结点时,要找到被删除结点 的前一个结点.
            //   A B C D E F G
            // t                所以我们要先判断t的next的位置是否是D,不是的话就先发送,再到next
            //   A B C D E F G
            if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(ptemp->next->clientaddr), sizeof(ptemp->next->clientaddr)))
                ERRLOG("sendto error");
            ptemp = ptemp->next; //不能写在if语句的后面,因为当把D删除之后,t会走到E的位置,而判断的时候会判断t->next也就是F 这样就会丢下E没判断
            //      t        
            //   ABCEFG
        }else{
            //是自己了 则把自己在链表里删除
            node_t *pdel=ptemp->next;
            ptemp->next=pdel->next;
            free(pdel);
            pdel=NULL;
        }
    }
    return ;
}

client.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <signal.h>
#include <arpa/inet.h>

#define ERRLOG(msg) do{\
        printf("%s %s %d\n", __FILE__, __func__, __LINE__);\
        perror(msg);\
        exit(-1);\
    }while(0)

typedef struct MSG{
    char type;//'L' 登录   'C' 群聊  'Q' 退出
    char name[32];
    char txt[128];
}msg_t;

int main(int argc,const char * argv[])
{
    //入参合理性检查
    if(3 != argc){
        printf("Usage : %s <ip> <port>\n", argv[0]);
        exit(-1);
    }
    //创建套接字
    int sockfd = 0;
    if(-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0)))
        ERRLOG("socket error");
    //填充服务器的网络信息结构体
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(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);

    //bind写上会报错:bind error: Address already in use  
    //客户端不能指定端口,如果客户端去指定端口会可能出现端口已经被占用导致出错,而需要修改
    // × 3.将套接字与服务器的网络信息结构体绑定
    // × if (-1 == (bind(sockfd, (struct sockaddr *)&serveraddr, serveraddr_len)))
    // ×   ERRLOG("bind error");
    msg_t msg;
    memset(&msg, 0, sizeof(msg));

    //输入用户名 完成登录操作
    printf("请输入用户名:");
    fgets(msg.name, sizeof(msg.name), stdin);
    msg.name[strlen(msg.name)-1] = '\0';
    msg.type = 'L';
    strcpy(msg.txt, "加入群聊");
    
    if(-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, serveraddr_len))
        ERRLOG("sendto error");

    pid_t pid = 0;
    if(-1 == (pid = fork())){
        ERRLOG("fork error");
    }else if(0 == pid){//子进程
        //负责接收服务器发来的消息并打印到终端
        while(1){
            memset(&msg,0,sizeof(msg));
            if(-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL))
                ERRLOG("recvfrom error");
            printf("%s : %s\n", msg.name, msg.txt);
        }
    }else if(0 < pid){//父
        //负责在终端获取聊天内容发给服务器
        while(1){
            //此处只需要清理txt内容即可
            memset(&msg.txt,0,sizeof(msg.txt));
            fgets(msg.txt,sizeof(msg.txt),stdin);
            msg.txt[strlen(msg.txt)-1]='\0';
           
            //设置一个 如果输入quit,则退出群聊
            if(!strcmp(msg.txt,"quit")){
                //则修改msg.type的类型为Q
                msg.type = 'Q';
                strcpy(msg.txt,"退出群聊");
            }else{
                msg.type = 'C';
            }

            if(-1 == sendto(sockfd,&msg,sizeof(msg),0,(struct sockaddr *)&serveraddr,serveraddr_len)){
                ERRLOG("sendto error");
            }
            //如果接收的类型是 Q 的话
            if( 'Q'== msg.type ){
                //父进程退出之前,先给子进程信号让子进程退出--否则子进程就变成孤儿进程了
                kill(pid, SIGKILL);
                break;
            }
        }
    }
    close(sockfd);
    return 0;
}
  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值