简介:

半同步/半反应堆线程池是通过一个线程往工作队列添加任务T,然后工作线程竞争工作队列获得任务T。HTTP请求解析服务端程序:逐行解析客户端发送来的HTTP请求然后作出HTTP回答。采用线程池就是:服务端创建一个线程池,然后有HTTP请求到达就将HTTP请求的处理添加到线程池任务队列中去,线程池工作线程通过竞态机制(信号量)竞争任务T(HTTP请求处理)。

HTTP请求内容:

GET http://www.baidu.com/index.html HTTP/1.1       //该行是HTTP请求行,GET是请求方法表示以只读方式获取资源  http那一串是url   HTTP那个是客户端HTTP版本

User-Agent:Wget/1.12 (linux-gnu)                               //客户端使用的程序

Host: www.baidu.com                                                     //目的主机名,必须有

Connection: close                                                            //用于告诉服务器处理完这个请求后关闭连接,可选其它

注意:第一行为请求行,后面3行是HTTP头部,HTTP请求每行均以回车符和换行符结束,所有头部字段结束后必须包含一个空行,该空行只能有一个 回车和换行符。HTTP回答类似。可见HTTP解析分为请求行和头部,每行按照\r\t提取

半同步/半异步线程池:threadpool.h

[cpp] view plaincopy
  1. #ifndef THREADPOOL_H
  2. #define THREADPOOL_H
  3. #include <list>
  4. #include <cstdio>
  5. #include <exception>
  6. #include <pthread.h>
  7. #include "locker.h"//简单封装了互斥量和信号量的接口
  8. template< typename T >
  9. class threadpool//线程池类模板参数T是任务类型,T中必须有接口process
  10. {
  11. public:
  12. threadpool( int thread_number = 8, int max_requests = 10000 );//线程数目和最大连接处理数
  13. ~threadpool();
  14. bool append( T* request );
  15. private:
  16. static void* worker( void* arg );//线程工作函数
  17. void run();//启动线程池
  18. private:
  19. int m_thread_number;//线程数量
  20. int m_max_requests;//最大连接数目
  21. pthread_t* m_threads;//线程id
  22. std::list< T* > m_workqueue;//工作队列:各线程竞争该队列并处理相应的任务逻辑T
  23. locker m_queuelocker;//工作队列互斥量
  24. sem m_queuestat;//信号量:用于工作队列
  25. bool m_stop;//终止标志
  26. };
  27. template< typename T >
  28. threadpool< T >::threadpool( int thread_number, int max_requests ) :
  29. m_thread_number( thread_number ), m_max_requests( max_requests ), m_stop( false ), m_threads( NULL )
  30. {
  31. if( ( thread_number <= 0 ) || ( max_requests <= 0 ) )
  32. {
  33. throw std::exception();
  34. }
  35. m_threads = new pthread_t[ m_thread_number ];//工作线程数组
  36. if( ! m_threads )
  37. {
  38. throw std::exception();
  39. }
  40. for ( int i = 0; i < thread_number; ++i )//创建工作线程
  41. {
  42. printf( "create the %dth thread\n", i );
  43. if( pthread_create( m_threads + i, NULL, worker, this ) != 0 )//注意C++调用pthread_create函数的第三个参数必须是一个静态函数,一个静态成员使用动态成员的方式:通过类静态对象、将类对象作为参数传给静态函数。这里使用了后者所以有this
  44. {
  45. delete [] m_threads;
  46. throw std::exception();
  47. }
  48. if( pthread_detach( m_threads[i] ) )//线程分离后其它线程无法pthread_join等待
  49. {
  50. delete [] m_threads;
  51. throw std::exception();
  52. }
  53. }
  54. }
  55. template< typename T >
  56. threadpool< T >::~threadpool()
  57. {
  58. delete [] m_threads;
  59. m_stop = true;
  60. }
  61. template< typename T >
  62. bool threadpool< T >::append( T* request )//向工作队列添加任务T
  63. {
  64. m_queuelocker.lock();//非原子操作需要互斥量保护
  65. if ( m_workqueue.size() > m_max_requests )//任务队列满了
  66. {
  67. m_queuelocker.unlock();
  68. return false;
  69. }
  70. m_workqueue.push_back( request );
  71. m_queuelocker.unlock();
  72. m_queuestat.post();//信号量的V操作,即信号量+1多了个工作任务T
  73. return true;
  74. }
  75. template< typename T >
  76. void* threadpool< T >::worker( void* arg )//工作线程函数
  77. {
  78. threadpool* pool = ( threadpool* )arg;//获取进程池对象
  79. pool->run();//调用线程池run函数
  80. return pool;
  81. }
  82. template< typename T >
  83. void threadpool< T >::run()//工作线程真正工作逻辑:从任务队列领取任务T并执行任务T
  84. {
  85. while ( ! m_stop )
  86. {
  87. m_queuestat.wait();//信号量P操作,申请信号量获取任务T
  88. m_queuelocker.lock();//互斥量保护任务队列,和前面的信号量顺序不能呼唤。。。你懂的
  89. if ( m_workqueue.empty() )
  90. {
  91. m_queuelocker.unlock();//任务队列空那就没任务呗
  92. continue;
  93. }
  94. T* request = m_workqueue.front();//获取任务T
  95. m_workqueue.pop_front();
  96. m_queuelocker.unlock();
  97. if ( ! request )
  98. {
  99. continue;
  100. }
  101. request->process();//执行任务T的相应逻辑,任务T中必须有process接口
  102. }
  103. }
  104. #endif

HTTP请求任务T:http_conn.h接收到一个HTTP请求后解析该请求,然后作出应答

