Tinyhttpd for Windows

TinyHTTPd forWindows

前言

TinyHTTPd是一个开源的简易学习型的HTTP服务器,项目主页在:http://tinyhttpd.sourceforge.NET/,源代码下载:https://sourceforge.Net/projects/tinyhttpd/,因为是学习型的代码,已经有好多年没更新了,也没什么更新必要,整个代码才500多行,10多个函数,对于学习HTTP服务器的原理来说非常有帮助,把代码读一遍,再按照执行处理流程调试一下,基本上可以搞清楚Web服务器在收到静态页面请求和CGI请求的一些基本处理逻辑。源代码的注释我这里就不讲了,本身代码比较简单,而且网上这样的文章汗牛充栋,可以去后面的参考文档阅读下。

本文主要是将TinyHTTPd进行一些简单移植,使其可以在Windows上面运行调试,让只有Windows开发调试环境的小伙伴也能够学习学习。

修改明细

支持Windows部分

1、  Windows下的socket支持(微小改动,变量,宏调整等等)。

2、  接入基于Windows平台的一个微型线程池库(项目在用),对于每个新的http请求抛到线程池处理,源代码使用的是基于Linux的pthread。

3、  部分字符串比较函数修改为windows平台的对应支持函数,以及其它一些相应兼容性修改。

4、  CGI部分支持了Python脚本和Windows批处理脚本,其它的如果需要支持可以进行修改,另外,CGI部分目前实现比较粗糙,完全是为了体现一下CGI请求的原理(POST的CGI处理仅仅是把提交的数据返回给客户端显示)。

5、  CGI部分使用了匿名管道,匿名管道不支持异步读写数据,因此需要控制读写匿名管道的次数(建议仅读一次,并且CGI的返回字符长度不要超过2048字节)。

优化

1、  给客户端返回数据时,合并了需要发送的数据,使用send一次发送,而不是每次发送几个字符,不过这里没有进行send失败的错误处理,学习代码吧,如果是商用代码,send失败是需要重试的(当然,商用代码一般都是使用异步socket),这个地方之前作者分成多次发送的目的可能是为了体现网络数据传输的原理。

2、  合并了一些公用代码。

3、  代码里面直接写死了绑定80端口,如果需要由系统自动分配端口就把这句代码:u_short port = 80修改为u_short port = 0,绑死80端口是为了使用浏览器测试时比较方便。

bug修改

1、  cat函数里面使用fgets读取文件进行数据发送时,有可能发送不完整。

资源补充

1、  补充批处理的cgi支持和py脚本的cgi支持(都比较简单,需要注意的是py脚本支持需要本地安装python2.x的环境)。

测试情况

主页

URL:http://127.0.0.1/

其它静态页面

URL:http://127.0.0.1/detect.html

Python CGI

URL:http://127.0.0.1/cgipy?p.py

批处理 CGI

URL:http://127.0.0.1/cgibat?p.bat

POST CGI

URL:http://127.0.0.1/index.html

源代码

本来不想帖代码的,还是贴一点吧,工程下载请点这里

