linux下C++ socket网络编程——即时通信系统(含源码)
moboyou 2025-08-03 16:01 10 浏览
一:项目内容
本项目使用C++实现一个具备服务器端和客户端即时通信且具有私聊功能的聊天室。
目的是学习C++网络开发的基本概念,同时也可以熟悉下Linux下的C++程序编译和简单MakeFile编写
二:需求分析
这个聊天室主要有两个程序:
1.服务端:能够接受新的客户连接,并将每个客户端发来的信息,广播给对应的目标客户端。
2.客户端:能够连接服务器,并向服务器发送消息,同时可以接收服务器发来的消息。
即最简单的C/S模型。
三:抽象与细化
服务端类需要支持:
1.支持多个客户端接入,实现聊天室基本功能。
2.启动服务,建立监听端口等待客户端连接。
3.使用epoll机制实现并发,增加效率。
4.客户端连接时,发送欢迎消息,并存储连接记录。
5.客户端发送消息时,根据消息类型,广播给所有用户(群聊)或者指定用户(私聊)。
6.客户端请求退出时,对相应连接信息进行清理。
客户端类需要支持:
1.连接服务器。
2.支持用户输入消息,发送给服务端。
3.接受并显示服务端发来的消息。
4.退出连接。
涉及两个事情,一个写,一个读。所以客户端需要两个进程分别支持以下功能。
子进程:
1.等待用户输入信息。
2.将聊天信息写入管道(pipe),并发送给父进程。
父进程:
1.使用epoll机制接受服务端发来的消息,并显示给用户,使用户看到其他用户的信息。
2.将子进程发送的聊天信息从管道(pipe)中读取出来,并发送给客户端。
四:C/S模型
TCP服务端通信常规步骤:
1.socket()创建TCP套接字
2.bind()将创建的套接字绑定到一个本地地址和端口上
3.listen(),将套接字设为监听模式,准备接受客户请求
4.accept()等用户请求到来时接受,返回一个对应此连接新套接字
5.用accept()返回的套接字和客户端进行通信,recv()/send() 接受/发送信息。
6.返回,等待另一个客户请求。
7.关闭套接字
TCP客户端通信常规步骤:
1.socket()创建TCP套接字。
2.connect()建立到达服务器的连接。
3.与客户端进行通信,recv()/send()接受/发送信息,write()/read() 子进程写入管道,父进程从管道中读取信息然后send给客户端
5. close() 关闭客户连接。
五:相关技术介绍
1.socket 阻塞与非阻塞
阻塞与非阻塞关注的是程序在等待调用结果时(消息,返回值)的状态。
阻塞调用是指在调用结果返回前,当前线程会被挂起,调用线程只有在得到调用结果之后才会返回。
非阻塞调用是指在不能立刻得到结果之前,该调用不会阻塞当前线程。
eg. 你打电话问书店老板有没有《网络编程》这本书,老板去书架上找,如果是阻塞式调用,你就会把自己一直挂起,守在电话边上,直到得到这本书有或者没有的答案。如果是非阻塞式调用,你可以干别的事情去,隔一段时间来看一下老板有没有告诉你结果。
同步异步是对书店老板而言(同步老板不会提醒你找到结果了,异步老板会打电话告诉你),阻塞和非阻塞是对你而言。
socket()函数创建套接字时,默认的套接字都是阻塞的,非阻塞设置方式代码:
//将文件描述符设置为非阻塞方式(利用fcntl函数)
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);
2. epoll
当服务端的人数越来越多,会导致资源吃紧,I/O效率越来越低,这时就应该考虑epoll,epoll是Linux内核为处理大量句柄而改进的poll,是linux特有的I/O函数。其特点如下:
1)epoll是Linux下多路复用IO接口select/poll的增强版本,其实现和使用方式与select/poll大有不同,epoll通过一组函数来完成有关任务,而不是一个函数。
2)epoll之所以高效,是因为epoll将用户关心的文件描述符放到内核里的一个事件列表中,而不是像select/poll每次调用都需要重复传入文件描述符集或事件集(大量拷贝开销),比如一个事件发生,epoll无需遍历整个被监听的描述符集,而只需要遍历哪些被内核IO事件异步唤醒而加入就绪队列的描述符集合即可。
3)epoll有两种工作方式,LT(Level triggered) 水平触发 、ET(Edge triggered)边沿触发。LT是select/poll的工作方式,比较低效,而ET是epoll具有的高速工作方式。
Epoll 用法(三步曲):
第一步:int epoll_create(int size)系统调用,创建一个epoll句柄,参数size用来告诉内核监听的数目,size为epoll支持的最大句柄数。
第二步:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 事件注册函数
参数 epfd为epoll的句柄。参数op 表示动作 三个宏来表示:EPOLL_CTL_ADD注册新fd到epfd 、EPOLL_CTL_MOD 修改已经注册的fd的监听事件、EPOLL_CTL_DEL从epfd句柄中删除fd。参数fd为需要监听的标识符。参数结构体epoll_event告诉内核需要监听的事件。
第三步:int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 等待事件的产生,通过调用收集在epoll监控中已经发生的事件。参数struct epoll_event 是事件队列 把就绪的事件放进去。
eg. 服务端使用epoll的时候步骤如下:
1.调用epoll_create()在linux内核中创建一个事件表。
2.然后将文件描述符(监听套接字listener)添加到事件表中
3.在主循环中,调用epoll_wait()等待返回就绪的文件描述符集合。
4.分别处理就绪的事件集合,本项目中一共有两类事件:新用户连接事件和用户发来消息事件。
六:代码结构
每个文件的作用:
1.Common.h:公共头文件,包括所有需要的宏以及socket网络编程头文件,以及消息结构体(用来表示消息类别等)
2.Client.h Client.cpp :客户端类的实现
3.Server.h Server.cpp : 服务端类的实现
4.ClientMain.cpp ServerMain.cpp 客户端及服务端的主函数。
七:代码实现
Common.h
定义一些共用的宏定义,包括一些共用的网络编程相关头文件。
1)定义一个函数将文件描述符fd添加到epfd表示的内核事件表中供客户端和服务端两个类使用。
2)定义一个信息数据结构,用来表示传送的信息,结构体包括发送方fd, 接收方fd,用来表示消息类别的type,还有文字信息。
函数recv() send() write() read() 参数传递是字符串,所以在传送前/接受后要把结构体转换为字符串/字符串转换为结构体。
#ifndef CHATROOM_COMMON_H
#define CHATROOM_COMMON_H
#include <iostream>
#include <list>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 默认服务器端IP地址
#define SERVER_IP "127.0.0.1"
// 服务器端口号
#define SERVER_PORT 8888
// int epoll_create(int size)中的size
// 为epoll支持的最大句柄数
#define EPOLL_SIZE 5000
// 缓冲区大小65535
#define BUF_SIZE 0xFFFF
// 新用户登录后的欢迎信息
#define SERVER_WELCOME "Welcome you join to the chat room! Your chat ID is: Client #%d"
// 其他用户收到消息的前缀
#define SERVER_MESSAGE "ClientID %d say >> %s"
#define SERVER_PRIVATE_MESSAGE "Client %d say to you privately >> %s"
#define SERVER_PRIVATE_ERROR_MESSAGE "Client %d is not in the chat room yet~"
// 退出系统
#define EXIT "EXIT"
// 提醒你是聊天室中唯一的客户
#define CAUTION "There is only one int the char room!"
// 注册新的fd到epollfd中
// 参数enable_et表示是否启用ET模式,如果为True则启用,否则使用LT模式
static void addfd( int epollfd, int fd, bool enable_et )
{
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN;
if( enable_et )
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
// 设置socket为非阻塞模式
// 套接字立刻返回,不管I/O是否完成,该函数所在的线程会继续运行
//eg. 在recv(fd...)时,该函数立刻返回,在返回时,内核数据还没准备好会返回WSAEWOULDBLOCK错误代码
fcntl(fd, F_SETFL, fcntl(fd, F_GETFD, 0)| O_NONBLOCK);
printf("fd added to epoll!\n\n");
}
//定义信息结构,在服务端和客户端之间传送
struct Msg
{
int type;
int fromID;
int toID;
char content[BUF_SIZE];
};
#endif // CHATROOM_COMMON_H
服务端类 Server.h Server.cpp
服务端需要的接口:
1)init()初始化
2)Start()启动服务
3)Close()关闭服务
4)广播消息给所有客户端函数 SendBroadcastMessage()
服务端的主循环中每次都会检查并处理EPOLL中的就绪事件,而就绪事件列表主要是两种类型:新连接或新消息。服务器会依次从就绪事件列表里提取事件进行处理,如果是新连接则accept()然后addfd(),如果是新消息则SendBroadcastMessage()实现聊天功能。
Server.h
#ifndef CHATROOM_SERVER_H
#define CHATROOM_SERVER_H
#include <string>
#include "Common.h"
using namespace std;
// 服务端类,用来处理客户端请求
class Server {
public:
// 无参数构造函数
Server();
// 初始化服务器端设置
void Init();
// 关闭服务
void Close();
// 启动服务端
void Start();
private:
// 广播消息给所有客户端
int SendBroadcastMessage(int clientfd);
// 服务器端serverAddr信息
struct sockaddr_in serverAddr;
//创建监听的socket
int listener;
// epoll_create创建后的返回值
int epfd;
// 客户端列表
list<int> clients_list;
};
//Server.cpp
#include <iostream>
#include "Server.h"
using namespace std;
// 服务端类成员函数
// 服务端类构造函数
Server::Server(){
// 初始化服务器地址和端口
serverAddr.sin_family = PF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
// 初始化socket
listener = 0;
// epool fd
epfd = 0;
}
// 初始化服务端并启动监听
void Server::Init() {
cout << "Init Server..." << endl;
//创建监听socket
listener = socket(PF_INET, SOCK_STREAM, 0);
if(listener < 0) { perror("listener"); exit(-1);}
//绑定地址
if( bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("bind error");
exit(-1);
}
//监听
int ret = listen(listener, 5);
if(ret < 0) {
perror("listen error");
exit(-1);
}
cout << "Start to listen: " << SERVER_IP << endl;
//在内核中创建事件表 epfd是一个句柄
epfd = epoll_create (EPOLL_SIZE);
if(epfd < 0) {
perror("epfd error");
exit(-1);
}
//往事件表里添加监听事件
addfd(epfd, listener, true);
}
// 关闭服务,清理并关闭文件描述符
void Server::Close() {
//关闭socket
close(listener);
//关闭epoll监听
close(epfd);
}
// 发送广播消息给所有客户端
int Server::SendBroadcastMessage(int clientfd)
{
// buf[BUF_SIZE] 接收新消息
// message[BUF_SIZE] 保存格式化的消息
char recv_buf[BUF_SIZE];
char send_buf[BUF_SIZE];
Msg msg;
bzero(recv_buf, BUF_SIZE);
// 接收新消息
cout << "read from client(clientID = " << clientfd << ")" << endl;
int len = recv(clientfd, recv_buf, BUF_SIZE, 0);
//清空结构体,把接受到的字符串转换为结构体
memset(&msg,0,sizeof(msg));
memcpy(&msg,recv_buf,sizeof(msg));
//判断接受到的信息是私聊还是群聊
msg.fromID=clientfd;
if(msg.content[0]=='\\'&&isdigit(msg.content[1])){
msg.type=1;
msg.toID=msg.content[1]-'0';
memcpy(msg.content,msg.content+2,sizeof(msg.content));
}
else
msg.type=0;
// 如果客户端关闭了连接
if(len == 0)
{
close(clientfd);
// 在客户端列表中删除该客户端
clients_list.remove(clientfd);
cout << "ClientID = " << clientfd
<< " closed.\n now there are "
<< clients_list.size()
<< " client in the char room"
<< endl;
}
// 发送广播消息给所有客户端
else
{
// 判断是否聊天室还有其他客户端
if(clients_list.size() == 1){
// 发送提示消息
memcpy(&msg.content,CAUTION,sizeof(msg.content));
bzero(send_buf, BUF_SIZE);
memcpy(send_buf,&msg,sizeof(msg));
send(clientfd, send_buf, sizeof(send_buf), 0);
return len;
}
//存放格式化后的信息
char format_message[BUF_SIZE];
//群聊
if(msg.type==0){
// 格式化发送的消息内容 #define SERVER_MESSAGE "ClientID %d say >> %s"
sprintf(format_message, SERVER_MESSAGE, clientfd, msg.content);
memcpy(msg.content,format_message,BUF_SIZE);
// 遍历客户端列表依次发送消息,需要判断不要给来源客户端发
list<int>::iterator it;
for(it = clients_list.begin(); it != clients_list.end(); ++it) {
if(*it != clientfd){
//把发送的结构体转换为字符串
bzero(send_buf, BUF_SIZE);
memcpy(send_buf,&msg,sizeof(msg));
if( send(*it,send_buf, sizeof(send_buf), 0) < 0 ) {
return -1;
}
}
}
}
//私聊
if(msg.type==1){
bool private_offline=true;
sprintf(format_message, SERVER_PRIVATE_MESSAGE, clientfd, msg.content);
memcpy(msg.content,format_message,BUF_SIZE);
// 遍历客户端列表依次发送消息,需要判断不要给来源客户端发
list<int>::iterator it;
for(it = clients_list.begin(); it != clients_list.end(); ++it) {
if(*it == msg.toID){
private_offline=false;
//把发送的结构体转换为字符串
bzero(send_buf, BUF_SIZE);
memcpy(send_buf,&msg,sizeof(msg));
if( send(*it,send_buf, sizeof(send_buf), 0) < 0 ) {
return -1;
}
}
}
//如果私聊对象不在线
if(private_offline){
sprintf(format_message,SERVER_PRIVATE_ERROR_MESSAGE,msg.toID);
memcpy(msg.content,format_message,BUF_SIZE);
bzero(send_buf,BUF_SIZE);
memcpy(send_buf,&msg,sizeof(msg));
if(send(msg.fromID,send_buf,sizeof(send_buf),0)<0)
return -1;
}
}
}
return len;
}
// 启动服务端
void Server::Start() {
// epoll 事件队列
static struct epoll_event events[EPOLL_SIZE];
// 初始化服务端
Init();
//主循环
while(1)
{
//epoll_events_count表示就绪事件的数目
int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if(epoll_events_count < 0) {
perror("epoll failure");
break;
}
cout << "epoll_events_count =\n" << epoll_events_count << endl;
//处理这epoll_events_count个就绪事件
for(int i = 0; i < epoll_events_count; ++i)
{
int sockfd = events[i].data.fd;
//新用户连接
if(sockfd == listener)
{
struct sockaddr_in client_address;
socklen_t client_addrLength = sizeof(struct sockaddr_in);
int clientfd = accept( listener, ( struct sockaddr* )&client_address, &client_addrLength );
cout << "client connection from: "
<< inet_ntoa(client_address.sin_addr) << ":"
<< ntohs(client_address.sin_port) << ", clientfd = "
<< clientfd << endl;
addfd(epfd, clientfd, true);
// 服务端用list保存用户连接
clients_list.push_back(clientfd);
cout << "Add new clientfd = " << clientfd << " to epoll" << endl;
cout << "Now there are " << clients_list.size() << " clients int the chat room" << endl;
// 服务端发送欢迎信息
cout << "welcome message" << endl;
char message[BUF_SIZE];
bzero(message, BUF_SIZE);
sprintf(message, SERVER_WELCOME, clientfd);
int ret = send(clientfd, message, BUF_SIZE, 0);
if(ret < 0) {
perror("send error");
Close();
exit(-1);
}
}
//处理用户发来的消息,并广播,使其他用户收到信息
else {
int ret = SendBroadcastMessage(sockfd);
if(ret < 0) {
perror("error");
Close();
exit(-1);
}
}
}
}
// 关闭服务
Close();
}
客户端类实现
需要的接口:
1)连接服务端connect()
2)退出连接close()
3)启动客户端Start()
Client.h
#ifndef CHATROOM_CLIENT_H
#define CHATROOM_CLIENT_H
#include <string>
#include "Common.h"
using namespace std;
// 客户端类,用来连接服务器发送和接收消息
class Client {
public:
// 无参数构造函数
Client();
// 连接服务器
void Connect();
// 断开连接
void Close();
// 启动客户端
void Start();
private:
// 当前连接服务器端创建的socket
int sock;
// 当前进程ID
int pid;
// epoll_create创建后的返回值
int epfd;
// 创建管道,其中fd[0]用于父进程读,fd[1]用于子进程写
int pipe_fd[2];
// 表示客户端是否正常工作
bool isClientwork;
// 聊天信息
Msg msg;
//结构体要转换为字符串
char send_buf[BUF_SIZE];
char recv_buf[BUF_SIZE];
//用户连接的服务器 IP + port
struct sockaddr_in serverAddr;
};
Client.cpp
#include <iostream>
#include "Client.h"
using namespace std;
// 客户端类成员函数
// 客户端类构造函数
Client::Client(){
// 初始化要连接的服务器地址和端口
serverAddr.sin_family = PF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
// 初始化socket
sock = 0;
// 初始化进程号
pid = 0;
// 客户端状态
isClientwork = true;
// epool fd
epfd = 0;
}
// 连接服务器
void Client::Connect() {
cout << "Connect Server: " << SERVER_IP << " : " << SERVER_PORT << endl;
// 创建socket
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock < 0) {
perror("sock error");
exit(-1);
}
// 连接服务端
if(connect(sock, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
perror("connect error");
exit(-1);
}
// 创建管道,其中fd[0]用于父进程读,fd[1]用于子进程写
if(pipe(pipe_fd) < 0) {
perror("pipe error");
exit(-1);
}
// 创建epoll
epfd = epoll_create(EPOLL_SIZE);
if(epfd < 0) {
perror("epfd error");
exit(-1);
}
//将sock和管道读端描述符都添加到内核事件表中
addfd(epfd, sock, true);
addfd(epfd, pipe_fd[0], true);
}
// 断开连接,清理并关闭文件描述符
void Client::Close() {
if(pid){
//关闭父进程的管道和sock
close(pipe_fd[0]);
close(sock);
}else{
//关闭子进程的管道
close(pipe_fd[1]);
}
}
// 启动客户端
void Client::Start() {
// epoll 事件队列
static struct epoll_event events[2];
// 连接服务器
Connect();
// 创建子进程
pid = fork();
// 如果创建子进程失败则退出
if(pid < 0) {
perror("fork error");
close(sock);
exit(-1);
} else if(pid == 0) {
// 进入子进程执行流程
//子进程负责写入管道,因此先关闭读端
close(pipe_fd[0]);
// 输入exit可以退出聊天室
cout << "Please input 'exit' to exit the chat room" << endl;
cout<<"\\ + ClientID to private chat "<<endl;
// 如果客户端运行正常则不断读取输入发送给服务端
while(isClientwork){
//清空结构体
memset(msg.content,0,sizeof(msg.content));
fgets(msg.content, BUF_SIZE, stdin);
// 客户输出exit,退出
if(strncasecmp(msg.content, EXIT, strlen(EXIT)) == 0){
isClientwork = 0;
}
// 子进程将信息写入管道
else {
//清空发送缓存
memset(send_buf,0,BUF_SIZE);
//结构体转换为字符串
memcpy(send_buf,&msg,sizeof(msg));
if( write(pipe_fd[1], send_buf, sizeof(send_buf)) < 0 ) {
perror("fork error");
exit(-1);
}
}
}
} else {
//pid > 0 父进程
//父进程负责读管道数据,因此先关闭写端
close(pipe_fd[1]);
// 主循环(epoll_wait)
while(isClientwork) {
int epoll_events_count = epoll_wait( epfd, events, 2, -1 );
//处理就绪事件
for(int i = 0; i < epoll_events_count ; ++i)
{
memset(recv_buf,0,sizeof(recv_buf));
//服务端发来消息
if(events[i].data.fd == sock)
{
//接受服务端广播消息
int ret = recv(sock, recv_buf, BUF_SIZE, 0);
//清空结构体
memset(&msg,0,sizeof(msg));
//将发来的消息转换为结构体
memcpy(&msg,recv_buf,sizeof(msg));
// ret= 0 服务端关闭
if(ret == 0) {
cout << "Server closed connection: " << sock << endl;
close(sock);
isClientwork = 0;
} else {
cout << msg.content << endl;
}
}
//子进程写入事件发生,父进程处理并发送服务端
else {
//父进程从管道中读取数据
int ret = read(events[i].data.fd, recv_buf, BUF_SIZE);
// ret = 0
if(ret == 0)
isClientwork = 0;
else {
// 将从管道中读取的字符串信息发送给服务端
send(sock, recv_buf, sizeof(recv_buf), 0);
}
}
}//for
}//while
}
// 退出进程
Close();
}
ClientMain.cpp
#include "Client.h"
// 客户端主函数
// 创建客户端对象后启动客户端
int main(int argc, char *argv[]) {
Client client;
client.Start();
return 0;
}
ServerMain.cpp
#include "Server.h"
// 服务端主函数
// 创建服务端对象后启动服务端
int main(int argc, char *argv[]) {
Server server;
server.Start();
return 0;
}
最后是Makefile 文件 对上面的文件进行编译
CC = g++
CFLAGS = -std=c++11
all: ClientMain.cpp ServerMain.cpp Server.o Client.o
$(CC) $(CFLAGS) ServerMain.cpp Server.o -o chatroom_server
$(CC) $(CFLAGS) ClientMain.cpp Client.o -o chatroom_client
Server.o: Server.cpp Server.h Common.h
$(CC) $(CFLAGS) -c Server.cpp
Client.o: Client.cpp Client.h Common.h
$(CC) $(CFLAGS) -c Client.cpp
clean:
rm -f *.o chatroom_server chatroom_client
注:需要C/C++ Linux服务器开发学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
相关推荐
- Excel批量生成随机人名_excel批量生成随机数
-
之前的文章讲过怎么用在Excel生成随机银行名字。今天继续给大家分享下怎么在Excel生成随机人名。随机数据工具包书接上回,本文对之前的随机数据生成工具包进行封装调用,生成的结果直接写入到Excel表...
- 一学就会:Excel MOD函数,让数字周期循环变得easy
-
今日推荐:MOD函数。目的:根据当前日期在年内的周数对5个小组取余,再根据余数的值获取对应的值班小组。MOD函数也可以作为获取随机数的一种,只不过这种随机数是有一定规律的。【函数介绍】MOD——返回两...
- 1条公式,自动随机分配座位,你会么?
-
随机座位困局、老办法效率低、新公式能否破局?.上周学校开会说要给教室换排座位,教务处老师愁得头发都快白了。以前都是靠老师自己写名单再划拉划分组,现在新教室三列座位,学生又多,折腾了三天都没摆顺当。听说...
- excel快速制作姓名随机分配表_姓名随机分组
-
快速制作随机分配表。当需要把这一列的姓名进行随机分组应该怎么操作?是不是还在一行一行的去复制粘贴,这样效率是非常慢的。怎么快速的制作一个随机的分组?·首先在第一组输入等于第一个姓名的A2单元格,双击填...
- Excel里实现随机分组案例:导入名单随机分组
-
大家好呀,今天来给大家分享如何快速在Excel里实现随机分组。如下图所示,有15个人,现在要随机分成3组,每组5个人。只要简单两步,就能完成分组。第一步:为每个人设置一个随机数并编序号C列输入公式=R...
- 办公必备的15个Excel技巧,绝对的硬核干货,收藏备用
-
Excel的灵魂在于数据的分析与统计,而分析与统计就离不开函数或公式,今天要给大家分享的15个函数公式,是工作中常用的,可以直接套用。一、从身份证号码中提取出生年月。函数1:Tex...
- Excel如何将某单元格区域数据随机排序
-
如下图是某公司人员名单,现在想要对这些员工进行随机分组。即对单元格区域内数据进行随机排序。选中B2:E10单元格区域点击下图选项(Excel工具箱,百度它即可了解详细的下载安装方法,本文这里就不做具体...
- 一键生成随机口算题,Excel工具妙用
-
小学生每天都要做口算,今天我给大家分享一下如何用excel来自制小学生的口算题。看我这里已经做好了,它的公式是这样,然后往下去拉,想要多少要多少,而且每一道题都是随机的。而且这一个表做好了之后,只要让...
- Excel秒变抽签神器!1分钟搞定随机点名/抽奖
-
还在为年会抽奖、课堂点名、分组任务抓狂?别求人写代码啦!Excel自带隐藏大招1分钟设置,永久使用,按个键就能开抽超简单3步设置(有手就会版)1随机号生成在姓名表旁新建「随机号」列输入=RAND...
- 基础函数20例,案例解读,再不掌握就真的Out了
-
Excel中的函数是Excel的一个重要工具,如果你不及时掌握,对于Excel的应用、工作效率等会受到很大的影响,今天,小编给大家分享20个Excel的基础函数,对大家肯定很有帮助。练习文件在文末领取...
- 怎么利用Excel实现随机取样_excel随机取数据
-
今天跟大家分享一下Excel如何随机抽样1.打开Excel软件2.选中要抽取数据的单元格区域3.点击下图选项(Excel工具箱,百度即可了解详细下载安装信息,本文这里就不做详细解说。)4.点击【统计与...
- 1分钟学会Excel总表更新,分表实时同步,再也不用熬夜了!
-
你是不是还在用筛选→复制→粘贴的老方法拆分Excel数据?每次按类别整理报表都要折腾半小时?别傻了!今天教你用FILTER函数一键搞定,数据更新还能自动同步!第一步:准备工作表新建3个工作表,分...
- excel计算几个数范围,excel怎么计算一个范围的个数
-
excel怎么计算某些范围的数的个数,需要计算0-5,5-10,10-15,……1000的...比如这些数字在A列,从B1至B10求10个范围的数量。在B1输入:=countif(a:a,=10)在B...
- 让Excel随机排序_excel如何设置随机排序
-
随机排序如下图,希望对A列的应聘人员随机安排面试顺序。先将标题复制到右侧的空白单元格内,然后在第一个标题下方输入公式:=SORTBY(A2:B11,RANDARRAY(10),1)RANDARRAY的...
- 对人员进行随机分组,分步骤详细解释,看了就学会了
-
大家好,我是套路EXCEL!如上图,需要将12个人随机分成3组,每组4人。函数公式如下:=ROUNDUP(CHOOSECOLS(SORT(HSTACK(ROW(1:12),RANDARRAY(12...
- 一周热门
- 最近发表
- 标签列表
-
- 外键约束 oracle (36)
- oracle的row number (32)
- 唯一索引 oracle (34)
- oracle in 表变量 (28)
- oracle导出dmp导出 (28)
- 多线程的创建方式 (29)
- 多线程 python (30)
- java多线程并发处理 (32)
- 宏程序代码一览表 (35)
- c++需要学多久 (25)
- css class选择器用法 (25)
- css样式引入 (30)
- css教程文字移动 (33)
- php简单源码 (36)
- php个人中心源码 (25)
- php小说爬取源码 (23)
- 云电脑app源码 (22)
- html画折线图 (24)
- docker好玩的应用 (28)
- linux有没有pe工具 (34)
- mysql数据库源码 (21)
- php开源万能表单系统源码 (21)
- 可以上传视频的网站源码 (25)
- 随机函数如何生成小数点数字 (31)
- 随机函数excel公式总和不变30个数据随机 (33)