[cpp] view plaincopy
  1. #ifndef HTTPCONNECTION_H
  2. #define HTTPCONNECTION_H
  3. #include <unistd.h>
  4. #include <signal.h>
  5. #include <sys/types.h>
  6. #include <sys/epoll.h>
  7. #include <fcntl.h>
  8. #include <sys/socket.h>
  9. #include <netinet/in.h>
  10. #include <arpa/inet.h>
  11. #include <assert.h>
  12. #include <sys/stat.h>
  13. #include <string.h>
  14. #include <pthread.h>
  15. #include <stdio.h>
  16. #include <stdlib.h>
  17. #include <sys/mman.h>
  18. #include <stdarg.h>
  19. #include <errno.h>
  20. #include "locker.h"//互斥量和信号量的简单封装
  21. class http_conn//HTTP连接任务类型,用于线程池工作队列的任务类型T
  22. {
  23. public:
  24. static const int FILENAME_LEN = 200;//文件名最大长度,文件是HTTP请求的资源页文件
  25. static const int READ_BUFFER_SIZE = 2048;//读缓冲区,用于读取HTTP请求
  26. static const int WRITE_BUFFER_SIZE = 1024;//写缓冲区,用于HTTP回答
  27. enum METHOD { GET = 0, POST, HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH };//HTTP请求方法,本程序只定义了GET逻辑
  28. enum CHECK_STATE { CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER, CHECK_STATE_CONTENT };//HTTP请求状态:正在解析请求行、正在解析头部、解析中
  29. enum HTTP_CODE { NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE, FORBIDDEN_REQUEST, FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION };//HTTP请求结果:未完整的请求(客户端仍需要提交请求)、完整的请求、错误请求...只用了前三个
  30. enum LINE_STATUS { LINE_OK = 0, LINE_BAD, LINE_OPEN };//HTTP每行解析状态:改行解析完毕、错误的行、正在解析行
  31. public:
  32. http_conn(){}
  33. ~http_conn(){}
  34. public:
  35. void init( int sockfd, const sockaddr_in& addr );//初始化新的HTTP连接
  36. void close_conn( bool real_close = true );
  37. void process();//处理客户请求
  38. bool read();//读取客户发送来的数据(HTTP请求)
  39. bool write();//将请求结果返回给客户端
  40. private:
  41. void init();//重载init初始化连接,用于内部调用
  42. HTTP_CODE process_read();//解析HTTP请求,内部调用parse_系列函数
  43. bool process_write( HTTP_CODE ret );//填充HTTP应答,通常是将客户请求的资源页发送给客户,内部调用add_系列函数
  44. HTTP_CODE parse_request_line( char* text );//解析HTTP请求的请求行
  45. HTTP_CODE parse_headers( char* text );//解析HTTP头部数据
  46. HTTP_CODE parse_content( char* text );//获取解析结果
  47. HTTP_CODE do_request();//处理HTTP连接:内部调用process_read(),process_write()
  48. char* get_line() { return m_read_buf + m_start_line; }//获取HTTP请求数据中的一行数据
  49. LINE_STATUS parse_line();//解析行内部调用parse_request_line和parse_headers
  50. //下面的函数被process_write填充HTTP应答
  51. void unmap();//解除内存映射,这里内存映射是指将客户请求的资源页文件映射通过mmap映射到内存
  52. bool add_response( const char* format, ... );
  53. bool add_content( const char* content );
  54. bool add_status_line( int status, const char* title );
  55. bool add_headers( int content_length );
  56. bool add_content_length( int content_length );
  57. bool add_linger();
  58. bool add_blank_line();
  59. public:
  60. static int m_epollfd;//所有socket上的事件都注册到一个epoll事件表中所以用static
  61. static int m_user_count;//用户数量
  62. private:
  63. int m_sockfd;//HTTP连接对应的客户在服务端的描述符m_sockfd和地址m_address
  64. sockaddr_in m_address;
  65. char m_read_buf[ READ_BUFFER_SIZE ];//读缓冲区,读取HTTP请求
  66. int m_read_idx;//已读入的客户数据最后一个字节的下一个位置,即未读数据的第一个位置
  67. int m_checked_idx;//当前已经解析的字节(HTTP请求需要逐个解析)
  68. int m_start_line;//当前解析行的起始位置
  69. char m_write_buf[ WRITE_BUFFER_SIZE ];//写缓冲区
  70. int m_write_idx;//写缓冲区待发送的数据
  71. CHECK_STATE m_check_state;//HTTP解析的状态:请求行解析、头部解析
  72. METHOD m_method;//HTTP请求方法,只实现了GET
  73. char m_real_file[ FILENAME_LEN ];//HTTP请求的资源页对应的文件名称,和服务端的路径拼接就形成了资源页的路径
  74. char* m_url;//请求的具体资源页名称,如:www.baidu.com/index.html
  75. char* m_version;//HTTP协议版本号,一般是:HTTP/1.1
  76. char* m_host;//主机名,客户端要在HTTP请求中的目的主机名
  77. int m_content_length;//HTTP消息体的长度,简单的GET请求这个为空
  78. bool m_linger;//HTTP请求是否保持连接
  79. char* m_file_address;//资源页文件内存映射后的地址
  80. struct stat m_file_stat;//资源页文件的状态,stat文件结构体
  81. struct iovec m_iv[2];//调用writev集中写函数需要m_iv_count表示被写内存块的数量,iovec结构体存放了一段内存的起始位置和长度,
  82. int m_iv_count;//m_iv_count是指iovec结构体数组的长度即多少个内存块
  83. };
  84. #endif

任务T:http_conn.cpp

