TCP聊天室-简易版

本文介绍了基于Linux高性能服务器编程实现的TCP聊天室,详细讲解了服务端使用IO多路复用处理客户端连接,客户端数据交互以及技术要点,如零拷贝、splice函数和poll机制。特别讨论了断开连接的检测、消息实时性优化和POLLOUT事件的策略。
摘要由CSDN通过智能技术生成

参考于Linux高性能服务器编程,并对代码进行了深度剖析。

服务端

使用IO多路复用技术实现多个用户连接,主要功能是负责接受客户的数据,并将客户的数据发送给每一个登陆到该服务器上的客户端。
服务端代码

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

#define users_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_flag = fcntl(fd,F_GETFL);
    int new_flag = old_flag | O_NONBLOCK;
    fcntl(fd,F_SETFL,new_flag);
    return old_flag;
}

int main(int argc,char** argv)
{
    if(argc <= 2)
    {
        printf("usage:%s ip_address port_number\n",basename(argv[0]));
        return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    int ret = 0;
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr.s_addr);
    address.sin_port = htons(port);
    
    int listenfd = socket(PF_INET,SOCK_STREAM,0);
    assert(listenfd >= 0);

    ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));
    assert(ret >= 0);

    ret = listen(listenfd,5);
    assert(ret >= 0);
    //创建users数组,分配FD_LIMIT个对象,可以预期每个可能的socket连接都可以获得一个这样的对象,且socket的值直接作为索引
    client_data* users = new client_data[FD_LIMIT];
    //限制用户数量
    pollfd fds[users_LIMIT+1];
    int user_counter = 0;
    for(int i = 1;i <= users_LIMIT;i++)
    {
        fds[i].fd = -1;
        fds[i].events = 0;
    }
    fds[0].fd = listenfd;
    fds[0].events = POLLIN|POLLERR;
    fds[0].revents = 0;

    while(1)
    {
        ret = poll(fds,user_counter+1,-1);
        if(ret < 0)
        {
            printf("poll failure\n");
            break;
        }

        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 < 0)
                {
                    printf("errno is: %d\n",errno);
                    continue;
                }
                //请求过多,则关闭新到的连接
                if(user_counter >= users_LIMIT)
                {
                    const char* info = "too many users\n";
                    printf("%s",info);
                    send(connfd,info,strlen(info),0);
                    close(connfd);
                    continue;
                }
                //对于新的连接,同时修改fds和userss数组。
                user_counter ++;
                users[connfd].address = client_address;
                setnonblocking(connfd);
                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 err from %d\n",fds[i].fd);
                char errors[100];
                memset(errors,'\0',sizeof(errors));
                socklen_t length = sizeof(errors);
                if(getsockopt(fds[i].fd,SOL_SOCKET,SO_ERROR,&errors,&length) < 0)
                {
                    printf("get socket option failed\n");
                }
                continue;
            }
            else if(fds[i].revents & POLLRDHUP)
            {
                //如果客户端关闭连接,则服务器也关闭对应的连接,并将用户数减1;
                users[fds[i].fd] = users[fds[user_counter].fd];
                close(fds[i].fd);
                fds[i] = fds[user_counter];
                i--;
                user_counter--;
                printf("a client lefter\n");
            }
            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);
                if(ret < 0)
                {
                    //读操作出错,关闭连接
                    if(errno != EAGAIN || errno != EWOULDBLOCK)
                    {
                        close(connfd);
                        users[fds[i].fd] = users[fds[user_counter].fd];
                        fds[i] = fds[user_counter];
                        i--;
                        user_counter--;
                    }
                }
                else if(ret == 0)
                {
                    //此处不需要处理,因为注册了POLLRDHUB事件,客户端关闭了连接会触发该事件
                }
                else
                {
                    //如果接收到客户端的数据,通知其他socket连接接收数据
                    for(int j = 1;j <= user_counter;j++)
                    {
                        if(fds[j].fd == connfd)
                        {
                            continue;
                        }
                        fds[j].events |= ~POLLIN;
                        fds[j].events |= POLLOUT;
                        users[fds[j].fd].write_buf = users[connfd].buf;
                    }
                }
            }
            else if(fds[i].revents & POLLOUT)
            {
                int connfd = fds[i].fd;
                if(!users[connfd].write_buf)
                {
                    continue;
                }
                ret = send(connfd,users[connfd].write_buf,strlen(users[connfd].write_buf),0);
                users[connfd].write_buf = NULL;
                //写完数据需要重新注册fds[i]上的可读事件
                fds[i].events |= ~POLLOUT;
                fds[i].events |= POLLIN;
            }
        }
    }
    delete [] users;
    close(listenfd);
    return 0;

}

