【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析

这篇具有很好参考价值的文章主要介绍了【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

hello !大家好呀! 欢迎大家来到我的Linux高性能服务器编程系列之项目实战——仿QQ聊天程序源码剖析,在这篇文章中,你将会学习到如何利用Linux网络编程技术来实现一个简单的聊天程序,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了解网络编程技术!!!

希望这篇文章能对你有所帮助【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析,服务器,运维,网络,linux,c++,大家要是觉得我写的不错的话,那就点点免费的小爱心吧!【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析,服务器,运维,网络,linux,c++(注:这章对于高性能服务器的架构非常重要哟!!!)

【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析,服务器,运维,网络,linux,c++         

目录

一.项目介绍

二.服务器代码剖析

2.1 头文件和相关数据声明

2.2 服务器连接准备代码

2.3 服务器处理逻辑代码

2.3 客户端代码剖析


 

一.项目介绍

      像ssh这样的登录服务通常要同时处理网络连接和用户输入,这也可以使用I/O复用来实现。我们以poll为例实现一个简单的聊天室程序,以阐述如何使用I/O 复用技术来同时处理网络连接和用户输入。该聊天室程序能让所有用户同时在线群聊,它分为客户端和服务器两个部分。其中客户端程序有两个功能:一是从标准输入终端读入用户数据,并将用户数据发送至服务器;二是往标准输出终端打印服务器发送给它的数据。服务器的功能是接收,客户数据,并把客户数据发送给每一个登录到该服务器上的客户端(数据发送者除外)。下面我们依次给出客户端程序和服务器程序的代码。

二.服务器代码剖析

2.1 头文件和相关数据声明

#define _GNU_SOURCE 1
#include<t_stdio.h>
#include<t_file.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/poll.h>
#include<fcntl.h>
#include<errno.h>
#define user_limit 5 //最大客户连接数量
#define buffer_size 64
#define fd_limit 65535 //最大文件描述符数量

struct client_data{//创建一个客户地址结构体
    struct 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;

}

这部分代码包含了头文件,定义了一些宏,以及一个用于存储客户端数据的结构体,这个结构体是为了服务器更好控制来自客户端的socket套接字,以及灵活控制对socket套接字的读写,然后还定义了setnonblocking()函数来将传入的文件描述符利用fcntl函数改为非阻塞模式,方便服务器进行监听。

2.2 服务器连接准备代码

这段代码是服务器端程序的入口和初始化部分。下面是逐行的解释:

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

这段代码检查命令行参数的数量。如果参数少于两个(程序名称和IP地址/端口号),则打印使用说明并退出程序。

    const char * ip = argv[1] ;// 提取ip地址
    int port = atoi(argv[2]); //提取端口号

从命令行参数中提取服务器的IP地址和端口号。

    struct sockaddr_in address ; //服务器地址
    bzero(&address ,sizeof(address));//清空
    address.sin_family = AF_INET;
    inet_pton(AF_INET , ip ,&address.sin_addr);//设置ip
    address.sin_port = htons(port); //设置端口号

这里创建了一个sockaddr_in结构体来存储服务器的地址信息,并使用bzero函数将其清零。然后设置地址族为AF_INET(IPv4),使用inet_pton函数将点分十进制的IP地址转换为网络字节序的格式,并存储在sin_addr字段中。最后,将端口号从主机字节序转换为网络字节序并存储在sin_port字段中。

    int listenfd = socket(PF_INET ,SOCK_STREAM , 0);//创建监听套接字
    assert(listenfd >=0);

创建一个TCP套接字(SOCK_STREAM)用于监听客户端连接,并检查套接字是否创建成功。

    int ret = bind(listenfd , (struct sockaddr*)&address , sizeof(address));//绑定
    assert(ret !=-1);

将套接字绑定到之前设置的服务器地址上,并检查绑定操作是否成功。

    ret = listen(listenfd ,5);//最多同时监听五个
    assert(ret!=-1);

