实现简单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 Action StartupFailedEvent;
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
|
悬浮图标»
日历
最新文章
随机文章
热门文章
分类
存档
- 2025年11月(1)
- 2025年9月(3)
- 2025年7月(4)
- 2025年6月(5)
- 2025年5月(1)
- 2025年4月(5)
- 2025年3月(4)
- 2025年2月(3)
- 2025年1月(1)
- 2024年12月(5)
- 2024年11月(5)
- 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
- Open CASCADE
- CascadeStudio
交流QQ群
-
Flash游戏设计: 86184192
Unity游戏设计: 171855449
游戏设计订阅号












