博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
自己写的Web服务器
阅读量:4549 次
发布时间:2019-06-08

本文共 23307 字,大约阅读时间需要 77 分钟。

  自己写一个使用Http协议的服务器。在谷歌搜了一下,发现其实.NET Framework里面本身提供了HttpListener类,看别人的博文介绍是它是对Socket的简单封装,也有一些人没有用这个类,还是直接用Socekt写了服务器。说是Socket的扩展性反而比较好。HttpListener毕竟是微软封装好的,安全性应该一般会比用Socket写的要高,如果大牛写的就不同了,像我这等水货,其实还是用HttpListener要好一些。但也是个尝试,也是学习,我尝试用Socket写。虽然说是基于Socket,但实际上用的Socket的连接池。连接池的实现细节在上一篇博文《》有介绍。

  写之前肯定看过别人的博文,看了这篇《》,是翻译老外的博文的。既然是用到Http协议,那肯定要对它有一定的了解,至少懂得看他它的请求头和响应头吧。本人了解的不多,但知道的都能在写这个程序里用得上。

  用谷歌浏览器随便获取了请求和响应的消息结构,列出来简单看一下

1 GET /page/130970/ HTTP/1.12 Host: kb.cnblogs.com3 Connection: keep-alive4 Cache-Control: max-age=05 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.86 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.317 Accept-Encoding: gzip,deflate,sdch8 Accept-Language: zh-CN,zh;q=0.89 Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3

  这个有些部分被我删了,因为篇幅太长了。这个是GET的请求结构,写这个服务器要用到的信息只是第一行请求行而已。由于是GET请求,请求的方法是GET,请求要获取的资源就是“/page/130970”这段。暂时用到的信息就之后这两个而已。

  下面这个则是POST的请求消息结构

