参考来源:http://blog.csdn.net/column/details/yumeiz.html
一. 需求
原题:时间同步系统开发需求
需求场景:客户端向服务器发起登录请求,鉴权通过后(为了简化工作,所有登录请求,只要请求参数里的用户名和密码不为空,都鉴权通过),客户端再向服务器请求当前系统时间,服务器返回当前系统时间后关闭连接。
要求:
1、传输层使用TCP协议,应用层协议不限。
2、可支持同时在线用户量:大于 2W。
3、并发性能:没有明确指标。
4、服务器端运行环境:linux 2.4以上内核版本;开发语言:C++/C。
5、客户端运行环境:不限;开发语言:不限。(可以很简单,不要求界面)
6、要求有模拟性能测试操作方法,如多客户端、多线程模拟同时请求等。
7、用真实代码。
8、不要求日志系统,但是要考虑在主线程打印屏幕引起的性能问题。
我就觉得要是哪个公司面试的时候能出得出这样的题目,那就应该很专业了,对面试的人也就可以算得上挑战了。
tcp 服务端-客户端通信的程序,网上一搜一大堆,大家可能都会写,但是并发量和容错性不一定能上得去,2W的并发量不是盖出来的,如果这道题目能够搞定,基本上服务器这块应该是难不倒了。
下面说说我应该怎么做:
首先是建立前后端的通信协议:
request:
username/password, 约定username与password 各占32个字节(联同末位0)
response:
time_t 格式
由于通信内容简单,选择二进制传输,而不选择http,当然如果要考考 http 协议的了解,那就另当别论了。
二. 客户端
上篇规定的协议请求部分:
request:
username/password, 约定username与password 各占32个字节(联同末位0)
1.将username,password 封装进buffer
2.连接服务端
3.发送buffer
4.接收二进制的系统当前时间
5.显示时间
代码如下:
服务端地址设置部分:
- addr_server.sin_family = AF_INET;
- addr_server.sin_port = htons( port );
- addr_server.sin_addr.s_addr = inet_addr( ip );
创建连接:
- sock_client = socket( AF_INET, SOCK_STREAM, 0 );
连接服务端代码:
- flag = connect( sock_client, ( struct sockaddr* ) &addr_server, sizeof( addr_server ) );
设置buffer填充username/password代码:
- sprintf(buffer, "%s", "username");
- sprintf(buffer + 32, "%s", "password" );
- buffer[31]=buffer[63] = 0;
接着是发送:
- flag = send( sock_client, buffer, 64, 0 );
- if( flag == 64 )
- {
- printf( "send ok\n");
- }
接收部分代码:
- flag = recv( sock_client, buffer, 64, 0 );
- if( flag != sizeof( time_t ) )
- {
- printf( "recv does not follow protocal\n");
- close( sock_client );
- continue;
- }
将接收到的二进制数据转成时间
- memcpy( curtime, buffer, sizeof( time_t ) );
- struct tm *ptm = localtime( curtime );
显示时间:
- printf( "system time:%04d-%02d-%02d-%02d:%02d:%02d\n", ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday,
- ptm->tm_hour, ptm->tm_min, ptm->tm_sec );
关闭连接:
- printf( "ok,now we close connection\n" );
- close( sock_client );
实际测试中,我发现客户端的单进程并发量大概在1万左右,因此多开几个程序,已经可以满足需求了。
三. 服务器端(阻塞)
为什么 要在标题后面加个“阻塞”呢,因为系统为了增大并发,减小等待(阻塞),建立了另一种事件模式,后文将介绍,这里只介绍阻塞的模型。
阻塞服务器要干的事大致可以分为以下几步:
1.创建服务端监听连接
2.产生用户连接
3.接收用户请求
4.发送返回给用户
敲码过程如下:
设置监听地址与端口:
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons( port );
addr_server.sin_addr.s_addr = htonl( INADDR_ANY );
sock_server = socket( AF_INET, SOCK_STREAM, 0 );开始监听:
flag = bind( sock_server, ( struct sockaddr* )&addr_server, sizeof( struct sockaddr ) );
if( flag < 0 )
{
printf( "your bind is not ok\n" );
close( sock_server );
return 0;
}
flag = listen( sock_server, 50 );
if( flag < 0 )
{
printf( "your listen is not ok\n");
close( sock_server );
return 0;
}
接收并产生用户连接:
sock_client = accept( sock_server, ( struct sockaddr* )&addr_client, &size );
if( sock_client <=0 )
{
printf( "your accept is no ok\n");
close( sock_server );
return 0;
}
接收用户数据:
flag = recv( sock_client, buffer, RECV_BUF_LEN, 0 );
if( flag <= 0 )
{
printf( "your recv is no ok\n");
close( sock_client );
continue;
}
校验数据合法性:
if( flag != 64 )
{
printf( "your recv does follow the protocal\n");
close( sock_client );
continue;
}
if( buffer[31] || buffer[63] )
{
printf( "your recv does follow the protocal\n");
close( sock_client );
continue;
}
发送当前时间 至客户端:
current = time(0);
send( sock_client, ( const char* )¤t, sizeof( time_t), 0 );
关闭客户连接:
printf( "your connection is ok\n");
printf( "now close your connection\n");
close( sock_client );
四. 服务器端(异步,大并发)
上篇的阻塞模式下服务器的并发只有几K,而真正的server 像nginx, apache, yumeiz 轻轻松松处理几万个并发完全不在话下,因此大并发的场合下是不能用阻塞的。
1W的并发是一个分隔点,如果单进程模型下能达到 的话,说明至少在服务器这块你已经很厉害了。
服务器开发就像一门气功,能不能搞出大并发,容错性处理得怎么样,就是你有没有内功,内功有多深。
异步模式是专门为大并发而生,linux下一般用 epoll 来管理事件,下面就开始我们的异步大并发服务器实战吧。
跟阻塞开发一样,先来看看设计过程:
1.创建事件模型。
2.创建监听连接并监听。
3.将监听连接加入事件模型。
4.当有事件时,判断事件类型。
5.若事件为监听连接,则产生客户连接同时加入事件模型,对客户连接接收发送。
6.若事件为客户连接,处理相应IO请求。
为了让大家一概全貌,我用一个函数实现的( 一个函数写一个2W并发的服务器,你试过么),可读性可能会差点,但是对付这道面试题是绰绰有余了。
实际开发过程如下:
先定义一个事件结构,用于对客户连接进行缓存
- struct my_event_s
- {
- int fd;
- char recv[64];
- char send[64];
- int rc_pos;
- int sd_pos;
- };
建立缓存对象:
- struct epoll_event wait_events[ EPOLL_MAX ];
- struct my_event_s my_event[ EPOLL_MAX ];
创建监听连接:
- sock_server = socket( AF_INET, SOCK_STREAM, 0 );
- flag = fcntl( sock_server, F_GETFL, 0 );
- fcntl( sock_server, F_SETFL, flag | O_NONBLOCK );
绑定地址并监听:
- flag = bind( sock_server, ( struct sockaddr* )&addr_server, sizeof( struct sockaddr ) );
- if( flag < 0 )
- {
- printf( "your bind is not ok\n" );
- close( sock_server );
- return 0;
- }
- flag = listen( sock_server, 1024 );
- if( flag < 0 )
- {
- printf( "your listen is not ok\n");
- close( sock_server );
- return 0;
- }
- epfd = epoll_create( EPOLL_MAX );
- if( epfd <= 0 )
- {
- printf( "event module could not be setup\n");
- close( sock_server );
- return 0;
- }
- tobe_event.events = EPOLLIN;
- tobe_event.data.fd = sock_server;
- epoll_ctl( epfd, EPOLL_CTL_ADD, sock_server, &tobe_event );
事件模型处理:
- e_num = epoll_wait( epfd, wait_events, EPOLL_MAX, WAIT_TIME_OUT );
- if( e_num <= 0 )
- {
- continue;
- }
- for( i = 0; i < e_num; ++i )
- {
监听处理:
- if( sock_server == wait_events[ i ].data.fd )
- { while(1){
连接客户端:
- sock_client = accept( sock_server, ( struct sockaddr* )&addr_client, ( socklen_t*)&size );
- if( sock_client < 0 )
- {
- if( errno == EAGAIN )
- {
- break;
- }
- if( errno == EINTR )
- {
- continue;
- }
- break;
- }
将客户端连接设置为异步:
- flag = fcntl( sock_client, F_GETFL, 0 );
- fcntl( sock_client, F_SETFL, flag | O_NONBLOCK );
将客户端连接加入到 事件模型:
- tobe_event.events = EPOLLIN | EPOLLET;
- tobe_event.data.u32 = my_empty_index;
- epoll_ctl( epfd, EPOLL_CTL_ADD, sock_client, &tobe_event );
- ++num;
- current = time( 0 );
- if( current > last )
- {
- printf( "last sec qps:%d\n", num );
- num = 0;
- last = current;
- }
将时间填充到连接缓存中:
- memcpy( tobe_myevent->send, ¤t, sizeof(time_t) );
接收连接内容:
- flag = recv( sock_client, tobe_myevent->recv, 64, 0 );
- if( flag < 64 )
- {
- if( flag > 0 )
- tobe_myevent->rc_pos += flag;
- continue;
- }
- if( tobe_myevent->recv[31] || tobe_myevent->recv[63] )
- {
- printf( "your recv does follow the protocal\n");
- tobe_myevent->fd = 0;
- close( sock_client );
- continue;
- }
- flag = send( sock_client, tobe_myevent->send, sizeof( time_t ), 0 );
- if( flag < sizeof( time_t ) )
- {
- <span style="white-space:pre"> </span>tobe_event.events = EPOLLET | EPOLLOUT;
- <span style="white-space:pre"> </span>epoll_ctl( epfd, EPOLL_CTL_MOD, sock_client, &tobe_event );
- if( flag > 0 )
- tobe_myevent->sd_pos += flag;
- continue;
- }
- tobe_myevent->fd = 0;
- close( sock_client );
后面进行普通连接事件处理,错误处理:
- if( event_flag | EPOLLHUP )
- {
- tobe_myevent->fd = 0;
- close( sock_client );
- continue;
- }
- else if( event_flag | EPOLLERR )
- {
- tobe_myevent->fd = 0;
- close( sock_client );
- continue;
- }
写事件:
- else if( event_flag | EPOLLOUT )
- {
- if( tobe_myevent->rc_pos != 64 )
- {
- continue;
- }
- if( tobe_myevent->sd_pos >= sizeof( time_t ) )
- {
- tobe_myevent->fd = 0;
- close( sock_client );
- continue;
- }
- flag = send( sock_client, tobe_myevent->send + tobe_myevent->sd_pos, sizeof( time_t ) - tobe_myevent->sd_pos, 0 );
- if( flag < 0 )
- {
- if( errno == EAGAIN )
- {
- continue;
- }
- else if( errno == EINTR )
- {
- continue;
- }
- tobe_myevent->fd = 0;
- close( sock_client );
- continue;
- }
- if( flag >0 )
- {
- tobe_myevent->sd_pos += flag;
- if( tobe_myevent->sd_pos >= sizeof( time_t ) )
- {
- tobe_myevent->fd = 0;
- close( sock_client );
- continue;
- }
- }
- }
- if( event_flag | EPOLLIN )
- {
- if( tobe_myevent->rc_pos < 64 )
- {
- flag = recv( sock_client, tobe_myevent->recv + tobe_myevent->rc_pos, 64 - tobe_myevent->rc_pos, 0 );
- if( flag <= 0 )
- {
- continue;
- }
- tobe_myevent->rc_pos += flag;
- if( tobe_myevent->rc_pos < 64 )
- {
- continue;
- }
- if( tobe_myevent->recv[31] || tobe_myevent->recv[63] )
- {
- printf( "your recv does follow the protocal\n");
- tobe_myevent->fd = 0;
- close( sock_client );
- continue;
- }
- flag = send( sock_client, tobe_myevent->send, sizeof( time_t ), 0 );
- if( flag < sizeof( time_t ) )
- {
- if( flag > 0 )
- tobe_myevent->sd_pos += flag;
- <span style="white-space:pre"> </span>tobe_event.events = EPOLLET | EPOLLOUT;
- tobe_event.data.u32 = wait_events[i].data.u32;
- epoll_ctl( epfd, EPOLL_CTL_MOD, sock_client, &tobe_event );
- continue;
- }
- tobe_myevent->fd = 0;
- close( sock_client );
- }
- }