[cpp] view plaincopy
  1. #include "http_conn.h"
  2. //定义了HTTP请求的返回状态信息,类似大家都熟悉的404
  3. const char* ok_200_title = "OK";
  4. const char* error_400_title = "Bad Request";
  5. const char* error_400_form = "Your request has bad syntax or is inherently impossible to satisfy.\n";
  6. const char* error_403_title = "Forbidden";
  7. const char* error_403_form = "You do not have permission to get file from this server.\n";
  8. const char* error_404_title = "Not Found";
  9. const char* error_404_form = "The requested file was not found on this server.\n";
  10. const char* error_500_title = "Internal Error";
  11. const char* error_500_form = "There was an unusual problem serving the requested file.\n";
  12. const char* doc_root = "/var/www/html";//服务端资源页的路径,将其和HTTP请求中解析的m_url拼接形成资源页的位置
  13. int setnonblocking( int fd )//将fd设置为非阻塞
  14. {
  15. int old_option = fcntl( fd, F_GETFL );
  16. int new_option = old_option | O_NONBLOCK;
  17. fcntl( fd, F_SETFL, new_option );
  18. return old_option;
  19. }
  20. void addfd( int epollfd, int fd, bool one_shot )//将fd添加到事件表epollfd
  21. {
  22. epoll_event event;
  23. event.data.fd = fd;
  24. event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
  25. if( one_shot )//采用EPOLLONESHOT事件避免了同一事件被多次触发,因为一个事件只被触发一次且需要重置事件才能侦听下次是否发生
  26. {
  27. event.events |= EPOLLONESHOT;
  28. }
  29. epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
  30. setnonblocking( fd );
  31. }
  32. void removefd( int epollfd, int fd )//将fd从事件表epollfd中移除
  33. {
  34. epoll_ctl( epollfd, EPOLL_CTL_DEL, fd, 0 );
  35. close( fd );
  36. }
  37. void modfd( int epollfd, int fd, int ev )//EPOLLONESHOT需要重置事件后事件才能进行下次侦听
  38. {
  39. epoll_event event;
  40. event.data.fd = fd;
  41. event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
  42. epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event );//注意是EPOLL_CTL_MOD修改
  43. }
  44. int http_conn::m_user_count = 0;//连接数
  45. int http_conn::m_epollfd = -1;//事件表,注意是static故所有http_con类对象共享一个事件表
  46. void http_conn::close_conn( bool real_close )//关闭连接,从事件表中移除描述符
  47. {
  48. if( real_close && ( m_sockfd != -1 ) )//m_sockfd是该HTTP连接对应的描述符
  49. {
  50. //modfd( m_epollfd, m_sockfd, EPOLLIN );
  51. removefd( m_epollfd, m_sockfd );
  52. m_sockfd = -1;
  53. m_user_count--;
  54. }
  55. }
  56. void http_conn::init( int sockfd, const sockaddr_in& addr )//初始化连接
  57. {
  58. m_sockfd = sockfd;//sockfd是http连接对应的描述符用于接收http请求和http回答
  59. m_address = addr;//客户端地址
  60. int error = 0;
  61. socklen_t len = sizeof( error );
  62. getsockopt( m_sockfd, SOL_SOCKET, SO_ERROR, &error, &len );
  63. int reuse = 1;
  64. setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );//获取描述符状态,可以在调试时用
  65. addfd( m_epollfd, sockfd, true );
  66. m_user_count++;//多了一个http用户
  67. init();//调用重载函数
  68. }
  69. void http_conn::init()//重载init函数进行些连接前的初始化操作
  70. {
  71. m_check_state = CHECK_STATE_REQUESTLINE;
  72. m_linger = false;
  73. m_method = GET;
  74. m_url = 0;
  75. m_version = 0;
  76. m_content_length = 0;
  77. m_host = 0;
  78. m_start_line = 0;
  79. m_checked_idx = 0;
  80. m_read_idx = 0;
  81. m_write_idx = 0;
  82. memset( m_read_buf, '\0', READ_BUFFER_SIZE );
  83. memset( m_write_buf, '\0', WRITE_BUFFER_SIZE );
  84. memset( m_real_file, '\0', FILENAME_LEN );
  85. }
  86. http_conn::LINE_STATUS http_conn::parse_line()//解析HTTP数据:将HTTP数据的每行数据提取出来,每行以回车\r和换行符\n结束
  87. {
  88. char temp;
  89. for ( ; m_checked_idx < m_read_idx; ++m_checked_idx )//m_checked_idx是当前正在解析的字节,m_read_idx是读缓冲区中已有的数据(客户端发送了多少HTTP请求数据到来),解析到m_read_idx号字节
  90. {
  91. temp = m_read_buf[ m_checked_idx ];//当前解析字节
  92. if ( temp == '\r' )//若为回车符:
  93. {
  94. if ( ( m_checked_idx + 1 ) == m_read_idx )//若为回车符:若此回车符是已读取数据的最后一个则仍需要解析改行(即该行数据还没有接收完整)
  95. {
  96. return LINE_OPEN;
  97. }
  98. else if ( m_read_buf[ m_checked_idx + 1 ] == '\n' )//若回车符的下一个是换行符\r则表明该行解析完毕(回车+换行是HTTP请求每行固定结束规则)
  99. {
  100. m_read_buf[ m_checked_idx++ ] = '\0';//将该行数据送给缓冲区
  101. m_read_buf[ m_checked_idx++ ] = '\0';
  102. return LINE_OK;//返回状态:该行解析成功
  103. }
  104. return LINE_BAD;//否则解析失败
  105. }
  106. else if( temp == '\n' )//解析的字符是换行符则前一个必须是回车才解析成功
  107. {
  108. if( ( m_checked_idx > 1 ) && ( m_read_buf[ m_checked_idx - 1 ] == '\r' ) )
  109. {
  110. m_read_buf[ m_checked_idx-1 ] = '\0';
  111. m_read_buf[ m_checked_idx++ ] = '\0';
  112. return LINE_OK;
  113. }
  114. return LINE_BAD;
  115. }
  116. }
  117. return LINE_OPEN;//正在解析,还有HTTP请求数据没有接收到....
  118. }
  119. bool http_conn::read()//读取HTTP请求数据
  120. {
  121. if( m_read_idx >= READ_BUFFER_SIZE )//读缓冲区已满
  122. {
  123. return false;
  124. }
  125. int bytes_read = 0;//记录接收的字节数
  126. while( true )//循环读取的原因是EPOLLONESHOT一个事件只触发一次所以需要一次性读取完全否则数据丢失
  127. {
  128. bytes_read = recv( m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0 );//接收客户端的HTTP请求
  129. if ( bytes_read == -1 )
  130. {
  131. if( errno == EAGAIN || errno == EWOULDBLOCK )//非阻塞描述符这两个errno不是网络出错而是设备当前不可得,在这里就是一次事件的数据读取完毕
  132. {
  133. break;
  134. }
  135. return false;//否则recv出错
  136. }
  137. else if ( bytes_read == 0 )//客户端关闭了连接
  138. {
  139. return false;
  140. }
  141. m_read_idx += bytes_read;//更新读缓冲区的已读大小(用于解析行函数)
  142. }
  143. return true;
  144. }
  145. http_conn::HTTP_CODE http_conn::parse_request_line( char* text )//解析HTTP的请求行部分
  146. {
  147. m_url = strpbrk( text, " \t" );//在text搜索\t的位置
  148. if ( ! m_url )
  149. {
  150. return BAD_REQUEST;
  151. }
  152. *m_url++ = '\0';
  153. char* method = text;
  154. if ( strcasecmp( method, "GET" ) == 0 )//忽略大小写比较mehtod和GET的大小返回值和strcmp定义相同
  155. {
  156. m_method = GET;
  157. }
  158. else
  159. {
  160. return BAD_REQUEST;
  161. }
  162. m_url += strspn( m_url, " \t" );//strspn函数是在m_url找到第一个\t位置,拼接资源页文件路径
  163. m_version = strpbrk( m_url, " \t" );
  164. if ( ! m_version )
  165. {
  166. return BAD_REQUEST;
  167. }
  168. *m_version++ = '\0';
  169. m_version += strspn( m_version, " \t" );
  170. if ( strcasecmp( m_version, "HTTP/1.1" ) != 0 )//HTTP版本
  171. {
  172. return BAD_REQUEST;
  173. }
  174. if ( strncasecmp( m_url, "http://", 7 ) == 0 )
  175. {
  176. m_url += 7;
  177. m_url = strchr( m_url, '/' );
  178. }
  179. if ( ! m_url || m_url[ 0 ] != '/' )
  180. {
  181. return BAD_REQUEST;
  182. }
  183. m_check_state = CHECK_STATE_HEADER;//将HTTP解析状态更新为解析头部,那么HTTP解析进入解析HTTP头部。这是有限状态机
  184. return NO_REQUEST;
  185. }
  186. http_conn::HTTP_CODE http_conn::parse_headers( char* text )//解析HTTP头部
  187. {
  188. if( text[ 0 ] == '\0' )
  189. {
  190. if ( m_method == HEAD )
  191. {
  192. return GET_REQUEST;//已经获取了一个完整的HTTP请求
  193. }
  194. if ( m_content_length != 0 )//若HTTP请求消息长度不为空
  195. {
  196. m_check_state = CHECK_STATE_CONTENT;//则解析头部后还要解析消息体,所以HTTP解析状态仍为正在解析中...GET请求不会出现这个...
  197. return NO_REQUEST;
  198. }
  199. return GET_REQUEST;
  200. }
  201. else if ( strncasecmp( text, "Connection:", 11 ) == 0 )
  202. {
  203. text += 11;
  204. text += strspn( text, " \t" );
  205. if ( strcasecmp( text, "keep-alive" ) == 0 )
  206. {
  207. m_linger = true;
  208. }
  209. }
  210. else if ( strncasecmp( text, "Content-Length:", 15 ) == 0 )
  211. {
  212. text += 15;
  213. text += strspn( text, " \t" );
  214. m_content_length = atol( text );
  215. }
  216. else if ( strncasecmp( text, "Host:", 5 ) == 0 )
  217. {
  218. text += 5;
  219. text += strspn( text, " \t" );
  220. m_host = text;
  221. }
  222. else
  223. {
  224. printf( "oop! unknow header %s\n", text );
  225. }
  226. return NO_REQUEST;
  227. }
  228. http_conn::HTTP_CODE http_conn::parse_content( char* text )//解析结果
  229. {
  230. if ( m_read_idx >= ( m_content_length + m_checked_idx ) )//若解析到缓冲区的最后位置则获得一个一个完整的连接请求
  231. {
  232. text[ m_content_length ] = '\0';
  233. return GET_REQUEST;
  234. }
  235. return NO_REQUEST;//请求不完整
  236. }
  237. http_conn::HTTP_CODE http_conn::process_read()//完整的HTTP解析
  238. {
  239. LINE_STATUS line_status = LINE_OK;
  240. HTTP_CODE ret = NO_REQUEST;
  241. char* text = 0;
  242. while ( ( ( m_check_state == CHECK_STATE_CONTENT ) && ( line_status == LINE_OK  ) )
  243. || ( ( line_status = parse_line() ) == LINE_OK ) ){//满足条件:正在进行HTTP解析、读取一个完整行
  244. text = get_line();//从读缓冲区(HTTP请求数据)获取一行数据
  245. m_start_line = m_checked_idx;//行的起始位置等于正在每行解析的第一个字节
  246. printf( "got 1 http line: %s\n", text );
  247. switch ( m_check_state )//HTTP解析状态跳转
  248. {
  249. case CHECK_STATE_REQUESTLINE://正在分析请求行
  250. {
  251. ret = parse_request_line( text );//分析请求行
  252. if ( ret == BAD_REQUEST )
  253. {
  254. return BAD_REQUEST;
  255. }
  256. break;
  257. }
  258. case CHECK_STATE_HEADER://正在分析请求头部
  259. {
  260. ret = parse_headers( text );//分析头部
  261. if ( ret == BAD_REQUEST )
  262. {
  263. return BAD_REQUEST;
  264. }
  265. else if ( ret == GET_REQUEST )
  266. {
  267. return do_request();//当获得一个完整的连接请求则调用do_request分析处理资源页文件
  268. }
  269. break;
  270. }
  271. case CHECK_STATE_CONTENT://HTTP解析状态仍为正在解析...没有办法只好继续解析呗....解析消息体
  272. {
  273. ret = parse_content( text );
  274. if ( ret == GET_REQUEST )
  275. {
  276. return do_request();
  277. }
  278. line_status = LINE_OPEN;
  279. break;
  280. }
  281. default:
  282. {
  283. return INTERNAL_ERROR;//内部错误
  284. }
  285. }
  286. }
  287. return NO_REQUEST;
  288. }
  289. http_conn::HTTP_CODE http_conn::do_request()//用于获取资源页文件的状态
  290. {
  291. strcpy( m_real_file, doc_root );
  292. int len = strlen( doc_root );
  293. strncpy( m_real_file + len, m_url, FILENAME_LEN - len - 1 );
  294. if ( stat( m_real_file, &m_file_stat ) < 0 )
  295. {
  296. return NO_RESOURCE;//若资源页不存在则HTTP解析结果为:没有资源...万恶的404
  297. }
  298. if ( ! ( m_file_stat.st_mode & S_IROTH ) )
  299. {
  300. return FORBIDDEN_REQUEST;//资源没有权限获取
  301. }
  302. if ( S_ISDIR( m_file_stat.st_mode ) )
  303. {
  304. return BAD_REQUEST;//请求有错
  305. }
  306. int fd = open( m_real_file, O_RDONLY );
  307. m_file_address = ( char* )mmap( 0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );//将资源页文件映射到内存
  308. close( fd );
  309. return FILE_REQUEST;//资源页请求成功
  310. }
  311. void http_conn::unmap()//解除资源页文件映射的内存
  312. {
  313. if( m_file_address )
  314. {
  315. munmap( m_file_address, m_file_stat.st_size );//解除映射
  316. m_file_address = 0;
  317. }
  318. }
  319. bool http_conn::write()//将资源页文件发送给客户端
  320. {
  321. int temp = 0;
  322. int bytes_have_send = 0;
  323. int bytes_to_send = m_write_idx;
  324. if ( bytes_to_send == 0 )
  325. {
  326. modfd( m_epollfd, m_sockfd, EPOLLIN );//EPOLLONESHOT事件每次需要重置事件
  327. init();
  328. return true;
  329. }
  330. while( 1 )//
  331. {
  332. temp = writev( m_sockfd, m_iv, m_iv_count );//集中写,m_sockfd是http连接对应的描述符,m_iv是iovec结构体数组表示内存块地址,m_iv_count是数组的长度即多少个内存块将一次集中写到m_sockfd
  333. if ( temp <= -1 )//集中写失败
  334. {
  335. if( errno == EAGAIN )
  336. {
  337. modfd( m_epollfd, m_sockfd, EPOLLOUT );//重置EPOLLONESHOT事件,注册可写事件表示若m_sockfd没有写失败则关闭连接
  338. return true;
  339. }
  340. unmap();//解除内存映射
  341. return false;
  342. }
  343. bytes_to_send -= temp;//待发送数据
  344. bytes_have_send += temp;//已发送数据
  345. if ( bytes_to_send <= bytes_have_send )
  346. {
  347. unmap();//该资源页已经发送完毕该解除映射
  348. if( m_linger )//若要保持该http连接
  349. {
  350. init();//初始化http连接
  351. modfd( m_epollfd, m_sockfd, EPOLLIN );
  352. return true;
  353. }
  354. else
  355. {
  356. modfd( m_epollfd, m_sockfd, EPOLLIN );
  357. return false;
  358. }
  359. }
  360. }
  361. }
  362. bool http_conn::add_response( const char* format, ... )//HTTP应答主要是将应答数据添加到写缓冲区m_write_buf
  363. {
  364. if( m_write_idx >= WRITE_BUFFER_SIZE )
  365. {
  366. return false;
  367. }
  368. va_list arg_list;
  369. va_start( arg_list, format );
  370. int len = vsnprintf( m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list );//将fromat内容输出到m_write_buf
  371. if( len >= ( WRITE_BUFFER_SIZE - 1 - m_write_idx ) )
  372. {
  373. return false;
  374. }
  375. m_write_idx += len;
  376. va_end( arg_list );
  377. return true;
  378. }
  379. bool http_conn::add_status_line( int status, const char* title )
  380. {
  381. return add_response( "%s %d %s\r\n", "HTTP/1.1", status, title );//
  382. }
  383. bool http_conn::add_headers( int content_len )
  384. {
  385. add_content_length( content_len );
  386. add_linger();
  387. add_blank_line();//加空行:回车+换行
  388. }
  389. bool http_conn::add_content_length( int content_len )
  390. {
  391. return add_response( "Content-Length: %d\r\n", content_len );//
  392. }
  393. bool http_conn::add_linger()
  394. {
  395. return add_response( "Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" );//
  396. }
  397. bool http_conn::add_blank_line()
  398. {
  399. return add_response( "%s", "\r\n" );//
  400. }
  401. bool http_conn::add_content( const char* content )
  402. {
  403. return add_response( "%s", content );
  404. }
  405. bool http_conn::process_write( HTTP_CODE ret )//填充HTTP应答
  406. {
  407. switch ( ret )
  408. {
  409. case INTERNAL_ERROR:
  410. {
  411. add_status_line( 500, error_500_title );
  412. add_headers( strlen( error_500_form ) );
  413. if ( ! add_content( error_500_form ) )
  414. {
  415. return false;
  416. }
  417. break;
  418. }
  419. case BAD_REQUEST:
  420. {
  421. add_status_line( 400, error_400_title );
  422. add_headers( strlen( error_400_form ) );
  423. if ( ! add_content( error_400_form ) )
  424. {
  425. return false;
  426. }
  427. break;
  428. }
  429. case NO_RESOURCE:
  430. {
  431. add_status_line( 404, error_404_title );
  432. add_headers( strlen( error_404_form ) );
  433. if ( ! add_content( error_404_form ) )
  434. {
  435. return false;
  436. }
  437. break;
  438. }
  439. case FORBIDDEN_REQUEST:
  440. {
  441. add_status_line( 403, error_403_title );
  442. add_headers( strlen( error_403_form ) );
  443. if ( ! add_content( error_403_form ) )
  444. {
  445. return false;
  446. }
  447. break;
  448. }
  449. case FILE_REQUEST://资源页文件可用
  450. {
  451. add_status_line( 200, ok_200_title );
  452. if ( m_file_stat.st_size != 0 )
  453. {
  454. add_headers( m_file_stat.st_size );//m_file_stat资源页文件状态
  455. m_iv[ 0 ].iov_base = m_write_buf;//写缓冲区
  456. m_iv[ 0 ].iov_len = m_write_idx;//长度
  457. m_iv[ 1 ].iov_base = m_file_address;//资源页数据内存映射后在m_file_address地址
  458. m_iv[ 1 ].iov_len = m_file_stat.st_size;//文件长度就是该块内存长度
  459. m_iv_count = 2;
  460. return true;
  461. }
  462. else
  463. {
  464. const char* ok_string = "<html><body></body></html>";//请求页位空白
  465. add_headers( strlen( ok_string ) );
  466. if ( ! add_content( ok_string ) )
  467. {
  468. return false;
  469. }
  470. }
  471. }
  472. default:
  473. {
  474. return false;
  475. }
  476. }
  477. m_iv[ 0 ].iov_base = m_write_buf;
  478. m_iv[ 0 ].iov_len = m_write_idx;
  479. m_iv_count = 1;
  480. return true;
  481. }
  482. void http_conn::process()//处理HTTP请求
  483. {
  484. HTTP_CODE read_ret = process_read();//读取HTTP请求数据
  485. if ( read_ret == NO_REQUEST )
  486. {
  487. modfd( m_epollfd, m_sockfd, EPOLLIN );
  488. return;
  489. }
  490. bool write_ret = process_write( read_ret );//发送资源页给客户端
  491. if ( ! write_ret )
  492. {
  493. close_conn();
  494. }
  495. modfd( m_epollfd, m_sockfd, EPOLLOUT );
  496. }