调用listen函数,使套接字进入监听状态,并设置最大同时连接数为5。然后检查监听操作是否成功。

    //创建user数组,放入多个客户对象,并且使用socket的值可以直接用来索引(作为数组下标)连接对应的client_data对象
    struct client_data * user = malloc(fd_limit * sizeof(struct client_data));
   //为了提高poll性能,限制用户数量
    struct pollfd *fds = malloc(sizeof(struct pollfd) * 6);
    int user_counter = 0;//计算客户连接数量
    int i=0;
    for( i =  1 ; i<=user_limit ; ++i){//对每个fds数据初始化
        fds[i].fd = -1;
        fds[i].events =0;
    }

这段代码分配了两个数组:user数组用于存储客户端数据,fds数组用于poll函数。user数组的大小被设置为fd_limit,这是一个预定义的最大文件描述符数量。fds数组的大小被设置为6,这是因为服务器程序只监听一个套接字(listenfd),而其余的用于客户端连接。user_counter用于跟踪当前连接的客户端数量。fds数组的其余元素被初始化为-1,表示没有对应的文件描述符。

    //初始化怕poll中第一个数据:监听套接字
    fds[0].fd = listenfd;
    fds[0].events = POLLIN | POLLERR;
    fds[0].revents = 0;

最后,将监听套接字listenfd添加到fds数组中,并设置其监听的事件为可读事件(POLLIN)和错误事件(POLLERR)。revents字段用于poll函数返回时存储发生的事件,在这里初始化为0。

这段代码为服务器程序的后续操作设置了基础,包括套接字的创建和绑定,以及用于poll函数的数组的初始化。

2.3 服务器处理逻辑代码

这段代码是服务器程序的主循环,它使用poll系统调用来监控多个文件描述符(fds数组)的事件。这个循环会一直运行,直到遇到错误或者被显式地退出。

while(1){

这是一个无限循环,服务器程序将一直运行直到出现错误或者执行了退出循环的操作。

    ret = poll(fds , user_counter+1 , -1);//开始监听
    if(ret <0) {
        printf("poll failed..\n");
        break;
    }

在循环的顶部,调用poll函数来等待事件发生。fds数组包含了所有需要监控的文件描述符,user_counter+1表示总共有user_counter个客户端连接加上监听套接字listenfd-1表示poll函数将阻塞直到至少有一个文件描述符上有事件发生。如果poll调用失败(返回值小于0),则打印错误信息并退出循环。

    for ( i =0 ; i <user_counter+1;i++){//每次对整个fds数组进行遍历处理
        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("erron is:%dd\n");
                continue;
            }
            if(user_counter >=user_limit){//用户太多
                const char * info ="too many users\n";
                printf("%s\n",info);
                send(connfd , info ,strlen(info) , 0);//发送错误给客户端
                close(connfd);
                continue;
            }
            //对于新连接 ,我们要同时修改fds和users数组,user[connfd]即对应客户端数据
            user_counter++;//客户数量加一
            user[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 user\n",user_counter);
        }
        // ... 其他事件处理逻辑 ...
    }

这个循环遍历fds数组中的每个文件描述符,检查它们是否有事件发生。对于每个事件,服务器程序执行相应的操作:

  1. 如果监听套接字(listenfd)上有新的连接请求(POLLIN事件),服务器接受新连接,并将新的文件描述符(connfd)添加到fds数组中。如果连接数超过限制(user_limit),服务器会发送一个错误消息并关闭新连接。

  2. 如果有任何文件描述符上有POLLERR事件,表示发生了错误,服务器会打印错误信息。

  3. 如果有任何已连接的套接字上有POLLIN事件,表示有数据可读,服务器会读取数据并打印。

  4. 如果有任何套接字上有POLLRDHUP事件,表示对方已经关闭了连接,服务器会关闭对应的连接并更新fds数组。

  5. 如果有任何套接字上有POLLOUT事件,表示可以写数据,服务器会发送数据(如果有数据要发送)。

在循环结束后,服务器程序会继续执行下一次循环,等待更多的连接和事件。

在服务器端代码中,poll函数用于监控多个文件描述符的事件。poll函数的返回值表示有多少个文件描述符发生了事件,而每个文件描述符的事件类型存储在revents字段中。下面是服务器端代码中使用poll函数监控的不同事件类型及其解释:

if(fds[i].fd==listenfd && (fds[i].revents & POLLIN)){
    // ... 接受连接逻辑 ...
}
else if(fds[i].revents & POLLERR){
    // ... 错误处理逻辑 ...
}
else if(fds[i].revents & POLLIN){
    // ... 读取数据逻辑 ...
}
else if(fds[i].revents & POLLRDHUP){
    // ... 关闭连接逻辑 ...
}
else if(fds[i].revents & POLLOUT){
    // ... 写数据逻辑 ...
}
  1. POLLIN: 这个事件表示文件描述符上有数据可读。对于服务器来说,这意味着有新的客户端连接请求或者已连接的客户端有数据发送过来。

  2. POLLERR: 这个事件表示文件描述符发生了错误。可能是网络错误,也可能是其他类型的错误。服务器需要检查并处理这些错误。

  3. POLLRDHUP: 这个事件表示文件描述符的读端已经被对方关闭。这通常发生在客户端突然断开连接的情况下。

  4. POLLOUT: 这个事件表示文件描述符的写端准备好了,可以写入数据。对于服务器来说,这意味着它可以向客户端发送数据。

服务器程序通过检查fds数组中每个文件描述符的revents字段,来确定发生了哪种事件,并相应地执行处理逻辑。如果没有任何事件发生,poll函数会阻塞,直到至少有一个文件描述符上有事件发生。服务器程序通过这种方式可以高效地处理多个客户端连接。

2.3 客户端代码剖析

这段代码是一个简单的客户端程序,用于连接到一个服务器,并通过标准输入和输出与服务器进行通信

#define _GNU_SOURCE 1
#include<t_stdio.h>
#include<t_file.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include <sys/poll.h>
#include<fcntl.h>
#include<poll.h>
#define buffer_size 64 //缓冲区大小

这段代码包含了必要的头文件和宏定义。_GNU_SOURCE是一个宏,它用于启用一些GNU扩展,如splice系统调用。

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

这段代码检查命令行参数的数量。如果参数少于两个(程序名称和IP地址/端口号),则打印使用说明并退出程序。

    const char * ip = argv[1] ;// 提取ip地址
    int port = atoi(argv[2]); //提取端口号

从命令行参数中提取服务器的IP地址和端口号。

    struct sockaddr_in server_address ; //服务器地址
    bzero(&server_address ,sizeof(server_address));//清空
    server_address.sin_family = AF_INET;
    inet_pton(AF_INET , ip ,&server_address.sin_addr);//设置ip
    server_address.sin_port = htons(port); //设置端口号

创建一个sockaddr_in结构体来存储服务器的地址信息,并使用bzero函数将其清零。然后设置地址族为AF_INET(IPv4),使用inet_pton函数将点分十进制的IP地址转换为网络字节序的格式,并存储在sin_addr字段中。最后,将端口号从主机字节序转换为网络字节序并存储在sin_port字段中。

    int sockfd = socket(PF_INET , SOCK_STREAM , 0 );//创建本地套接字
    assert(socket >= 0 ); //判错
    if(connect(sockfd , (struct sockaddr *)&server_address , sizeof(server_address)) < 0){//连接失败的话
        printf("connection failed...\n");
        close(sockfd);
        return 1;
    }

创建一个TCP套接字(SOCK_STREAM)用于与服务器通信,并检查套接字是否创建成功。然后尝试连接到服务器。如果连接失败,打印错误信息并退出程序。

    struct pollfd fds[2];//创建pollfd结构类型数组,注册标准输入和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; //实际发生事件,由内核填充

