服务器源码解析
主函数流程
绑定tcp 监听描述符
listenfd = Tcp_listen(argv1, argv2, &addrlen);
开辟资源
cpp
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 *)Calloc(nthreads, sizeof(Thread));
开辟子进程
cpp
//根据进程数量开辟进程
for(i = 0; i < nprocesses; i++)
{
process_make(i, listenfd);
//将和子进程关联的描述符id放入主进程集合中监听
FD_SET(room->pptr[i].child_pipefd, &masterset);
maxfd = max(maxfd, room->pptr[i].child_pipefd);
}
接下来讲解process_make函数
cpp
int process_make(int i, int listenfd)
{
int sockfd[2];
pid_t pid;
void process_main(int, int);
//管道, sockfd[0]作为发送和接收端
Socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);
if((pid = fork()) > 0)
{
Close(sockfd[1]);
room->pptr[i].child_pid = pid;
//父进程和子进程通信,用sockfd[0],将来放入主进程的select模型的集合中
//子进程发送消息就会通知到父进程
room->pptr[i].child_pipefd = sockfd[0];
room->pptr[i].child_status = 0;
room->pptr[i].total = 0;
return pid; // father
}
Close(listenfd); // child not need this open
//子进程使用sockfd[1]和父进程通信
Close(sockfd[0]);
process_main(i, sockfd[1]); /* never returns */
}
综上所述,processmake就是开辟子进程
在主进程中,将子进程通信的sockfd0加入到主进程的select集合中
在子进程中,调用process_main函数
cpp
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 *);
void* send_func(void *);
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++)
{
//在子进程中,创建5个线程用来发送
Pthread_create(&pfd1, NULL, send_func, NULL);
}
//listen read data from fds
//子进程监听所有客户端发送的消息
for(;;)
{
fd_set rset = user_pool->fdset;
int nsel;
struct timeval time;
memset(&time, 0, sizeof(struct timeval));
while((nsel = Select(maxfd + 1, &rset, NULL, NULL, &time))== 0)
{
rset = user_pool->fdset; // make sure rset update
}
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
if(ret <= 0)
{
printf("peer close\n");
fdclose(i, fd);
}
else if(ret == 11)
{
if(head[0] == '$')
{
//solve datatype
MSG_TYPE msgtype;
memcpy(&msgtype, head + 1, 2);
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);
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);
}
}
}
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
{
err_msg("1 msg format error");
}
}
else
{
err_msg("2 msg format error");
}
if(--nsel <= 0) break;
}
}
}
}
上面我们谈到了,子线程调用accept_fd
cpp
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;
//相当于子进程的子线程从sockfd[1]读取父进程传输的消息
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
{
Pthread_mutex_lock(&user_pool->lock); //lock
//子进程将父进程传输过来的客户端fd,放入集合,监听读写
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
{
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,放入集合,监听读写
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;
}
总结
我们能可以理解为子进程开辟了子线程,
这个子线程就是接收父进程传输的描述fd,这个描述符fd,是父进程accept连接返回的新连接的描述符
子线程将这个描述符放入到自己的监听集合中
将来子进程通过select模型不断地去轮询监听客户端的读写请求。
子进程收发逻辑
cpp
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 *);
void* send_func(void *);
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++)
{
//在子进程中,创建5个线程用来发送
Pthread_create(&pfd1, NULL, send_func, NULL);
}
//listen read data from fds
//子进程监听所有客户端发送的消息
for(;;)
{
fd_set rset = user_pool->fdset;
int nsel;
struct timeval time;
memset(&time, 0, sizeof(struct timeval));
while((nsel = Select(maxfd + 1, &rset, NULL, NULL, &time))== 0)
{
rset = user_pool->fdset; // make sure rset update
}
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
if(ret <= 0)
{
printf("peer close\n");
fdclose(i, fd);
}
else if(ret == 11)
{
if(head[0] == '$')
{
//solve datatype
MSG_TYPE msgtype;
memcpy(&msgtype, head + 1, 2);
//解析消息类型,将消息id转为本地字节序
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);
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);
//服务器给客户端多回复了4个字节的消息,作为ip
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);
}
}
}
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
{
err_msg("1 msg format error");
}
}
else
{
err_msg("2 msg format error");
}
if(--nsel <= 0) break;
}
}
}
}
- 子进程只有一个线程处理接收,保证接受的顺序是一致的,并且通过TLV解析
- 解析完成后将消息回传给客户端,将消息投递给发送队列
- 子进程有五个线程从发送队列取出数据回传给客户端。
发送线程
发送队列
封装
cpp
void push_msg(MSG msg)
{
//对于队列为空还是满,分别使用两个条件变量才合适,否则一个条件变量容易逻辑混乱和卡死
Pthread_mutex_lock(&lock);
while(send_queue.size() >= MAXSIZE)
{
Pthread_cond_wait(&cond, &lock);
}
send_queue.push(msg);
Pthread_mutex_unlock(&lock);
Pthread_cond_signal(&cond);
}
MSG pop_msg()
{
Pthread_mutex_lock(&lock);
while(send_queue.empty())
{
Pthread_cond_wait(&cond, &lock);
}
MSG msg = send_queue.front();
send_queue.pop();
Pthread_mutex_unlock(&lock);
Pthread_cond_signal(&cond);
return msg;
}
- 新增两个条件变量,cond_full条件变量控制满挂起, cond_empty条件变量控制空挂起
发送逻辑
cpp
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;
}
创建线程
cpp
void Pthread_create(pthread_t * tid, const pthread_attr_t * attr,
THREAD_FUNC * func, void *arg)
{
int n;
if( (n = pthread_create(tid, attr, func, arg)) != 0)
{
errno = n;
err_quit("pthread create error");
}
}
- tid 线程id
- attr线程属性
- 线程回调函数
- 线程回调函数用到的参数
arg不可以是局部变量地址,否则线程触发回调函数时,可能会丢失数据。
分离线程
cpp
void Pthread_detach(pthread_t tid)
{
int n;
if((n = pthread_detach(tid)) == 0)
{
return;
}
else
{
errno = n;
err_quit("pthread detack error");
}
}
- detach 会将子线程放入后台运行
- 主进程退出时,detach的线程也会被回收
- 主进程退出时,要设置退出标记,通知子线程退出。
join 和detach
join就是汇合线程
主线程等待子线程结束时才回收资源。
加锁和解锁
cpp
void Pthread_mutex_lock(pthread_mutex_t *mptr)
{
int n;
if((n = pthread_mutex_lock(mptr)) == 0)
{
return;
}
else
{
errno = n;
err_quit("pthread_mutex_lock error");
}
}
void Pthread_mutex_unlock(pthread_mutex_t *mptr)
{
int n;
if((n = pthread_mutex_unlock(mptr)) == 0)
{
return;
}
else
{
errno = n;
err_quit("pthread_mutex_unlock error");
}
}
条件变量的等待和激活
等待
cpp
void Pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t *lock)
{
int n;
if((n = pthread_cond_wait(cond, lock)) == 0)
{
return;
}
else
{
errno = n;
err_quit("Pthread_cond_wait error");
}
}
激活一个
cpp
void Pthread_cond_signal(pthread_cond_t *cond)
{
int n;
if((n = pthread_cond_signal(cond)) == 0)
{
return;
}
else
{
errno = n;
err_quit("Pthread_cond_signal error");
}
}
激活所有
cpp
void Pthread_cond_broadcast(pthread_cond_t* cond){
int n ;
if((n = pthread_cond_broadcast(cond)) == 0){
return;
}else{
errno = n;
err_quit("Pthread_cond_signal error");
}
}
用户池
cpp
//用户池,所有用户都存储在这个里面
typedef struct pool
{
fd_set fdset;
pthread_mutex_t lock;
int owner;
int num;
int status[1024 + 10];
std::map<int, uint32_t> fdToIp;
pool()
{
memset(status, 0, sizeof(status));
owner = 0;
FD_ZERO(&fdset);
lock = PTHREAD_MUTEX_INITIALIZER;
num = 0;
}
void clear_room()
{
Pthread_mutex_lock(&lock);
roomstatus = CLOSE;
for(int i = 0; i <= maxfd; i++)
{
if(status[i] == ON)
{
Close(i);
}
}
memset(status, 0, sizeof(status));
num = 0;
owner = 0;
FD_ZERO(&fdset);
fdToIp.clear();
sendqueue.clear();
Pthread_mutex_unlock(&lock);
}
}Pool;
接收连接
cpp
void* thread_main(void *arg)
{
void dowithuser(int connfd);
int i = *(int *)arg;
free(arg); //free
int connfd;
Pthread_detach(pthread_self()); //detach child thread
// printf("thread %d starting\n", i);
SA *cliaddr;
socklen_t clilen;
cliaddr = (SA *)Calloc(1, addrlen);
char buf[MAXSOCKADDR];
for(;;)
{
clilen = addrlen;
//lock accept
Pthread_mutex_lock(&mlock);
connfd = Accept(listenfd, cliaddr, &clilen);
//unlock accept
Pthread_mutex_unlock(&mlock);
printf("connection from %s\n", Sock_ntop(buf, MAXSOCKADDR, cliaddr, clilen));
dowithuser(connfd); // process user
}
return NULL;
}
在主进程中接收连接,放入子进程中的fdset中
进程之间通信框架图


