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

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

希望这篇文章能对你有所帮助9fe07955741149f3aabeb4f503cab15a.png,大家要是觉得我写的不错的话,那就点点免费的小爱心吧!1a2b6b564fe64bee9090c1ca15a449e3.png(注:这章对于高性能服务器的架构非常重要哟!!!)

03d6d5d7168e4ccb946ff0532d6eb8b9.gif         

目录

一.项目介绍

二.服务器代码剖析

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);//设置ipaddress.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);//设置ipserver_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 | SPLICEret = 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);//从管道读端将数据传输到sockfdprintf("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系统调用来高效地传输数据。它使用管道作为中间缓冲区,以避免在用户空间和内核空间之间进行数据拷贝。

   好啦!到这里这篇文章就结束啦,关于实例代码中我写了很多注释,如果大家还有不懂得,可以评论区或者私信我都可以哦4d7d9707063b4d9c90ac2bca034b5705.png!! 感谢大家的阅读,我还会持续创造网络编程相关内容的,记得点点小爱心和关注哟!2cd0d6ee4ef84605933ed7c04d71cfef.jpeg 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/651774.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解决NetworkManager覆盖/etc/resolv.conf的问题

发布时间&#xff1a;2024.4.27 问题 /etc/resolv.conf是Linux下DNS的配置文件。 但是NetworkManager会用覆盖它&#xff0c;导致我们每次都要重新配置。 解决办法 这是官方推荐的做法。或者你可以用resolveconf工具。 $ nm-connection-editor会调起一个界面&#xff0c;…

HWOD:提取不重复的整数

一、知识点 不要看到题目是和整数的位相关的&#xff0c;就下意识用求余的操作去获得整数的每一位 这里用getchar()直接读取了整数的每一位 代码中k是往后走的 二、题目 1、描述 输入一个 int 型整数&#xff0c;按照从右向左的阅读顺序&#xff0c;返回一个不含重复数字…

SQL异常

异常 EXCEPTION 预定义异常 系统已经设置好的异常&#xff0c;包含了异常名&#xff0c;异常代码&#xff0c;异常信息组成 CASE NOT FOUND 未知异常&#xff1a;OTHERS 异常信息&#xff1a;SQLERRM 错误代码&#xff1a;SQLCODE 有各种各样的很多异常 捕获异常的语法 DE…

uniapp获取当前位置及检测授权状态

uniapp获取当前位置及检测授权定位权限 文章目录 uniapp获取当前位置及检测授权定位权限效果图创建js文件permission.jslocation.js 使用 效果图 Android设备 点击 “设置”&#xff0c;跳转应用信息&#xff0c;打开“权限即可”&#xff1b; 创建js文件 permission.js 新建…

Blender点操作

顶点操作即一般的“布线”操作 1.顶点移动 -先切到顶点模式 -移动&#xff0c;G 或 G X/Y/Z -旋转&#xff0c;R 同上 -缩放&#xff0c;S 同上 2.顶点滑移&#xff0c;用于微调顶点的位置 快捷键&#xff1a;Shift V&#xff0c;G G 3.顶点删除 -选中一个顶点 -按…

二 SSM整合实操

SSM整合实操 一 依赖管理 数据库准备 mysql8.0.33 CREATE DATABASE mybatis-example;USE mybatis-example;CREATE TABLE t_emp(emp_id INT AUTO_INCREMENT,emp_name CHAR(100),emp_salary DOUBLE(10,5),PRIMARY KEY(emp_id) );INSERT INTO t_emp(emp_name,emp_salary) VALUE…

基于Rust的多线程 Web 服务器

构建多线程 Web 服务器 在 socket 上监听 TCP 连接解析少量的 HTTP 请求创建一个合适的 HTTP 响应使用线程池改进服务器的吞吐量优雅的停机和清理注意&#xff1a;并不是最佳实践 创建项目 ~/rust ➜ cargo new helloCreated binary (application) hello package~/rust ➜ma…

mysql的多表查询和子查询

多表查询&#xff1a;查询数据时&#xff0c;需要使用多张表来查询 多表查询分类&#xff1a; 1.内连接查询 2.外连接查询 3.子查询 笛卡尔积&#xff1a; create table class (id int primary key auto_increment,name varchar(10) ); create table student (id int primar…

Vue 组件单元测试深度探索:细致解析与实战范例大全

Vue.js作为一款广受欢迎的前端框架&#xff0c;以其声明式的数据绑定、组件化开发和灵活的生态系统赢得了广大开发者的心。然而&#xff0c;随着项目规模的增长&#xff0c;确保组件的稳定性和可靠性变得愈发关键。单元测试作为软件质量的守护神&#xff0c;为Vue组件的开发过程…

uniapp-css多颜色渐变:左右+上下

案例展示 案例代码&#xff1a; 代码灵感&#xff1a;使用伪类进行处理 <view class"headBox"></view>.headBox {height: 200rpx;background: linear-gradient(to right, #D3D5F0, #F0DCF3, #F7F6FB, #DAE8F2, #E1D3EE);position: relative; }.headBox…

数据结构:时间复杂度和空间复杂度

目录 1. 如何衡量一个算法的好坏2. 算法效率3. 时间复杂度3.1 时间复杂度的概念3.2 大O的渐进表示法3.3 推导大O阶方法3.4 常见时间复杂度计算举例 3.空间复杂度 1. 如何衡量一个算法的好坏 下面求斐波那契数列的算法好还是不好&#xff0c;为什么&#xff1f;该如何衡量一个算…

单一职责原则介绍

单一职责原则&#xff08;Single Responsibility Principle&#xff0c;简称SRP&#xff09;是面向对象设计中的一个重要原则&#xff0c;它指出一个类应该只有一个引起变化的原因。换句话说&#xff0c;每个类都应该有一个明确的任务&#xff0c;只做一件事&#xff0c;并做好…