业精于勤,荒于嬉,再怎么忙也要坚持学习和积累,千万不能让自己深陷在公司的业务代码中而不能自拔。
要想具备多组服务的架构和开发能力,熟练服务端开发的相关知识是最基本的前提,从事服务器开发的同学肯定都听过One Loop One Thread思想;但是要把这种思想应用到实际的开发中,怎么去构建自己的服务器,让自己的服务能同时服务几千个甚至上万个客户端(单组服务做到极致),我相信很多同学都不具备这种能力(包括我自己)。
首先我再写一个服务端程序时,需要做哪些准备工作,采用哪种IO复用技术、创建多大的线程池、怎么去存储管理客户端的连接请求,如何将这些客户端请求(读、写数据请求)合理地分配给线程池中的线程等问题。基于此,我们逐一地进行分析。我在本地构建了一个客户端套接字的队列,将它作为共享的资源提供给线程池中的线程去访问和使用,代码如下(CommFunc.h和CommFunc.c):
1、CommFunc.h:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
typedef struct
{
int number; //number of socket
int *fd; //pointer Array which stores fd
int head; //queue head
int rear; //queue rear
pthread_mutex_t m_mutex;
pthread_cond_t m_cond;
}block_queue;
void Block_queue_init(block_queue *blockQueue,int number);
void Block_queue_push(block_queue *blockQueue,int fd);
int Block_queue_pop(block_queue *blockQueue);
2、CommFunc.c代码:
1 #include "CommFunc.h"
3 #define MAX_LEN 1024
4
5 void block_queue_init(block_queue *blockQueue,int number)
6 {
7 blockQueue->number = number;
8 blockQueue->fd = malloc(number*sizeof(int));
9 blockQueue->head = blockQueue->rear = 0;
10 pthread_mutex_init(&blockQueue->m_mutex,NULL);
11 pthread_cond_init(&blockQueue->m_cond,NULL);
12 }
15 void block_queue_push(block_queue *blockQueue,int fd)
16 {
17 //lock under MultiPthread condition
18 pthread_mutex_lock(&blockQueue->m_mutex);
19 blockQueue->fd[blockQueue->rear] = fd;
20 if(++blockQueue->rear == blockQueue->number)
21 {
22 blockQueue->rear = 0;
23 }
25 //wakeup work thread
26 pthread_cond_signal(&blockQueue->m_cond);
27 pthread_mutex_unlock(&blockQueue->m_mutex);
28 }
30 int block_queue_pop(block_queue *blockQueue)
31 {
32 pthread_mutex_lock(&blockQueue->m_mutex);
33 while(blockQueue->rear == blockQueue->head)
while(blockQueue->rear == blockQueue->head)
34 {
35 pthread_cond_wait(&blockQueue>m_cond,&blockQueue->m_mutex);
36 }
37 int fd = blockQueue->fd[blockQueue->head];
38 if(blockQueue->head++ == blockQueue->number)
39 {
40 blockQueue->head = 0;
41 }
42 printf("pop fd:%d",fd);
43 pthread_mutex_unlock(&blockQueue->m_mutex);
44 return fd;
45 }
当服务端为多个客户端提供服务时,自然会出现多个线程去访问公共队列获取其中的套接字,那么线程间的同步技术就不可避免地运被应用上来,因此在公共队列block_queue中定义了互斥锁和条件变量。
当向公共队列中添加套接字时,首先要给队列的互斥锁m_mutex上锁,此时只允许一个线程对该队列进行操作,添加完套接字,通过条件变量m_cond释放一个信号量,传递给阻塞在该条件变量上的线程,并释放互斥锁资源。此时我们再看看block_queue_pop函数(取出套接字),该函数也是一样,当一个线程获取到互斥锁资源并对m_mutex进行加锁,去队列中获取套接字。但是33~36行之间有个条件判断,为什么要加个while循环,有什么作用?因为当向队列中添加套接字后,肯定有多个线程想获取这个刚添加的套接字,但是套接字只能给一个线程;由于Linux中futex原则(”宁愿多发信号,也不漏发信号”),因此pthread_cond_signal唤醒的线程可能会有多个,但是能获取该套接字资源的线程只有一个,那么其他线程怎么办?此时加上这个条件判断,当公共队列中没有套接字资源时,唤醒的线程便会继续阻塞在条件变量m_cond上面。这样就有效地解决了***虚假唤醒***的问题。
介绍完公共队列相关代码,现在来介绍下服务端的代码编写:
3、Server.c代码如下:
#include "CommFunc.h"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define Serv_port 8888
#define BLOCKQueueSize 1024
#define Thread_Number 100
#define Thread_Stack_Size 200
#define MAX_LEN 1024
extern void block_queue_init(block_queue *blockQueue,int number);
extern void block_queue_push(block_queue *blockQueue,int fd);
extern int block_queue_pop(block_queue *blockQueue);
char ConvertChar(char ch)
{
if(ch >= 'a' && ch <= 'n')
{
ch += 13;
}
if((ch >= 'N' && ch <= 'Z') || (ch >= 'n' && ch <= 'z'))
{
ch -= 13;
}
return ch;
}
void loop_echo(int fd)
{
char outBuf[MAX_LEN];
size_t outBufUsed = 0;
size_t RecvNum;
char ch;
for(;;)
{
RecvNum = recv(fd,&ch,1,0);
if(RecvNum < 0)
{
printf("recv error");
break;
}else if(RecvNum == 0)
{
printf("Client Closed");
break;
}
if(outBufUsed++ < sizeof(outBuf))
{
outBuf[outBufUsed++] = ConvertChar(ch);
}
//Client Enter '\n' Represents Send closed
if(ch == '\n')
{
send(fd,outBuf,outBufUsed,0);
outBufUsed = 0;
continue;
}
}
}
void Thread_run(void *arg)
{
//Detach from main thread
pthread_detach(pthread_self());
block_queue *pQueue = (block_queue *)arg;
for(;;)
{
int fd = block_queue_pop(pQueue);
loop_echo(fd);
close(fd);
}
return ;
}
int main()
{
int Listen_fd,conn_fd;
struct sockaddr_in Client_addr,Serv_addr;
socklen_t clientlen;
Listen_fd = socket(AF_INET,SOCK_STREAM,0);
bzero(&Client_addr,sizeof(Client_addr));
bzero(&Serv_addr,sizeof(Serv_addr));
Serv_addr.sin_family = AF_INET;
Serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
Serv_addr.sin_port = htons(8888);
block_queue blockQueue;
block_queue_init(&blockQueue,(int)BLOCKQueueSize);
//create pthread pool
pthread_t *ThreadPoolArr = malloc(Thread_Number*Thread_Stack_Size);
for(int i = 0;i < Thread_Number;i++)
{
pthread_create(&ThreadPoolArr[i],NULL,&Thread_run,(void *)&blockQueue);
}
if(bind(Listen_fd,(struct sockaddr *)&Serv_addr,sizeof(Serv_addr)))
{
printf("bind error");
}
listen(Listen_fd,1024);
for(;;)
{
struct sockaddr_storage ss;
socklen_t socklen = sizeof(ss);
conn_fd = accept(Listen_fd,(struct sockaddr *)&ss,&socklen);
if(conn_fd < 0)
{
printf("accept error");
break;
}else
{
block_queue_push(&blockQueue,conn_fd);
}
}
return 0;
}
服务端这边主要是接受客户端的连接并对数据进行简单的加工,再发回给客户端,服务端的压力主要来自数据的读写操作,因为此时并没有涉及到大型的内存拷贝或者XML报文的解析等CPU耗时操作。服务端在接收客户端发来的数据前,先创建含有100个线程的线程池,把公共队列block_queue传递给每个线程,服务端这边不断地去接收客户端的连接请求并把客户端套接字conn_fd投递到公共队列中去,那么线程池中的线程也在不断地去公共队列中获取套接字资源并读取对应客户端发来的数据,这样就实现了一个线程服务一个客户端的思想(One Loop One Thread);同学们有没有注意到***pthread_detach(pthread_self())***,这个相当于每个子线程脱离主线程,当子线程完成服务时,自动回收线程的相关资源,无需主线程调用pthread_join函数去回收。这也是服务端开发中常用的操作。
同学们可以在Linux中多开几个终端,模拟客户端和该服务端进行通信,使用的命令是***nc -v 127.0.0.1 端口号***,大家可以自己去尝试。