创建一个pollfd结构体数组,用于监控标准输入(0)和套接字(sockfd)上的可读事件。

    while (1){
        ret = poll(fds , 2 , -1); //最大被监听事件只有两个, 返回符合条件文件总数
        if(ret < 0){//如果监听发生错误
            printf("poll falied..\n");
            break;
        }

在循环的顶部,调用poll函数来等待事件发生。fds数组包含了所有需要监控的文件描述符,2表示总共有两个文件描述符(标准输入和套接字)。-1表示poll函数将阻塞直到至少有一个文件描述符上有事件发生。如果poll调用失败(返回值小于0),则打印错误信息并退出循环。

        if(fds[1].revents & POLLRDHUP){//假如发生了关闭对端连接
            printf("server close the connection..\n");
            break;
        }
        else if(fds[1].revents & POLLIN){//假如sockfd文件发生可读,则读取服务器传来数据
            memset(readbuf , '\0' , buffer_size);
            recv(fds[1].fd , readbuf , buffer_size -1 , 0);//接收数据
             if(ret <= 0){// 如果接收失败或对方关闭了连接
                printf("server close the connection..\n");
               break;
           }
            printf("%s\n",readbuf);//打印数据
        }
 if(fds[0].revents & POLLIN){//标准输入文件描述符可读,说明我们需要写入数据

        ret = splice(0 , NULL , pipefd[1] , NULL ,32768 , SPLICE_F_MORE | SPLICE_F_MOVE);//从标准输入写入数据到管道写端
        ret = splice(pipefd[0] , NULL , sockfd , NULL ,32768 , SPLICE_F_MORE | SPLICE
        ret = splice(0 , NULL , pipefd[1] , NULL ,32768 , SPLICE_F_MORE | SPLICE_F_MOVE);//从标准输入写入数据到管道写端
        ret = splice(pipefd[0] , NULL , sockfd , NULL ,32768 , SPLICE_F_MORE | SPLICE_F_MOVE);//从管道读端将数据传输到sockfd
        printf("ok");
        }

    }
    close(sockfd);
    return 0;
}

这段代码检查套接字(sockfd)上的事件。如果套接字上有POLLRDHUP事件,表示对方已经关闭了连接,服务器会关闭对应的连接并退出循环。如果套接字上有POLLIN事件,表示有数据可读,服务器会读取数据并打印。

这段代码是客户端程序主循环的最后一部分,它处理标准输入(0)上的数据,并通过管道(pipefd)将其传输到套接字(sockfd)上。

  1. ret = splice(0 , NULL , pipefd[1] , NULL ,32768 , SPLICE_F_MORE | SPLICE_F_MOVE);:

    • splice是一个系统调用,用于直接在内核空间复制数据,避免了用户空间和内核空间之间的数据拷贝。
    • 第一个参数是源文件描述符,这里是从标准输入0
    • 第二个参数是源文件描述符的偏移量,这里为NULL,表示从文件开始读取。
    • 第三个参数是目标文件描述符,这里是对应的管道写端pipefd[1]
    • 第四个参数是目标文件描述符的偏移量,这里为NULL,表示从文件开始写入。
    • 第五个参数是传输的数据量,这里为32768,是一个系统定义的常量,表示最多传输32768字节。
    • 第六个参数是SPLICE_F_MORE,表示这只是一个中间步骤,还有更多的数据要传输。
    • 第七个参数是SPLICE_F_MOVE,表示传输的数据是从内核缓冲区直接移动,而不是复制。
  2. ret = splice(pipefd[0] , NULL , sockfd , NULL ,32768 , SPLICE_F_MORE | SPLICE_F_MOVE);:

    • 类似地,这段代码使用splice系统调用来从管道读端pipefd[0]传输数据到套接字sockfd
  3. printf("ok");:

    • 打印"ok"表示数据传输成功。

循环继续执行,重复上述操作,直到连接被关闭或出现错误。

  1. close(sockfd);:

    • 关闭套接字sockfd,释放资源。
  2. return 0;:

    • 程序返回0,表示正常退出。

这个客户端程序通过poll系统调用来监控标准输入和套接字的事件,并通过splice系统调用来高效地传输数据。它使用管道作为中间缓冲区,以避免在用户空间和内核空间之间进行数据拷贝。

   好啦!到这里这篇文章就结束啦,关于实例代码中我写了很多注释,如果大家还有不懂得,可以评论区或者私信我都可以哦【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析,服务器,运维,网络,linux,c++!! 感谢大家的阅读,我还会持续创造网络编程相关内容的,记得点点小爱心和关注哟!【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析,服务器,运维,网络,linux,c++ 文章来源地址https://www.toymoban.com/news/detail-860784.html