1 POST /ws/SideRightList.asmx/GetList HTTP/1.1 2 Host: kb.cnblogs.com 3 Connection: keep-alive 4 Content-Length: 35 5 Accept: application/json, text/javascript, */*; q=0.01 6 Origin: http://kb.cnblogs.com 7 X-Requested-With: XMLHttpRequest 8 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31 9 Content-Type: application/json; charset=UTF-810 Referer: http://kb.cnblogs.com/page/130970/11 Accept-Encoding: gzip,deflate,sdch12 Accept-Language: zh-CN,zh;q=0.813 Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3

  同样以POST这个请求方法作为开头,请求的资源就同样的跟在后面,但是请求的参数就跟在请求头的下两行,可是这里的没有带参数,参数的长度可以在请求头的Content-Length中获得,如这里的参数长度就是35。

1 HTTP/1.1 200 OK 2 Server: Tengine 3 Date: Fri, 26 Apr 2013 05:50:11 GMT 4 Content-Type: text/html; charset=utf-8 5 Transfer-Encoding: chunked 6 Connection: keep-alive 7 Vary: Accept-Encoding 8 Cache-Control: public, max-age=300 9 Expires: Fri, 26 Apr 2013 05:54:43 GMT10 Last-Modified: Fri, 26 Apr 2013 05:49:43 GMT11 X-AspNet-Version: 4.0.3031912 X-Powered-By: ASP.NET13 X-UA-Compatible: IE=edge14 Content-Encoding: gzip

当服务器接收到浏览器发送的请求处理完之后,要对浏览器进行响应,响应的消息结构如上所示,这里要提供的参数不是很多,首先是响应的状态码,下面则是状态码的类别

  • 1XX  提示信息 - 表示请求已被成功接收,继续处理
  • 2XX  成功 - 表示请求已被成功接收,理解,接受
  • 3XX  重定向 - 要完成请求必须进行更进一步的处理
  • 4XX  客户端错误 -  请求有语法错误或请求无法实现
  • 5XX  服务器端错误 -   服务器未能实现合法的请求

这里用到的状态码有三个 200 OK,404 Not Found,501 Not Implemented。

Server则是服务器的名称,Content-Length则是响应内容的长度,Content-Type是服务器定它们的响应中的内容的类型,也称为MIME,我在以往常见的是这几个

  • text/html
  • text/xml
  • text/plain
  • text/css
  • text/javascript

  HTTP的内容不讲太多了,知道这些足以写这个服务器了。在博客园的知识库了发现一篇文章讲HTTP协议的挺不错的,叫《》。

  吹完Http协议的,说回程序了,既然请求头都是一堆字符串,那现在可以把服务器的职能一份为二,一部分是面向网络的,是Socket那部分,刚好不久前完成了Socket的连接池,在这里可以用得上了;另一部分则是对接收到的请求进行处理,字符串处理为主,充其量就加上文件的IO处理。Socket那部分完成了,只要对连接池配置,使用就行了,剩下是对请求的处理和作出响应。

  这里我也是习惯性的定义了一些实体类,好让信息存取方便些。

  第一个则是服务器的配置信息类,配置过IIS的都知道配置服务器需要IP,端口,虚拟目录,有时候需要控制并发量,起始页。这里就简单地存放了这些信息。

 

1     public class ServerConfigEntity2     {3         public string IP { get; set; }//IP地址4         public int Port { get; set; }//端口号5         public int MaxConnect { get; set; }//最大并发量6         public string VirtualPath { get; set; }//虚拟目录7         public string DefaultPage { get; set; }//起始页8     }

 

  接着便是请求消息和响应消息了

1     class RequestHeader 2     { 3         public string ActionName { get; set; } 4         public string URL { get; set; } 5         public string Host { get; set; } 6         public string Accept { get; set; } 7         public string Connection { get; set; } 8         public string Accept_Language { get; set; } 9         public string User_Agent { get; set; }10         public string Accept_Encoding { get; set; }11 12         public string Form { get; set; }13         public int Content_Length { get; set; }14         public string Referer { get; set; }15         public string Content_Type { get; set; }16 17         public static RequestHeader ConvertRequestHander(string headerStr)18         {19             string regActionName = "GET|POST";20             string regURL = "(?<=GET|POST).*?(?=HTTP/1.1)";21             string regHost = @"(?<=Host\:\s).*(?=\r\n)";22             string regAccept = @"(?<=Accept\:\s).*(?=\r\n)";23             string regConnection = @"(?<=Connection\:\s).*(?=\r\n)";24             string regAcceptLanguage = @"(?<=Accept-Language\:\s).*(?=\r\n)";25             string regUserAgent = @"(?<=User-Agent\:\s).*(?=\r\n)";26             string regAcceptEncoding = @"(?<=Accept-Encoding\:\s).*(?=\r\n)";27 28             string regForm = @"(?<=\r\n\r\n).*";29             string regConntenLength = @"(?<=Connten-Length\:\s).*(?=\r\n)";30             string regRefere = @"(?<=Refere\:\s).*(?=\r\n)";31             string regContentType = @"(?<=Content-Type\:\s).*(?=\r\n)";32 33             RequestHeader hander = new RequestHeader();34             hander.ActionName = Regex.Match(headerStr, regActionName).Value;35             hander.URL = Regex.Match(headerStr, regURL).Value;36             hander.Host = Regex.Match(headerStr, regHost).Value;37             hander.Accept = Regex.Match(headerStr, regAccept).Value;38             hander.Connection = Regex.Match(headerStr, regConnection).Value;39             hander.Accept_Language = Regex.Match(headerStr, regAcceptLanguage).Value;40             hander.Accept_Encoding = Regex.Match(headerStr, regAcceptEncoding).Value;41             hander.User_Agent = Regex.Match(headerStr, regUserAgent).Value;42             string tempStr = Regex.Match(headerStr, regConntenLength).Value;43             hander.Content_Length = Convert.ToInt32(tempStr == "" ? "0" : tempStr);44             hander.Referer = Regex.Match(headerStr, regRefere).Value;45             hander.Content_Type = Regex.Match(headerStr, regContentType).Value;46             hander.Form = Regex.Match(headerStr, regForm).Value;47             return hander;48         }49     }

  这里用到了正则去提取请求消息的相应内容,虽然前面介绍时只是提到用到一点点信息,但是以后扩展开来说不能用到其他信息的,于是一概提取了。

1     class ResponseHeader 2     { 3         public string ResponseCode { get; set; } 4         public string Server { get; set; } 5         public int Content_Length { get; set; } 6         public string Connection { get; set; } 7         public string Content_Type { get; set; } 8  9         public override string ToString()10         {11             string result = string.Empty;12             result += "HTTP/1.1 " + this.ResponseCode + "\r\n";13             result += "Server: "+this.Server+"\r\n";14             result += "Content-Length: " + this.Content_Length + "\r\n";15             result += "Connection: "+this.Connection+"\r\n";16             result += "Content-Type: " + this.Content_Type + "\r\n\r\n";17             return result;18         }19 20         public string CreateErrorHtml()21         {22             string html = @"
{0}";23 html = html.Replace("{0}", "

My Web Server

{0}
");24 html = string.Format(html, this.ResponseCode);25 return html;26 }27 }

  响应消息这里重写了基类的ToString()方法,能比较方便的根据当前响应的信息转成字符串。还提供了一个生成错误页面内容的方法CreateErrorHtml()。

       接着到服务器的类了,类里面有以下的私有字段

1         private SocketPoolController _pool;//Socket连接池2         private Dictionary
_supportExtension;//支持的资源后缀3 private ServerConfigEntity config;//服务器的配置信息4 private bool _runFlag;//服务器启动标识

  类的构造函数,构造一个连接池,给支持的附上支持的后缀以及相对应的MIME。

1         public HttpProtocolServer(ServerConfigEntity config) 2         { 3             this.config = config; 4             _pool = new SocketPoolController(32768, config.MaxConnect); 5             _supportExtension = new Dictionary
() 6 { 7 { "htm", "text/html" }, 8 { "html", "text/html" }, 9 { "xml", "text/xml" },10 { "txt", "text/plain" },11 { "css", "text/css" },12 { "png", "image/png" },13 { "gif", "image/gif" },14 { "jpg", "image/jpg" },15 { "jpeg", "image/jpeg" },16 { "zip", "application/zip"},17 {
"js","text/javascript"},18 { "dll", "text/plain" }19 //{"aspx","text/html"}20 };21 _runFlag = false;22 }

外放的方法有两个,分别是启动服务器RunServer()和停止服务器StopServer()

  启动服务器要把连接池运行起来,同时给注册一个接收事件,以便在接收到浏览器的请求时做出响应。

1         public void RunServer()2         {3             if (_runFlag) return;4             _pool.OnReceive += new SocketPoolController.RecevieHandler(HandleRequest);5             _pool.RunPool(config.IP, config.Port);6             _runFlag = true;7         }

       关闭服务器只需要把连接池关闭了就行了

1         public void StopServer()2         {3             _pool.StopPool();4             _pool = null;5             _runFlag = false;6         }

接收到浏览器请求后处理的方法(也就是跟连接池注册的方法)如下

1         private void HandleRequest(string uid, string header) 2         { 3             RequestHeader request = RequestHeader.ConvertRequestHander(header); 4             ResponseHeader response = new ResponseHeader(); 5             response.Server = "My Test WebSite"; 6             response.Connection = "close"; 7  8             //暂时只支持POST和GET的请求,其他的请求就视为未实现,发501响应 9             if (request.ActionName != "GET" && request.ActionName != "POST")10             {11                 response.ResponseCode = "501 Not Implemented";12                 response.Content_Type = "text/html";13                 SendErrorResponse(uid, response);14                 return;15             }16 17             //对请求资源名称经行处理。主要是去除GET时带的参数,还有把斜杠换过来18             string fullURL = config.VirtualPath + request.URL;19             string fileName =(fullURL.Contains('?')? Regex.Match(fullURL, @".*(?=\?)").Value:fullURL).Replace('/','\\');20 21             //如果请求的只是一个斜杠的,那证明请求的是默认页面22             if (fileName == fullURL + "\\")23             {24                 //如果配置中有默认页的,发200的响应25                 string defaultFile = Path.Combine(config.VirtualPath, config.DefaultPage);26                 if (File.Exists(defaultFile))27                 {28                     response.Content_Type = "text/html";29                     response.ResponseCode = "200 OK";30                     SendResponse(uid, File.ReadAllText(defaultFile), response);31                     return;32                 }33                 //如果不存在的,当404处理了34                 else35                 {36                     response.ResponseCode = "404 Not Found";37                     response.Content_Type = "text/html";38                     SendErrorResponse(uid, response);39                     return;40                 }41             }42 43             //如果请求的资源不存在的,那就发送40444             FileInfo fileInfo = new FileInfo(fileName);45             if (!fileInfo.Exists)46             {47                 response.ResponseCode = "404 Not Found";48                 response.Content_Type = "text/html";49                 SendErrorResponse(uid, response);50                 return;51             }52 53             //如果请求的资源不在支持的范围内,也当作404了,感觉不是404的,貌似是403的54             string extension = fileInfo.Extension.TrimStart('.');55             if (string.IsNullOrEmpty(extension) || !_supportExtension.ContainsKey(extension))56             {57                 response.ResponseCode = "404 Not Found";58                 response.Content_Type = "text/html";59                 SendErrorResponse(uid, response);60                 return;61             }62 63             //既然也不是请求起始页的,也没发生上面列的错误的,就正常响应64             response.Content_Type = _supportExtension[extension];65             response.ResponseCode = "200 OK";66             FileStream fs =null;67             try68             {69                 fs = File.OpenRead(fileInfo.FullName);70                 byte[] datas = new byte[fileInfo.Length];71                 fs.Read(datas, 0, datas.Length);72                 SendResponse(uid, datas, response);73             }74             finally75             {76                 fs.Close();77                 fs.Dispose();78                 fs = null;79             }80             return;81         }

发送消息的方法有三个,都是内部方法

  • SendResponse(string uid,string content,ResponseHeader header)是原始版的,发送普通的响应。
  • SendResponse(string uid, byte[] content, ResponseHeader header)是重载过的,专门发送内容已经是byte[]的响应。
  • SendErrorResponse(string uid,ResponseHeader header)用于专门发送错误响应的,其实内部也是调用了SendResponse(string uid,string content,ResponseHeader header)方法。从发送消息的情况来看,总共利用Socket发了两次数据,第一次是发响应消息,第二次才是发响应内容。
1         private void SendErrorResponse(string uid,ResponseHeader header) 2         { 3             string errorPageContent = header.CreateErrorHtml(); 4             header.Content_Length = errorPageContent.Length; 5             SendResponse(uid, errorPageContent, header); 6         } 7  8         private void SendResponse(string uid,string content,ResponseHeader header) 9         {10             header.Content_Length = content.Length;11             _pool.SendMessage(uid, header.ToString());12             _pool.SendMessage(uid, content);13         }14 15         private void SendResponse(string uid, byte[] content, ResponseHeader header)16         {17             header.Content_Length = content.Length;18             _pool.SendMessage(uid, header.ToString());19             _pool.SendMessage(uid, content);20         }

 

  这个简单的Web服务器就完成了,测试了一下,发现效率比不上IIS,普通浏览一个页面察觉不出来,当下载几个文件时就有差别了,IIS的用了接近2秒的时间,而这个服务器去用了接近四秒的时间。不知是哪里慢了,可能是连接池处理得不好。下面提供了源码,要用到连接池的,连接池的代码这里不提供了,需的话可以在上一篇博文《》里获取好了,做这个Web服务器是为了抛砖引玉,不足的,错的,遗漏的东西还很多,希望各位园友多多指点,谢谢!

整份源码
1    class RequestHeader  2     {  3         public string ActionName { get; set; }  4         public string URL { get; set; }  5         public string Host { get; set; }  6         public string Accept { get; set; }  7         public string Connection { get; set; }  8         public string Accept_Language { get; set; }  9         public string User_Agent { get; set; } 10         public string Accept_Encoding { get; set; } 11  12         public string Form { get; set; } 13         public int Content_Length { get; set; } 14         public string Referer { get; set; } 15         public string Content_Type { get; set; } 16  17         public static RequestHeader ConvertRequestHander(string headerStr) 18         { 19             string regActionName = "GET|POST"; 20             string regURL = "(?<=GET|POST).*?(?=HTTP/1.1)"; 21             string regHost = @"(?<=Host\:\s).*(?=\r\n)"; 22             string regAccept = @"(?<=Accept\:\s).*(?=\r\n)"; 23             string regConnection = @"(?<=Connection\:\s).*(?=\r\n)"; 24             string regAcceptLanguage = @"(?<=Accept-Language\:\s).*(?=\r\n)"; 25             string regUserAgent = @"(?<=User-Agent\:\s).*(?=\r\n)"; 26             string regAcceptEncoding = @"(?<=Accept-Encoding\:\s).*(?=\r\n)"; 27  28             string regForm = @"(?<=\r\n\r\n).*"; 29             string regConntenLength = @"(?<=Connten-Length\:\s).*(?=\r\n)"; 30             string regRefere = @"(?<=Refere\:\s).*(?=\r\n)"; 31             string regContentType = @"(?<=Content-Type\:\s).*(?=\r\n)"; 32  33             RequestHeader hander = new RequestHeader(); 34             hander.ActionName = Regex.Match(headerStr, regActionName).Value; 35             hander.URL = Regex.Match(headerStr, regURL).Value; 36             hander.Host = Regex.Match(headerStr, regHost).Value; 37             hander.Accept = Regex.Match(headerStr, regAccept).Value; 38             hander.Connection = Regex.Match(headerStr, regConnection).Value; 39             hander.Accept_Language = Regex.Match(headerStr, regAcceptLanguage).Value; 40             hander.Accept_Encoding = Regex.Match(headerStr, regAcceptEncoding).Value; 41             hander.User_Agent = Regex.Match(headerStr, regUserAgent).Value; 42             string tempStr = Regex.Match(headerStr, regConntenLength).Value; 43             hander.Content_Length = Convert.ToInt32(tempStr == "" ? "0" : tempStr); 44             hander.Referer = Regex.Match(headerStr, regRefere).Value; 45             hander.Content_Type = Regex.Match(headerStr, regContentType).Value; 46             hander.Form = Regex.Match(headerStr, regForm).Value; 47             return hander; 48         } 49     } 50  51     class ResponseHeader 52     { 53         public string ResponseCode { get; set; } 54         public string Server { get; set; } 55         public int Content_Length { get; set; } 56         public string Connection { get; set; } 57         public string Content_Type { get; set; } 58  59         public override string ToString() 60         { 61             string result = string.Empty; 62             result += "HTTP/1.1 " + this.ResponseCode + "\r\n"; 63             result += "Server: "+this.Server+"\r\n"; 64             result += "Content-Length: " + this.Content_Length + "\r\n"; 65             result += "Connection: "+this.Connection+"\r\n"; 66             result += "Content-Type: " + this.Content_Type + "\r\n\r\n"; 67             return result; 68         } 69  70         public string CreateErrorHtml() 71         { 72             string html = @"
{0}"; 73 html = html.Replace("{0}", "

My Web Server

{0}
"); 74 html = string.Format(html, this.ResponseCode); 75 return html; 76 } 77 } 78 79 public class ServerConfigEntity 80 { 81 public string IP { get; set; } 82 public int Port { get; set; } 83 public int MaxConnect { get; set; } 84 public string VirtualPath { get; set; } 85 public string DefaultPage { get; set; } 86 } 87 88 public class HttpProtocolServer 89 { 90 private SocketPoolController _pool; 91 private Dictionary
_supportExtension; 92 private ServerConfigEntity config; 93 private bool _runFlag; 94 95 public HttpProtocolServer(ServerConfigEntity config) 96 { 97 this.config = config; 98 _pool = new SocketPoolController(32768, config.MaxConnect); 99 _supportExtension = new Dictionary
() 100 {101 { "htm", "text/html" },102      { "html", "text/html" },103      { "xml", "text/xml" },104      { "txt", "text/plain" },105      { "css", "text/css" },106      { "png", "image/png" },107      { "gif", "image/gif" },108      { "jpg", "image/jpg" },109      { "jpeg", "image/jpeg" },110      { "zip", "application/zip"},111 {
"js","text/javascript"},112      { "dll", "text/plain" },113 {
"aspx","text/html"}114 };115 _runFlag = false;116 }117 118 public void RunServer()119 {120 if (_runFlag) return;121 _pool.OnReceive += new SocketPoolController.RecevieHandler(HandleRequest);122 _pool.RunPool(config.IP, config.Port);123 _runFlag = true;124 }125 126 public void StopServer()127 {128 _pool.StopPool();129 _pool = null;130 _runFlag = false;131 }132 133 private void HandleRequest(string uid, string header)134 {135 RequestHeader request = RequestHeader.ConvertRequestHander(header);136 ResponseHeader response = new ResponseHeader();137 response.Server = "My Test WebSite";138 response.Connection = "close";139 140 //暂时只支持POST和GET的请求,其他的请求就视为未实现,发501响应141 if (request.ActionName != "GET" && request.ActionName != "POST")142 {143 response.ResponseCode = "501 Not Implemented";144 response.Content_Type = "text/html";145 SendErrorResponse(uid, response);146 return;147 }148 149 //对请求资源名称经行处理。主要是去除GET时带的参数,还有把斜杠换过来150 string fullURL = config.VirtualPath + request.URL;151 string fileName =(fullURL.Contains('?')? Regex.Match(fullURL, @".*(?=\?)").Value:fullURL).Replace('/','\\');152 153 //如果请求的只是一个斜杠的,那证明请求的是默认页面154 if (fileName == fullURL + "\\")155 {156 //如果配置中有默认页的,发200的响应157 string defaultFile = Path.Combine(config.VirtualPath, config.DefaultPage);158 if (File.Exists(defaultFile))159 {160 response.Content_Type = "text/html";161 response.ResponseCode = "200 OK";162 SendResponse(uid, File.ReadAllText(defaultFile), response);163 return;164 }165 //如果不存在的,当404处理了166 else167 {168 response.ResponseCode = "404 Not Found";169 response.Content_Type = "text/html";170 SendErrorResponse(uid, response);171 return;172 }173 }174 175 //如果请求的资源不存在的,那就发送404176 FileInfo fileInfo = new FileInfo(fileName);177 if (!fileInfo.Exists)178 {179 response.ResponseCode = "404 Not Found";180 response.Content_Type = "text/html";181 SendErrorResponse(uid, response);182 return;183 }184 185 //如果请求的资源不在支持的范围内,也当作404了,感觉不是404的,貌似是403的186 string extension = fileInfo.Extension.TrimStart('.');187 if (string.IsNullOrEmpty(extension) || !_supportExtension.ContainsKey(extension))188 {189 response.ResponseCode = "404 Not Found";190 response.Content_Type = "text/html";191 SendErrorResponse(uid, response);192 return;193 }194 195 //既然也不是请求起始页的,也没发生上面列的错误的,就正常响应196 response.Content_Type = _supportExtension[extension];197 response.ResponseCode = "200 OK";198 FileStream fs =null;199 try200 {201 fs = File.OpenRead(fileInfo.FullName);202 byte[] datas = new byte[fileInfo.Length];203 fs.Read(datas, 0, datas.Length);204 SendResponse(uid, datas, response);205 }206 finally207 {208 fs.Close();209 fs.Dispose();210 fs = null;211 }212 return;213 }214 215 private void SendErrorResponse(string uid,ResponseHeader header)216 {217 string errorPageContent = header.CreateErrorHtml();218 header.Content_Length = errorPageContent.Length;219 SendResponse(uid, errorPageContent, header);220 }221 222 private void SendResponse(string uid,string content,ResponseHeader header)223 {224 header.Content_Length = content.Length;225 _pool.SendMessage(uid, header.ToString());226 _pool.SendMessage(uid, content);227 }228 229 private void SendResponse(string uid, byte[] content, ResponseHeader header)230 {231 header.Content_Length = content.Length;232 _pool.SendMessage(uid, header.ToString());233 _pool.SendMessage(uid, content);234 }235 }

 对本服务器的更改情况,将在下一篇博文《》列出

转载于:https://www.cnblogs.com/HopeGi/archive/2013/04/27/3046202.html

你可能感兴趣的文章
RocketMQ 简单梳理 及 集群部署笔记
查看>>
是自己的事情了
查看>>
20155227 《信息安全系统设计基础》课程总结
查看>>
char型变量中能不能存贮一个中文汉字?为什么?
查看>>
php header解决跨域
查看>>
git 乌龟记住密码
查看>>
从零开始搭建高性能高可用Tomcat服务器
查看>>
js class类名操作
查看>>
HDU5441 Travel (离线操作+并查集)
查看>>
idea主题包下载(推荐)
查看>>
Ubuntu 16.04 source.list 更新源
查看>>
Oracle数据库高效sql语句的整理
查看>>
01_中文编程 银弹?
查看>>
线性筛素数
查看>>
Chrome浏览器记住密码后input框黄色背景且背景图片不显示的问题
查看>>
python——函数 14、global与nonlocal
查看>>
代理商与经销商的区别
查看>>
关于C/C++内存管理malloc的一些注意事项
查看>>
@SuppressWarnings注解的使用
查看>>
Leaf Sets CodeForces - 1042F (树,最小划分)
查看>>