文章目录
main.py 流程
#include <iostream>
#include "unpthread.h"
#include "unp.h"
using namespace std;
Thread * tptr;
// 网络编程中主要用于表示套接字地址结构的长度。它通常作为函数参数用于传递和接收套接字地址的长度信息
// 具体怎么用还得对addrlen具体分析
socklen_t addrlen; //套接字地址结构长度
int listenfd; // 套接字监听描述符
int navail, nprocesses; // 可用线程和进程数量
Room *room;
int main(int argc, char **argv)
{
void sig_chld(int signo); // 信号处理函数,用来处理SIGCHLD信号,表示子进程状态发生了变化
Signal(SIGCHLD, sig_chld);
int i,maxfd;
void thread_make(int);
//这里void改为了int 为了后面的兼容问题
int process_make(int, int); // 这里是进程定义
// 每个进程都会绑定到一个特定的房间(room),并且将每个进程的管道文件描述符添加到主fd_set集合(masterset)中
fd_set rset, masterset; // fd_set是一组文件描述字(fd)的集合
FD_ZERO(&masterset); // FD_ZERO(fd_set *fdset);将指定的文件描述符集清空,
if(argc == 4)
{
listenfd = Tcp_listen(NULL, argv[1], &addrlen);
}
else if(argc == 5)
{
listenfd = Tcp_listen(argv[1], argv[2], &addrlen); // 5 至此监听套接字就创建好了
}
else
{
err_quit("usage: ./app [host] <port #> <#threads> <#processes>");
}
maxfd = listenfd;
int nthreads = atoi(argv[argc - 2]); // 线程数量
nprocesses = atoi(argv[argc-1]); // 进程数量
//init room
room = new Room(nprocesses);
printf("total threads: %d total process: %d\n", nthreads, nprocesses);
// tptr 的主要作用是分配一块内存空间来存储 Thread 结构体类型的对象数组。
tptr = (Thread *)Calloc(nthreads, sizeof(Thread)); //这是根据线程数量 又要申请内存了??
//process pool----room 进程池
for(i = 0; i < nprocesses; i++) //创建房间呗?? 并且房间已经开始了
{
process_make(i, listenfd);
// FD_SET(int fd,fd_set*fdset);用于在文件描述符集合中增加一个新的文件描述符。
FD_SET(room->pptr[i].child_pipefd, &masterset);
maxfd = max(maxfd, room->pptr[i].child_pipefd);
}
// thread pool 线程池
// 多线程的实现部分。它创建了多个线程来处理客户端的连接和请求。
for(i = 0; i < nthreads; i++)
{
thread_make(i);
}
// 主要用于监听子进程管道的可读事件,并根据读取的数据类型进行相应的处理
for(;;) //死循环 直到退出
{
//listen
rset = masterset;
// rset需要检测的可读文件描述符的集合 通过select监听子进程的管道和客户端连接的输入
// 循环中通过select函数监听文件描述符集合rset上是否有可读事件发生。
int nsel = Select(maxfd + 1, &rset, NULL, NULL, NULL); // 参数maxfd是需要监视的最大的文件描述符值+1
if(nsel == 0) continue; //返回0,表示没有可读事件发生 继续下次循环
//set room status to 0(empty)
for(i = 0; i < nprocesses; i++)
{
// 遍历nprocesses个子进程,检查每个子进程的管道child_pipefd是否在rset中。
// 如果在其中,说明该子进程向管道中写入了数据。
if(FD_ISSET(room->pptr[i].child_pipefd, &rset)) // FD_ISSET测试一个文件描述符是否在集合中
{
char rc;
int n;
// Readn是一个函数,它用于从文件描述符中读取指定数量的数据。
// Readn函数被用来从 房间子进程的管道中 读取一个字节的数据
if((n = Readn(room->pptr[i].child_pipefd, &rc, 1)) <= 0)
{
err_quit("child %d terminated unexpectedly", i);
}
printf("c = %c\n", rc);
// rc是一个字符变量,用于保存从房间子进程的管道中读取的数据。它被用于判断读取的数据的类型
if(rc == 'E') // room empty 这个E哪来的??
{
pthread_mutex_lock(&room->lock); //互斥锁 在线程实际运行过程中,需要多个线程保持同步。
//这时可以用互斥锁来完成任务、
// 通过加锁保护共享资源room->pptr[i].child_status,将该子进程的状态设为0(空闲)
room->pptr[i].child_status = 0;
// 增加可用房间数 room->navail 然后释放锁。
room->navail++;
printf("room %d is now free\n", room->pptr[i].child_pid); //客户端连接断开 就说明房子空了?
pthread_mutex_unlock(&room->lock); // 释放互斥锁??
}
// 到的数据是字符 'Q',表示房间中的伙伴退出。
else if(rc == 'Q') // partner quit
{
// 使用自定义的线程互斥锁room->lock保护共享资源room->pptr[i].total的修改
Pthread_mutex_lock(&room->lock); //为何这里用自己定义的线程互斥锁??
// 将该子进程的伙伴总数减1。然后释放锁。
room->pptr[i].total--;
Pthread_mutex_unlock(&room->lock); //这里释放锁
}
// 表示读取到了 无效的数据,输出错误信息并继续下一次循环
else // trash data
{
err_msg("read from %d error", room->pptr[i].child_pipefd);
continue;
}
if(--nsel == 0) break; /*all done with select results*/
}
}
}
return 0;
}
// create threads
void thread_make(int i)
{
void * thread_main(void *);
int *arg = (int *) Calloc(1, sizeof(int));
*arg = i;
Pthread_create(&tptr[i].thread_tid, NULL, thread_main, arg);
}
// 实现了一个基于进程池的服务器模型,父进程负责监听新连接,子进程负责处理请求和发送数据。
int process_make(int i, int listenfd)
{
int sockfd[2]; // sockfd 数组(用于存储创建的套接字对)
pid_t pid; //pid变量(用于存储进程ID)
void process_main(int, int); //process_main函数(用于子进程的具体逻辑处理)
Socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd); // 创建了一个本地域套接字对,并将其存储在sockfd数组中
if((pid = fork()) > 0) // fork函数创建一个新的子进程 返回的值大于0,则表示当前进程是 父进程
{
Close(sockfd[1]); //它会关闭不需要的文件描述符 sockfd[1]
// 然后将一些相关信息存储到进程池中的房间结构体中,比如子进程的PID、管道文件描述符、子进程状态和处理请求总数等
room->pptr[i].child_pid = pid;
room->pptr[i].child_pipefd = sockfd[0];
room->pptr[i].child_status = 0;
room->pptr[i].total = 0;
return pid; // father 最后函数返回子进程的PID
}
// 子进程中,关闭不需要的文件描述符,即listenfd(父进程中监听套接字的文件描述符)
Close(listenfd); // child not need this open
Close(sockfd[0]); // sockfd[0]父进程中用于与子进程进行通信的文件描述符
// process_main创建了一个用于接受新连接的线程accept_fd和多个用于发送数据的线程send_func。然后进入一个无限循环,
process_main(i, sockfd[1]); /* never returns */ // 子进程具体逻辑处理 执行完此函数后不会返回
}
文件由三个函数组成,分别是main主函数,thread_make创建线程函数,process_make创建进程函数。main主函数中,先对信号进行初始化(信号是进程间通信的最古老的方式之一,异步通信)
然后声明两个函数,分别为创建线程函数和创建进程函数,然后设置文件描述符初始化。通过对传进来的参数进行判断后,利用Tcp_listen函数进行监听得到监听文件描述符listenfd,通过进程数量初始化房间。接下来创建进程池来处理客户端的连接请求,然后创建线程池来处理客户端的网络数据。最后进入一个服务器程序的主循环,用于监听子进程管道和客户端连接输入,循环实现了通过select函数监听多个文件描述符上的可读事件,并根据读取到的数据进行相应的处理。
主for循环
循环是一个服务器程序的主循环,用于监听子进程管道和客户端连接输入,并根据读取到的数据进行相应的处理。
- 首先,通过调用select函数在rset集合上监听可读事件。rset是需要检测的可读文件描述符的集合,包括子进程的管道和客户端连接的输入。如果select返回0,表示没有可读事件发生,则继续下一次循环。
- 然后,遍历nprocesses个子进程,检查每个子进程的管道child_pipefd是否在rset中。如果在其中,说明该子进程向管道中写入了数据。
- 接下来,通过Readn函数从房间子进程的管道中读取一个字节的数据,保存在变量rc中。根据读取到的数据类型进行相应的处理:
- 如果rc为’E’,表示房间空了,即客户端连接断开。这时,使用互斥锁(pthread_mutex_lock)保护共享资源room->pptr[i].child_status,将该子进程的状态设为0(空闲),增加可用房间数room->navail,并输出房间空闲的信息。
- 如果rc为’Q’,表示房间中的伙伴退出。使用自定义的线程互斥锁(room->lock)保护共享资源room->pptr[i].total的修改,将该子进程的伙伴总数减1。
- 如果读取到了无效的数据,输出错误信息。
- 最后,如果还有未处理的可读事件,继续下一次循环。
流程中函数详细解释:
Signal函数代码
void sig_chld(int signo);
Signal(SIGCHLD, sig_chld);
void sig_chld(int signo) //signal action
{
printf("signal\n");
pid_t pid;
int stat;
// waitpid函数来等待任意子进程的终止状态,并通过循环处理所有已终止的子进程
// 函数会检查子进程的终止原因,并执行一些后续操作,比如打印日志、回收子进程资源或重新启动子进程等。
while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
{
if(WIFEXITED(stat)) //正常终止
{
printf("child %d normal termination, exit status = %d\n",pid, WEXITSTATUS(stat));
}
else if(WIFSIGNALED(stat)) // 异常终止
{
printf("child %d abnormal termination, singal number = %d%s\n", pid, WTERMSIG(stat),
#ifdef WCOREDUMP
WCOREDUMP(stat)? " (core file generated) " : "");
#else
"");
#endif
}
}
return;
}
Sigfunc *Signal(int signo, Sigfunc * func)
{
struct sigaction act, oact; // 新和旧的信号要处理的动作
act.sa_handler = func;
sigemptyset(&act.sa_mask); // 清空act.sa_mask,确保在处理信号的时候不会阻塞其他信号
act.sa_flags = 0;
if(signo == SIGALRM){
#ifdef SA_INTERRUPT // 一些特定于操作系统的宏(如SA_INTERRUPT和SA_RESTART)这俩宏不同平台定义不同
act.sa_flags |= SA_INTERRUPT;
#endif
} else {
#ifdef SA_RESTART // 传入的信号编号,判断是否需要设置SA_INTERRUPT或SA_RESTART标志位到act.sa_flags中,
// 以控制系统对信号的默认行为(中断某些系统调用或重新启动被信号中断的系统调用)
act.sa_flags |= SA_RESTART;
#endif
}
// 使用sigaction函数将新的信号处理动作注册到系统中,并将原来的信号处理动作保存在oact中。
// 如果sigaction调用失败,则返回SIG_ERR
if(sigaction(signo, &act, &oact) < 0) // 功能:检查或者改变信号的处理,信号捕捉
{
return SIG_ERR;
}
return oact.sa_handler;
}
Signal(SIGCHLD, sig_chld) :注册信号处理函数 sig_chld来处理SIGCHLD信号。
在 Unix/Linux 系统中,当一个子进程终止时,会向其父进程发送 SIGCHLD信号。父进程可以通过捕获 SIGCHLD信号并注册一个相应的信号处理函数来处理子进程的终止状态。通过调用Signal(SIGCHLD, sig_chld)将sig_chld函数注册为处理SIGCHLD信号的信号处理函数。当父进程收到SIGCHLD信号时,会调用 sig_chld函数来处理已经终止的子进程。sig_chld)函数使用了 waitpid 函数来等待任意子进程的终止状态,并通加粗样式过循环处理所有已经终止的子进程。在处理每个子进程时,它会检查子进程的终止原因,并根据不同的情况执行相应的动作,例如打印日志或回收子进程资源。
总结起来,Signal(SIGCHLD, sig_chld)的作用是将sig_chld函数注册为处理SIGCHLD信号的信号处理函数,以便在父进程接收到子进程终止的信号时能够进行相应的处理。
fd_set(文件描述符集合)是一个在C语言中用于表示文件描述符的数据类型。它通常与I/O多路复用函数(如select()、pselect()、FD_SET()等)一起使用,用于监视和操作多个文件描述符的状态。rset和masterset是两个fd_set类型的变量,常用于实现一个基本的I/O多路复用循环。
masterset:一个保存所有待监视文件描述符的集合。在开始监听之前,将需要监听的文件描述符逐个添加到masterset中。
rset:一个用于存储就绪文件描述符的集合。在select()或类似函数调用后,rset将会被更新为处于就绪状态(可读)的文件描述符集合。
Tcp_listen函数代码
// 创建监听套接字并绑定地址 主机名 IP地址 服务名或端口号 传出参数 用于返回地址的长度
int Tcp_listen(const char * host, const char * service, socklen_t * addrlen)
{
int listenfd, n;
const int on = 1;
struct addrinfo hints, *res, *ressave;
bzero(&hints, sizeof(struct addrinfo));
// 获取适用于被动连接(例如服务器)的地址信息
hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC; //返回的是适用于指定主机名和服务名且适合任意协议族(IPv4或IPv6)的地址
hints.ai_socktype = SOCK_STREAM; // 表示获取用于流式传输的地址信息(例如TCP)
char addr[MAXSOCKADDR];
// 调用getaddrinfo函数来获取与给定主机名和服务名匹配的地址信息,存储在res指针指向的链表中。
if((n = getaddrinfo(host, service, &hints, &res)) > 0)
{
err_quit("tcp listen error for %s %s: %s", host, service, gai_strerror(n));
}
ressave = res;
// 下面是在循环中处理地址信息的常见做法,用于依次尝试不同的地址信息,直到找到合适的地址或者遍历完所有的地址信息
do // 循环遍历 链表 中的每个地址信息
{
listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if(listenfd < 0)
{
continue; //error try again
}
Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if(bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)
{
// 创建和绑定成功,则打印服务器的地址信息
printf("server address: %s\n", Sock_ntop(addr, MAXSOCKADDR, res->ai_addr, res->ai_addrlen));
break; //success
}
Close(listenfd);
}while((res = res->ai_next) != NULL);
freeaddrinfo(ressave); //free
if(res == NULL)
{
err_quit("tcp listen error for %s: %s", host, service);
}
Listen(listenfd, LISTENQ); //开始监听连接请求,并将地址的长度赋值给addrlen
if(addrlen)
{
*addrlen = res->ai_addrlen;
}
return listenfd; // 返回创建好的监听套接字listenfd
}
这段代码是一个用于创建TCP监听的函数Tcp_listen
,以及一些辅助函数Setsockopt
、Close
和Listen
。创建TCP监听套接字并绑定到指定的主机和服务上,以便接收客户端的连接请求。
-
Tcp_listen
函数的作用是创建一个TCP监听套接字并绑定到指定的主机和服务上。- 参数
host
是要监听的主机名或IP地址。 - 参数
service
是要监听的服务名或端口号。 - 参数
addrlen
是一个指向socklen_t
类型的指针,它用于返回绑定的地址长度。
- 参数
-
Tcp_listen
函数内部的逻辑如下:- 初始化变量
listenfd
和n
。 - 创建并初始化一个
struct addrinfo
结构体hints
,并设置相关参数。 - 调用
getaddrinfo
函数获取与给定主机名和服务名匹配的地址信息,并将结果存储在res
指针指向的链表中。 - 如果
getaddrinfo
函数返回错误,则调用err_quit
函数打印错误消息,并退出程序。 - 将
res
保存在ressave
中,用于释放内存。 - 循环遍历
res
指向的链表中的每个地址信息:- 调用
socket
函数创建一个套接字。 - 如果创建失败,则继续下一次循环。
- 调用
Setsockopt
函数设置套接字选项。 - 调用
bind
函数将套接字绑定到地址信息。 - 如果绑定成功,则打印服务器的地址信息,并跳出循环。
- 关闭套接字。
- 更新
res
为下一个地址信息。
- 调用
- 释放
ressave
所指向的内存空间。 - 如果无法找到合适的地址信息,则调用
err_quit
函数打印错误消息,并退出程序。 - 调用
Listen
函数开始监听连接请求,并将地址长度赋值给addrlen
指针所指向的变量。 - 如果
addrlen
不为NULL
,则将地址长度赋值给addrlen
指向的变量。 - 返回监听套接字
listenfd
。
- 初始化变量
-
Setsockopt
、Close
和Listen
是辅助函数,分别用于设置套接字选项、关闭套接字和监听套接字。它们的实现相对简单,主要是对对应的系统调用进行了封装,并在发生错误时调用err_msg
或err_quit
函数打印错误消息。
初始化房间代码
room = new Room(nprocesses);
typedef struct Room // single
{
int navail; // 表示 房间可用数量 的整数型变量。
Process *pptr; // 指向Process类的指针,用于表示房间内的进程
pthread_mutex_t lock; //互斥锁,用于保护对房间及其成员的访问
Room (int n) // 定义了一个Room的构造函数,接受一个整数参数n,用于初始化房间的可用数量
{
navail = n;
// pptr被动态分配了一个长度为n的Process类型数组,并使用Calloc函数(自定义的函数)进行内存分配
pptr = (Process *)Calloc(n, sizeof(Process));
lock = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥锁?
}
}Room;
void * Calloc(size_t n, size_t size)
{
void *ptr;
if( (ptr = calloc(n, size)) == NULL)
{
errno = ENOMEM;
err_quit("Calloc error");
}
return ptr; // 数返回一个void*类型的指针,指向分配的内存空间的 首地址
}
void err_quit(const char * fmt, ...)
{
va_list ap;
va_start(ap, fmt);
err_doit(1, errno, fmt, ap); // 将errnoflag设置为1表示需要打印错误号和错误信息
va_end(ap);
exit(1);
}
static void err_doit(int errnoflag, int error, const char *fmt, va_list ap)
{
char buf[MAXLINE];
// 将格式化字符串fmt和可变参数ap格式化成一个字符串,并将结果存在buf中。vsnprintf函数可以避免缓冲区溢出问题。
vsnprintf(buf, MAXLINE - 1, fmt, ap);
if(errnoflag)
{
// 将错误号error转换为相应的错误信息,并追加到buf字符串末尾。使用strerror函数将错误号转换为可读的错误描述
snprintf(buf + strlen(buf), MAXLINE - 1 - strlen(buf), ": %s", strerror(error));
}
strcat(buf, "\n");
fflush(stdout); //清洗标准输出流,确保之前的输出被及时刷新。
fputs(buf, stderr); // 将buf字符串输出到标准错误流stderr
fflush(NULL); // 清洗所有的输出流。
}
room = new Room(nprocesses)
的意思是创建一个名为 room
的 Room
对象,并调用 Room
类的构造函数 Room(int n)
进行初始化。上述代码中的 room = new Room(nprocesses)
表示创建了一个名为 room
的对象,该对象是 Room
类的实例。通过调用 Room
类的构造函数 Room(int n)
,将参数 nprocesses
传递给构造函数,用于初始化房间的可用数量。
具体来说,这行代码通过使用 new
运算符在堆上分配了内存空间,然后调用了 Room
类的构造函数 Room(int n)
,并将返回的指针赋值给 room
变量。
构造函数 Room(int n)
中,navail
成员变量被赋值为传入的参数 n
,表示房间的可用数量为 n
。然后,使用 Calloc
函数(这里假设为自定义的函数)动态分配了一个长度为 n
的 Process
类型数组,并将其地址赋值给 pptr
成员变量。最后,使用 PTHREAD_MUTEX_INITIALIZER
宏对 lock
互斥锁进行了初始化,以便后续对房间及其成员的访问进行保护。因此,room = new Room(nprocesses);
的含义是创建了一个包含 nprocesses
个可用数量的房间,并将其赋值给名为 room
的变量。
各函数具体参数:
- 结构体 Room:
int navail
:表示房间可用数量的整数型变量。Process *pptr
:指向 Process 类的指针,用于表示房间内的进程。pthread_mutex_t lock
:互斥锁,用于保护对房间及其成员的访问。
- 构造函数 Room(int n):
- 参数
n
:用于初始化房间的可用数量。 navail = n
:将房间可用数量设为传入的参数值。pptr = (Process *)Calloc(n, sizeof(Process))
:使用自定义的Calloc
函数在堆上动态分配一个长度为n
的Process
类型数组,并将其地址赋值给pptr
指针。lock = PTHREAD_MUTEX_INITIALIZER
:初始化互斥锁,供后续使用。
- 参数
- 函数
void * Calloc(size_t n, size_t size)
:- 参数
n
和size
:分别表示需要分配的元素数量和每个元素的大小。 ptr
:用于保存分配的内存空间的指针。- 根据传入的参数使用
calloc
函数在堆上分配内存空间,并将结果赋值给ptr
。 - 如果分配失败,则设置
errno
为ENOMEM
(内存不足错误)并调用err_quit
函数报错。 - 返回指向分配的内存空间首地址的
ptr
指针。
- 参数
- 函数
void err_quit(const char * fmt, ...)
:- 参数
fmt
:格式化字符串,用于指定错误信息的格式。 va_list ap
:可变参数列表。va_start(ap, fmt)
:初始化可变参数列表ap
。err_doit(1, errno, fmt, ap)
:调用err_doit
函数将错误信息输出到标准错误流 stderr。将errnoflag
设置为 1 表示需要打印错误号和错误信息。va_end(ap)
:结束可变参数的获取。exit(1)
:退出程序,返回状态码 1。
- 参数
- 静态函数
static void err_doit(int errnoflag, int error, const char *fmt, va_list ap)
:- 参数
errnoflag
:表示是否打印错误号的标志位。 - 参数
error
:存储错误号的变量。 - 参数
fmt
:格式化字符串,用于指定错误信息的格式。 - 参数
ap
:可变参数列表。 char buf[MAXLINE]
:用于存储格式化后的字符串。- 使用
vsnprintf
函数将格式化字符串fmt
和可变参数ap
格式化成一个字符串,并将结果存储在buf
中。这样可以避免缓冲区溢出问题。 - 如果
errnoflag
为真(即非零),则将错误号转换为可读的错误描述,并追加到buf
末尾。使用strerror
函数将错误号转换为错误描述。 - 在
buf
末尾加上换行符\n
,以及确保标准输出被刷新。 - 将
buf
字符串输出到标准错误流 stderr。 - 刷新所有的输出流。
- 参数
tptr = (Thread *)Calloc(nthreads, sizeof(Thread));
typedef struct // 定义一个结构体并给它取个别名 这里的别名就是Thread
{
pthread_t thread_tid;
}Thread;
void * Calloc(size_t n, size_t size)
{
void *ptr;
if( (ptr = calloc(n, size)) == NULL)
{
errno = ENOMEM;
err_quit("Calloc error");
}
return ptr; // 数返回一个void*类型的指针,指向分配的内存空间的 首地址
}
tptr = (Thread *)Calloc(nthreads, sizeof(Thread));代码的作用
这段代码的作用是动态分配一片内存空间,用于存储 nthreads个Thread 结构体对象,并将该空间的首地址通过tptr 指针返回。
具体解析如下:
typedef struct {...} Thread;
:定义了一个结构体 Thread,并使用 typedef 关键字给它取了别名 Thread。void *Calloc(size_t n, size_t size)
:是一个函数,用于动态分配内存空间。它接收两个参数:n 表示需要分配的元素数量,size 表示每个元素的大小。函数返回一个指向分配的内存空间的 void 指针。tptr = (Thread *)Calloc(nthreads, sizeof(Thread));
:调用 Calloc 函数分配大小为 nthreads * sizeof(Thread) 的内存空间,并将其首地址通过类型转换(Thread *)
赋值给 tptr 指针。由于 Thread 是之前通过 typedef 定义的别名,因此可以直接使用 Thread 类型。
综上所述,代码的作用是在堆上分配一片大小为 nthreads 个 Thread 结构体对象的内存空间,并通过 tptr 指针返回该空间的首地址。
进程池的创建
服务器启动时创建多个进程,并将这些进程绑定到不同的房间,以便并发的处理多个客户端的连接。每个进程都独立的处理自己分配到的客户端请求,从而提高并发能力。
void Close(int fd) //关闭套接字
{
if(close(fd) < 0)
{
err_msg("Close error");
}
}
void process_main(int i, int fd) // room start
{
//create accpet fd thread
printf("room %d starting \n", getpid());
Signal(SIGPIPE, SIG_IGN);
pthread_t pfd1;
void* accept_fd(void *); // 创建一个接收文件描述符(接受新连接)的线程 accept_fd
void* send_func(void *); // 多个用于发送数据的线程 send_func
void fdclose(int, int);
int *ptr = (int *)malloc(4);
*ptr = fd;
Pthread_create(&pfd1, NULL, accept_fd, ptr); // accept fd
for(int i = 0; i < SENDTHREADSIZE; i++)
{
Pthread_create(&pfd1, NULL, send_func, NULL); // 循环创建SENDTHREADSIZE个线程来执行send_func函数
}
//listen read data from fds
for(;;) // 进入一个无限循环,不断监听文件描述符集合(fd)中的套接字上是否有可读事件发生。
{
fd_set rset = user_pool->fdset;// 将user_pool中的文件描述符集合fdset 复制给变量rset。
int nsel;
struct timeval time;
memset(&time, 0, sizeof(struct timeval));
// Select() 函数监听文件描述符上是否有可读事件发生,当超时时间为零时阻塞等待。
while((nsel = Select(maxfd + 1, &rset, NULL, NULL, &time))== 0)
{
rset = user_pool->fdset; // make sure rset update
}
// 有可读事件发生,则遍历所有文件描述符(从0到maxfd),检查是否有新的数据到达。
for(int i = 0; i <= maxfd; i++)
{
//check data arrive
if(FD_ISSET(i, &rset)) // 如果有数据到达
{
char head[15] = {0};
int ret = Readn(i, head, 11); // head size = 11 首先读取头部11字节的内容
if(ret <= 0) //如果读取的字节数<=0,则表示对端关闭了连接,关闭此文件描述符并进行相关清理操作
{
printf("peer close\n");
fdclose(i, fd);
}
else if(ret == 11) //读取的字节数等于11,则对消息头部进行解析判断,并根据不同的消息类型执行相应的逻辑处理
{
if(head[0] == '$') // 消息头部以 $ 开头,则表示是有效的消息
{
//solve datatype
MSG_TYPE msgtype;
memcpy(&msgtype, head + 1, 2); // 解析消息类型、目标文件描述符、IP 地址和消息长度等信息
msgtype = (MSG_TYPE)ntohs(msgtype);
MSG msg;
memset(&msg, 0, sizeof(MSG));
msg.targetfd = i;
memcpy(&msg.ip, head + 3, 4);
int msglen;
memcpy(&msglen, head + 7, 4);
msg.len = ntohl(msglen);
// 消息类型是图片发送、音频发送或文本发送,则将消息加入发送队列 sendqueue 中等待处理。
if(msgtype == IMG_SEND || msgtype == AUDIO_SEND || msgtype == TEXT_SEND)
{
msg.msgType = (msgtype == IMG_SEND) ? IMG_RECV : ((msgtype == AUDIO_SEND)? AUDIO_RECV : TEXT_RECV);
msg.ptr = (char *)malloc(msg.len);
msg.ip = user_pool->fdToIp[i];
if((ret = Readn(i, msg.ptr, msg.len)) < msg.len)
{
err_msg("3 msg format error");
}
else
{
int tail;
Readn(i, &tail, 1);
if(tail != '#')
{
err_msg("4 msg format error");
}
else
{
sendqueue.push_msg(msg);
}
}
}
// 消息类型是关闭摄像头,则将消息加入发送队列 sendqueue 中等待处理。
else if(msgtype == CLOSE_CAMERA)
{
char tail;
Readn(i, &tail, 1);
if(tail == '#' && msg.len == 0)
{
msg.msgType = CLOSE_CAMERA;
sendqueue.push_msg(msg);
}
else
{
err_msg("camera data error ");
}
}
}
else // 如果读取的字节数不等于 11,则表示消息格式错误,执行相应的错误处理
{
err_msg("1 msg format error");
}
}
else
{
err_msg("2 msg format error");
}
if(--nsel <= 0) break; // 减少还需要处理的文件描述符计数 nsel,并跳出内循环继续监听剩余的文件描述符。
}
}
}
}
// 实现了一个基于进程池的服务器模型,父进程负责监听新连接,子进程负责处理请求和发送数据。
int process_make(int i, int listenfd)
{
int sockfd[2]; // sockfd 数组(用于存储创建的套接字对)
pid_t pid; //pid变量(用于存储进程ID)
void process_main(int, int); //process_main函数(用于子进程的具体逻辑处理)
Socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd); // 创建了一个本地域套接字对,并将其存储在sockfd数组中
if((pid = fork()) > 0) // fork函数创建一个新的子进程 返回的值大于0,则表示当前进程是 父进程
{
Close(sockfd[1]); //它会关闭不需要的文件描述符 sockfd[1]
// 然后将一些相关信息存储到进程池中的房间结构体中,比如子进程的PID、管道文件描述符、子进程状态和处理请求总数等
room->pptr[i].child_pid = pid;
room->pptr[i].child_pipefd = sockfd[0];
room->pptr[i].child_status = 0;
room->pptr[i].total = 0;
return pid; // father 最后函数返回子进程的PID
}
// 在父进程中创建了一个监听套接字listenfd用于接受客户端的连接请求。但在子进程中并不需要使用这个监听套接字,
// 因为子进程的主要任务是处理已经建立连接的客户端请求,而不涉及监听新连接。
// 因此,在子进程中关闭listenfd可以释放对应的文件描述符,避免造成资源浪费和混乱。
Close(listenfd); // child not need this open
// sockfd[0]是父进程用于与子进程通信的文件描述符,而在子进程中并不需要使用该文件描述符进行通信。
// 因此,通过关闭sockfd[0]可以释放对应的文件描述符,避免造成资源浪费和混乱。
// 需要注意的是,关闭sockfd[0]只会影响到当前子进程,并不会影响其他子进程或父进程。
// 每个子进程都会有自己独立的文件描述符表,关闭某个文件描述符只会对当前进程产生影响。
Close(sockfd[0]); // sockfd[0]父进程中用于与子进程进行通信的文件描述符
// process_main创建了一个用于接受新连接的线程accept_fd和多个用于发送数据的线程send_func。然后进入一个无限循环,
// 不断监听文件描述符集合中的套接字上是否有可读事件发生。如果有可读事件发生,它会读取消息头部
// 并根据消息类型执行相应的逻辑处理
process_main(i, sockfd[1]); /* never returns */ // 子进程具体逻辑处理 执行完此函数后不会返回
}
for(i = 0; i < nprocesses; i++) //创建房间呗?? 并且房间已经开始了
{
process_make(i, listenfd);
// FD_SET(int fd,fd_set*fdset);用于在文件描述符集合中增加一个新的文件描述符。
FD_SET(room->pptr[i].child_pipefd, &masterset);
maxfd = max(maxfd, room->pptr[i].child_pipefd); // maxfd用于多路复用技术(select poll epoll),设置监听的文件描述符范围
}
process_make
函数的作用是创建子进程,在子进程中执行具体的逻辑处理,并设置子进程的通信管道和状态。父进程主要负责创建子进程和管理子进程的相关信息。子进程则负责接收新连接、发送数据以及处理请求。通过通信管道实现父子进程之间的数据交换。
在上述代码中,父进程和子进程分别承担以下作用:
-
父进程:
- 创建本地域套接字对,并将其存储在
sockfd
数组中。 - 通过
fork
函数创建一个新的子进程,并根据返回值判断当前进程是父进程还是子进程。 - 如果是父进程,关闭不需要的文件描述符,将子进程的PID、管道文件描述符、子进程状态和处理请求总数等信息存储到进程池中的房间结构体中,并返回子进程的PID。
- 创建本地域套接字对,并将其存储在
-
子进程:
- 关闭不需要的文件描述符,包括父进程中监听套接字的文件描述符
listenfd
和与父进程进行通信的文件描述符sockfd[0]
。 - 调用
process_main
函数进行具体的逻辑处理。 - 在
process_main
函数中,子进程会创建一个线程accept_fd
来接受新连接,并循环创建多个线程send_func
用于发送数据。 - 子进程进入一个无限循环,在循环中使用
Select
函数监听文件描述符上是否有可读事件发生,并根据不同的消息类型执行相应的逻辑处理。
- 关闭不需要的文件描述符,包括父进程中监听套接字的文件描述符
具体步骤如下:
- 创建一个本地域套接字对,通过Socketpair函数将它们存储在sockfd数组中。套接字对主要用于父子进程之间的通信。
- 调用fork函数创建一个新的子进程。如果返回值大于0,则表示当前进程是父进程。
- 在父进程中,关闭不需要的文件描述符sockfd[1],即关闭用于向子进程发送数据的管道的写端。
- 将相关信息存储到进程池中的房间结构体中,包括子进程的PID、管道文件描述符、子进程状态和处理请求总数等。
- 返回子进程的PID。
- 在子进程中,关闭不需要的文件描述符,包括父进程中的监听套接字文件描述符listenfd和用于与父进程通信的管道的读端sockfd[0]。
- 调用process_main函数,该函数创建一个用于接收新连接的线程accept_fd和多个用于发送数据的线程send_func。
- 进入一个无限循环,不断监听文件描述符集合中的套接字上是否有可读事件发生。
- 如果有可读事件发生,从套接字中读取消息头部并根据消息类型执行相应的逻辑处理。
process_main
处理客户端连接的房间(room)的主函数,函数的作用是在子进程中执行具体的逻辑处理。它创建了一个用于接收新连接(文件描述符)的线程accept_fd和多个用于发送数据的线程send_func,并通过监听文件描述符集合中的套接字上是否有可读事件发生来处理不同类型的消息。同时,也实现了错误处理和容错机制。
为什么只有一个accept_fd
,而有多个send_func
呢?
在上述代码中,只创建一个accept_fd
线程而创建多个send_func
线程的设计是为了实现并发处理客户端连接和数据发送的功能。
accept_fd
线程:
该线程主要负责处理客户端的连接请求,接收客户端的连接,并与客户端建立起通信。由于每个客户端连接需要占用一个线程来处理,因此只需创建一个accept_fd
线程即可。send_func
线程:
这些线程负责向客户端发送数据。由于数据发送的过程可能涉及网络延迟或者阻塞,为了提高并发性和响应能力,可以创建多个send_func
线程并行地向不同的客户端发送数据。这样可以同时处理多个客户端的数据发送请求,减少等待时间,提高系统的吞吐量。
通过将连接请求和数据发送两个功能分开,分别由不同的线程处理,可以实现更高效的并发处理。accept_fd
线程负责接收新的连接请求,并将连接传递给相应的send_func
线程进行数据发送,从而充分利用系统资源,提高服务器的性能和并发处理能力。
需要注意的是,在实际应用中,还需要考虑线程之间的同步与数据共享,例如使用锁来保护共享数据,以避免竞争条件和数据不一致等问题。
rset = user_pool->fdset
解释:
select函数在每次调用时,都需要传入一个可读文件描述符集合来检查是否有可读事件发生,因此需要使用变量rset来存储要传递给select函数的文件描述符集合的值,同时确保每次循环中rset的值与fdset保持同步。
FD_ISSET(i, &rset)
解释:
一个宏定义,用于检查给定的文件描述符 i
是否在文件描述符集合 rset
中被设置为可读。FD_ISSET(i, &rset)
的作用是检查位图中与文件描述符 i
对应的位是否被设置为 1,如果设置为 1,则表示该文件描述符对应的套接字上有数据可读。具体解释如下:
- 参数
i
:表示要检查的文件描述符。 - 参数
&rset
:表示要检查的文件描述符集合,即将要进行可读事件检查的文件描述符集合。 - 返回值:如果文件描述符
i
在文件描述符集合rset
中被设置为可读,返回值为真(非零);否则返回值为假(零)。
通过使用FD_ISSET(i, &rset)
进行判断,可以确定哪些文件描述符对应的套接字上有可读事件发生,然后针对这些文件描述符执行相应的操作或处理。通过遍历文件描述符集合rset
,使用FD_ISSET
来检查每个文件描述符的可读状态,可以有效地处理多个文件描述符上的并发读取操作。一旦FD_ISSET(i, &rset)
返回真,就意味着对应的文件描述符i
上有可读事件发生,可以读取套接字上的数据并进行相应的处理。
具体步骤如下:
- 输出当前进程的PID作为房间的标识符。
- 忽略SIGPIPE信号,避免在发送数据给已关闭的套接字时导致进程终止。
- 创建一个用于接收新连接的线程accept_fd和多个用于发送数据的线程send_func。
- 分配内存并初始化用于存储套接字文件描述符的指针ptr,将传入的fd赋值给该指针。
- 调用Pthread_create函数创建线程accept_fd,并传入ptr作为参数。accept_fd函数用于接收新连接。
- 使用循环创建SENDTHREADSIZE个线程来执行send_func函数,send_func函数用于发送数据。
- 进入一个无限循环,不断监听文件描述符集合中的套接字是否有可读事件发生。
- 复制user_pool中的文件描述符集合fdset到变量rset,并设置超时时间为0。
- 调用Select函数监听文件描述符上是否有可读事件发生,当超时时间为零时,如果没有事件发生,则重新更新rset。
- 遍历所有文件描述符,检查是否有新的数据到达。
- 对于有新数据到达的文件描述符,首先读取头部11字节的内容并判断读取结果。
- 如果读取的字节数小于等于0,表示对端关闭了连接,则关闭此文件描述符并进行相关清理操作。
- 如果读取的字节数等于11,则对消息头部进行解析判断,并根据不同的消息类型执行相应的逻辑处理。
- 如果消息类型是图片发送、音频发送或文本发送,则将消息加入发送队列sendqueue中等待处理。
- 如果消息类型是关闭摄像头,则将消息加入发送队列sendqueue中等待处理。
- 根据实际情况进行错误处理和容错机制。
- 最后重新更新需要处理的文件描述符计数nsel,并继续监听剩余的文件描述符。
CLOSE_CAMERA为什么要与其他的 IMG_SEND、AUDIO_SEND、TEXT_SEND分开来讲?
原因是因为它们在逻辑上具有不同的含义和处理方式。
IMG_SEND
、AUDIO_SEND
、TEXT_SEND
消息类型:
这些消息类型表示需要发送图片、音频或文本数据。在处理这些消息时,程序会将消息放入发送队列等待处理,并对接收到的数据进行相关的校验和处理。CLOSE_CAMERA
消息类型:
这个消息类型表示关闭摄像头操作。当客户端发送了一个CLOSE_CAMERA
消息时,服务器会解析该消息并执行相应的关闭摄像头的操作。由于关闭摄像头操作不涉及具体的数据发送,因此其处理逻辑与发送数据的逻辑有所区别。
通过将CLOSE_CAMERA
消息类型与其他消息类型分开讲解,可以更清晰地理解这两种不同类型消息的处理方式和目的。这样的组织结构使得代码更加清晰和易于维护。同时,通过区分不同类型的消息,可以在后续的代码编写中方便地扩展和修改各个消息类型的处理逻辑,提高代码的可读性和可维护性。
accept_fd函数
void* accept_fd(void *arg) //accept fd from father
{
uint32_t getpeerip(int);
Pthread_detach(pthread_self());
int fd = *(int *)arg, tfd = -1;
free(arg);
while(1)
{
int n, c;
if((n = read_fd(fd, &c, 1, &tfd)) <= 0)
{
err_quit("read_fd error");
}
if(tfd < 0)
{
printf("c = %c\n", c);
err_quit("no descriptor from read_fd");
}
//add to poll
if(c == 'C')
{ // //create 收到字符'C'表示创建房间,将文件描述符添加到用户池中,并向该文件描述符发送创建会议的响应消息
Pthread_mutex_lock(&user_pool->lock); //lock
FD_SET(tfd, &user_pool->fdset);
user_pool->owner = tfd;
user_pool->fdToIp[tfd] = getpeerip(tfd);
user_pool->num++;
// user_pool->fds[user_pool->num++] = tfd;
user_pool->status[tfd] = ON;
maxfd = MAX(maxfd, tfd);
//printf("c %d\n", maxfd);
//write room No to tfd
roomstatus = ON; // set on
Pthread_mutex_unlock(&user_pool->lock); //unlock
MSG msg;
msg.msgType = CREATE_MEETING_RESPONSE;
msg.targetfd = tfd;
int roomNo = htonl(getpid());
msg.ptr = (char *) malloc(sizeof(int));
memcpy(msg.ptr, &roomNo, sizeof(int));
msg.len = sizeof(int);
sendqueue.push_msg(msg);
// printf("create meeting: %d\n", tfd);
}
else if(c == 'J') // join
{ // 如果收到字符'J',表示加入房间,将文件描述符添加到用户池中,并向其他用户 广播 该用户的加入消息。(广播得注意)
Pthread_mutex_lock(&user_pool->lock); //lock
if(roomstatus == CLOSE) // meeting close (owner close)
{
close(tfd);
Pthread_mutex_unlock(&user_pool->lock); //unlock
continue;
}
else
{
FD_SET(tfd, &user_pool->fdset);
user_pool->num++;
// user_pool->fds[user_pool->num++] = tfd;
user_pool->status[tfd] = ON;
maxfd = MAX(maxfd, tfd);
user_pool->fdToIp[tfd] = getpeerip(tfd);
Pthread_mutex_unlock(&user_pool->lock); //unlock
//broadcast to others
MSG msg;
memset(&msg, 0, sizeof(MSG));
msg.msgType = PARTNER_JOIN;
msg.ptr = NULL;
msg.len = 0;
msg.targetfd = tfd;
msg.ip = user_pool->fdToIp[tfd];
sendqueue.push_msg(msg);
//broadcast to others
MSG msg1;
memset(&msg1, 0, sizeof(MSG));
msg1.msgType = PARTNER_JOIN2;
msg1.targetfd = tfd;
int size = user_pool->num * sizeof(uint32_t);
msg1.ptr = (char *)malloc(size);
int pos = 0;
for(int i = 0; i <= maxfd; i++)
{
if(user_pool->status[i] == ON && i != tfd)
{
uint32_t ip = user_pool->fdToIp[i];
memcpy(msg1.ptr + pos, &ip, sizeof(uint32_t));
pos += sizeof(uint32_t);
msg1.len += sizeof(uint32_t);
}
}
sendqueue.push_msg(msg1);
printf("join meeting: %d\n", msg.ip);
}
}
}
return NULL;
}
accept_fd
是一个线程函数,用于接收来自父进程的文件描述符,并进行相应的处理。该函数的主要作用是处理来自父进程的文件描述符,并根据不同的请求类型添加到用户池中,同时发送适当的响应和广播消息给其他客户端。以下是对代码的详细逻辑解析:
- 解析参数:
- 从
arg
参数中获取文件描述符fd
和临时文件描述符tfd
。 - 释放
arg
的内存空间。
- 从
- 进入无限循环:
- 读取来自父进程的文件描述符并存储到
tfd
中。- 如果读取失败(返回值小于等于0),则报错。
- 如果
tfd
小于0(即未读取到有效的文件描述符),则报错。 - 否则,继续下一步操作。
- 读取来自父进程的文件描述符并存储到
- 根据读取到的字符
c
进行不同的处理:- 如果
c
是字符'C'
,表示收到创建房间的请求。- 在加锁的情况下,将
tfd
添加到用户池中。 - 设置会议的所有者为
tfd
。 - 将
tfd
对应的 IP 地址存储在用户池中。 - 增加用户池中的用户数量。
- 将
tfd
的状态设置为ON
。 - 更新最大文件描述符
maxfd
。 - 发送带有创建房间响应消息的
msg
到发送队列sendqueue
中。
- 在加锁的情况下,将
- 如果
c
是字符'J'
,表示收到加入房间的请求。- 在加锁的情况下,判断会议状态,如果会议已关闭(由所有者关闭),则关闭
tfd
并进入下一次循环。 - 否则,
- 将
tfd
添加到用户池中。 - 增加用户池中的用户数量。
- 将
tfd
的状态设置为ON
。 - 更新最大文件描述符
maxfd
。 - 将
tfd
对应的 IP 地址存储在用户池中。 - 发送会议中其他成员的加入消息给其他成员(使用
msg
)。 - 构造包含其他成员 IP 地址列表的消息
msg1
,并发送给新加入的成员tfd
。 - 打印新加入成员的 IP 地址。
- 将
- 在加锁的情况下,判断会议状态,如果会议已关闭(由所有者关闭),则关闭
- 如果
- 返回
NULL
。
send_func函数
// 用于从发送队列中获取消息并发送给对应的文件描述符
void *send_func(void *arg)
{
Pthread_detach(pthread_self());
char * sendbuf = (char *)malloc(4 * MB);
/*
* $_msgType_ip_size_data_#
*/
for(;;)
{
memset(sendbuf, 0, 4 * MB);
MSG msg = sendqueue.pop_msg();
int len = 0;
sendbuf[len++] = '$';
short type = htons((short)msg.msgType);
memcpy(sendbuf + len, &type, sizeof(short)); //msgtype
len+=2;
if(msg.msgType == CREATE_MEETING_RESPONSE || msg.msgType == PARTNER_JOIN2)
{
len += 4;
}
// 对于创建会议的响应、退出会议、接收文本、接收图片、接收音频和关闭摄像头等消息类型,会将消息发送给用户池
// 中除目标文件描述符外的其他文件描述符。对于加入房间的消息类型,会将消息发送给用户池中的所有文件描述符。对于广播
// 用户列表的消息类型,会将消息发送给目标文件描述符。
else if(msg.msgType == TEXT_RECV || msg.msgType == PARTNER_EXIT || msg.msgType == PARTNER_JOIN || msg.msgType == IMG_RECV || msg.msgType == AUDIO_RECV || msg.msgType == CLOSE_CAMERA)
{
memcpy(sendbuf + len, &msg.ip, sizeof(uint32_t));
len+=4;
}
int msglen = htonl(msg.len);
memcpy(sendbuf + len, &msglen, sizeof(int));
len += 4;
memcpy(sendbuf + len, msg.ptr, msg.len);
len += msg.len;
sendbuf[len++] = '#';
Pthread_mutex_lock(&user_pool->lock);
if(msg.msgType == CREATE_MEETING_RESPONSE)
{
//send buf to target
if(writen(msg.targetfd, sendbuf, len) < 0)
{
err_msg("writen error");
}
}
else if(msg.msgType == PARTNER_EXIT || msg.msgType == IMG_RECV || msg.msgType == AUDIO_RECV || msg.msgType == TEXT_RECV || msg.msgType == CLOSE_CAMERA)
{
for(int i = 0; i <= maxfd; i++)
{
if(user_pool->status[i] == ON && msg.targetfd != i)
{
if(writen(i, sendbuf, len) < 0)
{
err_msg("writen error");
}
}
}
}
else if(msg.msgType == PARTNER_JOIN)
{
for(int i = 0; i <= maxfd; i++)
{
if(user_pool->status[i] == ON && i != msg.targetfd)
{
if(writen(i, sendbuf, len) < 0)
{
err_msg("writen error");
}
}
}
}
else if(msg.msgType == PARTNER_JOIN2)
{
for(int i = 0; i <= maxfd; i++)
{
if(user_pool->status[i] == ON && i == msg.targetfd)
{
if(writen(i, sendbuf, len) < 0)
{
err_msg("writen error");
}
}
}
}
Pthread_mutex_unlock(&user_pool->lock);
//free
if(msg.ptr)
{
free(msg.ptr);
msg.ptr = NULL;
}
}
free(sendbuf);
return NULL;
}
send_func
线程函数用于从发送队列中获取消息并将其发送给对应的文件描述符。根据消息类型的不同,它会按照一定的规则将消息发送给目标文件描述符或用户池中的其他文件描述符。这样可以确保消息正确地传递给会议参与者或其他相关用户。以下是对代码的详细解析:
- 解析参数:
- 调用
Pthread_detach(pthread_self())
将线程设置为分离状态。 - 分配一个大小为 4MB 的发送缓冲区
sendbuf
。
- 调用
- 进入无限循环:
- 清空发送缓冲区
sendbuf
。 - 从发送队列
sendqueue
中取出一条消息msg
。 - 根据消息类型进行处理:
- 对于创建会议的响应和广播用户列表的消息类型,将
msgType
和msg.len
直接添加到发送缓冲区sendbuf
中。 - 对于其他消息类型,根据情况将 IP 地址和消息长度添加到发送缓冲区
sendbuf
中。
- 对于创建会议的响应和广播用户列表的消息类型,将
- 清空发送缓冲区
- 在加锁的情况下,根据不同的消息类型发送消息给对应的文件描述符:
- 如果消息类型是创建会议的响应,则将发送缓冲区
sendbuf
的内容发送给目标文件描述符msg.targetfd
。 - 如果消息类型是退出会议、接收文本、接收图片、接收音频或关闭摄像头等类型,则将发送缓冲区
sendbuf
的内容发送给用户池中除目标文件描述符外的其他文件描述符。 - 如果消息类型是加入房间,则将发送缓冲区
sendbuf
的内容发送给用户池中的所有文件描述符。 - 如果消息类型是广播用户列表,则将发送缓冲区
sendbuf
的内容发送给目标文件描述符。
- 如果消息类型是创建会议的响应,则将发送缓冲区
- 在解锁的情况下,释放
msg.ptr
的内存空间。 - 释放发送缓冲区
sendbuf
的内存空间。
进程中的主循环
代码能够实现在一个无限循环中监听并处理客户端发送的数据。根据不同的消息类型,它将消息加入发送队列以便后续处理,并对一些错误情况进行了相应的处理。具体的逻辑如下:
- 首先将存储客户端文件描述符的文件描述符集合
user_pool->fdset
复制给变量rset
,以备后续select
函数使用。 - 设置一个超时时间
time
为0,并使用select
函数监听文件描述符上是否有可读事件发生,如果没有事件发生,则select
函数会阻塞等待。 - 如果
select
函数返回一个大于0的值,表示至少有一个文件描述符上有可读事件发生,进入下一步操作。 - 遍历从0到
maxfd
的所有文件描述符,检查每个文件描述符是否有可读事件发生,即使用FD_ISSET
宏判断。 - 如果某个文件描述符有可读事件发生,即
FD_ISSET(i, &rset)
为真,则执行以下操作:- 创建一个长度为15字节的缓冲区
head
用于存储消息头部; - 使用
Readn
函数从文件描述符i
中读取11字节的数据到head
缓冲区; - 检查读取的字节数
ret
:- 如果
ret <= 0
,表示对端关闭了连接,输出"peer close"并调用fdclose
函数关闭该文件描述符并进行相关清理操作; - 如果
ret == 11
,表示成功读取了11字节的头部数据,继续处理; - 如果
ret
既不是0也不是11,表示读取的消息格式错误,输出"2 msg format error"。
- 如果
- 创建一个长度为15字节的缓冲区
- 对于成功读取了11字节头部数据的情况,接下来进行消息的解析和处理:
- 检查消息头部是否以"$“开头,如果不是则输出"1 msg format error”;
- 从头部中解析出消息类型
msgtype
、目标文件描述符msg.targetfd
、IP地址msg.ip
和消息长度msg.len
; - 如果消息类型是图片发送(
IMG_SEND
)、音频发送(AUDIO_SEND
)或文本发送(TEXT_SEND
),则执行以下操作:- 设置
msg.msgType
为图片接收(IMG_RECV
)、音频接收(AUDIO_RECV
)或文本接收(TEXT_RECV
); - 分配一个长度为
msg.len
的缓冲区msg.ptr
,用于存储消息内容; - 从文件描述符
i
中读取msg.len
字节的消息内容到msg.ptr
; - 检查读取的字节数
ret
是否与msg.len
相等,并且读取剩余的1字节作为尾部检查,如果不满足条件则输出相应的错误信息; - 将
msg
加入发送队列sendqueue
中等待处理。
- 设置
- 如果消息类型是关闭摄像头(
CLOSE_CAMERA
),则执行以下操作:- 检查消息长度
msg.len
是否为0; - 检查读取的剩余1字节是否为"#“,如果是则将
msg
加入发送队列sendqueue
中等待处理,否则输出"camera data error”。
- 检查消息长度
- 当处理完当前所有可读事件后,进入下一轮循环继续监听剩余的文件描述符。
fdclose
用于处理文件描述符关闭的操作
void fdclose(int fd, int pipefd) {
if (user_pool->owner == fd) { // 如果关闭的是房间(会议室)的所有者
user_pool->clear_room(); // 清空会议室中的用户信息
printf("clear room\n");
// 向父进程发送消息
char cmd = 'E';
if (writen(pipefd, &cmd, 1) < 1) {
err_msg("writen error");
}
} else { // 关闭的是普通客户端的连接
uint32_t getpeerip(int);
uint32_t ip;
// 从连接池中删除该文件描述符
Pthread_mutex_lock(&user_pool->lock);
ip = user_pool->fdToIp[fd];
FD_CLR(fd, &user_pool->fdset);
user_pool->num--;
user_pool->status[fd] = CLOSE;
if (fd == maxfd) maxfd--;
Pthread_mutex_unlock(&user_pool->lock);
// 向父进程发送消息
char cmd = 'Q';
if (writen(pipefd, &cmd, 1) < 1) {
err_msg("write error");
}
// 构造用于发送的消息结构体
MSG msg;
memset(&msg, 0, sizeof(MSG));
msg.msgType = PARTNER_EXIT;
msg.targetfd = -1;
msg.ip = ip; // 网络字节序的IP地址
Close(fd); // 关闭文件描述符对应的连接
sendqueue.push_msg(msg); // 将消息加入发送队列,准备发送给其他客户端
}
}
这段代码主要用于处理文件描述符关闭时相关的操作,包括清理会议室信息、通知父进程和发送消息给其他客户端。
根据代码的逻辑,可以分为以下两个部分:
- 如果关闭的是房间(会议室)的所有者:
- 调用
user_pool->clear_room()
清空会议室中的用户信息。 - 向父进程发送一个命令
'E'
,通知父进程关闭会议室。
- 调用
- 如果关闭的是普通客户端的连接:
- 从连接池中删除该文件描述符。
- 向父进程发送一个命令
'Q'
,通知父进程有一个客户端退出。 - 构造一个
MSG
消息结构体,设置消息类型为PARTNER_EXIT
,targetfd
为-1
,ip
为客户端的 IP 地址(网络字节序)。 - 关闭文件描述符对应的连接。
- 将构造好的消息加入发送队列,准备发送给其他客户端。
线程池的创建
创建线程池来处理客户端连接的输入/输出操作,当有客户端连接到服务器时,线程池中的空闲线程被唤醒来处理网络数据,数据的收发和数据的处理进行了分离,接收数据的线程并不会处理数据,而是转发给其他线程或者进程进行处理。这样能快速进入下一个数据的接受过程。避免因为长时间得不到处理而失去客户端响应。其中采用select IO多路复用函数进行监听是否有可读事件发生。
int Accept(int listenfd, SA * addr, socklen_t *addrlen) // 接收客户端的请求
{
int n;
for(;;)
{
// accept函数用于在服务器端接受客户端的连接请求,创建一个新的套接字,
// 并返回该套接字的文件描述符,以便服务器与客户端进行通信。通过传递给accept函数的参数,可以获取客户端的地址信息。
if((n = accept(listenfd, addr, addrlen)) < 0)
{
if(errno == EINTR)
continue;
else
err_quit("accept error");
}
else
{
return n; // 返回的是一个文件描述符
}
}
}
void Pthread_create(pthread_t * tid, const pthread_attr_t * attr,
THREAD_FUNC * func, void *arg)
{
int n;
// 数返回0表示成功创建了一个新线程,否则返回一个非零值表示创建线程失败
if( (n = pthread_create(tid, attr, func, arg)) != 0)
{
errno = n;
err_quit("pthread create error");
}
}
void* thread_main(void *arg)
{
void dowithuser(int connfd);
int i = *(int *)arg;
free(arg); //free //释放arg的内存
int connfd;
Pthread_detach(pthread_self()); // 子线程设置为分离状态 这样子线程执行完成后会自动释放资源
printf("thread %d starting\n", i); //多少多少线程开始创建ing
SA *cliaddr;
socklen_t clilen;
// 分配大小为addrlen的内存空间,将返回的指针赋给cliaddr,即分配存储客户端地址的内存空间
cliaddr = (SA *)Calloc(1, addrlen);
char buf[MAXSOCKADDR]; // 存储地址信息的字符串形式
for(;;) // 无限循环 用于接收客户端连接并处理请求
{
clilen = addrlen; // 设置clilen的值为addrlen,即cliaddr结构体的长度
//lock accept
Pthread_mutex_lock(&mlock); // 使用Pthread_mutex_lock函数锁定互斥量mlock,保证在接收连接时的线程安全性。
// 调用Accept函数接受客户端的连接请求,将连接的文件描述符存储在变量connfd中,
// 同时将客户端的地址信息存储在cliaddr中,并将clilen设置为实际接收到的客户端地址信息的长度。
connfd = Accept(listenfd, cliaddr, &clilen);
//unlock accept // 解锁
Pthread_mutex_unlock(&mlock);
// 将客户端的地址信息转换为字符串格式,并将其存在buf中
printf("connection from %s\n", Sock_ntop(buf, MAXSOCKADDR, cliaddr, clilen));
// dowithuser函数处理与客户端的交互和请求
dowithuser(connfd); // process user
}
return NULL;
}
void thread_make(int i)
{
void * thread_main(void *);
int *arg = (int *) Calloc(1, sizeof(int));
*arg = i;
Pthread_create(&tptr[i].thread_tid, NULL, thread_main, arg);
}
Pthread_create和thread_main函数
Pthread_create用于创建多个线程,并将thread_main
函数作为新线程的入口函数,从而实现多线程并发处理客户端的连接请求和交互。每个线程都执行相同的逻辑,即接收客户端连接,处理请求,然后循环等待下一个连接。Pthread_create
函数的调用成功返回0表示成功创建了一个新线程。如果返回非零值,则表示创建线程失败,会通过错误处理函数输出相应的错误信息。
thread_main
函数的作用是作为线程的入口函数,用于处理客户端的连接请求和交互。具体逻辑如下:
- 解析传入的参数
arg
,获得当前线程的编号。 - 释放
arg
所指向的内存空间。 - 将当前线程设置为分离状态,以便线程执行完成后自动释放资源。
- 输出当前线程的开始创建消息。
- 分配内存空间,用于存储客户端地址信息。
- 进入一个无限循环,用于接收客户端连接并处理请求。
- 设置客户端地址信息结构体的长度为传入的
addrlen
的值。 - 使用互斥量
mlock
进行加锁操作,保证在接收连接时的线程安全性。 - 调用
Accept
函数接收客户端的连接请求,将连接的文件描述符存储在变量connfd
中,同时将客户端的地址信息存储在cliaddr
中,并将clilen
设置为实际接收到的客户端地址信息的长度。 - 使用互斥量
mlock
进行解锁操作。 - 将客户端的地址信息转换为字符串格式,并打印输出。
- 调用
dowithuser(connfd)
函数,处理与客户端的交互和请求。 - 返回
NULL
。
Pthread_create
函数的作用是创建一个新线程,并将指定的函数作为新线程的入口函数。具体作用如下:
- 将传入的参数
tid
所指向的变量设置为新线程的ID。 - 将传入的参数
attr
所指向的线程属性设置为默认值(即NULL
)。 - 调用
func
所指向的函数作为新线程的入口函数。 - 将传入的
arg
作为参数传递给新线程的入口函数。
void dowithuser(int connfd) // 处理客户端的数据请求
{
void writetofd(int fd, MSG msg); //往文件描述符fd中写消息
char head[15] = {0};
//read head
while(1) //使用无限循环读取客户端发送的数据头信息(11字节) 直到满足连接断开啥的退出
{ // 如果数据格式正确,则解析出消息类型、IP地址和数据大小。
ssize_t ret = Readn(connfd, head, 11);
if(ret <= 0)
{
close(connfd); //close
printf("%d close\n", connfd);
return;
}
else if(ret < 11)
{
printf("data len too short\n");
}
else if(head[0] != '$')
{
printf("data format error\n");
}
else // 这里说明正确 开始解析消息
{
//solve datatype 解析消息类型
MSG_TYPE msgtype;
// 数据头中的第2和第3个字节head + 1复制到变量msgtype中,并通过ntohs函数将网络字节序转换为主机字节序
memcpy(&msgtype, head + 1, 2);
msgtype = (MSG_TYPE)ntohs(msgtype);
//solve ip 解析IP地址
uint32_t ip;
// 数据头中的第4到第7个字节head + 3复制到变量ip中,并通过ntohl函数将网络字节序转换为主机字节序。
memcpy(&ip, head + 3, 4);
ip = ntohl(ip);
//solve datasize 解析数据大小
uint32_t datasize;
// 数据头中的第8到第11个字节head + 7复制到变量datasize中
memcpy(&datasize, head + 7, 4);
datasize = ntohl(datasize);
// printf("msg type %d\n", msgtype);
// 消息类型是 CREATE_MEETING,则继续读取一个字节的数据尾部信息,并判断数据是否为空且尾
// 部信息是否正确。如果满足条件,则表示客户端要创建会议,根据会议室的可用情况来决定是否创建新
// 的会议室。如果没有可用的会议室,则向客户端发送创建会议失败的响应消息;如果有可用的会议室,
// 则向客户端发送创建会议成功的响应消息,并将客户端连接关闭。
if(msgtype == CREATE_MEETING)
{
char tail;
Readn(connfd, &tail, 1); // 读取一个字节的数据尾部信息
//read data from client
if(datasize == 0 && tail == '#') // 数据大小为0且尾部信息为#,表示客户端要创建会议
{
char *c = (char *)&ip;
printf("create meeting ip: %d.%d.%d.%d\n", (u_char)c[3], (u_char)c[2], (u_char)c[1], (u_char)c[0]);
if(room->navail <=0) //no room 会议室没有可用空位,向客户端发送创建会议失败的响应消息。
{ // room中的navail代表的就是进程数量
MSG msg;
memset(&msg, 0, sizeof(msg));
// 创建失败的响应消息的结构体成员msg.msgType被设置为CREATE_MEETING_RESPONSE,
msg.msgType = CREATE_MEETING_RESPONSE;
// msg.ptr指向一个int类型的内存空间,值为0,表示创建失败,
int roomNo = 0;
msg.ptr = (char *) malloc(sizeof(int));
memcpy(msg.ptr, &roomNo, sizeof(int));
msg.len = sizeof(roomNo);
writetofd(connfd, msg); // 通过调用writetofd函数向客户端发送该响应消息
}
else // 会议室有可用空位 向客户端发送创建会议成功的响应消息
{
int i;
//find room empty
Pthread_mutex_lock(&room->lock);
for(i = 0; i < nprocesses; i++) // 进程
{
if(room->pptr[i].child_status == 0) break;
}
if(i == nprocesses) //no room empty 当前i已经=进程数量说明房间已经创够了
{
MSG msg;
memset(&msg, 0, sizeof(msg));
// 创建成功的响应消息的结构体成员msg.msgType被设置为CREATE_MEETING_RESPONSE
msg.msgType = CREATE_MEETING_RESPONSE;
int roomNo = 0;
// msg.ptr指向一个int类型的内存空间,值为会议室号码
msg.ptr = (char *) malloc(sizeof(int));
memcpy(msg.ptr, &roomNo, sizeof(int));
msg.len = sizeof(roomNo);
// 通过调用writetofd函数向客户端发送该响应消息,并将客户端连接关闭
writetofd(connfd, msg);
}
else // 在找到一个可用的会议室空位后,将该空位占据并更新相关状态信息。
{
char cmd = 'C';
if(write_fd(room->pptr[i].child_pipefd, &cmd, 1, connfd) < 0)
{
printf("write fd error");
}
else
{
// 使用close(connfd)关闭与客户端的连接,因为会议室需要使用该套接字与客户端建立通信
close(connfd);
// 通过printf函数打印出当前空位对应的房间编号,即room->pptr[i].child_pid。
printf("room %d empty\n", room->pptr[i].child_pid);
// 将room->pptr[i].child_status设置为1,表示该空位被占用。该字段用于记录会议室空位的状态,0表示空闲,1表示占用。
room->pptr[i].child_status = 1; // occupy
// 更新会议室可用空位数量和被占用空位数量。room->navail--表示可用空位数量减一,即当前会议室还剩余的可用空位数。
// room->pptr[i].total++表示该空位被占用后的总占用数加一,即表示该会议室被占用的总人数。
room->navail--;
room->pptr[i].total++;
// 最后,使用Pthread_mutex_unlock(&room->lock)释放对会议室锁的持有,以便其他线程可以继续访问和操作会议室。
Pthread_mutex_unlock(&room->lock);
return;
}
}
Pthread_mutex_unlock(&room->lock);
}
}
else
{
printf("1 data format error\n");
}
}
// 如果消息类型是JOIN_MEETING,则继续读取数据中的房间号信息,并判断数据格式是否正确。
// 格式正确则根据房间号查找对应的会议室,如果找到且会议室有空位,则向客户端发送加入会议
// 成功的响应消息,并将客户端连接关闭;如果找不到对应的会议室或者会议室已满,则向客户端发送加入
// 会议失败的响应消息。这里有个问题 会议室的人数限制在哪设置???
else if(msgtype == JOIN_MEETING)
{
//read msgsize
uint32_t msgsize, roomno;
memcpy(&msgsize, head + 7, 4);
msgsize = ntohl(msgsize);
//read data + #
int r = Readn(connfd, head, msgsize + 1 ); //从connfd文件描述符对应的连接中读取消息到head缓冲区中
if(r < msgsize + 1)
{
printf("data too short\n");
}
else // 读取的数据足够
{
if(head[msgsize] == '#') // 且结尾为 #
{
memcpy(&roomno, head, msgsize); //拷贝数据从head到roomno中
roomno = ntohl(roomno);
// printf("room : %d\n", roomno);
//find room no
bool ok = false;
int i;
for(i = 0; i < nprocesses; i++)
{
// 根据进程数量遍历一个room->pptr的数组,查找与roomno匹配的会议室,且会议室有空 即child_status=1
if(room->pptr[i].child_pid == roomno && room->pptr[i].child_status == 1)
{
ok = true; //find room
break;
}
}
MSG msg; // 创建一个MSG类型的消息结构体,并进行初始化。根据是否找到匹配的会议室进行不同的处理。
memset(&msg, 0, sizeof(msg));
// 找到对应的会议室
msg.msgType = JOIN_MEETING_RESPONSE;
msg.len = sizeof(uint32_t);
if(ok)
{
// 会议室满员
if(room->pptr[i].total >= 1024)
{
// 成功加入会议的响应消息中,msg.ptr指向的内存空间存储的是一个表示成功的标识
msg.ptr = (char *)malloc(msg.len);
uint32_t full = -1; // 分配一个值为-1的uint32_t类型的内存空间
memcpy(msg.ptr, &full, sizeof(uint32_t)); // 表示加入失败
writetofd(connfd, msg); // 向客户端发送加入会议成功的响应消息
}
else // 会议室有空位
{
Pthread_mutex_lock(&room->lock); // 确保只有一个线程可以修改会议室的状态
char cmd = 'J';
// printf("i = %d\n", i);
// 将J写入指向会议室的管道room->pptr[i].child_pipefd中,表示要加入该会议室
if(write_fd(room->pptr[i].child_pipefd, &cmd, 1, connfd) < 0)
{
err_msg("write fd:");
}
else // 写入成功
{
msg.ptr = (char *)malloc(msg.len);
memcpy(msg.ptr, &roomno, sizeof(uint32_t));
writetofd(connfd, msg);
room->pptr[i].total++;// add 1
Pthread_mutex_unlock(&room->lock);
close(connfd);
return;
}
Pthread_mutex_unlock(&room->lock);
}
}
else // 找不到对应的会议室或者会议室已满,则向客户端发送加入会议失败的响应消息。
{
msg.ptr = (char *)malloc(msg.len);
// 失败的响应消息的结构体成员msg.msgType被设置为JOIN_MEETING_RESPONSE,
// msg.ptr指向一个uint32_t类型的内存空间,值为0,表示加入失败
uint32_t fail = 0;
memcpy(msg.ptr, &fail, sizeof(uint32_t));
writetofd(connfd, msg);
}
}
else
{
printf("format error\n");
}
}
}
else
{
printf("data format error\n");
}
}
}
}
dowithuser
函数的主要作用是根据客户端发送的数据请求,解析消息类型、IP地址和数据大小,并根据不同的请求类型进行相应的处理。处理过程中对会议室进行操作,并发送响应消息给客户端。完成一次请求处理后,继续等待下一个请求。当客户端发送数据请求时,dowithuser
函数会通过无限循环读取客户端发送的数据头信息(11字节)来解析消息类型、IP地址和数据大小。具体的处理逻辑如下:
- 无限循环读取客户端发送的数据头信息,直到连接断开或满足其他条件退出循环。
- 解析数据头信息,判断数据格式是否正确。如果格式不正确,则打印相应的错误信息,并继续下一次循环。
- 如果数据格式正确,则解析出消息类型、IP地址和数据大小。
- 根据解析得到的消息类型进行不同的处理:
- 如果消息类型是CREATE_MEETING,表示客户端要创建会议。此时会根据数据大小和尾部信息来判断请求是否合法。
- 如果会议室没有可用空位,则向客户端发送创建会议失败的响应消息,并继续下一次循环。
- 如果会议室有可用空位,则向客户端发送创建会议成功的响应消息,并将客户端连接关闭。
- 如果消息类型是JOIN_MEETING,表示客户端要加入会议。此时会根据数据大小和结尾信息来判断请求是否合法。
- 如果找到匹配的会议室且会议室有空位,则向客户端发送加入会议成功的响应消息,并将客户端连接关闭。
- 如果未找到匹配的会议室或会议室已满,则向客户端发送加入会议失败的响应消息,并继续下一次循环。
- 如果消息类型是CREATE_MEETING,表示客户端要创建会议。此时会根据数据大小和尾部信息来判断请求是否合法。
- 在处理过程中,涉及到对会议室的操作,包括查找可用会议室、占用会议室空位、更新会议室状态等。
- 完成一次数据请求处理后,继续下一次循环,等待下一个数据请求的到来。
dowithuser中for循环具体逻辑
在上述代码的 while(1)
循环中,主要处理客户端发送的数据请求。下面对循环体内的代码逻辑进行详细解释:
ssize_t ret = Readn(connfd, head, 11)
:
这行代码从客户端连接connfd
中读取 11 字节的数据到head
缓冲区,用于接收客户端发送的数据头信息。- 对读取到的数据头信息进行判断:
- 如果
ret <= 0
,说明读取数据发生错误或客户端连接断开,关闭connfd
并返回函数。 - 如果
ret < 11
,说明读取到的数据不完整,可能存在格式错误或其他问题,打印相应的错误提示信息。 - 如果
head[0] != '$'
,说明数据头格式错误,不满足预期的数据格式,打印相关错误提示信息。
- 如果
- 解析数据头信息:
- 通过
memcpy
将数据头中的消息类型、IP 地址和数据大小解析出来,并转换为主机字节序。 - 消息类型存储在变量
msgtype
中,IP 地址存储在变量ip
中,数据大小存储在变量datasize
中。
- 通过
- 处理不同的消息类型:
- 如果消息类型为
CREATE_MEETING
:- 读取一个字节的数据尾部信息。
- 如果数据大小为 0 且尾部信息为
#
,表示客户端要创建会议。- 如果会议室没有剩余可用空位(
room->navail <= 0
),向客户端发送创建会议失败的响应消息。 - 如果会议室有剩余可用空位,找到一个空闲的会议室空位,并将其占用。发送创建会议成功的响应消息给客户端,关闭连接。
- 如果会议室没有剩余可用空位(
- 如果消息类型为
JOIN_MEETING
:- 读取消息大小并根据大小读取数据及结尾标识符
#
。 - 根据读取到的房间号 (
roomno
) 在会议室列表中查找对应的会议室。- 如果找到匹配的会议室且会议室有剩余空位,向对应的会议室发送加入会议请求。
- 如果会议室已满员,则向客户端发送加入会议失败的响应消息。
- 如果成功加入会议室,向客户端发送加入会议成功的响应消息,关闭连接。
- 如果没有找到匹配的会议室或会议室已满员,向客户端发送加入会议失败的响应消息。
如果在循环体内的某一步发生错误或者条件不满足,则会打印相应的错误信息,并根据具体情况执行相应的处理操作。通过以上的逻辑,可以根据不同的消息类型进行业务处理并与客户端进行准确的通信。
- 如果找到匹配的会议室且会议室有剩余空位,向对应的会议室发送加入会议请求。
- 读取消息大小并根据大小读取数据及结尾标识符
- 如果消息类型为
main.py中的 for循环再现
int Select(int nfds, fd_set * readfds, fd_set * writefds, fd_set * execpfds, struct timeval *timeout)
{
int n;
for(;;)
{
if((n = select(nfds, readfds, writefds, execpfds, timeout)) < 0)
{
if(errno == EINTR) continue;
else err_quit("select error");
}
else break;
}
return n; //can return 0 on timeout 超时时可以返回0
}
ssize_t Readn(int fd, void * buf, size_t size) // 从套接字中读取指定长度的数据,确保读取到指定长度的 数据
{
ssize_t lefttoread = size, hasread = 0; // 已读取的字节数,初始为0
char *ptr = (char *)buf; // 缓冲区指针,指向要读取数据的位置
while(lefttoread > 0)
{
if((hasread = read(fd, ptr, lefttoread))<0) // 读取数据,并将返回值赋给hasread
{
if(errno == EINTR) // 若读取被信号中断,则继续读取
{
hasread = 0;
}
else
{
return -1;
}
}
else if(hasread == 0) //eof 若返回值为0表示已到达文件末尾(EOF),则退出循环
{
break;
}
lefttoread -= hasread; // 更新剩余待读取的字节数
ptr += hasread; // 更新缓冲区指针,指向下一次要读取数据的位置
}
return size - lefttoread; // 返回实际读取到的字节数
}
for(;;) //死循环 直到退出
{
//listen
rset = masterset;
// rset需要检测的可读文件描述符的集合 通过select监听子进程的管道和客户端连接的输入
// 循环中通过select函数监听文件描述符集合rset上是否有可读事件发生。
int nsel = Select(maxfd + 1, &rset, NULL, NULL, NULL); // 参数maxfd是需要监视的最大的文件描述符值+1
if(nsel == 0) continue; //返回0,表示没有可读事件发生 继续下次循环
//set room status to 0(empty)
for(i = 0; i < nprocesses; i++)
{
// 遍历nprocesses个子进程,检查每个子进程的管道child_pipefd是否在rset中。
// 如果在其中,说明该子进程向管道中写入了数据。
if(FD_ISSET(room->pptr[i].child_pipefd, &rset)) // FD_ISSET测试一个文件描述符是否在集合中
{
char rc;
int n;
// Readn是一个函数,它用于从文件描述符中读取指定数量的数据。
// Readn函数被用来从 房间子进程的管道中 读取一个字节的数据
if((n = Readn(room->pptr[i].child_pipefd, &rc, 1)) <= 0)
{
err_quit("child %d terminated unexpectedly", i);
}
printf("c = %c\n", rc);
// rc是一个字符变量,用于保存从房间子进程的管道中读取的数据。它被用于判断读取的数据的类型
if(rc == 'E') // room empty 这个E哪来的??
{
pthread_mutex_lock(&room->lock); //互斥锁 在线程实际运行过程中,需要多个线程保持同步。
//这时可以用互斥锁来完成任务、
// 通过加锁保护共享资源room->pptr[i].child_status,将该子进程的状态设为0(空闲)
room->pptr[i].child_status = 0;
// 增加可用房间数 room->navail 然后释放锁。
room->navail++;
printf("room %d is now free\n", room->pptr[i].child_pid); //客户端连接断开 就说明房子空了?
pthread_mutex_unlock(&room->lock); // 释放互斥锁??
}
// 到的数据是字符 'Q',表示房间中的伙伴退出。
else if(rc == 'Q') // partner quit
{
// 使用自定义的线程互斥锁room->lock保护共享资源room->pptr[i].total的修改
Pthread_mutex_lock(&room->lock); //为何这里用自己定义的线程互斥锁??
// 将该子进程的伙伴总数减1。然后释放锁。
room->pptr[i].total--;
Pthread_mutex_unlock(&room->lock); //这里释放锁
}
// 表示读取到了 无效的数据,输出错误信息并继续下一次循环
else // trash data
{
err_msg("read from %d error", room->pptr[i].child_pipefd);
continue;
}
if(--nsel == 0) break; /*all done with select results*/
}
}
}
这段代码是一个死循环,主要用于监听文件描述符集合中的可读事件,并做出相应处理。
首先,通过调用Select()
函数来监听文件描述符集合rset
上是否有可读事件发生。Select()
函数是一个封装了select()
系统调用的函数,它的作用是阻塞等待,直到有文件描述符准备就绪(可读、可写或发生错误)或超时。参数nfds
表示最大的文件描述符值加1,readfds
、writefds
和execpfds
分别表示需要监视的可读、可写和异常事件的文件描述符集合,timeout
表示超时时间。
如果Select()
函数返回值小于0,表示出错。当出错的原因是由于被信号中断(errno == EINTR
)时,继续下一次循环;否则,调用err_quit()
函数输出错误信息并退出。
如果Select()
函数返回值大于等于0,说明文件描述符上有可读事件发生。接下来进入一个循环,遍历所有子进程的管道,检查每个子进程的管道是否在rset
中,即判断该子进程是否向管道中写入了数据。
当某个子进程的管道在rset
中,表示该子进程向管道中写入了数据。此时调用Readn()
函数从管道中读取一个字节的数据。
如果读取出错,即返回值小于等于0,则调用err_quit()
函数输出错误信息并退出。
如果读取到的数据是字符E
,表示房间为空,即客户端连接断开,该子进程空闲。通过加锁保护共享资源room->pptr[i].child_status
,将该子进程的状态设为0(空闲),增加可用房间数room->navail
,然后释放锁。
如果读取到的数据是字符Q
,表示房间中的伙伴退出。使用自定义的线程互斥锁room->lock
保护共享资源room->pptr[i].total
的修改,将该子进程的伙伴总数减1,然后释放锁。
如果读取到的数据是其他无效的数据,即不是字符E
也不是字符Q
,则输出错误信息并继续下一次循环。最后,如果nsel
减少为0,表示所有的可读事件已处理完毕,可以退出循环。否则,继续下一次循环,继续监听文件描述符集合中的可读事件。
项目技术栈
多进程编程
这里的每个房间采用独立进程来处理,一方面保证了各个房间数据的私密性,一方面也加强了各个房间的稳定性,由于进程天然具有内存隔离的特性,所以各个房间的数据不会意外串访,另外进程的独立性也使得某个房间崩溃的时候,不会让其他房间立刻也一起崩溃。如果采用多线程来做,一个线程的崩溃肯定导致整个进程的崩溃,进而使得其他房间也失去服务。
多线程编程
为提高服务器对网络数据处理的效率,采用了多线程的模式来处理网络数据,另外将数据的收发和数据的处理也进行了分离。接受数据的线程并不会处理数据,而是转发给其他线程或者进程进行处理。这样能快速进入下一个数据的接收过程。避免因为长时间跌不到处理而导致客户端失去响应。
网络编程
为啥用select而不是用epoll
- epoll模型更复杂,开发和维护成本更高
- 涉及视频传输的服务器,瓶颈往往是带宽而不是其他硬件资源,也就是说并发量不会太大,一般200左右,一个用户的带宽500k,200个用户就有100 M的带宽。这个带宽成本已经很高了
- 视频会议中人数并不会太夸张,往往几十到上百,上千人的会议需要用直播。超过一百人的会议,往往不可能用户同时进行网络视频,这样带宽无法承受,而往往这样的会议都会有一个主讲人,直播更方便,我们做的是小巧的视频会议系统,而不是大型直播系统,所以我认为select更适合我的项目。
视频会议服务器
简单的视频会议服务器核心功能其实是提供房间服务。
- 如房间的创建和销毁
- 用户加入和离开房间
- 用户在房间中获得的消息转发服务等等
视频数据在服务器也只是一个普通的数据包,只是尺寸可能大一点而已.这里服务器采用了Linux系统,这种系统内存占用较小的操作系统可以腾出更多资源来开启房间进程,提供更高的服务上限,最大可能的提高硬件资源的利用率。
项目服务端就到这 基本流程结束,完结撒花 !!!