到了这里,关于【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux高性能服务器编程——学习笔记①

    第一章有一些概念讲的很好,值得好好关注一下!!! 1.1 主要的协议 1.1.1 数据链路层 ​ 数据链路层实现了网卡接口的网络驱动程序,以处理数据在物理媒介(以太网、令牌环)上的传输。 ​ 常用的协议有两种: ARP协议(Address Resolve Protocol,地址解析协议) RARP(Reverse

    2024年01月20日
    浏览(59)
  • Linux高性能服务器编程——ch10笔记

    信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。 :::tips int kill(pid_t pid, int sig); ::: kill函数:一个进程给其他进程发送信号的API。 sig一般大于0,如果设为0则表示不发送信号,可以用来检测进程或进程组是否存在。由于进程P

    2024年02月06日
    浏览(40)
  • Linux高性能服务器编程 学习笔记 第五章 Linux网络编程基础API

    我们将从以下3方面讨论Linux网络API: 1.socket地址API。socket最开始的含义是一个IP地址和端口对(ip,port),它唯一表示了使用TCP通信的一端,本书称其为socket地址。 2.socket基础API。socket的主要API都定义在sys/socket.h头文件中,包括创建socket、命名socket、监听socket、接受连接、发

    2024年02月07日
    浏览(53)
  • Linux高性能服务器编程 学习笔记 第二章 IP协议详解

    本章从两方面探讨IP协议: 1.IP头部信息。IP头部出现在每个IP数据报中,用于指定IP通信的源端IP地址、目的端IP地址,指导IP分片和重组,指定部分通信行为。 2.IP数据报的路由和转发。IP数据报的路由和转发发生在除目标机器外的所有主机和路由器上,它们决定数据报是否应

    2024年02月09日
    浏览(39)
  • Linux高性能服务器编程 学习笔记 第一章 TCP/IP协议族

    现在Internet使用的主流协议族是TCP/IP协议族,它是一个分层、多协议的通信体系。 TCP/IP协议族包含众多协议,我们只详细讨论IP协议和TCP协议,因为它们对编写网络应用程序有最直接的影响。如果想系统学习网络协议,RFC(Request For Comments,评论请求)是首选资料。 TCP/IP协议

    2024年02月09日
    浏览(61)
  • 【linux高性能服务器编程】项目实战——仿QQ聊天程序源码剖析

    hello !大家好呀! 欢迎大家来到我的Linux高性能服务器编程系列之项目实战——仿QQ聊天程序源码剖析,在这篇文章中, 你将会学习到如何利用Linux网络编程技术来实现一个简单的聊天程序,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了

    2024年04月28日
    浏览(39)
  • Linux高性能服务器编程|阅读笔记:第1章 - TCP/IP协议族

    Hello! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~   ଘ(੭ˊᵕˋ)੭ 昵称:海轰 标签:程序猿|C++选手|学生 简介:因C语言结识编程,随后转入计算机专业,获得过国家奖学金,有幸在竞赛中拿过一些国奖、省奖…已保研 学习经验:扎实基础 + 多做

    2024年02月01日
    浏览(53)
  • Linux高性能服务器编程|阅读笔记:第6章 - 高级I/O函数

    Hello! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~   ଘ(੭ˊᵕˋ)੭ 昵称:海轰 标签:程序猿

    2024年02月03日
    浏览(37)
  • 强推Linux高性能服务器编程, 真的是后端开发技术提升, 沉淀自身不容错过的一本经典书籍

    目录 第1章 TCP/IP协议 1.1 TCP/IP协议族体系结构以及主要协议 1.1.1 数据链路层 1.1.2 网络层 1.1.3 传输层 1.1.4 应用层 1.2 封装 1.3 分用 1.5 ARP协议工作原理 1.5.1 以太网ARP请求/应答报文详解 1.5.2 ARP高速缓存的查看和修改 1.5.3 使用tcpdump观察ARP通信过程所得结果如下 本篇核心关键所在

    2024年02月07日
    浏览(46)
  • 【网络编程】高性能并发服务器源码剖析

      hello !大家好呀! 欢迎大家来到我的网络编程系列之洪水网络攻击,在这篇文章中, 你将会学习到在网络编程中如何搭建一个高性能的并发服务器,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了解网络编程技术!!! 希望这篇文章能

    2024年04月15日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包