服务端程序:接收HTTP请求并交给线程池处理这些请求

[cpp] view plaincopy
  1. #include <sys/socket.h>
  2. #include <netinet/in.h>
  3. #include <arpa/inet.h>
  4. #include <stdio.h>
  5. #include <unistd.h>
  6. #include <errno.h>
  7. #include <string.h>
  8. #include <fcntl.h>
  9. #include <stdlib.h>
  10. #include <cassert>
  11. #include <sys/epoll.h>
  12. #include "locker.h"//该头文件封装了信号量和互斥量
  13. #include "threadpool.h"//半同步/半反应堆线程池
  14. #include "http_conn.h"//HTTP连接任务类T
  15. #define MAX_FD 65536//最大文件数目
  16. #define MAX_EVENT_NUMBER 10000//最大事件数目
  17. extern int addfd( int epollfd, int fd, bool one_shot );//采用http_conn.h的addfd函数
  18. extern int removefd( int epollfd, int fd );//这也是http_conn.h中的函数
  19. void addsig( int sig, void( handler )(int), bool restart = true )//安装信号,用于统一事件源(将信号和IO事件统一监听)
  20. {
  21. struct sigaction sa;
  22. memset( &sa, '\0', sizeof( sa ) );
  23. sa.sa_handler = handler;
  24. if( restart )
  25. {
  26. sa.sa_flags |= SA_RESTART;
  27. }
  28. sigfillset( &sa.sa_mask );
  29. assert( sigaction( sig, &sa, NULL ) != -1 );
  30. }
  31. void show_error( int connfd, const char* info )
  32. {
  33. printf( "%s", info );
  34. send( connfd, info, strlen( info ), 0 );
  35. close( connfd );
  36. }
  37. int main( int argc, char* argv[] )
  38. {
  39. if( argc <= 2 )
  40. {
  41. printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
  42. return 1;
  43. }
  44. const char* ip = argv[1];
  45. int port = atoi( argv[2] );
  46. addsig( SIGPIPE, SIG_IGN );
  47. threadpool< http_conn >* pool = NULL;
  48. try
  49. {
  50. pool = new threadpool< http_conn >;//创建线程池
  51. }
  52. catch( ... )
  53. {
  54. return 1;
  55. }
  56. http_conn* users = new http_conn[ MAX_FD ];//创建超大的用户HTTP连接任务数组,给定一个http连接的描述符作为下标即可索引到这个任务,空间换时间
  57. assert( users );
  58. int user_count = 0;
  59. int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
  60. assert( listenfd >= 0 );
  61. struct linger tmp = { 1, 0 };
  62. setsockopt( listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof( tmp ) );
  63. int ret = 0;
  64. struct sockaddr_in address;
  65. bzero( &address, sizeof( address ) );
  66. address.sin_family = AF_INET;
  67. inet_pton( AF_INET, ip, &address.sin_addr );
  68. address.sin_port = htons( port );
  69. ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
  70. assert( ret >= 0 );
  71. ret = listen( listenfd, 5 );
  72. assert( ret >= 0 );
  73. epoll_event events[ MAX_EVENT_NUMBER ];
  74. int epollfd = epoll_create( 5 );//创建事件表
  75. assert( epollfd != -1 );
  76. addfd( epollfd, listenfd, false );//将监听端口添加到事件表,false表示不注册EPOLLONESHOT事件,注意不能将监听端口注册为EPOLLONESHOT事件因为该事件每次发生只触发一次,而accept每次只能连接一个客户,那么多个客户连接请求到来,则必然丢失客户连接请求
  77. http_conn::m_epollfd = epollfd;
  78. while( true )
  79. {
  80. int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );//无限期等待sockfd上的注册事件
  81. if ( ( number < 0 ) && ( errno != EINTR ) )//若epoll_wait不是因中断EINTR是出错
  82. {
  83. printf( "epoll failure\n" );
  84. break;
  85. }
  86. for ( int i = 0; i < number; i++ )
  87. {
  88. int sockfd = events[i].data.fd;//获取就绪事件描述符
  89. if( sockfd == listenfd )//监听端口有可读事件则表明有HTTP请求
  90. {
  91. struct sockaddr_in client_address;
  92. socklen_t client_addrlength = sizeof( client_address );
  93. int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );//建立客户连接
  94. if ( connfd < 0 )
  95. {
  96. printf( "errno is: %d\n", errno );
  97. continue;
  98. }
  99. if( http_conn::m_user_count >= MAX_FD )//HTTP客户数超过MAX_FD
  100. {
  101. show_error( connfd, "Internal server busy" );
  102. continue;
  103. }
  104. users[connfd].init( connfd, client_address );//利用connfd快速索引到http_conn任务类
  105. }
  106. else if( events[i].events & ( EPOLLRDHUP | EPOLLHUP | EPOLLERR ) )
  107. {
  108. users[sockfd].close_conn();
  109. }
  110. else if( events[i].events & EPOLLIN )//数据可读:
  111. {
  112. if( users[sockfd].read() )
  113. {
  114. pool->append( users + sockfd );
  115. }
  116. else
  117. {
  118. users[sockfd].close_conn();
  119. }
  120. }
  121. else if( events[i].events & EPOLLOUT )//数据可写,哪里注册了可写EPOLLOUT事件?http_conn工作任务类中write函数将那个http连接的描述符m_sockfd注册了可写事件
  122. {
  123. if( !users[sockfd].write() )//若该http_conn任务对应的http连接写失败了则关闭该http连接
  124. {
  125. users[sockfd].close_conn();
  126. }
  127. }
  128. else
  129. {}
  130. }
  131. }
  132. close( epollfd );
  133. close( listenfd );//这里要提醒的是listenfd由创建它的函数关闭,谁污染谁治理的原则
  134. delete [] users;
  135. delete pool;
  136. return 0;
  137. }

