c语言实现tcp 服务器设计_TCP与链表实现了聊天室服务器

148c88e6416cb66d11c9e90db643131f.gif

首先说一下Select函数的作用,再来说说服务器......

0827ebf4887eaf1f3066f906120988a1.png

Select在Socket编程中还是比较重要的,可以起到多路复用,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect、accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。

f0b35238b1ee90b6e4e1e549ed2869c4.png

函数原型

int select(int nfds, fd_set *readfds, fd_set *writefds,              fd_set *exceptfds, struct timeval *timeout);

参数解释

nfds: 要检测的文件描述符的范围,为文件最大描述符+1

readfds: 包含所有可能因状态变成可读而触发select函数返回的文件描述符

writefds: 包含所有可能因状态变成可写而触发select函数返回的文件描述符

exceptfds: 包含所有可能因状态发生异常而触发select函数返回的文件描述符

0827ebf4887eaf1f3066f906120988a1.png

可选择图一或图二进行对服务器分析理解:

图一:

c6eea8dd231c84dfad9fa699440194b4.png

图二:

0946be9e3b735458f9e04f835bf36fdc.png

0827ebf4887eaf1f3066f906120988a1.png

实现功能

1.有人上线时,更新用户列表并通知所有人

2. 有人下线时,更新用户列表并通知所有人

3.将用户发来的消息,群发给其他人

4.私聊消息

0827ebf4887eaf1f3066f906120988a1.png

过程简述分析:

1.创建TCP套接字,同时也准备好地址结构体,接着,绑定套接字和地址,然后将套接字设置为监听状态,顺便设置最大同时连接个数(客户端);

2.准备好用户聊表(小浩用的是内核链表,你也可自写链表);

3.循环多路监控所有的套接字,即将监听套接字置入读就绪集合中等待读就绪,同时将用户链表中的所有connfd,置入读就绪集合中等待读就绪,多路监听所有集合中得所有套接字

4.监听中,如果有新的连接请求响应,然后接受请求,并产生一个新用户节点,将新用户节点插入用户节点,并给新加入得用户发欢迎信息和提醒其他用户,该用户上线了;如该用户下线,并通知当前用户列表,并更新聊表。

5.判断是否有用户发来数据,然后将数据发给其他人,进行条件选择,群发、私聊。

        具体操作,请阅读代码。

0827ebf4887eaf1f3066f906120988a1.png

代码如下,左右滑动阅读,注释良好,可读性强:

/*公众号:小浩笔记*/#include "wrap.h"#include "kernel_list.h"// TCP聊天室服务器(多路复用)#define IN  0#define OUT 1// 用户节点struct user{    int ID;    int connfd;    struct sockaddr_in addr;    struct list_head list;};// 初始化一个空的用户链表struct user *init_list(){    struct user *clients = calloc(1, sizeof(struct user));    if(clients != NULL)    {        INIT_LIST_HEAD(&clients->list);    }    return clients;}// 有人上线时,更新用户列表并通知所有人// 有人下线时,更新用户列表并通知所有人void inform(struct user *clients, int ID, int how){    char *allClients = calloc(1, 2048);    snprintf(allClients+strlen(allClients), 2048, "\n===========\n");    snprintf(allClients+strlen(allClients), 2048, "当前活跃用户:\n");    // 将所有的现存的用户ID放入一个列表中    struct user *p;    list_for_each_entry(p, &clients->list, list)    {        if(p->ID == ID && how == IN)            snprintf(allClients+strlen(allClients), 2048, "[%d](新人,欢迎你!)\n", ID);        else if(p->ID == ID && how == OUT)            snprintf(allClients+strlen(allClients), 2048, "[%d](下线,再见!)\n", ID);        else            snprintf(allClients+strlen(allClients), 2048, "[%d]\n", p->ID);    }    snprintf(allClients+strlen(allClients), 2048, "===========\n\n");    // 将当前服务器上的活跃用户列表发给所有人    list_for_each_entry(p, &clients->list, list)    {        write(p->connfd, allClients, strlen(allClients));    }    printf("%s", allClients);    free(allClients);#ifdef DEBUG    printf("[%s:%d] 更新用户列表并通知完毕\n", __FUNCTION__, __LINE__);#endif}// 将用户p发来的消息,群发给其他人void broadcastMsg(struct user *clients, struct user *sender, const char *msg){    struct list_head *pos;    list_for_each(pos, &clients->list)    {        struct user *p = list_entry(pos, struct user, list);        // 跳过信息发送者本人        if(sender->ID == p->ID)        {            continue;        }        // 群发用户的消息msg        int n = write(p->connfd, msg, strlen(msg));#ifdef DEBUG        printf("已向[%d]发送消息%d个字节消息:%s\n", p->ID, n, msg);#endif    }}// 私聊消息void privateTalk(struct user *clients, struct user *sender,                 int recvID, const char *msg){    printf("私聊消息:%s", msg);    struct user *p;    list_for_each_entry(p, &clients->list, list)    {        if(p->ID == recvID)        {            int n = write(p->connfd, msg, strlen(msg));#ifdef DEBUG            printf("已向[%d]发送消息%d个字节消息:%s\n", p->ID, n, msg);#endif            return;        }    }    // 找不到信息的接收者,要发送一个OOB通知发送者该情况    send(sender->connfd, "x", 1, MSG_OOB);}// 聊天室服务器int main(int argc, char **argv) // ./main IP PORT{    if(argc != 3)    {        printf("参数错误!用法: \n");        exit(0);    }    // 1,创建TCP套接字    int sockfd = Socket(AF_INET, SOCK_STREAM, 0);#ifdef DEBUG    printf("[%s:%d] 创建套接字成功\n", __FUNCTION__, __LINE__);#endif    int on = 1;    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));    // 2,准备好地址结构体    struct sockaddr_in addr;    socklen_t len = sizeof(addr);    bzero(&addr, len);    addr.sin_family = AF_INET;    addr.sin_addr.s_addr = inet_addr(argv[1]);    addr.sin_port = htons(atoi(argv[2]));    // 3,绑定套接字和地址    Bind(sockfd, (struct sockaddr *)&addr, len);#ifdef DEBUG    printf("[%s:%d] 绑定地址成功\n", __FUNCTION__, __LINE__);#endif    // 4,将套接字设置为监听状态,顺便设置最大同时连接个数    listen(sockfd, 3);    // 5,准备好用户链表    struct user *clients = init_list();    struct list_head *pos;    struct user *p;    // 6,循环多路监控所有的套接字    fd_set rset; // 读就绪套接字集合    fd_set wset; // 写就绪套接字集合    fd_set eset; // 异常就绪套接字集合    while(1)    {        FD_ZERO(&rset);        FD_ZERO(&wset);        FD_ZERO(&eset);        // 将监听套接字,置入rset中等待读就绪        int maxfd = sockfd;        FD_SET(sockfd, &rset);        // 将用户链表中的所有connfd,置入rset中等待读就绪        list_for_each(pos, &clients->list)        {            p = list_entry(pos, struct user, list);            FD_SET(p->connfd, &rset);            maxfd = (maxfd>p->connfd) ? maxfd : p->connfd;        }        // 同时多路监控所有集合中的所有套接字        select(maxfd+1, &rset, &wset, &eset, NULL);        // a.判断是否有新的连接        socklen_t len;        if(FD_ISSET(sockfd, &rset))        {            #ifdef DEBUG                printf("[%s:%d] 收到新连接请求\n", __FUNCTION__, __LINE__);            #endif            // 接受连接请求,并产生一个新用户节点            struct user *newone = calloc(1, sizeof(struct user));            newone->connfd = accept(sockfd, (struct sockaddr *)&newone->addr, &len);            // 给新用户分配一个随机ID            newone->ID = rand() % 10000;            // 将新用户节点链入用户链表            list_add_tail(&newone->list, &clients->list);            // 给新加入的用户发欢迎信息和当前用户            // 并立刻告知新用户的ID            inform(clients, newone->ID, IN);        }        // b.判断是否有用户发来数据        struct list_head *n;        list_for_each_safe(pos, n, &clients->list)        {            p = list_entry(pos, struct user, list);            // 判断每一个用户是否已发来数据            if(!FD_ISSET(p->connfd, &rset))                continue;            char buf[100];            bzero(buf, 100);            int m = read(p->connfd, buf, 100);            // 将数据群发给其他所有人            if(m > 0)            {                char *msg = strstr(buf, ":");                // a.群发消息                if(msg == NULL)                {                    #ifdef DEBUG                        printf("[%s:%d] 收到群发消息\n", __FUNCTION__, __LINE__);                    #endif                    broadcastMsg(clients, p, buf);                }                // b.私聊消息                else                {                    #ifdef DEBUG                        printf("[%s:%d] 收到私聊消息\n", __FUNCTION__, __LINE__);                    #endif                    privateTalk(clients, p, atoi(buf), msg+1);                }            }            // 用户跑了(关闭了)            if(m == 0)            {                // a.通知所有客户端当前的活跃用户列表                inform(clients, p->ID, OUT);                // b.更新用户链表                printf("%d下线了!\n", p->ID);                list_del(pos);                free(p);#ifdef DEBUG                printf("[%s:%d] 删除用户完毕\n", __FUNCTION__, __LINE__);#endif            }        }    }    // 释放资源    return 0;}

如需要头文件和内核链表等源文件,后台回复【TCP聊天室服务器】获取。

记录 点点滴滴的笔记 欢迎关注,共同学习

小浩笔记

71def73fcd83439268d0fa5597a8abeb.png 3715eacd7722a7e160ab4bbfa681a208.gif
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值