【linux网络编程】C++使用select实现本地简易聊天室多路IO转接服务器(110行代码)

看完黑马的课后,感觉他在c语言下实现的示例服务器性能不是很好,所以打算使用stl的set容器来存储文件描述符(Socket客户端),学习了select之后也是动手练习搭建一个简单的本地聊天室服务器。

话不多说直接上代码。

服务器代码:

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <set>
using namespace std;
#define SERV_PORT 5555

set<int> connectset;
void error_exit(const char* str)  //错误处理函数
{
    perror(str);
    exit(1);
}
void WriteMessageIn(const int fd, const string& str)  //写入发送的信息到其他的客户端中
{
    const char* buf = str.c_str();
    cout << buf;
    for (auto& i : connectset)
    {
        if (i != fd)
        {
            write(i, buf, strlen(buf));
        }
    }
    return;
}
int main()
{
    int listenfd;
    int ret;
    //声明套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1)
        error_exit("socket error");
    int optval = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));  //设置端口复用
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    ret = bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
    if (ret == -1)
        error_exit("bind error");
    listen(listenfd, 128);
    cout << "Server Started." << endl;
    //设置监听集合,rset指读取的集合,allset三全局集合
    fd_set rset, allset;
    int maxfd = listenfd;
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    while (1)
    {
        rset = allset;
        ret = select(maxfd + 1, &rset, NULL, NULL, NULL);  // select语句设置IO多路转接,最后一个参数为NULL表示阻塞。
        if (ret < 0)
        {
            error_exit("select error");
        }
        else if (ret > 0)
        {
            if (FD_ISSET(listenfd, &rset))  //解决连接服务器的请求
            {
                int fd = accept(listenfd, NULL, NULL);
                if (fd == -1)
                    error_exit("accept error");
                connectset.insert(fd);
                FD_SET(fd, &allset);
                maxfd = max(fd, maxfd);
                string s = "Client Connected: " + to_string(fd) + '\n';
                WriteMessageIn(-1, s);
                if (--ret == 0)
                    continue;
            }

            for (auto& i : connectset)
            {
                if (FD_ISSET(i, &rset))
                {
                    char buf[BUFSIZ];
                    int siz = read(i, buf, BUFSIZ);
                    // cout << "siz:" << siz << endl;
                    if (siz > 0)  //处理有输入的情况
                    {             // string(buf).substr(0, siz):这里不止为何,若不取子串单单传入buf会导致发送信息混乱的bug
                        /*这个问题的原因在于,当 read 函数从套接字读取数据时,它不会自动添加空字符('\0')来标记字符串的结尾。因此,如果直接将 buf
                         * 作为 C 风格字符串传递给 write 函数,write
                         * 函数会一直读取直到遇到第一个空字符,这可能会导致它读取到缓冲区之外的内存,从而产生未定义行为,包括发送错误的数据。*/
                        WriteMessageIn(i, "User " + to_string(i) + " :" + string(buf).substr(0, siz) + '\n');
                    }
                    else if (siz == 0)
                    {
                        FD_CLR(i, &allset);
                        connectset.erase(i);
                        close(i);
                        string s = "Client DisConnected: " + to_string(i) + '\n';
                        WriteMessageIn(i, s);
                    }
                    else
                    {
                        error_exit("read error");
                    }
                    if (--ret == 0)
                        break;
                }
            }
        }
    }

    close(listenfd);
    cout << "Server Exited." << endl;
    return 0;
}

代码运行截图:

因为这个运行效果和我上一篇文章的练习效果差不多,所以直接把图片拿来用了。

代码使用方法:

        首先使用g++进行编译,接着启动服务器。由于该程序比较简单就没有设置客户端的代码,这里在linux下打开任意终端后,输入下述指令来连接本地服务器。

nc 127.0.0.1 5555

        nc是netcat的意思,后面的127.0.0.1是本地的虚拟地址,在后面的5555是服务器的端口号,这样就可以连上服务器。

        连上服务器后就可以在终端里打字说话,其他在服务器的用户可以收到你的信息。

       接着如果需要断开与服务器的连接,在控制台输入 ctrl+c 来终止程序,或者直接关闭终端,这样就可以断开服务器了。

代码分析:

这段代码是一个简单的多客户端服务器应用程序,使用 C++ 和 POSIX 套接字编程。服务器能够接受多个客户端的连接,并将消息广播给所有连接的客户端。以下是代码的主要思路和工作流程:

  1. 初始化和设置套接字

    • 创建一个 TCP 套接字。
    • 设置套接字选项 SO_REUSEADDR,允许服务器在端口还在 TIME_WAIT 状态时重新绑定端口。
    • 绑定套接字到服务器的地址和端口上。
    • 监听连接请求。
  2. 服务器主循环

    • 使用 select() 函数等待任何套接字上的活动(包括监听套接字和已连接的客户端套接字)。
    • 如果监听套接字上有活动(新连接请求),接受连接并将新的客户端套接字添加到套接字集合中。
    • 如果客户端套接字上有活动(数据到达),读取数据并将其广播给所有其他客户端。
  3. 处理新连接

    • select() 检测到监听套接字有连接请求时,服务器调用 accept() 接受连接。
    • 新的客户端套接字被添加到套接字集合中,以便它可以被 select() 监视。
    • 服务器向所有客户端发送一条消息,通知它们有新的客户端连接。
  4. 读取和广播消息

    • 对于每个有数据到达的客户端套接字,服务器读取数据。
    • 服务器将读取的数据加上前缀(包括客户端的描述和消息内容),然后使用 WriteMessageIn 函数将消息广播给所有其他客户端。
  5. 处理客户端断开连接

    • 如果 read() 返回 0,表示客户端已经关闭了连接。
    • 服务器将关闭的客户端从套接字集合中移除,并关闭该客户端套接字。
  6. 错误处理

    • 如果在任何网络操作中发生错误(如 socket()bind()accept()read() 等),服务器将调用 error_exit() 函数,打印错误信息并退出。
  7. 消息发送

    • WriteMessageIn 函数负责将消息发送给指定的客户端(除了发送者自己)。
    • 它通过遍历 connectset 集合,使用 write() 函数将消息发送给每个客户端。
  8. 关闭服务器

    • 当服务器退出主循环时(例如,由于错误或管理员干预),它会关闭监听套接字并退出。

代码中的关键点是 select() 函数的使用,它允许服务器同时监视多个套接字的活动,这是实现非阻塞 I/O 和处理多个客户端连接的关键技术。此外,代码中的 WriteMessageIn 函数确保了消息的正确广播,并且通过 cout 将消息打印到服务器的控制台,以便进行调试和监控。

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值