互斥量和信号量的简单封装:locker.h

[cpp] view plaincopy
  1. #ifndef LOCKER_H
  2. #define LOCKER_H
  3. #include <exception>
  4. #include <pthread.h>
  5. #include <semaphore.h>
  6. class sem
  7. {
  8. public:
  9. sem()
  10. {
  11. if( sem_init( &m_sem, 0, 0 ) != 0 )
  12. {
  13. throw std::exception();
  14. }
  15. }
  16. ~sem()
  17. {
  18. sem_destroy( &m_sem );
  19. }
  20. bool wait()
  21. {
  22. return sem_wait( &m_sem ) == 0;
  23. }
  24. bool post()
  25. {
  26. return sem_post( &m_sem ) == 0;
  27. }
  28. private:
  29. sem_t m_sem;
  30. };
  31. class locker
  32. {
  33. public:
  34. locker()
  35. {
  36. if( pthread_mutex_init( &m_mutex, NULL ) != 0 )
  37. {
  38. throw std::exception();
  39. }
  40. }
  41. ~locker()
  42. {
  43. pthread_mutex_destroy( &m_mutex );
  44. }
  45. bool lock()
  46. {
  47. return pthread_mutex_lock( &m_mutex ) == 0;
  48. }
  49. bool unlock()
  50. {
  51. return pthread_mutex_unlock( &m_mutex ) == 0;
  52. }
  53. private:
  54. pthread_mutex_t m_mutex;
  55. };
  56. class cond
  57. {
  58. public:
  59. cond()
  60. {
  61. if( pthread_mutex_init( &m_mutex, NULL ) != 0 )
  62. {
  63. throw std::exception();
  64. }
  65. if ( pthread_cond_init( &m_cond, NULL ) != 0 )
  66. {
  67. pthread_mutex_destroy( &m_mutex );
  68. throw std::exception();
  69. }
  70. }
  71. ~cond()
  72. {
  73. pthread_mutex_destroy( &m_mutex );
  74. pthread_cond_destroy( &m_cond );
  75. }
  76. bool wait()
  77. {
  78. int ret = 0;
  79. pthread_mutex_lock( &m_mutex );
  80. ret = pthread_cond_wait( &m_cond, &m_mutex );
  81. pthread_mutex_unlock( &m_mutex );
  82. return ret == 0;
  83. }
  84. bool signal()
  85. {
  86. return pthread_cond_signal( &m_cond ) == 0;
  87. }
  88. private:
  89. pthread_mutex_t m_mutex;
  90. pthread_cond_t m_cond;
  91. };
  92. #endif