[cpp] view plain copy

  1. /* -------------------------------------------------------------------------
  2. //  文件名     :   tinyhttp.cpp
  3. //  创建者     :   magictong
  4. //  创建时间    :   2016/11/16 17:13:55
  5. //  功能描述    :   support windows of tinyhttpd, use mutilthread...
  6. //
  7. //  $Id: $
  8. // -----------------------------------------------------------------------*/
  9. /* J. David‘s webserver */
  10. /* This is a simple webserver.
  11. * Created November 1999 by J. David Blackstone.
  12. * CSE 4344 (Network concepts), Prof. Zeigler
  13. * University of Texas at Arlington
  14. */
  15. /* This program compiles for Sparc Solaris 2.6.
  16. * To compile for Linux:
  17. *  1) Comment out the #include <pthread.h> line.
  18. *  2) Comment out the line that defines the variable newthread.
  19. *  3) Comment out the two lines that run pthread_create().
  20. *  4) Uncomment the line that runs accept_request().
  21. *  5) Remove -lsocket from the Makefile.
  22. */
  23. #include "stdafx.h"
  24. #include "windowcgi.h"
  25. #include "ThreadProc.h"
  26. #include <stdio.h>
  27. #include <ctype.h>
  28. #include <stdlib.h>
  29. #include <string.h>
  30. #include <sys/stat.h>
  31. #include <sys/types.h>
  32. #include <WinSock2.h>
  33. #pragma comment(lib, "wsock32.lib")
  34. #pragma warning(disable : 4267)
  35. #define ISspace(x) isspace((int)(x))
  36. #define SERVER_STRING "Server: tinyhttp /0.1.0\r\n"
  37. // -------------------------------------------------------------------------
  38. // -------------------------------------------------------------------------
  39. // 类名       : CTinyHttp
  40. // 功能       :
  41. // 附注       :
  42. // -------------------------------------------------------------------------
  43. class CTinyHttp
  44. {
  45. public:
  46. typedef struct tagSocketContext
  47. {
  48. SOCKET socket_Client;
  49. tagSocketContext() : socket_Client(-1) {}
  50. } SOCKET_CONTEXT, *PSOCKET_CONTEXT;
  51. /**********************************************************************/
  52. /* A request has caused a call to accept() on the server port to
  53. * return.  Process the request appropriately.
  54. * Parameters: the socket connected to the client */
  55. /**********************************************************************/
  56. void accept_request(nilstruct&, SOCKET_CONTEXT& socket_context)
  57. {
  58. printf("Tid[%u] accept_request\n", (unsigned int)::GetCurrentThreadId());
  59. #ifdef _DEBUG
  60. // 测试是否可以并发
  61. ::Sleep(200);
  62. #endif
  63. char buf[1024] = {0};
  64. int numchars = 0;
  65. char method[255] = {0};
  66. char url[255] = {0};
  67. char path[512] = {0};
  68. int i = 0, j = 0;
  69. struct stat st;
  70. int cgi = 0;      /* becomes true if server decides this is a CGI program */
  71. char* query_string = NULL;
  72. SOCKET client = socket_context.socket_Client;
  73. numchars = get_line(client, buf, sizeof(buf));
  74. // 获取HTTP的请求方法名
  75. while (j < numchars && !ISspace(buf[j]) && (i < sizeof(method) - 1))
  76. {
  77. method[i] = buf[j];
  78. i++; j++;
  79. }
  80. method[i] = ‘\0‘;
  81. if (_stricmp(method, "GET") != 0 && _stricmp(method, "POST"))      // 只处理GET请求
  82. {
  83. if (numchars > 0)
  84. {
  85. discardheaders(client);
  86. }
  87. unimplemented(client);
  88. closesocket(client);
  89. return;
  90. }
  91. if (_stricmp(method, "POST") == 0)
  92. cgi = 1; // POST请求,当成CGI处理
  93. // 获取到URL路径,存放到url字符数组里面
  94. i = 0;
  95. while (ISspace(buf[j]) && (j < sizeof(buf)))
  96. {
  97. j++;
  98. }
  99. while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
  100. {
  101. url[i] = buf[j];
  102. i++;
  103. j++;
  104. }
  105. url[i] = ‘\0‘;
  106. if (_stricmp(method, "GET") == 0)
  107. {
  108. query_string = url;
  109. while ((*query_string != ‘?‘) && (*query_string != ‘\0‘))
  110. query_string++;
  111. if (*query_string == ‘?‘)
  112. {
  113. // URL带参数,当成CGI处理
  114. cgi = 1;
  115. *query_string = ‘\0‘;
  116. query_string++;
  117. }
  118. }
  119. sprintf_s(path, 512, "htdocs%s", url);
  120. if (path[strlen(path) - 1] == ‘/‘)
  121. {
  122. // 补齐
  123. strcat_s(path, 512, "index.html");
  124. }
  125. if (stat(path, &st) == -1)
  126. {
  127. // 文件不存在
  128. if (numchars > 0)
  129. {
  130. discardheaders(client);
  131. }
  132. not_found(client);
  133. }
  134. else
  135. {
  136. // 如果是文件夹则补齐
  137. if ((st.st_mode & S_IFMT) == S_IFDIR)
  138. strcat_s(path, 512, "/index.html");
  139. if (st.st_mode & S_IEXEC)
  140. cgi = 1; // 具有可执行权限
  141. if (!cgi)
  142. {
  143. serve_file(client, path);
  144. }
  145. else
  146. {
  147. execute_cgi(client, path, method, query_string);
  148. }
  149. }
  150. closesocket(client);
  151. }
  152. /**********************************************************************/
  153. /* Execute a CGI script.  Will need to set environment variables as
  154. * appropriate.
  155. * Parameters: client socket descriptor
  156. *             path to the CGI script */
  157. /**********************************************************************/
  158. void execute_cgi(SOCKET client, const char *path, const char* method, const char* query_string)
  159. {
  160. char buf[1024] = {0};
  161. int cgi_output[2] = {0};
  162. int cgi_input[2] = {0};
  163. int i = 0;
  164. char c = 0;
  165. int numchars = 1;
  166. int content_length = -1;
  167. buf[0] = ‘A‘; buf[1] = ‘\0‘;
  168. if (_stricmp(method, "GET") == 0)
  169. {
  170. discardheaders(client);
  171. }
  172. else    /* POST */
  173. {
  174. numchars = get_line(client, buf, sizeof(buf));
  175. while ((numchars > 0) && strcmp("\n", buf))
  176. {
  177. buf[15] = ‘\0‘;
  178. if (_stricmp(buf, "Content-Length:") == 0)
  179. {
  180. content_length = atoi(&(buf[16]));
  181. }
  182. numchars = get_line(client, buf, sizeof(buf));
  183. }
  184. if (content_length == -1)
  185. {
  186. bad_request(client);
  187. return;
  188. }
  189. }
  190. CWinCGI cgi;
  191. if (!cgi.Exec(path, query_string))
  192. {
  193. bad_request(client);
  194. return;
  195. }
  196. //SOCKET client, const char *path, const char* method, const char* query_string
  197. if (_stricmp(method, "POST") == 0)
  198. {
  199. for (i = 0; i < content_length; i++)
  200. {
  201. recv(client, &c, 1, 0);
  202. cgi.Write((PBYTE)&c, 1);
  203. }
  204. c = ‘\n‘;
  205. cgi.Write((PBYTE)&c, 1);
  206. }
  207. cgi.Wait();
  208. char outBuff[2048] = {0};
  209. cgi.Read((PBYTE)outBuff, 2047);
  210. send(client, outBuff, strlen(outBuff), 0);
  211. }
  212. /**********************************************************************/
  213. /* Put the entire contents of a file out on a socket.  This function
  214. * is named after the UNIX "cat" command, because it might have been
  215. * easier just to do something like pipe, fork, and exec("cat").
  216. * Parameters: the client socket descriptor
  217. *             FILE pointer for the file to cat */
  218. /**********************************************************************/
  219. void cat(SOCKET client, FILE *resource)
  220. {
  221. char buf[1024] = {0};
  222. do
  223. {
  224. fgets(buf, sizeof(buf), resource);
  225. size_t len = strlen(buf);
  226. if (len > 0)
  227. {
  228. send(client, buf, len, 0);
  229. }
  230. } while (!feof(resource));
  231. }
  232. /**********************************************************************/
  233. /* Print out an error message with perror() (for system errors; based
  234. * on value of errno, which indicates system call errors) and exit the
  235. * program indicating an error. */
  236. /**********************************************************************/
  237. void error_die(const char *sc)
  238. {
  239. perror(sc);
  240. exit(1);
  241. }
  242. /**********************************************************************/
  243. /* Get a line from a socket, whether the line ends in a newline,
  244. * carriage return, or a CRLF combination.  Terminates the string read
  245. * with a null character.  If no newline indicator is found before the
  246. * end of the buffer, the string is terminated with a null.  If any of
  247. * the above three line terminators is read, the last character of the
  248. * string will be a linefeed and the string will be terminated with a
  249. * null character.
  250. * Parameters: the socket descriptor
  251. *             the buffer to save the data in
  252. *             the size of the buffer
  253. * Returns: the number of bytes stored (excluding null) */
  254. /**********************************************************************/
  255. int get_line(SOCKET sock, char *buf, int size)
  256. {
  257. int i = 0;
  258. char c = ‘\0‘;
  259. int n;
  260. while ((i < size - 1) && (c != ‘\n‘))
  261. {
  262. n = recv(sock, &c, 1, 0);
  263. /* DEBUG printf("%02X\n", c); */
  264. if (n > 0)
  265. {
  266. if (c == ‘\r‘)
  267. {
  268. n = recv(sock, &c, 1, MSG_PEEK);
  269. /* DEBUG printf("%02X\n", c); */
  270. if ((n > 0) && (c == ‘\n‘))
  271. {
  272. recv(sock, &c, 1, 0);
  273. }
  274. else
  275. {
  276. c = ‘\n‘;
  277. }
  278. }
  279. buf[i] = c;
  280. i++;
  281. }
  282. else
  283. {
  284. c = ‘\n‘;
  285. }
  286. }
  287. buf[i] = ‘\0‘;
  288. return(i);
  289. }
  290. /**********************************************************************/
  291. /* Return the informational HTTP headers about a file. */
  292. /* Parameters: the socket to print the headers on
  293. *             the name of the file */
  294. /**********************************************************************/
  295. void headers(SOCKET client, const char *filename)
  296. {
  297. (void)filename;
  298. char* pHeader = "HTTP/1.0 200 OK\r\n"\
  299. SERVER_STRING \
  300. "Content-Type: text/html\r\n\r\n";
  301. send(client, pHeader, strlen(pHeader), 0);
  302. }
  303. /**********************************************************************/
  304. /* Give a client a 404 not found status message. */
  305. /**********************************************************************/
  306. void not_found(SOCKET client)
  307. {
  308. char* pResponse = "HTTP/1.0 404 NOT FOUND\r\n"\
  309. SERVER_STRING \
  310. "Content-Type: text/html\r\n\r\n"\
  311. "<HTML><TITLE>Not Found</TITLE>\r\n"\
  312. "<BODY><P>The server could not fulfill\r\n"\
  313. "your request because the resource specified\r\n"\
  314. "is unavailable or nonexistent.\r\n"\
  315. "</BODY></HTML>\r\n";
  316. send(client, pResponse, strlen(pResponse), 0);
  317. }
  318. /**********************************************************************/
  319. /* Inform the client that the requested web method has not been
  320. * implemented.
  321. * Parameter: the client socket */
  322. /**********************************************************************/
  323. void unimplemented(SOCKET client)
  324. {
  325. char* pResponse = "HTTP/1.0 501 Method Not Implemented\r\n"\
  326. SERVER_STRING \
  327. "Content-Type: text/html\r\n\r\n"\
  328. "<HTML><HEAD><TITLE>Method Not Implemented\r\n"\
  329. "</TITLE></HEAD>\r\n"\
  330. "<BODY><P>HTTP request method not supported.</P>\r\n"\
  331. "</BODY></HTML>\r\n";
  332. send(client, pResponse, strlen(pResponse), 0);
  333. }
  334. /**********************************************************************/
  335. /* Inform the client that a CGI script could not be executed.
  336. * Parameter: the client socket descriptor. */
  337. /**********************************************************************/
  338. void cannot_execute(SOCKET client)
  339. {
  340. char* pResponse = "HTTP/1.0 500 Internal Server Error\r\n"\
  341. "Content-Type: text/html\r\n\r\n"\
  342. "<P>Error prohibited CGI execution.</P>\r\n";
  343. send(client, pResponse, strlen(pResponse), 0);
  344. }
  345. /**********************************************************************/
  346. /* Inform the client that a request it has made has a problem.
  347. * Parameters: client socket */
  348. /**********************************************************************/
  349. void bad_request(SOCKET client)
  350. {
  351. char* pResponse = "HTTP/1.0 400 BAD REQUEST\r\n"\
  352. "Content-Type: text/html\r\n\r\n"\
  353. "<P>Your browser sent a bad request, such as a POST without a Content-Length.</P>\r\n";
  354. send(client, pResponse, strlen(pResponse), 0);
  355. }
  356. /**********************************************************************/
  357. /* Send a regular file to the client.  Use headers, and report
  358. * errors to client if they occur.
  359. * Parameters: a pointer to a file structure produced from the socket
  360. *              file descriptor
  361. *             the name of the file to serve */
  362. /**********************************************************************/
  363. void serve_file(SOCKET client, const char *filename)
  364. {
  365. FILE *resource = NULL;
  366. discardheaders(client);
  367. fopen_s(&resource, filename, "r");
  368. if (resource == NULL)
  369. {
  370. not_found(client);
  371. }
  372. else
  373. {
  374. headers(client, filename);
  375. cat(client, resource);
  376. }
  377. fclose(resource);
  378. }
  379. // -------------------------------------------------------------------------
  380. // 函数       : discardheaders
  381. // 功能       : 清除http头数据(从网络中全部读出来)
  382. // 返回值  : void
  383. // 参数       : SOCKET client
  384. // 附注       :
  385. // -------------------------------------------------------------------------
  386. void discardheaders(SOCKET client)
  387. {
  388. char buf[1024] = {0};
  389. int numchars = 1;
  390. while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
  391. {
  392. numchars = get_line(client, buf, sizeof(buf));
  393. }
  394. }
  395. /**********************************************************************/
  396. /* This function starts the process of listening for web connections
  397. * on a specified port.  If the port is 0, then dynamically allocate a
  398. * port and modify the original port variable to reflect the actual
  399. * port.
  400. * Parameters: pointer to variable containing the port to connect on
  401. * Returns: the socket */
  402. /**********************************************************************/
  403. SOCKET startup(u_short* port)
  404. {
  405. SOCKET httpd = 0;
  406. struct sockaddr_in name = {0};
  407. httpd = socket(AF_INET, SOCK_STREAM, 0);
  408. if (httpd == INVALID_SOCKET)
  409. {
  410. error_die("startup socket");
  411. }
  412. name.sin_family = AF_INET;
  413. name.sin_port = htons(*port);
  414. name.sin_addr.s_addr = inet_addr("127.0.0.1");
  415. if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
  416. {
  417. error_die("startup bind");
  418. }
  419. if (*port == 0)  /* if dynamically allocating a port */
  420. {
  421. int namelen = sizeof(name);
  422. if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
  423. {
  424. error_die("getsockname");
  425. }
  426. *port = ntohs(name.sin_port);
  427. }
  428. if (listen(httpd, 5) < 0)
  429. {
  430. error_die("listen");
  431. }
  432. return httpd;
  433. }
  434. }; // End Class CTinyHttp
  435. int _tmain(int argc, _TCHAR* argv[])
  436. {
  437. SOCKET server_sock = INVALID_SOCKET;
  438. //u_short port = 0;
  439. u_short port = 80;
  440. struct sockaddr_in client_name = {0};
  441. int client_name_len = sizeof(client_name);
  442. typedef CMultiTaskThreadPoolT<CTinyHttp, CTinyHttp::SOCKET_CONTEXT, nilstruct, 5, CComMultiThreadModel::AutoCriticalSection> CMultiTaskThreadPool;
  443. CTinyHttp tinyHttpSvr;
  444. // init socket
  445. WSADATA wsaData = {0};
  446. WSAStartup(MAKEWORD(2, 2), &wsaData);
  447. server_sock = tinyHttpSvr.startup(&port);
  448. printf("httpd running on port: %d\n", port);
  449. CMultiTaskThreadPool m_threadpool(&tinyHttpSvr, &CTinyHttp::accept_request);
  450. while (1)
  451. {
  452. CTinyHttp::SOCKET_CONTEXT socket_context;
  453. socket_context.socket_Client = accept(server_sock, (struct sockaddr *)&client_name, &client_name_len);
  454. if (socket_context.socket_Client == INVALID_SOCKET)
  455. {
  456. tinyHttpSvr.error_die("accept");
  457. }
  458. printf("Tid[%u] accetp new connect: %u\n", (unsigned int)::GetCurrentThreadId(), (unsigned int)socket_context.socket_Client);
  459. m_threadpool.AddTask(socket_context);
  460. }
  461. // can not to run this
  462. m_threadpool.EndTasks();
  463. closesocket(server_sock);
  464. WSACleanup();
  465. return 0;
  466. }
  467. // -------------------------------------------------------------------------
  468. // $Log: $