重构思路
将主进程由select监听子进程信息,改为主进程用epoll监听子进程信息
cpp
int main(int argc, char **argv)
{
Signal(SIGCHLD, sig_chld);
//listenfd将来用来监听对方连接
if (argc == 4)
{
listenfd = Tcp_listen(NULL, argv[1], &addrlen);
}
else if (argc == 5)
{
listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
}
else
{
err_quit("usage: ./app [host] <port #> <#threads> <#processes>");
}
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 *)Calloc(nthreads, sizeof(Thread));
//创建epoll
int epfd = epoll_create1(0);
if (epfd < 0)
err_quit("epoll_create1 error");
//process pool----room
for (int i = 0; i < nprocesses; i++)
{
pid_t pid = process_make(i, listenfd);
//父进程
if (pid > 0)
{
int pipefd = room->pptr[i].child_pipefd;
struct epoll_event ev;
ev.events = EPOLLIN;
//房间号
ev.data.u32 = i;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd, &ev) < 0)
err_quit("epoll_ctl add pipefd error");
}
}
//thread pool
for (int i = 0; i < nthreads; i++)
{
thread_make(i);
}
const int MAX_EVENTS = 64;
std::vector<struct epoll_event> events(MAX_EVENTS);
for (;;)
{
int nfds = epoll_wait(epfd, events.data(), MAX_EVENTS, -1);
if (nfds < 0)
{
if (errno == EINTR)
continue;
err_quit("epoll_wait error");
}
for (int i = 0; i < nfds; i++)
{
int idx = events[i].data.u32;
int pipefd = room->pptr[idx].child_pipefd;
char rc;
ssize_t n = Readn(pipefd, &rc, 1);
if (n <= 0)
{
err_quit("child %d terminated unexpectedly", idx);
}
printf("c = %c\n", rc);
if (rc == 'E') // room empty
{
pthread_mutex_lock(&room->lock);
room->pptr[i].child_status = 0;
room->navail++;
printf("room %d is now free\n", room->pptr[i].child_pid);
pthread_mutex_unlock(&room->lock);
}
else if (rc == 'Q') // partner quit
{
Pthread_mutex_lock(&room->lock);
room->pptr[i].total--;
Pthread_mutex_unlock(&room->lock);
}
else // trash data
{
err_msg("read from %d error", room->pptr[i].child_pipefd);
}
}
}
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];
pid_t pid;
void process_main(int, int);
Socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);
if ((pid = fork()) > 0)
{
Close(sockfd[1]);
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
}
Close(listenfd); // child not need this open
Close(sockfd[0]);
process_main(i, sockfd[1]); /* never returns */
}
- epoll使用LT模式
- 主进程和子进程仍然采用管道通信,子进程发送消息,主进程通过epoll监听管道信息
- 创建子进程时采用process_make,将管道信息注册到epoll表中。
- 主进程主要处理客户端返回的退出信息,从而清除房间。所以房间的管理是放在主进程中的。
子进程重构为epoll模型
cpp
typedef struct pool
{
pthread_mutex_t lock;
int owner;
int num;
std::map<int, int> fdToStatus;
std::map<int, uint32_t> fdToIp;
// 使用epoll
int epollfd;
pool()
{
fdToStatus.clear();
owner = 0;
lock = PTHREAD_MUTEX_INITIALIZER;
num = 0;
// 创建epoll实例
epollfd = epoll_create1(0);
if (epollfd < 0)
{
err_quit("epoll_create1 error");
}
}
void clear_room()
{
Pthread_mutex_lock(&lock);
roomstatus = CLOSE;
for (auto iter = fdToStatus.begin(); iter != fdToStatus.end();
iter++)
{
int sock_fd = iter->first;
epoll_ctl(epollfd, EPOLL_CTL_DEL, sock_fd, NULL);
Close(sock_fd);
}
fdToStatus.clear();
num = 0;
owner = 0;
fdToIp.clear();
sendqueue.clear();
Pthread_mutex_unlock(&lock);
}
void clear_status(int fd){
auto iter = fdToStatus.find(fd);
if(iter != fdToStatus.end()){
fdToStatus.erase(iter);
}
}
void add_status(int fd){
fdToStatus[fd] = STATUS::ON;
}
} Pool;
Pool *user_pool = new Pool();
- 子进程epoll采用LT模式,每一个房间(每一个进程)独立有一个epoll表
- 在子进程启动时,首先启动一个子线程,然后子线程死循环阻塞从管道中读取主进程发送的消息。读取消息后,就是新建的socket连接,将socket放入子进程的epoll表。
- 首先主进程还是获取客户端连接,将新生成socket通过管道发送给子进程,子进程收到后,将这个socket放入epoll表,以后这个socket发过来的数据,交给子进程自己处理。子进程epoll_wait就会返回读就绪事件列表。
当发现客户端socket关闭或者异常时,需要将其从epoll表中移除
待改进问题
希望大家重写send_func,将发送改为判断发送长度,如果未发完就继续发送。
-
- 死循环轮询发送
- 配合发送队列,将未发送完成的消息放入队列即可。