压力测试程序:通常有IO复用、多线程、多进程实现压力测试,其中IO复用施压程度最高其它的要切换CPU

本程序是客户端通过命令行参数指定num个客户连接,并向服务端发送HTTP请求,服务端HTTP应答,客户端输出是请求和应答数据交替出现

[cpp] view plaincopy
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <assert.h>
  4. #include <unistd.h>
  5. #include <sys/types.h>
  6. #include <sys/epoll.h>
  7. #include <fcntl.h>
  8. #include <sys/socket.h>
  9. #include <netinet/in.h>
  10. #include <arpa/inet.h>
  11. #include <string.h>
  12. //向服务器发送HTTP请求内容
  13. static const char* request = "GET http://localhost/index.html HTTP/1.1\r\nConnection: keep-alive\r\n\r\nxxxxxxxxxxxx";
  14. int setnonblocking( int fd )//设置非阻塞描述符
  15. {
  16. int old_option = fcntl( fd, F_GETFL );
  17. int new_option = old_option | O_NONBLOCK;
  18. fcntl( fd, F_SETFL, new_option );
  19. return old_option;
  20. }
  21. void addfd( int epoll_fd, int fd )//添加描述符到事件表
  22. {
  23. epoll_event event;
  24. event.data.fd = fd;
  25. event.events = EPOLLOUT | EPOLLET | EPOLLERR;//可写事件
  26. epoll_ctl( epoll_fd, EPOLL_CTL_ADD, fd, &event );
  27. setnonblocking( fd );
  28. }
  29. bool write_nbytes( int sockfd, const char* buffer, int len )//向服务器写函数即发送HTTP请求
  30. {
  31. int bytes_write = 0;
  32. printf( "write out %d bytes to socket %d\n", len, sockfd );
  33. while( 1 ) //循环写直至写完一次buffer也就是HTTP requst
  34. {
  35. bytes_write = send( sockfd, buffer, len, 0 );
  36. if ( bytes_write == -1 )
  37. {
  38. return false;
  39. }
  40. else if ( bytes_write == 0 )
  41. {
  42. return false;
  43. }
  44. len -= bytes_write;
  45. buffer = buffer + bytes_write;
  46. if ( len <= 0 )
  47. {
  48. return true;
  49. }
  50. }
  51. }
  52. bool read_once( int sockfd, char* buffer, int len )//读一次,接收服务器发送来的HTTP应答
  53. {
  54. int bytes_read = 0;
  55. memset( buffer, '\0', len );
  56. bytes_read = recv( sockfd, buffer, len, 0 );
  57. if ( bytes_read == -1 )
  58. {
  59. return false;
  60. }
  61. else if ( bytes_read == 0 )
  62. {
  63. return false;
  64. }
  65. printf( "read in %d bytes from socket %d with content: %s\n", bytes_read, sockfd, buffer );
  66. return true;
  67. }
  68. void start_conn( int epoll_fd, int num, const char* ip, int port )//发起num个连接
  69. {
  70. int ret = 0;
  71. struct sockaddr_in address;
  72. bzero( &address, sizeof( address ) );
  73. address.sin_family = AF_INET;
  74. inet_pton( AF_INET, ip, &address.sin_addr );
  75. address.sin_port = htons( port );
  76. for ( int i = 0; i < num; ++i )
  77. {
  78. sleep( 1 );
  79. int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
  80. printf( "create 1 sock\n" );
  81. if( sockfd < 0 )
  82. {
  83. continue;
  84. }
  85. if (  connect( sockfd, ( struct sockaddr* )&address, sizeof( address ) ) == 0  )
  86. {
  87. printf( "build connection %d\n", i );
  88. addfd( epoll_fd, sockfd );//初始注册为可写事件
  89. }
  90. }
  91. }
  92. void close_conn( int epoll_fd, int sockfd )//关闭连接
  93. {
  94. epoll_ctl( epoll_fd, EPOLL_CTL_DEL, sockfd, 0 );
  95. close( sockfd );
  96. }
  97. int main( int argc, char* argv[] )
  98. {
  99. assert( argc == 4 );
  100. int epoll_fd = epoll_create( 100 );
  101. start_conn( epoll_fd, atoi( argv[ 3 ] ), argv[1], atoi( argv[2] ) );
  102. epoll_event events[ 10000 ];
  103. char buffer[ 2048 ];
  104. while ( 1 )
  105. {
  106. int fds = epoll_wait( epoll_fd, events, 10000, 2000 );//2000ms内等待最多10000个事件
  107. for ( int i = 0; i < fds; i++ )
  108. {
  109. int sockfd = events[i].data.fd;
  110. if ( events[i].events & EPOLLIN )//HTTP连接上可读事件即服务端发送给客户端HTTP回答报文
  111. {
  112. if ( ! read_once( sockfd, buffer, 2048 ) )//读取HTTP应答
  113. {
  114. close_conn( epoll_fd, sockfd );
  115. }
  116. struct epoll_event event;
  117. event.events = EPOLLOUT | EPOLLET | EPOLLERR;//更改为可写事件
  118. event.data.fd = sockfd;
  119. epoll_ctl( epoll_fd, EPOLL_CTL_MOD, sockfd, &event );//
  120. }
  121. else if( events[i].events & EPOLLOUT ) //可写事件初始就是可写
  122. {
  123. if ( ! write_nbytes( sockfd, request, strlen( request ) ) )//向服务端发送HTTP请求
  124. {
  125. close_conn( epoll_fd, sockfd );
  126. }
  127. struct epoll_event event;
  128. event.events = EPOLLIN | EPOLLET | EPOLLERR;//更改为可写事件
  129. event.data.fd = sockfd;
  130. epoll_ctl( epoll_fd, EPOLL_CTL_MOD, sockfd, &event );//这样做的目的是客户端发送HTTP请求和服务端HTTP回答交替出现
  131. }
  132. else if( events[i].events & EPOLLERR )
  133. {
  134. close_conn( epoll_fd, sockfd );
  135. }
  136. }
  137. }
  138. }