客户端

  1. 从标准输入终端读入用户数据,并将用户数据发送到服务器
  2. 往标准输出终端打印服务器发给它的数据

客户端代码

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

#define BUFFER_SIZE 64

int main(int argc,char** argv)
{
    if(argc <= 2)
    {
        printf("usage:%s ip_address port_number\n",basename(argv[1]));
        return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    int ret = 0;
    struct sockaddr_in ser_address;
    bzero(&ser_address,sizeof(ser_address));
    ser_address.sin_family = AF_INET;
    inet_pton(AF_INET,ip,&ser_address.sin_addr.s_addr);
    ser_address.sin_port = htons(port);

    //创建TCPsocket将其绑定在端口port上
    int sockfd = socket(PF_INET,SOCK_STREAM,0);
    assert(sockfd >= 0);

    if(connect(sockfd,(struct sockaddr*)&ser_address,sizeof(ser_address)) < 0)
    {
        printf("connection failed\n");
        close(sockfd);
        return 1;
    }

    pollfd fds[2];
    //注册文件描述符0(标准输入)和sockfd上的可读事件
    fds[0].fd = 0;
    fds[0].events = POLLIN;
    fds[0].revents = 0;
    fds[1].fd = sockfd;
    fds[1].events = POLLIN|POLLRDHUP;
    fds[1].revents = 0;
    char read_buf[BUFFER_SIZE];
    //创建管道
    int pipefd[2];
    ret = pipe(pipefd);
    assert(ret != -1);
    while(1)
    {
        ret = poll(fds,2,-1);
        if(ret < 0)
        {
            printf("poll failed\n");
            break;
        }
        else if(fds[1].revents & POLLRDHUP)
        {
            printf("server close the connection\n");
            break;
        }
        else if(fds[1].revents & POLLIN)
        {
            memset(read_buf,'\0',BUFFER_SIZE);
            recv(fds[1].fd,read_buf,BUFFER_SIZE-1,0);
            printf("%s\n",read_buf);
        }

        if(fds[0].revents & POLLIN)
        {
            //使用零拷贝技术将数据直接写到sockfd上
            ret = splice(0,NULL,pipefd[1],NULL,37628,SPLICE_F_MORE|SPLICE_F_MOVE);
            ret = splice(pipefd[0],NULL,sockfd,NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
        }
    }
    close(sockfd);
    return 0;
}

技术要点

  • 零拷贝技术
    • splice函数
  • poll机制
  • 对于客户端的断开连接并不是通过ret == 0来判断,对于ret == 0不做处理,而是注册POLLRDHUP事件,每当一个连接断开时,会触发POLLRDHUP事件,执行用户删除的逻辑
  • 已断开连接用户的删除逻辑(重点体会)
close(fds[i].fd);
users[fds[i].fd] = users[fds[user_counter].fd];
fds[i] = fds[user_counter];
i--;
user_counter--;
  • 有两处要执行用户的删除逻辑分别是
    • POLLRDHUP事件发生时
    • POLLIN事件发生在读取数据的时候出错(ret < 0);
  • 如何做到将消息通知除自己以外得所有在线socket
for(int j = 1;j <= user_counter;j++)
{
    if(fds[j].fd == connfd)
    {
        continue;
    }
   fds[j].events |= ~POLLIN;
   fds[j].events |= POLLOUT;
   users[fds[j].fd].write_buf = users[connfd].buf;
 }
  • 为什么要关闭POLLIN事件?
    • 从代码逻辑我们可以知道,POLLIN事件是先于POLLOUT事件被判断是否触发,为了提高消息的实时性,每当接受到客户端的数据时,服务端优先把数据发送给其他在线客户端。
  • 为什么不在一开始就注册POLLOUT事件,而是收到客户端数据后开启该事件,发送完数据后又关闭该事件?
    • 这是个大坑,要搞懂这个POLLOUT什么时候被触发,就需要我们来看看什么时候socket可写:
      • socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩) 大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写, 并且返回值大于0;
      • socket 的写操作被关闭(调用了 close 或者 shutdown 函数)( 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号);
      • socket 使⽤非阻塞 connect 连接成功或失败之后;
    • 从上socket的可写条件我们可以看到POLLOUT是否被触发取决于发送缓冲区的可用字节数,而不是我们是否想写数据,也就是说即使我们不想写数据,只要发送缓冲区可用空间大小满足条件就会触发该事件。
    • 综合上面所表达的内容我们也就知道了服务端是如何把消息发送给其他客户端的。
里面包含聊天室的客户端和服务器端的源文件和一份完整的设计报告。 一、 系统概要 本系统能实现基于VC++的网络聊天室系统。有单独的客户端、服务器端。 服务器应用程序能够接受来自客户端的广播,然后向客户端发送本机的IP与服务端口,让客户端接入到服务器进行聊天,检测用户名是否合法(重复),服务器责接收来自客户端的聊天信息,并根据用户的需求发送给指定的人或所有人,能够给出上线下线提示。客户端能够发出连接请求,能编辑发送信息,可以指定发给单人或所有人,能显示聊天人数,上线下线用户等。 二、 通信规范的制定 服务请求规范: 服务器端: (1) 创建一个UDP的套接字,接受来自客户端的广播请求,当请求报文内容为“REQUEST FOR IP ADDRESS AND SERVERPORT”时,接受请求,给客户端发送本服务器TCP聊天室的端口号。 (2) 创建一个主要的TCP协议的套接字负责客户端TCP连接 ,处理它的连接请求事件。 (3)在主要的TCP连接协议的套接字里面再创建TCP套接字保存到动态数组里,在主要的套接字接受请求后 ,就用这些套接字和客户端发送和接受数据。 客户端: (1) 当用户按“连接”按钮时,创建UDP协议套接字,给本地计算机发广播,广播内容为“REQUEST FOR IP ADDRESS AND SERVERPORT”。 (2)当收到服务器端的回应,收到服务器发来的端口号后,关闭UDP连接。根据服务器的IP地址和端口号重新创建TCP连接。 故我思考:客户端一定要知道服务器的一个端口,我假设它知道服务器UDP服务的端口,通过发广播给服务器的UDP服务套接字,然后等待该套接字发回服务器TCP聊天室服务的端口号,IP地址用ReceiveForom也苛刻得到。 通信规范 通信规范的制定主要跟老师给出的差不多,并做了一小点增加: (增加验证用户名是否与聊天室已有用户重复,在服务器给客户端的消息中,增加标志0) ① TCP/IP数据通信 --- “聊天”消息传输格式 客户机 - 服务器 (1)传输“用户名” STX+1+用户名+ETX (2) 悄悄话 STX+2+用户名+”,”+内容+ETX (3) 对所有人说 STX+3+内容+ETX 服务器- 客户机 (0)请求用户名与在线用户名重复 //改进 STX+0+用户名+EXT (1)首次传输在线用户名 STX+1+用户名+ETX (2)传输新到用户名 STX+2+用户名+ETX (3)传输离线用户名 STX+3+用户名+ETX (4)传输聊天数据 STX+4+内容+ETX (注:STX为CHR(2),ETX 为CHR(3)) 三、 主要模块的设计分析 四、 系统运行效果 (要求有屏幕截图) 五、 心得与体会
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值