参考文档

[1] tinyhttp源码阅读(注释) http://www.cnblogs.com/oloroso/p/5459196.html

[2] 【源码剖析】tinyhttpd —— C 语言实现最简单的 HTTP 服务器http://blog.csdn.net/jcjc918/article/details/42129311

[3] Tinyhttp源码分析http://blog.csdn.net/yzhang6_10/article/details/51534409

[4] tinyhttpd源码详解http://blog.csdn.net/baiwfg2/article/details/45582723

[5] CGI介绍 http://www.jdon.com/idea/cgi.htm

http://blog.csdn.net/magictong/article/details/53201038

时间: 2024-08-11 03:28:39

Tinyhttpd for Windows的相关文章

Windows API参考大全新编

书名:新编Windows API参考大全 作者:本书编写组 页数:981页 开数:16开 字数:2392千字 出版日期:2000年4月第二次印刷 出版社:电子工业出版社 书号:ISBN 7-5053-5777-8 定价:98.00元 内容简介 作为Microsoft 32位平台的应用程序编程接口,Win32 API是从事Windows应用程序开发所必备的.本书首先对Win32 API函数做完整的概述:然后收录五大类函数:窗口管理.图形设备接口.系统服务.国际特性以及网络服务:在附录部分,讲解如何

开源学习:tinyhttpd

