实现简单Web服务器
作者:追风剑情 发布于:2016-5-17 18:45 分类:C#
HTTP 1.1支持持久连接,即客户端和服务器建立连接后,可以发送请求和接收应答,然后迅速发送另一个请求和接收另一个应答。同时,持久连接也使得在得到上一个请求的应答之前可以发送多个请求,这是HTTP 1.1与HTTP 1.0明显不同的地方。
除此之外,HTTP 1.1可以发送的请求类型也比HTTP 1.0多。
HTTP 1.1提供的请求方法 (HTTP 1.0仅定义了GET、POST、HEAD) |
|
请求的方法名 | 说明 |
GET | 请求获取特定的资源,例如,请求一个Web页面 |
POST | 请求向指定资源提交数据进行处理(例如,提交表单或者上传文件),请求的数据被包含在请求体中 |
PUT | 向指定资源位置上传最新内容,例如,请求存储一个Web页面 |
HEAD | 向服务器请求获取与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。例如,可以使用HEAD请求来传递认证信息。可以使用HEAD请求对资源有效性进行检查。 |
DELETE | 请求删除指定的资源 |
OPTIONS | 返回服务器针对特定资源所支持的HTTP请求方法 |
TRACE | 回显服务器收到的请求 |
CONNECT | 预留给能够将连接改为管道方式的代理服务器 |
HTTP常用状态码 | |
状态码 | 说明 |
200 OK | 找到了该资源,并且一切正常 |
304 NOT MODIFIED | 该资源在上次请求之后没有任何修改。这通常用于浏览器的缓存机制 |
401 UNAUTHORIZED | 客户端无权访问该资源。这通常会使得浏览器要求用户输入用户名和密码,以登录到服务器 |
403 FORBIDDEN | 客户端未授权,这通常是在401之后输入了不正确的用户名或密码 |
404 NOT FOUND | 在指定的位置不存在所申请的资源 |
405 Method Not Allowed | 不支持对应的请求方法 |
501 Not Implemented | 服务器不能识别请求或者未实现指定的请求 |
Http 与 Https
参见 [CSDN] Http与Https>
using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Net; using System.Net.Sockets; using System.Threading; namespace SimpleWebServer { class Program { static void Main(string[] args) { WebServer webServer = new WebServer(); webServer.StartListener(8899, "127.0.0.1"); } } public class WebServer { //存放web内容的目录 public static string webRoot = @"F:\web"; public void StartListener(Int32 port, String ip) { TcpListener server = null; try { IPAddress localAddr = IPAddress.Parse(ip); server = new TcpListener(localAddr, port); server.Start(); Console.WriteLine("Server started"); HttpHeader header = new HttpHeader(); byte[] contentBytes; while (true) { Console.WriteLine("Waiting for a connection... "); Socket client = server.AcceptSocket(); Console.WriteLine("收到HTTP请求:"); //读取GET请求的头信息 Byte[] bReceive = new Byte[1024]; int i = client.Receive(bReceive, bReceive.Length, 0); string headInfo = Encoding.ASCII.GetString(bReceive); Console.WriteLine(headInfo); if (headInfo.Substring(0, 3) != "GET") { Console.WriteLine("无效请求,仅处理GET请求"); client.Close(); continue; } int iStartPos = headInfo.IndexOf("HTTP", 1); //获取HTTP版本号 string httpVersion = headInfo.Substring(iStartPos+5, 3); Console.WriteLine("版本号: " + httpVersion); //获取请求的文件 string requestFile = headInfo.Substring(4, iStartPos - 4); requestFile = requestFile.Trim(); Console.WriteLine("请求的文件: " + requestFile); requestFile = webRoot + requestFile; //如果请求的文件不存在,则向浏览器发送错误提示。 if (!File.Exists(requestFile)) { Console.WriteLine("请求的文件不存在!"); string errorMessage = "<H2>Error!! Requested file does not exists</H2><Br>"; contentBytes = Encoding.ASCII.GetBytes(errorMessage); header.SetStatusCode(httpVersion, 404, "Not Found"); header.SetContentLength(contentBytes.Length); SendToBrowser(header.GetBytes(), ref client); SendToBrowser(contentBytes, ref client); client.Close(); continue; } //读取服务器文件 StreamReader sr = File.OpenText(requestFile); string content = sr.ReadToEnd(); contentBytes = Encoding.UTF8.GetBytes(content); //设置HTTP头信息 header.SetStatusCode(httpVersion, 200, "OK"); header.SetContentLength(contentBytes.Length); //发送HTTP头信息及文件内容 SendToBrowser(header.GetBytes(), ref client); SendToBrowser(contentBytes, ref client); //关闭连接 client.Close(); } } catch (SocketException e) { Console.WriteLine("SocketException: {0}", e); } finally { server.Stop(); } Console.WriteLine("\nHit enter to continue..."); Console.Read(); } public void SendToBrowser(byte[] bSendData, ref Socket socket) { int numBytes = 0; try { if (socket.Connected) { if ((numBytes = socket.Send(bSendData, bSendData.Length, 0)) == -1) Console.WriteLine("Socket Error cannot Send Packet"); else { Console.WriteLine("No. of bytes send {0}", numBytes); } } else Console.WriteLine("连接失败...."); } catch (Exception e) { Console.WriteLine("发生错误 : {0} ", e); } } } #region HTTP头信息 public class HttpHeader { private string statusCode = "HTTP/1.1 200 OK\r\n"; private string server = "Server: Simple-WebServer/1.0\r\n"; private string date = "Date: Thu, 13 Jul 2016 05:46:53 GMT\r\n"; private string contentType = "Content-Type: text/html\r\n"; private string acceptRanges = "Accept-Ranges: bytes\r\n"; //特别注意: 头信息的最后一行有两个空行,否则浏览器无法正确显示内容。 private string contentLength = "Content-Length: 2291\r\n\r\n"; public void SetStatusCode(string version, int code, string msg) { statusCode = string.Format("HTTP/{0} {1} {2}\r\n", version, code, msg); } public void SetContentType(string type) { contentType = string.Format("Content-Type: {0}\r\n", type); } public void SetContentLength(int length) { contentLength = string.Format("Content-Length: {0}\r\n\r\n", length); } public string GetString() { date = string.Format("Date: {0:R}\r\n", DateTime.Now); string header = statusCode + server + date + acceptRanges + contentLength; return header; } public byte[] GetBytes() { return Encoding.ASCII.GetBytes(GetString()); } } #endregion }
浏览器访问测试
访问存在的网页
访问不存在的文件
Unity版本 (可放到Unity中执行)
using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using UnityEngine; /// <summary> /// Unity版本 /// 局域网访问需要关闭防火墙 /// </summary> namespace SimpleWebServer { public enum RequestType { GET, POST } public sealed class HttpContentType { public static readonly string TEXT_HTML = "text/html"; public static readonly string JSON = "application/json"; //json public static readonly string VIDEO_MPEG4 = "video/mpeg4";//mp4 } public interface IHttpResponse { void OnRequest(HttpHeader header); string ContentType { get; } byte[] OnResponseContent(); void OnDispose(); } [System.Serializable] public class ResponseMessage { public int request_code = 0; //请求码 public int error_code = -1; //错误码, -1代表没出错 public string error_msg = "";//错误描述 public string data = ""; public static byte[] Success(int request_code=0, string data="") { ResponseMessage msg = new ResponseMessage(); msg.request_code = request_code; msg.error_code = -1; msg.error_msg = string.Empty; msg.data = data; string json = JsonUtility.ToJson(msg); byte[] bytes = Encoding.UTF8.GetBytes(json); return bytes; } public static byte[] Failure(int request_code=0, int error_code=-1, string error_msg="") { ResponseMessage msg = new ResponseMessage(); msg.request_code = request_code; msg.error_code = error_code; msg.error_msg = error_msg; string json = JsonUtility.ToJson(msg); byte[] bytes = Encoding.UTF8.GetBytes(json); return bytes; } } public class WorkState { public Socket client; public IHttpResponse response; } public class WebServer { //存放web内容的目录 public static string WebRoot = @"D:\Dev\Temp\AVProMovieCapture\Web"; public static int Port = 80; public static int ReceiveBufferSize = 1024; //接收缓冲区大小 public static Type IHttpResponseType; public static bool DebugDetailLog = true; //true: 会打印更多日志,有助于调式. private static WebServer Instance; private Thread listenerThead; private TcpListener server = null; public static ActionStartupFailedEvent; public static void Start() { if (Instance == null) Instance = new WebServer(); Instance.StartServer(); } public static void Stop() { if (Instance != null) Instance.StopServer(); } private void StopServer() { if (server != null) server.Stop(); server = null; if (listenerThead == null || !listenerThead.IsAlive) return; listenerThead.Abort(); listenerThead = null; } private void StartServer() { if (PortInUse(Port)) { Debug.LogErrorFormat("Web server startup failed, Because port {0} occupied", Port); if (StartupFailedEvent != null) StartupFailedEvent(string.Format("Web服务启动失败,{0}端口被占用", Port)); return; } if (listenerThead != null && listenerThead.IsAlive) return; listenerThead = new Thread(StartListener); listenerThead.IsBackground = true; listenerThead.Start(); } private void StartListener() { string ip = GetLocalIP(); TcpListener server = null; try { IPAddress localAddr = IPAddress.Parse(ip); // 在指定端口监听Http请求 server = new TcpListener(localAddr, Port); server.Start(); Debug.Log("WebServer started"); Debug.LogFormat("Listen: ip={0}, port={1}", ip, Port); Debug.Log("Start listening to http requests..."); // 监听Http请求 server.BeginAcceptSocket(new AsyncCallback(OnBeginAcceptSocket), server); } catch (SocketException e) { Debug.LogFormat("SocketException: {0}", e); } finally { } } private void OnBeginAcceptSocket(IAsyncResult ar) { TcpListener tcp = (TcpListener)ar.AsyncState; Socket socket = tcp.EndAcceptSocket(ar); Debug.LogFormat("收到TCP连接: IP={0}", socket.RemoteEndPoint.ToString()); // 创建要传递给处理线程的参数 IHttpResponse response = null; if (IHttpResponseType != null) { object[] paramObject = new object[] { }; response = Activator.CreateInstance(IHttpResponseType, paramObject) as IHttpResponse; } WorkState state = new WorkState(); state.client = socket; state.response = response; // 从线程池启动一条线程处理请求 ThreadPool.QueueUserWorkItem(new WaitCallback(DoProcessRequest), state); tcp.BeginAcceptSocket(new AsyncCallback(OnBeginAcceptSocket), tcp); } // 求理Http请求 private void DoProcessRequest(object state) { if (DebugDetailLog) Debug.Log("[Http]: DoProcessRequest"); WorkState workState = state as WorkState; Socket client = workState.client; IHttpResponse response = workState.response; //=============== 解析收到的数据 ==============/ HttpHeader revHeader = new HttpHeader(); //读取请求数据 Byte[] bReceive = new Byte[ReceiveBufferSize]; client.ReceiveTimeout = 10; client.SendTimeout = 10; client.Blocking = true; int i = client.Receive(bReceive, 0, bReceive.Length, SocketFlags.None); string receive_str = Encoding.UTF8.GetString(bReceive); revHeader.Parse(receive_str); //Head信息与POST数据存在断包的情况,需要再次检查数据有没读完 if (revHeader.reqType == RequestType.POST) { if (string.IsNullOrEmpty(revHeader.revPostData)) { byte[] post_bytes = new byte[revHeader.revContentLength]; try { if (DebugDetailLog) Debug.Log("重新读取POST数据"); i = 0; while (i != revHeader.revContentLength) { //client.Available等于0时调用Receive()会引发SocketException if (client.Connected && client.Available > 0) i += client.Receive(post_bytes, i, post_bytes.Length, SocketFlags.None); Thread.Sleep(10); } if (i > 0) { string post_data = Encoding.UTF8.GetString(post_bytes); revHeader.revPostData = post_data; } } catch (SocketException ex) { Debug.Log(ex.Message); } } } //=============== 调式日志 ===================/ if (DebugDetailLog) { StringBuilder sb = new StringBuilder(); sb.AppendFormat("[Http] 请求的URL: {0}\n", revHeader.revUrl); sb.AppendFormat("[Http] 请求类型: {0}\n", revHeader.reqType.ToString()); sb.AppendFormat("[Http] 版本号: {0}\n", revHeader.revHttpVersion); if (revHeader.reqType == RequestType.POST) sb.AppendFormat("[Http] POST数据: {0}\n", revHeader.revPostData); Debug.Log(sb.ToString()); } //=============== 准备要返回的数据 =============/ byte[] rspBytes = null; string contentType = HttpContentType.JSON; if (response != null) { response.OnRequest(revHeader); rspBytes = response.OnResponseContent(); if (!string.IsNullOrEmpty(response.ContentType)) contentType = response.ContentType; response.OnDispose(); } else { string error_msg = "{\"error_msg\": \"no data\"}"; rspBytes = Encoding.ASCII.GetBytes(error_msg); } //=============== 向客户端返回数据 =============/ HttpHeader header = new HttpHeader(); // 设置HTTP头信息 header.SetStatusCode(revHeader.revHttpVersion, 200, "OK"); header.SetContentLength(rspBytes != null ? rspBytes.Length : 0); header.SetContentType(contentType); // 发送HTTP头信息及文件内容 SendToBrowser(header.GetBytes(), ref client); SendToBrowser(rspBytes, ref client); // 关闭连接 client.Close(); } private void SendToBrowser(byte[] bSendData, ref Socket socket) { int numBytes = 0; try { if (socket.Connected) { if ((numBytes = socket.Send(bSendData, bSendData.Length, 0)) == -1) Debug.Log("Socket Error cannot Send Packet"); else { Debug.LogFormat("No. of bytes send {0}", numBytes); } } else Debug.Log("连接失败...."); } catch (Exception e) { Console.WriteLine("发生错误 : {0} ", e); } } public static string GetLocalIP() { string IP = "127.0.0.1"; try { string HostName = Dns.GetHostName(); IPHostEntry IpEntry = Dns.GetHostEntry(HostName); for (int i=0; i<IpEntry.AddressList.Length; i++) { //AddressFamily.InterNetwork为IPv4 if (IpEntry.AddressList[i].AddressFamily == AddressFamily.InterNetwork) { IP = IpEntry.AddressList[i].ToString(); break; } } } catch (Exception ex) { Debug.Log(ex.Message); } return IP; } // 检查端口是否被占用 public static bool PortInUse(int port) { bool flag = false; try { //这个功能会加载Windows系统中的iphlpapi.dll //这个文件可能会因权限不足加载失败(比如在UWP平台下), //所以需要处理DllNotFoundException异常. IPGlobalProperties properties = IPGlobalProperties.GetIPGlobalProperties(); IPEndPoint[] ipendpoints = null; ipendpoints = properties.GetActiveTcpListeners(); foreach (IPEndPoint ipendpoint in ipendpoints) { if (ipendpoint.Port == port) { flag = true; break; } } if (!flag) { ipendpoints = properties.GetActiveUdpListeners(); foreach (IPEndPoint ipendpoint in ipendpoints) { if (ipendpoint.Port == port) { flag = true; break; } } } ipendpoints = null; properties = null; } catch (DllNotFoundException e) { if (Application.platform == RuntimePlatform.WindowsEditor) Debug.LogErrorFormat("Method PortInUse()\n{0}", e.Message); else Debug.LogFormat("Method PortInUse()\n{0}", e.Message); } return flag; } } #region HTTP头信息 public class HttpHeader { private string statusCode = "HTTP/1.1 200 OK\r\n"; private string server = "Server: Simple-WebServer/1.0\r\n"; private string date = "Date: Thu, 13 Jul 2016 05:46:53 GMT\r\n"; private string contentType = "Content-Type: text/html\r\n"; private string acceptRanges = "Accept-Ranges: bytes\r\n"; //特别注意: 头信息的最后一行有两个空行,否则浏览器无法正确显示内容。 private string contentLength = "Content-Length: 2291\r\n\r\n"; public RequestType reqType = RequestType.GET; //请求类型 public string revHttpVersion = "HTTP/1.1"; public string revUrl; //请求的完整url public string revUrlPage; //url中的面页 public Dictionary<string, string> revUrlParams; //url参数 public int revContentLength = 0; //内容长度 public string revPostData;//接收到的POST数据 public void SetStatusCode(string version, int code, string msg) { statusCode = string.Format("HTTP/{0} {1} {2}\r\n", version, code, msg); } public void SetContentType(string type) { contentType = string.Format("Content-Type: {0}\r\n", type); } public void SetContentLength(int length) { contentLength = string.Format("Content-Length: {0}\r\n\r\n", length); } public string GetString() { date = string.Format("Date: {0:R}\r\n", DateTime.Now); string header = statusCode + server + date + acceptRanges + contentLength; return header; } public byte[] GetBytes() { return Encoding.UTF8.GetBytes(GetString()); } public void Parse(string data) { int head_end_index = data.IndexOf("\r\n\r\n"); string head_info = data.Substring(0, head_end_index).Trim(); string post_info = data.Substring(head_end_index + 1).Trim(); if (!string.IsNullOrEmpty(post_info) && post_info[0] != '\0') { int end_index = post_info.IndexOf('\0'); if (end_index != -1) this.revPostData = post_info.Substring(0, end_index); else this.revPostData = post_info; } else { this.revPostData = string.Empty; } string[] head_rows = head_info.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); string row0 = head_rows[0]; this.reqType = row0.StartsWith("GET") ? RequestType.GET : RequestType.POST; this.revHttpVersion = row0.EndsWith("HTTP/1.1") ? "1.1" : "1.0"; this.revUrl = row0.Replace("GET", "").Replace("POST", "").Replace("HTTP/1.1", "").Replace("HTTP/1.0", "").Trim(); //根据需要解析其他字段 for (int i=1; i < head_rows.Length; i++) { string[] arr = head_rows[i].Split(':'); string key = arr[0].Trim(); string value = arr[1].Trim(); if ("Content-Length" == key) { revContentLength = int.Parse(value); break; } } //解析url if (!string.IsNullOrEmpty(this.revUrl)) { if (this.revUrl[0] == '/') { this.revUrl = this.revUrl.Substring(1); } //判断URL后面是否跟了参数 if (this.revUrl.StartsWith("?")) { //解析url参数 int index = this.revUrl.IndexOf("?"); this.revUrlPage = this.revUrl.Substring(0, index); string param_str = this.revUrl.Substring(index + 1); string[] param_arr = param_str.Split('&'); this.revUrlParams = new Dictionary (); if (param_arr != null && param_arr.Length > 0) { for (int i=0; i<param_arr.Length; i++) { string param = param_arr[i]; string[] kv = param.Split('='); if (kv != null && kv.Length == 2 && !this.revUrlParams.ContainsKey(kv[0])) { this.revUrlParams[kv[0]] = kv[1]; } } } } else { this.revUrlPage = this.revUrl; } } } public string GetUrlParam(string key, string defaultValue=null) { if (string.IsNullOrEmpty(key) || !revUrlParams.ContainsKey(key)) return defaultValue; return revUrlParams[key]; } } #endregion }
命令队列
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// 命令队列 /// </summary> public static class CommandQueue { private static Queue<ICommand> queue = new Queue<ICommand>(); public static void Enqueue(ICommand cmd) { lock (queue) { queue.Enqueue(cmd); } } public static ICommand Dequeue() { ICommand cmd = null; lock (queue) { cmd = queue.Dequeue(); } return cmd; } public static void Clear() { lock (queue) { queue.Clear(); } } public static void Execute() { if (Count <= 0) return; ICommand cmd = Dequeue(); if (cmd != null) cmd.Execute(); } public static int Count { get { int count = 0; lock (queue) { count = queue.Count; } return count; } } } // 命令接口 public interface ICommand { void Execute(); byte[] Response(); }
using System; using System.Collections; using System.Collections.Generic; using System.Text; using System.IO; using UnityEngine; using SimpleWebServer; /// <summary> /// 处理Http请求 /// </summary> public class HttpProcess : IHttpResponse { private string page; private string postData; private RequestDataType requestDataType; public void OnRequest(HttpHeader header) { Debug.Log("[HttpProcess] OnRequest()"); page = header.revUrlPage; postData = header.revPostData; if (page.EndsWith(".mp4")) requestDataType = RequestDataType.MP4; else if (!Enum.TryParse(page, true, out requestDataType)) requestDataType = RequestDataType.COMMAND; } public string ContentType { get { string contentType = (requestDataType == RequestDataType.MP4) ? HttpContentType.VIDEO_MPEG4 : HttpContentType.JSON; return contentType; } } public byte[] OnResponseContent() { Debug.Log("[HttpProcess] OnResponseContent()"); byte[] bytes = null; string json; switch (requestDataType) { //需要特殊处理的命令加case case RequestDataType.Test: //TestJsonObject obj = new TestJsonObject(); //obj.id = 18; //obj.name = "名字"; //json = JsonUtility.ToJson(obj); //bytes = Encoding.UTF8.GetBytes(json); ConnectTestCommand cmd_test = new ConnectTestCommand(string.Empty); CommandQueue.Enqueue(cmd_test); bytes = cmd_test.Response(); break; case RequestDataType.MP4: break; case RequestDataType.COMMAND: //调用指定命令,这里利用反射创建命令对象 Type type = Type.GetType(page + "Command"); if (type != null) { ICommand cmd = Activator.CreateInstance(type, postData) as ICommand; CommandQueue.Enqueue(cmd); bytes = cmd.Response(); } break; } return bytes; } public void OnDispose() { Debug.Log("[HttpProcess] OnDispose()"); } } // 定义请求的业务数据类型 public enum RequestDataType { Test, MP4, COMMAND } // 测试数据 [System.Serializable] public class TestJsonObject { public int id; public string name; }
using UnityEngine; /// <summary> /// 连接测试命令 /// </summary> public class ConnectTestCommand : ICommand { public ConnectTestCommand(string json) { Debug.Log("创建 ConnectTestCommand"); } public void Execute() { Debug.Log("执行 ConnectTestCommand"); } public byte[] Response() { return SimpleWebServer.ResponseMessage.Success(); } }
标签: C#
« 欢迎来到Android
|
悬浮图标»
日历
最新文章
随机文章
热门文章
分类
存档
- 2024年11月(3)
- 2024年10月(5)
- 2024年9月(3)
- 2024年8月(3)
- 2024年7月(11)
- 2024年6月(3)
- 2024年5月(9)
- 2024年4月(10)
- 2024年3月(11)
- 2024年2月(24)
- 2024年1月(12)
- 2023年12月(3)
- 2023年11月(9)
- 2023年10月(7)
- 2023年9月(2)
- 2023年8月(7)
- 2023年7月(9)
- 2023年6月(6)
- 2023年5月(7)
- 2023年4月(11)
- 2023年3月(6)
- 2023年2月(11)
- 2023年1月(8)
- 2022年12月(2)
- 2022年11月(4)
- 2022年10月(10)
- 2022年9月(2)
- 2022年8月(13)
- 2022年7月(7)
- 2022年6月(11)
- 2022年5月(18)
- 2022年4月(29)
- 2022年3月(5)
- 2022年2月(6)
- 2022年1月(8)
- 2021年12月(5)
- 2021年11月(3)
- 2021年10月(4)
- 2021年9月(9)
- 2021年8月(14)
- 2021年7月(8)
- 2021年6月(5)
- 2021年5月(2)
- 2021年4月(3)
- 2021年3月(7)
- 2021年2月(2)
- 2021年1月(8)
- 2020年12月(7)
- 2020年11月(2)
- 2020年10月(6)
- 2020年9月(9)
- 2020年8月(10)
- 2020年7月(9)
- 2020年6月(18)
- 2020年5月(4)
- 2020年4月(25)
- 2020年3月(38)
- 2020年1月(21)
- 2019年12月(13)
- 2019年11月(29)
- 2019年10月(44)
- 2019年9月(17)
- 2019年8月(18)
- 2019年7月(25)
- 2019年6月(25)
- 2019年5月(17)
- 2019年4月(10)
- 2019年3月(36)
- 2019年2月(35)
- 2019年1月(28)
- 2018年12月(30)
- 2018年11月(22)
- 2018年10月(4)
- 2018年9月(7)
- 2018年8月(13)
- 2018年7月(13)
- 2018年6月(6)
- 2018年5月(5)
- 2018年4月(13)
- 2018年3月(5)
- 2018年2月(3)
- 2018年1月(8)
- 2017年12月(35)
- 2017年11月(17)
- 2017年10月(16)
- 2017年9月(17)
- 2017年8月(20)
- 2017年7月(34)
- 2017年6月(17)
- 2017年5月(15)
- 2017年4月(32)
- 2017年3月(8)
- 2017年2月(2)
- 2017年1月(5)
- 2016年12月(14)
- 2016年11月(26)
- 2016年10月(12)
- 2016年9月(25)
- 2016年8月(32)
- 2016年7月(14)
- 2016年6月(21)
- 2016年5月(17)
- 2016年4月(13)
- 2016年3月(8)
- 2016年2月(8)
- 2016年1月(18)
- 2015年12月(13)
- 2015年11月(15)
- 2015年10月(12)
- 2015年9月(18)
- 2015年8月(21)
- 2015年7月(35)
- 2015年6月(13)
- 2015年5月(9)
- 2015年4月(4)
- 2015年3月(5)
- 2015年2月(4)
- 2015年1月(13)
- 2014年12月(7)
- 2014年11月(5)
- 2014年10月(4)
- 2014年9月(8)
- 2014年8月(16)
- 2014年7月(26)
- 2014年6月(22)
- 2014年5月(28)
- 2014年4月(15)
友情链接
- Unity官网
- Unity圣典
- Unity在线手册
- Unity中文手册(圣典)
- Unity官方中文论坛
- Unity游戏蛮牛用户文档
- Unity下载存档
- Unity引擎源码下载
- Unity服务
- Unity Ads
- wiki.unity3d
- Visual Studio Code官网
- SenseAR开发文档
- MSDN
- C# 参考
- C# 编程指南
- .NET Framework类库
- .NET 文档
- .NET 开发
- WPF官方文档
- uLua
- xLua
- SharpZipLib
- Protobuf-net
- Protobuf.js
- OpenSSL
- OPEN CASCADE
- JSON
- MessagePack
- C在线工具
- 游戏蛮牛
- GreenVPN
- 聚合数据
- 热云
- 融云
- 腾讯云
- 腾讯开放平台
- 腾讯游戏服务
- 腾讯游戏开发者平台
- 腾讯课堂
- 微信开放平台
- 腾讯实时音视频
- 腾讯即时通信IM
- 微信公众平台技术文档
- 白鹭引擎官网
- 白鹭引擎开放平台
- 白鹭引擎开发文档
- FairyGUI编辑器
- PureMVC-TypeScript
- 讯飞开放平台
- 亲加通讯云
- Cygwin
- Mono开发者联盟
- Scut游戏服务器引擎
- KBEngine游戏服务器引擎
- Photon游戏服务器引擎
- 码云
- SharpSvn
- 腾讯bugly
- 4399原创平台
- 开源中国
- Firebase
- Firebase-Admob-Unity
- google-services-unity
- Firebase SDK for Unity
- Google-Firebase-SDK
- AppsFlyer SDK
- android-repository
- CQASO
- Facebook开发者平台
- gradle下载
- GradleBuildTool下载
- Android Developers
- Google中国开发者
- AndroidDevTools
- Android社区
- Android开发工具
- Google Play Games Services
- Google商店
- Google APIs for Android
- 金钱豹VPN
- TouchSense SDK
- MakeHuman
- Online RSA Key Converter
- Windows UWP应用
- Visual Studio For Unity
- Open CASCADE Technology
- 慕课网
- 阿里云服务器ECS
- 在线免费文字转语音系统
- AI Studio
- 网云穿
- 百度网盘开放平台
- 迅捷画图
- 菜鸟工具
- [CSDN] 程序员研修院
- 华为人脸识别
- 百度AR导航导览SDK
- 海康威视官网
- 海康开放平台
- 海康SDK下载
- git download
交流QQ群
-
Flash游戏设计: 86184192
Unity游戏设计: 171855449
游戏设计订阅号