基于半同步/半反应堆线程池实现的HTTP解析服务端程序相关推荐

  1. 带线程池的socket客户端与服务端

    前言 socket(套接字),Socket和ServerSocket位于java.net包中,之前虽然对socket有过一些了解,但一直都是云里雾里的,特意仔细的学习了一个socket,用socket ...

  2. 半同步/半反应堆线程池实现

    半同步/半反应堆线程池:主线程监听listen socket和接收到的所有连接socket,当有客户端请求任务时,将任务对象插入到工作任务对象中:等待在任务队列上的工作线程通过竞争来取得任务对象并处理 ...

  3. 用Linux / C实现基于自动扩/减容线程池+epoll反应堆检测沉寂用户模型的服务器框架(含源码)

    用Linux/ C实现基于自动扩/减容线程池+epoll反应堆模型的服务器框架 前言 服务器端源码 客户端源码 自定义库 helper.c 和 helper.h helper.c helper.h M ...

  4. Linux网络编程 | 并发模式:半同步/半异步模式、领导者/追随者模式

    文章目录 同步与异步 半同步/半异步模式 变体:半同步/半反应堆模式 改进:更高效的半同步/半异步模式 领导者/追随者模式 组件 :句柄集.线程集.事件处理器 并发模式是指I/O处理单元和多个逻辑单元 ...

  5. Linux服务器 | 服务器模型与三个模块、两种并发模式:半同步/半异步、领导者/追随者

    文章目录 两种服务器模型及三个模块 C/S模型 P2P模型 I/O处理单元.逻辑单元.存储单元 并发 同步与异步 半同步/半异步模式 变体:半同步/半反应堆模式 改进:高效的半同步/半异步模式 领导者 ...

  6. 高山仰之可极,谈半同步/半异步网络并发模型

    0. 仰之弥高 2015年,在腾讯暑期实习期间,leader给我布置的一个任务是整理分析网络模型.虽然也有正常工作要做,但这个任务贯穿了整个实习期.后来实习结束的总结PPT上,这部分内容占到了一半篇幅 ...

  7. 两种并发模式:半同步半异步 领导者追随者

    介绍 半同步半异步     介绍     变体        半同步半反应堆        高效的半同步半反应堆  领导者追随者     介绍     实现       组成       过程   优 ...

  8. 半同步/半异步和领导者/追随者 有趣的解释

    转自:http://blog.csdn.net/jinchaoh/article/details/50427733 这不仅仅两个山贼的故事! 先介绍下关系: 下面开始讲故事: 1.领导者/追随者模型: ...

  9. 两种高效的并发模式:半同步/半异步和领导者/追随者

    这不仅仅两个山贼的故事! 先介绍下关系: 下面开始讲故事: 1.领导者/追随者模型: 故事: 话说一个地方有一群有组织无纪律的人从事山贼这个很有前途的职业. 一般就是有一个山贼在山路口察看,其他人在林 ...