tinyhttpd 算是轻量级的http服务器,原版是linux代码,支持cgi脚本,我改了windows版本,去掉cgi脚本支持.添加了支持二级制文件下载,这样可以在本地测试下载东西了. // tinyhttpd.cpp : 定义控制台应用程序的入口点.// #include "stdafx.h"#include "windows.h"#include "winsock2.h"#include "WS2tcpip.h"#in

Tinyhttpd精读解析

首先,本人刚刚开始开源代码精读,写的不对的地方,大家轻拍,一起进步.本文是对Tinyhttpd的一次精读,大家每天都在用着http服务,很多人也一直活跃在上层,使用IIS.Apache等,大家是否想看看http服务器大概是怎么运作的,通过一个500多行的源码加上完整的注释,和大家逛一逛http服务器.Tinyhttpd真的非常适合阅读尤其是刚入门的,清晰的代码,简单的makefile...其实有很多分析tinyghttpd的,这边抱着人家写的是人家,自己写的才是自己的态度,写的尽量详细,尽量简单

Windows Server定时重启任务制定

[本篇以Windows Server 2012 R2为例] 第一步:编写重启脚步 其实就是一句话:shutdown /r 其他shutdown命令参考可以使用shutdown /?查阅 第二步:设置任务计划程序 1.再开始-所有应用中找到任务计划程序 2.展开任务计划程序库,这里对任务计划程序做了很多的分类,我们找到System Manager类,在此类下创建自动重启系统任务 3.选择窗口右侧的创建任务(也可以使用创建基本任务,它是以向导的方法创建) 4.常规页面用于定义任务名称及执行任务的用户

