【代码错误纠正】《Linux高性能服务器编程》:I/O复用的高级应应用2:聊天室程序

在作者给的聊天室程序代码中有着这样一段:

else if( fds[i].revents & POLLIN )
{
	if( ret < 0 ) 
	{
        ...
    } 
    else if( ret == 0 ) 
    {
        ...
    } 
    else {
    for( int j = 1; j <= user_counter; ++j )
    {
        if( fds[j].fd == connfd ) 
        {
            continue;
        }
        fds[j].events |= ~POLLIN; // 取消该套接字POLLIN,但是你觉得真的是这么写吗?
        fds[j].events |= POLLOUT;
        users[fds[j].fd].write_buf = users[connfd].buf;
    }
}

可以看到作者是想临时取消掉除信息发送者的以外的套接字上的读事件然后并注册写事件以便将信息转发给其他的套接字,但是这首先会造成一个问题:在转发过程中如果其他套接字有信息传来,因为POLLIN被取消掉了,所以该消息会被忽略,这是问题1。

接下来的问题才是我想讲的:fds[j].events |= ~POLLIN;是正确的取消事件的方式吗?

下面来逐步讲解:
在有新的连接到来时,该连接套接字的事件会被设置成这样:

fds[user_counter].events = POLLIN | POLLRDHUP | POLLERR; 
十进制:8201 二进制:00000000000000000010000000001001

我在代码中列出了它的二进制,那向上面的那段代码一样,如果我们要取消掉POLLIN事件呢,是下面这样吗?

fds[j].events |= ~POLLIN; // 是这样吗?

我们可以知道POLLIN#define POLLIN 0x001,因此我们可以使用8201 | ~1来模拟fds[j].events |= ~POLLIN;于是按照作者的代码试着取消掉POLLIN事件:

printf("%d\n", 8201 | ~1 ); // 竟然是-1! 

进行按位或运算|:
00000000000000000010000000001001 ( a = 8201)
11111111111111111111111111111110 (~1 = -2 )
——————————————————————————————————————
11111111111111111111111111111111 (-1)
这肯定不是我们想要的结果
我们想要的是:
00000000000000000010000000001000

也就是说fds[j].events |= ~POLLIN;fds[j].events为-1,其二进制是全1,可想而知接下来fds[j].events |= POLLOUT;也必然的得到-1,因此我们发现fds[j].events |= ~POLLIN;这一句是错误的写法。

因此在取消某个事件中,我们需要将|换成&

fds[j].events &= ~POLLIN; // 取消该套接字POLLIN,这才是正确的写法
fds[j].events |= POLLOUT;

相应地在另一处代码应改成

else if( fds[i].revents & POLLOUT )
{
    int connfd = fds[i].fd;
    if( users[connfd].write_buf == NULL ) {
        continue;
    }
    ret = send( connfd, users[connfd].write_buf, strlen( users[connfd].write_buf ), 0 );
    users[connfd].write_buf = NULL;
    fds[i].events &= ~POLLOUT;
    fds[i].events |= POLLIN;
}


将读事件的else if判断前置:

#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <poll.h>

#include <libgen.h>    // basename

#define USER_LIMIT 5
#define BUFFER_SIZE 64
#define FD_LIMIT 65535

struct client_data
{
    sockaddr_in address;
    char* write_buf;
    char buf[ BUFFER_SIZE ];
};

int setnonblocking( int fd )
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}