最新文章

  1. boost::mpl::aux::template_arity相关用法的测试程序
  2. session机制和cookie机制
  3. 枚举:如何正确使用name()和toString()方法
  4. 我把这篇文章给女朋友看,她终于明白什么是「数据中台」了
  5. Python接口自动化测试系列文章汇总
  6. 汉明码---存储器校验(简单易懂详解)
  7. ubuntu mysql 内存满了_Ubuntu 下 mysql 卸载后重安装时遇到的问题
  8. 《深入浅出统计学》笔记二--第二章:集中趋势的量度,第三章:分散性与变异性的量度
  9. PDF文件转base64显示
  10. 玲听 | 蚂蚁金服布局区块链核心3问
  11. IT牛人进阶的必经之路
  12. 他是“自由软件”之父,天才程序员,史上最伟大的黑客!最后却黯然离场
  13. 3D Style Transfer
  14. QT自定义控件之车辆远近光灯
  15. Spring Boot 项目 - API 文档搜索引擎
  16. 动态规划统计正方形子矩阵
  17. 什么是rmi?为什么要使用rmi框架?
  18. 对WEB应用的粗浅理解
  19. 监控摄像头上滤光片的作用是什么
  20. 软件测试的底层逻辑思维是什么?

热门文章

  1. 系统垃圾清理.cmd
  2. 【C语言进阶深度学习记录】三十三 C语言中动态内存分配
  3. 【C语言进阶深度学习记录】二十二 指针的本质分析
  4. git基础-远程仓库的使用
  5. warning: expression result unuesd 可能原因是函数忘了加括号,
  6. Android WebView Long Press长按保存图片到手机
  7. 微信公众账户模拟登陆后的一系列操作
  8. C#控制台程序,发送邮件,可带附件
  9. 关于html和javascript在浏览器中的加载顺序问题的讨论(zz)
  10. WCF从理论到实践(14):WCF解决方案模板 (转)