windows安装TortoiseGit详细使用教程【基础篇】

环境:win8.1 64bit 安装准备: 首先你得安装windows下的git msysgit1.9.5 安装版本控制器客户端tortoisegit  tortoisegit1.8.12.0 [32和64别下载错,不习惯英文的朋友,也可以下个语言包] 一.安装图解: 先安装GIT[一路默认即可] 安装好git以后,右键,会发现菜单多了几项关于GIT的选项 2.安装tortoisegit[一路默认即可] 安装好以后,右键,会发现菜单多了几项关于tortoisegit的选项 到此,安装算完成了,相

Windows下尝试PHP7提示丢失VCRUNTIME140.DLL的问题解决

前天PHP7.0.0正式版发布了,有一些比较好的改进,官方也说速度比php5.6快了两倍,性能上有了很大提升,并且也发布了从php5.x向php7迁移的问题,所以今后php网站迁移后能够大幅度的提升网站性能,所以为了尝鲜我也去php官网下载了7.0的版本,通过命令行进行独立的测试,下载zip包后解压出来,下载后进入目录,将php.ini-development改为php.ini其余的参数暂时不用修改,然后在当前目录下新建test.php,输入简单的代码: 1 <?php 2 echo "H

AD 脚本kixtart运用之三(添加windows共享打印机)

在http://windyma.blog.51cto.com/661702/1967027文章,已做好用户脚本基础上 在脚本文件kixtart.kix里添加如下内容: --------------------------------------- IF INGROUP ("Color_Printer") If AddPrinterConnection ("\\zsprinter.nccn.int\NEO-Color-Printer") = 0 ? "Add

Install Hyper-V on Windows 10

? Enable Hyper-V to create virtual machines on Windows 10.Hyper-V can be enabled in many ways including using the Windows 10 control panel, PowerShell (my favorite) or using the Deployment Imaging Servicing and Management tool (DISM). This documents

Windows Git+TortoiseGit简易使用教程

转载自 http://blog.csdn.net/jarelzhou/article/details/8256139 官方教程:http://tortoisegit.org/docs/tortoisegit/(英文版) 为什么选择Git 效率 很多人有一种习惯吧,什么软件都要最新的,最好的.其实吧,软件就是工具,生产力工具,为的是提高我们的生产力.如果现有的工具已经可以满足生产力要求了,就没有必要换了.生产效率高低应当是选择工具的第一位. 历史 开源世界的版本控制系统,经历了这么几代: 第一代,