int main( int argc, char* argv[] )
{
    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
	address.sin_addr.s_addr=htonl(INADDR_ANY);
    address.sin_port = htons( atoi( "9190" ) );

    int listenfd = socket( PF_INET, SOCK_STREAM, 0 ); assert( listenfd != -1 );
    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) ); assert( ret != -1 );
    ret = listen( listenfd, 5 ); assert( ret != -1 );


    // 初始化pollfd类型的fds
    pollfd fds[USER_LIMIT+1];
    for( int i = 1; i <= USER_LIMIT; ++i ) {
        fds[i].fd = -1;
        fds[i].events = 0;
    }
    int user_counter = 0;
    // 添加监听套接字的事件
    fds[user_counter].fd      = listenfd;
    fds[user_counter].events  = POLLIN | POLLERR;
    fds[user_counter].revents = 0;

    client_data* users = new client_data[FD_LIMIT];
    while(1)
    {
        ret = poll( fds, user_counter+1, -1 );
        if ( ret == -1 ) {
            printf( "poll failure\n" );
            break;
        }

        // 遍历所有套接字(包括监听套接字,所有要+1)
        for( int i = 0; i < user_counter+1; ++i )
        {
            //! 有新的连接
            if( ( fds[i].fd == listenfd ) && ( fds[i].revents & POLLIN ) )
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof( client_address );
                int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
                if ( connfd == -1 )
                {
                    printf( "errno is: %d\n", errno );
                    continue;
                }
                // 已连接的用户数大于最大用户数,则不允许连接
                if( user_counter >= USER_LIMIT )
                {
                    const char* info = "too many users\n";
                    printf( "%s", info );
                    send( connfd, info, strlen( info ), 0 );
                    close( connfd );
                    continue;
                }
                user_counter++;
                users[connfd].address = client_address;
                setnonblocking( connfd ); // 套接字使用非阻塞IO
                fds[user_counter].fd = connfd;
                fds[user_counter].events = POLLIN | POLLRDHUP | POLLERR;
                fds[user_counter].revents = 0;
                printf( "comes a new user, now have %d users\n", user_counter );
            }
            //! 错误事件
            else if( fds[i].revents & POLLERR )
            {
                printf( "get an error from %d\n", fds[i].fd );
                char errors[ 100 ];
                memset( errors, '\0', 100 );
                socklen_t length = sizeof( errors );
                if( getsockopt( fds[i].fd, SOL_SOCKET, SO_ERROR, &errors, &length ) < 0 ) {
                    printf( "get socket option failed\n" );
                }
                continue;
            }
            //! 客户端关闭了TCP连接/写操作
            else if( fds[i].revents & POLLRDHUP )
            {
                users[fds[i].fd] = users[fds[user_counter].fd]; //! 这一句似乎是没有必要的
                close( fds[i].fd );
                fds[i--] = fds[user_counter--];
                printf( "a client left\n" );
                continue;
            }
            //! 要转发用户消息(由用户套接字的读事件触发的写事件)
            else if( fds[i].revents & POLLOUT )
            {
                int connfd = fds[i].fd;
                if( users[connfd].write_buf == NULL ) {
                    continue;
                }
                ret = send( connfd, users[connfd].write_buf, strlen( users[connfd].write_buf ), 0 );
                // 转发完后,注销写事件,恢复读事件
                users[connfd].write_buf = NULL;
                fds[i].events &= ~POLLOUT; // 正确的取消
                fds[i].events |= POLLIN;
            }
            //! 客户端发送信息(有读事件)
            else if( fds[i].revents & POLLIN )
            {
                int connfd = fds[i].fd;
                memset( users[connfd].buf, '\0', BUFFER_SIZE );
                ret = recv( connfd, users[connfd].buf, BUFFER_SIZE-1, 0 ); // 非阻塞recv
                printf( "get %d bytes of client data %s from %d\n", ret, users[connfd].buf, connfd );
                if( ret < 0 )
                {
                    // 有读事件但是接收缓冲没有数据,因此断开用户连接
                    if( errno != EAGAIN )
                    {
                        close( connfd );
                        users[fds[i].fd] = users[fds[user_counter].fd];
                        fds[i--] = fds[user_counter--];
                    }
                } else
                if( ret == 0 ) {
                    printf( "code should not come to here\n" );
                } else {
                    // 给除了消息发送者以外的每个用户注册写事件并填充写buf(使得将本人的信息转发给聊天室的其他人)
                    for( int j = 1; j <= user_counter; ++j )
                    {
                        if( fds[j].fd == connfd ) {
                            continue;
                        }

                        // 为了其他用户的write_buf不被覆盖,故暂时注销其他用户的读事件
                        fds[j].events &= ~POLLIN;
                        // 注册其他用户的写事件以转发该消息
                        fds[j].events |= POLLOUT;
                        // 其他用户的写缓存write_buf是发送消息用户的接收缓存buf
                        users[fds[j].fd].write_buf = users[connfd].buf;

                        /* 可能会担心为什么不注销消息发送者的读事件呢?同一个消息发送者再次发送消息
                        然后将users[connfd].buf中的消息给冲刷掉导致users[fds[j].fd].write_buf更换了内容
                            其实并不会的,因为本次i循环后需要再遍历一圈才会到达这次的消息发送者
                        而通过这一轮我们已经将消息发送者的消息转发出去了
                        */
                        /* 在转发过程中如果其他套接字有信息传来,
                        因为其POLLIN事件被取消掉了,所以该消息会被忽略。
                        */
                    }
                }
            }
        }
    }

    delete [] users;
    close( listenfd );
    return 0;
}

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
对于基于I/O复用的TCP回射服务器程序,我们可以在读入客户端发送的数据后,先对其进行大小写字母转换,再将转换后的数据发送回客户端即可。 具体实现步骤如下: 1.读取客户端发送的数据,可以使用select()函数进行I/O复用。 2.对读入的数据进行大小写字母转换,可以使用标准库中的toupper()和tolower()函数。 3.将转换后的数据发送回客户端。 下面是一个示例代码: ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #define MAX_LINE 1024 /* 最大数据长度 */ int main(int argc, char **argv) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; ssize_t n; fd_set rset, allset; char buf[MAX_LINE]; socklen_t cliaddr_len; struct sockaddr_in cliaddr, servaddr; char *str; /* 创建socket,即TCP套接字 */ listenfd = socket(AF_INET, SOCK_STREAM, 0); /* 绑定IP地址和端口号 */ bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(12345); bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)); /* 监听 */ listen(listenfd, 20); /* 初始化 */ maxfd = listenfd; /* 初始化最大文件描述符 */ maxi = -1; /* 初始化客户端数组下标 */ for (i = 0; i < FD_SETSIZE; i++) client[i] = -1; /* 初始化客户端数组 */ FD_ZERO(&allset); /* 初始化文件描述符集 */ FD_SET(listenfd, &allset); /* 循环处理 */ for (;;) { rset = allset; nready = select(maxfd + 1, &rset, NULL, NULL, NULL); /* 处理新的连接 */ if (FD_ISSET(listenfd, &rset)) { cliaddr_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &cliaddr_len); /* 将新的连接添加到客户端数组中 */ for (i = 0; i < FD_SETSIZE; i++) if (client[i] < 0) { client[i] = connfd; break; } if (i == FD_SETSIZE) { printf("too many clients"); exit(1); } /* 添加到文件描述符集中 */ FD_SET(connfd, &allset); if (connfd > maxfd) maxfd = connfd; if (i > maxi) maxi = i; /* 输出客户端IP地址和端口号 */ printf("new client: %s, port %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); /* 处理已经连接的客户端 */ for (i = 0; i <= maxi; i++) { if ((sockfd = client[i]) < 0) continue; if (FD_ISSET(sockfd, &rset)) { if ((n = read(sockfd, buf, MAX_LINE)) == 0) { /* 连接已经关闭 */ close(sockfd); FD_CLR(sockfd, &allset); client[i] = -1; } else { /* 数据处理 */ int j; for (j = 0; j < n; j++) { buf[j] = isupper(buf[j]) ? tolower(buf[j]) : toupper(buf[j]); //大小写字母转换 } write(sockfd, buf, n); } if (--nready <= 0) break; } } } } } ``` 以上代码实现了一个基于I/O复用的TCP回射服务器程序,并且在客户端发送数据后,对其进行大小写字母转换后再发送回客户端。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值